-
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?
Conversation
…f setting `self.grabber.frametime = 0` to avoid failing 0-second frames
…need for 'asic' config Reading tiff is faster than pgm virtually always, especially if PixelDepth = 2**N (benchmark: https://chatgpt.com/canvas/shared/67b4942e54808191bf08685ce305112d): bit_depth pgm png tiff 1 117.41 ± 6.32 ms nan ± nan ms 0.17 ± 0.03 ms 4 117.24 ± 11.10 ms nan ± nan ms 0.17 ± 0.04 ms 6 151.23 ± 48.35 ms nan ± nan ms 0.16 ± 0.01 ms 8 0.08 ± 0.22 ms 1.41 ± 0.09 ms 0.20 ± 0.07 ms 10 153.95 ± 13.69 ms nan ± nan ms 0.63 ± 0.10 ms 12 153.15 ± 23.07 ms nan ± nan ms 0.46 ± 0.17 ms 14 150.50 ± 17.25 ms nan ± nan ms 0.68 ± 0.11 ms 16 1.84 ± 0.20 ms 2.10 ± 0.15 ms 0.42 ± 0.15 ms 20 nan ± nan ms nan ± nan ms 2.17 ± 0.22 ms 24 nan ± nan ms nan ± nan ms 2.21 ± 0.21 ms 32 nan ± nan ms nan ± nan ms 2.22 ± 0.19 ms
# Conflicts: # src/instamatic/camera/camera_serval.py
@hzanoli , @ErikHogenbirkASI Could you kindly confirm that both Medipix3 and Timepix3 allow for exposures greater than 0 and lower/equal than 10? This is what I found in both documentations. |
@ErikHogenbirkASI Do you remember how you determined that
|
@hzanoli This PR addresses the same issues as your fork. If I understand everything correctly, it makes the |
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.
Nice work, looks good to me. Glad that you are willing to debug the serval integration.
Let me know when this is ready to merge.
@@ -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) |
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.
exposure = self._validate_exposure(exposure) | |
if not (0.001 < exposure < 10): | |
raise ValueError('Exposure must be between 0.001 and 10 s') |
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.
@@ -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 comment
The reason will be displayed to describe this comment to others. Learn more.
See comment above about raising a ValueError instead.
exposure = self._validate_exposure(exposure) | |
if not (0.001 < exposure < 10): | |
raise ValueError('Exposure must be between 0.001 and 10 s') |
Yes. We enforce |
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 comment
The 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:
# TriggerPeriod and ExposureTime are used to derive the deadtime/cooldown of the detector.
# Please verify in the manual since different TriggerMode and other detector configurations
# require different deadtimes.
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.
Currently SOFTWARESTART_TIMERSTOP
is the only trigger mode that I managed to get working with Instamatic. I briefly tested AUTOTRIGSTART_TIMERSTOP
which massively improves the fps because it does not wait for a /measurement/start
, but it is not handled by Instamatic yet (sent data can not be opened by Pillow
/tifffile
) so I will need to take a closer look at it in the future. For the purpose of collecting "movies" (hypothetical/untested), Instamatic sets trigger mode to CONTINUOUS
, but also disregards "cooldown" and uses the same value for ExposureTime
and TriggerPeriod
so it will comply with Serval requirements.
I definitely like the idea of placing additional comment in the yaml file so that a user can make a conscious choice about setting ExposureTime
and TriggerPeriod
. I will base and possibly expand it on your suggestion. Thank you!
Observation regarding
|
Co-authored-by: Henrique Zanoli <henrique.zanoli@amscins.com>
Regarding the last point of controversy revolving around the exposure limits enforced by Serval, I read your suggestions @stefsmeets but I politely disagree. Allowing Instead of trying to prevent this 8-year-old who cannot swim from swimming, I taught him how to swim. After discussion with Erik Maddox, from ASI I split the
The existing implementation of I found that the existing implementation of
The new implementation also suffered from multiple instances of While debugging the new implementation, I had a (dis)pleasure to realize that adding print statements to the code running in second thread caused the GUI to freeze on exit. To be precise, it took me some hours to realize By the time I re-open this PR for review/merge, I will have edited the initial message to reflect these changes. |
src/instamatic/main.py
Outdated
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 comment
The 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 -v
, and allowing all statement if -vv
. To the best of my understanding, debug statement are not something that a log should be collecting in normal conditions. I do not want to cause a revolution though.
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.
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 comment
The 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 -v
is specified (and external debug statements only if -vv
) then it becomes fine to put debug statements even in "critical loops". Which in my opinion is the correct way to go, because with this change the "critical loops" remain fast if instamatic is run as normal, but can be easily investigated if -v
is specified.
For reference, according to simple search instamatic currently does not have particularly much logging, but there seems to be a healthy split between debug
that I suggest to omit by default and info
that I suggest to always show:
- 74 debug statements (including some in Merlin/Serval data collection loop, auto-cred, serialED, img conversion),
- 100 info statements (same files, plus more in microscope interfaces)
- 22 warn(ing) statements (joel microscope, camera merlin, comera serval, mrc format, simulation...)
- 2 error statements.
I will have that implemented today and ask for a re-review afterwards.
Fair enough, I don't want to get in the way of unnecessarily limiting the code. I'm happy with the solution you suggested. Let me know if the PR is ready and I will do a review. |
Alright, I modified my proposition as to how logging should be handled. With the current code changes:
This means that e.g. As a showcase of how this new system is adaptable, and because even with In my opinion the PR is ready to be merged, however I do not mind waiting a few days more for suggestions. Ultimately, since it by default removes instamatic debug messages from the log, I guess there might be feedback against it. |
The context
This week I worked on adapting Instamatic to our ASI Medipix3 detector via the
CameraServal
class. Despite variety of issues, many of them boiled to the same problems. Actions listed below led to GUI crashing:AD 1 & 2, I learned to my surprise that both ASI Medipix3 and Timepix3 impose hard limits on exposure: >0, ≤10 seconds.
I have therefore added a validate method toCameraServal
that trims any exposure passed to a [0.001, 10] range and logs a warning. If anything requests 20-second frame, it will receive a 10-second frame after approx. 10.2 seconds. It is neither pretty nor general but it gets the job done and fixes 99% of issues.EDIT: After reviewing and consulting this issue, instead of trimming the values to the limits, I have implemented a way to collect <0 and >10 second exposures. The former returns empty images, the latter sums shorter frames collected via a re-implemented
get_movie
. I also synchronizedCameraServal.get_image
andget_movie
and removed an issue with GUI freezing at exit. For more details, see this comment.AD 3, In #108, I addressed 0-second preview crashing the GUI. Since then I learned that even if exposure > 0, the GUI itself sometimes requests 0-second images. This happens because
VideoSteram.get_image
starts by settingself.grabber.frametime = 0
to "prevent it lagging data acquisition" and does some stuff, but beforeself.grabber.acquireInitiateEvent.set()
, theImageGraber.run
in 2nd thread races toself.cam.get_image
and often manages to call it – withframetime=0
. I found that replacingself.grabber.frametime
set/restore withself.block()
/unblock()
achieves the same effect while never requesting a dreaded 0-second image.AD 4, As for why the default config fails, the "cooldown" between exposures is hard-coded to 0.5ms. In fact, it should be higher if using Timepix3 asic or
"PixelDepth": 24
. On his fork, @hzanoli suggests using different hard-coded values based on a new config parameterasic
but I believe a more elegant solution is to derive it from the difference between initialExposureTime
andTriggerPeriod
.Furthermore. I noticed that for
"PixelDepth": 24
, the preview was much smoother than for"PixelDepth": 12
. After some tests I realized that in 12-bit mode, data was passed aspgm
and in 24-bit – astiff
(also enforced for Timepix3 asic by @hzanoli). I testedpgm
andtiff
and found out that readingtiff
with Pillow was significantly faster. So here I ask: why even usepgm
in the first place (benchmark code)?When debugging, I was trying to use instamatic log but it was extremely inconvenient because currently it captures every debug message from every package. Unrelevant debug messages, mostly from PIL, are generated in an absurd rate: ~2000 if running instamatic for 12 seconds, ~100 MB / hour. So here I suggest modifying GUI file handler so that:
logger.debug
messages are logged only if at least-v
is set;-vv
is set;-vvv
is set.Finally, during my tests I noticed that some of the links were outdated or wrong. I was also disappointed I could not copy-and-paste Instamatic citation. So I changed that: looks slightly different but CTRL+C works!
Major changes
CameraServal
: Derive the length of cooldown period between exposures from config (trigger - exposure) rather than using a hard-coded value;CameraServal
: Hard-limit exposure times between 0.001 and 10 seconds;CameraServal
: If requested exposure equal or lower than 0, return an empty array.CameraServal
: If requested exposure >10, return sum of images from re-implementedget_movie
.instamatic/main.py
(GUI): Limit log size from ~100 MB/h to KB/h by not-logging debug messages unless a new flag-v
(for insamatic) for-vv
(for imported libraries) is specified.Minor changes
VideoStream.get_image
: Useblock()
instead offrametime=0
to temporarily pause the preview.CameraServal
: Send images intiff
rather than significantly-slower-to-decodepgm
.CameraServal
: Re-implementget_movie
, synchronize it withget_image
to prevent errors.serval.yaml
: Add comments, update values to work for ASI Timepix3 (credit: @hzanoli);AboutFrame
: Fix links and make author & reference fields select-and-copy-able.Bugfixes
cred/experiment.py
: Allow using simulated FEI camera (credit: @hzanoli);camera_serval.py
: Remove unused import statements;AboutFrame
: Fix__main__
so that it can be shown stand-alone if ever desired;THANKS.md
: Fix the link so that it points to the correct contributors list;gui.py
: onclose()
, redirectsys.stdout
andsys.stderr
back so that it does not freeze GUI;tem_server.py
: make imports global to facilitate running viamain
/instamatic.temserver
.Effect on the codebase
Using ASI detectors in instamatic should now work and shouldn't randomly crash at every chance. I analyzed the code and successfully ran some RED (!), and I think replacing
framerate=0
withblock()
should be fine, but arguably I do now know if it does not affect something I did not touch. Likewise withpgm
: I don't understand why it was used in the first place, it is not faster. I do not believe anyone will miss debug logs from external libraries, but importantly now-v
is needed to see instamatic debug messages as well.