def expose(self, t, name, frametype='frame', trigger_duration=None): """Request an exposure at the given time. A trigger will be produced by the parent trigger object, with duration trigger_duration, or if not specified, of self.trigger_duration. The frame should have a `name, and optionally a `frametype`, both strings. These determine where the image will be stored in the hdf5 file. `name` should be a description of the image being taken, such as "insitu_absorption" or "fluorescence" or similar. `frametype` is optional and is the type of frame being acquired, for imaging methods that involve multiple frames. For example an absorption image of atoms might have three frames: 'probe', 'atoms' and 'background'. For this one might call expose three times with the same name, but three different frametypes. """ # Backward compatibility with code that calls expose with name as the first # argument and t as the second argument: if isinstance(t, str) and isinstance(name, (int, float)): msg = """expose() takes `t` as the first argument and `name` as the second argument, but was called with a string as the first argument and a number as the second. Swapping arguments for compatibility, but you are advised to modify your code to the correct argument order.""" print(dedent(msg), file=sys.stderr) t, name = name, t if trigger_duration is None: trigger_duration = self.trigger_duration if trigger_duration is None: msg = """%s %s has not had an trigger_duration set as an instantiation argument, and none was specified for this exposure""" raise ValueError(dedent(msg) % (self.description, self.name)) if not trigger_duration > 0: msg = "trigger_duration must be > 0, not %s" % str(trigger_duration) raise ValueError(msg) self.trigger(t, trigger_duration) self.exposures.append((t, name, frametype, trigger_duration)) return trigger_duration
def serialise_function(function, *args, **kwargs): """Serialise a function based on its source code, and serialise the additional args and kwargs that it will be called with. Raise an exception if the function signature does not begin with (shot_context, t) or if the additional args and kwargs are incompatible with the rest of the function signature""" signature = inspect.signature(function) if not tuple(signature.parameters)[:2] == ('shot_context', 't'): msg = """function must be defined with (shot_context, t, ...) as its first two arguments""" raise ValueError(dedent(msg)) # This will raise an error if the arguments do not match the function's call # signature: _ = signature.bind(None, None, *args, **kwargs) # Enure it's a bona fide function and not some other callable: if not isinstance(function, FunctionType): msg = f"""callable of type {type(function)} is not a function. Only functions can be used, not other callables""" raise TypeError(dedent(msg)) # Serialise the function, args and kwargs: source = textwrap.dedent(inspect.getsource(function)) args = serialise(args) kwargs = serialise(kwargs) return function.__name__, source, args, kwargs
def __init__( self, config_path=default_config_path, required_params=None, defaults=None, ): if required_params is None: required_params = {} if defaults is None: defaults = {} defaults['labscript_suite'] = LABSCRIPT_SUITE_PROFILE if isinstance(config_path, list): self.config_path = config_path[0] else: self.config_path = config_path self.file_format = "" for section, options in required_params.items(): self.file_format += "[%s]\n" % section for option in options: self.file_format += "%s = <value>\n" % option # Load the config file configparser.ConfigParser.__init__(self, defaults=defaults, interpolation=EnvInterpolation()) # read all files in the config path if it is a list (self.config_path only # contains one string): self.read(config_path) # Rename experiment_name to apparatus_name and raise a DeprectionWarning experiment_name = self.get("DEFAULT", "experiment_name", fallback=None) if experiment_name: msg = """The experiment_name keyword has been renamed apparatus_name in labscript_utils 3.0, and will be removed in a future version. Please update your labconfig to use the apparatus_name keyword.""" warnings.warn(dedent(msg), FutureWarning) if self.get("DEFAULT", "apparatus_name", fallback=None): msg = """You have defined both experiment_name and apparatus_name in your labconfig. Please omit the deprecate experiment_name keyword.""" raise Exception(dedent(msg)) else: self.set("DEFAULT", "apparatus_name", experiment_name) try: for section, options in required_params.items(): for option in options: self.get(section, option) except configparser.NoOptionError: msg = f"""The experiment configuration file located at {config_path} does not have the required keys. Make sure the config file contains the following structure:\n{self.file_format}""" raise Exception(dedent(msg))
def _check_even_children(self, analogs, digitals): """Check that there are an even number of children of each type.""" errmsg = """{0} {1} must have an even number of {2}s in order to guarantee an even total number of samples, which is a limitation of the DAQmx library. Please add a dummy {2} device or remove one you're not using, so that there is an even number.""" if len(analogs) % 2: msg = errmsg.format(self.description, self.name, 'analog output') raise LabscriptError(dedent(msg)) if len(digitals) % 2: msg = errmsg.format(self.description, self.name, 'digital output') raise LabscriptError(dedent(msg))
def register_plot_class(identifier, cls): if not spinning_top: msg = """Warning: lyse.register_plot_class has no effect on scripts not run with the lyse GUI. """ sys.stderr.write(dedent(msg)) _plot_classes[identifier] = cls
def __init__(self, port=None, dtype='pyobj', pull_only=False, bind_address='tcp://0.0.0.0', timeout_interval=None, **kwargs): # There are ways to process args and exclude the keyword arguments we disallow # without having to include the whole function signature above, but they are # Python 3 only, so we avoid them for now. msg = """keyword argument {} not allowed - it will be set according to LabConfig. To make a custom ZMQServer, use zprocess.ZMQserver instead of labscript_utils.zprocess.ZMQServer""" # Error if these args are provided, since we provide them: for kwarg in ['shared_secret', 'allow_insecure']: if kwarg in kwargs: raise ValueError(dedent(msg.format(kwarg))) config = get_config() shared_secret = config['shared_secret'] allow_insecure = config['allow_insecure'] zprocess.ZMQServer.__init__(self, port=port, dtype=dtype, pull_only=pull_only, bind_address=bind_address, shared_secret=shared_secret, allow_insecure=allow_insecure, timeout_interval=timeout_interval, **kwargs)
def _decode_image_data(self, img): """Formats returned FlyCapture2 API image buffers. FlyCapture2 image buffers require significant formatting. This returns what one would expect from a camera. :obj:`configure_acquisition` must be called first to set image format parameters. Args: img (numpy.array): A 1-D array image buffer of uint8 values to format Returns: numpy.array: Formatted array based on :obj:`width`, :obj:`height`, and :obj:`pixelFormat`. """ pix_fmt = self.pixelFormat if pix_fmt.startswith('MONO'): if pix_fmt.endswith('8'): dtype = 'uint8' else: dtype = 'uint16' image = np.frombuffer(img, dtype=dtype).reshape(self.height, self.width) else: msg = """Only MONO image types currently supported. To add other image types, add conversion logic from returned uint8 data to desired format in _decode_image_data() method.""" raise ValueError(dedent(msg)) return image.copy()
def __getitem__(self, name): try: # Ensure the module's code has run (this does not re-import it if it is already in sys.modules) importlib.import_module('.' + name, __name__) except ImportError: msg = """No %s registered for a device named %s. Ensure that there is a file 'register_classes.py' with a call to labscript_devices.register_classes() for this device, with the device name passed to register_classes() matching the name of the device class. Fallback method of looking for and importing a module in labscript_devices with the same name as the device also failed. If using this method, check that the module exists, has the same name as the device class, and can be imported with no errors. Import error was:\n\n""" msg = dedent(msg) % (self.instancename, name) + traceback.format_exc() raise ImportError(msg) # Class definitions in that module have executed now, check to see if class is in our register: try: return self.registered_classes[name] except KeyError: # No? No such class is defined then, or maybe the user forgot to decorate it. raise ValueError( 'No class decorated as a %s found in module %s, ' % (self.instancename, __name__ + '.' + name) + 'Did you forget to decorate the class definition with @%s?' % (self.instancename))
def wait_monitor(self): try: # Read edge times from the counter input task, indiciating the times of the # pulses that occur at the start of the experiment and after every wait. If a # timeout occurs, pulse the timeout output to force a resume of the master # pseudoclock. Save the resulting self.logger.debug('Wait monitor thread starting') with self.kill_lock: self.logger.debug('Waiting for start of experiment') # Wait for the pulse indicating the start of the experiment: if self.incomplete_sample_detection: semiperiods = self.read_edges(1, timeout=None) else: semiperiods = self.read_edges(2, timeout=None) self.logger.debug('Experiment started, got edges:' + str(semiperiods)) # May have been one or two edges, depending on whether the device has # incomplete sample detection. We are only interested in the second one # anyway, it tells us how long the initial pulse was. Store the pulse width # for later, we will use it for making timeout pulses if necessary. Note # that the variable current_time is labscript time, so it will be reset # after each wait to the time of that wait plus pulse_width. current_time = pulse_width = semiperiods[-1] self.semiperiods.append(semiperiods[-1]) # Alright, we're now a short way into the experiment. for wait in self.wait_table: # How long until when the next wait should timeout? timeout = wait['time'] + wait['timeout'] - current_time timeout = max(timeout, 0) # ensure non-negative # Wait that long for the next pulse: self.logger.debug( 'Waiting for pulse indicating end of wait') semiperiods = self.read_edges(2, timeout) # Did the wait finish of its own accord, or time out? if semiperiods is None: # It timed out. Better trigger the clock to resume! msg = """Wait timed out; retriggering clock with {:.3e} s pulse ({} edge)""" msg = dedent(msg).format(pulse_width, self.timeout_trigger_type) self.logger.debug(msg) self.send_resume_trigger(pulse_width) # Wait for it to respond to that: self.logger.debug( 'Waiting for pulse indicating end of wait') semiperiods = self.read_edges(2, timeout=None) # Alright, now we're at the end of the wait. self.semiperiods.extend(semiperiods) self.logger.debug('Wait completed') current_time = wait['time'] + semiperiods[-1] # Inform any interested parties that a wait has completed: postdata = _ensure_str(wait['label']) self.wait_completed.post(self.h5_file, data=postdata) # Inform any interested parties that waits have all finished: self.logger.debug('All waits finished') self.all_waits_finished.post(self.h5_file) except Exception: self.logger.exception('Exception in wait monitor thread:') # Save the exception so it can be raised in transition_to_manual self.wait_monitor_thread_exception = sys.exc_info()
def delay_results_return(): global _delay_flag if not spinning_top: msg = """Warning: lyse.delay_results_return has no effect on scripts not run with the lyse GUI. """ sys.stderr.write(dedent(msg)) _delay_flag = True
def split_conn_AI(connection): """Return analog input number of a connection string such as 'ai1' as an integer, or raise ValueError if format is invalid""" try: return int(connection.split('ai', 1)[1]) except (ValueError, IndexError): msg = """Analog input connection string %s does not match format 'ai<N>' for integer N""" raise ValueError(dedent(msg) % str(connection))
def _check_wait_monitor_timeout_device_config(self): """Check that if we are the wait monitor acquisition device and another device is the wait monitor timeout device, that a) the other device is a DAQmx device and b) the other device has a start_order lower than us and a stop_order higher than us.""" if compiler.wait_monitor is None: return acquisition_device = compiler.wait_monitor.acquisition_device timeout_device = compiler.wait_monitor.timeout_device if acquisition_device is not self or timeout_device is None: return if timeout_device is self: return if not isinstance(timeout_device, NI_DAQmx): msg = """If using an NI DAQmx device as a wait monitor acquisition device, then the wait monitor timeout device must also be an NI DAQmx device, not {}.""" raise TypeError(dedent(msg).format(type(timeout_device))) timeout_start = timeout_device.start_order if timeout_start is None: timeout_start = 0 timeout_stop = timeout_device.stop_order if timeout_stop is None: timeout_stop = 0 self_start = self.start_order if self_start is None: self_start = 0 self_stop = self.stop_order if self_stop is None: self_stop = 0 if timeout_start >= self_start or timeout_stop <= self_stop: msg = """If using different DAQmx devices as the wait monitor acquisition and timeout devices, the timeout device must transition_to_buffered before the acquisition device, and transition_to_manual after it, in order to ensure the output port for timeout pulses is not in use (by the manual mode DO task) when the wait monitor subprocess attempts to use it. To achieve this, pass the start_order and stop_order keyword arguments to the devices in your connection table, ensuring that the timeout device has a lower start_order and a higher stop_order than the acquisition device. The default start_order and stop_order is zero, so if you are not otherwise controlling the order that devices are programmed, you can set start_order=-1, stop_order=1 on the timeout device only.""" raise RuntimeError(dedent(msg))
def __init__(self, com_port): global zaber try: import zaber.serial as zaber except ImportError: msg = """Could not import zaber.serial module. Please ensure it is installed. It is installable via pip with 'pip install zaber.serial'""" raise ImportError(dedent(msg)) self.port = zaber.BinarySerial(com_port)
def wrapper(*args, **kwargs): if not cls: cls.append(import_class_by_fullname(fullname)) shortname = fullname.split('.')[-1] newmodule = '.'.join(fullname.split('.')[:-1]) msg = """Importing %s from %s is deprecated, please instead import it from %s. Importing anyway for backward compatibility, but this may cause some unexpected behaviour.""" msg = dedent(msg) % (shortname, calling_module_name, newmodule) warnings.warn(msg, stacklevel=2) return cls[0](*args, **kwargs)
def init(self): """Initializes basic worker and opens VISA connection to device. Default connection timeout is 2 seconds""" self.VISA_name = self.address self.resourceMan = visa.ResourceManager() try: self.connection = self.resourceMan.open_resource(self.VISA_name) except visa.VisaIOError: msg = '''{:s} not found! Is it connected?'''.format(self.VISA_name) raise LabscriptError(dedent(msg)) from None self.connection.timeout = 2000
def get_device_number(connection_str): """Return the integer device number from the connection string or raise ValueError if the connection string is not in the format "device <n>" with positive n.""" try: prefix, num = connection_str.split(' ') num = int(num) if prefix != 'device' or num <= 0: raise ValueError except (TypeError, ValueError): msg = f"""Connection string '{connection_str}' not in required format 'device <n>' with n > 0""" raise ValueError(dedent(msg)) from None return num
def check_version(self): """Check the version of PyDAQmx is high enough to avoid a known bug""" major = uInt32() minor = uInt32() patch = uInt32() DAQmxGetSysNIDAQMajorVersion(major) DAQmxGetSysNIDAQMinorVersion(minor) DAQmxGetSysNIDAQUpdateVersion(patch) if major.value == 14 and minor.value < 2: msg = """There is a known bug with buffered shots using NI DAQmx v14.0.0. This bug does not exist on v14.2.0. You are currently using v%d.%d.%d. Please ensure you upgrade to v14.2.0 or higher.""" raise Exception(dedent(msg) % (major.value, minor.value, patch.value))
def transition_to_manual(self, abort=False): self.logger.debug('transition_to_manual') # If we were doing buffered mode acquisition, stop the buffered mode task and # start the manual mode task. We might not have been doing buffered mode # acquisition if abort() was called when we are not in buffered mode, or if # there were no acuisitions this shot. if not self.buffered_mode: return True if self.buffered_chans is not None: self.stop_task() self.buffered_mode = False self.logger.info('transitioning to manual mode, task stopped') self.start_task(self.manual_mode_chans, self.manual_mode_rate) if abort: self.acquired_data = None self.buffered_chans = None self.h5_file = None self.buffered_rate = None return True with h5py.File(self.h5_file, 'a') as hdf5_file: data_group = hdf5_file['data'] data_group.create_group(self.device_name) waits_in_use = len(hdf5_file['waits']) > 0 if self.buffered_chans is not None and not self.acquired_data: msg = """No data was acquired. Perhaps the acquisition task was not triggered to start, is the device connected to a pseudoclock?""" raise RuntimeError(dedent(msg)) # Concatenate our chunks of acquired data and recast them as a structured # array with channel names: if self.acquired_data: start_time = time.time() dtypes = [(chan, np.float32) for chan in self.buffered_chans] raw_data = np.concatenate(self.acquired_data).view(dtypes) raw_data = raw_data.reshape((len(raw_data), )) self.acquired_data = None self.buffered_chans = None self.extract_measurements(raw_data, waits_in_use) self.h5_file = None self.buffered_rate = None msg = 'data written, time taken: %ss' % str(time.time() - start_time) else: msg = 'No acquisitions in this shot.' self.logger.info(msg) return True
def _check_AI_not_too_fast(self, AI_table): if AI_table is None: return n = len(set(AI_table['connection'])) if n < 2: # Either no AI in use, or already checked against single channel rate in # __init__. return if self.acquisition_rate <= self.max_AI_multi_chan_rate / n: return msg = """Requested acqusition_rate %f for device %s with %d analog input channels in use is too fast. Device supports a rate of %f per channel when multiple channels are in use.""" msg = msg % (self.acquisition_rate, self.name, n, self.max_AI_multi_chan_rate) raise ValueError(dedent(msg))
def set_image_mode(self, image_settings): """Configures ROI and image control via Format 7, Mode 0 interface. Args: image_settings (dict): dictionary of image settings. Allowed keys: * 'pixelFormat': valid pixel format string, i.e. 'MONO8' * 'offsetX': int * 'offsetY': int * 'width': int * 'height': int """ image_info, supported = self.camera.getFormat7Info(0) Hstep = image_info.offsetHStepSize Vstep = image_info.offsetVStepSize image_dict = image_settings.copy() if supported: image_mode, packetSize, percentage = self.camera.getFormat7Configuration( ) image_mode.mode = 0 # validate and set the ROI settings # this rounds the ROI settings to nearest allowed pixel if 'offsetX' in image_dict: image_dict['offsetX'] -= image_dict['offsetX'] % Hstep if 'offsetY' in image_dict: image_dict['offsetY'] -= image_dict['offsetY'] % Vstep if 'width' in image_dict: image_dict['width'] -= image_dict['width'] % Hstep if 'height' in image_dict: image_dict['height'] -= image_dict['height'] % Vstep # need to set pixel format separately to get correct enum value if 'pixelFormat' in image_dict: fmt = image_dict.pop('pixelFormat') image_mode.pixelFormat = self.pixel_formats[fmt].value for k, v in image_dict.items(): setattr(image_mode, k, v) self._send_format7_config(image_mode) else: msg = """Camera does not support Format7, Mode 0 custom image configuration. This driver is therefore not compatible, as written.""" raise RuntimeError(dedent(msg))
def _decode_image_data(self, img): """Spinnaker image buffers require significant formatting. This returns what one would expect from a camera. configure_acquisition must be called first to set image format parameters.""" if self.pix_fmt.startswith('Mono'): if self.pix_fmt.endswith('8'): dtype = 'uint8' else: dtype = 'uint16' image = np.frombuffer(img, dtype=dtype).reshape(self.height, self.width) else: msg = """Only MONO image types currently supported. To add other image types, add conversion logic from returned uint8 data to desired format in _decode_image_data() method.""" raise ValueError(dedent(msg)) return image.copy()
def split_conn_DO(connection): """Return the port and line number of a connection string such as 'port0/line1 as two integers, or raise ValueError if format is invalid. Accepts connection strings such as port1/line0 (PFI0) - the PFI bit is just ignored""" try: if len(connection.split()) == 2: # Just raise a ValueError if the second bit isn't of the form ('PFI<n>') connection, PFI_bit = connection.split() if not (PFI_bit.startswith('(') and PFI_bit.endswith(')')): raise ValueError split_conn_PFI(PFI_bit[1:-1]) port, line = [int(n) for n in connection.split('port', 1)[1].split('/line')] except (ValueError, IndexError): msg = """Digital output connection string %s does not match format 'port<N>/line<M>' for integers N, M""" raise ValueError(dedent(msg) % str(connection)) return port, line
def reformat_files(filepaths): """Apply black formatter to a list of source files""" try: import black except ImportError: msg = """Cannot import code formatting library 'black'. Generated labscript device code may be poorly formatted. Install black (Python 3.6+ only) via pip and run again to produce better formatted files""" warnings.warn(dedent(msg)) return from click.testing import CliRunner runner = CliRunner() result = runner.invoke(black.main, ["-S"] + filepaths) print(result.output) assert result.exit_code == 0, result.output
def set_image_mode(self, image_settings): """Configures ROI and image control via Format 7, Mode 0 interface.""" image_info, supported = self.camera.getFormat7Info(0) Hstep = image_info.offsetHStepSize Vstep = image_info.offsetVStepSize image_dict = image_settings.copy() if supported: image_mode, packetSize, percentage = self.camera.getFormat7Configuration( ) image_mode.mode = 0 # validate and set the ROI settings # this rounds the ROI settings to nearest allowed pixel if 'offsetX' in image_dict: image_dict['offsetX'] -= image_dict['offsetX'] % Hstep if 'offsetY' in image_dict: image_dict['offsetY'] -= image_dict['offsetY'] % Vstep if 'width' in image_dict: image_dict['width'] -= image_dict['width'] % Hstep if 'height' in image_dict: image_dict['height'] -= image_dict['height'] % Vstep # need to set pixel format separately to get correct enum value if 'pixelFormat' in image_dict: fmt = image_dict.pop('pixelFormat') image_mode.pixelFormat = self.pixel_formats[fmt].value for k, v in image_dict.items(): setattr(image_mode, k, v) try: fmt7PktInfo, valid = self.camera.validateFormat7Settings( image_mode) if valid: self.camera.setFormat7ConfigurationPacket( fmt7PktInfo.recommendedBytesPerPacket, image_mode) except PyCapture2.Fc2error as e: raise RuntimeError('Error configuring image settings') from e else: msg = """Camera does not support Format7, Mode 0 custom image configuration. This driver is therefore not compatible, as written.""" raise RuntimeError(dedent(msg))
def _make_analog_input_table(self, inputs): """Collect analog input instructions and create the acquisition table""" if not inputs: return None acquisitions = [] for connection, input in inputs.items(): for acq in input.acquisitions: acquisitions.append(( connection, acq['label'], acq['start_time'], acq['end_time'], acq['wait_label'], acq['scale_factor'], acq['units'], )) if acquisitions and compiler.wait_table and compiler.wait_monitor is None: msg = """Cannot do analog input on an NI DAQmx device in an experiment that uses waits without a wait monitor. This is because input data cannot be 'chunked' into requested segments without knowledge of the durations of the waits. See labscript.WaitMonitor for details.""" raise LabscriptError(dedent(msg)) # The 'a256' dtype below limits the string fields to 256 # characters. Can't imagine this would be an issue, but to not # specify the string length (using dtype=str) causes the strings # to all come out empty. acquisitions_table_dtypes = [ ('connection', 'a256'), ('label', 'a256'), ('start', float), ('stop', float), ('wait label', 'a256'), ('scale factor', float), ('units', 'a256'), ] acquisition_table = np.empty(len(acquisitions), dtype=acquisitions_table_dtypes) for i, acq in enumerate(acquisitions): acquisition_table[i] = acq return acquisition_table
def register_classes(labscript_device_name, BLACS_tab=None, runviewer_parser=None): """Register the name of the BLACS tab and/or runviewer parser that belong to a particular labscript device. labscript_device_name should be a string of just the device name, i.e. "DeviceName". BLACS_tab_fullname and runviewer_parser_fullname should be strings containing the fully qualified import paths for the BLACS tab and runviewer parser classes, such as "labscript_devices.DeviceName.DeviceTab" and "labscript_devices.DeviceName.DeviceParser". These need not be in the same module as the device class as in this example, but should be within labscript_devices. This function should be called from a file called "register_classes.py" within a subfolder of labscript_devices. When BLACS or runviewer start up, they will call populate_registry(), which will find and run all such files to populate the class registries prior to looking up the classes they need""" if labscript_device_name in _register_classes_script_files: other_script =_register_classes_script_files[labscript_device_name] msg = """A device named %s has already been registered by the script %s. Labscript devices must have unique names.""" raise ValueError(dedent(msg) % (labscript_device_name, other_script)) BLACS_tab_registry[labscript_device_name] = BLACS_tab runviewer_parser_registry[labscript_device_name] = runviewer_parser script_filename = os.path.abspath(inspect.stack()[1][0].f_code.co_filename) _register_classes_script_files[labscript_device_name] = script_filename
def _check_bounds(self, analogs): """Check that all analog outputs are in bounds""" if not analogs: return vmin, vmax = self.AO_range # Floating point rounding error can produce values that would mathematically be # within bounds, but have ended up numerically out of bounds. We allow # out-of-bounds values within a small threshold through, but apply clipping to # keep them numerically within bounds. 1e-10 of the total range corresponds to > # 32 bits of precision, so this is not changing the voltages at all since none # of the DACs are that precise. eps = abs(vmax - vmin) * 1e-10 for output in analogs.values(): if any((output.raw_output < vmin - eps) | (output.raw_output > vmax + eps)): msg = """%s %s can only have values between %e and %e Volts, the limit imposed by %s.""" msg = msg % (output.description, output.name, vmin, vmax, self.name) raise LabscriptError(dedent(msg)) np.clip(output.raw_output, vmin, vmax, out=output.raw_output)
def trigger(self, t, duration): """Request parent trigger device to produce a trigger at time t with given duration.""" # Only ask for a trigger if one has not already been requested by another device # attached to the same trigger: already_requested = False for other_device in self.trigger_device.child_devices: if other_device is not self: for other_t, other_duration in other_device.__triggers: if t == other_t and duration == other_duration: already_requested = True if not already_requested: self.trigger_device.trigger(t, duration) # Check for triggers too close together (check for overlapping triggers already # performed in Trigger.trigger()): start = t end = t + duration for other_t, other_duration in self.__triggers: other_start = other_t other_end = other_t + other_duration if (abs(other_start - end) < self.minimum_recovery_time or abs(other_end - start) < self.minimum_recovery_time): msg = """%s %s has two triggers closer together than the minimum recovery time: one at t = %fs for %fs, and another at t = %fs for %fs. The minimum recovery time is %fs.""" msg = msg % ( self.description, self.name, t, duration, start, duration, self.minimum_recovery_time, ) raise ValueError(dedent(msg)) self.__triggers.append([t, duration])
def __init__(self, serial_number): """Initialize FlyCapture2 API camera. Searches all cameras reachable by the host using the provided serial number. Fails with API error if camera not found. This function also does a significant amount of default configuration. * It defaults the grab timeout to 1 s * Ensures use of the API's HighPerformanceRetrieveBuffer * Ensures the camera is in Format 7, Mode 0 with full frame readout and MONO8 pixels * If using a GigE camera, automatically maximizes the packet size and warns if Jumbo packets are not enabled on the NIC Args: serial_number (int): serial number of camera to connect to """ global PyCapture2 import PyCapture2 ver = PyCapture2.getLibraryVersion() min_ver = (2, 12, 3, 31) # first release with python 3.6 support if ver < min_ver: raise RuntimeError( f"PyCapture2 version {ver} must be >= {min_ver}") print('Connecting to SN:%d ...' % serial_number) bus = PyCapture2.BusManager() self.camera = PyCapture2.Camera() self.camera.connect(bus.getCameraFromSerialNumber(serial_number)) # set which values of properties to return self.get_props = [ 'present', 'absControl', 'absValue', 'onOff', 'autoManualMode', 'valueA', 'valueB' ] fmts = { prop: getattr(PyCapture2.PIXEL_FORMAT, prop) for prop in dir(PyCapture2.PIXEL_FORMAT) if not prop.startswith('_') } self.pixel_formats = IntEnum('pixel_formats', fmts) self._abort_acquisition = False # check if GigE camera. If so, ensure max packet size is used cam_info = self.camera.getCameraInfo() if cam_info.interfaceType == PyCapture2.INTERFACE_TYPE.GIGE: # need to close generic camera first to avoid strange interactions print('Checking Packet size for GigE Camera...') self.camera.disconnect() gige_camera = PyCapture2.GigECamera() gige_camera.connect(bus.getCameraFromSerialNumber(serial_number)) mtu = gige_camera.discoverGigEPacketSize() if mtu <= 1500: msg = """WARNING: Maximum Transmission Unit (MTU) for ethernet NIC FlyCapture2_Camera SN:%d is connected to is only %d. Reliable operation not expected. Please enable Jumbo frames on NIC.""" print(dedent(msg % (serial_number, mtu))) gige_pkt_size = gige_camera.getGigEProperty( PyCapture2.GIGE_PROPERTY_TYPE.GIGE_PACKET_SIZE) # only set if not already at correct value if gige_pkt_size.value != mtu: gige_pkt_size.value = mtu gige_camera.setGigEProperty(gige_pkt_size) print(' Packet size set to %d' % mtu) else: print(' GigE Packet size is %d' % gige_pkt_size.value) # close GigE handle to camera, re-open standard handle gige_camera.disconnect() self.camera.connect(bus.getCameraFromSerialNumber(serial_number)) # set standard device configuration config = self.camera.getConfiguration() config.grabTimeout = 1000 # in ms config.highPerformanceRetrieveBuffer = True self.camera.setConfiguration(config) # ensure camera is in Format7,Mode 0 custom image mode fmt7_info, supported = self.camera.getFormat7Info(0) if supported: # to ensure Format7, must set custom image settings # defaults to full sensor size and 'MONO8' pixel format print('Initializing to default Format7, Mode 0 configuration...') fmt7_default = PyCapture2.Format7ImageSettings( 0, 0, 0, fmt7_info.maxWidth, fmt7_info.maxHeight, self.pixel_formats['MONO8'].value) self._send_format7_config(fmt7_default) else: msg = """Camera does not support Format7, Mode 0 custom image configuration. This driver is therefore not compatible, as written.""" raise RuntimeError(dedent(msg))
def transition_to_manual(self): if self.h5_filepath is None: print('No camera exposures in this shot.\n') return True assert self.acquisition_thread is not None self.acquisition_thread.join(timeout=self.stop_acquisition_timeout) if self.acquisition_thread.is_alive(): msg = """Acquisition thread did not finish. Likely did not acquire expected number of images. Check triggering is connected/configured correctly""" if self.exception_on_failed_shot: self.abort() raise RuntimeError(dedent(msg)) else: self.camera.abort_acquisition() self.acquisition_thread.join() print(dedent(msg), file=sys.stderr) self.acquisition_thread = None print("Stopping acquisition.") self.camera.stop_acquisition() print(f"Saving {len(self.images)}/{len(self.exposures)} images.") with h5py.File(self.h5_filepath, 'r+') as f: # Use orientation for image path, device_name if orientation unspecified if self.orientation is not None: image_path = 'images/' + self.orientation else: image_path = 'images/' + self.device_name image_group = f.require_group(image_path) image_group.attrs['camera'] = self.device_name # Save camera attributes to the HDF5 file: if self.attributes_to_save is not None: set_attributes(image_group, self.attributes_to_save) # Whether we failed to get all the expected exposures: image_group.attrs['failed_shot'] = len(self.images) != len( self.exposures) # key the images by name and frametype. Allow for the case of there being # multiple images with the same name and frametype. In this case we will # save an array of images in a single dataset. images = {(exposure['name'], exposure['frametype']): [] for exposure in self.exposures} # Iterate over expected exposures, sorted by acquisition time, to match them # up with the acquired images: self.exposures.sort(order='t') for image, exposure in zip(self.images, self.exposures): images[(exposure['name'], exposure['frametype'])].append(image) # Save images to the HDF5 file: for (name, frametype), imagelist in images.items(): data = imagelist[0] if len(imagelist) == 1 else np.array( imagelist) print(f"Saving frame(s) {name}/{frametype}.") group = image_group.require_group(name) dset = group.create_dataset(frametype, data=data, dtype='uint16', compression='gzip') # Specify this dataset should be viewed as an image dset.attrs['CLASS'] = np.string_('IMAGE') dset.attrs['IMAGE_VERSION'] = np.string_('1.2') dset.attrs['IMAGE_SUBCLASS'] = np.string_('IMAGE_GRAYSCALE') dset.attrs['IMAGE_WHITE_IS_ZERO'] = np.uint8(0) # If the images are all the same shape, send them to the GUI for display: try: image_block = np.stack(self.images) except ValueError: print( "Cannot display images in the GUI, they are not all the same shape" ) else: self._send_image_to_parent(image_block) self.images = None self.n_images = None self.attributes_to_save = None self.exposures = None self.h5_filepath = None self.stop_acquisition_timeout = None self.exception_on_failed_shot = None print("Setting manual mode camera attributes.\n") self.set_attributes_smart(self.manual_mode_camera_attributes) if self.continuous_dt is not None: # If continuous manual mode acquisition was in progress before the bufferd # run, resume it: self.start_continuous(self.continuous_dt) return True