-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve ASI detector support and reduce excessive logging #111
base: main
Are you sure you want to change the base?
Changes from 16 commits
f202a34
8ccdef1
8f0deed
1ef5a36
6e18b2c
d8cbb8e
ea83287
81a6ba6
5dab929
1c5febb
a6aa242
63afa09
31eca62
0d10902
dcea143
5d96d30
d0bc19b
3a811bf
089f905
d9db13e
0e784ba
d3a67fe
2df07b5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||
---|---|---|---|---|---|---|---|---|
|
@@ -2,13 +2,11 @@ | |||||||
|
||||||||
import atexit | ||||||||
import logging | ||||||||
from pathlib import Path | ||||||||
from typing import Tuple | ||||||||
|
||||||||
import numpy as np | ||||||||
from serval_toolkit.camera import Camera as ServalCamera | ||||||||
|
||||||||
from instamatic import config | ||||||||
from instamatic.camera.camera_base import CameraBase | ||||||||
|
||||||||
logger = logging.getLogger(__name__) | ||||||||
|
@@ -27,14 +25,22 @@ class CameraServal(CameraBase): | |||||||
def __init__(self, name='serval'): | ||||||||
"""Initialize camera module.""" | ||||||||
super().__init__(name) | ||||||||
|
||||||||
self.establish_connection() | ||||||||
|
||||||||
msg = f'Camera {self.get_name()} initialized' | ||||||||
logger.info(msg) | ||||||||
|
||||||||
self.exposure_cooldown = ( | ||||||||
self.detector_config['TriggerPeriod'] - self.detector_config['ExposureTime'] | ||||||||
) | ||||||||
logger.info(f'Camera {self.get_name()} initialized') | ||||||||
atexit.register(self.release_connection) | ||||||||
|
||||||||
def _validate_exposure(self, exposure: float) -> float: | ||||||||
if exposure < 0.001: | ||||||||
logger.warning(f'Exposure {exposure} too low. Adjusting to 0.001s') | ||||||||
exposure = 0.001 | ||||||||
elif exposure > 10: | ||||||||
logger.warning(f'Exposure {exposure} too high. Adjusting to 10s') | ||||||||
exposure = 10 | ||||||||
return exposure | ||||||||
|
||||||||
def get_image(self, exposure=None, binsize=None, **kwargs) -> np.ndarray: | ||||||||
"""Image acquisition routine. If the exposure and binsize are not | ||||||||
given, the default values are read from the config file. | ||||||||
|
@@ -46,12 +52,12 @@ def get_image(self, exposure=None, binsize=None, **kwargs) -> np.ndarray: | |||||||
""" | ||||||||
if exposure is None: | ||||||||
exposure = self.default_exposure | ||||||||
if not binsize: | ||||||||
binsize = self.default_binsize | ||||||||
exposure = self._validate_exposure(exposure) | ||||||||
|
||||||||
# Upload exposure settings (Note: will do nothing if no change in settings) | ||||||||
self.conn.set_detector_config( | ||||||||
ExposureTime=exposure, TriggerPeriod=exposure + 0.00050001 | ||||||||
ExposureTime=exposure, | ||||||||
TriggerPeriod=exposure + self.exposure_cooldown, | ||||||||
) | ||||||||
|
||||||||
# Check if measurement is running. If not: start | ||||||||
|
@@ -82,6 +88,7 @@ def get_movie(self, n_frames, exposure=None, binsize=None, **kwargs): | |||||||
exposure = self.default_exposure | ||||||||
if not binsize: | ||||||||
binsize = self.default_binsize | ||||||||
exposure = self._validate_exposure(exposure) | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment above about raising a ValueError instead.
Suggested change
|
||||||||
|
||||||||
self.conn.set_detector_config(TriggerMode='CONTINUOUS') | ||||||||
|
||||||||
|
@@ -111,21 +118,14 @@ def establish_connection(self) -> None: | |||||||
bpc_file_path=self.bpc_file_path, dacs_file_path=self.dacs_file_path | ||||||||
) | ||||||||
self.conn.set_detector_config(**self.detector_config) | ||||||||
# Check pixel depth. If 24 bit mode is used, the pgm format does not work | ||||||||
# (has support up to 16 bits) so use tiff in that case. In other cases (1, 6, 12 bits) | ||||||||
# use pgm since it is more efficient | ||||||||
self.pixel_depth = self.conn.detector_config['PixelDepth'] | ||||||||
if self.pixel_depth == 24: | ||||||||
file_format = 'tiff' | ||||||||
else: | ||||||||
file_format = 'pgm' | ||||||||
|
||||||||
self.conn.destination = { | ||||||||
'Image': [ | ||||||||
{ | ||||||||
# Where to place the preview files (HTTP end-point: GET localhost:8080/measurement/image) | ||||||||
'Base': 'http://localhost', | ||||||||
# What (image) format to provide the files in. | ||||||||
'Format': file_format, | ||||||||
'Format': 'tiff', | ||||||||
# What data to build a frame from | ||||||||
'Mode': 'count', | ||||||||
} | ||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,21 +1,36 @@ | ||
camera_rotation_vs_stage_xy: 0.0 | ||
default_binsize: 1 | ||
default_exposure: 0.02 | ||
|
||
# Double-check that your dimensions match the ones from your detector. | ||
dimensions: [512, 512] | ||
|
||
dynamic_range: 11800 | ||
interface: serval | ||
|
||
physical_pixelsize: 0.055 | ||
possible_binsizes: [1] | ||
|
||
stretch_amplitude: 0.0 | ||
stretch_azimuth: 0.0 | ||
|
||
# This configuration can be tuned based on your needs. | ||
# Please refer to the Serval manual. | ||
# For Serval 3.3.x, it is in section 4.5.7 - Detector Config JSON structure. | ||
detector_config: | ||
BiasVoltage: 100 | ||
BiasEnabled: True | ||
TriggerMode: SOFTWARESTART_TIMERSTOP | ||
ExposureTime: 1.0 | ||
TriggerPeriod: 1.001 | ||
TriggerPeriod: 1.002 # Try 1.0005 for Medipix3 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it is a nice idea to derive the cooldown/deadtime to avoid having an asic field. But since that might not be very transparent to the user, I think that it deserves a small comment here, something like:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Currently I definitely like the idea of placing additional comment in the yaml file so that a user can make a conscious choice about setting |
||
nTriggers: 1000000000 | ||
GainMode: HGM | ||
# Disregard the following lines if you are using CheeTah T3 variants (timepix3), | ||
# as they do not support these settings. | ||
GainMode: HGM # Only for Medipix3 | ||
PixelDepth: 24 # Set to 12 for 12-bit (normal) mode - only Medipix3 | ||
BothCounters: False # Set to True for 12-bit mode - only Medipix3 | ||
|
||
# Change this to the location of your actual factory settings | ||
bpc_file_path: '/home/asi/Desktop/Factory_settings/SPM-HGM/config.bpc' | ||
dacs_file_path: '/home/asi/Desktop/Factory_settings/SPM-HGM/config.dacs' | ||
url: 'http://localhost:8080' |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -117,6 +117,14 @@ def main(): | |
help='Show info about the current instamatic installation.', | ||
) | ||
|
||
parser.add_argument( | ||
'-v', | ||
'--verbose', | ||
action='store_true', | ||
dest='verbose', | ||
help="Show imported packages' DEBUG records in log", | ||
) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One more thing that I considered suggesting was disabling by default instamatic debug statement, allowing instamatic debug statement if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is some performance penalty to logging. This is why I tried to limit logging as much as possible in the performance critical loops (data collection), to avoid unnecessary locks or interruptions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, I think your opinion aligns with my suggestion. If Instamatic debug statements are logged only if For reference, according to simple search instamatic currently does not have particularly much logging, but there seems to be a healthy split between
I will have that implemented today and ask for a re-review afterwards. |
||
|
||
parser.set_defaults( | ||
script=None, | ||
acquire_at_items=False, | ||
|
@@ -155,11 +163,21 @@ def main(): | |
date = datetime.datetime.now().strftime('%Y-%m-%d') | ||
logfile = config.locations['logs'] / f'instamatic_{date}.log' | ||
|
||
logging.basicConfig( | ||
format='%(asctime)s | %(module)s:%(lineno)s | %(levelname)s | %(message)s', | ||
filename=logfile, | ||
level=logging.DEBUG, | ||
) | ||
def filter_out_imported_package_debug_records(r: logging.LogRecord) -> bool: | ||
return ( | ||
r.name.startswith('instamatic') | ||
or r.name == '__main__' | ||
or r.levelno > logging.DEBUG | ||
or options.verbose | ||
) | ||
|
||
log_main = logging.getLogger() | ||
log_main.setLevel(logging.DEBUG) | ||
log_format = '%(asctime)s | %(module)s:%(lineno)s | %(levelname)s | %(message)s' | ||
log_handler = logging.FileHandler(logfile) | ||
log_handler.setFormatter(logging.Formatter(log_format)) | ||
log_handler.addFilter(filter_out_imported_package_debug_records) | ||
log_main.addHandler(log_handler) | ||
|
||
logging.captureWarnings(True) | ||
log = logging.getLogger(__name__) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this call is not going to be too spammy. I'd say it's also fair to raise a ValueError here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While this would be a more elegant approach, unfortunately this error is something I am specifically trying to avoid. Serval toolkit already does raise a
RuntimeError
and specifically describes in the error message that exposure must be in this range; However, the moment any error is raised here (and this can be done completely by accident e.g. when changing exposure in preview), the GUI dies. I discovered the 0-second exposure because trying to get any images other than preview caused this. If you don't like this approach, I can try tomorrow to make a precise try-except statement here instead.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say let Serval handle the data validation, but then the error must be caught at a higher level. No need to do data validation twice. You want to avoid the function invisibly updating the exposure, that's why it is better to raise.
If it's related to the gui, perhaps the gui call to this function can be behind a try/except. I think it is mainly the lower limit that causes issues in practice. Perhaps the number input in the gui could have a lower limit (exposure must be > 0). I think this makes sense in general.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@stefsmeets The upper limit is also causing exceptions, and in places other than GUI. The other day I was testing flatfield algorithm and it requested 370-second exposure. I came to conclusion that instead of piling up try/except statements, I could actually implement exposures of = 0 seconds or > 10 seconds by summing images from
get_movie
. This will be accompanied with appropriate warning but IMO will be more general than arbitrary cut-off. I will convert this PR to draft while I am working on it.