Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 84 additions & 23 deletions labscript_devices/AndorSolis/andor_sdk/andor_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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'):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}")
Expand Down Expand Up @@ -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 <self.emccd_gain_range[1]):
raise ValueError(
f"""Invalid emccd gain value, valid range is {self.emccd_gain_range}"""
)
Expand Down Expand Up @@ -282,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
Expand All @@ -297,15 +319,14 @@ def setup_acquisition(self, added_attributes=None):

if self.acquisition_attributes['preamp']:
self.enable_preamp(self.acquisition_attributes['preamp_gain'])

if self.acquisition_attributes['emccd']:
self.enable_emccd(self.acquisition_attributes['emccd_gain'])

#we need to do cooldown before emccd gain
if self.acquisition_attributes['cooldown']:
self.enable_cooldown(
self.acquisition_attributes['temperature'],
self.acquisition_attributes['water_cooling'],
wait_until_stable = True,
wait_until_stable = self.acquisition_attributes['wait_until_stable'],
wait_until_cool= self.acquisition_attributes['wait_until_cool'],
)

# Get current temperature and temperature status
Expand All @@ -317,6 +338,17 @@ def setup_acquisition(self, added_attributes=None):
color='magenta',
)

if self.acquisition_attributes['emccd']:
#set the emccd gain mode so the gain range is correct
print(f'setting em gain mode to {self.acquisition_attributes['em_gain_mode']}')
print(GetEMGainRange())
SetEMGainMode(self.acquisition_attributes['em_gain_mode'])
self.emccd_gain_range = GetEMGainRange()
print(GetEMGainRange())
self.enable_emccd(self.acquisition_attributes['emccd_gain'])



# Available modes
modes = {
'single': 1,
Expand Down Expand Up @@ -362,7 +394,13 @@ def setup_acquisition(self, added_attributes=None):
# Arm sensor
self.armed = True

self.keepClean_time = GetKeepCleanTime()
#on the ixon 855 in the caf lab this does not seem to work somehow
try:
self.keepClean_time = GetKeepCleanTime()
except AndorException:
print('could not get keepClean time, defaulting to 1s')
self.keepClean_time = 1

# Note: The GetReadOutTime call breaks in FK mode for unknown reasons
if 'fast_kinetics' not in self.acquisition_mode:
self.readout_time = GetReadOutTime()
Expand Down Expand Up @@ -516,26 +554,44 @@ def setup_readout(self, **attrs):
if self.acquisition_attributes['crop']:
SetOutputAmplifier(0)
SetFrameTransferMode(1)
SetIsolatedCropModeEx(
int(1),
if self.model[0]=='IXION ULTRA': #only impolemented with IXON ultra cams
SetIsolatedCropModeEx(#only impolemented with IXON ultra cams
int(1),
int(attrs['height']),
int(attrs['width']),
attrs['ybin'],
attrs['xbin'],
attrs['left_start'],
attrs['bottom_start'],
)
else:
SetIsolatedCropMode(
int(1),
int(attrs['height']),
int(attrs['width']),
attrs['ybin'],
attrs['xbin'],
)
else:
SetFrameTransferMode(0)
if self.model[0]=='IXON ULTRA':
SetIsolatedCropModeEx( #only impolemented with IXON ultra cams
int(0),
int(attrs['height']),
int(attrs['width']),
attrs['ybin'],
attrs['xbin'],
attrs['left_start'],
attrs['bottom_start'],
)
else:
SetFrameTransferMode(0)
SetIsolatedCropModeEx(
int(0),
int(attrs['height']),
int(attrs['width']),
attrs['ybin'],
attrs['xbin'],
attrs['left_start'],
attrs['bottom_start'],
)
else:
SetIsolatedCropMode(
int(0),
int(attrs['height']),
int(attrs['width']),
attrs['ybin'],
attrs['xbin'],
)
SetImage(
attrs['xbin'],
attrs['ybin'],
Expand All @@ -545,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 """
Expand All @@ -566,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} ",
Expand All @@ -583,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
Expand Down
15 changes: 11 additions & 4 deletions labscript_devices/AndorSolis/blacs_workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
from labscript_devices.IMAQdxCamera.blacs_workers import MockCamera, IMAQdxCameraWorker

class AndorCamera(object):
supportSemaphore = True

def __init__(self):
global AndorCam
from .andor_sdk.andor_utils import AndorCam
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)
Expand All @@ -43,15 +45,19 @@ 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 """
img = self.snap()
# 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.
Expand All @@ -71,11 +77,12 @@ 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")
images.append(downloaded)
images.append(downloaded[0])
self.camera.armed = True
self.camera.armed = False
print(f"Got {len(images)} of {n_images} acquisition(s).")
Expand Down
31 changes: 25 additions & 6 deletions labscript_devices/IMAQdxCamera/blacs_workers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down