-
Notifications
You must be signed in to change notification settings - Fork 39
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
[TRCL-570][feat] introduce startScan Event to synchronize CCD with e-beam during SPARC acq #3027
base: master
Are you sure you want to change the base?
Changes from all commits
2b1dfed
64f46d2
5822427
0312f39
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 | ||||
---|---|---|---|---|---|---|
|
@@ -175,7 +175,7 @@ def __init__(self, name, streams): | |||||
self._current_scan_area = None # l,t,r,b (int) | ||||||
|
||||||
# Start threading event for live update overlay | ||||||
self._live_update_period = 2 | ||||||
self._live_update_period = 2 # s | ||||||
self._im_needs_recompute = threading.Event() | ||||||
self._init_thread(self._live_update_period) | ||||||
|
||||||
|
@@ -1083,7 +1083,7 @@ def __init__(self, name, streams): | |||||
self._sccd = s1 | ||||||
self._ccd = s1._detector | ||||||
self._ccd_df = s1._dataflow | ||||||
self._trigger = self._ccd.softwareTrigger | ||||||
self._trigger = self._emitter.startScan # to acquire a CCD image every time the SEM starts a new scan | ||||||
self._ccd_idx = len(self._streams) - 1 # optical detector is always last in streams | ||||||
|
||||||
def _supports_hw_sync(self): | ||||||
|
@@ -1653,11 +1653,6 @@ def _runAcquisitionEbeam(self, future): | |||||
# retrigger, or unsynchronise/resynchronise just before the end of | ||||||
# last scan). | ||||||
|
||||||
# prepare detector | ||||||
self._ccd_df.synchronizedOn(self._trigger) | ||||||
# subscribe to last entry in _subscribers (optical detector) | ||||||
self._ccd_df.subscribe(self._subscribers[self._ccd_idx]) | ||||||
|
||||||
# Instead of subscribing/unsubscribing to the SEM for each pixel, | ||||||
# we've tried to keep subscribed, but request to be unsynchronised/ | ||||||
# synchronised. However, synchronizing doesn't cancel the current | ||||||
|
@@ -1699,6 +1694,12 @@ def _runAcquisitionEbeam(self, future): | |||||
self._emitter.resolution.value, | ||||||
self._emitter.scale.value) | ||||||
|
||||||
# Prepare CCD: acquire one frame every time the SEM starts scanning. | ||||||
# The SEM may scan multiple times for each CCD frame. | ||||||
self._ccd_df.synchronizedOn(self._trigger) | ||||||
# Get the CCD ready to acquire | ||||||
self._ccd_df.subscribe(self._subscribers[self._ccd_idx]) | ||||||
|
||||||
last_ccd_update = 0 | ||||||
start_t = time.time() | ||||||
n = 0 # number of images acquired so far | ||||||
|
@@ -1887,26 +1888,10 @@ def _acquireImage(self, n, px_idx, img_time, sem_time, | |||||
raise CancelledError() | ||||||
|
||||||
# subscribe to _subscribers | ||||||
# As soon as the e-beam start scanning (which can take a couple of ms), the | ||||||
# startScan event is sent, which triggers the acquisition of one CCD frame. | ||||||
for s, sub in zip(self._streams[:-1], self._subscribers[:-1]): | ||||||
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. Can you comment on why the last element is excluded? |
||||||
s._dataflow.subscribe(sub) | ||||||
# TODO: in theory (aka in a perfect world), the ebeam would immediately | ||||||
# be at the requested position after the subscription starts. However, | ||||||
# that's not exactly the case due to: | ||||||
# * physics limits the speed of voltage change in the ebeam column, | ||||||
# so it takes the "settle time" before the beam is at the right | ||||||
# place (in the order of 10 µs). | ||||||
# * the (odemis) driver is asynchronous, and between the moment it | ||||||
# receives the request to start and the actual moment it asks the | ||||||
# hardware to change voltages, several ms might have passed. | ||||||
# One thing that would help is to not park the e-beam between each | ||||||
# spot. This way, the ebeam would reach the position much quicker, | ||||||
# and if it's not yet at the right place, it's still not that far. | ||||||
# In the meantime, waiting a tiny bit ensures the CCD receives the | ||||||
# right data. | ||||||
time.sleep(5e-3) # give more chances spot has been already processed | ||||||
|
||||||
# send event to detector to acquire one image | ||||||
self._trigger.notify() | ||||||
|
||||||
# wait for detector to acquire image | ||||||
timedout = self._waitForImage(img_time) | ||||||
|
@@ -1971,6 +1956,11 @@ def _acquireImage(self, n, px_idx, img_time, sem_time, | |||||
leech_nimg[li] -= 1 | ||||||
if leech_nimg[li] == 0: | ||||||
try: | ||||||
# Temporarily switch the CCD to a different event trigger, so that it | ||||||
# doesn't get triggered while the leech is running (because it could use the | ||||||
# e-beam, which would send a startScan event) | ||||||
self._ccd_df.synchronizedOn(self._ccd.softwareTrigger) | ||||||
|
||||||
nimg = l.next([d[-1] for d in self._acq_data]) | ||||||
logging.debug("Ran leech %s successfully. Will run next leech after %s acquisitions.", l, nimg) | ||||||
except Exception: | ||||||
|
@@ -1980,6 +1970,9 @@ def _acquireImage(self, n, px_idx, img_time, sem_time, | |||||
if self._acq_state == CANCELLED: | ||||||
raise CancelledError() | ||||||
|
||||||
# re-use the real trigger | ||||||
self._ccd_df.synchronizedOn(self._trigger) | ||||||
|
||||||
# Since we reached this point means everything went fine, so | ||||||
# no need to retry | ||||||
break | ||||||
|
@@ -2008,9 +2001,9 @@ def _runAcquisitionScanStage(self, future): | |||||
# The idea of the acquiring with a scan stage: | ||||||
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.
Suggested change
|
||||||
# (Note we expect the scan stage to be about at the center of its range) | ||||||
# * Move the ebeam to 0, 0 (center), for the best image quality | ||||||
# * Start CCD acquisition with software synchronisation | ||||||
# * Start CCD acquisition with synchronisation on e-beam startScan | ||||||
# * Move to next position with the stage and wait for it | ||||||
# * Start SED acquisition and trigger CCD | ||||||
# * Start SED acquisition -> startScan event triggers CCD | ||||||
# * Wait for the CCD/SED data | ||||||
# * Repeat until all the points have been scanned | ||||||
# * Move back the stage to center in case of an 'independent' stage | ||||||
|
@@ -2119,12 +2112,11 @@ def _runAcquisitionScanStage(self, future): | |||||
if self._acq_state == CANCELLED: | ||||||
raise CancelledError() | ||||||
|
||||||
# Start e-beam scan. As soon as it really start, a startScan event is sent, which | ||||||
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.
Suggested change
|
||||||
# triggers the CCD acquisition. | ||||||
for s, sub in zip(self._streams[:-1], self._subscribers[:-1]): | ||||||
s._dataflow.subscribe(sub) | ||||||
|
||||||
time.sleep(5e-3) # give more chances spot has been already processed | ||||||
self._trigger.notify() | ||||||
|
||||||
# wait for detector to acquire image | ||||||
timedout = self._waitForImage(px_time) | ||||||
|
||||||
|
@@ -2210,6 +2202,11 @@ def _runAcquisitionScanStage(self, future): | |||||
sstage.moveAbsSync(orig_spos) | ||||||
prev_spos.update(orig_spos) | ||||||
try: | ||||||
# Temporarily switch the CCD to a different event trigger, so that it | ||||||
# doesn't get triggered while the leech is running (because it could use the | ||||||
# e-beam, which would send a startScan event) | ||||||
self._ccd_df.synchronizedOn(self._ccd.softwareTrigger) | ||||||
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. Maybe an assert line can be used to check that the notification boolean value of startscan is False before reusing the real trigger? Same for _acquireImage |
||||||
|
||||||
np = l.next([d[-1] for d in self._acq_data]) | ||||||
except Exception: | ||||||
logging.exception("Leech %s failed, will retry next pixel", l) | ||||||
|
@@ -2218,6 +2215,9 @@ def _runAcquisitionScanStage(self, future): | |||||
if self._acq_state == CANCELLED: | ||||||
raise CancelledError() | ||||||
|
||||||
# re-use the real trigger | ||||||
self._ccd_df.synchronizedOn(self._trigger) | ||||||
|
||||||
for i, das in enumerate(self._acq_data): | ||||||
self._assembleLiveData(i, das[-1], px_idx, cor_pos, rep, 0) | ||||||
|
||||||
|
@@ -2513,6 +2513,7 @@ def _runAcquisition(self, future): | |||||
self._df0.synchronizedOn(self._trigger) | ||||||
for s, sub in zip(self._streams, self._subscribers): | ||||||
s._dataflow.subscribe(sub) | ||||||
|
||||||
start = time.time() | ||||||
self._acq_min_date = start | ||||||
self._trigger.notify() | ||||||
|
@@ -2529,8 +2530,8 @@ def _runAcquisition(self, future): | |||||
for i, s in enumerate(self._streams): | ||||||
timeout = max(0.1, max_end_t - time.time()) | ||||||
if not self._acq_complete[i].wait(timeout): | ||||||
raise TimeoutError("Acquisition of repetition stream for frame %s timed out after %g s" | ||||||
% (self._emitter.translation.value, time.time() - max_end_t)) | ||||||
raise TimeoutError("Acquisition of repetition stream at pos %s timed out after %g s" | ||||||
% (self._emitter.translation.value, time.time() - start)) | ||||||
if self._acq_state == CANCELLED: | ||||||
raise CancelledError() | ||||||
s._dataflow.unsubscribe(self._subscribers[i]) | ||||||
|
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.