From 96fc1b34bff84af975ce10c28902fb3a457a691b Mon Sep 17 00:00:00 2001 From: Tesse Tiemens Date: Fri, 20 Mar 2026 10:45:24 +0100 Subject: [PATCH 1/3] Bugfixes for older andor cameras and using cooldown and emccd gain --- .../AndorSolis/andor_sdk/andor_utils.py | 80 ++++++++++++++----- labscript_devices/AndorSolis/blacs_workers.py | 2 +- 2 files changed, 60 insertions(+), 22 deletions(-) diff --git a/labscript_devices/AndorSolis/andor_sdk/andor_utils.py b/labscript_devices/AndorSolis/andor_sdk/andor_utils.py index 4a0c5095..95e7ba76 100644 --- a/labscript_devices/AndorSolis/andor_sdk/andor_utils.py +++ b/labscript_devices/AndorSolis/andor_sdk/andor_utils.py @@ -14,6 +14,7 @@ class AndorCam(object): 'acquisition': 'single', 'emccd': False, 'emccd_gain': 50, + 'em_gain_mode': 0, 'preamp': False, 'preamp_gain': 1.0, 'exposure_time': 20 * ms, @@ -42,6 +43,8 @@ class AndorCam(object): 'cooldown': False, 'water_cooling': False, 'temperature': 20, + 'wait_until_cool': True, + 'wait_until_stable': True, } def __init__(self, name='andornymous'): @@ -128,7 +131,7 @@ def check_capabilities(self): rich_print(f" emgain_caps: {self.emgain_caps}", color='lightsteelblue') def enable_cooldown( - self, temperature_setpoint=20, water_cooling=False, wait_until_stable=False + self, temperature_setpoint=20, water_cooling=False, wait_until_stable=False, wait_until_cool=True, ): """ Calls all the functions relative to temperature control and stabilization. Enables cooling down, waits for stabilization @@ -162,12 +165,13 @@ def enable_cooldown( self.temperature, self.temperature_status = GetTemperatureF() # Wait until stable - if wait_until_stable: + if wait_until_cool: while 'TEMP_NOT_REACHED' in self.temperature_status: if self.chatty: print(f"Temperature not reached: T = {self.temperature}") time.sleep(thermal_timeout) self.temperature, self.temperature_status = GetTemperatureF() + if wait_until_stable: while 'TEMP_STABILIZED' not in self.temperature_status: if self.chatty: print(f"Temperature not stable: T = {self.temperature}") @@ -201,7 +205,7 @@ def enable_emccd(self, emccd_gain): """ Calls all the functions relative to the emccd gain control. """ - if not emccd_gain in self.emccd_gain_range: + if not (emccd_gain > self.emccd_gain_range[0] and emccd_gain Date: Fri, 20 Mar 2026 11:21:09 +0100 Subject: [PATCH 2/3] added SetupAcquisitionFast for true smart programming --- .../AndorSolis/andor_sdk/andor_utils.py | 18 ++++++++++++++++++ labscript_devices/AndorSolis/blacs_workers.py | 7 ++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/labscript_devices/AndorSolis/andor_sdk/andor_utils.py b/labscript_devices/AndorSolis/andor_sdk/andor_utils.py index 95e7ba76..5028c542 100644 --- a/labscript_devices/AndorSolis/andor_sdk/andor_utils.py +++ b/labscript_devices/AndorSolis/andor_sdk/andor_utils.py @@ -286,6 +286,24 @@ def setup_horizontal_shift(self, custom_option=None): # Get actual horizontal shifting (i.e. digitization) speed self.horizontal_shift_speed = GetHSSpeed(ad_number, 0, self.index_hs_speed) + def setup_acquisition_fast(self, added_attributes=None): + #should only be called if the normal setup_Acquisition has already been done + #I tried pairing this down to just the essentials that gets reset when reverting to manual + #although your mileage may vary + if added_attributes is None: + added_attributes = {} + + # Override default acquisition attrs with added ones + self.acquisition_attributes = self.default_acquisition_attrs.copy() + self.acquisition_attributes.update(added_attributes) + + # We setup trigger and shutter since they are likely to be set for manual mode + self.setup_trigger(**self.acquisition_attributes) + print(f'full attrs: {self.acquisition_attributes}') + self.setup_shutter(**self.acquisition_attributes) + # Arm sensor + self.armed = True + def setup_acquisition(self, added_attributes=None): """ Main acquisition configuration method. Available acquisition modes are below. The relevant methods are called with the corresponding acquisition diff --git a/labscript_devices/AndorSolis/blacs_workers.py b/labscript_devices/AndorSolis/blacs_workers.py index 988e90a5..d713ad3a 100644 --- a/labscript_devices/AndorSolis/blacs_workers.py +++ b/labscript_devices/AndorSolis/blacs_workers.py @@ -21,6 +21,7 @@ def __init__(self): self.camera = AndorCam() self.attributes = self.camera.default_acquisition_attrs self.exception_on_failed_shot = True + self.conf_attributes = {} def set_attributes(self, attr_dict): self.attributes.update(attr_dict) @@ -43,7 +44,11 @@ def snap(self): return images # This may be a 3D array of several images def configure_acquisition(self, continuous=False, bufferCount=None): - self.camera.setup_acquisition(self.attributes) + if self.attributes == self.conf_attributes: #this is still very crude but it should help a lot + self.camera.setup_acquisition_fast(self.attributes) + else: + self.camera.setup_acquisition(self.attributes) + self.conf_attributes = self.attributes.copy() #cache the latest configured attributes def grab(self): """ Grab last/single image """ From b82947b07717c4c666006c5e337f87b6c18fbfdd Mon Sep 17 00:00:00 2001 From: Tesse Tiemens Date: Fri, 20 Mar 2026 17:16:59 +0100 Subject: [PATCH 3/3] Setup acquisition start semaphores in a backwards cmpatible way --- .../AndorSolis/andor_sdk/andor_utils.py | 9 ++++-- labscript_devices/AndorSolis/blacs_workers.py | 6 ++-- .../IMAQdxCamera/blacs_workers.py | 31 +++++++++++++++---- 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/labscript_devices/AndorSolis/andor_sdk/andor_utils.py b/labscript_devices/AndorSolis/andor_sdk/andor_utils.py index 5028c542..d385b359 100644 --- a/labscript_devices/AndorSolis/andor_sdk/andor_utils.py +++ b/labscript_devices/AndorSolis/andor_sdk/andor_utils.py @@ -601,7 +601,7 @@ def setup_readout(self, **attrs): attrs['height'] + attrs['bottom_start'] - 1, ) - def acquire(self): + def acquire(self,semaphore=None): """ Carries down the acquisition, if the camera is armed and waits for an acquisition event for acquisition timeout (has to be in milliseconds), default to 5 seconds """ @@ -622,7 +622,7 @@ def homemade_wait_for_acquisition(): color='firebrick', ) break - time.sleep(0.05) + time.sleep(0.001) if self.chatty: rich_print( f"Leaving homemade_wait with status {self.acquisition_status} ", @@ -639,11 +639,16 @@ def homemade_wait_for_acquisition(): self.acquisition_status = GetStatus() if 'DRV_IDLE' in self.acquisition_status: StartAcquisition() + #print(f'StartAcquisition finished at time {time.time()}') + #this semaphore we passed around from the IMAQdx worker should now be released because the actual acquisition has started + if semaphore is not None: + semaphore.release() if self.chatty: rich_print( f"Waiting for {acquisition_timeout} ms for timeout ...", color='yellow', ) + homemade_wait_for_acquisition() # Last chance, check if the acquisition is finished, update diff --git a/labscript_devices/AndorSolis/blacs_workers.py b/labscript_devices/AndorSolis/blacs_workers.py index d713ad3a..d0aec4ec 100644 --- a/labscript_devices/AndorSolis/blacs_workers.py +++ b/labscript_devices/AndorSolis/blacs_workers.py @@ -14,6 +14,7 @@ from labscript_devices.IMAQdxCamera.blacs_workers import MockCamera, IMAQdxCameraWorker class AndorCamera(object): + supportSemaphore = True def __init__(self): global AndorCam @@ -56,7 +57,7 @@ def grab(self): # Consider using run til abort acquisition mode... return img - def grab_multiple(self, n_images, images, waitForNextBuffer=True): + def grab_multiple(self, n_images, images, acquisitionSemaphore=None, waitForNextBuffer=True): """Grab n_images into images array during buffered acquistion.""" # TODO: Catch timeout errors, check if abort, else keep trying. @@ -76,7 +77,8 @@ def grab_multiple(self, n_images, images, waitForNextBuffer=True): if 'single' in self.camera.acquisition_mode: for image_number in range(n_images): - self.camera.acquire() + self.camera.acquire(acquisitionSemaphore) + acquisitionSemaphore = None #once we've done one acquisition we don't need this anymore print(f" {image_number}: Acquire complete") downloaded = self.camera.download_acquisition() print(f" {image_number}: Download complete") diff --git a/labscript_devices/IMAQdxCamera/blacs_workers.py b/labscript_devices/IMAQdxCamera/blacs_workers.py index 2433fa19..61b2087c 100644 --- a/labscript_devices/IMAQdxCamera/blacs_workers.py +++ b/labscript_devices/IMAQdxCamera/blacs_workers.py @@ -260,6 +260,12 @@ class IMAQdxCameraWorker(Worker): def init(self): self.camera = self.get_camera() + # Check if the camera supports semaphores for acquisition start, and if so use them to avoid missing shots. + # If not, we just hope the camera is fast enough. + if hasattr(self.camera, 'supportSemaphore'): + self.supportSemaphore = self.camera.supportSemaphore + else: + self.supportSemaphore = False print("Setting attributes...") self.smart_cache = {} self.set_attributes_smart(self.camera_attributes) @@ -410,12 +416,25 @@ def transition_to_buffered(self, device_name, h5_filepath, initial_values, fresh print(f"Configuring camera for {self.n_images} images.") self.camera.configure_acquisition(continuous=False, bufferCount=self.n_images) self.images = [] - self.acquisition_thread = threading.Thread( - target=self.camera.grab_multiple, - args=(self.n_images, self.images), - daemon=True, - ) - self.acquisition_thread.start() + + #if the camera supports semaphores, we pass it, if not, we don't. + #this is done to remain backwards compatible + if self.supportSemaphore: + aquisitionSemaphore = threading.Semaphore(0) + self.acquisition_thread = threading.Thread( + target=self.camera.grab_multiple, + args=(self.n_images, self.images,aquisitionSemaphore), + daemon=True, + ) + self.acquisition_thread.start() + aquisitionSemaphore.acquire() + else: + self.acquisition_thread = threading.Thread( + target=self.camera.grab_multiple, + args=(self.n_images, self.images), + daemon=True, + ) + self.acquisition_thread.start() return {} def transition_to_manual(self):