From 5345e6d0ace1786cca3b2a8cc00d58c71727a3a2 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 30 Dec 2023 21:11:48 +1100 Subject: [PATCH 01/19] Initial work centered around refactoring the `Device` and `compiler` classes, and updating code to take advantage of more modern Python (3.6+) features. --- labscript/compiler.py | 66 +++++++ labscript/labscript.py | 399 +++++++++++++++++++---------------------- labscript/remote.py | 30 ++++ labscript/utils.py | 107 +++++++++++ 4 files changed, 387 insertions(+), 215 deletions(-) create mode 100644 labscript/compiler.py create mode 100644 labscript/remote.py create mode 100644 labscript/utils.py diff --git a/labscript/compiler.py b/labscript/compiler.py new file mode 100644 index 0000000..321d30f --- /dev/null +++ b/labscript/compiler.py @@ -0,0 +1,66 @@ +import builtins + +from labscript_utils.labconfig import LabConfig + +_builtins_dict = builtins.__dict__ + +# Extract settings from labconfig +_SAVE_HG_INFO = LabConfig().getboolean("labscript", "save_hg_info", fallback=False) +_SAVE_GIT_INFO = LabConfig().getboolean("labscript", "save_git_info", fallback=False) + + +class Compiler(object): + """Compiler singleton that saves relevant parameters during compilation of each shot""" + + _instance = None + + _existing_builtins_dict = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + # Save initial builtins state so that we (and possibly someone else) can + # call ``reset()`` at any time. + cls._instance.save_builtins_state() + # Initialise state + cls._instance.reset() + return cls._instance + + def save_builtins_state(self): + self._existing_builtins_dict = _builtins_dict.copy() + + def reset(self): + """restores builtins and the labscript module to its state before + labscript_init() was called""" + # Reset builtins + for name in _builtins_dict.copy(): + if name not in self._existing_builtins_dict: + del _builtins_dict[name] + else: + _builtins_dict[name] = self._existing_builtins_dict[name] + + # Reset other variables + + # The labscript file being compiled: + self.labscript_file = None + # All defined devices: + self.inventory = [] + # The filepath of the h5 file containing globals and which will + # contain compilation output: + self.hdf5_filename = None + + self.start_called = False + self.wait_table = {} + self.wait_monitor = None + self.master_pseudoclock = None + self.all_pseudoclocks = None + self.trigger_duration = 0 + self.wait_delay = 0 + self.time_markers = {} + self._PrimaryBLACS = None + self.save_hg_info = _SAVE_HG_INFO + self.save_git_info = _SAVE_GIT_INFO + self.shot_properties = {} + +compiler = Compiler() +"""The compiler instance""" diff --git a/labscript/labscript.py b/labscript/labscript.py index 9535383..089e514 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -12,6 +12,7 @@ ##################################################################### import builtins +import contextlib import os import sys import subprocess @@ -30,10 +31,16 @@ # The code to be removed relates to the move of the globals loading code from # labscript to runmanager batch compiler. - +from .compiler import compiler +from .utils import ( + LabscriptError, + is_clock_line, + is_pseudoclock_device, + is_remote_connection, + set_passed_properties +) import labscript_utils.h5_lock, h5py import labscript_utils.properties -from labscript_utils.labconfig import LabConfig from labscript_utils.filewatcher import FileWatcher # This imports the default Qt library that other labscript suite code will @@ -74,27 +81,49 @@ else: startupinfo = None -# Extract settings from labconfig -_SAVE_HG_INFO = LabConfig().getboolean('labscript', 'save_hg_info', fallback=False) -_SAVE_GIT_INFO = LabConfig().getboolean('labscript', 'save_git_info', fallback=False) - + class config(object): suppress_mild_warnings = True suppress_all_warnings = False compression = 'gzip' # set to 'gzip' for compression - - -class NoWarnings(object): - """A context manager which sets config.suppress_mild_warnings to True - whilst in use. Allows the user to suppress warnings for specific - lines when they know that the warning does not indicate a problem.""" - def __enter__(self): - self.existing_warning_setting = config.suppress_all_warnings - config.suppress_all_warnings = True - def __exit__(self, *args): - config.suppress_all_warnings = self.existing_warning_setting + + +@contextlib.contextmanager() +def suppress_mild_warnings(state=True): + """A context manager which modifies config.suppress_mild_warnings + + Allows the user to suppress (or show) mild warnings for specific lines. Useful when + you want to hide/show all warnings from specific lines. + + Arguments: + state (bool): The new state for ``config.suppress_mild_warnings``. Defaults to + ``True`` if not explicitly provided. + """ + previous_warning_setting = config.suppress_mild_warnings + config.suppress_mild_warnings = state + yield + config.suppress_mild_warnings = previous_warning_setting + + +@contextlib.contextmanager() +def suppress_all_warnings(state=True): + """A context manager which modifies config.suppress_all_warnings + + Allows the user to suppress (or show) all warnings for specific lines. Useful when + you want to hide/show all warnings from specific lines. -no_warnings = NoWarnings() # This is the object that should be used, not the class above + Arguments: + state (bool): The new state for ``config.suppress_all_warnings``. Defaults to + ``True`` if not explicitly provided. + """ + previous_warning_setting = config.suppress_all_warnings + config.suppress_all_warnings = state + yield + config.suppress_all_warnings = previous_warning_setting + + +no_warnings = suppress_all_warnings # Historical alias + def max_or_zero(*args, **kwargs): """Returns max of the arguments or zero if sequence is empty. @@ -114,7 +143,7 @@ def max_or_zero(*args, **kwargs): return 0 else: return max(*args, **kwargs) - + def bitfield(arrays,dtype): """Converts a list of arrays of ones and zeros into a single array of unsigned ints of the given datatype. @@ -168,54 +197,6 @@ def fastflatten(inarray, dtype): i += 1 return flat -def set_passed_properties(property_names = {}): - """ - Decorator for device __init__ methods that saves the listed arguments/keyword - arguments as properties. - - Argument values as passed to __init__ will be saved, with - the exception that if an instance attribute exists after __init__ has run that has - the same name as an argument, the instance attribute will be saved instead of the - argument value. This allows code within __init__ to process default arguments - before they are saved. - - Internally, all properties are accessed by calling :obj:`self.get_property() `. - - Args: - property_names (dict): is a dictionary {key:val}, where each val - is a list [var1, var2, ...] of variables to be pulled from - properties_dict and added to the property with name key (its location) - """ - def decorator(func): - @wraps(func) - def new_function(inst, *args, **kwargs): - - return_value = func(inst, *args, **kwargs) - - # Get a dict of the call arguments/keyword arguments by name: - call_values = getcallargs(func, inst, *args, **kwargs) - - all_property_names = set() - for names in property_names.values(): - all_property_names.update(names) - - property_values = {} - for name in all_property_names: - # If there is an instance attribute with that name, use that, otherwise - # use the call value: - if hasattr(inst, name): - property_values[name] = getattr(inst, name) - else: - property_values[name] = call_values[name] - - # Save them: - inst.set_properties(property_values, property_names) - - return return_value - - return new_function - - return decorator class Device(object): @@ -227,14 +208,26 @@ class Device(object): """ description = 'Generic Device' """Brief description of the device.""" + allowed_children = None """list: Defines types of devices that are allowed to be children of this device.""" @set_passed_properties( property_names = {"device_properties": ["added_properties"]} ) - def __init__(self,name,parent_device,connection, call_parents_add_device=True, - added_properties = {}, gui=None, worker=None, start_order=None, stop_order=None, **kwargs): + def __init__( + self, + name, + parent_device, + connection, + call_parents_add_device=True, + added_properties={}, + gui=None, + worker=None, + start_order=None, + stop_order=None, + **kwargs, + ): """Creates a Device. Args: @@ -253,7 +246,9 @@ def __init__(self,name,parent_device,connection, call_parents_add_device=True, # Verify that no invalid kwargs were passed and the set properties if len(kwargs) != 0: - raise LabscriptError('Invalid keyword arguments: %s.'%kwargs) + raise LabscriptError( + f"Invalid keyword arguments ({kwargs}) passed to '{name}'." + ) if self.allowed_children is None: self.allowed_children = [Device] @@ -263,11 +258,19 @@ def __init__(self,name,parent_device,connection, call_parents_add_device=True, self.start_order = start_order self.stop_order = stop_order if start_order is not None and not isinstance(start_order, int): - raise TypeError("start_order must be int, not %s" % type(start_order).__name__) + raise TypeError( + f"Error when instantiating {name}. start_order must be an integer, not " + f"{start_order.__class__.__name__} (the value provided was " + f"{start_order})." + ) if stop_order is not None and not isinstance(stop_order, int): - raise TypeError("stop_order must be int, not %s" % type(stop_order).__name__) + raise TypeError( + f"Error when instantiating {name}. start_order must be an integer, not " + f"{stop_order.__class__.__name__} (the value provided was " + f"{stop_order})." + ) self.child_devices = [] - + # self._properties may be instantiated already if not hasattr(self, "_properties"): self._properties = {} @@ -284,21 +287,23 @@ def __init__(self,name,parent_device,connection, call_parents_add_device=True, # self.parent_device.add_device(self) *must* be called later # on, it is not optional. parent_device.add_device(self) - + # Check that the name doesn't already exist in the python namespace if name in locals() or name in globals() or name in _builtins_dict: - raise LabscriptError('The device name %s already exists in the Python namespace. Please choose another.'%name) - if name in keyword.kwlist: - raise LabscriptError('%s is a reserved Python keyword.'%name + - ' Please choose a different device name.') - - try: - # Test that name is a valid Python variable name: - exec('%s = None'%name) - assert '.' not in name - except: - raise ValueError('%s is not a valid Python variable name.'%name) - + raise LabscriptError( + f"{name} already exists in the Python namespace. " + f"Please choose another name for this {self.__class__.__name__}." + ) + if keyword.iskeyword(name): + raise LabscriptError( + f"{name} is a reserved Python keyword. " + f"Please choose a different {self.__class__.__name__} name." + ) + + # Test that name is a valid Python variable name: + if not name.isidentifier(): + raise ValueError(f"{name} is not a valid Python variable name.") + # Put self into the global namespace: _builtins_dict[name] = self @@ -314,24 +319,35 @@ def __init__(self,name,parent_device,connection, call_parents_add_device=True, worker = gui # check that worker and gui are appropriately typed - if not isinstance(gui, _RemoteConnection): - raise LabscriptError('the "gui" argument for %s must be specified as a subclass of _RemoteConnection'%(self.name)) + if not is_remote_connection(gui): + raise LabscriptError( + f"The 'gui' argument for {name} must be specified as a " + "subclass of _RemoteConnection" + ) else: # just remote worker gui = compiler._PrimaryBLACS - if not isinstance(worker, _RemoteConnection): - raise LabscriptError('the "worker" argument for %s must be specified as a subclass of _RemoteConnection'%(self.name)) + if not is_remote_connection(worker): + raise LabscriptError( + f"The 'worker' argument for {name} must be specified as a " + "subclass of _RemoteConnection" + ) # check that worker is equal to, or a child of, gui if worker != gui and worker not in gui.get_all_children(): - print(gui.get_all_children()) - raise LabscriptError('The remote worker (%s) for %s must be a child of the specified gui (%s) '%(worker.name, self.name, gui.name)) + raise LabscriptError( + f"The remote worker ({worker.name}) for {name} must be a child of " + f"the specified gui ({gui.name}). Available gui children are: " + f"{gui.get_all_children()}" + ) # store worker and gui as properties of the connection table - self.set_property('gui', gui.name, 'connection_table_properties') - self.set_property('worker', worker.name, 'connection_table_properties') - + self.set_property("gui", gui.name, "connection_table_properties") + self.set_property("worker", worker.name, "connection_table_properties") + + def __repr__(self): + return f"{self.name} ({self.__class__.__name__})" def set_property(self, name, value, location=None, overwrite=False): """Method to set a property for this device. @@ -354,7 +370,10 @@ def set_property(self, name, value, location=None, overwrite=False): existing property with `'overwrite'=False`. """ if location is None or location not in labscript_utils.properties.VALID_PROPERTY_LOCATIONS: - raise LabscriptError('Device %s requests invalid property assignment %s for property %s'%(self.name, location, name)) + raise LabscriptError( + f"Device {self.name} requests invalid property assignment {location} " + f"for property {name}" + ) # if this try fails then self."location" may not be instantiated if not hasattr(self, "_properties"): @@ -370,7 +389,7 @@ def set_property(self, name, value, location=None, overwrite=False): selected_properties[name] = value - def set_properties(self, properties_dict, property_names, overwrite = False): + def set_properties(self, properties_dict, property_names, overwrite=False): """ Add one or a bunch of properties packed into properties_dict @@ -378,20 +397,22 @@ def set_properties(self, properties_dict, property_names, overwrite = False): properties_dict (dict): Dictionary of properties and their values. property_names (dict): Is a dictionary {key:val, ...} where each val is a list [var1, var2, ...] of variables to be pulled from - properties_dict and added to the property with name key (it's location) + properties_dict and added to the property localtion with name ``key`` overwrite (bool, optional): Toggles overwriting of existing properties. """ for location, names in property_names.items(): - if not isinstance(names, list) and not isinstance(names, tuple): - raise TypeError('%s names (%s) must be list or tuple, not %s'%(location, repr(names), str(type(names)))) - temp_dict = {key:val for key, val in properties_dict.items() if key in names} - for (name, value) in temp_dict.items(): - self.set_property(name, value, - overwrite = overwrite, - location = location) - + if not isinstance(names, (list, tuple, set)): + raise TypeError( + f"Names for {location} ({names}) must be a list, tuple, or set, " + f"not {names.__class__.__name__}." + ) + properties_for_location = { + key: val for key, val in properties_dict.items() if key in names + } + for (name, value) in properties_for_location.items(): + self.set_property(name, value, overwrite=overwrite, location=location) - def get_property(self, name, location = None, *args, **kwargs):#default = None): + def get_property(self, name, location=None, *args, **kwargs): """Method to get a property of this device already set using :func:`Device.set_property`. If the property is not already set, a default value will be returned @@ -403,10 +424,8 @@ def get_property(self, name, location = None, *args, **kwargs):#default = None): name (str): Name of property to get. location (str, optional): If not `None`, only search for `name` in `location`. - *args: Must be length 1, provides a default value if property - is not defined. - **kwargs: Must have key `'default'`, provides a default value - if property is not defined. + default: The default value. If not provided, an exception is raised if the + value is not set. Returns: : Property value. @@ -429,12 +448,20 @@ def get_property(self, name, location = None, *args, **kwargs):#default = None): >>> get_property('example', default=7, x=9) """ if len(kwargs) == 1 and 'default' not in kwargs: - raise LabscriptError('A call to %s.get_property had a keyword argument that was not name or default'%self.name) + raise LabscriptError( + f"A call to {self.name}.get_property had a keyword argument that was " + "not name, location, or default" + ) if len(args) + len(kwargs) > 1: - raise LabscriptError('A call to %s.get_property has too many arguments and/or keyword arguments'%self.name) + raise LabscriptError( + f"A call to {self.name}.get_property has too many arguments and/or " + "keyword arguments" + ) if (location is not None) and (location not in labscript_utils.properties.VALID_PROPERTY_LOCATIONS): - raise LabscriptError('Device %s requests invalid property read location %s'%(self.name, location)) + raise LabscriptError( + f"Device {self.name} requests invalid property read location {location}" + ) # self._properties may not be instantiated if not hasattr(self, "_properties"): @@ -450,7 +477,9 @@ def get_property(self, name, location = None, *args, **kwargs):#default = None): elif len(args) == 1: return args[0] else: - raise LabscriptError('The property %s has not been set for device %s'%(name, self.name)) + raise LabscriptError( + f"The property {name} has not been set for device {self.name}" + ) def get_properties(self, location = None): """ @@ -463,18 +492,19 @@ def get_properties(self, location = None): Returns: dict: Dictionary of properties. """ - + # self._properties may not be instantiated if not hasattr(self, "_properties"): self._properties = {} if location is not None: - temp_dict = self._properties.get(location, {}) + properties = self._properties.get(location, {}) else: - temp_dict = {} - for key,val in self._properties.items(): temp_dict.update(val) - - return temp_dict + properties = {} + for key, val in self._properties.items(): + properties.update(val) + + return properties def add_device(self, device): """Adds a child device to this device. @@ -485,23 +515,29 @@ def add_device(self, device): Raises: LabscriptError: If `device` is not an allowed child of this device. """ - if any([isinstance(device,DeviceClass) for DeviceClass in self.allowed_children]): + if any([isinstance(device, DeviceClass) for DeviceClass in self.allowed_children]): self.child_devices.append(device) else: - raise LabscriptError('Devices of type %s cannot be attached to devices of type %s.'%(device.description,self.description)) + raise LabscriptError( + f"Devices of type {device.description} cannot be attached to devices " + f"of type {self.description}." + ) @property def pseudoclock_device(self): """:obj:`PseudoclockDevice`: Stores the clocking pseudoclock, which may be itself.""" - if isinstance(self, PseudoclockDevice): + if is_pseudoclock_device(self): return self parent = self.parent_device try: - while parent is not None and not isinstance(parent,PseudoclockDevice): + while parent is not None and not is_pseudoclock_device(parent): parent = parent.parent_device return parent except Exception as e: - raise LabscriptError('Couldn\'t find parent pseudoclock device of %s, what\'s going on? Original error was %s.'%(self.name, str(e))) + raise LabscriptError( + f"Couldn't find parent pseudoclock device of {self.name}, what's going " + f"on? Original error was {e}." + ) def quantise_to_pseudoclock(self, times): """Quantises `times` to the resolution of the controlling pseudoclock. @@ -533,15 +569,18 @@ def quantise_to_pseudoclock(self, times): @property def parent_clock_line(self): """:obj:`ClockLine`: Stores the clocking clockline, which may be itself.""" - if isinstance(self, ClockLine): + if is_clock_line(self): return self parent = self.parent_device try: - while not isinstance(parent,ClockLine): + while not is_clock_line(parent): parent = parent.parent_device return parent except Exception as e: - raise LabscriptError('Couldn\'t find parent ClockLine of %s, what\'s going on? Original error was %s.'%(self.name, str(e))) + raise LabscriptError( + f"Couldn't find parent ClockLine of {self.name}, what's going on? " + f"Original error was {e}." + ) @property def t0(self): @@ -557,15 +596,15 @@ def t0(self): def get_all_outputs(self): """Get all children devices that are outputs. + Recursively calls ``get_all_outputs()`` on each child device. ``Output``'s will + return a list containing just themselves. + Returns: list: List of children :obj:`Output`. """ all_outputs = [] for device in self.child_devices: - if isinstance(device,Output): - all_outputs.append(device) - else: - all_outputs.extend(device.get_all_outputs()) + all_outputs.extend(device.get_all_outputs()) return all_outputs def get_all_children(self): @@ -606,32 +645,6 @@ def init_device_group(self, hdf5_file): return group -class _PrimaryBLACS(Device): - pass - -class _RemoteConnection(Device): - @set_passed_properties( - property_names = {} - ) - def __init__(self, name, parent=None, connection=None): - if parent is None: - # define a hidden parent of top level remote connections so that - # "connection" is stored correctly in the connection table - if compiler._PrimaryBLACS is None: - compiler._PrimaryBLACS = _PrimaryBLACS('__PrimaryBLACS', None, None) - parent = compiler._PrimaryBLACS - Device.__init__(self, name, parent, connection) - - -class RemoteBLACS(_RemoteConnection): - def __init__(self, name, host, port=7341, parent=None): - _RemoteConnection.__init__(self, name, parent, "%s:%s"%(host, port)) - - -class SecondaryControlSystem(_RemoteConnection): - def __init__(self, name, host, port, parent=None): - _RemoteConnection.__init__(self, name, parent, "%s:%s"%(host, port)) - class IntermediateDevice(Device): """Base class for all devices that are to be clocked by a pseudoclock.""" @@ -1472,6 +1485,16 @@ def wait_delay(self): """ delay = compiler.wait_delay if self.pseudoclock_device.is_master_pseudoclock else 0 return self.trigger_delay + delay + + def get_all_outputs(self): + """Get all children devices that are outputs. + + For ``Output``, this is `self`. + + Returns: + list: List of children :obj:`Output`. + """ + return [self] def apply_calibration(self,value,units): """Apply the calibration defined by the unit conversion class, if present. @@ -3158,14 +3181,6 @@ def disable(self,t=None): self.gate.go_low(t) else: raise LabscriptError('DDS %s does not have a digital gate, so you cannot use the disable(t) method.'%(self.name)) - -class LabscriptError(Exception): - """A *labscript* error. - - This is used to denote an error within the labscript suite itself. - Is a thin wrapper of :obj:`Exception`. - """ - pass def save_time_markers(hdf5_file): """Save shot time markers to the shot file. @@ -3711,7 +3726,7 @@ def labscript_init(hdf5_filename, labscript_file=None, new=False, overwrite=Fals from the existing shot file. """ # save the builtins for later restoration in labscript_cleanup - compiler._existing_builtins_dict = _builtins_dict.copy() + compiler.save_builtins_state() if new: # defer file creation until generate_code(), so that filesystem @@ -3736,50 +3751,4 @@ def labscript_init(hdf5_filename, labscript_file=None, new=False, overwrite=Fals def labscript_cleanup(): """restores builtins and the labscript module to its state before labscript_init() was called""" - for name in _builtins_dict.copy(): - if name not in compiler._existing_builtins_dict: - del _builtins_dict[name] - else: - _builtins_dict[name] = compiler._existing_builtins_dict[name] - - compiler.inventory = [] - compiler.hdf5_filename = None - compiler.labscript_file = None - compiler.start_called = False - compiler.wait_table = {} - compiler.wait_monitor = None - compiler.master_pseudoclock = None - compiler.all_pseudoclocks = None - compiler.trigger_duration = 0 - compiler.wait_delay = 0 - compiler.time_markers = {} - compiler._PrimaryBLACS = None - compiler.save_hg_info = _SAVE_HG_INFO - compiler.save_git_info = _SAVE_GIT_INFO - compiler.shot_properties = {} - -class compiler(object): - """Compiler object that saves relevant parameters during - compilation of each shot.""" - # The labscript file being compiled: - labscript_file = None - # All defined devices: - inventory = [] - # The filepath of the h5 file containing globals and which will - # contain compilation output: - hdf5_filename = None - start_called = False - wait_table = {} - wait_monitor = None - master_pseudoclock = None - all_pseudoclocks = None - trigger_duration = 0 - wait_delay = 0 - time_markers = {} - _PrimaryBLACS = None - save_hg_info = _SAVE_HG_INFO - save_git_info = _SAVE_GIT_INFO - shot_properties = {} - - # safety measure in case cleanup is called before init - _existing_builtins_dict = _builtins_dict.copy() + compiler.reset() diff --git a/labscript/remote.py b/labscript/remote.py new file mode 100644 index 0000000..1d0d979 --- /dev/null +++ b/labscript/remote.py @@ -0,0 +1,30 @@ +from .compiler import compiler +from .labscript import Device, set_passed_properties + + +class _PrimaryBLACS(Device): + pass + + +class _RemoteConnection(Device): + @set_passed_properties( + property_names = {} + ) + def __init__(self, name, parent=None, connection=None): + if parent is None: + # define a hidden parent of top level remote connections so that + # "connection" is stored correctly in the connection table + if compiler._PrimaryBLACS is None: + compiler._PrimaryBLACS = _PrimaryBLACS('__PrimaryBLACS', None, None) + parent = compiler._PrimaryBLACS + Device.__init__(self, name, parent, connection) + + +class RemoteBLACS(_RemoteConnection): + def __init__(self, name, host, port=7341, parent=None): + _RemoteConnection.__init__(self, name, parent, "%s:%s"%(host, port)) + + +class SecondaryControlSystem(_RemoteConnection): + def __init__(self, name, host, port, parent=None): + _RemoteConnection.__init__(self, name, parent, "%s:%s"%(host, port)) diff --git a/labscript/utils.py b/labscript/utils.py new file mode 100644 index 0000000..5db811f --- /dev/null +++ b/labscript/utils.py @@ -0,0 +1,107 @@ +from inspect import getcallargs +from functools import wraps + +_RemoteConnection = None +ClockLine = None +PseudoClockDevice = None + + +def is_remote_connection(connection): + """Returns whether the connection is an instance of ``_RemoteConnection`` + + This function defers and caches the import of ``_RemoteConnection``. This both + breaks the circular import between ``Device`` and ``_RemoteConnection``, while + maintaining reasonable performance (this performs better than importing each time as + the lookup in the modules hash table is slower). + """ + if _RemoteConnection is None: + from .remote import _RemoteConnection + return isinstance(connection, _RemoteConnection) + + +def is_clock_line(device): + """Returns whether the connection is an instance of ``ClockLine`` + + This function defers and caches the import of ``ClockLine``. This both + breaks the circular import between ``Device`` and ``ClockLine``, while + maintaining reasonable performance (this performs better than importing each time as + the lookup in the modules hash table is slower). + """ + if ClockLine is None: + from .labscript import ClockLine + return isinstance(device, _RemoteConnection) + + +def is_pseudoclock_device(device): + """Returns whether the connection is an instance of ``PseudoclockDevice`` + + This function defers and caches the import of ``_RemoteConnection``. This both + breaks the circular import between ``Device`` and ``_RemoteConnection``, while + maintaining reasonable performance (this performs better than importing each time as + the lookup in the modules hash table is slower). + """ + if PseudoclockDevice is None: + from .labscript import PseudoclockDevice + return isinstance(device, PseudoclockDevice) + + +def set_passed_properties(property_names=None): + """ + Decorator for device __init__ methods that saves the listed arguments/keyword + arguments as properties. + + Argument values as passed to __init__ will be saved, with + the exception that if an instance attribute exists after __init__ has run that has + the same name as an argument, the instance attribute will be saved instead of the + argument value. This allows code within __init__ to process default arguments + before they are saved. + + Internally, all properties are accessed by calling :obj:`self.get_property() `. + + Args: + property_names (dict): A dictionary {key:val}, where each ``val`` + is a list [var1, var2, ...] of instance attribute names and/or method call + arguments (of the decorated method). Values of the instance + attributes/method call arguments will be saved to the location specified by + ``key``. + """ + property_names = property_names or {} + + def decorator(func): + @wraps(func) + def new_function(inst, *args, **kwargs): + + return_value = func(inst, *args, **kwargs) + + # Get a dict of the call arguments/keyword arguments by name: + call_values = getcallargs(func, inst, *args, **kwargs) + + all_property_names = set() + for names in property_names.values(): + all_property_names.update(names) + + property_values = {} + for name in all_property_names: + # If there is an instance attribute with that name, use that, otherwise + # use the call value: + if hasattr(inst, name): + property_values[name] = getattr(inst, name) + else: + property_values[name] = call_values[name] + + # Save them: + inst.set_properties(property_values, property_names) + + return return_value + + return new_function + + return decorator + + +class LabscriptError(Exception): + """A *labscript* error. + + This is used to denote an error within the labscript suite itself. + Is a thin wrapper of :obj:`Exception`. + """ From 461344e37e6ad280de64ccd86a05e38af64ce7d6 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 30 Dec 2023 21:14:30 +1100 Subject: [PATCH 02/19] Break out `Device` class into it's own file --- labscript/base_device.py | 466 +++++++++++++++++++++++++++++++++++++++ labscript/labscript.py | 460 +------------------------------------- 2 files changed, 469 insertions(+), 457 deletions(-) create mode 100644 labscript/base_device.py diff --git a/labscript/base_device.py b/labscript/base_device.py new file mode 100644 index 0000000..00f9644 --- /dev/null +++ b/labscript/base_device.py @@ -0,0 +1,466 @@ +import builtins +import keyword + +import numpy as np + +import labscript_utils.properties + +from .compiler import compiler +from .utils import ( + LabscriptError, + is_clock_line, + is_pseudoclock_device, + is_remote_connection, + set_passed_properties +) + +# Create a reference to the builtins dict +# update this if accessing builtins ever changes +_builtins_dict = builtins.__dict__ + + +class Device(object): + """Parent class of all device and input/output channels. + + You usually won't interact directly with this class directly (i.e. you never + instantiate this class directly) but it provides some useful functionality + that is then available to all subclasses. + """ + + description = 'Generic Device' + """Brief description of the device.""" + + allowed_children = None + """list: Defines types of devices that are allowed to be children of this device.""" + + @set_passed_properties( + property_names = {"device_properties": ["added_properties"]} + ) + def __init__( + self, + name, + parent_device, + connection, + call_parents_add_device=True, + added_properties={}, + gui=None, + worker=None, + start_order=None, + stop_order=None, + **kwargs, + ): + """Creates a Device. + + Args: + name (str): python variable name to assign this device to. + parent_device (:obj:`Device`): Parent of this device. + connection (str): Connection on this device that links to parent. + call_parents_add_device (bool, optional): Flag to command device to + call its parent device's add_device when adding a device. + added_properties (dict, optional): + gui : + worker : + start_order (int, optional): Priority when starting, sorted with all devices. + stop_order (int, optional): Priority when stopping, sorted with all devices. + **kwargs: Other options to pass to parent. + """ + + # Verify that no invalid kwargs were passed and the set properties + if len(kwargs) != 0: + raise LabscriptError( + f"Invalid keyword arguments ({kwargs}) passed to '{name}'." + ) + + if self.allowed_children is None: + self.allowed_children = [Device] + self.name = name + self.parent_device = parent_device + self.connection = connection + self.start_order = start_order + self.stop_order = stop_order + if start_order is not None and not isinstance(start_order, int): + raise TypeError( + f"Error when instantiating {name}. start_order must be an integer, not " + f"{start_order.__class__.__name__} (the value provided was " + f"{start_order})." + ) + if stop_order is not None and not isinstance(stop_order, int): + raise TypeError( + f"Error when instantiating {name}. stop_order must be an integer, not " + f"{stop_order.__class__.__name__} (the value provided was " + f"{stop_order})." + ) + self.child_devices = [] + + # self._properties may be instantiated already + if not hasattr(self, "_properties"): + self._properties = {} + for location in labscript_utils.properties.VALID_PROPERTY_LOCATIONS: + if location not in self._properties: + self._properties[location] = {} + + if parent_device and call_parents_add_device: + # This is optional by keyword argument, so that subclasses + # overriding __init__ can call call Device.__init__ early + # on and only call self.parent_device.add_device(self) + # a bit later, allowing for additional code in + # between. If setting call_parents_add_device=False, + # self.parent_device.add_device(self) *must* be called later + # on, it is not optional. + parent_device.add_device(self) + + # Check that the name doesn't already exist in the python namespace + if name in locals() or name in globals() or name in _builtins_dict: + raise LabscriptError( + f"{name} already exists in the Python namespace. " + f"Please choose another name for this {self.__class__.__name__}." + ) + if keyword.iskeyword(name): + raise LabscriptError( + f"{name} is a reserved Python keyword. " + f"Please choose a different {self.__class__.__name__} name." + ) + + # Test that name is a valid Python variable name: + if not name.isidentifier(): + raise ValueError(f"{name} is not a valid Python variable name.") + + # Put self into the global namespace: + _builtins_dict[name] = self + + # Add self to the compiler's device inventory + compiler.inventory.append(self) + + # handle remote workers/gui interface + if gui is not None or worker is not None: + # remote GUI and worker + if gui is not None: + # if no worker is specified, assume it is the same as the gui + if worker is None: + worker = gui + + # check that worker and gui are appropriately typed + if not is_remote_connection(gui): + raise LabscriptError( + f"The 'gui' argument for {name} must be specified as a " + "subclass of _RemoteConnection" + ) + else: + # just remote worker + gui = compiler._PrimaryBLACS + + if not is_remote_connection(worker): + raise LabscriptError( + f"The 'worker' argument for {name} must be specified as a " + "subclass of _RemoteConnection" + ) + + # check that worker is equal to, or a child of, gui + if worker != gui and worker not in gui.get_all_children(): + raise LabscriptError( + f"The remote worker ({worker.name}) for {name} must be a child of " + f"the specified gui ({gui.name}). Available gui children are: " + f"{gui.get_all_children()}" + ) + + # store worker and gui as properties of the connection table + self.set_property("gui", gui.name, "connection_table_properties") + self.set_property("worker", worker.name, "connection_table_properties") + + def __repr__(self): + return f"{self.name} ({self.__class__.__name__})" + + def set_property(self, name, value, location=None, overwrite=False): + """Method to set a property for this device. + + Property will be stored in the connection table and used + during connection table comparisons. + + Value must satisfy `eval(repr(value)) == value`. + + Args: + name (str): Name to save property value to. + value: Value to set property to. + location (str, optional): Specify a location to save property to, such as + `'device_properties'` or `'connection_table_properties'`. + overwrite (bool, optional): If `True`, allow overwriting a property + already set. + + Raises: + LabscriptError: If `'location'` is not valid or trying to overwrite an + existing property with `'overwrite'=False`. + """ + if location is None or location not in labscript_utils.properties.VALID_PROPERTY_LOCATIONS: + raise LabscriptError( + f"Device {self.name} requests invalid property assignment {location} " + f"for property {name}" + ) + + # if this try fails then self."location" may not be instantiated + if not hasattr(self, "_properties"): + self._properties = {} + + if location not in self._properties: + self._properties[location] = {} + + selected_properties = self._properties[location] + + if name in selected_properties and not overwrite: + raise LabscriptError('Device %s has had the property %s set more than once. This is not allowed unless the overwrite flag is explicitly set'%(self.name, name)) + + selected_properties[name] = value + + def set_properties(self, properties_dict, property_names, overwrite=False): + """ + Add one or a bunch of properties packed into properties_dict + + Args: + properties_dict (dict): Dictionary of properties and their values. + property_names (dict): Is a dictionary {key:val, ...} where each val + is a list [var1, var2, ...] of variables to be pulled from + properties_dict and added to the property localtion with name ``key`` + overwrite (bool, optional): Toggles overwriting of existing properties. + """ + for location, names in property_names.items(): + if not isinstance(names, (list, tuple, set)): + raise TypeError( + f"Names for {location} ({names}) must be a list, tuple, or set, " + f"not {names.__class__.__name__}." + ) + properties_for_location = { + key: val for key, val in properties_dict.items() if key in names + } + for (name, value) in properties_for_location.items(): + self.set_property(name, value, overwrite=overwrite, location=location) + + def get_property(self, name, location=None, *args, **kwargs): + """Method to get a property of this device already set using :func:`Device.set_property`. + + If the property is not already set, a default value will be returned + if specified as the argument after `'name'`, if there is only one argument + after `'name'` and the argument is either not a keyword argurment or is a + keyword argument with the name `'default'`. + + Args: + name (str): Name of property to get. + location (str, optional): If not `None`, only search for `name` + in `location`. + default: The default value. If not provided, an exception is raised if the + value is not set. + + Returns: + : Property value. + + Raises: + LabscriptError: If property not set and default not provided, or default + conventions not followed. + + Examples: + Examples of acceptable signatures: + + >>> get_property('example') # 'example' will be returned if set, or an exception raised + >>> get_property('example', 7) # 7 returned if 'example' is not set + >>> get_property('example', default=7) # 7 returnd if 'example' is not set + + Example signatures that WILL ALWAYS RAISE AN EXCEPTION: + + >>> get_property('example', 7, 8) + >>> get_property('example', 7, default=9) + >>> get_property('example', default=7, x=9) + """ + if len(kwargs) == 1 and 'default' not in kwargs: + raise LabscriptError( + f"A call to {self.name}.get_property had a keyword argument that was " + "not name, location, or default" + ) + if len(args) + len(kwargs) > 1: + raise LabscriptError( + f"A call to {self.name}.get_property has too many arguments and/or " + "keyword arguments" + ) + + if (location is not None) and (location not in labscript_utils.properties.VALID_PROPERTY_LOCATIONS): + raise LabscriptError( + f"Device {self.name} requests invalid property read location {location}" + ) + + # self._properties may not be instantiated + if not hasattr(self, "_properties"): + self._properties = {} + + # Run through all keys of interest + for key, val in self._properties.items(): + if (location is None or key == location) and (name in val): + return val[name] + + if 'default' in kwargs: + return kwargs['default'] + elif len(args) == 1: + return args[0] + else: + raise LabscriptError( + f"The property {name} has not been set for device {self.name}" + ) + + def get_properties(self, location = None): + """ + Get all properties in location. + + Args: + location (str, optional): Location to get properties from. + If `None`, return all properties. + + Returns: + dict: Dictionary of properties. + """ + + # self._properties may not be instantiated + if not hasattr(self, "_properties"): + self._properties = {} + + if location is not None: + properties = self._properties.get(location, {}) + else: + properties = {} + for key, val in self._properties.items(): + properties.update(val) + + return properties + + def add_device(self, device): + """Adds a child device to this device. + + Args: + device (:obj:`Device`): Device to add. + + Raises: + LabscriptError: If `device` is not an allowed child of this device. + """ + if any([isinstance(device, DeviceClass) for DeviceClass in self.allowed_children]): + self.child_devices.append(device) + else: + raise LabscriptError( + f"Devices of type {device.description} cannot be attached to devices " + f"of type {self.description}." + ) + + @property + def pseudoclock_device(self): + """:obj:`PseudoclockDevice`: Stores the clocking pseudoclock, which may be itself.""" + if is_pseudoclock_device(self): + return self + parent = self.parent_device + try: + while parent is not None and not is_pseudoclock_device(parent): + parent = parent.parent_device + return parent + except Exception as e: + raise LabscriptError( + f"Couldn't find parent pseudoclock device of {self.name}, what's going " + f"on? Original error was {e}." + ) + + def quantise_to_pseudoclock(self, times): + """Quantises `times` to the resolution of the controlling pseudoclock. + + Args: + times (:obj:`numpy:numpy.ndarray` or list or set or float): Time, + in seconds, to quantise. + + Returns: + same type as `times`: Quantised times. + """ + convert_back_to = None + if not isinstance(times, np.ndarray): + if isinstance(times, list): + convert_back_to = list + elif isinstance(times, set): + convert_back_to = set + else: + convert_back_to = float + times = np.array(times) + # quantise the times to the pseudoclock clock resolution + times = (times/self.pseudoclock_device.clock_resolution).round()*self.pseudoclock_device.clock_resolution + + if convert_back_to is not None: + times = convert_back_to(times) + + return times + + @property + def parent_clock_line(self): + """:obj:`ClockLine`: Stores the clocking clockline, which may be itself.""" + if is_clock_line(self): + return self + parent = self.parent_device + try: + while not is_clock_line(parent): + parent = parent.parent_device + return parent + except Exception as e: + raise LabscriptError( + f"Couldn't find parent ClockLine of {self.name}, what's going on? " + f"Original error was {e}." + ) + + @property + def t0(self): + """float: The earliest time output can be commanded from this device at + the start of the experiment. This is nonzeo on secondary pseudoclock + devices due to triggering delays.""" + parent = self.pseudoclock_device + if parent is None or parent.is_master_pseudoclock: + return 0 + else: + return round(parent.trigger_times[0] + parent.trigger_delay, 10) + + def get_all_outputs(self): + """Get all children devices that are outputs. + + Recursively calls ``get_all_outputs()`` on each child device. ``Output``'s will + return a list containing just themselves. + + Returns: + list: List of children :obj:`Output`. + """ + all_outputs = [] + for device in self.child_devices: + all_outputs.extend(device.get_all_outputs()) + return all_outputs + + def get_all_children(self): + """Get all children devices for this device. + + Returns: + list: List of children :obj:`Device`. + """ + all_children = [] + for device in self.child_devices: + all_children.append(device) + all_children.extend(device.get_all_children()) + return all_children + + def generate_code(self, hdf5_file): + """Generate hardware instructions for device and children, then save + to h5 file. + + Will recursively call `generate_code` for all children devices. + + Args: + hdf5_file (:obj:`h5py:h5py.File`): Handle to shot file. + """ + for device in self.child_devices: + device.generate_code(hdf5_file) + + def init_device_group(self, hdf5_file): + """Creates the device group in the shot file. + + Args: + hdf5_file (:obj:`h5py:h5py.File`): File handle to + create the group in. + + Returns: + :class:`h5py:h5py.Group`: Created group handle. + """ + group = hdf5_file['/devices'].create_group(self.name) + return group diff --git a/labscript/labscript.py b/labscript/labscript.py index 089e514..7d02da9 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -18,8 +18,7 @@ import subprocess import keyword import threading -from inspect import getcallargs -from functools import wraps, lru_cache +from functools import lru_cache import numpy as np # Notes for v3 @@ -31,14 +30,9 @@ # The code to be removed relates to the move of the globals loading code from # labscript to runmanager batch compiler. +from .base_device import Device from .compiler import compiler -from .utils import ( - LabscriptError, - is_clock_line, - is_pseudoclock_device, - is_remote_connection, - set_passed_properties -) +from .utils import LabscriptError, set_passed_properties import labscript_utils.h5_lock, h5py import labscript_utils.properties from labscript_utils.filewatcher import FileWatcher @@ -198,454 +192,6 @@ def fastflatten(inarray, dtype): return flat - -class Device(object): - """Parent class of all device and input/output channels. - - You usually won't interact directly with this class directly (i.e. you never - instantiate this class directly) but it provides some useful functionality - that is then available to all subclasses. - """ - description = 'Generic Device' - """Brief description of the device.""" - - allowed_children = None - """list: Defines types of devices that are allowed to be children of this device.""" - - @set_passed_properties( - property_names = {"device_properties": ["added_properties"]} - ) - def __init__( - self, - name, - parent_device, - connection, - call_parents_add_device=True, - added_properties={}, - gui=None, - worker=None, - start_order=None, - stop_order=None, - **kwargs, - ): - """Creates a Device. - - Args: - name (str): python variable name to assign this device to. - parent_device (:obj:`Device`): Parent of this device. - connection (str): Connection on this device that links to parent. - call_parents_add_device (bool, optional): Flag to command device to - call its parent device's add_device when adding a device. - added_properties (dict, optional): - gui : - worker : - start_order (int, optional): Priority when starting, sorted with all devices. - stop_order (int, optional): Priority when stopping, sorted with all devices. - **kwargs: Other options to pass to parent. - """ - - # Verify that no invalid kwargs were passed and the set properties - if len(kwargs) != 0: - raise LabscriptError( - f"Invalid keyword arguments ({kwargs}) passed to '{name}'." - ) - - if self.allowed_children is None: - self.allowed_children = [Device] - self.name = name - self.parent_device = parent_device - self.connection = connection - self.start_order = start_order - self.stop_order = stop_order - if start_order is not None and not isinstance(start_order, int): - raise TypeError( - f"Error when instantiating {name}. start_order must be an integer, not " - f"{start_order.__class__.__name__} (the value provided was " - f"{start_order})." - ) - if stop_order is not None and not isinstance(stop_order, int): - raise TypeError( - f"Error when instantiating {name}. start_order must be an integer, not " - f"{stop_order.__class__.__name__} (the value provided was " - f"{stop_order})." - ) - self.child_devices = [] - - # self._properties may be instantiated already - if not hasattr(self, "_properties"): - self._properties = {} - for location in labscript_utils.properties.VALID_PROPERTY_LOCATIONS: - if location not in self._properties: - self._properties[location] = {} - - if parent_device and call_parents_add_device: - # This is optional by keyword argument, so that subclasses - # overriding __init__ can call call Device.__init__ early - # on and only call self.parent_device.add_device(self) - # a bit later, allowing for additional code in - # between. If setting call_parents_add_device=False, - # self.parent_device.add_device(self) *must* be called later - # on, it is not optional. - parent_device.add_device(self) - - # Check that the name doesn't already exist in the python namespace - if name in locals() or name in globals() or name in _builtins_dict: - raise LabscriptError( - f"{name} already exists in the Python namespace. " - f"Please choose another name for this {self.__class__.__name__}." - ) - if keyword.iskeyword(name): - raise LabscriptError( - f"{name} is a reserved Python keyword. " - f"Please choose a different {self.__class__.__name__} name." - ) - - # Test that name is a valid Python variable name: - if not name.isidentifier(): - raise ValueError(f"{name} is not a valid Python variable name.") - - # Put self into the global namespace: - _builtins_dict[name] = self - - # Add self to the compiler's device inventory - compiler.inventory.append(self) - - # handle remote workers/gui interface - if gui is not None or worker is not None: - # remote GUI and worker - if gui is not None: - # if no worker is specified, assume it is the same as the gui - if worker is None: - worker = gui - - # check that worker and gui are appropriately typed - if not is_remote_connection(gui): - raise LabscriptError( - f"The 'gui' argument for {name} must be specified as a " - "subclass of _RemoteConnection" - ) - else: - # just remote worker - gui = compiler._PrimaryBLACS - - if not is_remote_connection(worker): - raise LabscriptError( - f"The 'worker' argument for {name} must be specified as a " - "subclass of _RemoteConnection" - ) - - # check that worker is equal to, or a child of, gui - if worker != gui and worker not in gui.get_all_children(): - raise LabscriptError( - f"The remote worker ({worker.name}) for {name} must be a child of " - f"the specified gui ({gui.name}). Available gui children are: " - f"{gui.get_all_children()}" - ) - - # store worker and gui as properties of the connection table - self.set_property("gui", gui.name, "connection_table_properties") - self.set_property("worker", worker.name, "connection_table_properties") - - def __repr__(self): - return f"{self.name} ({self.__class__.__name__})" - - def set_property(self, name, value, location=None, overwrite=False): - """Method to set a property for this device. - - Property will be stored in the connection table and used - during connection table comparisons. - - Value must satisfy `eval(repr(value)) == value`. - - Args: - name (str): Name to save property value to. - value: Value to set property to. - location (str, optional): Specify a location to save property to, such as - `'device_properties'` or `'connection_table_properties'`. - overwrite (bool, optional): If `True`, allow overwriting a property - already set. - - Raises: - LabscriptError: If `'location'` is not valid or trying to overwrite an - existing property with `'overwrite'=False`. - """ - if location is None or location not in labscript_utils.properties.VALID_PROPERTY_LOCATIONS: - raise LabscriptError( - f"Device {self.name} requests invalid property assignment {location} " - f"for property {name}" - ) - - # if this try fails then self."location" may not be instantiated - if not hasattr(self, "_properties"): - self._properties = {} - - if location not in self._properties: - self._properties[location] = {} - - selected_properties = self._properties[location] - - if name in selected_properties and not overwrite: - raise LabscriptError('Device %s has had the property %s set more than once. This is not allowed unless the overwrite flag is explicitly set'%(self.name, name)) - - selected_properties[name] = value - - def set_properties(self, properties_dict, property_names, overwrite=False): - """ - Add one or a bunch of properties packed into properties_dict - - Args: - properties_dict (dict): Dictionary of properties and their values. - property_names (dict): Is a dictionary {key:val, ...} where each val - is a list [var1, var2, ...] of variables to be pulled from - properties_dict and added to the property localtion with name ``key`` - overwrite (bool, optional): Toggles overwriting of existing properties. - """ - for location, names in property_names.items(): - if not isinstance(names, (list, tuple, set)): - raise TypeError( - f"Names for {location} ({names}) must be a list, tuple, or set, " - f"not {names.__class__.__name__}." - ) - properties_for_location = { - key: val for key, val in properties_dict.items() if key in names - } - for (name, value) in properties_for_location.items(): - self.set_property(name, value, overwrite=overwrite, location=location) - - def get_property(self, name, location=None, *args, **kwargs): - """Method to get a property of this device already set using :func:`Device.set_property`. - - If the property is not already set, a default value will be returned - if specified as the argument after `'name'`, if there is only one argument - after `'name'` and the argument is either not a keyword argurment or is a - keyword argument with the name `'default'`. - - Args: - name (str): Name of property to get. - location (str, optional): If not `None`, only search for `name` - in `location`. - default: The default value. If not provided, an exception is raised if the - value is not set. - - Returns: - : Property value. - - Raises: - LabscriptError: If property not set and default not provided, or default - conventions not followed. - - Examples: - Examples of acceptable signatures: - - >>> get_property('example') # 'example' will be returned if set, or an exception raised - >>> get_property('example', 7) # 7 returned if 'example' is not set - >>> get_property('example', default=7) # 7 returnd if 'example' is not set - - Example signatures that WILL ALWAYS RAISE AN EXCEPTION: - - >>> get_property('example', 7, 8) - >>> get_property('example', 7, default=9) - >>> get_property('example', default=7, x=9) - """ - if len(kwargs) == 1 and 'default' not in kwargs: - raise LabscriptError( - f"A call to {self.name}.get_property had a keyword argument that was " - "not name, location, or default" - ) - if len(args) + len(kwargs) > 1: - raise LabscriptError( - f"A call to {self.name}.get_property has too many arguments and/or " - "keyword arguments" - ) - - if (location is not None) and (location not in labscript_utils.properties.VALID_PROPERTY_LOCATIONS): - raise LabscriptError( - f"Device {self.name} requests invalid property read location {location}" - ) - - # self._properties may not be instantiated - if not hasattr(self, "_properties"): - self._properties = {} - - # Run through all keys of interest - for key, val in self._properties.items(): - if (location is None or key == location) and (name in val): - return val[name] - - if 'default' in kwargs: - return kwargs['default'] - elif len(args) == 1: - return args[0] - else: - raise LabscriptError( - f"The property {name} has not been set for device {self.name}" - ) - - def get_properties(self, location = None): - """ - Get all properties in location. - - Args: - location (str, optional): Location to get properties from. - If `None`, return all properties. - - Returns: - dict: Dictionary of properties. - """ - - # self._properties may not be instantiated - if not hasattr(self, "_properties"): - self._properties = {} - - if location is not None: - properties = self._properties.get(location, {}) - else: - properties = {} - for key, val in self._properties.items(): - properties.update(val) - - return properties - - def add_device(self, device): - """Adds a child device to this device. - - Args: - device (:obj:`Device`): Device to add. - - Raises: - LabscriptError: If `device` is not an allowed child of this device. - """ - if any([isinstance(device, DeviceClass) for DeviceClass in self.allowed_children]): - self.child_devices.append(device) - else: - raise LabscriptError( - f"Devices of type {device.description} cannot be attached to devices " - f"of type {self.description}." - ) - - @property - def pseudoclock_device(self): - """:obj:`PseudoclockDevice`: Stores the clocking pseudoclock, which may be itself.""" - if is_pseudoclock_device(self): - return self - parent = self.parent_device - try: - while parent is not None and not is_pseudoclock_device(parent): - parent = parent.parent_device - return parent - except Exception as e: - raise LabscriptError( - f"Couldn't find parent pseudoclock device of {self.name}, what's going " - f"on? Original error was {e}." - ) - - def quantise_to_pseudoclock(self, times): - """Quantises `times` to the resolution of the controlling pseudoclock. - - Args: - times (:obj:`numpy:numpy.ndarray` or list or set or float): Time, - in seconds, to quantise. - - Returns: - same type as `times`: Quantised times. - """ - convert_back_to = None - if not isinstance(times, ndarray): - if isinstance(times, list): - convert_back_to = list - elif isinstance(times, set): - convert_back_to = set - else: - convert_back_to = float - times = array(times) - # quantise the times to the pseudoclock clock resolution - times = (times/self.pseudoclock_device.clock_resolution).round()*self.pseudoclock_device.clock_resolution - - if convert_back_to is not None: - times = convert_back_to(times) - - return times - - @property - def parent_clock_line(self): - """:obj:`ClockLine`: Stores the clocking clockline, which may be itself.""" - if is_clock_line(self): - return self - parent = self.parent_device - try: - while not is_clock_line(parent): - parent = parent.parent_device - return parent - except Exception as e: - raise LabscriptError( - f"Couldn't find parent ClockLine of {self.name}, what's going on? " - f"Original error was {e}." - ) - - @property - def t0(self): - """float: The earliest time output can be commanded from this device at - the start of the experiment. This is nonzeo on secondary pseudoclock - devices due to triggering delays.""" - parent = self.pseudoclock_device - if parent is None or parent.is_master_pseudoclock: - return 0 - else: - return round(parent.trigger_times[0] + parent.trigger_delay, 10) - - def get_all_outputs(self): - """Get all children devices that are outputs. - - Recursively calls ``get_all_outputs()`` on each child device. ``Output``'s will - return a list containing just themselves. - - Returns: - list: List of children :obj:`Output`. - """ - all_outputs = [] - for device in self.child_devices: - all_outputs.extend(device.get_all_outputs()) - return all_outputs - - def get_all_children(self): - """Get all children devices for this device. - - Returns: - list: List of children :obj:`Device`. - """ - all_children = [] - for device in self.child_devices: - all_children.append(device) - all_children.extend(device.get_all_children()) - return all_children - - def generate_code(self, hdf5_file): - """Generate hardware instructions for device and children, then save - to h5 file. - - Will recursively call `generate_code` for all children devices. - - Args: - hdf5_file (:obj:`h5py:h5py.File`): Handle to shot file. - """ - for device in self.child_devices: - device.generate_code(hdf5_file) - - def init_device_group(self, hdf5_file): - """Creates the device group in the shot file. - - Args: - hdf5_file (:obj:`h5py:h5py.File`): File handle to - create the group in. - - Returns: - :class:`h5py:h5py.Group`: Created group handle. - """ - group = hdf5_file['/devices'].create_group(self.name) - return group - - - class IntermediateDevice(Device): """Base class for all devices that are to be clocked by a pseudoclock.""" From 994ccc6d38b8ca525d5536f4f4068ebd5f29f42c Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Wed, 3 Jan 2024 16:14:57 +1100 Subject: [PATCH 03/19] Updated core device subclasses (`Pseudoclock`, `ClockLine`, `TriggerableDevice`, `IntermediateDevice`, etc.) to more modern Python and improved some formatting. Also fixed a bug in an error message. --- labscript/{base_device.py => base.py} | 2 +- labscript/labscript.py | 385 ++++++++++++++++---------- 2 files changed, 242 insertions(+), 145 deletions(-) rename labscript/{base_device.py => base.py} (99%) diff --git a/labscript/base_device.py b/labscript/base.py similarity index 99% rename from labscript/base_device.py rename to labscript/base.py index 00f9644..139c61d 100644 --- a/labscript/base_device.py +++ b/labscript/base.py @@ -406,7 +406,7 @@ def parent_clock_line(self): @property def t0(self): """float: The earliest time output can be commanded from this device at - the start of the experiment. This is nonzeo on secondary pseudoclock + the start of the experiment. This is nonzero on secondary pseudoclock devices due to triggering delays.""" parent = self.pseudoclock_device if parent is None or parent.is_master_pseudoclock: diff --git a/labscript/labscript.py b/labscript/labscript.py index 7d02da9..98172d3 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -30,7 +30,7 @@ # The code to be removed relates to the move of the globals loading code from # labscript to runmanager batch compiler. -from .base_device import Device +from .base import Device from .compiler import compiler from .utils import LabscriptError, set_passed_properties import labscript_utils.h5_lock, h5py @@ -207,16 +207,21 @@ def __init__(self, name, parent_device, **kwargs): parent_device (:obj:`ClockLine`): Parent ClockLine device. """ self.name = name - # this should be checked here because it should only be connected a clockline - # The allowed_children attribute of parent classes doesn't prevent this from being connected to something that accepts - # an instance of 'Device' as a child + # This should be checked here because it should only be connected a clockline. + # The allowed_children attribute of parent classes doesn't prevent this from + # being connected to something that accepts an instance of 'Device' as a child if parent_device is not None and not isinstance(parent_device, ClockLine): - if not hasattr(parent_device, 'name'): - parent_device_name = 'Unknown: not an instance of a labscript device class' + if not hasattr(parent_device, "name"): + parent_device_name = "Unknown: not an instance of a labscript device class" else: parent_device_name = parent_device.name - raise LabscriptError('Error instantiating device %s. The parent (%s) must be an instance of ClockLine.'%(name, parent_device_name)) - Device.__init__(self, name, parent_device, 'internal', **kwargs) # This 'internal' should perhaps be more descriptive? + raise LabscriptError( + f"Error instantiating device {name}. The parent ({parent_device_name}) " + "must be an instance of ClockLine." + ) + + # This 'internal' (the connection name) should perhaps be more descriptive? + Device.__init__(self, name, parent_device, "internal", **kwargs) @property def minimum_clock_high_time(self): @@ -230,32 +235,40 @@ def minimum_clock_high_time(self): class ClockLine(Device): - description = 'Generic ClockLine' + description = "Generic ClockLine" allowed_children = [IntermediateDevice] _clock_limit = None _minimum_clock_high_time = 0 - + @set_passed_properties(property_names = {}) - def __init__(self, name, pseudoclock, connection, ramping_allowed = True, **kwargs): + def __init__(self, name, pseudoclock, connection, ramping_allowed=True, **kwargs): - # TODO: Verify that connection is valid connection of Pseudoclock.parent_device (the PseudoclockDevice) + # TODO: Verify that connection is valid connection of Pseudoclock.parent_device + # (the PseudoclockDevice) Device.__init__(self, name, pseudoclock, connection, **kwargs) self.ramping_allowed = ramping_allowed def add_device(self, device): Device.add_device(self, device) - if getattr(device, 'clock_limit', None) is not None and (self._clock_limit is None or device.clock_limit < self.clock_limit): + # Update clock limit if this new device is slower than all others attached + if ( + getattr(device, 'clock_limit', None) is not None + and (self._clock_limit is None or device.clock_limit < self.clock_limit) + ): self._clock_limit = device.clock_limit + # Update minimum clock high time if this new device requires a longer high time. if getattr(device, 'minimum_clock_high_time', None) is not None: - self._minimum_clock_high_time = max( - device.minimum_clock_high_time, self._minimum_clock_high_time + self.minimum_clock_high_time = max( + device.minimum_clock_high_time, self.minimum_clock_high_time ) - - # define a property to make sure no children overwrite this value themselves - # The calculation of maximum clock_limit should be done by the add_device method above @property def clock_limit(self): """float: Clock limit for this line, typically set by speed of child Intermediate Devices.""" + + # Define a property to make sure no children overwrite this value themselves. + # The calculation of maximum clock_limit should be done by the add_device method + # above + # If no child device has specified a clock limit if self._clock_limit is None: # return the Pseudoclock clock_limit @@ -266,16 +279,18 @@ def clock_limit(self): @property def minimum_clock_high_time(self): + """float: The minimum time a clock tick must be in the logical high state""" return self._minimum_clock_high_time - + class Pseudoclock(Device): """Parent class of all pseudoclocks. - You won't usually interact with this class directly, but it provides - common functionality to subclasses. + You won't usually interact with this class directly, unless you are implementing a + new PsedoclockDevice in labscript-devices. It provides common functionality for + generating pseudoclock instructions.. """ - description = 'Generic Pseudoclock' + description = "Generic Pseudoclock" allowed_children = [ClockLine] @set_passed_properties(property_names = {}) @@ -487,33 +502,22 @@ def expand_change_times(self, all_change_times, change_times, outputs_by_clockli of the clocking; self.clock. This list contains the information that facilitates programming a pseudo clock using loops.""" all_times = {} - clocks_in_use = [] - # for output in outputs: - # if output.parent_device.clock_type != 'slow clock': - # if output.parent_device.clock_type not in all_times: - # all_times[output.parent_device.clock_type] = [] - # if output.parent_device.clock_type not in clocks_in_use: - # clocks_in_use.append(output.parent_device.clock_type) - clock = [] clock_line_current_indices = {} for clock_line, outputs in outputs_by_clockline.items(): clock_line_current_indices[clock_line] = 0 all_times[clock_line] = [] - + # iterate over all change times - # for clock_line, times in change_times.items(): - # print '%s: %s'%(clock_line.name, times) for i, time in enumerate(all_change_times): if time in self.parent_device.trigger_times[1:]: # A wait instruction: - clock.append('WAIT') - + clock.append("WAIT") + # list of enabled clocks enabled_clocks = [] enabled_looping_clocks = [] - # enabled_non_looping_clocks = [] - + # update clock_line indices for clock_line in clock_line_current_indices: try: @@ -523,40 +527,47 @@ def expand_change_times(self, all_change_times, change_times, outputs_by_clockli # Fix the index to the last one clock_line_current_indices[clock_line] = len(change_times[clock_line]) - 1 # print a warning - message = ''.join(['WARNING: ClockLine %s has it\'s last change time at t=%.15f but another ClockLine has a change time at t=%.15f. '%(clock_line.name, change_times[clock_line][-1], time), - 'This should never happen, as the last change time should always be the time passed to stop(). ', - 'Perhaps you have an instruction after the stop time of the experiment?']) - sys.stderr.write(message+'\n') - + sys.stderr.write( + f"WARNING: ClockLine {clock_line.name} has it's last change " + f"time at t={change_times[clock_line][-1]:.15f} but another " + f"ClockLine has a change time at t={time:.15f}. " + "This should never happen, as the last change time should " + "always be the time passed to stop(). Perhaps you have an " + "instruction after the stop time of the experiment?" + "\n" + ) + # Let's work out which clock_lines are enabled for this instruction if time == change_times[clock_line][clock_line_current_indices[clock_line]]: enabled_clocks.append(clock_line) - + # what's the fastest clock rate? maxrate = 0 local_clock_limit = self.clock_limit # the Pseudoclock clock limit for clock_line in enabled_clocks: for output in outputs_by_clockline[clock_line]: + if not hasattr(output, "timeseries"): + continue # Check if output is sweeping and has highest clock rate # so far. If so, store its clock rate to max_rate: - if hasattr(output,'timeseries') and isinstance(output.timeseries[clock_line_current_indices[clock_line]],dict): + output_instruction = output.timeseries[ + clock_line_current_indices[clock_line] + ] + if isinstance(output_instruction, dict): if clock_line not in enabled_looping_clocks: enabled_looping_clocks.append(clock_line) - - if output.timeseries[clock_line_current_indices[clock_line]]['clock rate'] > maxrate: - # It does have the highest clock rate? Then store that rate to max_rate: - maxrate = output.timeseries[clock_line_current_indices[clock_line]]['clock rate'] - + + if output_instruction["clock rate"] > maxrate: + # It does have the highest clock rate? Then store that rate + # to max_rate: + maxrate = output_instruction["clock rate"] + # only check this for ramping clock_lines - # non-ramping clock-lines have already had the clock_limit checked within collect_change_times() + # non-ramping clock-lines have already had the clock_limit + # checked within collect_change_times() if local_clock_limit > clock_line.clock_limit: local_clock_limit = clock_line.clock_limit - - # find non-looping clocks - # for clock_line in enabled_clocks: - # if clock_line not in enabled_looping_clocks: - # enabled_non_looping_clocks.append(clock_line) - + if maxrate: # round to the nearest clock rate that the pseudoclock can actually support: period = 1/maxrate @@ -565,12 +576,15 @@ def expand_change_times(self, all_change_times, change_times, outputs_by_clockli period = quantised_period*self.clock_resolution maxrate = 1/period if maxrate > local_clock_limit: - raise LabscriptError('At t = %s sec, a clock rate of %s Hz was requested. '%(str(time),str(maxrate)) + - 'One or more devices connected to %s cannot support clock rates higher than %sHz.'%(str(self.name),str(local_clock_limit))) - + raise LabscriptError( + f"At t = {time} sec, a clock rate of {maxrate} Hz was requested. " + f"One or more devices connected to {self.name} cannot support " + f"clock rates higher than {local_clock_limit} Hz." + ) + if maxrate: # If there was ramping at this timestep, how many clock ticks fit before the next instruction? - n_ticks, remainder = divmod((all_change_times[i+1] - time)*maxrate,1) + n_ticks, remainder = divmod((all_change_times[i+1] - time)*maxrate, 1) n_ticks = int(n_ticks) # Can we squeeze the final clock cycle in at the end? if remainder and remainder/float(maxrate) >= 1/float(local_clock_limit): @@ -579,52 +593,92 @@ def expand_change_times(self, all_change_times, change_times, outputs_by_clockli # be too long, by the fraction 'remainder'. n_ticks += 1 duration = n_ticks/float(maxrate) # avoiding integer division - ticks = linspace(time,time + duration,n_ticks,endpoint=False) - + ticks = linspace(time, time + duration, n_ticks, endpoint=False) + for clock_line in enabled_clocks: if clock_line in enabled_looping_clocks: all_times[clock_line].append(ticks) else: all_times[clock_line].append(time) - + if n_ticks > 1: # If n_ticks is only one, then this step doesn't do # anything, it has reps=0. So we should only include # it if n_ticks > 1. if n_ticks > 2: - #If there is more than one clock tick here, - #then we split the ramp into an initial clock - #tick, during which the slow clock ticks, and - #the rest of the ramping time, during which the - #slow clock does not tick. - clock.append({'start': time, 'reps': 1, 'step': 1/float(maxrate), 'enabled_clocks':enabled_clocks}) - clock.append({'start': time + 1/float(maxrate), 'reps': n_ticks-2, 'step': 1/float(maxrate), 'enabled_clocks':enabled_looping_clocks}) + # If there is more than one clock tick here, then we split the + # ramp into an initial clock tick, during which the slow clock + # ticks, and the rest of the ramping time, during which the slow + # clock does not tick. + clock.append( + { + "start": time, + "reps": 1, + "step": 1/float(maxrate), + "enabled_clocks": enabled_clocks, + } + ) + clock.append( + { + "start": time + 1/float(maxrate), + "reps": n_ticks - 2, + "step": 1/float(maxrate), + "enabled_clocks": enabled_looping_clocks, + } + ) else: - clock.append({'start': time, 'reps': n_ticks-1, 'step': 1/float(maxrate), 'enabled_clocks':enabled_clocks}) - - # clock.append({'start': time, 'reps': n_ticks-1, 'step': 1/float(maxrate), 'enabled_clocks':enabled_clocks}) + clock.append( + { + "start": time, + "reps": n_ticks - 1, + "step": 1/float(maxrate), + "enabled_clocks": enabled_clocks, + } + ) + # The last clock tick has a different duration depending on the next step. - clock.append({'start': ticks[-1], 'reps': 1, 'step': all_change_times[i+1] - ticks[-1], 'enabled_clocks':enabled_clocks if n_ticks == 1 else enabled_looping_clocks}) + clock.append( + { + "start": ticks[-1], + "reps": 1, + "step": all_change_times[i+1] - ticks[-1], + "enabled_clocks": enabled_clocks + if n_ticks == 1 + else enabled_looping_clocks, + } + ) else: for clock_line in enabled_clocks: all_times[clock_line].append(time) try: # If there was no ramping, here is a single clock tick: - clock.append({'start': time, 'reps': 1, 'step': all_change_times[i+1] - time, 'enabled_clocks':enabled_clocks}) + clock.append( + { + "start": time, + "reps": 1, + "step": all_change_times[i+1] - time, + "enabled_clocks": enabled_clocks, + } + ) except IndexError: if i != len(all_change_times) - 1: raise if self.parent_device.stop_time > time: # There is no next instruction. Hold the last clock # tick until self.parent_device.stop_time. - raise Exception('This shouldn\'t happen -- stop_time should always be equal to the time of the last instruction. Please report a bug.') - # I commented this out because it is after a raised exception so never ran.... - Phil - # clock.append({'start': time, 'reps': 1, 'step': self.parent_device.stop_time - time,'slow_clock_tick':True}) + raise LabscriptError( + "This shouldn't happen -- stop_time should always be equal " + "to the time of the last instruction. Please report a bug." + ) # Error if self.parent_device.stop_time has been set to less # than the time of the last instruction: elif self.parent_device.stop_time < time: - raise LabscriptError('%s %s has more instructions (at t=%.15f) after the experiment\'s stop time (t=%.15f).'%(self.description,self.name, time, self.parent_device.stop_time)) + raise LabscriptError( + f"{self.description} {self.name} has more instructions " + f"(at t={time:.15f}) after the experiment's stop time " + f"(t={self.parent_device.stop_time:.15f})." + ) # If self.parent_device.stop_time is the same as the time of the last # instruction, then we'll get the last instruction # out still, so that the total number of clock @@ -632,18 +686,26 @@ def expand_change_times(self, all_change_times, change_times, outputs_by_clockli # Output.raw_output arrays. We'll make this last # cycle be at ten times the minimum step duration. else: - # find the slowest clock_limit + # Find the slowest clock_limit enabled_clocks = [] - local_clock_limit = 1.0/self.clock_resolution # the Pseudoclock clock limit + # The Pseudoclock clock limit + local_clock_limit = 1.0/self.clock_resolution + # Update with clock line limits for clock_line, outputs in outputs_by_clockline.items(): if local_clock_limit > clock_line.clock_limit: local_clock_limit = clock_line.clock_limit enabled_clocks.append(clock_line) - clock.append({'start': time, 'reps': 1, 'step': 10.0/self.clock_limit, 'enabled_clocks':enabled_clocks}) - # for row in clock: - # print row + clock.append( + { + "start": time, + "reps": 1, + "step": 10.0/self.clock_limit, + "enabled_clocks": enabled_clocks, + } + ) + return all_times, clock - + def get_outputs_by_clockline(self): """Obtain all outputs by clockline. @@ -660,7 +722,8 @@ def get_outputs_by_clockline(self): all_outputs = self.get_all_outputs() for output in all_outputs: - # TODO: Make this a bit more robust (can we assume things always have this hierarchy?) + # TODO: Make this a bit more robust (can we assume things always have this + # hierarchy?) clock_line = output.parent_clock_line assert clock_line.parent_device == self outputs_by_clockline[clock_line].append(output) @@ -672,10 +735,10 @@ def generate_clock(self): of the clock. """ all_outputs, outputs_by_clockline = self.get_outputs_by_clockline() - + # Get change_times for all outputs, and also grouped by clockline all_change_times, change_times = self.collect_change_times(all_outputs, outputs_by_clockline) - + # for each clock line for clock_line, clock_line_change_times in change_times.items(): # and for each output on the clockline @@ -685,12 +748,14 @@ def generate_clock(self): # now generate the clock meta data for the Pseudoclock # also generate everytime point each clock line will tick (expand ramps) - all_times, self.clock = self.expand_change_times(all_change_times, change_times, outputs_by_clockline) - + all_times, self.clock = self.expand_change_times( + all_change_times, change_times, outputs_by_clockline + ) + # Flatten the clock line times for use by the child devices for writing instruction tables self.times = {} for clock_line, time_array in all_times.items(): - self.times[clock_line] = fastflatten(time_array,np.dtype(float)) + self.times[clock_line] = fastflatten(time_array, np.dtype(float)) # for each clockline for clock_line, outputs in outputs_by_clockline.items(): @@ -699,11 +764,11 @@ def generate_clock(self): for output in outputs: # evaluate the output at each time point the clock line will tick at output.expand_timeseries(all_times[clock_line], clock_line_len) - + def generate_code(self, hdf5_file): self.generate_clock() Device.generate_code(self, hdf5_file) - + class TriggerableDevice(Device): """A triggerable version of :obj:`Device`. @@ -712,11 +777,11 @@ class TriggerableDevice(Device): pseudoclock, but do require a trigger. This enables them to have a Trigger device as a parent. """ - trigger_edge_type = 'rising' + trigger_edge_type = "rising" """str: Type of trigger. Must be `'rising'` or `'falling'`.""" minimum_recovery_time = 0 """float: Minimum time required before another trigger can occur.""" - + @set_passed_properties(property_names = {}) def __init__(self, name, parent_device, connection, parentless=False, **kwargs): """Instantiate a Triggerable Device. @@ -733,18 +798,25 @@ def __init__(self, name, parent_device, connection, parentless=False, **kwargs): the trigger type of the parent Trigger. """ if None in [parent_device, connection] and not parentless: - raise LabscriptError('No parent specified. If this device does not require a parent, set parentless=True') + raise LabscriptError( + "No parent specified. If this device does not require a parent, set " + "parentless=True" + ) if isinstance(parent_device, Trigger): if self.trigger_edge_type != parent_device.trigger_edge_type: - raise LabscriptError('Trigger edge type for %s is \'%s\', ' % (name, self.trigger_edge_type) + - 'but existing Trigger object %s ' % parent_device.name + - 'has edge type \'%s\'' % parent_device.trigger_edge_type) + raise LabscriptError( + f"Trigger edge type for {name} is {self.trigger_edge_type}', but " + f"existing Trigger object {parent_device.name} has edge type " + f"'{parent_device.trigger_edge_type}'" + ) self.trigger_device = parent_device elif parent_device is not None: # Instantiate a trigger object to be our parent: - self.trigger_device = Trigger(name + '_trigger', parent_device, connection, self.trigger_edge_type) + self.trigger_device = Trigger( + f"{name}_trigger", parent_device, connection, self.trigger_edge_type + ) parent_device = self.trigger_device - connection = 'trigger' + connection = "trigger" self.__triggers = [] Device.__init__(self, name, parent_device, connection, **kwargs) @@ -778,19 +850,13 @@ def trigger(self, t, duration): 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( + f"{self.description} {self.name} has two triggers closer together " + f"than the minimum recovery time: one at t = {start:.15f}s for " + f"{duration}s, and another at t = {other_start:.15f}s for " + f"{other_duration}s. " + f"The minimum recovery time is {self.minimum_recovery_time}s." ) - raise ValueError(dedent(msg)) self.__triggers.append([t, duration]) @@ -807,21 +873,24 @@ def do_checks(self): for trigger in self.__triggers: if trigger not in device.__triggers: start, duration = trigger - raise LabscriptError('TriggerableDevices %s and %s share a trigger. ' % (self.name, device.name) + - '%s has a trigger at %fs for %fs, ' % (self.name, start, duration) + - 'but there is no matching trigger for %s. ' % device.name + - 'Devices sharing a trigger must have identical trigger times and durations.') + raise LabscriptError( + f"TriggerableDevices {self.name} and {device.name} share a " + f"trigger. {self.name} has a trigger at {start}s for " + f"{duration}s, but there is no matching trigger for " + f"{device.name}. Devices sharing a trigger must have " + "identical trigger times and durations." + ) def generate_code(self, hdf5_file): self.do_checks() Device.generate_code(self, hdf5_file) - + class PseudoclockDevice(TriggerableDevice): """Device that implements a pseudoclock.""" - description = 'Generic Pseudoclock Device' + description = "Generic Pseudoclock Device" allowed_children = [Pseudoclock] - trigger_edge_type = 'rising' + trigger_edge_type = "rising" # How long after a trigger the next instruction is actually output: trigger_delay = 0 # How long a trigger line must remain high/low in order to be detected: @@ -844,26 +913,40 @@ def __init__(self, name, trigger_device=None, trigger_connection=None, **kwargs) if trigger_device is None: for device in compiler.inventory: if isinstance(device, PseudoclockDevice) and device.is_master_pseudoclock: - raise LabscriptError('There is already a master pseudoclock device: %s.'%device.name + - 'There cannot be multiple master pseudoclock devices - please provide a trigger_device for one of them.') - TriggerableDevice.__init__(self, name, parent_device=None, connection=None, parentless=True, **kwargs) + raise LabscriptError( + f"There is already a master pseudoclock device: {device.name}. " + "There cannot be multiple master pseudoclock devices - please " + "provide a trigger_device for one of them." + ) + TriggerableDevice.__init__( + self, name, parent_device=None, connection=None, parentless=True, **kwargs + ) else: # The parent device declared was a digital output channel: the following will # automatically instantiate a Trigger for it and set it as self.trigger_device: - TriggerableDevice.__init__(self, name, parent_device=trigger_device, connection=trigger_connection, **kwargs) + TriggerableDevice.__init__( + self, + name, + parent_device=trigger_device, + connection=trigger_connection, + **kwargs + ) # Ensure that the parent pseudoclock device is, in fact, the master pseudoclock device. if not self.trigger_device.pseudoclock_device.is_master_pseudoclock: - raise LabscriptError('All secondary pseudoclock devices must be triggered by a device being clocked by the master pseudoclock device.' + - 'Pseudoclocks triggering each other in series is not supported.') + raise LabscriptError( + "All secondary pseudoclock devices must be triggered by a device " + "being clocked by the master pseudoclock device. Pseudoclocks " + "triggering each other in series is not supported." + ) self.trigger_times = [] self.wait_times = [] self.initial_trigger_time = 0 - + @property def is_master_pseudoclock(self): """bool: Whether this device is the master pseudoclock.""" return self.parent_device is None - + def set_initial_trigger_time(self, t): """Sets the initial trigger time of the pseudoclock. @@ -874,9 +957,14 @@ def set_initial_trigger_time(self, t): """ t = round(t,10) if compiler.start_called: - raise LabscriptError('Initial trigger times must be set prior to calling start()') + raise LabscriptError( + "Initial trigger times must be set prior to calling start()" + ) if self.is_master_pseudoclock: - raise LabscriptError('Initial trigger time of master clock is always zero, it cannot be changed.') + raise LabscriptError( + "Initial trigger time of master clock is always zero, it cannot be " + "changed." + ) else: self.initial_trigger_time = t @@ -889,18 +977,19 @@ def trigger(self, t, duration, wait_delay = 0): duration (float): Duration, in seconds, of the trigger pulse. wait_delay (float, optional): Time, in seconds, to delay the trigger. """ - if type(t) in [str, bytes] and t == 'initial': + if isinstance(t, str) and t == "initial": t = self.initial_trigger_time - t = round(t,10) + t = round(t, 10) if self.is_master_pseudoclock: if compiler.wait_monitor is not None: - # Make the wait monitor pulse to signify starting or resumption of the experiment: + # Make the wait monitor pulse to signify starting or resumption of the + # experiment: compiler.wait_monitor.trigger(t, duration) self.trigger_times.append(t) else: TriggerableDevice.trigger(self, t, duration) - self.trigger_times.append(round(t + wait_delay,10)) - + self.trigger_times.append(round(t + wait_delay, 10)) + def do_checks(self, outputs): """Basic error checking to ensure the user's instructions make sense. @@ -909,7 +998,7 @@ def do_checks(self, outputs): """ for output in outputs: output.do_checks(self.trigger_times) - + def offset_instructions_from_trigger(self, outputs): """Offset instructions for child devices by the appropriate trigger times. @@ -918,27 +1007,35 @@ def offset_instructions_from_trigger(self, outputs): """ for output in outputs: output.offset_instructions_from_trigger(self.trigger_times) - + if not self.is_master_pseudoclock: # Store the unmodified initial_trigger_time initial_trigger_time = self.trigger_times[0] # Adjust the stop time relative to the last trigger time - self.stop_time = round(self.stop_time - initial_trigger_time - self.trigger_delay * len(self.trigger_times),10) - + self.stop_time = round( + self.stop_time + - initial_trigger_time + - self.trigger_delay * len(self.trigger_times), + 10 + ) + # Modify the trigger times themselves so that we insert wait instructions at the right times: - self.trigger_times = [round(t - initial_trigger_time - i*self.trigger_delay,10) for i, t in enumerate(self.trigger_times)] - + self.trigger_times = [ + round(t - initial_trigger_time - i*self.trigger_delay, 10) + for i, t in enumerate(self.trigger_times) + ] + # quantise the trigger times and stop time to the pseudoclock clock resolution self.trigger_times = self.quantise_to_pseudoclock(self.trigger_times) self.stop_time = self.quantise_to_pseudoclock(self.stop_time) - + def generate_code(self, hdf5_file): outputs = self.get_all_outputs() self.do_checks(outputs) self.offset_instructions_from_trigger(outputs) Device.generate_code(self, hdf5_file) - - + + class Output(Device): """Base class for all output classes.""" description = 'generic output' From 6e438727738198ca4a64e1b5696bf42bad442d99 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Wed, 3 Jan 2024 16:31:23 +1100 Subject: [PATCH 04/19] Moved core classes into their own files. Also moved some utils functions out of labscript.py. --- labscript/core.py | 854 +++++++++++++++++++++++++++++++++++++ labscript/labscript.py | 932 +---------------------------------------- labscript/utils.py | 84 +++- 3 files changed, 950 insertions(+), 920 deletions(-) create mode 100644 labscript/core.py diff --git a/labscript/core.py b/labscript/core.py new file mode 100644 index 0000000..a906d1a --- /dev/null +++ b/labscript/core.py @@ -0,0 +1,854 @@ +import sys + +import numpy as np + +from .base import Device +from .compiler import compiler +from .utils import LabscriptError, fastflatten, set_passed_properties + + +class IntermediateDevice(Device): + """Base class for all devices that are to be clocked by a pseudoclock.""" + + @set_passed_properties(property_names = {}) + def __init__(self, name, parent_device, **kwargs): + """Provides some error checking to ensure parent_device + is a :obj:`ClockLine`. + + Calls :func:`Device.__init__`. + + Args: + name (str): python variable name to assign to device + parent_device (:obj:`ClockLine`): Parent ClockLine device. + """ + self.name = name + # This should be checked here because it should only be connected a clockline. + # The allowed_children attribute of parent classes doesn't prevent this from + # being connected to something that accepts an instance of 'Device' as a child + if parent_device is not None and not isinstance(parent_device, ClockLine): + if not hasattr(parent_device, "name"): + parent_device_name = "Unknown: not an instance of a labscript device class" + else: + parent_device_name = parent_device.name + raise LabscriptError( + f"Error instantiating device {name}. The parent ({parent_device_name}) " + "must be an instance of ClockLine." + ) + + # This 'internal' (the connection name) should perhaps be more descriptive? + Device.__init__(self, name, parent_device, "internal", **kwargs) + + @property + def minimum_clock_high_time(self): + if getattr(self, "clock_limit", None) is None: + return 0 + + # Convert clock limit to minimum pulse period and then divide by 2 to + # get minimum half period. This is the fastest assuming the minimum high + # time corresponds to half the fastest clock pulse supported. + return 1/self.clock_limit/2 + + +class ClockLine(Device): + description = "Generic ClockLine" + allowed_children = [IntermediateDevice] + _clock_limit = None + _minimum_clock_high_time = 0 + + @set_passed_properties(property_names = {}) + def __init__(self, name, pseudoclock, connection, ramping_allowed=True, **kwargs): + + # TODO: Verify that connection is valid connection of Pseudoclock.parent_device + # (the PseudoclockDevice) + Device.__init__(self, name, pseudoclock, connection, **kwargs) + self.ramping_allowed = ramping_allowed + + def add_device(self, device): + Device.add_device(self, device) + # Update clock limit if this new device is slower than all others attached + if ( + getattr(device, 'clock_limit', None) is not None + and (self._clock_limit is None or device.clock_limit < self.clock_limit) + ): + self._clock_limit = device.clock_limit + # Update minimum clock high time if this new device requires a longer high time. + if getattr(device, 'minimum_clock_high_time', None) is not None: + self.minimum_clock_high_time = max( + device.minimum_clock_high_time, self.minimum_clock_high_time + ) + @property + def clock_limit(self): + """float: Clock limit for this line, typically set by speed of child Intermediate Devices.""" + + # Define a property to make sure no children overwrite this value themselves. + # The calculation of maximum clock_limit should be done by the add_device method + # above + + # If no child device has specified a clock limit + if self._clock_limit is None: + # return the Pseudoclock clock_limit + # TODO: Maybe raise an error instead? + # Maybe all Intermediate devices should be required to have a clock_limit? + return self.parent_device.clock_limit + return self._clock_limit + + @property + def minimum_clock_high_time(self): + """float: The minimum time a clock tick must be in the logical high state""" + return self._minimum_clock_high_time + + +class Pseudoclock(Device): + """Parent class of all pseudoclocks. + + You won't usually interact with this class directly, unless you are implementing a + new PsedoclockDevice in labscript-devices. It provides common functionality for + generating pseudoclock instructions.. + """ + description = "Generic Pseudoclock" + allowed_children = [ClockLine] + + @set_passed_properties(property_names = {}) + def __init__(self, name, pseudoclock_device, connection, **kwargs): + """Creates a Pseudoclock. + + Args: + name (str): python variable name to assign the device instance to. + pseudoclock_device (:obj:`PseudoclockDevice`): Parent pseudoclock device + connection (str): Connection on this device that links to parent + **kwargs: Passed to `Device()`. + """ + Device.__init__(self, name, pseudoclock_device, connection, **kwargs) + self.clock_limit = pseudoclock_device.clock_limit + self.clock_resolution = pseudoclock_device.clock_resolution + + def add_device(self, device): + Device.add_device(self, device) + #TODO: Maybe verify here that device.connection (the ClockLine connection) is a valid connection of the parent PseudoClockDevice + # Also see the same comment in ClockLine.__init__ + # if device.connection not in self.clock_lines: + # self.clock_lines[ + + def collect_change_times(self, all_outputs, outputs_by_clockline): + """Asks all connected outputs for a list of times that they + change state. + + Takes the union of all of these times. Note + that at this point, a change from holding-a-constant-value + to ramping-through-values is considered a single state + change. The clocking times will be filled in later in the + expand_change_times function, and the ramp values filled in with + expand_timeseries. + + Args: + all_outputs (list): List of all outputs connected to this + pseudoclock. + outputs_by_clockline (dict): List of all outputs connected + to this pseudoclock, organized by clockline. + + Returns: + tuple: Tuple containing: + + - **all_change_times** (list): List of all change times. + - **change_times** (dict): Dictionary of all change times + organised by which clock they are attached to. + """ + change_times = {} + all_change_times = [] + ramps_by_clockline = {} + for clock_line, outputs in outputs_by_clockline.items(): + change_times.setdefault(clock_line, []) + ramps_by_clockline.setdefault(clock_line, []) + for output in outputs: + # print 'output name: %s'%output.name + output_change_times = output.get_change_times() + # print output_change_times + change_times[clock_line].extend(output_change_times) + all_change_times.extend(output_change_times) + ramps_by_clockline[clock_line].extend(output.get_ramp_times()) + + # Change to a set and back to get rid of duplicates: + if not all_change_times: + all_change_times.append(0) + all_change_times.append(self.parent_device.stop_time) + # include trigger times in change_times, so that pseudoclocks always + # have an instruction immediately following a wait: + all_change_times.extend(self.parent_device.trigger_times) + + ######################################################################## + # Find out whether any other clockline has a change time during a ramp # + # on another clockline. If it does, we need to let the ramping # + # clockline know it needs to break it's loop at that time # + ######################################################################## + # convert all_change_times to a numpy array + all_change_times_numpy = np.array(all_change_times) + + # quantise the all change times to the pseudoclock clock resolution + all_change_times_numpy = self.quantise_to_pseudoclock( + all_change_times_numpy + ) + + # Loop through each clockline + for clock_line, ramps in ramps_by_clockline.items(): + # for each clockline, loop through the ramps on that clockline + for ramp_start_time, ramp_end_time in ramps: + # for each ramp, check to see if there is a change time in + # all_change_times which intersects with the ramp. If there is, + # add a change time into this clockline at that point + indices = np.where( + (ramp_start_time < all_change_times_numpy) & + (all_change_times_numpy < ramp_end_time) + ) + for idx in indices[0]: + change_times[clock_line].append(all_change_times_numpy[idx]) + + # Get rid of duplicates: + all_change_times = list(set(all_change_times_numpy)) + all_change_times.sort() + + # Check that the pseudoclock can handle updates this fast + for i, t in enumerate(all_change_times[:-1]): + dt = all_change_times[i+1] - t + if dt < 1.0/self.clock_limit: + raise LabscriptError( + "Commands have been issued to devices attached to " + f"{self.name} at t={t} and t={all_change_times[i+1]}. " + "This Pseudoclock cannot support update delays shorter " + f"than {1.0/self.clock_limit} seconds." + ) + + ######################################################################## + # For each clockline, make sure we have a change time for triggers, # + # stop_time, t=0 and check that no change times are too close together # + ######################################################################## + for clock_line, change_time_list in change_times.items(): + # include trigger times in change_times, so that pseudoclocks always + # have an instruction immediately following a wait: + change_time_list.extend(self.parent_device.trigger_times) + + # If the device has no children, we still need it to have a + # single instruction. So we'll add 0 as a change time: + if not change_time_list: + change_time_list.append(0) + + # quantise the all change times to the pseudoclock clock resolution + change_time_list = self.quantise_to_pseudoclock(change_time_list) + + # Get rid of duplicates if trigger times were already in the list: + change_time_list = list(set(change_time_list)) + change_time_list.sort() + + # Also add the stop time as as change time. First check that it + # isn't too close to the time of the last instruction: + if not self.parent_device.stop_time in change_time_list: + dt = self.parent_device.stop_time - change_time_list[-1] + if abs(dt) < 1.0/clock_line.clock_limit: + raise LabscriptError( + "The stop time of the experiment is " + f"t={self.parent_device.stop_time}, but the last " + f"instruction for a device attached to {self.name} is " + f"at t={change_time_list[-1]}. One or more connected " + "devices cannot support update delays shorter than " + f"{1.0/clock_line.clock_limit} seconds. Please set the " + "stop_time a bit later." + ) + + change_time_list.append(self.parent_device.stop_time) + + # Sort change times so self.stop_time will be in the middle + # somewhere if it is prior to the last actual instruction. + # Whilst this means the user has set stop_time in error, not + # catching the error here allows it to be caught later by the + # specific device that has more instructions after + # self.stop_time. Thus we provide the user with sligtly more + # detailed error info. + change_time_list.sort() + + # index to keep track of in all_change_times + j = 0 + # Check that no two instructions are too close together: + for i, t in enumerate(change_time_list[:-1]): + dt = change_time_list[i+1] - t + if dt < 1.0/clock_line.clock_limit: + raise LabscriptError( + "Commands have been issued to devices attached to " + f"clockline {clock_line.name} at t={t} and " + f"t={change_time_list[i+1]}. One or more connected " + f"devices on ClockLine {clock_line.name} cannot " + "support update delays shorter than " + f"{1.0/clock_line.clock_limit} seconds" + ) + + all_change_times_len = len(all_change_times) + # increment j until we reach the current time + while all_change_times[j] < t and j < all_change_times_len-1: + j += 1 + # j should now index all_change_times at "t" + # Check that the next all change_time is not too close (and thus + # would force this clock tick to be faster than the minimum + # clock high time) + dt = all_change_times[j+1] - t + if dt < (2 * clock_line.minimum_clock_high_time): + raise LabscriptError( + "Commands have been issued to devices attached to " + f"{self.name} at t={t} and t={all_change_times[j+1]}. " + "One or more connected devices on ClockLine " + f"{clock_line.name} cannot support clock ticks with a " + "digital high time shorter than " + f"{clock_line.minimum_clock_high_time} which is more " + "than half the available time between the event at " + f"t={t} on ClockLine {clock_line.name} and the next " + "event on another ClockLine." + ) + + # because we made the list into a set and back to a list, it is now + # a different object so modifying it won't update the list in the + # dictionary. So store the updated list in the dictionary + change_times[clock_line] = change_time_list + return all_change_times, change_times + + def expand_change_times(self, all_change_times, change_times, outputs_by_clockline): + """For each time interval delimited by change_times, constructs + an array of times at which the clock for this device needs to + tick. If the interval has all outputs having constant values, + then only the start time is stored. If one or more outputs are + ramping, then the clock ticks at the maximum clock rate requested + by any of the outputs. Also produces a higher level description + of the clocking; self.clock. This list contains the information + that facilitates programming a pseudo clock using loops.""" + all_times = {} + clock = [] + clock_line_current_indices = {} + for clock_line, outputs in outputs_by_clockline.items(): + clock_line_current_indices[clock_line] = 0 + all_times[clock_line] = [] + + # iterate over all change times + for i, time in enumerate(all_change_times): + if time in self.parent_device.trigger_times[1:]: + # A wait instruction: + clock.append("WAIT") + + # list of enabled clocks + enabled_clocks = [] + enabled_looping_clocks = [] + + # update clock_line indices + for clock_line in clock_line_current_indices: + try: + while change_times[clock_line][clock_line_current_indices[clock_line]] < time: + clock_line_current_indices[clock_line] += 1 + except IndexError: + # Fix the index to the last one + clock_line_current_indices[clock_line] = len(change_times[clock_line]) - 1 + # print a warning + sys.stderr.write( + f"WARNING: ClockLine {clock_line.name} has it's last change " + f"time at t={change_times[clock_line][-1]:.15f} but another " + f"ClockLine has a change time at t={time:.15f}. " + "This should never happen, as the last change time should " + "always be the time passed to stop(). Perhaps you have an " + "instruction after the stop time of the experiment?" + "\n" + ) + + # Let's work out which clock_lines are enabled for this instruction + if time == change_times[clock_line][clock_line_current_indices[clock_line]]: + enabled_clocks.append(clock_line) + + # what's the fastest clock rate? + maxrate = 0 + local_clock_limit = self.clock_limit # the Pseudoclock clock limit + for clock_line in enabled_clocks: + for output in outputs_by_clockline[clock_line]: + if not hasattr(output, "timeseries"): + continue + # Check if output is sweeping and has highest clock rate + # so far. If so, store its clock rate to max_rate: + output_instruction = output.timeseries[ + clock_line_current_indices[clock_line] + ] + if isinstance(output_instruction, dict): + if clock_line not in enabled_looping_clocks: + enabled_looping_clocks.append(clock_line) + + if output_instruction["clock rate"] > maxrate: + # It does have the highest clock rate? Then store that rate + # to max_rate: + maxrate = output_instruction["clock rate"] + + # only check this for ramping clock_lines + # non-ramping clock-lines have already had the clock_limit + # checked within collect_change_times() + if local_clock_limit > clock_line.clock_limit: + local_clock_limit = clock_line.clock_limit + + if maxrate: + # round to the nearest clock rate that the pseudoclock can actually support: + period = 1/maxrate + quantised_period = period/self.clock_resolution + quantised_period = round(quantised_period) + period = quantised_period*self.clock_resolution + maxrate = 1/period + if maxrate > local_clock_limit: + raise LabscriptError( + f"At t = {time} sec, a clock rate of {maxrate} Hz was requested. " + f"One or more devices connected to {self.name} cannot support " + f"clock rates higher than {local_clock_limit} Hz." + ) + + if maxrate: + # If there was ramping at this timestep, how many clock ticks fit before the next instruction? + n_ticks, remainder = divmod((all_change_times[i+1] - time)*maxrate, 1) + n_ticks = int(n_ticks) + # Can we squeeze the final clock cycle in at the end? + if remainder and remainder/float(maxrate) >= 1/float(local_clock_limit): + # Yes we can. Clock speed will be as + # requested. Otherwise the final clock cycle will + # be too long, by the fraction 'remainder'. + n_ticks += 1 + duration = n_ticks/float(maxrate) # avoiding integer division + ticks = np.linspace(time, time + duration, n_ticks, endpoint=False) + + for clock_line in enabled_clocks: + if clock_line in enabled_looping_clocks: + all_times[clock_line].append(ticks) + else: + all_times[clock_line].append(time) + + if n_ticks > 1: + # If n_ticks is only one, then this step doesn't do + # anything, it has reps=0. So we should only include + # it if n_ticks > 1. + if n_ticks > 2: + # If there is more than one clock tick here, then we split the + # ramp into an initial clock tick, during which the slow clock + # ticks, and the rest of the ramping time, during which the slow + # clock does not tick. + clock.append( + { + "start": time, + "reps": 1, + "step": 1/float(maxrate), + "enabled_clocks": enabled_clocks, + } + ) + clock.append( + { + "start": time + 1/float(maxrate), + "reps": n_ticks - 2, + "step": 1/float(maxrate), + "enabled_clocks": enabled_looping_clocks, + } + ) + else: + clock.append( + { + "start": time, + "reps": n_ticks - 1, + "step": 1/float(maxrate), + "enabled_clocks": enabled_clocks, + } + ) + + # The last clock tick has a different duration depending on the next step. + clock.append( + { + "start": ticks[-1], + "reps": 1, + "step": all_change_times[i+1] - ticks[-1], + "enabled_clocks": enabled_clocks + if n_ticks == 1 + else enabled_looping_clocks, + } + ) + else: + for clock_line in enabled_clocks: + all_times[clock_line].append(time) + + try: + # If there was no ramping, here is a single clock tick: + clock.append( + { + "start": time, + "reps": 1, + "step": all_change_times[i+1] - time, + "enabled_clocks": enabled_clocks, + } + ) + except IndexError: + if i != len(all_change_times) - 1: + raise + if self.parent_device.stop_time > time: + # There is no next instruction. Hold the last clock + # tick until self.parent_device.stop_time. + raise LabscriptError( + "This shouldn't happen -- stop_time should always be equal " + "to the time of the last instruction. Please report a bug." + ) + # Error if self.parent_device.stop_time has been set to less + # than the time of the last instruction: + elif self.parent_device.stop_time < time: + raise LabscriptError( + f"{self.description} {self.name} has more instructions " + f"(at t={time:.15f}) after the experiment's stop time " + f"(t={self.parent_device.stop_time:.15f})." + ) + # If self.parent_device.stop_time is the same as the time of the last + # instruction, then we'll get the last instruction + # out still, so that the total number of clock + # ticks matches the number of data points in the + # Output.raw_output arrays. We'll make this last + # cycle be at ten times the minimum step duration. + else: + # Find the slowest clock_limit + enabled_clocks = [] + # The Pseudoclock clock limit + local_clock_limit = 1.0/self.clock_resolution + # Update with clock line limits + for clock_line, outputs in outputs_by_clockline.items(): + if local_clock_limit > clock_line.clock_limit: + local_clock_limit = clock_line.clock_limit + enabled_clocks.append(clock_line) + clock.append( + { + "start": time, + "reps": 1, + "step": 10.0/self.clock_limit, + "enabled_clocks": enabled_clocks, + } + ) + + return all_times, clock + + def get_outputs_by_clockline(self): + """Obtain all outputs by clockline. + + Returns: + tuple: Tuple containing: + + - **all_outputs** (list): List of all outputs, obtained from :meth:`get_all_outputs`. + - **outputs_by_clockline** (dict): Dictionary of outputs, organised by clockline. + """ + outputs_by_clockline = {} + for clock_line in self.child_devices: + if isinstance(clock_line, ClockLine): + outputs_by_clockline[clock_line] = [] + + all_outputs = self.get_all_outputs() + for output in all_outputs: + # TODO: Make this a bit more robust (can we assume things always have this + # hierarchy?) + clock_line = output.parent_clock_line + assert clock_line.parent_device == self + outputs_by_clockline[clock_line].append(output) + + return all_outputs, outputs_by_clockline + + def generate_clock(self): + """Generate the pseudoclock and configure outputs for each tick + of the clock. + """ + all_outputs, outputs_by_clockline = self.get_outputs_by_clockline() + + # Get change_times for all outputs, and also grouped by clockline + all_change_times, change_times = self.collect_change_times(all_outputs, outputs_by_clockline) + + # for each clock line + for clock_line, clock_line_change_times in change_times.items(): + # and for each output on the clockline + for output in outputs_by_clockline[clock_line]: + # call make_timeseries to expand the list of instructions for each change_time on this clock line + output.make_timeseries(clock_line_change_times) + + # now generate the clock meta data for the Pseudoclock + # also generate everytime point each clock line will tick (expand ramps) + all_times, self.clock = self.expand_change_times( + all_change_times, change_times, outputs_by_clockline + ) + + # Flatten the clock line times for use by the child devices for writing instruction tables + self.times = {} + for clock_line, time_array in all_times.items(): + self.times[clock_line] = fastflatten(time_array, np.dtype(float)) + + # for each clockline + for clock_line, outputs in outputs_by_clockline.items(): + clock_line_len = len(self.times[clock_line]) + # and for each output + for output in outputs: + # evaluate the output at each time point the clock line will tick at + output.expand_timeseries(all_times[clock_line], clock_line_len) + + def generate_code(self, hdf5_file): + self.generate_clock() + Device.generate_code(self, hdf5_file) + + +class TriggerableDevice(Device): + """A triggerable version of :obj:`Device`. + + This class is for devices that do not require a + pseudoclock, but do require a trigger. This enables + them to have a Trigger device as a parent. + """ + trigger_edge_type = "rising" + """str: Type of trigger. Must be `'rising'` or `'falling'`.""" + minimum_recovery_time = 0 + """float: Minimum time required before another trigger can occur.""" + + @set_passed_properties(property_names = {}) + def __init__(self, name, parent_device, connection, parentless=False, **kwargs): + """Instantiate a Triggerable Device. + + Args: + name (str): + parent_device (): + connection (str): + parentless (bool, optional): + **kwargs: Passed to :meth:`Device.__init__`. + + Raises: + LabscriptError: If trigger type of this device does not match + the trigger type of the parent Trigger. + """ + from .labscript import Trigger + + if None in [parent_device, connection] and not parentless: + raise LabscriptError( + "No parent specified. If this device does not require a parent, set " + "parentless=True" + ) + if isinstance(parent_device, Trigger): + if self.trigger_edge_type != parent_device.trigger_edge_type: + raise LabscriptError( + f"Trigger edge type for {name} is {self.trigger_edge_type}', but " + f"existing Trigger object {parent_device.name} has edge type " + f"'{parent_device.trigger_edge_type}'" + ) + self.trigger_device = parent_device + elif parent_device is not None: + # Instantiate a trigger object to be our parent: + self.trigger_device = Trigger( + f"{name}_trigger", parent_device, connection, self.trigger_edge_type + ) + parent_device = self.trigger_device + connection = "trigger" + + self.__triggers = [] + Device.__init__(self, name, parent_device, connection, **kwargs) + + def trigger(self, t, duration): + """Request parent trigger device to produce a trigger. + + Args: + t (float): Time, in seconds, to produce a trigger. + duration (float): Duration, in seconds, of the trigger pulse. + """ + # 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 + ): + raise ValueError( + f"{self.description} {self.name} has two triggers closer together " + f"than the minimum recovery time: one at t = {start:.15f}s for " + f"{duration}s, and another at t = {other_start:.15f}s for " + f"{other_duration}s. " + f"The minimum recovery time is {self.minimum_recovery_time}s." + ) + + self.__triggers.append([t, duration]) + + def do_checks(self): + """Check that all devices sharing a trigger device have triggers when + this device has a trigger. + + Raises: + LabscriptError: If correct triggers do not exist for all devices. + """ + # Check that all devices sharing a trigger device have triggers when we have triggers: + for device in self.trigger_device.child_devices: + if device is not self: + for trigger in self.__triggers: + if trigger not in device.__triggers: + start, duration = trigger + raise LabscriptError( + f"TriggerableDevices {self.name} and {device.name} share a " + f"trigger. {self.name} has a trigger at {start}s for " + f"{duration}s, but there is no matching trigger for " + f"{device.name}. Devices sharing a trigger must have " + "identical trigger times and durations." + ) + + def generate_code(self, hdf5_file): + self.do_checks() + Device.generate_code(self, hdf5_file) + + +class PseudoclockDevice(TriggerableDevice): + """Device that implements a pseudoclock.""" + description = "Generic Pseudoclock Device" + allowed_children = [Pseudoclock] + trigger_edge_type = "rising" + # How long after a trigger the next instruction is actually output: + trigger_delay = 0 + # How long a trigger line must remain high/low in order to be detected: + trigger_minimum_duration = 0 + # How long after the start of a wait instruction the device is actually capable of resuming: + wait_delay = 0 + + @set_passed_properties(property_names = {}) + def __init__(self, name, trigger_device=None, trigger_connection=None, **kwargs): + """Instantiates a pseudoclock device. + + Args: + name (str): python variable to assign to this device. + trigger_device (:obj:`DigitalOut`): Sets the parent triggering output. + If `None`, this is considered the master pseudoclock. + trigger_connection (str, optional): Must be provided if `trigger_device` is + provided. Specifies the channel of the parent device. + **kwargs: Passed to :meth:`TriggerableDevice.__init__`. + """ + if trigger_device is None: + for device in compiler.inventory: + if isinstance(device, PseudoclockDevice) and device.is_master_pseudoclock: + raise LabscriptError( + f"There is already a master pseudoclock device: {device.name}. " + "There cannot be multiple master pseudoclock devices - please " + "provide a trigger_device for one of them." + ) + TriggerableDevice.__init__( + self, name, parent_device=None, connection=None, parentless=True, **kwargs + ) + else: + # The parent device declared was a digital output channel: the following will + # automatically instantiate a Trigger for it and set it as self.trigger_device: + TriggerableDevice.__init__( + self, + name, + parent_device=trigger_device, + connection=trigger_connection, + **kwargs + ) + # Ensure that the parent pseudoclock device is, in fact, the master pseudoclock device. + if not self.trigger_device.pseudoclock_device.is_master_pseudoclock: + raise LabscriptError( + "All secondary pseudoclock devices must be triggered by a device " + "being clocked by the master pseudoclock device. Pseudoclocks " + "triggering each other in series is not supported." + ) + self.trigger_times = [] + self.wait_times = [] + self.initial_trigger_time = 0 + + @property + def is_master_pseudoclock(self): + """bool: Whether this device is the master pseudoclock.""" + return self.parent_device is None + + def set_initial_trigger_time(self, t): + """Sets the initial trigger time of the pseudoclock. + + If this is the master pseudoclock, time must be 0. + + Args: + t (float): Time, in seconds, to trigger this device. + """ + t = round(t,10) + if compiler.start_called: + raise LabscriptError( + "Initial trigger times must be set prior to calling start()" + ) + if self.is_master_pseudoclock: + raise LabscriptError( + "Initial trigger time of master clock is always zero, it cannot be " + "changed." + ) + else: + self.initial_trigger_time = t + + def trigger(self, t, duration, wait_delay = 0): + """Ask the trigger device to produce a digital pulse of a given duration + to trigger this pseudoclock. + + Args: + t (float): Time, in seconds, to trigger this device. + duration (float): Duration, in seconds, of the trigger pulse. + wait_delay (float, optional): Time, in seconds, to delay the trigger. + """ + if isinstance(t, str) and t == "initial": + t = self.initial_trigger_time + t = round(t, 10) + if self.is_master_pseudoclock: + if compiler.wait_monitor is not None: + # Make the wait monitor pulse to signify starting or resumption of the + # experiment: + compiler.wait_monitor.trigger(t, duration) + self.trigger_times.append(t) + else: + TriggerableDevice.trigger(self, t, duration) + self.trigger_times.append(round(t + wait_delay, 10)) + + def do_checks(self, outputs): + """Basic error checking to ensure the user's instructions make sense. + + Args: + outputs (list): List of outputs to check. + """ + for output in outputs: + output.do_checks(self.trigger_times) + + def offset_instructions_from_trigger(self, outputs): + """Offset instructions for child devices by the appropriate trigger times. + + Args: + outputs (list): List of outputs to offset. + """ + for output in outputs: + output.offset_instructions_from_trigger(self.trigger_times) + + if not self.is_master_pseudoclock: + # Store the unmodified initial_trigger_time + initial_trigger_time = self.trigger_times[0] + # Adjust the stop time relative to the last trigger time + self.stop_time = round( + self.stop_time + - initial_trigger_time + - self.trigger_delay * len(self.trigger_times), + 10 + ) + + # Modify the trigger times themselves so that we insert wait instructions at the right times: + self.trigger_times = [ + round(t - initial_trigger_time - i*self.trigger_delay, 10) + for i, t in enumerate(self.trigger_times) + ] + + # quantise the trigger times and stop time to the pseudoclock clock resolution + self.trigger_times = self.quantise_to_pseudoclock(self.trigger_times) + self.stop_time = self.quantise_to_pseudoclock(self.stop_time) + + def generate_code(self, hdf5_file): + outputs = self.get_all_outputs() + self.do_checks(outputs) + self.offset_instructions_from_trigger(outputs) + Device.generate_code(self, hdf5_file) + diff --git a/labscript/labscript.py b/labscript/labscript.py index 98172d3..3adf511 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -32,7 +32,20 @@ from .base import Device from .compiler import compiler -from .utils import LabscriptError, set_passed_properties +from .core import ( + ClockLine, + IntermediateDevice, + Pseudoclock, + PseudoclockDevice, + TriggerableDevice, +) +from .utils import ( + LabscriptError, + bitfield, + fastflatten, + max_or_zero, + set_passed_properties +) import labscript_utils.h5_lock, h5py import labscript_utils.properties from labscript_utils.filewatcher import FileWatcher @@ -119,923 +132,6 @@ def suppress_all_warnings(state=True): no_warnings = suppress_all_warnings # Historical alias -def max_or_zero(*args, **kwargs): - """Returns max of the arguments or zero if sequence is empty. - - The protects the call to `max()` which would normally throw an error on an empty sequence. - - Args: - *args: Items to compare. - **kwargs: Passed to `max()`. - - Returns: - : Max of \*args. - """ - if not args: - return 0 - if not args[0]: - return 0 - else: - return max(*args, **kwargs) - -def bitfield(arrays,dtype): - """Converts a list of arrays of ones and zeros into a single - array of unsigned ints of the given datatype. - - Args: - arrays (list): List of numpy arrays consisting of ones and zeros. - dtype (data-type): Type to convert to. - - Returns: - :obj:`numpy:numpy.ndarray`: Numpy array with data type `dtype`. - """ - n = {uint8:8,uint16:16,uint32:32} - if np.array_equal(arrays[0], 0): - y = zeros(max([len(arr) if iterable(arr) else 1 for arr in arrays]),dtype=dtype) - else: - y = array(arrays[0],dtype=dtype) - for i in range(1,n[dtype]): - if iterable(arrays[i]): - y |= arrays[i]< maxrate: - # It does have the highest clock rate? Then store that rate - # to max_rate: - maxrate = output_instruction["clock rate"] - - # only check this for ramping clock_lines - # non-ramping clock-lines have already had the clock_limit - # checked within collect_change_times() - if local_clock_limit > clock_line.clock_limit: - local_clock_limit = clock_line.clock_limit - - if maxrate: - # round to the nearest clock rate that the pseudoclock can actually support: - period = 1/maxrate - quantised_period = period/self.clock_resolution - quantised_period = round(quantised_period) - period = quantised_period*self.clock_resolution - maxrate = 1/period - if maxrate > local_clock_limit: - raise LabscriptError( - f"At t = {time} sec, a clock rate of {maxrate} Hz was requested. " - f"One or more devices connected to {self.name} cannot support " - f"clock rates higher than {local_clock_limit} Hz." - ) - - if maxrate: - # If there was ramping at this timestep, how many clock ticks fit before the next instruction? - n_ticks, remainder = divmod((all_change_times[i+1] - time)*maxrate, 1) - n_ticks = int(n_ticks) - # Can we squeeze the final clock cycle in at the end? - if remainder and remainder/float(maxrate) >= 1/float(local_clock_limit): - # Yes we can. Clock speed will be as - # requested. Otherwise the final clock cycle will - # be too long, by the fraction 'remainder'. - n_ticks += 1 - duration = n_ticks/float(maxrate) # avoiding integer division - ticks = linspace(time, time + duration, n_ticks, endpoint=False) - - for clock_line in enabled_clocks: - if clock_line in enabled_looping_clocks: - all_times[clock_line].append(ticks) - else: - all_times[clock_line].append(time) - - if n_ticks > 1: - # If n_ticks is only one, then this step doesn't do - # anything, it has reps=0. So we should only include - # it if n_ticks > 1. - if n_ticks > 2: - # If there is more than one clock tick here, then we split the - # ramp into an initial clock tick, during which the slow clock - # ticks, and the rest of the ramping time, during which the slow - # clock does not tick. - clock.append( - { - "start": time, - "reps": 1, - "step": 1/float(maxrate), - "enabled_clocks": enabled_clocks, - } - ) - clock.append( - { - "start": time + 1/float(maxrate), - "reps": n_ticks - 2, - "step": 1/float(maxrate), - "enabled_clocks": enabled_looping_clocks, - } - ) - else: - clock.append( - { - "start": time, - "reps": n_ticks - 1, - "step": 1/float(maxrate), - "enabled_clocks": enabled_clocks, - } - ) - - # The last clock tick has a different duration depending on the next step. - clock.append( - { - "start": ticks[-1], - "reps": 1, - "step": all_change_times[i+1] - ticks[-1], - "enabled_clocks": enabled_clocks - if n_ticks == 1 - else enabled_looping_clocks, - } - ) - else: - for clock_line in enabled_clocks: - all_times[clock_line].append(time) - - try: - # If there was no ramping, here is a single clock tick: - clock.append( - { - "start": time, - "reps": 1, - "step": all_change_times[i+1] - time, - "enabled_clocks": enabled_clocks, - } - ) - except IndexError: - if i != len(all_change_times) - 1: - raise - if self.parent_device.stop_time > time: - # There is no next instruction. Hold the last clock - # tick until self.parent_device.stop_time. - raise LabscriptError( - "This shouldn't happen -- stop_time should always be equal " - "to the time of the last instruction. Please report a bug." - ) - # Error if self.parent_device.stop_time has been set to less - # than the time of the last instruction: - elif self.parent_device.stop_time < time: - raise LabscriptError( - f"{self.description} {self.name} has more instructions " - f"(at t={time:.15f}) after the experiment's stop time " - f"(t={self.parent_device.stop_time:.15f})." - ) - # If self.parent_device.stop_time is the same as the time of the last - # instruction, then we'll get the last instruction - # out still, so that the total number of clock - # ticks matches the number of data points in the - # Output.raw_output arrays. We'll make this last - # cycle be at ten times the minimum step duration. - else: - # Find the slowest clock_limit - enabled_clocks = [] - # The Pseudoclock clock limit - local_clock_limit = 1.0/self.clock_resolution - # Update with clock line limits - for clock_line, outputs in outputs_by_clockline.items(): - if local_clock_limit > clock_line.clock_limit: - local_clock_limit = clock_line.clock_limit - enabled_clocks.append(clock_line) - clock.append( - { - "start": time, - "reps": 1, - "step": 10.0/self.clock_limit, - "enabled_clocks": enabled_clocks, - } - ) - - return all_times, clock - - def get_outputs_by_clockline(self): - """Obtain all outputs by clockline. - - Returns: - tuple: Tuple containing: - - - **all_outputs** (list): List of all outputs, obtained from :meth:`get_all_outputs`. - - **outputs_by_clockline** (dict): Dictionary of outputs, organised by clockline. - """ - outputs_by_clockline = {} - for clock_line in self.child_devices: - if isinstance(clock_line, ClockLine): - outputs_by_clockline[clock_line] = [] - - all_outputs = self.get_all_outputs() - for output in all_outputs: - # TODO: Make this a bit more robust (can we assume things always have this - # hierarchy?) - clock_line = output.parent_clock_line - assert clock_line.parent_device == self - outputs_by_clockline[clock_line].append(output) - - return all_outputs, outputs_by_clockline - - def generate_clock(self): - """Generate the pseudoclock and configure outputs for each tick - of the clock. - """ - all_outputs, outputs_by_clockline = self.get_outputs_by_clockline() - - # Get change_times for all outputs, and also grouped by clockline - all_change_times, change_times = self.collect_change_times(all_outputs, outputs_by_clockline) - - # for each clock line - for clock_line, clock_line_change_times in change_times.items(): - # and for each output on the clockline - for output in outputs_by_clockline[clock_line]: - # call make_timeseries to expand the list of instructions for each change_time on this clock line - output.make_timeseries(clock_line_change_times) - - # now generate the clock meta data for the Pseudoclock - # also generate everytime point each clock line will tick (expand ramps) - all_times, self.clock = self.expand_change_times( - all_change_times, change_times, outputs_by_clockline - ) - - # Flatten the clock line times for use by the child devices for writing instruction tables - self.times = {} - for clock_line, time_array in all_times.items(): - self.times[clock_line] = fastflatten(time_array, np.dtype(float)) - - # for each clockline - for clock_line, outputs in outputs_by_clockline.items(): - clock_line_len = len(self.times[clock_line]) - # and for each output - for output in outputs: - # evaluate the output at each time point the clock line will tick at - output.expand_timeseries(all_times[clock_line], clock_line_len) - - def generate_code(self, hdf5_file): - self.generate_clock() - Device.generate_code(self, hdf5_file) - - -class TriggerableDevice(Device): - """A triggerable version of :obj:`Device`. - - This class is for devices that do not require a - pseudoclock, but do require a trigger. This enables - them to have a Trigger device as a parent. - """ - trigger_edge_type = "rising" - """str: Type of trigger. Must be `'rising'` or `'falling'`.""" - minimum_recovery_time = 0 - """float: Minimum time required before another trigger can occur.""" - - @set_passed_properties(property_names = {}) - def __init__(self, name, parent_device, connection, parentless=False, **kwargs): - """Instantiate a Triggerable Device. - - Args: - name (str): - parent_device (): - connection (str): - parentless (bool, optional): - **kwargs: Passed to :meth:`Device.__init__`. - - Raises: - LabscriptError: If trigger type of this device does not match - the trigger type of the parent Trigger. - """ - if None in [parent_device, connection] and not parentless: - raise LabscriptError( - "No parent specified. If this device does not require a parent, set " - "parentless=True" - ) - if isinstance(parent_device, Trigger): - if self.trigger_edge_type != parent_device.trigger_edge_type: - raise LabscriptError( - f"Trigger edge type for {name} is {self.trigger_edge_type}', but " - f"existing Trigger object {parent_device.name} has edge type " - f"'{parent_device.trigger_edge_type}'" - ) - self.trigger_device = parent_device - elif parent_device is not None: - # Instantiate a trigger object to be our parent: - self.trigger_device = Trigger( - f"{name}_trigger", parent_device, connection, self.trigger_edge_type - ) - parent_device = self.trigger_device - connection = "trigger" - - self.__triggers = [] - Device.__init__(self, name, parent_device, connection, **kwargs) - - def trigger(self, t, duration): - """Request parent trigger device to produce a trigger. - - Args: - t (float): Time, in seconds, to produce a trigger. - duration (float): Duration, in seconds, of the trigger pulse. - """ - # 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 - ): - raise ValueError( - f"{self.description} {self.name} has two triggers closer together " - f"than the minimum recovery time: one at t = {start:.15f}s for " - f"{duration}s, and another at t = {other_start:.15f}s for " - f"{other_duration}s. " - f"The minimum recovery time is {self.minimum_recovery_time}s." - ) - - self.__triggers.append([t, duration]) - - def do_checks(self): - """Check that all devices sharing a trigger device have triggers when - this device has a trigger. - - Raises: - LabscriptError: If correct triggers do not exist for all devices. - """ - # Check that all devices sharing a trigger device have triggers when we have triggers: - for device in self.trigger_device.child_devices: - if device is not self: - for trigger in self.__triggers: - if trigger not in device.__triggers: - start, duration = trigger - raise LabscriptError( - f"TriggerableDevices {self.name} and {device.name} share a " - f"trigger. {self.name} has a trigger at {start}s for " - f"{duration}s, but there is no matching trigger for " - f"{device.name}. Devices sharing a trigger must have " - "identical trigger times and durations." - ) - - def generate_code(self, hdf5_file): - self.do_checks() - Device.generate_code(self, hdf5_file) - - -class PseudoclockDevice(TriggerableDevice): - """Device that implements a pseudoclock.""" - description = "Generic Pseudoclock Device" - allowed_children = [Pseudoclock] - trigger_edge_type = "rising" - # How long after a trigger the next instruction is actually output: - trigger_delay = 0 - # How long a trigger line must remain high/low in order to be detected: - trigger_minimum_duration = 0 - # How long after the start of a wait instruction the device is actually capable of resuming: - wait_delay = 0 - - @set_passed_properties(property_names = {}) - def __init__(self, name, trigger_device=None, trigger_connection=None, **kwargs): - """Instantiates a pseudoclock device. - - Args: - name (str): python variable to assign to this device. - trigger_device (:obj:`DigitalOut`): Sets the parent triggering output. - If `None`, this is considered the master pseudoclock. - trigger_connection (str, optional): Must be provided if `trigger_device` is - provided. Specifies the channel of the parent device. - **kwargs: Passed to :meth:`TriggerableDevice.__init__`. - """ - if trigger_device is None: - for device in compiler.inventory: - if isinstance(device, PseudoclockDevice) and device.is_master_pseudoclock: - raise LabscriptError( - f"There is already a master pseudoclock device: {device.name}. " - "There cannot be multiple master pseudoclock devices - please " - "provide a trigger_device for one of them." - ) - TriggerableDevice.__init__( - self, name, parent_device=None, connection=None, parentless=True, **kwargs - ) - else: - # The parent device declared was a digital output channel: the following will - # automatically instantiate a Trigger for it and set it as self.trigger_device: - TriggerableDevice.__init__( - self, - name, - parent_device=trigger_device, - connection=trigger_connection, - **kwargs - ) - # Ensure that the parent pseudoclock device is, in fact, the master pseudoclock device. - if not self.trigger_device.pseudoclock_device.is_master_pseudoclock: - raise LabscriptError( - "All secondary pseudoclock devices must be triggered by a device " - "being clocked by the master pseudoclock device. Pseudoclocks " - "triggering each other in series is not supported." - ) - self.trigger_times = [] - self.wait_times = [] - self.initial_trigger_time = 0 - - @property - def is_master_pseudoclock(self): - """bool: Whether this device is the master pseudoclock.""" - return self.parent_device is None - - def set_initial_trigger_time(self, t): - """Sets the initial trigger time of the pseudoclock. - - If this is the master pseudoclock, time must be 0. - - Args: - t (float): Time, in seconds, to trigger this device. - """ - t = round(t,10) - if compiler.start_called: - raise LabscriptError( - "Initial trigger times must be set prior to calling start()" - ) - if self.is_master_pseudoclock: - raise LabscriptError( - "Initial trigger time of master clock is always zero, it cannot be " - "changed." - ) - else: - self.initial_trigger_time = t - - def trigger(self, t, duration, wait_delay = 0): - """Ask the trigger device to produce a digital pulse of a given duration - to trigger this pseudoclock. - - Args: - t (float): Time, in seconds, to trigger this device. - duration (float): Duration, in seconds, of the trigger pulse. - wait_delay (float, optional): Time, in seconds, to delay the trigger. - """ - if isinstance(t, str) and t == "initial": - t = self.initial_trigger_time - t = round(t, 10) - if self.is_master_pseudoclock: - if compiler.wait_monitor is not None: - # Make the wait monitor pulse to signify starting or resumption of the - # experiment: - compiler.wait_monitor.trigger(t, duration) - self.trigger_times.append(t) - else: - TriggerableDevice.trigger(self, t, duration) - self.trigger_times.append(round(t + wait_delay, 10)) - - def do_checks(self, outputs): - """Basic error checking to ensure the user's instructions make sense. - - Args: - outputs (list): List of outputs to check. - """ - for output in outputs: - output.do_checks(self.trigger_times) - - def offset_instructions_from_trigger(self, outputs): - """Offset instructions for child devices by the appropriate trigger times. - - Args: - outputs (list): List of outputs to offset. - """ - for output in outputs: - output.offset_instructions_from_trigger(self.trigger_times) - - if not self.is_master_pseudoclock: - # Store the unmodified initial_trigger_time - initial_trigger_time = self.trigger_times[0] - # Adjust the stop time relative to the last trigger time - self.stop_time = round( - self.stop_time - - initial_trigger_time - - self.trigger_delay * len(self.trigger_times), - 10 - ) - - # Modify the trigger times themselves so that we insert wait instructions at the right times: - self.trigger_times = [ - round(t - initial_trigger_time - i*self.trigger_delay, 10) - for i, t in enumerate(self.trigger_times) - ] - - # quantise the trigger times and stop time to the pseudoclock clock resolution - self.trigger_times = self.quantise_to_pseudoclock(self.trigger_times) - self.stop_time = self.quantise_to_pseudoclock(self.stop_time) - - def generate_code(self, hdf5_file): - outputs = self.get_all_outputs() - self.do_checks(outputs) - self.offset_instructions_from_trigger(outputs) - Device.generate_code(self, hdf5_file) - - class Output(Device): """Base class for all output classes.""" description = 'generic output' diff --git a/labscript/utils.py b/labscript/utils.py index 5db811f..37ef294 100644 --- a/labscript/utils.py +++ b/labscript/utils.py @@ -1,6 +1,8 @@ from inspect import getcallargs from functools import wraps +import numpy as np + _RemoteConnection = None ClockLine = None PseudoClockDevice = None @@ -28,7 +30,7 @@ def is_clock_line(device): the lookup in the modules hash table is slower). """ if ClockLine is None: - from .labscript import ClockLine + from .core import ClockLine return isinstance(device, _RemoteConnection) @@ -41,7 +43,7 @@ def is_pseudoclock_device(device): the lookup in the modules hash table is slower). """ if PseudoclockDevice is None: - from .labscript import PseudoclockDevice + from .core import PseudoclockDevice return isinstance(device, PseudoclockDevice) @@ -99,6 +101,84 @@ def new_function(inst, *args, **kwargs): return decorator +def fastflatten(inarray, dtype): + """A faster way of flattening our arrays than pylab.flatten. + + pylab.flatten returns a generator which takes a lot of time and memory + to convert into a numpy array via array(list(generator)). The problem + is that generators don't know how many values they'll return until + they're done. This algorithm produces a numpy array directly by + first calculating what the length will be. It is several orders of + magnitude faster. Note that we can't use numpy.ndarray.flatten here + since our inarray is really a list of 1D arrays of varying length + and/or single values, not a N-dimenional block of homogeneous data + like a numpy array. + + Args: + inarray (list): List of 1-D arrays to flatten. + dtype (data-type): Type of the data in the arrays. + + Returns: + :obj:`numpy:numpy.ndarray`: Flattened array. + """ + total_points = np.sum([len(element) if np.iterable(element) else 1 for element in inarray]) + flat = np.empty(total_points,dtype=dtype) + i = 0 + for val in inarray: + if np.iterable(val): + flat[i:i+len(val)] = val[:] + i += len(val) + else: + flat[i] = val + i += 1 + return flat + + +def max_or_zero(*args, **kwargs): + """Returns max of the arguments or zero if sequence is empty. + + This protects the call to `max()` which would normally throw an error on an empty + sequence. + + Args: + *args: Items to compare. + **kwargs: Passed to `max()`. + + Returns: + : Max of \*args. + """ + if not args: + return 0 + if not args[0]: + return 0 + else: + return max(*args, **kwargs) + + +def bitfield(arrays,dtype): + """Converts a list of arrays of ones and zeros into a single + array of unsigned ints of the given datatype. + + Args: + arrays (list): List of numpy arrays consisting of ones and zeros. + dtype (data-type): Type to convert to. + + Returns: + :obj:`numpy:numpy.ndarray`: Numpy array with data type `dtype`. + """ + n = {np.uint8: 8, np.uint16: 16, np.uint32: 32} + if np.array_equal(arrays[0], 0): + y = np.zeros( + max([len(arr) if np.iterable(arr) else 1 for arr in arrays]), dtype=np.dtype + ) + else: + y = np.array(arrays[0], dtype=dtype) + for i in range(1, n[dtype]): + if np.iterable(arrays[i]): + y |= arrays[i] << i + return y + + class LabscriptError(Exception): """A *labscript* error. From 63ce8134d4f952b4656bbe62a5203cf2f49263eb Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Wed, 3 Jan 2024 19:59:13 +1100 Subject: [PATCH 05/19] Updated output classes to more modern Python and improved some formatting. Also fixed a couple of bugs with error messages (not being raised, having incorrect text, etc.) --- labscript/labscript.py | 1177 ++++++++++++++++++++++++++++------------ 1 file changed, 830 insertions(+), 347 deletions(-) diff --git a/labscript/labscript.py b/labscript/labscript.py index 3adf511..91fcb30 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -134,15 +134,23 @@ def suppress_all_warnings(state=True): class Output(Device): """Base class for all output classes.""" - description = 'generic output' - allowed_states = {} - dtype = float64 + description = "generic output" + allowed_states = None + dtype = np.float64 scale_factor = 1 - + @set_passed_properties(property_names={}) - def __init__(self, name, parent_device, connection, limits=None, - unit_conversion_class=None, unit_conversion_parameters=None, - default_value=None, **kwargs): + def __init__( + self, + name, + parent_device, + connection, + limits=None, + unit_conversion_class=None, + unit_conversion_parameters=None, + default_value=None, + **kwargs + ): """Instantiate an Output. Args: @@ -172,38 +180,57 @@ def __init__(self, name, parent_device, connection, limits=None, if not unit_conversion_parameters: unit_conversion_parameters = {} self.unit_conversion_class = unit_conversion_class - self.set_properties(unit_conversion_parameters, - {'unit_conversion_parameters': list(unit_conversion_parameters.keys())}) + self.set_properties( + unit_conversion_parameters, + {"unit_conversion_parameters": list(unit_conversion_parameters.keys())} + ) # Instantiate the calibration if unit_conversion_class is not None: self.calibration = unit_conversion_class(unit_conversion_parameters) # Validate the calibration class for units in self.calibration.derived_units: - #Does the conversion to base units function exist for each defined unit type? - if not hasattr(self.calibration,units+"_to_base"): - raise LabscriptError('The function "%s_to_base" does not exist within the calibration "%s" used in output "%s"'%(units,self.unit_conversion_class,self.name)) - #Does the conversion to base units function exist for each defined unit type? - if not hasattr(self.calibration,units+"_from_base"): - raise LabscriptError('The function "%s_from_base" does not exist within the calibration "%s" used in output "%s"'%(units,self.unit_conversion_class,self.name)) - + # Does the conversion to base units function exist for each defined unit + # type? + if not hasattr(self.calibration, f"{units}_to_base"): + raise LabscriptError( + f'The function "{units}_to_base" does not exist within the ' + f'calibration "{self.unit_conversion_class}" used in output ' + f'"{self.name}"' + ) + # Does the conversion to base units function exist for each defined unit + # type? + if not hasattr(self.calibration, f"{units}_from_base"): + raise LabscriptError( + f'The function "{units}_from_base" does not exist within the ' + f'calibration "{self.unit_conversion_class}" used in output ' + f'"{self.name}"' + ) + # If limits exist, check they are valid - # Here we specifically differentiate "None" from False as we will later have a conditional which relies on - # self.limits being either a correct tuple, or "None" + # Here we specifically differentiate "None" from False as we will later have a + # conditional which relies on self.limits being either a correct tuple, or + # "None" if limits is not None: - if not isinstance(limits,tuple) or len(limits) != 2: - raise LabscriptError('The limits for "%s" must be tuple of length 2. Eg. limits=(1,2)'%(self.name)) + if not isinstance(limits, tuple) or len(limits) != 2: + raise LabscriptError( + f'The limits for "{self.name}" must be tuple of length 2. ' + 'Eg. limits=(1, 2)' + ) if limits[0] > limits[1]: - raise LabscriptError('The first element of the tuple must be lower than the second element. Eg limits=(1,2), NOT limits=(2,1)') + raise LabscriptError( + "The first element of the tuple must be lower than the second " + "element. Eg limits=(1, 2), NOT limits=(2, 1)" + ) # Save limits even if they are None self.limits = limits - + @property def clock_limit(self): """float: Returns the parent clock line's clock limit.""" parent = self.parent_clock_line return parent.clock_limit - + @property def trigger_delay(self): """float: The earliest time output can be commanded from this device after a trigger. @@ -213,7 +240,7 @@ def trigger_delay(self): return 0 else: return parent.trigger_delay - + @property def wait_delay(self): """float: The earliest time output can be commanded from this device after a wait. @@ -252,15 +279,21 @@ def apply_calibration(self,value,units): """ # Is a calibration in use? if self.unit_conversion_class is None: - raise LabscriptError('You can not specify the units in an instruction for output "%s" as it does not have a calibration associated with it'%(self.name)) - + raise LabscriptError( + 'You can not specify the units in an instruction for output ' + f'"{self.name}" as it does not have a calibration associated with it' + ) + # Does a calibration exist for the units specified? if units not in self.calibration.derived_units: - raise LabscriptError('The units "%s" does not exist within the calibration "%s" used in output "%s"'%(units,self.unit_conversion_class,self.name)) - + raise LabscriptError( + f'The units "{units}" does not exist within the calibration ' + f'"{self.unit_conversion_class}" used in output "{self.name}"' + ) + # Return the calibrated value return getattr(self.calibration,units+"_to_base")(value) - + def instruction_to_string(self,instruction): """Gets a human readable description of an instruction. @@ -271,14 +304,14 @@ def instruction_to_string(self,instruction): Returns: str: Instruction description. """ - if isinstance(instruction,dict): - return instruction['description'] + if isinstance(instruction, dict): + return instruction["description"] elif self.allowed_states: return str(self.allowed_states[instruction]) else: return str(instruction) - def add_instruction(self,time,instruction,units=None): + def add_instruction(self, time, instruction, units=None): """Adds a hardware instruction to the device instruction list. Args: @@ -292,54 +325,77 @@ def add_instruction(self,time,instruction,units=None): is too fast. """ if not compiler.start_called: - raise LabscriptError('Cannot add instructions prior to calling start()') + raise LabscriptError("Cannot add instructions prior to calling start()") # round to the nearest 0.1 nanoseconds, to prevent floating point # rounding errors from breaking our equality checks later on. - time = round(time,10) + time = round(time, 10) # Also round end time of ramps to the nearest 0.1 ns: if isinstance(instruction,dict): - instruction['end time'] = round(instruction['end time'],10) - instruction['initial time'] = round(instruction['initial time'],10) + instruction["end time"] = round(instruction["end time"], 10) + instruction["initial time"] = round(instruction["initial time"], 10) # Check that time is not negative or too soon after t=0: if time < self.t0: - err = ' '.join([self.description, self.name, 'has an instruction at t=%ss,'%str(time), - 'Due to the delay in triggering its pseudoclock device, the earliest output possible is at t=%s.'%str(self.t0)]) - raise LabscriptError(err) + raise LabscriptError( + f"{self.description} {self.name} has an instruction at t={time}s. " + "Due to the delay in triggering its pseudoclock device, the earliest " + f"output possible is at t={self.t0}." + ) # Check that this doesn't collide with previous instructions: if time in self.instructions.keys(): if not config.suppress_all_warnings: - message = ' '.join(['WARNING: State of', self.description, self.name, 'at t=%ss'%str(time), - 'has already been set to %s.'%self.instruction_to_string(self.instructions[time]), - 'Overwriting to %s. (note: all values in base units where relevant)'%self.instruction_to_string(self.apply_calibration(instruction,units) if units and not isinstance(instruction,dict) else instruction)]) - sys.stderr.write(message+'\n') + current_value = self.instruction_to_string(self.instructions[time]) + new_value = self.instruction_to_string( + self.apply_calibration(instruction, units) + if units and not isinstance(instruction, dict) + else instruction + ) + sys.stderr.write( + f"WARNING: State of {self.description} {self.name} at t={time}s " + f"has already been set to {current_value}. Overwriting to " + f"{new_value}. (note: all values in base units where relevant)" + "\n" + ) # Check that ramps don't collide - if isinstance(instruction,dict): + if isinstance(instruction, dict): # No ramps allowed if this output is on a slow clock: if not self.parent_clock_line.ramping_allowed: - raise LabscriptError('%s %s is on clockline that does not support ramping. '%(self.description, self.name) + - 'It cannot have a function ramp as an instruction.') + raise LabscriptError( + f"{self.description} {self.name} is on clockline that does not " + "support ramping. It cannot have a function ramp as an instruction." + ) for start, end in self.ramp_limits: - if start < time < end or start < instruction['end time'] < end: - err = ' '.join(['State of', self.description, self.name, 'from t = %ss to %ss'%(str(start),str(end)), - 'has already been set to %s.'%self.instruction_to_string(self.instructions[start]), - 'Cannot set to %s from t = %ss to %ss.'%(self.instruction_to_string(instruction),str(time),str(instruction['end time']))]) - raise LabscriptError(err) - self.ramp_limits.append((time,instruction['end time'])) + if start < time < end or start < instruction["end time"] < end: + start_value = self.instruction_to_string(self.instructions[start]) + new_value = self.instruction_to_string(instruction) + raise LabscriptError( + f"State of {self.description} {self.name} from t = {start}s to " + f"{end}s has already been set to {start_value}. Cannot set to " + f"{new_value} from t = {time}s to {instruction['end time']}s." + ) + self.ramp_limits.append((time, instruction["end time"])) # Check that start time is before end time: - if time > instruction['end time']: - raise LabscriptError('%s %s has been passed a function ramp %s with a negative duration.'%(self.description, self.name, self.instruction_to_string(instruction))) - if instruction['clock rate'] == 0: - raise LabscriptError('A nonzero sample rate is required.') + if time > instruction["end time"]: + raise LabscriptError( + f"{self.description} {self.name} has been passed a function ramp " + f"{self.instruction_to_string(instruction)} with a negative " + "duration." + ) + if instruction["clock rate"] == 0: + raise LabscriptError("A nonzero sample rate is required.") # Else we have a "constant", single valued instruction else: # If we have units specified, convert the value if units is not None: # Apply the unit calibration now - instruction = self.apply_calibration(instruction,units) + instruction = self.apply_calibration(instruction, units) # if we have limits, check the value is valid if self.limits: if (instruction < self.limits[0]) or (instruction > self.limits[1]): - raise LabscriptError('You cannot program the value %s (base units) to %s as it falls outside the limits (%d to %d)'%(str(instruction), self.name, self.limits[0], self.limits[1])) + raise LabscriptError( + f"You cannot program the value {instruction} (base units) to " + f"{self.name} as it falls outside the limits " + f"({self.limits[0]} to {self.limits[1]})" + ) self.instructions[time] = instruction def do_checks(self, trigger_times): @@ -356,40 +412,71 @@ def do_checks(self, trigger_times): # instruction telling the output to remain at its default value. if not self.instructions: if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write(' '.join(['WARNING:', self.name, 'has no instructions. It will be set to %s for all time.\n'%self.instruction_to_string(self.default_value)])) + sys.stderr.write( + f"WARNING: {self.name} has no instructions. It will be set to " + f"{self.instruction_to_string(self.default_value)} for all time.\n" + ) self.add_instruction(self.t0, self.default_value) # Check if there are no instructions at the initial time. Generate a warning and insert an # instruction telling the output to start at its default value. if self.t0 not in self.instructions.keys(): if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write(' '.join(['WARNING:', self.name, 'has no initial instruction. It will initially be set to %s.\n'%self.instruction_to_string(self.default_value)])) + sys.stderr.write( + f"WARNING: {self.name} has no initial instruction. It will " + "initially be set to " + f"{self.instruction_to_string(self.default_value)}.\n" + ) self.add_instruction(self.t0, self.default_value) # Check that ramps have instructions following them. # If they don't, insert an instruction telling them to hold their final value. for instruction in list(self.instructions.values()): - if isinstance(instruction, dict) and instruction['end time'] not in self.instructions.keys(): - self.add_instruction(instruction['end time'], instruction['function'](instruction['end time']-instruction['initial time']), instruction['units']) + if ( + isinstance(instruction, dict) + and instruction["end time"] not in self.instructions.keys() + ): + self.add_instruction( + instruction["end time"], + instruction["function"]( + instruction["end time"] - instruction["initial time"] + ), + instruction["units"], + ) # Checks for trigger times: for trigger_time in trigger_times: - for t, instruction in self.instructions.items(): + for t, inst in self.instructions.items(): # Check no ramps are happening at the trigger time: - if isinstance(instruction, dict) and instruction['initial time'] < trigger_time and instruction['end time'] > trigger_time: - err = (' %s %s has a ramp %s from t = %s to %s. ' % (self.description, - self.name, instruction['description'], str(instruction['initial time']), str(instruction['end time'])) + - 'This overlaps with a trigger at t=%s, and so cannot be performed.' % str(trigger_time)) - raise LabscriptError(err) + if ( + isinstance(inst, dict) + and inst["initial time"] < trigger_time + and inst["end time"] > trigger_time + ): + raise LabscriptError( + f"{self.description} {self.name} has a ramp " + f"{inst['description']} from t = {inst['initial time']} to " + f"{inst['end time']}. This overlaps with a trigger at " + f"t={trigger_time}, and so cannot be performed." + ) # Check that nothing is happening during the delay time after the trigger: - if round(trigger_time,10) < round(t,10) < round(trigger_time + self.trigger_delay, 10): - err = (' %s %s has an instruction at t = %s. ' % (self.description, self.name, str(t)) + - 'This is too soon after a trigger at t=%s, '%str(trigger_time) + - 'the earliest output possible after this trigger is at t=%s'%str(trigger_time + self.trigger_delay)) - raise LabscriptError(err) + if ( + round(trigger_time, 10) + < round(t, 10) + < round(trigger_time + self.trigger_delay, 10) + ): + raise LabscriptError( + f"{self.description} {self.name} has an instruction at t={t}. " + f"This is too soon after a trigger at t={trigger_time}, " + "the earliest output possible after this trigger is at " + f"t={trigger_time + self.trigger_delay}" + ) # Check that there are no instructions too soon before the trigger: if 0 < trigger_time - t < max(self.clock_limit, compiler.wait_delay): - err = (' %s %s has an instruction at t = %s. ' % (self.description, self.name, str(t)) + - 'This is too soon before a trigger at t=%s, '%str(trigger_time) + - 'the latest output possible before this trigger is at t=%s'%str(trigger_time - max(self.clock_limit, compiler.wait_delay))) - + raise LabscriptError( + f"{self.description} {self.name} has an instruction at t={t}. " + f"This is too soon before a trigger at t={trigger_time}, " + "the latest output possible before this trigger is at " + f"t={trigger_time - max(self.clock_limit, compiler.wait_delay)}" + ) + def offset_instructions_from_trigger(self, trigger_times): """Subtracts self.trigger_delay from all instructions at or after each trigger_time. @@ -402,30 +489,39 @@ def offset_instructions_from_trigger(self, trigger_times): n_triggers_prior = len([time for time in trigger_times if time < t]) # The cumulative offset at this point in time: offset = self.trigger_delay * n_triggers_prior + trigger_times[0] - offset = round(offset,10) - if isinstance(instruction,dict): + offset = round(offset, 10) + if isinstance(instruction, dict): offset_instruction = instruction.copy() - offset_instruction['end time'] = self.quantise_to_pseudoclock(round(instruction['end time'] - offset,10)) - offset_instruction['initial time'] = self.quantise_to_pseudoclock(round(instruction['initial time'] - offset,10)) + offset_instruction["end time"] = self.quantise_to_pseudoclock( + round(instruction["end time"] - offset, 10) + ) + offset_instruction["initial time"] = self.quantise_to_pseudoclock( + round(instruction["initial time"] - offset, 10) + ) else: offset_instruction = instruction - - offset_instructions[self.quantise_to_pseudoclock(round(t - offset,10))] = offset_instruction + + new_time = self.quantise_to_pseudoclock(round(t - offset, 10)) + offset_instructions[new_time] = offset_instruction self.instructions = offset_instructions - - # offset each of the ramp_limits for use in the calculation within Pseudoclock/ClockLine - # so that the times in list are consistent with the ones in self.instructions + + # offset each of the ramp_limits for use in the calculation within + # Pseudoclock/ClockLine so that the times in list are consistent with the ones + # in self.instructions for i, times in enumerate(self.ramp_limits): n_triggers_prior = len([time for time in trigger_times if time < times[0]]) # The cumulative offset at this point in time: offset = self.trigger_delay * n_triggers_prior + trigger_times[0] - offset = round(offset,10) - + offset = round(offset, 10) + # offset start and end time of ramps # NOTE: This assumes ramps cannot proceed across a trigger command # (for instance you cannot ramp an output across a WAIT) - self.ramp_limits[i] = (self.quantise_to_pseudoclock(round(times[0]-offset,10)), self.quantise_to_pseudoclock(round(times[1]-offset,10))) - + self.ramp_limits[i] = ( + self.quantise_to_pseudoclock(round(times[0] - offset, 10)), + self.quantise_to_pseudoclock(round(times[1] - offset, 10)), + ) + def get_change_times(self): """If this function is being called, it means that the parent Pseudoclock has requested a list of times that this output changes @@ -441,14 +537,22 @@ def get_change_times(self): for time in times: if isinstance(self.instructions[time], dict) and current_dict_time is None: current_dict_time = self.instructions[time] - elif current_dict_time is not None and current_dict_time['initial time'] < time < current_dict_time['end time']: - err = ("{:s} {:s} has an instruction at t={:.10f}s. This instruction collides with a ramp on this output at that time. ".format(self.description, self.name, time)+ - "The collision {:s} is happening from {:.10f}s till {:.10f}s".format(current_dict_time['description'], current_dict_time['initial time'], current_dict_time['end time'])) - raise LabscriptError(err) + elif ( + current_dict_time is not None + and current_dict_time['initial time'] < time < current_dict_time['end time'] + ): + raise LabscriptError( + f"{self.description} {self.name} has an instruction at " + f"t={time:.10f}s. This instruction collides with a ramp on this " + "output at that time. The collision " + f"{current_dict_time['description']} is happening from " + f"{current_dict_time['initial time']:.10f}s untill " + f"{current_dict_time['end time']:.10f}s" + ) self.times = times return times - + def get_ramp_times(self): """If this is being called, then it means the parent Pseuedoclock has asked for a list of the output ramp start and stop times. @@ -457,7 +561,7 @@ def get_ramp_times(self): list: List of (start, stop) times of ramps for this Output. """ return self.ramp_limits - + def make_timeseries(self, change_times): """If this is being called, then it means the parent Pseudoclock has asked for a list of this output's states at each time in @@ -474,7 +578,7 @@ def make_timeseries(self, change_times): while i < time_len and change_time >= self.times[i]: i += 1 self.timeseries.append(self.instructions[self.times[i-1]]) - + def expand_timeseries(self,all_times,flat_all_times_len): """This function evaluates the ramp functions in self.timeseries at the time points in all_times, and creates an array of output @@ -488,11 +592,11 @@ def expand_timeseries(self,all_times,flat_all_times_len): self.raw_output = np.array(self.timeseries, dtype=np.dtype(self.dtype)) return outputarray = np.empty((flat_all_times_len,), dtype=np.dtype(self.dtype)) - j=0 + j = 0 for i, time in enumerate(all_times): - if iterable(time): + if np.iterable(time): time_len = len(time) - if isinstance(self.timeseries[i],dict): + if isinstance(self.timeseries[i], dict): # We evaluate the functions at the midpoints of the # timesteps in order to remove the zero-order hold # error introduced by sampling an analog signal: @@ -510,16 +614,25 @@ def expand_timeseries(self,all_times,flat_all_times_len): # by another ramp or not: next_time = all_times[i+1][0] if iterable(all_times[i+1]) else all_times[i+1] midpoints[-1] = time[-1] + 0.5*(next_time - time[-1]) - outarray = self.timeseries[i]['function'](midpoints-self.timeseries[i]['initial time']) + outarray = self.timeseries[i]["function"]( + midpoints - self.timeseries[i]["initial time"] + ) # Now that we have the list of output points, pass them through the unit calibration - if self.timeseries[i]['units'] is not None: - outarray = self.apply_calibration(outarray,self.timeseries[i]['units']) + if self.timeseries[i]["units"] is not None: + outarray = self.apply_calibration( + outarray, self.timeseries[i]["units"] + ) # if we have limits, check the value is valid if self.limits: if ((outarrayself.limits[1])).any(): - raise LabscriptError('The function %s called on "%s" at t=%d generated a value which falls outside the base unit limits (%d to %d)'%(self.timeseries[i]['function'],self.name,midpoints[0],self.limits[0],self.limits[1])) + raise LabscriptError( + f"The function {self.timeseries[i]['function']} called " + f'on "{self.name}" at t={midpoints[0]} generated a ' + "value which falls outside the base unit limits " + f"({self.limits[0]} to {self.limits[1]})" + ) else: - outarray = empty(time_len,dtype=self.dtype) + outarray = empty(time_len, dtype=self.dtype) outarray.fill(self.timeseries[i]) outputarray[j:j+time_len] = outarray j += time_len @@ -529,19 +642,22 @@ def expand_timeseries(self,all_times,flat_all_times_len): del self.timeseries # don't need this any more. self.raw_output = outputarray + class AnalogQuantity(Output): """Base class for :obj:`AnalogOut`. It is also used internally by :obj:`DDS`. You should never instantiate this class directly. """ - description = 'analog quantity' + description = "analog quantity" default_value = 0 def _check_truncation(self, truncation, min=0, max=1): if not (min <= truncation <= max): raise LabscriptError( - 'Truncation argument must be between %f and %f (inclusive), but is %f.' % (min, max, truncation)) + f"Truncation argument must be between {min} and {max} (inclusive), but " + f"is {truncation}." + ) def ramp(self, t, duration, initial, final, samplerate, units=None, truncation=1.): """Command the output to perform a linear ramp. @@ -564,18 +680,46 @@ def ramp(self, t, duration, initial, final, samplerate, units=None, truncation=1 """ self._check_truncation(truncation) if truncation > 0: - # if start and end value are the same, we don't need to ramp and can save the sample ticks etc + # if start and end value are the same, we don't need to ramp and can save + # the sample ticks etc if initial == final: self.constant(t, initial, units) if not config.suppress_mild_warnings and not config.suppress_all_warnings: - message = ''.join(['WARNING: AnalogOutput \'%s\' has the same initial and final value at time t=%.10fs with duration %.10fs. In order to save samples and clock ticks this instruction is replaced with a constant output. '%(self.name, t, duration)]) - sys.stderr.write(message + '\n') + sys.stderr.write( + f"WARNING: {self.__class__.__name__} '{self.name}' has the " + f"same initial and final value at time t={t:.10f}s with " + f"duration {duration:.10f}s. In order to save samples and " + "clock ticks this instruction is replaced with a constant " + "output.\n" + ) else: - self.add_instruction(t, {'function': functions.ramp(round(t + duration, 10) - round(t, 10), initial, final), 'description': 'linear ramp', - 'initial time': t, 'end time': t + truncation * duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": functions.ramp( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "linear ramp", + "initial time": t, + "end time": t + truncation * duration, + "clock rate": samplerate, + "units": units, + } + ) return truncation * duration - def sine(self, t, duration, amplitude, angfreq, phase, dc_offset, samplerate, units=None, truncation=1.): + def sine( + self, + t, + duration, + amplitude, + angfreq, + phase, + dc_offset, + samplerate, + units=None, + truncation=1. + ): """Command the output to perform a sinusoidal modulation. Defined by @@ -598,11 +742,28 @@ def sine(self, t, duration, amplitude, angfreq, phase, dc_offset, samplerate, un """ self._check_truncation(truncation) if truncation > 0: - self.add_instruction(t, {'function': functions.sine(round(t + duration, 10) - round(t, 10), amplitude, angfreq, phase, dc_offset), 'description': 'sine wave', - 'initial time': t, 'end time': t + truncation*duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": functions.sine( + round(t + duration, 10) - round(t, 10), + amplitude, + angfreq, + phase, + dc_offset, + ), + "description": "sine wave", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) return truncation*duration - def sine_ramp(self, t, duration, initial, final, samplerate, units=None, truncation=1.): + def sine_ramp( + self, t, duration, initial, final, samplerate, units=None, truncation=1. + ): """Command the output to perform a ramp defined by one half period of a squared sine wave. Defined by @@ -623,11 +784,24 @@ def sine_ramp(self, t, duration, initial, final, samplerate, units=None, truncat """ self._check_truncation(truncation) if truncation > 0: - self.add_instruction(t, {'function': functions.sine_ramp(round(t + duration, 10) - round(t, 10), initial, final), 'description': 'sinusoidal ramp', - 'initial time': t, 'end time': t + truncation*duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": functions.sine_ramp( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "sinusoidal ramp", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) return truncation*duration - def sine4_ramp(self, t, duration, initial, final, samplerate, units=None, truncation=1.): + def sine4_ramp( + self, t, duration, initial, final, samplerate, units=None, truncation=1. + ): """Command the output to perform an increasing ramp defined by one half period of a quartic sine wave. Defined by @@ -648,11 +822,24 @@ def sine4_ramp(self, t, duration, initial, final, samplerate, units=None, trunca """ self._check_truncation(truncation) if truncation > 0: - self.add_instruction(t, {'function': functions.sine4_ramp(round(t + duration, 10) - round(t, 10), initial, final), 'description': 'sinusoidal ramp', - 'initial time': t, 'end time': t + truncation*duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": functions.sine4_ramp( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "sinusoidal ramp", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) return truncation*duration - def sine4_reverse_ramp(self, t, duration, initial, final, samplerate, units=None, truncation=1.): + def sine4_reverse_ramp( + self, t, duration, initial, final, samplerate, units=None, truncation=1. + ): """Command the output to perform a decreasing ramp defined by one half period of a quartic sine wave. Defined by @@ -673,11 +860,34 @@ def sine4_reverse_ramp(self, t, duration, initial, final, samplerate, units=None """ self._check_truncation(truncation) if truncation > 0: - self.add_instruction(t, {'function': functions.sine4_reverse_ramp(round(t + duration, 10) - round(t, 10), initial, final), 'description': 'sinusoidal ramp', - 'initial time': t, 'end time': t + truncation*duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": functions.sine4_reverse_ramp( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "sinusoidal ramp", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) return truncation*duration - def exp_ramp(self, t, duration, initial, final, samplerate, zero=0, units=None, truncation=None, truncation_type='linear', **kwargs): + def exp_ramp( + self, + t, + duration, + initial, + final, + samplerate, + zero=0, + units=None, + truncation=None, + truncation_type="linear", + **kwargs, + ): """Exponential ramp whose rate of change is set by an asymptotic value (zero argument). Args: @@ -698,33 +908,60 @@ def exp_ramp(self, t, duration, initial, final, samplerate, zero=0, units=None, """ # Backwards compatibility for old kwarg names - if 'trunc' in kwargs: - truncation = kwargs.pop('trunc') - if 'trunc_type' in kwargs: - truncation_type = kwargs.pop('trunc_type') + if "trunc" in kwargs: + truncation = kwargs.pop("trunc") + if "trunc_type" in kwargs: + truncation_type = kwargs.pop("trunc_type") if truncation is not None: # Computed the truncated duration based on the truncation_type - if truncation_type == 'linear': - self._check_truncation(truncation, min(initial, final), max(initial, final)) + if truncation_type == "linear": + self._check_truncation( + truncation, min(initial, final), max(initial, final) + ) # Truncate the ramp when it reaches the value truncation trunc_duration = duration * \ - log((initial-zero)/(truncation-zero)) / \ - log((initial-zero)/(final-zero)) - elif truncation_type == 'exponential': + np.log((initial-zero)/(truncation-zero)) / \ + np.log((initial-zero)/(final-zero)) + elif truncation_type == "exponential": # Truncate the ramps duration by a fraction truncation self._check_truncation(truncation) trunc_duration = truncation * duration else: raise LabscriptError( - 'Truncation type for exp_ramp not supported. Must be either linear or exponential.') + "Truncation type for exp_ramp not supported. Must be either linear " + "or exponential." + ) else: trunc_duration = duration if trunc_duration > 0: - self.add_instruction(t, {'function': functions.exp_ramp(round(t + duration, 10) - round(t, 10), initial, final, zero), 'description': 'exponential ramp', - 'initial time': t, 'end time': t + trunc_duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": functions.exp_ramp( + round(t + duration, 10) - round(t, 10), initial, final, zero + ), + "description": 'exponential ramp', + "initial time": t, + "end time": t + trunc_duration, + "clock rate": samplerate, + "units": units, + } + ) return trunc_duration - def exp_ramp_t(self, t, duration, initial, final, time_constant, samplerate, units=None, truncation=None, truncation_type='linear', **kwargs): + def exp_ramp_t( + self, + t, + duration, + initial, + final, + time_constant, + samplerate, + units=None, + truncation=None, + truncation_type="linear", + **kwargs + ): """Exponential ramp whose rate of change is set by the time_constant. Args: @@ -745,31 +982,49 @@ def exp_ramp_t(self, t, duration, initial, final, time_constant, samplerate, uni """ # Backwards compatibility for old kwarg names - if 'trunc' in kwargs: - truncation = kwargs.pop('trunc') - if 'trunc_type' in kwargs: - truncation_type = kwargs.pop('trunc_type') + if "trunc" in kwargs: + truncation = kwargs.pop("trunc") + if "trunc_type" in kwargs: + truncation_type = kwargs.pop("trunc_type") if truncation is not None: - zero = (final-initial*exp(-duration/time_constant)) / \ - (1-exp(-duration/time_constant)) - if truncation_type == 'linear': + zero = (final-initial*np.exp(-duration/time_constant)) / \ + (1-np.exp(-duration/time_constant)) + if truncation_type == "linear": self._check_truncation(truncation, min(initial, final), max(initial, final)) trunc_duration = time_constant * \ - log((initial-zero)/(truncation-zero)) + np.log((initial-zero)/(truncation-zero)) elif truncation_type == 'exponential': self._check_truncation(truncation) trunc_duration = truncation * duration else: raise LabscriptError( - 'Truncation type for exp_ramp_t not supported. Must be either linear or exponential.') + "Truncation type for exp_ramp_t not supported. Must be either " + "linear or exponential." + ) else: trunc_duration = duration if trunc_duration > 0: - self.add_instruction(t, {'function': functions.exp_ramp_t(round(t + duration, 10) - round(t, 10), initial, final, time_constant), 'description': 'exponential ramp with time consntant', - 'initial time': t, 'end time': t + trunc_duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": functions.exp_ramp_t( + round(t + duration, 10) - round(t, 10), + initial, + final, + time_constant, + ), + "description": "exponential ramp with time consntant", + "initial time": t, + "end time": t + trunc_duration, + "clock rate": samplerate, + "units": units, + } + ) return trunc_duration - def piecewise_accel_ramp(self, t, duration, initial, final, samplerate, units=None, truncation=1.): + def piecewise_accel_ramp( + self, t, duration, initial, final, samplerate, units=None, truncation=1. + ): """Changes the output so that the second derivative follows one period of a triangle wave. Args: @@ -786,12 +1041,34 @@ def piecewise_accel_ramp(self, t, duration, initial, final, samplerate, units=No """ self._check_truncation(truncation) if truncation > 0: - self.add_instruction(t, {'function': functions.piecewise_accel(round(t + duration, 10) - round(t, 10), initial, final), 'description': 'piecewise linear accelleration ramp', - 'initial time': t, 'end time': t + truncation*duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": functions.piecewise_accel( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "piecewise linear accelleration ramp", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) return truncation*duration - def square_wave(self, t, duration, amplitude, frequency, phase, offset, - duty_cycle, samplerate, units=None, truncation=1.): + def square_wave( + self, + t, + duration, + amplitude, + frequency, + phase, + offset, + duty_cycle, + samplerate, + units=None, + truncation=1. + ): """A standard square wave. This method generates a square wave which starts HIGH (when its phase is @@ -879,9 +1156,19 @@ def square_wave(self, t, duration, amplitude, frequency, phase, offset, truncation, ) - def square_wave_levels(self, t, duration, level_0, level_1, frequency, - phase, duty_cycle, samplerate, units=None, - truncation=1.): + def square_wave_levels( + self, + t, + duration, + level_0, + level_1, + frequency, + phase, + duty_cycle, + samplerate, + units=None, + truncation=1. + ): """A standard square wave. This method generates a square wave which starts at `level_0` (when its @@ -933,11 +1220,10 @@ def square_wave_levels(self, t, duration, level_0, level_1, frequency, # Check the argument values. self._check_truncation(truncation) if duty_cycle < 0 or duty_cycle > 1: - msg = """Square wave duty cycle must be in the range [0, 1] - (inclusively) but was set to {duty_cycle}.""".format( - duty_cycle=duty_cycle + raise LabscriptError( + "Square wave duty cycle must be in the range [0, 1] (inclusively) but " + f"was set to {duty_cycle}." ) - raise LabscriptError(dedent(msg)) if truncation > 0: # Add the instruction. @@ -952,12 +1238,12 @@ def square_wave_levels(self, t, duration, level_0, level_1, frequency, self.add_instruction( t, { - 'function': func, - 'description': 'square wave', - 'initial time': t, - 'end time': t + truncation * duration, - 'clock rate': samplerate, - 'units': units, + "function": func, + "description": "square wave", + "initial time": t, + "end time": t + truncation * duration, + "clock rate": samplerate, + "units": units, } ) return truncation * duration @@ -980,22 +1266,33 @@ def customramp(self, t, duration, function, *args, **kwargs): float: Duration the function is to be evaluate for. Equivalent to `truncation*duration`. """ - units = kwargs.pop('units', None) - samplerate = kwargs.pop('samplerate') - truncation = kwargs.pop('truncation', 1.) + units = kwargs.pop("units", None) + samplerate = kwargs.pop("samplerate") + truncation = kwargs.pop("truncation", 1.) self._check_truncation(truncation) def custom_ramp_func(t_rel): """The function that will return the result of the user's function, evaluated at relative times t_rel from 0 to duration""" - return function(t_rel, round(t + duration, 10) - round(t, 10), *args, **kwargs) + return function( + t_rel, round(t + duration, 10) - round(t, 10), *args, **kwargs + ) if truncation > 0: - self.add_instruction(t, {'function': custom_ramp_func, 'description': 'custom ramp: %s' % function.__name__, - 'initial time': t, 'end time': t + truncation*duration, 'clock rate': samplerate, 'units': units}) + self.add_instruction( + t, + { + "function": custom_ramp_func, + "description": f"custom ramp: {function.__name__}", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) return truncation*duration - def constant(self,t,value,units=None): + def constant(self, t, value, units=None): """Sets the output to a constant value at time `t`. Args: @@ -1007,13 +1304,16 @@ def constant(self,t,value,units=None): try: val = float(value) except: - raise LabscriptError('in constant, value cannot be converted to float') + raise LabscriptError( + f"Cannot set {self.name} to value={value} at t={t} as the value cannot " + "be converted to float" + ) self.add_instruction(t, value, units) - - + + class AnalogOut(AnalogQuantity): """Analog Output class for use with all devices that support timed analog outputs.""" - description = 'analog output' + description = "analog output" class StaticAnalogQuantity(Output): @@ -1021,7 +1321,7 @@ class StaticAnalogQuantity(Output): It can also be used internally by other more complex output types. """ - description = 'static analog quantity' + description = "static analog quantity" default_value = 0.0 """float: Value of output if no constant value is commanded.""" @@ -1038,7 +1338,7 @@ def __init__(self, *args, **kwargs): """ Output.__init__(self, *args, **kwargs) self._static_value = None - + def constant(self, value, units=None): """Set the static output value of the output. @@ -1059,23 +1359,33 @@ def constant(self, value, units=None): if self.limits: minval, maxval = self.limits if not minval <= value <= maxval: - raise LabscriptError('You cannot program the value %s (base units) to %s as it falls outside the limits (%s to %s)'%(str(value), self.name, str(self.limits[0]), str(self.limits[1]))) + raise LabscriptError( + f"You cannot program the value {value} (base units) to " + f"{self.name} as it falls outside the limits " + f"({self.limits[0]} to {self.limits[1]})" + ) self._static_value = value else: - raise LabscriptError('%s %s has already been set to %s (base units). It cannot also be set to %s (%s).'%(self.description, self.name, str(self._static_value), str(value),units if units is not None else "base units")) - + raise LabscriptError( + f"{self.description} {self.name} has already been set to " + f"{self._static_value} (base units). It cannot also be set to " + f"{value} ({units if units is not None else 'base units'})." + ) + def get_change_times(self): """Enforces no change times. Returns: list: An empty list, as expected by the parent pseudoclock. """ - return [] # Return an empty list as the calling function at the pseudoclock level expects a list - + # Return an empty list as the calling function at the pseudoclock level expects + # a list + return [] + def make_timeseries(self,change_times): """Since output is static, does nothing.""" pass - + def expand_timeseries(self,*args,**kwargs): """Defines the `raw_output` attribute. """ @@ -1086,26 +1396,30 @@ def static_value(self): """float: The value of the static output.""" if self._static_value is None: if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write(' '.join(['WARNING:', self.name, 'has no value set. It will be set to %s.\n'%self.instruction_to_string(self.default_value)])) + sys.stderr.write( + f"WARNING: {self.name} has no value set. It will be set to " + f"{self.instruction_to_string(self.default_value)}.\n" + ) self._static_value = self.default_value return self._static_value - + + class StaticAnalogOut(StaticAnalogQuantity): """Static Analog Output class for use with all devices that have constant outputs.""" - description = 'static analog output' + description = "static analog output" class DigitalQuantity(Output): """Base class for :obj:`DigitalOut`. It is also used internally by other, more complex, output types. """ - description = 'digital quantity' - allowed_states = {1:'high', 0:'low'} + description = "digital quantity" + allowed_states = {1: "high", 0: "low"} default_value = 0 - dtype = uint32 + dtype = np.uint32 # Redefine __init__ so that you cannot define a limit or calibration for DO - @set_passed_properties(property_names = {"connection_table_properties": ["inverted"]}) + @set_passed_properties(property_names={"connection_table_properties": ["inverted"]}) def __init__(self, name, parent_device, connection, inverted=False, **kwargs): """Instantiate a digital quantity. @@ -1119,7 +1433,7 @@ def __init__(self, name, parent_device, connection, inverted=False, **kwargs): Output.__init__(self,name,parent_device,connection, **kwargs) self.inverted = bool(inverted) - def go_high(self,t): + def go_high(self, t): """Commands the output to go high. Args: @@ -1127,7 +1441,7 @@ def go_high(self,t): """ self.add_instruction(t, 1) - def go_low(self,t): + def go_low(self, t): """Commands the output to go low. Args: @@ -1135,7 +1449,7 @@ def go_low(self,t): """ self.add_instruction(t, 0) - def enable(self,t): + def enable(self, t): """Commands the output to enable. If `inverted=True`, this will set the output low. @@ -1148,7 +1462,7 @@ def enable(self,t): else: self.go_high(t) - def disable(self,t): + def disable(self, t): """Commands the output to disable. If `inverted=True`, this will set the output high. @@ -1161,8 +1475,8 @@ def disable(self,t): else: self.go_low(t) - def repeat_pulse_sequence(self,t,duration,pulse_sequence,period,samplerate): - '''This function only works if the DigitalQuantity is on a fast clock + def repeat_pulse_sequence(self, t, duration, pulse_sequence, period, samplerate): + """This function only works if the DigitalQuantity is on a fast clock The pulse sequence specified will be repeated from time t until t+duration. @@ -1183,27 +1497,36 @@ def repeat_pulse_sequence(self,t,duration,pulse_sequence,period,samplerate): repeating the pulse sequence. In general, should be longer than the entire pulse sequence. samplerate (float): How often to update the output, in Hz. - ''' - self.add_instruction(t, {'function': functions.pulse_sequence(pulse_sequence,period), 'description':'pulse sequence', - 'initial time':t, 'end time': t + duration, 'clock rate': samplerate, 'units': None}) - + """ + self.add_instruction( + t, + { + "function": functions.pulse_sequence(pulse_sequence, period), + "description": "pulse sequence", + "initial time":t, + "end time": t + duration, + "clock rate": samplerate, + "units": None, + } + ) + return duration - + class DigitalOut(DigitalQuantity): """Digital output class for use with all devices.""" - description = 'digital output' + description = "digital output" + - class StaticDigitalQuantity(DigitalQuantity): """Base class for :obj:`StaticDigitalOut`. It can also be used internally by other, more complex, output types. """ - description = 'static digital quantity' + description = "static digital quantity" default_value = 0 """float: Value of output if no constant value is commanded.""" - + @set_passed_properties(property_names = {}) def __init__(self, *args, **kwargs): """Instatiantes the static digital quantity. @@ -1217,7 +1540,7 @@ def __init__(self, *args, **kwargs): """ DigitalQuantity.__init__(self, *args, **kwargs) self._static_value = None - + def go_high(self): """Command a static high output. @@ -1233,7 +1556,7 @@ def go_high(self): f"{self.instruction_to_string(self._static_value)}. It cannot " "also be set to 1." ) - + def go_low(self): """Command a static low output. @@ -1249,20 +1572,22 @@ def go_low(self): f"{self.instruction_to_string(self._static_value)}. It cannot " "also be set to 0." ) - + def get_change_times(self): """Enforces no change times. Returns: list: An empty list, as expected by the parent pseudoclock. """ - return [] # Return an empty list as the calling function at the pseudoclock level expects a list - - def make_timeseries(self,change_times): + # Return an empty list as the calling function at the pseudoclock level expects + # a list + return [] + + def make_timeseries(self, change_times): """Since output is static, does nothing.""" pass - def expand_timeseries(self,*args,**kwargs): + def expand_timeseries(self, *args, **kwargs): """Defines the `raw_output` attribute. """ self.raw_output = array([self.static_value], dtype=self.dtype) @@ -1272,21 +1597,27 @@ def static_value(self): """float: The value of the static output.""" if self._static_value is None: if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write(' '.join(['WARNING:', self.name, 'has no value set. It will be set to %s.\n'%self.instruction_to_string(self.default_value)])) + sys.stderr.write( + f"WARNING: {self.name} has no value set. It will be set to " + f"{self.instruction_to_string(self.default_value)}.\n" + ) self._static_value = self.default_value return self._static_value - + class StaticDigitalOut(StaticDigitalQuantity): """Static Digital Output class for use with all devices that have constant outputs.""" - description = 'static digital output' - + description = "static digital output" + + class AnalogIn(Device): """Analog Input for use with all devices that have an analog input.""" - description = 'Analog Input' - - @set_passed_properties(property_names = {}) - def __init__(self,name,parent_device,connection,scale_factor=1.0,units='Volts',**kwargs): + description = "Analog Input" + + @set_passed_properties(property_names={}) + def __init__( + self, name, parent_device, connection, scale_factor=1.0, units="Volts", **kwargs + ): """Instantiates an Analog Input. Args: @@ -1299,9 +1630,11 @@ def __init__(self,name,parent_device,connection,scale_factor=1.0,units='Volts',* self.acquisitions = [] self.scale_factor = scale_factor self.units=units - Device.__init__(self,name,parent_device,connection, **kwargs) - - def acquire(self,label,start_time,end_time,wait_label='',scale_factor=None,units=None): + Device.__init__(self, name, parent_device, connection, **kwargs) + + def acquire( + self, label, start_time, end_time, wait_label="", scale_factor=None, units=None + ): """Command an acquisition for this input. Args: @@ -1319,8 +1652,16 @@ def acquire(self,label,start_time,end_time,wait_label='',scale_factor=None,units scale_factor = self.scale_factor if units is None: units = self.units - self.acquisitions.append({'start_time': start_time, 'end_time': end_time, - 'label': label, 'wait_label':wait_label, 'scale_factor':scale_factor,'units':units}) + self.acquisitions.append( + { + "start_time": start_time, + "end_time": end_time, + "label": label, + "wait_label": wait_label, + "scale_factor": scale_factor, + "units": units, + } + ) return end_time - start_time @@ -1339,13 +1680,14 @@ class Shutter(DigitalOut): moving earlier than that. This means the initial shutter states will have imprecise timing. """ - description = 'shutter' - + description = "shutter" + @set_passed_properties( - property_names = {"connection_table_properties": ["open_state"]} - ) - def __init__(self,name,parent_device,connection,delay=(0,0),open_state=1, - **kwargs): + property_names={"connection_table_properties": ["open_state"]} + ) + def __init__( + self, name, parent_device, connection, delay=(0, 0), open_state=1, **kwargs + ): """Instantiates a Shutter. Args: @@ -1362,32 +1704,40 @@ def __init__(self,name,parent_device,connection,delay=(0,0),open_state=1, Raises: LabscriptError: If the `open_state` is not `0` or `1`. """ - DigitalOut.__init__(self, name, parent_device, connection, inverted=not bool(open_state), **kwargs) + inverted = not bool(open_state) + DigitalOut.__init__( + self, name, parent_device, connection, inverted=inverted, **kwargs + ) self.open_delay, self.close_delay = delay self.open_state = open_state if self.open_state == 1: - self.allowed_states = {0: 'closed', 1: 'open'} + self.allowed_states = {0: "closed", 1: "open"} elif self.open_state == 0: - self.allowed_states = {1: 'closed', 0: 'open'} + self.allowed_states = {1: "closed", 0: "open"} else: - raise LabscriptError("Shutter %s wasn't instantiated with open_state = 0 or 1." % self.name) + raise LabscriptError( + f"Shutter {self.name} wasn't instantiated with open_state = 0 or 1." + ) self.actual_times = {} - - # If a shutter is asked to do something at t=0, it cannot start moving - # earlier than that. So initial shutter states will have imprecise - # timing. Not throwing a warning here because if I did, every run - # would throw a warning for every shutter. The documentation will - # have to make a point of this. def open(self, t): """Command the shutter to open at time `t`. Takes the open delay time into account. + Note that the delay time will not be take into account the open delay if the + command is made at t=0 (or other times less than the open delay). No warning + will be issued for this loss of precision during compilation. + Args: t (float): Time, in seconds, when shutter should be open. """ + # If a shutter is asked to do something at t=0, it cannot start moving + # earlier than that. So initial shutter states will have imprecise + # timing. Not throwing a warning here because if I did, every run + # would throw a warning for every shutter. The documentation will + # have to make a point of this. t_calc = t-self.open_delay if t >= self.open_delay else 0 - self.actual_times[t] = {'time': t_calc, 'instruction': 1} + self.actual_times[t] = {"time": t_calc, "instruction": 1} self.enable(t_calc) def close(self, t): @@ -1395,57 +1745,72 @@ def close(self, t): Takes the close delay time into account. + Note that the delay time will not be take into account the close delay if the + command is made at t=0 (or other times less than the close delay). No warning + will be issued for this loss of precision during compilation. + Args: t (float): Time, in seconds, when shutter should be closed. """ t_calc = t-self.close_delay if t >= self.close_delay else 0 - self.actual_times[t] = {'time': t_calc, 'instruction': 0} + self.actual_times[t] = {"time": t_calc, "instruction": 0} self.disable(t_calc) def generate_code(self, hdf5_file): classname = self.__class__.__name__ - calibration_table_dtypes = [('name','a256'), ('open_delay',float), ('close_delay',float)] - if classname not in hdf5_file['calibrations']: - hdf5_file['calibrations'].create_dataset(classname, (0,), dtype=calibration_table_dtypes, maxshape=(None,)) - metadata = (self.name,self.open_delay,self.close_delay) - dataset = hdf5_file['calibrations'][classname] - dataset.resize((len(dataset)+1,)) - dataset[len(dataset)-1] = metadata + calibration_table_dtypes = [ + ("name", "a256"), ("open_delay", float), ("close_delay", float) + ] + if classname not in hdf5_file["calibrations"]: + hdf5_file["calibrations"].create_dataset( + classname, (0,), dtype=calibration_table_dtypes, maxshape=(None,) + ) + metadata = (self.name, self.open_delay, self.close_delay) + dataset = hdf5_file["calibrations"][classname] + dataset.resize((len(dataset) + 1,)) + dataset[len(dataset) - 1] = metadata def get_change_times(self, *args, **kwargs): retval = DigitalOut.get_change_times(self, *args, **kwargs) - if len(self.actual_times)>1: + if len(self.actual_times) > 1: sorted_times = list(self.actual_times.keys()) sorted_times.sort() - for i in range(len(sorted_times)-1): + for i in range(len(sorted_times) - 1): time = sorted_times[i] - next_time = sorted_times[i+1] + next_time = sorted_times[i + 1] + instruction = self.actual_times[time]["instruction"] + next_instruction = self.actual_times[next_time]["instruction"] + state = "opened" if instruction == 1 else "closed" + next_state = "open" if next_instruction == 1 else "close" # only look at instructions that contain a state change - if self.actual_times[time]['instruction'] != self.actual_times[next_time]['instruction']: - state1 = 'open' if self.actual_times[next_time]['instruction'] == 1 else 'close' - state2 = 'opened' if self.actual_times[time]['instruction'] == 1 else 'closed' - if self.actual_times[next_time]['time'] < self.actual_times[time]['time']: - message = "WARNING: The shutter '{:s}' is requested to {:s} too early (taking delay into account) at t={:.10f}s when it is still not {:s} from an earlier instruction at t={:.10f}s".format(self.name, state1, next_time, state2, time) - sys.stderr.write(message+'\n') + if instruction != next_instruction: + if self.actual_times[next_time]["time"] < self.actual_times[time]["time"]: + sys.stderr.write( + f"WARNING: The shutter '{self.name}' is requested to " + f"{next_state} too early (taking delay into account) at " + f"t={next_time:.10f}s when it is still not {state} from " + f"an earlier instruction at t={time:.10f}s\n" + ) elif not config.suppress_mild_warnings and not config.suppress_all_warnings: - state1 = 'open' if self.actual_times[next_time]['instruction'] == 1 else 'close' - state2 = 'opened' if self.actual_times[time]['instruction'] == 0 else 'closed' - message = "WARNING: The shutter '{:s}' is requested to {:s} at t={:.10f}s but was never {:s} after an earlier instruction at t={:.10f}s".format(self.name, state1, next_time, state2, time) - sys.stderr.write(message+'\n') + sys.stderr.write( + f"WARNING: The shutter '{self.name}' is requested to " + f"{next_state} at t={next_time:.10f}s but was never {state} " + f"after an earlier instruction at t={time:.10f}s\n" + ) return retval class Trigger(DigitalOut): """Customized version of :obj:`DigitalOut` that tracks edge type. """ - description = 'trigger device' - allowed_states = {1:'high', 0:'low'} + description = "trigger device" allowed_children = [TriggerableDevice] - @set_passed_properties(property_names = {}) - def __init__(self, name, parent_device, connection, trigger_edge_type='rising', - **kwargs): + @set_passed_properties(property_names={}) + def __init__( + self, name, parent_device, connection, trigger_edge_type="rising", **kwargs + ): """Instantiates a DigitalOut object that tracks the trigger edge type. Args: @@ -1455,22 +1820,24 @@ def __init__(self, name, parent_device, connection, trigger_edge_type='rising', **kwargs: Passed to :func:`Output.__init__`. """ - DigitalOut.__init__(self,name,parent_device,connection, **kwargs) + DigitalOut.__init__(self, name, parent_device, connection, **kwargs) self.trigger_edge_type = trigger_edge_type - if self.trigger_edge_type == 'rising': + if self.trigger_edge_type == "rising": self.enable = self.go_high self.disable = self.go_low - self.allowed_states = {1:'enabled', 0:'disabled'} - elif self.trigger_edge_type == 'falling': + self.allowed_states = {1: "enabled", 0: "disabled"} + elif self.trigger_edge_type == "falling": self.enable = self.go_low self.disable = self.go_high - self.allowed_states = {1:'disabled', 0:'enabled'} + self.allowed_states = {1: "disabled", 0: "enabled"} else: - raise ValueError('trigger_edge_type must be \'rising\' or \'falling\', not \'%s\'.'%trigger_edge_type) + raise ValueError( + "trigger_edge_type must be 'rising' or 'falling', not " + f"'{trigger_edge_type}'." + ) # A list of the times this trigger has been asked to trigger: self.triggerings = [] - - + def trigger(self, t, duration): """Command a trigger pulse. @@ -1481,28 +1848,33 @@ def trigger(self, t, duration): assert duration > 0, "Negative or zero trigger duration given" if t != self.t0 and self.t0 not in self.instructions: self.disable(self.t0) - + start = t end = t + duration for other_start, other_duration in self.triggerings: other_end = other_start + other_duration # Check for overlapping exposures: if not (end < other_start or start > other_end): - raise LabscriptError('%s %s has two overlapping triggerings: ' %(self.description, self.name) + \ - 'one at t = %fs for %fs, and another at t = %fs for %fs.'%(start, duration, other_start, other_duration)) + raise LabscriptError( + f"{self.description} {self.name} has two overlapping triggerings: " + f"one at t = {start}s for {duration}s, and another at " + f"t = {other_start}s for {other_duration}s." + ) self.enable(t) - self.disable(round(t + duration,10)) + self.disable(round(t + duration, 10)) self.triggerings.append((t, duration)) def add_device(self, device): - if not device.connection == 'trigger': - raise LabscriptError('The \'connection\' string of device %s '%device.name + - 'to %s must be \'trigger\', not \'%s\''%(self.name, repr(device.connection))) + if device.connection != "trigger": + raise LabscriptError( + f"The 'connection' string of device {device.name} " + f"to {self.name} must be 'trigger', not '{device.connection}'" + ) DigitalOut.add_device(self, device) - + class WaitMonitor(Trigger): - + @set_passed_properties(property_names={}) def __init__( self, @@ -1605,14 +1977,32 @@ class DDSQuantity(Device): amplitude, and phase of the output as :obj:`AnalogQuantity`. It can also have a gate, which provides enable/disable control of the output as :obj:`DigitalOut`. + + This class instantiates channels for frequency/amplitude/phase (and optionally the + gate) itself. """ description = 'DDS' - allowed_children = [AnalogQuantity,DigitalOut,DigitalQuantity] # Adds its own children when initialised + allowed_children = [AnalogQuantity, DigitalOut, DigitalQuantity] - @set_passed_properties(property_names = {}) - def __init__(self, name, parent_device, connection, digital_gate={}, freq_limits=None, freq_conv_class=None, freq_conv_params={}, - amp_limits=None, amp_conv_class=None, amp_conv_params={}, phase_limits=None, phase_conv_class=None, phase_conv_params = {}, - call_parents_add_device = True, **kwargs): + @set_passed_properties(property_names={}) + def __init__( + self, + name, + parent_device, + connection, + digital_gate=None, + freq_limits=None, + freq_conv_class=None, + freq_conv_params=None, + amp_limits=None, + amp_conv_class=None, + amp_conv_params=None, + phase_limits=None, + phase_conv_class=None, + phase_conv_params=None, + call_parents_add_device=True, + **kwargs + ): """Instantiates a DDS quantity. Args: @@ -1645,18 +2035,19 @@ def __init__(self, name, parent_device, connection, digital_gate={}, freq_limits call_parents_add_device (bool, optional): Have the parent device run its `add_device` method. **kwargs: Keyword arguments passed to :func:`Device.__init__`. - """ - #self.clock_type = parent_device.clock_type # Don't see that this is needed anymore - + """ # Here we set call_parents_add_device=False so that we # can do additional initialisation before manually calling # self.parent_device.add_device(self). This allows the parent's # add_device method to perform checks based on the code below, # whilst still providing us with the checks and attributes that # Device.__init__ gives us in the meantime. - Device.__init__(self, name, parent_device, connection, call_parents_add_device=False, **kwargs) - - # Ask the parent device if it has default unit conversion classes it would like us to use: + Device.__init__( + self, name, parent_device, connection, call_parents_add_device=False, **kwargs + ) + + # Ask the parent device if it has default unit conversion classes it would like + # us to use: if hasattr(parent_device, 'get_default_unit_conversion_classes'): classes = self.parent_device.get_default_unit_conversion_classes(self) default_freq_conv, default_amp_conv, default_phase_conv = classes @@ -1669,20 +2060,45 @@ def __init__(self, name, parent_device, connection, digital_gate={}, freq_limits amp_conv_class = default_amp_conv if phase_conv_class is None: phase_conv_class = default_phase_conv - - self.frequency = AnalogQuantity(self.name + '_freq', self, 'freq', freq_limits, freq_conv_class, freq_conv_params) - self.amplitude = AnalogQuantity(self.name + '_amp', self, 'amp', amp_limits, amp_conv_class, amp_conv_params) - self.phase = AnalogQuantity(self.name + '_phase', self, 'phase', phase_limits, phase_conv_class, phase_conv_params) + + self.frequency = AnalogQuantity( + f"{self.name}_freq", + self, + "freq", + freq_limits, + freq_conv_class, + freq_conv_params, + ) + self.amplitude = AnalogQuantity( + f"{self.name}_amp", + self, + "amp", + amp_limits, + amp_conv_class, + amp_conv_params, + ) + self.phase = AnalogQuantity( + f"{self.name}_phase", + self, + "phase", + phase_limits, + phase_conv_class, + phase_conv_params, + ) self.gate = None - if 'device' in digital_gate and 'connection' in digital_gate: - dev = digital_gate.pop('device') - conn = digital_gate.pop('connection') - self.gate = DigitalOut(name + '_gate', dev, conn, **digital_gate) + digital_gate = digital_gate or {} + if "device" in digital_gate and "connection" in digital_gate: + dev = digital_gate.pop("device") + conn = digital_gate.pop("connection") + self.gate = DigitalOut(f"{name}_gate", dev, conn, **digital_gate) # Did they only put one key in the dictionary, or use the wrong keywords? elif len(digital_gate) > 0: - raise LabscriptError('You must specify the "device" and "connection" for the digital gate of %s.' % (self.name)) - + raise LabscriptError( + 'You must specify the "device" and "connection" for the digital gate ' + f"of {self.name}." + ) + # If the user has not specified a gate, and the parent device # supports gating of DDS output, it should add a gate to this # instance in its add_device method, which is called below. If @@ -1696,7 +2112,7 @@ def __init__(self, name, parent_device, connection, digital_gate={}, freq_limits # e.g., see PulseBlasterDDS in PulseBlaster.py if call_parents_add_device: self.parent_device.add_device(self) - + def setamp(self, t, value, units=None): """Set the amplitude of the output. @@ -1706,7 +2122,7 @@ def setamp(self, t, value, units=None): units: Units that the value is defined in. """ self.amplitude.constant(t, value, units) - + def setfreq(self, t, value, units=None): """Set the frequency of the output. @@ -1716,7 +2132,7 @@ def setfreq(self, t, value, units=None): units: Units that the value is defined in. """ self.frequency.constant(t, value, units) - + def setphase(self, t, value, units=None): """Set the phase of the output. @@ -1726,7 +2142,7 @@ def setphase(self, t, value, units=None): units: Units that the value is defined in. """ self.phase.constant(t, value, units) - + def enable(self, t): """Enable the Output. @@ -1737,7 +2153,10 @@ def enable(self, t): LabscriptError: If the DDS is not instantiated with a digital gate. """ if self.gate is None: - raise LabscriptError('DDS %s does not have a digital gate, so you cannot use the enable(t) method.' % (self.name)) + raise LabscriptError( + f"DDS {self.name} does not have a digital gate, so you cannot use the " + "enable(t) method." + ) self.gate.go_high(t) def disable(self, t): @@ -1750,10 +2169,24 @@ def disable(self, t): LabscriptError: If the DDS is not instantiated with a digital gate. """ if self.gate is None: - raise LabscriptError('DDS %s does not have a digital gate, so you cannot use the disable(t) method.' % (self.name)) + raise LabscriptError( + f"DDS {self.name} does not have a digital gate, so you cannot use the " + "disable(t) method." + ) self.gate.go_low(t) - - def pulse(self, t, duration, amplitude, frequency, phase=None, amplitude_units = None, frequency_units = None, phase_units = None, print_summary=False): + + def pulse( + self, + t, + duration, + amplitude, + frequency, + phase=None, + amplitude_units=None, + frequency_units=None, + phase_units=None, + print_summary=False, + ): """Pulse the output. Args: @@ -1772,7 +2205,10 @@ def pulse(self, t, duration, amplitude, frequency, phase=None, amplitude_units = float: Duration of the pulse, in seconds. """ if print_summary: - functions.print_time(t, '%s pulse at %.4f MHz for %.3f ms' % (self.name, frequency/MHz, duration/ms)) + functions.print_time( + t, + f"{self.name} pulse at {frequency/MHz:.4f} MHz for {duration/ms:.3f} ms", + ) self.setamp(t, amplitude, amplitude_units) if frequency is not None: self.setfreq(t, frequency, frequency_units) @@ -1784,18 +2220,34 @@ def pulse(self, t, duration, amplitude, frequency, phase=None, amplitude_units = self.setamp(t + duration, 0) return duration + class DDS(DDSQuantity): """DDS class for use with all devices that have DDS-like outputs.""" - pass + class StaticDDS(Device): """Static DDS class for use with all devices that have static DDS-like outputs.""" - description = 'Static RF' + description = "Static RF" allowed_children = [StaticAnalogQuantity,DigitalOut,StaticDigitalOut] - + @set_passed_properties(property_names = {}) - def __init__(self,name,parent_device,connection,digital_gate = {},freq_limits = None,freq_conv_class = None,freq_conv_params = {},amp_limits=None,amp_conv_class = None,amp_conv_params = {},phase_limits=None,phase_conv_class = None,phase_conv_params = {}, - **kwargs): + def __init__( + self, + name, + parent_device, + connection, + digital_gate=None, + freq_limits=None, + freq_conv_class=None, + freq_conv_params=None, + amp_limits=None, + amp_conv_class=None, + amp_conv_params=None, + phase_limits=None, + phase_conv_class=None, + phase_conv_params=None, + **kwargs, + ): """Instantiates a Static DDS quantity. Args: @@ -1829,14 +2281,14 @@ def __init__(self,name,parent_device,connection,digital_gate = {},freq_limits = its `add_device` method. **kwargs: Keyword arguments passed to :func:`Device.__init__`. """ - #self.clock_type = parent_device.clock_type # Don't see that this is needed anymore - # We tell Device.__init__ to not call # self.parent.add_device(self), we'll do that ourselves later # after further intitialisation, so that the parent can see the # freq/amp/phase objects and manipulate or check them from within # its add_device method. - Device.__init__(self,name,parent_device,connection, call_parents_add_device=False, **kwargs) + Device.__init__( + self, name, parent_device, connection, call_parents_add_device=False, **kwargs + ) # Ask the parent device if it has default unit conversion classes it would like us to use: if hasattr(parent_device, 'get_default_unit_conversion_classes'): @@ -1852,21 +2304,46 @@ def __init__(self,name,parent_device,connection,digital_gate = {},freq_limits = if phase_conv_class is None: phase_conv_class = default_phase_conv - self.frequency = StaticAnalogQuantity(self.name+'_freq',self,'freq',freq_limits,freq_conv_class,freq_conv_params) - self.amplitude = StaticAnalogQuantity(self.name+'_amp',self,'amp',amp_limits,amp_conv_class,amp_conv_params) - self.phase = StaticAnalogQuantity(self.name+'_phase',self,'phase',phase_limits,phase_conv_class,phase_conv_params) - - if 'device' in digital_gate and 'connection' in digital_gate: - dev = digital_gate.pop('device') - conn = digital_gate.pop('connection') - self.gate = DigitalOut(name + '_gate', dev, conn, **digital_gate) + self.frequency = StaticAnalogQuantity( + f"{self.name}_freq", + self, + "freq", + freq_limits, + freq_conv_class, + freq_conv_params + ) + self.amplitude = StaticAnalogQuantity( + f"{self.name}_amp", + self, + "amp", + amp_limits, + amp_conv_class, + amp_conv_params, + ) + self.phase = StaticAnalogQuantity( + f"{self.name}_phase", + self, + "phase", + phase_limits, + phase_conv_class, + phase_conv_params, + ) + + digital_gate = digital_gate or {} + if "device" in digital_gate and "connection" in digital_gate: + dev = digital_gate.pop("device") + conn = digital_gate.pop("connection") + self.gate = DigitalOut(f"{name}_gate", dev, conn, **digital_gate) # Did they only put one key in the dictionary, or use the wrong keywords? elif len(digital_gate) > 0: - raise LabscriptError('You must specify the "device" and "connection" for the digital gate of %s.'%(self.name)) + raise LabscriptError( + 'You must specify the "device" and "connection" for the digital gate ' + f"of {self.name}" + ) # Now we call the parent's add_device method. This is a must, since we didn't do so earlier from Device.__init__. self.parent_device.add_device(self) - - def setamp(self,value,units=None): + + def setamp(self, value, units=None): """Set the static amplitude of the output. Args: @@ -1874,8 +2351,8 @@ def setamp(self,value,units=None): units: Units that the value is defined in. """ self.amplitude.constant(value,units) - - def setfreq(self,value,units=None): + + def setfreq(self, value, units=None): """Set the static frequency of the output. Args: @@ -1883,8 +2360,8 @@ def setfreq(self,value,units=None): units: Units that the value is defined in. """ self.frequency.constant(value,units) - - def setphase(self,value,units=None): + + def setphase(self, value, units=None): """Set the static phase of the output. Args: @@ -1892,8 +2369,8 @@ def setphase(self,value,units=None): units: Units that the value is defined in. """ self.phase.constant(value,units) - - def enable(self,t=None): + + def enable(self, t=None): """Enable the Output. Args: @@ -1905,9 +2382,12 @@ def enable(self,t=None): if self.gate: self.gate.go_high(t) else: - raise LabscriptError('DDS %s does not have a digital gate, so you cannot use the enable(t) method.'%(self.name)) - - def disable(self,t=None): + raise LabscriptError( + f"DDS {self.name} does not have a digital gate, so you cannot use the " + "enable(t) method." + ) + + def disable(self, t=None): """Disable the Output. Args: @@ -1919,7 +2399,10 @@ def disable(self,t=None): if self.gate: self.gate.go_low(t) else: - raise LabscriptError('DDS %s does not have a digital gate, so you cannot use the disable(t) method.'%(self.name)) + raise LabscriptError( + f"DDS {self.name} does not have a digital gate, so you cannot use the " + "disable(t) method." + ) def save_time_markers(hdf5_file): """Save shot time markers to the shot file. From 6d4535b0555b16e7c83fd48a6314b14792336c38 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Wed, 3 Jan 2024 20:40:40 +1100 Subject: [PATCH 06/19] * Move `Output` classes to a separate file. * `config` also refactored to be inside `compiler`. * constants moved into their own file. * warnings context managers moved into the `utils` file --- labscript/compiler.py | 7 + labscript/constants.py | 8 + labscript/core.py | 2 +- labscript/labscript.py | 2289 +--------------------------------------- labscript/outputs.py | 2188 ++++++++++++++++++++++++++++++++++++++ labscript/utils.py | 37 + 6 files changed, 2285 insertions(+), 2246 deletions(-) create mode 100644 labscript/constants.py create mode 100644 labscript/outputs.py diff --git a/labscript/compiler.py b/labscript/compiler.py index 321d30f..ff25d64 100644 --- a/labscript/compiler.py +++ b/labscript/compiler.py @@ -62,5 +62,12 @@ def reset(self): self.save_git_info = _SAVE_GIT_INFO self.shot_properties = {} + # This used to be in a separate config object, but it's been moved here so it + # gets reset + self.suppress_mild_warnings = True + self.suppress_all_warnings = False + self.compression = 'gzip' # set to 'gzip' for compression + + compiler = Compiler() """The compiler instance""" diff --git a/labscript/constants.py b/labscript/constants.py new file mode 100644 index 0000000..907bc5b --- /dev/null +++ b/labscript/constants.py @@ -0,0 +1,8 @@ +ns = 1e-9 +us = 1e-6 +ms = 1e-3 +s = 1 +Hz = 1 +kHz = 1e3 +MHz = 1e6 +GHz = 1e9 \ No newline at end of file diff --git a/labscript/core.py b/labscript/core.py index a906d1a..bf79c8c 100644 --- a/labscript/core.py +++ b/labscript/core.py @@ -399,7 +399,7 @@ def expand_change_times(self, all_change_times, change_times, outputs_by_clockli if maxrate: # If there was ramping at this timestep, how many clock ticks fit before the next instruction? - n_ticks, remainder = divmod((all_change_times[i+1] - time)*maxrate, 1) + n_ticks, remainder = np.divmod((all_change_times[i+1] - time)*maxrate, 1) n_ticks = int(n_ticks) # Can we squeeze the final clock cycle in at the end? if remainder and remainder/float(maxrate) >= 1/float(local_clock_limit): diff --git a/labscript/labscript.py b/labscript/labscript.py index 91fcb30..5f87f80 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -12,7 +12,6 @@ ##################################################################### import builtins -import contextlib import os import sys import subprocess @@ -30,22 +29,6 @@ # The code to be removed relates to the move of the globals loading code from # labscript to runmanager batch compiler. -from .base import Device -from .compiler import compiler -from .core import ( - ClockLine, - IntermediateDevice, - Pseudoclock, - PseudoclockDevice, - TriggerableDevice, -) -from .utils import ( - LabscriptError, - bitfield, - fastflatten, - max_or_zero, - set_passed_properties -) import labscript_utils.h5_lock, h5py import labscript_utils.properties from labscript_utils.filewatcher import FileWatcher @@ -68,14 +51,43 @@ except ImportError: sys.stderr.write('Warning: Failed to import unit conversion classes\n') -ns = 1e-9 -us = 1e-6 -ms = 1e-3 -s = 1 -Hz = 1 -kHz = 1e3 -MHz = 1e6 -GHz = 1e9 +# Imports to maintain backwards compatibility with previous module interface +from .base import Device +from .compiler import compiler +from .constants import * +from .core import ( + ClockLine, + IntermediateDevice, + Pseudoclock, + PseudoclockDevice, + TriggerableDevice, +) +from .outputs import ( + Output, + AnalogIn, + AnalogOut, + AnalogQuantity, + DDS, + DDSQuantity, + DigitalOut, + DigitalQuantity, + Shutter, + StaticDDS, + StaticAnalogOut, + StaticAnalogQuantity, + StaticDigitalOut, + StaticDigitalQuantity, + Trigger, +) +from .utils import ( + LabscriptError, + bitfield, + fastflatten, + max_or_zero, + set_passed_properties, + suppress_all_warnings, + suppress_mild_warnings +) # Create a reference to the builtins dict # update this if accessing builtins ever changes @@ -88,1791 +100,12 @@ else: startupinfo = None - -class config(object): - suppress_mild_warnings = True - suppress_all_warnings = False - compression = 'gzip' # set to 'gzip' for compression - - -@contextlib.contextmanager() -def suppress_mild_warnings(state=True): - """A context manager which modifies config.suppress_mild_warnings - - Allows the user to suppress (or show) mild warnings for specific lines. Useful when - you want to hide/show all warnings from specific lines. - - Arguments: - state (bool): The new state for ``config.suppress_mild_warnings``. Defaults to - ``True`` if not explicitly provided. - """ - previous_warning_setting = config.suppress_mild_warnings - config.suppress_mild_warnings = state - yield - config.suppress_mild_warnings = previous_warning_setting - - -@contextlib.contextmanager() -def suppress_all_warnings(state=True): - """A context manager which modifies config.suppress_all_warnings - - Allows the user to suppress (or show) all warnings for specific lines. Useful when - you want to hide/show all warnings from specific lines. - - Arguments: - state (bool): The new state for ``config.suppress_all_warnings``. Defaults to - ``True`` if not explicitly provided. - """ - previous_warning_setting = config.suppress_all_warnings - config.suppress_all_warnings = state - yield - config.suppress_all_warnings = previous_warning_setting - - +# Alias for backwards compatibility. The config has been moved into the compiler with +# the other settings. +config = compiler no_warnings = suppress_all_warnings # Historical alias -class Output(Device): - """Base class for all output classes.""" - description = "generic output" - allowed_states = None - dtype = np.float64 - scale_factor = 1 - - @set_passed_properties(property_names={}) - def __init__( - self, - name, - parent_device, - connection, - limits=None, - unit_conversion_class=None, - unit_conversion_parameters=None, - default_value=None, - **kwargs - ): - """Instantiate an Output. - - Args: - name (str): python variable name to assign the Output to. - parent_device (:obj:`IntermediateDevice`): Parent device the output - is connected to. - connection (str): Channel of parent device output is connected to. - limits (tuple, optional): `(min,max)` allowed for the output. - unit_conversion_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): - Unit concersion class to use for the output. - unit_conversion_parameters (dict, optional): Dictonary or kwargs to - pass to the unit conversion class. - default_value (float, optional): Default value of the output if no - output is commanded. - **kwargs: Passed to :meth:`Device.__init__`. - - Raises: - LabscriptError: Limits tuple is invalid or unit conversion class - units don't line up. - """ - Device.__init__(self,name,parent_device,connection, **kwargs) - - self.instructions = {} - self.ramp_limits = [] # For checking ramps don't overlap - if default_value is not None: - self.default_value = default_value - if not unit_conversion_parameters: - unit_conversion_parameters = {} - self.unit_conversion_class = unit_conversion_class - self.set_properties( - unit_conversion_parameters, - {"unit_conversion_parameters": list(unit_conversion_parameters.keys())} - ) - - # Instantiate the calibration - if unit_conversion_class is not None: - self.calibration = unit_conversion_class(unit_conversion_parameters) - # Validate the calibration class - for units in self.calibration.derived_units: - # Does the conversion to base units function exist for each defined unit - # type? - if not hasattr(self.calibration, f"{units}_to_base"): - raise LabscriptError( - f'The function "{units}_to_base" does not exist within the ' - f'calibration "{self.unit_conversion_class}" used in output ' - f'"{self.name}"' - ) - # Does the conversion to base units function exist for each defined unit - # type? - if not hasattr(self.calibration, f"{units}_from_base"): - raise LabscriptError( - f'The function "{units}_from_base" does not exist within the ' - f'calibration "{self.unit_conversion_class}" used in output ' - f'"{self.name}"' - ) - - # If limits exist, check they are valid - # Here we specifically differentiate "None" from False as we will later have a - # conditional which relies on self.limits being either a correct tuple, or - # "None" - if limits is not None: - if not isinstance(limits, tuple) or len(limits) != 2: - raise LabscriptError( - f'The limits for "{self.name}" must be tuple of length 2. ' - 'Eg. limits=(1, 2)' - ) - if limits[0] > limits[1]: - raise LabscriptError( - "The first element of the tuple must be lower than the second " - "element. Eg limits=(1, 2), NOT limits=(2, 1)" - ) - # Save limits even if they are None - self.limits = limits - - @property - def clock_limit(self): - """float: Returns the parent clock line's clock limit.""" - parent = self.parent_clock_line - return parent.clock_limit - - @property - def trigger_delay(self): - """float: The earliest time output can be commanded from this device after a trigger. - This is nonzeo on secondary pseudoclocks due to triggering delays.""" - parent = self.pseudoclock_device - if parent.is_master_pseudoclock: - return 0 - else: - return parent.trigger_delay - - @property - def wait_delay(self): - """float: The earliest time output can be commanded from this device after a wait. - This is nonzeo on secondary pseudoclocks due to triggering delays and the fact - that the master clock doesn't provide a resume trigger to secondary clocks until - a minimum time has elapsed: compiler.wait_delay. This is so that if a wait is - extremely short, the child clock is actually ready for the trigger. - """ - delay = compiler.wait_delay if self.pseudoclock_device.is_master_pseudoclock else 0 - return self.trigger_delay + delay - - def get_all_outputs(self): - """Get all children devices that are outputs. - - For ``Output``, this is `self`. - - Returns: - list: List of children :obj:`Output`. - """ - return [self] - - def apply_calibration(self,value,units): - """Apply the calibration defined by the unit conversion class, if present. - - Args: - value (float): Value to apply calibration to. - units (str): Units to convert to. Must be defined by the unit - conversion class. - - Returns: - float: Converted value. - - Raises: - LabscriptError: If no unit conversion class is defined or `units` not - in that class. - """ - # Is a calibration in use? - if self.unit_conversion_class is None: - raise LabscriptError( - 'You can not specify the units in an instruction for output ' - f'"{self.name}" as it does not have a calibration associated with it' - ) - - # Does a calibration exist for the units specified? - if units not in self.calibration.derived_units: - raise LabscriptError( - f'The units "{units}" does not exist within the calibration ' - f'"{self.unit_conversion_class}" used in output "{self.name}"' - ) - - # Return the calibrated value - return getattr(self.calibration,units+"_to_base")(value) - - def instruction_to_string(self,instruction): - """Gets a human readable description of an instruction. - - Args: - instruction (dict or str): Instruction to get description of, - or a fixed instruction defined in :attr:`allowed_states`. - - Returns: - str: Instruction description. - """ - if isinstance(instruction, dict): - return instruction["description"] - elif self.allowed_states: - return str(self.allowed_states[instruction]) - else: - return str(instruction) - - def add_instruction(self, time, instruction, units=None): - """Adds a hardware instruction to the device instruction list. - - Args: - time (float): Time, in seconds, that the instruction begins. - instruction (dict or float): Instruction to add. - units (str, optional): Units instruction is in, if `instruction` - is a `float`. - - Raises: - LabscriptError: If time requested is not allowed or samplerate - is too fast. - """ - if not compiler.start_called: - raise LabscriptError("Cannot add instructions prior to calling start()") - # round to the nearest 0.1 nanoseconds, to prevent floating point - # rounding errors from breaking our equality checks later on. - time = round(time, 10) - # Also round end time of ramps to the nearest 0.1 ns: - if isinstance(instruction,dict): - instruction["end time"] = round(instruction["end time"], 10) - instruction["initial time"] = round(instruction["initial time"], 10) - # Check that time is not negative or too soon after t=0: - if time < self.t0: - raise LabscriptError( - f"{self.description} {self.name} has an instruction at t={time}s. " - "Due to the delay in triggering its pseudoclock device, the earliest " - f"output possible is at t={self.t0}." - ) - # Check that this doesn't collide with previous instructions: - if time in self.instructions.keys(): - if not config.suppress_all_warnings: - current_value = self.instruction_to_string(self.instructions[time]) - new_value = self.instruction_to_string( - self.apply_calibration(instruction, units) - if units and not isinstance(instruction, dict) - else instruction - ) - sys.stderr.write( - f"WARNING: State of {self.description} {self.name} at t={time}s " - f"has already been set to {current_value}. Overwriting to " - f"{new_value}. (note: all values in base units where relevant)" - "\n" - ) - # Check that ramps don't collide - if isinstance(instruction, dict): - # No ramps allowed if this output is on a slow clock: - if not self.parent_clock_line.ramping_allowed: - raise LabscriptError( - f"{self.description} {self.name} is on clockline that does not " - "support ramping. It cannot have a function ramp as an instruction." - ) - for start, end in self.ramp_limits: - if start < time < end or start < instruction["end time"] < end: - start_value = self.instruction_to_string(self.instructions[start]) - new_value = self.instruction_to_string(instruction) - raise LabscriptError( - f"State of {self.description} {self.name} from t = {start}s to " - f"{end}s has already been set to {start_value}. Cannot set to " - f"{new_value} from t = {time}s to {instruction['end time']}s." - ) - self.ramp_limits.append((time, instruction["end time"])) - # Check that start time is before end time: - if time > instruction["end time"]: - raise LabscriptError( - f"{self.description} {self.name} has been passed a function ramp " - f"{self.instruction_to_string(instruction)} with a negative " - "duration." - ) - if instruction["clock rate"] == 0: - raise LabscriptError("A nonzero sample rate is required.") - # Else we have a "constant", single valued instruction - else: - # If we have units specified, convert the value - if units is not None: - # Apply the unit calibration now - instruction = self.apply_calibration(instruction, units) - # if we have limits, check the value is valid - if self.limits: - if (instruction < self.limits[0]) or (instruction > self.limits[1]): - raise LabscriptError( - f"You cannot program the value {instruction} (base units) to " - f"{self.name} as it falls outside the limits " - f"({self.limits[0]} to {self.limits[1]})" - ) - self.instructions[time] = instruction - - def do_checks(self, trigger_times): - """Basic error checking to ensure the user's instructions make sense. - - Args: - trigger_times (iterable): Times to confirm don't conflict with - instructions. - - Raises: - LabscriptError: If a trigger time conflicts with an instruction. - """ - # Check if there are no instructions. Generate a warning and insert an - # instruction telling the output to remain at its default value. - if not self.instructions: - if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write( - f"WARNING: {self.name} has no instructions. It will be set to " - f"{self.instruction_to_string(self.default_value)} for all time.\n" - ) - self.add_instruction(self.t0, self.default_value) - # Check if there are no instructions at the initial time. Generate a warning and insert an - # instruction telling the output to start at its default value. - if self.t0 not in self.instructions.keys(): - if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write( - f"WARNING: {self.name} has no initial instruction. It will " - "initially be set to " - f"{self.instruction_to_string(self.default_value)}.\n" - ) - self.add_instruction(self.t0, self.default_value) - # Check that ramps have instructions following them. - # If they don't, insert an instruction telling them to hold their final value. - for instruction in list(self.instructions.values()): - if ( - isinstance(instruction, dict) - and instruction["end time"] not in self.instructions.keys() - ): - self.add_instruction( - instruction["end time"], - instruction["function"]( - instruction["end time"] - instruction["initial time"] - ), - instruction["units"], - ) - # Checks for trigger times: - for trigger_time in trigger_times: - for t, inst in self.instructions.items(): - # Check no ramps are happening at the trigger time: - if ( - isinstance(inst, dict) - and inst["initial time"] < trigger_time - and inst["end time"] > trigger_time - ): - raise LabscriptError( - f"{self.description} {self.name} has a ramp " - f"{inst['description']} from t = {inst['initial time']} to " - f"{inst['end time']}. This overlaps with a trigger at " - f"t={trigger_time}, and so cannot be performed." - ) - # Check that nothing is happening during the delay time after the trigger: - if ( - round(trigger_time, 10) - < round(t, 10) - < round(trigger_time + self.trigger_delay, 10) - ): - raise LabscriptError( - f"{self.description} {self.name} has an instruction at t={t}. " - f"This is too soon after a trigger at t={trigger_time}, " - "the earliest output possible after this trigger is at " - f"t={trigger_time + self.trigger_delay}" - ) - # Check that there are no instructions too soon before the trigger: - if 0 < trigger_time - t < max(self.clock_limit, compiler.wait_delay): - raise LabscriptError( - f"{self.description} {self.name} has an instruction at t={t}. " - f"This is too soon before a trigger at t={trigger_time}, " - "the latest output possible before this trigger is at " - f"t={trigger_time - max(self.clock_limit, compiler.wait_delay)}" - ) - - def offset_instructions_from_trigger(self, trigger_times): - """Subtracts self.trigger_delay from all instructions at or after each trigger_time. - - Args: - trigger_times (iterable): Times of all trigger events. - """ - offset_instructions = {} - for t, instruction in self.instructions.items(): - # How much of a delay is there for this instruction? That depends how many triggers there are prior to it: - n_triggers_prior = len([time for time in trigger_times if time < t]) - # The cumulative offset at this point in time: - offset = self.trigger_delay * n_triggers_prior + trigger_times[0] - offset = round(offset, 10) - if isinstance(instruction, dict): - offset_instruction = instruction.copy() - offset_instruction["end time"] = self.quantise_to_pseudoclock( - round(instruction["end time"] - offset, 10) - ) - offset_instruction["initial time"] = self.quantise_to_pseudoclock( - round(instruction["initial time"] - offset, 10) - ) - else: - offset_instruction = instruction - - new_time = self.quantise_to_pseudoclock(round(t - offset, 10)) - offset_instructions[new_time] = offset_instruction - self.instructions = offset_instructions - - # offset each of the ramp_limits for use in the calculation within - # Pseudoclock/ClockLine so that the times in list are consistent with the ones - # in self.instructions - for i, times in enumerate(self.ramp_limits): - n_triggers_prior = len([time for time in trigger_times if time < times[0]]) - # The cumulative offset at this point in time: - offset = self.trigger_delay * n_triggers_prior + trigger_times[0] - offset = round(offset, 10) - - # offset start and end time of ramps - # NOTE: This assumes ramps cannot proceed across a trigger command - # (for instance you cannot ramp an output across a WAIT) - self.ramp_limits[i] = ( - self.quantise_to_pseudoclock(round(times[0] - offset, 10)), - self.quantise_to_pseudoclock(round(times[1] - offset, 10)), - ) - - def get_change_times(self): - """If this function is being called, it means that the parent - Pseudoclock has requested a list of times that this output changes - state. - - Returns: - list: List of times output changes values. - """ - times = list(self.instructions.keys()) - times.sort() - - current_dict_time = None - for time in times: - if isinstance(self.instructions[time], dict) and current_dict_time is None: - current_dict_time = self.instructions[time] - elif ( - current_dict_time is not None - and current_dict_time['initial time'] < time < current_dict_time['end time'] - ): - raise LabscriptError( - f"{self.description} {self.name} has an instruction at " - f"t={time:.10f}s. This instruction collides with a ramp on this " - "output at that time. The collision " - f"{current_dict_time['description']} is happening from " - f"{current_dict_time['initial time']:.10f}s untill " - f"{current_dict_time['end time']:.10f}s" - ) - - self.times = times - return times - - def get_ramp_times(self): - """If this is being called, then it means the parent Pseuedoclock - has asked for a list of the output ramp start and stop times. - - Returns: - list: List of (start, stop) times of ramps for this Output. - """ - return self.ramp_limits - - def make_timeseries(self, change_times): - """If this is being called, then it means the parent Pseudoclock - has asked for a list of this output's states at each time in - change_times. (Which are the times that one or more connected - outputs in the same pseudoclock change state). By state, I don't - mean the value of the output at that moment, rather I mean what - instruction it has. This might be a single value, or it might - be a reference to a function for a ramp etc. This list of states - is stored in self.timeseries rather than being returned.""" - self.timeseries = [] - i = 0 - time_len = len(self.times) - for change_time in change_times: - while i < time_len and change_time >= self.times[i]: - i += 1 - self.timeseries.append(self.instructions[self.times[i-1]]) - - def expand_timeseries(self,all_times,flat_all_times_len): - """This function evaluates the ramp functions in self.timeseries - at the time points in all_times, and creates an array of output - values at those times. These are the values that this output - should update to on each clock tick, and are the raw values that - should be used to program the output device. They are stored - in self.raw_output.""" - # If this output is not ramping, then its timeseries should - # not be expanded. It's already as expanded as it'll get. - if not self.parent_clock_line.ramping_allowed: - self.raw_output = np.array(self.timeseries, dtype=np.dtype(self.dtype)) - return - outputarray = np.empty((flat_all_times_len,), dtype=np.dtype(self.dtype)) - j = 0 - for i, time in enumerate(all_times): - if np.iterable(time): - time_len = len(time) - if isinstance(self.timeseries[i], dict): - # We evaluate the functions at the midpoints of the - # timesteps in order to remove the zero-order hold - # error introduced by sampling an analog signal: - try: - midpoints = time + 0.5*(time[1] - time[0]) - except IndexError: - # Time array might be only one element long, so we - # can't calculate the step size this way. That's - # ok, the final midpoint is determined differently - # anyway: - midpoints = zeros(1) - # We need to know when the first clock tick is after - # this ramp ends. It's either an array element or a - # single number depending on if this ramp is followed - # by another ramp or not: - next_time = all_times[i+1][0] if iterable(all_times[i+1]) else all_times[i+1] - midpoints[-1] = time[-1] + 0.5*(next_time - time[-1]) - outarray = self.timeseries[i]["function"]( - midpoints - self.timeseries[i]["initial time"] - ) - # Now that we have the list of output points, pass them through the unit calibration - if self.timeseries[i]["units"] is not None: - outarray = self.apply_calibration( - outarray, self.timeseries[i]["units"] - ) - # if we have limits, check the value is valid - if self.limits: - if ((outarrayself.limits[1])).any(): - raise LabscriptError( - f"The function {self.timeseries[i]['function']} called " - f'on "{self.name}" at t={midpoints[0]} generated a ' - "value which falls outside the base unit limits " - f"({self.limits[0]} to {self.limits[1]})" - ) - else: - outarray = empty(time_len, dtype=self.dtype) - outarray.fill(self.timeseries[i]) - outputarray[j:j+time_len] = outarray - j += time_len - else: - outputarray[j] = self.timeseries[i] - j += 1 - del self.timeseries # don't need this any more. - self.raw_output = outputarray - - -class AnalogQuantity(Output): - """Base class for :obj:`AnalogOut`. - - It is also used internally by :obj:`DDS`. You should never instantiate this - class directly. - """ - description = "analog quantity" - default_value = 0 - - def _check_truncation(self, truncation, min=0, max=1): - if not (min <= truncation <= max): - raise LabscriptError( - f"Truncation argument must be between {min} and {max} (inclusive), but " - f"is {truncation}." - ) - - def ramp(self, t, duration, initial, final, samplerate, units=None, truncation=1.): - """Command the output to perform a linear ramp. - - Defined by - `f(t) = ((final - initial)/duration)*t + initial` - - Args: - t (float): Time, in seconds, to begin the ramp. - duration (float): Length, in seconds, of the ramp. - initial (float): Initial output value, at time `t`. - final (float): Final output value, at time `t+duration`. - samplerate (float): Rate, in Hz, to update the output. - units: Units the output values are given in, as specified by the - unit conversion class. - truncation (float, optional): Fraction of ramp to perform. Must be between 0 and 1. - - Returns: - float: Length of time ramp will take to complete. - """ - self._check_truncation(truncation) - if truncation > 0: - # if start and end value are the same, we don't need to ramp and can save - # the sample ticks etc - if initial == final: - self.constant(t, initial, units) - if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write( - f"WARNING: {self.__class__.__name__} '{self.name}' has the " - f"same initial and final value at time t={t:.10f}s with " - f"duration {duration:.10f}s. In order to save samples and " - "clock ticks this instruction is replaced with a constant " - "output.\n" - ) - else: - self.add_instruction( - t, - { - "function": functions.ramp( - round(t + duration, 10) - round(t, 10), initial, final - ), - "description": "linear ramp", - "initial time": t, - "end time": t + truncation * duration, - "clock rate": samplerate, - "units": units, - } - ) - return truncation * duration - - def sine( - self, - t, - duration, - amplitude, - angfreq, - phase, - dc_offset, - samplerate, - units=None, - truncation=1. - ): - """Command the output to perform a sinusoidal modulation. - - Defined by - `f(t) = amplitude*sin(angfreq*t + phase) + dc_offset` - - Args: - t (float): Time, in seconds, to begin the ramp. - duration (float): Length, in seconds, of the ramp. - amplitude (float): Amplitude of the modulation. - angfreq (float): Angular frequency, in radians per second. - phase (float): Phase offset of the sine wave, in radians. - dc_offset (float): DC offset of output away from 0. - samplerate (float): Rate, in Hz, to update the output. - units: Units the output values are given in, as specified by the - unit conversion class. - truncation (float, optional): Fraction of duration to perform. Must be between 0 and 1. - - Returns: - float: Length of time modulation will take to complete. Equivalent to `truncation*duration`. - """ - self._check_truncation(truncation) - if truncation > 0: - self.add_instruction( - t, - { - "function": functions.sine( - round(t + duration, 10) - round(t, 10), - amplitude, - angfreq, - phase, - dc_offset, - ), - "description": "sine wave", - "initial time": t, - "end time": t + truncation*duration, - "clock rate": samplerate, - "units": units, - } - ) - return truncation*duration - - def sine_ramp( - self, t, duration, initial, final, samplerate, units=None, truncation=1. - ): - """Command the output to perform a ramp defined by one half period of a squared sine wave. - - Defined by - `f(t) = (final-initial)*(sin(pi*t/(2*duration)))^2 + initial` - - Args: - t (float): Time, in seconds, to begin the ramp. - duration (float): Length, in seconds, of the ramp. - initial (float): Initial output value, at time `t`. - final (float): Final output value, at time `t+duration`. - samplerate (float): Rate, in Hz, to update the output. - units: Units the output values are given in, as specified by the - unit conversion class. - truncation (float, optional): Fraction of ramp to perform. Must be between 0 and 1. - - Returns: - float: Length of time ramp will take to complete. - """ - self._check_truncation(truncation) - if truncation > 0: - self.add_instruction( - t, - { - "function": functions.sine_ramp( - round(t + duration, 10) - round(t, 10), initial, final - ), - "description": "sinusoidal ramp", - "initial time": t, - "end time": t + truncation*duration, - "clock rate": samplerate, - "units": units, - } - ) - return truncation*duration - - def sine4_ramp( - self, t, duration, initial, final, samplerate, units=None, truncation=1. - ): - """Command the output to perform an increasing ramp defined by one half period of a quartic sine wave. - - Defined by - `f(t) = (final-initial)*(sin(pi*t/(2*duration)))^4 + initial` - - Args: - t (float): Time, in seconds, to begin the ramp. - duration (float): Length, in seconds, of the ramp. - initial (float): Initial output value, at time `t`. - final (float): Final output value, at time `t+duration`. - samplerate (float): Rate, in Hz, to update the output. - units: Units the output values are given in, as specified by the - unit conversion class. - truncation (float, optional): Fraction of ramp to perform. Must be between 0 and 1. - - Returns: - float: Length of time ramp will take to complete. - """ - self._check_truncation(truncation) - if truncation > 0: - self.add_instruction( - t, - { - "function": functions.sine4_ramp( - round(t + duration, 10) - round(t, 10), initial, final - ), - "description": "sinusoidal ramp", - "initial time": t, - "end time": t + truncation*duration, - "clock rate": samplerate, - "units": units, - } - ) - return truncation*duration - - def sine4_reverse_ramp( - self, t, duration, initial, final, samplerate, units=None, truncation=1. - ): - """Command the output to perform a decreasing ramp defined by one half period of a quartic sine wave. - - Defined by - `f(t) = (final-initial)*(sin(pi*t/(2*duration)))^4 + initial` - - Args: - t (float): Time, in seconds, to begin the ramp. - duration (float): Length, in seconds, of the ramp. - initial (float): Initial output value, at time `t`. - final (float): Final output value, at time `t+duration`. - samplerate (float): Rate, in Hz, to update the output. - units: Units the output values are given in, as specified by the - unit conversion class. - truncation (float, optional): Fraction of ramp to perform. Must be between 0 and 1. - - Returns: - float: Length of time ramp will take to complete. - """ - self._check_truncation(truncation) - if truncation > 0: - self.add_instruction( - t, - { - "function": functions.sine4_reverse_ramp( - round(t + duration, 10) - round(t, 10), initial, final - ), - "description": "sinusoidal ramp", - "initial time": t, - "end time": t + truncation*duration, - "clock rate": samplerate, - "units": units, - } - ) - return truncation*duration - - def exp_ramp( - self, - t, - duration, - initial, - final, - samplerate, - zero=0, - units=None, - truncation=None, - truncation_type="linear", - **kwargs, - ): - """Exponential ramp whose rate of change is set by an asymptotic value (zero argument). - - Args: - t (float): time to start the ramp - duration (float): duration of the ramp - initial (float): initial value of the ramp (sans truncation) - final (float): final value of the ramp (sans truncation) - zero (float): asymptotic value of the exponential decay/rise, i.e. limit as t --> inf - samplerate (float): rate to sample the function - units: unit conversion to apply to specified values before generating raw output - truncation_type (str): - - * `'linear'` truncation stops the ramp when it reaches the value given by the - truncation parameter, which must be between initial and final - * `'exponential'` truncation stops the ramp after a period of truncation*duration - In this instance, the truncation parameter should be between 0 (full truncation) - and 1 (no truncation). - - """ - # Backwards compatibility for old kwarg names - if "trunc" in kwargs: - truncation = kwargs.pop("trunc") - if "trunc_type" in kwargs: - truncation_type = kwargs.pop("trunc_type") - if truncation is not None: - # Computed the truncated duration based on the truncation_type - if truncation_type == "linear": - self._check_truncation( - truncation, min(initial, final), max(initial, final) - ) - # Truncate the ramp when it reaches the value truncation - trunc_duration = duration * \ - np.log((initial-zero)/(truncation-zero)) / \ - np.log((initial-zero)/(final-zero)) - elif truncation_type == "exponential": - # Truncate the ramps duration by a fraction truncation - self._check_truncation(truncation) - trunc_duration = truncation * duration - else: - raise LabscriptError( - "Truncation type for exp_ramp not supported. Must be either linear " - "or exponential." - ) - else: - trunc_duration = duration - if trunc_duration > 0: - self.add_instruction( - t, - { - "function": functions.exp_ramp( - round(t + duration, 10) - round(t, 10), initial, final, zero - ), - "description": 'exponential ramp', - "initial time": t, - "end time": t + trunc_duration, - "clock rate": samplerate, - "units": units, - } - ) - return trunc_duration - - def exp_ramp_t( - self, - t, - duration, - initial, - final, - time_constant, - samplerate, - units=None, - truncation=None, - truncation_type="linear", - **kwargs - ): - """Exponential ramp whose rate of change is set by the time_constant. - - Args: - t (float): time to start the ramp - duration (float): duration of the ramp - initial (float): initial value of the ramp (sans truncation) - final (float): final value of the ramp (sans truncation) - time_constant (float): 1/e time of the exponential decay/rise - samplerate (float): rate to sample the function - units: unit conversion to apply to specified values before generating raw output - truncation_type (str): - - * `'linear'` truncation stops the ramp when it reaches the value given by the - truncation parameter, which must be between initial and final - * `'exponential'` truncation stops the ramp after a period of truncation*duration - In this instance, the truncation parameter should be between 0 (full truncation) - and 1 (no truncation). - - """ - # Backwards compatibility for old kwarg names - if "trunc" in kwargs: - truncation = kwargs.pop("trunc") - if "trunc_type" in kwargs: - truncation_type = kwargs.pop("trunc_type") - if truncation is not None: - zero = (final-initial*np.exp(-duration/time_constant)) / \ - (1-np.exp(-duration/time_constant)) - if truncation_type == "linear": - self._check_truncation(truncation, min(initial, final), max(initial, final)) - trunc_duration = time_constant * \ - np.log((initial-zero)/(truncation-zero)) - elif truncation_type == 'exponential': - self._check_truncation(truncation) - trunc_duration = truncation * duration - else: - raise LabscriptError( - "Truncation type for exp_ramp_t not supported. Must be either " - "linear or exponential." - ) - else: - trunc_duration = duration - if trunc_duration > 0: - self.add_instruction( - t, - { - "function": functions.exp_ramp_t( - round(t + duration, 10) - round(t, 10), - initial, - final, - time_constant, - ), - "description": "exponential ramp with time consntant", - "initial time": t, - "end time": t + trunc_duration, - "clock rate": samplerate, - "units": units, - } - ) - return trunc_duration - - def piecewise_accel_ramp( - self, t, duration, initial, final, samplerate, units=None, truncation=1. - ): - """Changes the output so that the second derivative follows one period of a triangle wave. - - Args: - t (float): Time, in seconds, at which to begin the ramp. - duration (float): Duration of the ramp, in seconds. - initial (float): Initial output value at time `t`. - final (float): Final output value at time `t+duration`. - samplerate (float): Update rate of the output, in Hz. - units: Units, defined by the unit conversion class, the value is in. - truncation (float, optional): Fraction of ramp to perform. Default 1.0. - - Returns: - float: Time the ramp will take to complete. - """ - self._check_truncation(truncation) - if truncation > 0: - self.add_instruction( - t, - { - "function": functions.piecewise_accel( - round(t + duration, 10) - round(t, 10), initial, final - ), - "description": "piecewise linear accelleration ramp", - "initial time": t, - "end time": t + truncation*duration, - "clock rate": samplerate, - "units": units, - } - ) - return truncation*duration - - def square_wave( - self, - t, - duration, - amplitude, - frequency, - phase, - offset, - duty_cycle, - samplerate, - units=None, - truncation=1. - ): - """A standard square wave. - - This method generates a square wave which starts HIGH (when its phase is - zero) then transitions to/from LOW at the specified `frequency` in Hz. - The `amplitude` parameter specifies the peak-to-peak amplitude of the - square wave which is centered around `offset`. For example, setting - `amplitude=1` and `offset=0` would give a square wave which transitions - between `0.5` and `-0.5`. Similarly, setting `amplitude=2` and - `offset=3` would give a square wave which transitions between `4` and - `2`. To instead specify the HIGH/LOW levels directly, use - `square_wave_levels()`. - - Note that because the transitions of a square wave are sudden and - discontinuous, small changes in timings (e.g. due to numerical rounding - errors) can affect the output value. This is particularly relevant at - the end of the waveform, as the final output value may be different than - expected if the end of the waveform is close to an edge of the square - wave. Care is taken in the implementation of this method to avoid such - effects, but it still may be desirable to call `constant()` after - `square_wave()` to ensure a particular final value. The output value may - also be different than expected at certain moments in the middle of the - waveform due to the finite samplerate (which may be different than the - requested `samplerate`), particularly if the actual samplerate is not a - multiple of `frequency`. - - Args: - t (float): The time at which to start the square wave. - duration (float): The duration for which to output a square wave - when `truncation` is set to `1`. When `truncation` is set to a - value less than `1`, the actual duration will be shorter than - `duration` by that factor. - amplitude (float): The peak-to-peak amplitude of the square wave. - See above for an example of how to calculate the HIGH/LOW output - values given the `amplitude` and `offset` values. - frequency (float): The frequency of the square wave, in Hz. - phase (float): The initial phase of the square wave. Note that the - square wave is defined such that the phase goes from 0 to 1 (NOT - 2 pi) over one cycle, so setting `phase=0.5` will start the - square wave advanced by 1/2 of a cycle. Setting `phase` equal to - `duty_cycle` will cause the waveform to start LOW rather than - HIGH. - offset (float): The offset of the square wave, which is the value - halfway between the LOW and HIGH output values. Note that this - is NOT the LOW output value; setting `offset` to `0` will cause - the HIGH/LOW values to be symmetrically split around `0`. See - above for an example of how to calculate the HIGH/LOW output - values given the `amplitude` and `offset` values. - duty_cycle (float): The fraction of the cycle for which the output - should be HIGH. This should be a number between zero and one - inclusively. For example, setting `duty_cycle=0.1` will - create a square wave which outputs HIGH over 10% of the - cycle and outputs LOW over 90% of the cycle. - samplerate (float): The requested rate at which to update the output - value. Note that the actual samplerate used may be different if, - for example, another output of the same device has a - simultaneous ramp with a different requested `samplerate`, or if - `1 / samplerate` isn't an integer multiple of the pseudoclock's - timing resolution. - units (str, optional): The units of the output values. If set to - `None` then the output's base units will be used. Defaults to - `None`. - truncation (float, optional): The actual duration of the square wave - will be `duration * truncation` and `truncation` must be set to - a value in the range [0, 1] (inclusively). Set to `1` to output - the full duration of the square wave. Setting it to `0` will - skip the square wave entirely. Defaults to `1.`. - - Returns: - duration (float): The actual duration of the square wave, accounting - for `truncation`. - """ - # Convert to values used by square_wave_levels, then call that method. - level_0 = offset + 0.5 * amplitude - level_1 = offset - 0.5 * amplitude - return self.square_wave_levels( - t, - duration, - level_0, - level_1, - frequency, - phase, - duty_cycle, - samplerate, - units, - truncation, - ) - - def square_wave_levels( - self, - t, - duration, - level_0, - level_1, - frequency, - phase, - duty_cycle, - samplerate, - units=None, - truncation=1. - ): - """A standard square wave. - - This method generates a square wave which starts at `level_0` (when its - phase is zero) then transitions to/from `level_1` at the specified - `frequency`. This is the same waveform output by `square_wave()`, but - parameterized differently. See that method's docstring for more - information. - - Args: - t (float): The time at which to start the square wave. - duration (float): The duration for which to output a square wave - when `truncation` is set to `1`. When `truncation` is set to a - value less than `1`, the actual duration will be shorter than - `duration` by that factor. - level_0 (float): The initial level of the square wave, when the - phase is zero. - level_1 (float): The other level of the square wave. - frequency (float): The frequency of the square wave, in Hz. - phase (float): The initial phase of the square wave. Note that the - square wave is defined such that the phase goes from 0 to 1 (NOT - 2 pi) over one cycle, so setting `phase=0.5` will start the - square wave advanced by 1/2 of a cycle. Setting `phase` equal to - `duty_cycle` will cause the waveform to start at `level_1` - rather than `level_0`. - duty_cycle (float): The fraction of the cycle for which the output - should be set to `level_0`. This should be a number between zero - and one inclusively. For example, setting `duty_cycle=0.1` will - create a square wave which outputs `level_0` over 10% of the - cycle and outputs `level_1` over 90% of the cycle. - samplerate (float): The requested rate at which to update the output - value. Note that the actual samplerate used may be different if, - for example, another output of the same device has a - simultaneous ramp with a different requested `samplerate`, or if - `1 / samplerate` isn't an integer multiple of the pseudoclock's - timing resolution. - units (str, optional): The units of the output values. If set to - `None` then the output's base units will be used. Defaults to - `None`. - truncation (float, optional): The actual duration of the square wave - will be `duration * truncation` and `truncation` must be set to - a value in the range [0, 1] (inclusively). Set to `1` to output - the full duration of the square wave. Setting it to `0` will - skip the square wave entirely. Defaults to `1.`. - - Returns: - duration (float): The actual duration of the square wave, accounting - for `truncation`. - """ - # Check the argument values. - self._check_truncation(truncation) - if duty_cycle < 0 or duty_cycle > 1: - raise LabscriptError( - "Square wave duty cycle must be in the range [0, 1] (inclusively) but " - f"was set to {duty_cycle}." - ) - - if truncation > 0: - # Add the instruction. - func = functions.square_wave( - round(t + duration, 10) - round(t, 10), - level_0, - level_1, - frequency, - phase, - duty_cycle, - ) - self.add_instruction( - t, - { - "function": func, - "description": "square wave", - "initial time": t, - "end time": t + truncation * duration, - "clock rate": samplerate, - "units": units, - } - ) - return truncation * duration - - def customramp(self, t, duration, function, *args, **kwargs): - """Define a custom function for the output. - - Args: - t (float): Time, in seconds, to start the function. - duration (float): Length in time, in seconds, to perform the function. - function (func): Function handle that defines the output waveform. - First argument is the relative time from function start, in seconds. - *args: Arguments passed to `function`. - **kwargs: Keyword arguments pass to `function`. - Standard kwargs common to other output functions are: `units`, - `samplerate`, and `truncation`. These kwargs are optional, but will - not be passed to `function` if present. - - Returns: - float: Duration the function is to be evaluate for. Equivalent to - `truncation*duration`. - """ - units = kwargs.pop("units", None) - samplerate = kwargs.pop("samplerate") - truncation = kwargs.pop("truncation", 1.) - self._check_truncation(truncation) - - def custom_ramp_func(t_rel): - """The function that will return the result of the user's function, - evaluated at relative times t_rel from 0 to duration""" - return function( - t_rel, round(t + duration, 10) - round(t, 10), *args, **kwargs - ) - - if truncation > 0: - self.add_instruction( - t, - { - "function": custom_ramp_func, - "description": f"custom ramp: {function.__name__}", - "initial time": t, - "end time": t + truncation*duration, - "clock rate": samplerate, - "units": units, - } - ) - return truncation*duration - - def constant(self, t, value, units=None): - """Sets the output to a constant value at time `t`. - - Args: - t (float): Time, in seconds, to set the constant output. - value (float): Value to set. - units: Units, defined by the unit conversion class, the value is in. - """ - # verify that value can be converted to float - try: - val = float(value) - except: - raise LabscriptError( - f"Cannot set {self.name} to value={value} at t={t} as the value cannot " - "be converted to float" - ) - self.add_instruction(t, value, units) - - -class AnalogOut(AnalogQuantity): - """Analog Output class for use with all devices that support timed analog outputs.""" - description = "analog output" - - -class StaticAnalogQuantity(Output): - """Base class for :obj:`StaticAnalogOut`. - - It can also be used internally by other more complex output types. - """ - description = "static analog quantity" - default_value = 0.0 - """float: Value of output if no constant value is commanded.""" - - @set_passed_properties(property_names = {}) - def __init__(self, *args, **kwargs): - """Instatiantes the static analog quantity. - - Defines an internal tracking variable of the static output value and - calls :func:`Output.__init__`. - - Args: - *args: Passed to :func:`Output.__init__`. - **kwargs: Passed to :func:`Output.__init__`. - """ - Output.__init__(self, *args, **kwargs) - self._static_value = None - - def constant(self, value, units=None): - """Set the static output value of the output. - - Args: - value (float): Value to set the output to. - units: Units, defined by the unit conversion class, the value is in. - - Raises: - LabscriptError: If static output has already been set to another value - or the value lies outside the output limits. - """ - if self._static_value is None: - # If we have units specified, convert the value - if units is not None: - # Apply the unit calibration now - value = self.apply_calibration(value, units) - # if we have limits, check the value is valid - if self.limits: - minval, maxval = self.limits - if not minval <= value <= maxval: - raise LabscriptError( - f"You cannot program the value {value} (base units) to " - f"{self.name} as it falls outside the limits " - f"({self.limits[0]} to {self.limits[1]})" - ) - self._static_value = value - else: - raise LabscriptError( - f"{self.description} {self.name} has already been set to " - f"{self._static_value} (base units). It cannot also be set to " - f"{value} ({units if units is not None else 'base units'})." - ) - - def get_change_times(self): - """Enforces no change times. - - Returns: - list: An empty list, as expected by the parent pseudoclock. - """ - # Return an empty list as the calling function at the pseudoclock level expects - # a list - return [] - - def make_timeseries(self,change_times): - """Since output is static, does nothing.""" - pass - - def expand_timeseries(self,*args,**kwargs): - """Defines the `raw_output` attribute. - """ - self.raw_output = array([self.static_value], dtype=self.dtype) - - @property - def static_value(self): - """float: The value of the static output.""" - if self._static_value is None: - if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write( - f"WARNING: {self.name} has no value set. It will be set to " - f"{self.instruction_to_string(self.default_value)}.\n" - ) - self._static_value = self.default_value - return self._static_value - - -class StaticAnalogOut(StaticAnalogQuantity): - """Static Analog Output class for use with all devices that have constant outputs.""" - description = "static analog output" - -class DigitalQuantity(Output): - """Base class for :obj:`DigitalOut`. - - It is also used internally by other, more complex, output types. - """ - description = "digital quantity" - allowed_states = {1: "high", 0: "low"} - default_value = 0 - dtype = np.uint32 - - # Redefine __init__ so that you cannot define a limit or calibration for DO - @set_passed_properties(property_names={"connection_table_properties": ["inverted"]}) - def __init__(self, name, parent_device, connection, inverted=False, **kwargs): - """Instantiate a digital quantity. - - Args: - name (str): python variable name to assign the quantity to. - parent_device (:obj:`IntermediateDevice`): Device this quantity is attached to. - connection (str): Connection on parent device we are connected to. - inverted (bool, optional): If `True`, output is logic inverted. - **kwargs: Passed to :func:`Output.__init__`. - """ - Output.__init__(self,name,parent_device,connection, **kwargs) - self.inverted = bool(inverted) - - def go_high(self, t): - """Commands the output to go high. - - Args: - t (float): Time, in seconds, when the output goes high. - """ - self.add_instruction(t, 1) - - def go_low(self, t): - """Commands the output to go low. - - Args: - t (float): Time, in seconds, when the output goes low. - """ - self.add_instruction(t, 0) - - def enable(self, t): - """Commands the output to enable. - - If `inverted=True`, this will set the output low. - - Args: - t (float): Time, in seconds, when the output enables. - """ - if self.inverted: - self.go_low(t) - else: - self.go_high(t) - - def disable(self, t): - """Commands the output to disable. - - If `inverted=True`, this will set the output high. - - Args: - t (float): Time, in seconds, when the output disables. - """ - if self.inverted: - self.go_high(t) - else: - self.go_low(t) - - def repeat_pulse_sequence(self, t, duration, pulse_sequence, period, samplerate): - """This function only works if the DigitalQuantity is on a fast clock - - The pulse sequence specified will be repeated from time t until t+duration. - - Note 1: The samplerate should be significantly faster than the smallest time difference between - two states in the pulse sequence, or else points in your pulse sequence may never be evaluated. - - Note 2: The time points your pulse sequence is evaluated at may be different than you expect, - if another output changes state between t and t+duration. As such, you should set the samplerate - high enough that even if this rounding of tie points occurs (to fit in the update required to change the other output) - your pulse sequence will not be significantly altered) - - Args: - t (float): Time, in seconds, to start the pulse sequence. - duration (float): How long, in seconds, to repeat the sequence. - pulse_sequence (list): List of tuples, with each tuple of the form - `(time, state)`. - period (float): Defines how long the final tuple will be held for before - repeating the pulse sequence. In general, should be longer than the - entire pulse sequence. - samplerate (float): How often to update the output, in Hz. - """ - self.add_instruction( - t, - { - "function": functions.pulse_sequence(pulse_sequence, period), - "description": "pulse sequence", - "initial time":t, - "end time": t + duration, - "clock rate": samplerate, - "units": None, - } - ) - - return duration - - -class DigitalOut(DigitalQuantity): - """Digital output class for use with all devices.""" - description = "digital output" - - -class StaticDigitalQuantity(DigitalQuantity): - """Base class for :obj:`StaticDigitalOut`. - - It can also be used internally by other, more complex, output types. - """ - description = "static digital quantity" - default_value = 0 - """float: Value of output if no constant value is commanded.""" - - @set_passed_properties(property_names = {}) - def __init__(self, *args, **kwargs): - """Instatiantes the static digital quantity. - - Defines an internal tracking variable of the static output value and - calls :func:`Output.__init__`. - - Args: - *args: Passed to :func:`Output.__init__`. - **kwargs: Passed to :func:`Output.__init__`. - """ - DigitalQuantity.__init__(self, *args, **kwargs) - self._static_value = None - - def go_high(self): - """Command a static high output. - - Raises: - LabscriptError: If output has already been set low. - """ - if self._static_value is None: - self.add_instruction(0,1) - self._static_value = 1 - else: - raise LabscriptError( - f"{self.description} {self.name} has already been set to " - f"{self.instruction_to_string(self._static_value)}. It cannot " - "also be set to 1." - ) - - def go_low(self): - """Command a static low output. - - Raises: - LabscriptError: If output has already been set high. - """ - if self._static_value is None: - self.add_instruction(0,0) - self._static_value = 0 - else: - raise LabscriptError( - f"{self.description} {self.name} has already been set to " - f"{self.instruction_to_string(self._static_value)}. It cannot " - "also be set to 0." - ) - - def get_change_times(self): - """Enforces no change times. - - Returns: - list: An empty list, as expected by the parent pseudoclock. - """ - # Return an empty list as the calling function at the pseudoclock level expects - # a list - return [] - - def make_timeseries(self, change_times): - """Since output is static, does nothing.""" - pass - - def expand_timeseries(self, *args, **kwargs): - """Defines the `raw_output` attribute. - """ - self.raw_output = array([self.static_value], dtype=self.dtype) - - @property - def static_value(self): - """float: The value of the static output.""" - if self._static_value is None: - if not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write( - f"WARNING: {self.name} has no value set. It will be set to " - f"{self.instruction_to_string(self.default_value)}.\n" - ) - self._static_value = self.default_value - return self._static_value - - -class StaticDigitalOut(StaticDigitalQuantity): - """Static Digital Output class for use with all devices that have constant outputs.""" - description = "static digital output" - - -class AnalogIn(Device): - """Analog Input for use with all devices that have an analog input.""" - description = "Analog Input" - - @set_passed_properties(property_names={}) - def __init__( - self, name, parent_device, connection, scale_factor=1.0, units="Volts", **kwargs - ): - """Instantiates an Analog Input. - - Args: - name (str): python variable to assign this input to. - parent_device (:obj:`IntermediateDevice`): Device input is connected to. - scale_factor (float, optional): Factor to scale the recorded values by. - units (str, optional): Units of the input. - **kwargs: Keyword arguments passed to :func:`Device.__init__`. - """ - self.acquisitions = [] - self.scale_factor = scale_factor - self.units=units - Device.__init__(self, name, parent_device, connection, **kwargs) - - def acquire( - self, label, start_time, end_time, wait_label="", scale_factor=None, units=None - ): - """Command an acquisition for this input. - - Args: - label (str): Unique label for the acquisition. Used to identify the saved trace. - start_time (float): Time, in seconds, when the acquisition should start. - end_time (float): Time, in seconds, when the acquisition should end. - wait_label (str, optional): - scale_factor (float): Factor to scale the saved values by. - units: Units of the input, consistent with the unit conversion class. - - Returns: - float: Duration of the acquistion, equivalent to `end_time - start_time`. - """ - if scale_factor is None: - scale_factor = self.scale_factor - if units is None: - units = self.units - self.acquisitions.append( - { - "start_time": start_time, - "end_time": end_time, - "label": label, - "wait_label": wait_label, - "scale_factor": scale_factor, - "units": units, - } - ) - return end_time - start_time - - -class Shutter(DigitalOut): - """Customized version of :obj:`DigitalOut` that accounts for the open/close - delay of a shutter automatically. - - When using the methods :meth:`open` and :meth:`close`, the shutter open - and close times are precise without haveing to track the delays. Note: - delays can be set using runmanager globals and periodically updated - via a calibration. - - .. Warning:: - - If the shutter is asked to do something at `t=0`, it cannot start - moving earlier than that. This means the initial shutter states - will have imprecise timing. - """ - description = "shutter" - - @set_passed_properties( - property_names={"connection_table_properties": ["open_state"]} - ) - def __init__( - self, name, parent_device, connection, delay=(0, 0), open_state=1, **kwargs - ): - """Instantiates a Shutter. - - Args: - name (str): python variable to assign the object to. - parent_device (:obj:`IntermediateDevice`): Parent device the - digital output is connected to. - connection (str): Physical output port of the device the digital - output is connected to. - delay (tuple, optional): Tuple of the (open, close) delays, specified - in seconds. - open_state (int, optional): Allowed values are `0` or `1`. Defines which - state of the digital output opens the shutter. - - Raises: - LabscriptError: If the `open_state` is not `0` or `1`. - """ - inverted = not bool(open_state) - DigitalOut.__init__( - self, name, parent_device, connection, inverted=inverted, **kwargs - ) - self.open_delay, self.close_delay = delay - self.open_state = open_state - if self.open_state == 1: - self.allowed_states = {0: "closed", 1: "open"} - elif self.open_state == 0: - self.allowed_states = {1: "closed", 0: "open"} - else: - raise LabscriptError( - f"Shutter {self.name} wasn't instantiated with open_state = 0 or 1." - ) - self.actual_times = {} - def open(self, t): - """Command the shutter to open at time `t`. - - Takes the open delay time into account. - - Note that the delay time will not be take into account the open delay if the - command is made at t=0 (or other times less than the open delay). No warning - will be issued for this loss of precision during compilation. - - Args: - t (float): Time, in seconds, when shutter should be open. - """ - # If a shutter is asked to do something at t=0, it cannot start moving - # earlier than that. So initial shutter states will have imprecise - # timing. Not throwing a warning here because if I did, every run - # would throw a warning for every shutter. The documentation will - # have to make a point of this. - t_calc = t-self.open_delay if t >= self.open_delay else 0 - self.actual_times[t] = {"time": t_calc, "instruction": 1} - self.enable(t_calc) - - def close(self, t): - """Command the shutter to close at time `t`. - - Takes the close delay time into account. - - Note that the delay time will not be take into account the close delay if the - command is made at t=0 (or other times less than the close delay). No warning - will be issued for this loss of precision during compilation. - - Args: - t (float): Time, in seconds, when shutter should be closed. - """ - t_calc = t-self.close_delay if t >= self.close_delay else 0 - self.actual_times[t] = {"time": t_calc, "instruction": 0} - self.disable(t_calc) - - def generate_code(self, hdf5_file): - classname = self.__class__.__name__ - calibration_table_dtypes = [ - ("name", "a256"), ("open_delay", float), ("close_delay", float) - ] - if classname not in hdf5_file["calibrations"]: - hdf5_file["calibrations"].create_dataset( - classname, (0,), dtype=calibration_table_dtypes, maxshape=(None,) - ) - metadata = (self.name, self.open_delay, self.close_delay) - dataset = hdf5_file["calibrations"][classname] - dataset.resize((len(dataset) + 1,)) - dataset[len(dataset) - 1] = metadata - - def get_change_times(self, *args, **kwargs): - retval = DigitalOut.get_change_times(self, *args, **kwargs) - - if len(self.actual_times) > 1: - sorted_times = list(self.actual_times.keys()) - sorted_times.sort() - for i in range(len(sorted_times) - 1): - time = sorted_times[i] - next_time = sorted_times[i + 1] - instruction = self.actual_times[time]["instruction"] - next_instruction = self.actual_times[next_time]["instruction"] - state = "opened" if instruction == 1 else "closed" - next_state = "open" if next_instruction == 1 else "close" - # only look at instructions that contain a state change - if instruction != next_instruction: - if self.actual_times[next_time]["time"] < self.actual_times[time]["time"]: - sys.stderr.write( - f"WARNING: The shutter '{self.name}' is requested to " - f"{next_state} too early (taking delay into account) at " - f"t={next_time:.10f}s when it is still not {state} from " - f"an earlier instruction at t={time:.10f}s\n" - ) - elif not config.suppress_mild_warnings and not config.suppress_all_warnings: - sys.stderr.write( - f"WARNING: The shutter '{self.name}' is requested to " - f"{next_state} at t={next_time:.10f}s but was never {state} " - f"after an earlier instruction at t={time:.10f}s\n" - ) - return retval - - -class Trigger(DigitalOut): - """Customized version of :obj:`DigitalOut` that tracks edge type. - """ - description = "trigger device" - allowed_children = [TriggerableDevice] - - @set_passed_properties(property_names={}) - def __init__( - self, name, parent_device, connection, trigger_edge_type="rising", **kwargs - ): - """Instantiates a DigitalOut object that tracks the trigger edge type. - - Args: - name (str): python variable name to assign the quantity to. - parent_device (:obj:`IntermediateDevice`): Device this quantity is attached to. - trigger_edge_type (str, optional): Allowed values are `'rising'` and `'falling'`. - **kwargs: Passed to :func:`Output.__init__`. - - """ - DigitalOut.__init__(self, name, parent_device, connection, **kwargs) - self.trigger_edge_type = trigger_edge_type - if self.trigger_edge_type == "rising": - self.enable = self.go_high - self.disable = self.go_low - self.allowed_states = {1: "enabled", 0: "disabled"} - elif self.trigger_edge_type == "falling": - self.enable = self.go_low - self.disable = self.go_high - self.allowed_states = {1: "disabled", 0: "enabled"} - else: - raise ValueError( - "trigger_edge_type must be 'rising' or 'falling', not " - f"'{trigger_edge_type}'." - ) - # A list of the times this trigger has been asked to trigger: - self.triggerings = [] - - def trigger(self, t, duration): - """Command a trigger pulse. - - Args: - t (float): Time, in seconds, for the trigger edge to occur. - duration (float): Duration of the trigger, in seconds. - """ - assert duration > 0, "Negative or zero trigger duration given" - if t != self.t0 and self.t0 not in self.instructions: - self.disable(self.t0) - - start = t - end = t + duration - for other_start, other_duration in self.triggerings: - other_end = other_start + other_duration - # Check for overlapping exposures: - if not (end < other_start or start > other_end): - raise LabscriptError( - f"{self.description} {self.name} has two overlapping triggerings: " - f"one at t = {start}s for {duration}s, and another at " - f"t = {other_start}s for {other_duration}s." - ) - self.enable(t) - self.disable(round(t + duration, 10)) - self.triggerings.append((t, duration)) - - def add_device(self, device): - if device.connection != "trigger": - raise LabscriptError( - f"The 'connection' string of device {device.name} " - f"to {self.name} must be 'trigger', not '{device.connection}'" - ) - DigitalOut.add_device(self, device) - - class WaitMonitor(Trigger): @set_passed_properties(property_names={}) @@ -1968,441 +201,7 @@ def __init__( self.timeout_device = timeout_device self.timeout_connection = timeout_connection self.timeout_trigger_type = timeout_trigger_type - - -class DDSQuantity(Device): - """Used to define a DDS output. - - It is a container class, with properties that allow access to a frequency, - amplitude, and phase of the output as :obj:`AnalogQuantity`. - It can also have a gate, which provides enable/disable control of the output - as :obj:`DigitalOut`. - This class instantiates channels for frequency/amplitude/phase (and optionally the - gate) itself. - """ - description = 'DDS' - allowed_children = [AnalogQuantity, DigitalOut, DigitalQuantity] - - @set_passed_properties(property_names={}) - def __init__( - self, - name, - parent_device, - connection, - digital_gate=None, - freq_limits=None, - freq_conv_class=None, - freq_conv_params=None, - amp_limits=None, - amp_conv_class=None, - amp_conv_params=None, - phase_limits=None, - phase_conv_class=None, - phase_conv_params=None, - call_parents_add_device=True, - **kwargs - ): - """Instantiates a DDS quantity. - - Args: - name (str): python variable for the object created. - parent_device (:obj:`IntermediateDevice`): Device this output is - connected to. - connection (str): Output of parent device this DDS is connected to. - digital_gate (dict, optional): Configures a digital output to use as an enable/disable - gate for the output. Should contain keys `'device'` and `'connection'` - with arguments for the `parent_device` and `connection` for instantiating - the :obj:`DigitalOut`. All other (optional) keys are passed as kwargs. - freq_limits (tuple, optional): `(lower, upper)` limits for the - frequency of the output - freq_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): - Unit conversion class for the frequency of the output. - freq_conv_params (dict, optional): Keyword arguments passed to the - unit conversion class for the frequency of the output. - amp_limits (tuple, optional): `(lower, upper)` limits for the - amplitude of the output - amp_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): - Unit conversion class for the amplitude of the output. - amp_conv_params (dict, optional): Keyword arguments passed to the - unit conversion class for the amplitude of the output. - phase_limits (tuple, optional): `(lower, upper)` limits for the - phase of the output - phase_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): - Unit conversion class for the phase of the output. - phase_conv_params (dict, optional): Keyword arguments passed to the - unit conversion class for the phase of the output. - call_parents_add_device (bool, optional): Have the parent device run - its `add_device` method. - **kwargs: Keyword arguments passed to :func:`Device.__init__`. - """ - # Here we set call_parents_add_device=False so that we - # can do additional initialisation before manually calling - # self.parent_device.add_device(self). This allows the parent's - # add_device method to perform checks based on the code below, - # whilst still providing us with the checks and attributes that - # Device.__init__ gives us in the meantime. - Device.__init__( - self, name, parent_device, connection, call_parents_add_device=False, **kwargs - ) - - # Ask the parent device if it has default unit conversion classes it would like - # us to use: - if hasattr(parent_device, 'get_default_unit_conversion_classes'): - classes = self.parent_device.get_default_unit_conversion_classes(self) - default_freq_conv, default_amp_conv, default_phase_conv = classes - # If the user has not overridden, use these defaults. If - # the parent does not have a default for one or more of amp, - # freq or phase, it should return None for them. - if freq_conv_class is None: - freq_conv_class = default_freq_conv - if amp_conv_class is None: - amp_conv_class = default_amp_conv - if phase_conv_class is None: - phase_conv_class = default_phase_conv - - self.frequency = AnalogQuantity( - f"{self.name}_freq", - self, - "freq", - freq_limits, - freq_conv_class, - freq_conv_params, - ) - self.amplitude = AnalogQuantity( - f"{self.name}_amp", - self, - "amp", - amp_limits, - amp_conv_class, - amp_conv_params, - ) - self.phase = AnalogQuantity( - f"{self.name}_phase", - self, - "phase", - phase_limits, - phase_conv_class, - phase_conv_params, - ) - - self.gate = None - digital_gate = digital_gate or {} - if "device" in digital_gate and "connection" in digital_gate: - dev = digital_gate.pop("device") - conn = digital_gate.pop("connection") - self.gate = DigitalOut(f"{name}_gate", dev, conn, **digital_gate) - # Did they only put one key in the dictionary, or use the wrong keywords? - elif len(digital_gate) > 0: - raise LabscriptError( - 'You must specify the "device" and "connection" for the digital gate ' - f"of {self.name}." - ) - - # If the user has not specified a gate, and the parent device - # supports gating of DDS output, it should add a gate to this - # instance in its add_device method, which is called below. If - # they *have* specified a gate device, but the parent device - # has its own gating (such as the PulseBlaster), it should - # check this and throw an error in its add_device method. See - # labscript_devices.PulseBlaster.PulseBlaster.add_device for an - # example of this. - # In some subclasses we need to hold off on calling the parent - # device's add_device function until further code has run, - # e.g., see PulseBlasterDDS in PulseBlaster.py - if call_parents_add_device: - self.parent_device.add_device(self) - - def setamp(self, t, value, units=None): - """Set the amplitude of the output. - - Args: - t (float): Time, in seconds, when the amplitude is set. - value (float): Amplitude to set to. - units: Units that the value is defined in. - """ - self.amplitude.constant(t, value, units) - - def setfreq(self, t, value, units=None): - """Set the frequency of the output. - - Args: - t (float): Time, in seconds, when the frequency is set. - value (float): Frequency to set to. - units: Units that the value is defined in. - """ - self.frequency.constant(t, value, units) - - def setphase(self, t, value, units=None): - """Set the phase of the output. - - Args: - t (float): Time, in seconds, when the phase is set. - value (float): Phase to set to. - units: Units that the value is defined in. - """ - self.phase.constant(t, value, units) - - def enable(self, t): - """Enable the Output. - - Args: - t (float): Time, in seconds, to enable the output at. - - Raises: - LabscriptError: If the DDS is not instantiated with a digital gate. - """ - if self.gate is None: - raise LabscriptError( - f"DDS {self.name} does not have a digital gate, so you cannot use the " - "enable(t) method." - ) - self.gate.go_high(t) - - def disable(self, t): - """Disable the Output. - - Args: - t (float): Time, in seconds, to disable the output at. - - Raises: - LabscriptError: If the DDS is not instantiated with a digital gate. - """ - if self.gate is None: - raise LabscriptError( - f"DDS {self.name} does not have a digital gate, so you cannot use the " - "disable(t) method." - ) - self.gate.go_low(t) - - def pulse( - self, - t, - duration, - amplitude, - frequency, - phase=None, - amplitude_units=None, - frequency_units=None, - phase_units=None, - print_summary=False, - ): - """Pulse the output. - - Args: - t (float): Time, in seconds, to start the pulse at. - duration (float): Length of the pulse, in seconds. - amplitude (float): Amplitude to set the output to during the pulse. - frequency (float): Frequency to set the output to during the pulse. - phase (float, optional): Phase to set the output to during the pulse. - amplitude_units: Units of `amplitude`. - frequency_units: Units of `frequency`. - phase_units: Units of `phase`. - print_summary (bool, optional): Print a summary of the pulse during - compilation time. - - Returns: - float: Duration of the pulse, in seconds. - """ - if print_summary: - functions.print_time( - t, - f"{self.name} pulse at {frequency/MHz:.4f} MHz for {duration/ms:.3f} ms", - ) - self.setamp(t, amplitude, amplitude_units) - if frequency is not None: - self.setfreq(t, frequency, frequency_units) - if phase is not None: - self.setphase(t, phase, phase_units) - if amplitude != 0 and self.gate is not None: - self.enable(t) - self.disable(t + duration) - self.setamp(t + duration, 0) - return duration - - -class DDS(DDSQuantity): - """DDS class for use with all devices that have DDS-like outputs.""" - - -class StaticDDS(Device): - """Static DDS class for use with all devices that have static DDS-like outputs.""" - description = "Static RF" - allowed_children = [StaticAnalogQuantity,DigitalOut,StaticDigitalOut] - - @set_passed_properties(property_names = {}) - def __init__( - self, - name, - parent_device, - connection, - digital_gate=None, - freq_limits=None, - freq_conv_class=None, - freq_conv_params=None, - amp_limits=None, - amp_conv_class=None, - amp_conv_params=None, - phase_limits=None, - phase_conv_class=None, - phase_conv_params=None, - **kwargs, - ): - """Instantiates a Static DDS quantity. - - Args: - name (str): python variable for the object created. - parent_device (:obj:`IntermediateDevice`): Device this output is - connected to. - connection (str): Output of parent device this DDS is connected to. - digital_gate (dict, optional): Configures a digital output to use as an enable/disable - gate for the output. Should contain keys `'device'` and `'connection'` - with arguments for the `parent_device` and `connection` for instantiating - the :obj:`DigitalOut`. All other (optional) keys are passed as kwargs. - freq_limits (tuple, optional): `(lower, upper)` limits for the - frequency of the output - freq_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): - Unit conversion class for the frequency of the output. - freq_conv_params (dict, optional): Keyword arguments passed to the - unit conversion class for the frequency of the output. - amp_limits (tuple, optional): `(lower, upper)` limits for the - amplitude of the output - amp_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): - Unit conversion class for the amplitude of the output. - amp_conv_params (dict, optional): Keyword arguments passed to the - unit conversion class for the amplitude of the output. - phase_limits (tuple, optional): `(lower, upper)` limits for the - phase of the output - phase_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): - Unit conversion class for the phase of the output. - phase_conv_params (dict, optional): Keyword arguments passed to the - unit conversion class for the phase of the output. - call_parents_add_device (bool, optional): Have the parent device run - its `add_device` method. - **kwargs: Keyword arguments passed to :func:`Device.__init__`. - """ - # We tell Device.__init__ to not call - # self.parent.add_device(self), we'll do that ourselves later - # after further intitialisation, so that the parent can see the - # freq/amp/phase objects and manipulate or check them from within - # its add_device method. - Device.__init__( - self, name, parent_device, connection, call_parents_add_device=False, **kwargs - ) - - # Ask the parent device if it has default unit conversion classes it would like us to use: - if hasattr(parent_device, 'get_default_unit_conversion_classes'): - classes = parent_device.get_default_unit_conversion_classes(self) - default_freq_conv, default_amp_conv, default_phase_conv = classes - # If the user has not overridden, use these defaults. If - # the parent does not have a default for one or more of amp, - # freq or phase, it should return None for them. - if freq_conv_class is None: - freq_conv_class = default_freq_conv - if amp_conv_class is None: - amp_conv_class = default_amp_conv - if phase_conv_class is None: - phase_conv_class = default_phase_conv - - self.frequency = StaticAnalogQuantity( - f"{self.name}_freq", - self, - "freq", - freq_limits, - freq_conv_class, - freq_conv_params - ) - self.amplitude = StaticAnalogQuantity( - f"{self.name}_amp", - self, - "amp", - amp_limits, - amp_conv_class, - amp_conv_params, - ) - self.phase = StaticAnalogQuantity( - f"{self.name}_phase", - self, - "phase", - phase_limits, - phase_conv_class, - phase_conv_params, - ) - - digital_gate = digital_gate or {} - if "device" in digital_gate and "connection" in digital_gate: - dev = digital_gate.pop("device") - conn = digital_gate.pop("connection") - self.gate = DigitalOut(f"{name}_gate", dev, conn, **digital_gate) - # Did they only put one key in the dictionary, or use the wrong keywords? - elif len(digital_gate) > 0: - raise LabscriptError( - 'You must specify the "device" and "connection" for the digital gate ' - f"of {self.name}" - ) - # Now we call the parent's add_device method. This is a must, since we didn't do so earlier from Device.__init__. - self.parent_device.add_device(self) - - def setamp(self, value, units=None): - """Set the static amplitude of the output. - - Args: - value (float): Amplitude to set to. - units: Units that the value is defined in. - """ - self.amplitude.constant(value,units) - - def setfreq(self, value, units=None): - """Set the static frequency of the output. - - Args: - value (float): Frequency to set to. - units: Units that the value is defined in. - """ - self.frequency.constant(value,units) - - def setphase(self, value, units=None): - """Set the static phase of the output. - - Args: - value (float): Phase to set to. - units: Units that the value is defined in. - """ - self.phase.constant(value,units) - - def enable(self, t=None): - """Enable the Output. - - Args: - t (float, optional): Time, in seconds, to enable the output at. - - Raises: - LabscriptError: If the DDS is not instantiated with a digital gate. - """ - if self.gate: - self.gate.go_high(t) - else: - raise LabscriptError( - f"DDS {self.name} does not have a digital gate, so you cannot use the " - "enable(t) method." - ) - - def disable(self, t=None): - """Disable the Output. - - Args: - t (float, optional): Time, in seconds, to disable the output at. - - Raises: - LabscriptError: If the DDS is not instantiated with a digital gate. - """ - if self.gate: - self.gate.go_low(t) - else: - raise LabscriptError( - f"DDS {self.name} does not have a digital gate, so you cannot use the " - "disable(t) method." - ) def save_time_markers(hdf5_file): """Save shot time markers to the shot file. @@ -2474,7 +273,7 @@ def generate_connection_table(hdf5_file): connection_table_array = empty(len(connection_table),dtype=connection_table_dtypes) for i, row in enumerate(connection_table): connection_table_array[i] = row - dataset = hdf5_file.create_dataset('connection table', compression=config.compression, data=connection_table_array, maxshape=(None,)) + dataset = hdf5_file.create_dataset('connection table', compression=compiler.compression, data=connection_table_array, maxshape=(None,)) if compiler.master_pseudoclock is None: master_pseudoclock_name = 'None' @@ -2924,11 +723,11 @@ def load_globals(hdf5_filename): raise LabscriptError('ERROR whilst parsing globals from %s. \'%s\''%(hdf5_filename,name) + 'is not a valid Python variable name.' + ' Please choose a different variable name.') - + # Workaround for the fact that numpy.bool_ objects dont # match python's builtin True and False when compared with 'is': - if type(params[name]) == bool_: # bool_ is numpy.bool_, imported from pylab - params[name] = bool(params[name]) + if type(params[name]) == np.bool_: + params[name] = bool(params[name]) # 'None' is stored as an h5py null object reference: if isinstance(params[name], h5py.Reference) and not params[name]: params[name] = None diff --git a/labscript/outputs.py b/labscript/outputs.py new file mode 100644 index 0000000..7a37545 --- /dev/null +++ b/labscript/outputs.py @@ -0,0 +1,2188 @@ +import sys + +import numpy as np + +from . import functions +from .base import Device +from .compiler import compiler +from .constants import * +from .core import TriggerableDevice +from .utils import LabscriptError, set_passed_properties + + +class Output(Device): + """Base class for all output classes.""" + description = "generic output" + allowed_states = None + dtype = np.float64 + scale_factor = 1 + + @set_passed_properties(property_names={}) + def __init__( + self, + name, + parent_device, + connection, + limits=None, + unit_conversion_class=None, + unit_conversion_parameters=None, + default_value=None, + **kwargs + ): + """Instantiate an Output. + + Args: + name (str): python variable name to assign the Output to. + parent_device (:obj:`IntermediateDevice`): Parent device the output + is connected to. + connection (str): Channel of parent device output is connected to. + limits (tuple, optional): `(min,max)` allowed for the output. + unit_conversion_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): + Unit concersion class to use for the output. + unit_conversion_parameters (dict, optional): Dictonary or kwargs to + pass to the unit conversion class. + default_value (float, optional): Default value of the output if no + output is commanded. + **kwargs: Passed to :meth:`Device.__init__`. + + Raises: + LabscriptError: Limits tuple is invalid or unit conversion class + units don't line up. + """ + Device.__init__(self,name,parent_device,connection, **kwargs) + + self.instructions = {} + self.ramp_limits = [] # For checking ramps don't overlap + if default_value is not None: + self.default_value = default_value + if not unit_conversion_parameters: + unit_conversion_parameters = {} + self.unit_conversion_class = unit_conversion_class + self.set_properties( + unit_conversion_parameters, + {"unit_conversion_parameters": list(unit_conversion_parameters.keys())} + ) + + # Instantiate the calibration + if unit_conversion_class is not None: + self.calibration = unit_conversion_class(unit_conversion_parameters) + # Validate the calibration class + for units in self.calibration.derived_units: + # Does the conversion to base units function exist for each defined unit + # type? + if not hasattr(self.calibration, f"{units}_to_base"): + raise LabscriptError( + f'The function "{units}_to_base" does not exist within the ' + f'calibration "{self.unit_conversion_class}" used in output ' + f'"{self.name}"' + ) + # Does the conversion to base units function exist for each defined unit + # type? + if not hasattr(self.calibration, f"{units}_from_base"): + raise LabscriptError( + f'The function "{units}_from_base" does not exist within the ' + f'calibration "{self.unit_conversion_class}" used in output ' + f'"{self.name}"' + ) + + # If limits exist, check they are valid + # Here we specifically differentiate "None" from False as we will later have a + # conditional which relies on self.limits being either a correct tuple, or + # "None" + if limits is not None: + if not isinstance(limits, tuple) or len(limits) != 2: + raise LabscriptError( + f'The limits for "{self.name}" must be tuple of length 2. ' + 'Eg. limits=(1, 2)' + ) + if limits[0] > limits[1]: + raise LabscriptError( + "The first element of the tuple must be lower than the second " + "element. Eg limits=(1, 2), NOT limits=(2, 1)" + ) + # Save limits even if they are None + self.limits = limits + + @property + def clock_limit(self): + """float: Returns the parent clock line's clock limit.""" + parent = self.parent_clock_line + return parent.clock_limit + + @property + def trigger_delay(self): + """float: The earliest time output can be commanded from this device after a trigger. + This is nonzeo on secondary pseudoclocks due to triggering delays.""" + parent = self.pseudoclock_device + if parent.is_master_pseudoclock: + return 0 + else: + return parent.trigger_delay + + @property + def wait_delay(self): + """float: The earliest time output can be commanded from this device after a wait. + This is nonzeo on secondary pseudoclocks due to triggering delays and the fact + that the master clock doesn't provide a resume trigger to secondary clocks until + a minimum time has elapsed: compiler.wait_delay. This is so that if a wait is + extremely short, the child clock is actually ready for the trigger. + """ + delay = compiler.wait_delay if self.pseudoclock_device.is_master_pseudoclock else 0 + return self.trigger_delay + delay + + def get_all_outputs(self): + """Get all children devices that are outputs. + + For ``Output``, this is `self`. + + Returns: + list: List of children :obj:`Output`. + """ + return [self] + + def apply_calibration(self,value,units): + """Apply the calibration defined by the unit conversion class, if present. + + Args: + value (float): Value to apply calibration to. + units (str): Units to convert to. Must be defined by the unit + conversion class. + + Returns: + float: Converted value. + + Raises: + LabscriptError: If no unit conversion class is defined or `units` not + in that class. + """ + # Is a calibration in use? + if self.unit_conversion_class is None: + raise LabscriptError( + 'You can not specify the units in an instruction for output ' + f'"{self.name}" as it does not have a calibration associated with it' + ) + + # Does a calibration exist for the units specified? + if units not in self.calibration.derived_units: + raise LabscriptError( + f'The units "{units}" does not exist within the calibration ' + f'"{self.unit_conversion_class}" used in output "{self.name}"' + ) + + # Return the calibrated value + return getattr(self.calibration,units+"_to_base")(value) + + def instruction_to_string(self,instruction): + """Gets a human readable description of an instruction. + + Args: + instruction (dict or str): Instruction to get description of, + or a fixed instruction defined in :attr:`allowed_states`. + + Returns: + str: Instruction description. + """ + if isinstance(instruction, dict): + return instruction["description"] + elif self.allowed_states: + return str(self.allowed_states[instruction]) + else: + return str(instruction) + + def add_instruction(self, time, instruction, units=None): + """Adds a hardware instruction to the device instruction list. + + Args: + time (float): Time, in seconds, that the instruction begins. + instruction (dict or float): Instruction to add. + units (str, optional): Units instruction is in, if `instruction` + is a `float`. + + Raises: + LabscriptError: If time requested is not allowed or samplerate + is too fast. + """ + if not compiler.start_called: + raise LabscriptError("Cannot add instructions prior to calling start()") + # round to the nearest 0.1 nanoseconds, to prevent floating point + # rounding errors from breaking our equality checks later on. + time = round(time, 10) + # Also round end time of ramps to the nearest 0.1 ns: + if isinstance(instruction,dict): + instruction["end time"] = round(instruction["end time"], 10) + instruction["initial time"] = round(instruction["initial time"], 10) + # Check that time is not negative or too soon after t=0: + if time < self.t0: + raise LabscriptError( + f"{self.description} {self.name} has an instruction at t={time}s. " + "Due to the delay in triggering its pseudoclock device, the earliest " + f"output possible is at t={self.t0}." + ) + # Check that this doesn't collide with previous instructions: + if time in self.instructions.keys(): + if not compiler.suppress_all_warnings: + current_value = self.instruction_to_string(self.instructions[time]) + new_value = self.instruction_to_string( + self.apply_calibration(instruction, units) + if units and not isinstance(instruction, dict) + else instruction + ) + sys.stderr.write( + f"WARNING: State of {self.description} {self.name} at t={time}s " + f"has already been set to {current_value}. Overwriting to " + f"{new_value}. (note: all values in base units where relevant)" + "\n" + ) + # Check that ramps don't collide + if isinstance(instruction, dict): + # No ramps allowed if this output is on a slow clock: + if not self.parent_clock_line.ramping_allowed: + raise LabscriptError( + f"{self.description} {self.name} is on clockline that does not " + "support ramping. It cannot have a function ramp as an instruction." + ) + for start, end in self.ramp_limits: + if start < time < end or start < instruction["end time"] < end: + start_value = self.instruction_to_string(self.instructions[start]) + new_value = self.instruction_to_string(instruction) + raise LabscriptError( + f"State of {self.description} {self.name} from t = {start}s to " + f"{end}s has already been set to {start_value}. Cannot set to " + f"{new_value} from t = {time}s to {instruction['end time']}s." + ) + self.ramp_limits.append((time, instruction["end time"])) + # Check that start time is before end time: + if time > instruction["end time"]: + raise LabscriptError( + f"{self.description} {self.name} has been passed a function ramp " + f"{self.instruction_to_string(instruction)} with a negative " + "duration." + ) + if instruction["clock rate"] == 0: + raise LabscriptError("A nonzero sample rate is required.") + # Else we have a "constant", single valued instruction + else: + # If we have units specified, convert the value + if units is not None: + # Apply the unit calibration now + instruction = self.apply_calibration(instruction, units) + # if we have limits, check the value is valid + if self.limits: + if (instruction < self.limits[0]) or (instruction > self.limits[1]): + raise LabscriptError( + f"You cannot program the value {instruction} (base units) to " + f"{self.name} as it falls outside the limits " + f"({self.limits[0]} to {self.limits[1]})" + ) + self.instructions[time] = instruction + + def do_checks(self, trigger_times): + """Basic error checking to ensure the user's instructions make sense. + + Args: + trigger_times (iterable): Times to confirm don't conflict with + instructions. + + Raises: + LabscriptError: If a trigger time conflicts with an instruction. + """ + # Check if there are no instructions. Generate a warning and insert an + # instruction telling the output to remain at its default value. + if not self.instructions: + if not compiler.suppress_mild_warnings and not compiler.suppress_all_warnings: + sys.stderr.write( + f"WARNING: {self.name} has no instructions. It will be set to " + f"{self.instruction_to_string(self.default_value)} for all time.\n" + ) + self.add_instruction(self.t0, self.default_value) + # Check if there are no instructions at the initial time. Generate a warning and insert an + # instruction telling the output to start at its default value. + if self.t0 not in self.instructions.keys(): + if not compiler.suppress_mild_warnings and not compiler.suppress_all_warnings: + sys.stderr.write( + f"WARNING: {self.name} has no initial instruction. It will " + "initially be set to " + f"{self.instruction_to_string(self.default_value)}.\n" + ) + self.add_instruction(self.t0, self.default_value) + # Check that ramps have instructions following them. + # If they don't, insert an instruction telling them to hold their final value. + for instruction in list(self.instructions.values()): + if ( + isinstance(instruction, dict) + and instruction["end time"] not in self.instructions.keys() + ): + self.add_instruction( + instruction["end time"], + instruction["function"]( + instruction["end time"] - instruction["initial time"] + ), + instruction["units"], + ) + # Checks for trigger times: + for trigger_time in trigger_times: + for t, inst in self.instructions.items(): + # Check no ramps are happening at the trigger time: + if ( + isinstance(inst, dict) + and inst["initial time"] < trigger_time + and inst["end time"] > trigger_time + ): + raise LabscriptError( + f"{self.description} {self.name} has a ramp " + f"{inst['description']} from t = {inst['initial time']} to " + f"{inst['end time']}. This overlaps with a trigger at " + f"t={trigger_time}, and so cannot be performed." + ) + # Check that nothing is happening during the delay time after the trigger: + if ( + round(trigger_time, 10) + < round(t, 10) + < round(trigger_time + self.trigger_delay, 10) + ): + raise LabscriptError( + f"{self.description} {self.name} has an instruction at t={t}. " + f"This is too soon after a trigger at t={trigger_time}, " + "the earliest output possible after this trigger is at " + f"t={trigger_time + self.trigger_delay}" + ) + # Check that there are no instructions too soon before the trigger: + if 0 < trigger_time - t < max(self.clock_limit, compiler.wait_delay): + raise LabscriptError( + f"{self.description} {self.name} has an instruction at t={t}. " + f"This is too soon before a trigger at t={trigger_time}, " + "the latest output possible before this trigger is at " + f"t={trigger_time - max(self.clock_limit, compiler.wait_delay)}" + ) + + def offset_instructions_from_trigger(self, trigger_times): + """Subtracts self.trigger_delay from all instructions at or after each trigger_time. + + Args: + trigger_times (iterable): Times of all trigger events. + """ + offset_instructions = {} + for t, instruction in self.instructions.items(): + # How much of a delay is there for this instruction? That depends how many triggers there are prior to it: + n_triggers_prior = len([time for time in trigger_times if time < t]) + # The cumulative offset at this point in time: + offset = self.trigger_delay * n_triggers_prior + trigger_times[0] + offset = round(offset, 10) + if isinstance(instruction, dict): + offset_instruction = instruction.copy() + offset_instruction["end time"] = self.quantise_to_pseudoclock( + round(instruction["end time"] - offset, 10) + ) + offset_instruction["initial time"] = self.quantise_to_pseudoclock( + round(instruction["initial time"] - offset, 10) + ) + else: + offset_instruction = instruction + + new_time = self.quantise_to_pseudoclock(round(t - offset, 10)) + offset_instructions[new_time] = offset_instruction + self.instructions = offset_instructions + + # offset each of the ramp_limits for use in the calculation within + # Pseudoclock/ClockLine so that the times in list are consistent with the ones + # in self.instructions + for i, times in enumerate(self.ramp_limits): + n_triggers_prior = len([time for time in trigger_times if time < times[0]]) + # The cumulative offset at this point in time: + offset = self.trigger_delay * n_triggers_prior + trigger_times[0] + offset = round(offset, 10) + + # offset start and end time of ramps + # NOTE: This assumes ramps cannot proceed across a trigger command + # (for instance you cannot ramp an output across a WAIT) + self.ramp_limits[i] = ( + self.quantise_to_pseudoclock(round(times[0] - offset, 10)), + self.quantise_to_pseudoclock(round(times[1] - offset, 10)), + ) + + def get_change_times(self): + """If this function is being called, it means that the parent + Pseudoclock has requested a list of times that this output changes + state. + + Returns: + list: List of times output changes values. + """ + times = list(self.instructions.keys()) + times.sort() + + current_dict_time = None + for time in times: + if isinstance(self.instructions[time], dict) and current_dict_time is None: + current_dict_time = self.instructions[time] + elif ( + current_dict_time is not None + and current_dict_time['initial time'] < time < current_dict_time['end time'] + ): + raise LabscriptError( + f"{self.description} {self.name} has an instruction at " + f"t={time:.10f}s. This instruction collides with a ramp on this " + "output at that time. The collision " + f"{current_dict_time['description']} is happening from " + f"{current_dict_time['initial time']:.10f}s untill " + f"{current_dict_time['end time']:.10f}s" + ) + + self.times = times + return times + + def get_ramp_times(self): + """If this is being called, then it means the parent Pseuedoclock + has asked for a list of the output ramp start and stop times. + + Returns: + list: List of (start, stop) times of ramps for this Output. + """ + return self.ramp_limits + + def make_timeseries(self, change_times): + """If this is being called, then it means the parent Pseudoclock + has asked for a list of this output's states at each time in + change_times. (Which are the times that one or more connected + outputs in the same pseudoclock change state). By state, I don't + mean the value of the output at that moment, rather I mean what + instruction it has. This might be a single value, or it might + be a reference to a function for a ramp etc. This list of states + is stored in self.timeseries rather than being returned.""" + self.timeseries = [] + i = 0 + time_len = len(self.times) + for change_time in change_times: + while i < time_len and change_time >= self.times[i]: + i += 1 + self.timeseries.append(self.instructions[self.times[i-1]]) + + def expand_timeseries(self,all_times,flat_all_times_len): + """This function evaluates the ramp functions in self.timeseries + at the time points in all_times, and creates an array of output + values at those times. These are the values that this output + should update to on each clock tick, and are the raw values that + should be used to program the output device. They are stored + in self.raw_output.""" + # If this output is not ramping, then its timeseries should + # not be expanded. It's already as expanded as it'll get. + if not self.parent_clock_line.ramping_allowed: + self.raw_output = np.array(self.timeseries, dtype=np.dtype(self.dtype)) + return + outputarray = np.empty((flat_all_times_len,), dtype=np.dtype(self.dtype)) + j = 0 + for i, time in enumerate(all_times): + if np.iterable(time): + time_len = len(time) + if isinstance(self.timeseries[i], dict): + # We evaluate the functions at the midpoints of the + # timesteps in order to remove the zero-order hold + # error introduced by sampling an analog signal: + try: + midpoints = time + 0.5*(time[1] - time[0]) + except IndexError: + # Time array might be only one element long, so we + # can't calculate the step size this way. That's + # ok, the final midpoint is determined differently + # anyway: + midpoints = np.zeros(1) + # We need to know when the first clock tick is after + # this ramp ends. It's either an array element or a + # single number depending on if this ramp is followed + # by another ramp or not: + next_time = all_times[i+1][0] if np.iterable(all_times[i+1]) else all_times[i+1] + midpoints[-1] = time[-1] + 0.5*(next_time - time[-1]) + outarray = self.timeseries[i]["function"]( + midpoints - self.timeseries[i]["initial time"] + ) + # Now that we have the list of output points, pass them through the unit calibration + if self.timeseries[i]["units"] is not None: + outarray = self.apply_calibration( + outarray, self.timeseries[i]["units"] + ) + # if we have limits, check the value is valid + if self.limits: + if ((outarrayself.limits[1])).any(): + raise LabscriptError( + f"The function {self.timeseries[i]['function']} called " + f'on "{self.name}" at t={midpoints[0]} generated a ' + "value which falls outside the base unit limits " + f"({self.limits[0]} to {self.limits[1]})" + ) + else: + outarray = np.empty(time_len, dtype=self.dtype) + outarray.fill(self.timeseries[i]) + outputarray[j:j+time_len] = outarray + j += time_len + else: + outputarray[j] = self.timeseries[i] + j += 1 + del self.timeseries # don't need this any more. + self.raw_output = outputarray + + +class AnalogQuantity(Output): + """Base class for :obj:`AnalogOut`. + + It is also used internally by :obj:`DDS`. You should never instantiate this + class directly. + """ + description = "analog quantity" + default_value = 0 + + def _check_truncation(self, truncation, min=0, max=1): + if not (min <= truncation <= max): + raise LabscriptError( + f"Truncation argument must be between {min} and {max} (inclusive), but " + f"is {truncation}." + ) + + def ramp(self, t, duration, initial, final, samplerate, units=None, truncation=1.): + """Command the output to perform a linear ramp. + + Defined by + `f(t) = ((final - initial)/duration)*t + initial` + + Args: + t (float): Time, in seconds, to begin the ramp. + duration (float): Length, in seconds, of the ramp. + initial (float): Initial output value, at time `t`. + final (float): Final output value, at time `t+duration`. + samplerate (float): Rate, in Hz, to update the output. + units: Units the output values are given in, as specified by the + unit conversion class. + truncation (float, optional): Fraction of ramp to perform. Must be between 0 and 1. + + Returns: + float: Length of time ramp will take to complete. + """ + self._check_truncation(truncation) + if truncation > 0: + # if start and end value are the same, we don't need to ramp and can save + # the sample ticks etc + if initial == final: + self.constant(t, initial, units) + if not compiler.suppress_mild_warnings and not compiler.suppress_all_warnings: + sys.stderr.write( + f"WARNING: {self.__class__.__name__} '{self.name}' has the " + f"same initial and final value at time t={t:.10f}s with " + f"duration {duration:.10f}s. In order to save samples and " + "clock ticks this instruction is replaced with a constant " + "output.\n" + ) + else: + self.add_instruction( + t, + { + "function": functions.ramp( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "linear ramp", + "initial time": t, + "end time": t + truncation * duration, + "clock rate": samplerate, + "units": units, + } + ) + return truncation * duration + + def sine( + self, + t, + duration, + amplitude, + angfreq, + phase, + dc_offset, + samplerate, + units=None, + truncation=1. + ): + """Command the output to perform a sinusoidal modulation. + + Defined by + `f(t) = amplitude*sin(angfreq*t + phase) + dc_offset` + + Args: + t (float): Time, in seconds, to begin the ramp. + duration (float): Length, in seconds, of the ramp. + amplitude (float): Amplitude of the modulation. + angfreq (float): Angular frequency, in radians per second. + phase (float): Phase offset of the sine wave, in radians. + dc_offset (float): DC offset of output away from 0. + samplerate (float): Rate, in Hz, to update the output. + units: Units the output values are given in, as specified by the + unit conversion class. + truncation (float, optional): Fraction of duration to perform. Must be between 0 and 1. + + Returns: + float: Length of time modulation will take to complete. Equivalent to `truncation*duration`. + """ + self._check_truncation(truncation) + if truncation > 0: + self.add_instruction( + t, + { + "function": functions.sine( + round(t + duration, 10) - round(t, 10), + amplitude, + angfreq, + phase, + dc_offset, + ), + "description": "sine wave", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) + return truncation*duration + + def sine_ramp( + self, t, duration, initial, final, samplerate, units=None, truncation=1. + ): + """Command the output to perform a ramp defined by one half period of a squared sine wave. + + Defined by + `f(t) = (final-initial)*(sin(pi*t/(2*duration)))^2 + initial` + + Args: + t (float): Time, in seconds, to begin the ramp. + duration (float): Length, in seconds, of the ramp. + initial (float): Initial output value, at time `t`. + final (float): Final output value, at time `t+duration`. + samplerate (float): Rate, in Hz, to update the output. + units: Units the output values are given in, as specified by the + unit conversion class. + truncation (float, optional): Fraction of ramp to perform. Must be between 0 and 1. + + Returns: + float: Length of time ramp will take to complete. + """ + self._check_truncation(truncation) + if truncation > 0: + self.add_instruction( + t, + { + "function": functions.sine_ramp( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "sinusoidal ramp", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) + return truncation*duration + + def sine4_ramp( + self, t, duration, initial, final, samplerate, units=None, truncation=1. + ): + """Command the output to perform an increasing ramp defined by one half period of a quartic sine wave. + + Defined by + `f(t) = (final-initial)*(sin(pi*t/(2*duration)))^4 + initial` + + Args: + t (float): Time, in seconds, to begin the ramp. + duration (float): Length, in seconds, of the ramp. + initial (float): Initial output value, at time `t`. + final (float): Final output value, at time `t+duration`. + samplerate (float): Rate, in Hz, to update the output. + units: Units the output values are given in, as specified by the + unit conversion class. + truncation (float, optional): Fraction of ramp to perform. Must be between 0 and 1. + + Returns: + float: Length of time ramp will take to complete. + """ + self._check_truncation(truncation) + if truncation > 0: + self.add_instruction( + t, + { + "function": functions.sine4_ramp( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "sinusoidal ramp", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) + return truncation*duration + + def sine4_reverse_ramp( + self, t, duration, initial, final, samplerate, units=None, truncation=1. + ): + """Command the output to perform a decreasing ramp defined by one half period of a quartic sine wave. + + Defined by + `f(t) = (final-initial)*(sin(pi*t/(2*duration)))^4 + initial` + + Args: + t (float): Time, in seconds, to begin the ramp. + duration (float): Length, in seconds, of the ramp. + initial (float): Initial output value, at time `t`. + final (float): Final output value, at time `t+duration`. + samplerate (float): Rate, in Hz, to update the output. + units: Units the output values are given in, as specified by the + unit conversion class. + truncation (float, optional): Fraction of ramp to perform. Must be between 0 and 1. + + Returns: + float: Length of time ramp will take to complete. + """ + self._check_truncation(truncation) + if truncation > 0: + self.add_instruction( + t, + { + "function": functions.sine4_reverse_ramp( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "sinusoidal ramp", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) + return truncation*duration + + def exp_ramp( + self, + t, + duration, + initial, + final, + samplerate, + zero=0, + units=None, + truncation=None, + truncation_type="linear", + **kwargs, + ): + """Exponential ramp whose rate of change is set by an asymptotic value (zero argument). + + Args: + t (float): time to start the ramp + duration (float): duration of the ramp + initial (float): initial value of the ramp (sans truncation) + final (float): final value of the ramp (sans truncation) + zero (float): asymptotic value of the exponential decay/rise, i.e. limit as t --> inf + samplerate (float): rate to sample the function + units: unit conversion to apply to specified values before generating raw output + truncation_type (str): + + * `'linear'` truncation stops the ramp when it reaches the value given by the + truncation parameter, which must be between initial and final + * `'exponential'` truncation stops the ramp after a period of truncation*duration + In this instance, the truncation parameter should be between 0 (full truncation) + and 1 (no truncation). + + """ + # Backwards compatibility for old kwarg names + if "trunc" in kwargs: + truncation = kwargs.pop("trunc") + if "trunc_type" in kwargs: + truncation_type = kwargs.pop("trunc_type") + if truncation is not None: + # Computed the truncated duration based on the truncation_type + if truncation_type == "linear": + self._check_truncation( + truncation, min(initial, final), max(initial, final) + ) + # Truncate the ramp when it reaches the value truncation + trunc_duration = duration * \ + np.log((initial-zero)/(truncation-zero)) / \ + np.log((initial-zero)/(final-zero)) + elif truncation_type == "exponential": + # Truncate the ramps duration by a fraction truncation + self._check_truncation(truncation) + trunc_duration = truncation * duration + else: + raise LabscriptError( + "Truncation type for exp_ramp not supported. Must be either linear " + "or exponential." + ) + else: + trunc_duration = duration + if trunc_duration > 0: + self.add_instruction( + t, + { + "function": functions.exp_ramp( + round(t + duration, 10) - round(t, 10), initial, final, zero + ), + "description": 'exponential ramp', + "initial time": t, + "end time": t + trunc_duration, + "clock rate": samplerate, + "units": units, + } + ) + return trunc_duration + + def exp_ramp_t( + self, + t, + duration, + initial, + final, + time_constant, + samplerate, + units=None, + truncation=None, + truncation_type="linear", + **kwargs + ): + """Exponential ramp whose rate of change is set by the time_constant. + + Args: + t (float): time to start the ramp + duration (float): duration of the ramp + initial (float): initial value of the ramp (sans truncation) + final (float): final value of the ramp (sans truncation) + time_constant (float): 1/e time of the exponential decay/rise + samplerate (float): rate to sample the function + units: unit conversion to apply to specified values before generating raw output + truncation_type (str): + + * `'linear'` truncation stops the ramp when it reaches the value given by the + truncation parameter, which must be between initial and final + * `'exponential'` truncation stops the ramp after a period of truncation*duration + In this instance, the truncation parameter should be between 0 (full truncation) + and 1 (no truncation). + + """ + # Backwards compatibility for old kwarg names + if "trunc" in kwargs: + truncation = kwargs.pop("trunc") + if "trunc_type" in kwargs: + truncation_type = kwargs.pop("trunc_type") + if truncation is not None: + zero = (final-initial*np.exp(-duration/time_constant)) / \ + (1-np.exp(-duration/time_constant)) + if truncation_type == "linear": + self._check_truncation(truncation, min(initial, final), max(initial, final)) + trunc_duration = time_constant * \ + np.log((initial-zero)/(truncation-zero)) + elif truncation_type == 'exponential': + self._check_truncation(truncation) + trunc_duration = truncation * duration + else: + raise LabscriptError( + "Truncation type for exp_ramp_t not supported. Must be either " + "linear or exponential." + ) + else: + trunc_duration = duration + if trunc_duration > 0: + self.add_instruction( + t, + { + "function": functions.exp_ramp_t( + round(t + duration, 10) - round(t, 10), + initial, + final, + time_constant, + ), + "description": "exponential ramp with time consntant", + "initial time": t, + "end time": t + trunc_duration, + "clock rate": samplerate, + "units": units, + } + ) + return trunc_duration + + def piecewise_accel_ramp( + self, t, duration, initial, final, samplerate, units=None, truncation=1. + ): + """Changes the output so that the second derivative follows one period of a triangle wave. + + Args: + t (float): Time, in seconds, at which to begin the ramp. + duration (float): Duration of the ramp, in seconds. + initial (float): Initial output value at time `t`. + final (float): Final output value at time `t+duration`. + samplerate (float): Update rate of the output, in Hz. + units: Units, defined by the unit conversion class, the value is in. + truncation (float, optional): Fraction of ramp to perform. Default 1.0. + + Returns: + float: Time the ramp will take to complete. + """ + self._check_truncation(truncation) + if truncation > 0: + self.add_instruction( + t, + { + "function": functions.piecewise_accel( + round(t + duration, 10) - round(t, 10), initial, final + ), + "description": "piecewise linear accelleration ramp", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) + return truncation*duration + + def square_wave( + self, + t, + duration, + amplitude, + frequency, + phase, + offset, + duty_cycle, + samplerate, + units=None, + truncation=1. + ): + """A standard square wave. + + This method generates a square wave which starts HIGH (when its phase is + zero) then transitions to/from LOW at the specified `frequency` in Hz. + The `amplitude` parameter specifies the peak-to-peak amplitude of the + square wave which is centered around `offset`. For example, setting + `amplitude=1` and `offset=0` would give a square wave which transitions + between `0.5` and `-0.5`. Similarly, setting `amplitude=2` and + `offset=3` would give a square wave which transitions between `4` and + `2`. To instead specify the HIGH/LOW levels directly, use + `square_wave_levels()`. + + Note that because the transitions of a square wave are sudden and + discontinuous, small changes in timings (e.g. due to numerical rounding + errors) can affect the output value. This is particularly relevant at + the end of the waveform, as the final output value may be different than + expected if the end of the waveform is close to an edge of the square + wave. Care is taken in the implementation of this method to avoid such + effects, but it still may be desirable to call `constant()` after + `square_wave()` to ensure a particular final value. The output value may + also be different than expected at certain moments in the middle of the + waveform due to the finite samplerate (which may be different than the + requested `samplerate`), particularly if the actual samplerate is not a + multiple of `frequency`. + + Args: + t (float): The time at which to start the square wave. + duration (float): The duration for which to output a square wave + when `truncation` is set to `1`. When `truncation` is set to a + value less than `1`, the actual duration will be shorter than + `duration` by that factor. + amplitude (float): The peak-to-peak amplitude of the square wave. + See above for an example of how to calculate the HIGH/LOW output + values given the `amplitude` and `offset` values. + frequency (float): The frequency of the square wave, in Hz. + phase (float): The initial phase of the square wave. Note that the + square wave is defined such that the phase goes from 0 to 1 (NOT + 2 pi) over one cycle, so setting `phase=0.5` will start the + square wave advanced by 1/2 of a cycle. Setting `phase` equal to + `duty_cycle` will cause the waveform to start LOW rather than + HIGH. + offset (float): The offset of the square wave, which is the value + halfway between the LOW and HIGH output values. Note that this + is NOT the LOW output value; setting `offset` to `0` will cause + the HIGH/LOW values to be symmetrically split around `0`. See + above for an example of how to calculate the HIGH/LOW output + values given the `amplitude` and `offset` values. + duty_cycle (float): The fraction of the cycle for which the output + should be HIGH. This should be a number between zero and one + inclusively. For example, setting `duty_cycle=0.1` will + create a square wave which outputs HIGH over 10% of the + cycle and outputs LOW over 90% of the cycle. + samplerate (float): The requested rate at which to update the output + value. Note that the actual samplerate used may be different if, + for example, another output of the same device has a + simultaneous ramp with a different requested `samplerate`, or if + `1 / samplerate` isn't an integer multiple of the pseudoclock's + timing resolution. + units (str, optional): The units of the output values. If set to + `None` then the output's base units will be used. Defaults to + `None`. + truncation (float, optional): The actual duration of the square wave + will be `duration * truncation` and `truncation` must be set to + a value in the range [0, 1] (inclusively). Set to `1` to output + the full duration of the square wave. Setting it to `0` will + skip the square wave entirely. Defaults to `1.`. + + Returns: + duration (float): The actual duration of the square wave, accounting + for `truncation`. + """ + # Convert to values used by square_wave_levels, then call that method. + level_0 = offset + 0.5 * amplitude + level_1 = offset - 0.5 * amplitude + return self.square_wave_levels( + t, + duration, + level_0, + level_1, + frequency, + phase, + duty_cycle, + samplerate, + units, + truncation, + ) + + def square_wave_levels( + self, + t, + duration, + level_0, + level_1, + frequency, + phase, + duty_cycle, + samplerate, + units=None, + truncation=1. + ): + """A standard square wave. + + This method generates a square wave which starts at `level_0` (when its + phase is zero) then transitions to/from `level_1` at the specified + `frequency`. This is the same waveform output by `square_wave()`, but + parameterized differently. See that method's docstring for more + information. + + Args: + t (float): The time at which to start the square wave. + duration (float): The duration for which to output a square wave + when `truncation` is set to `1`. When `truncation` is set to a + value less than `1`, the actual duration will be shorter than + `duration` by that factor. + level_0 (float): The initial level of the square wave, when the + phase is zero. + level_1 (float): The other level of the square wave. + frequency (float): The frequency of the square wave, in Hz. + phase (float): The initial phase of the square wave. Note that the + square wave is defined such that the phase goes from 0 to 1 (NOT + 2 pi) over one cycle, so setting `phase=0.5` will start the + square wave advanced by 1/2 of a cycle. Setting `phase` equal to + `duty_cycle` will cause the waveform to start at `level_1` + rather than `level_0`. + duty_cycle (float): The fraction of the cycle for which the output + should be set to `level_0`. This should be a number between zero + and one inclusively. For example, setting `duty_cycle=0.1` will + create a square wave which outputs `level_0` over 10% of the + cycle and outputs `level_1` over 90% of the cycle. + samplerate (float): The requested rate at which to update the output + value. Note that the actual samplerate used may be different if, + for example, another output of the same device has a + simultaneous ramp with a different requested `samplerate`, or if + `1 / samplerate` isn't an integer multiple of the pseudoclock's + timing resolution. + units (str, optional): The units of the output values. If set to + `None` then the output's base units will be used. Defaults to + `None`. + truncation (float, optional): The actual duration of the square wave + will be `duration * truncation` and `truncation` must be set to + a value in the range [0, 1] (inclusively). Set to `1` to output + the full duration of the square wave. Setting it to `0` will + skip the square wave entirely. Defaults to `1.`. + + Returns: + duration (float): The actual duration of the square wave, accounting + for `truncation`. + """ + # Check the argument values. + self._check_truncation(truncation) + if duty_cycle < 0 or duty_cycle > 1: + raise LabscriptError( + "Square wave duty cycle must be in the range [0, 1] (inclusively) but " + f"was set to {duty_cycle}." + ) + + if truncation > 0: + # Add the instruction. + func = functions.square_wave( + round(t + duration, 10) - round(t, 10), + level_0, + level_1, + frequency, + phase, + duty_cycle, + ) + self.add_instruction( + t, + { + "function": func, + "description": "square wave", + "initial time": t, + "end time": t + truncation * duration, + "clock rate": samplerate, + "units": units, + } + ) + return truncation * duration + + def customramp(self, t, duration, function, *args, **kwargs): + """Define a custom function for the output. + + Args: + t (float): Time, in seconds, to start the function. + duration (float): Length in time, in seconds, to perform the function. + function (func): Function handle that defines the output waveform. + First argument is the relative time from function start, in seconds. + *args: Arguments passed to `function`. + **kwargs: Keyword arguments pass to `function`. + Standard kwargs common to other output functions are: `units`, + `samplerate`, and `truncation`. These kwargs are optional, but will + not be passed to `function` if present. + + Returns: + float: Duration the function is to be evaluate for. Equivalent to + `truncation*duration`. + """ + units = kwargs.pop("units", None) + samplerate = kwargs.pop("samplerate") + truncation = kwargs.pop("truncation", 1.) + self._check_truncation(truncation) + + def custom_ramp_func(t_rel): + """The function that will return the result of the user's function, + evaluated at relative times t_rel from 0 to duration""" + return function( + t_rel, round(t + duration, 10) - round(t, 10), *args, **kwargs + ) + + if truncation > 0: + self.add_instruction( + t, + { + "function": custom_ramp_func, + "description": f"custom ramp: {function.__name__}", + "initial time": t, + "end time": t + truncation*duration, + "clock rate": samplerate, + "units": units, + } + ) + return truncation*duration + + def constant(self, t, value, units=None): + """Sets the output to a constant value at time `t`. + + Args: + t (float): Time, in seconds, to set the constant output. + value (float): Value to set. + units: Units, defined by the unit conversion class, the value is in. + """ + # verify that value can be converted to float + try: + val = float(value) + except: + raise LabscriptError( + f"Cannot set {self.name} to value={value} at t={t} as the value cannot " + "be converted to float" + ) + self.add_instruction(t, value, units) + + +class AnalogOut(AnalogQuantity): + """Analog Output class for use with all devices that support timed analog outputs.""" + description = "analog output" + + +class StaticAnalogQuantity(Output): + """Base class for :obj:`StaticAnalogOut`. + + It can also be used internally by other more complex output types. + """ + description = "static analog quantity" + default_value = 0.0 + """float: Value of output if no constant value is commanded.""" + + @set_passed_properties(property_names = {}) + def __init__(self, *args, **kwargs): + """Instatiantes the static analog quantity. + + Defines an internal tracking variable of the static output value and + calls :func:`Output.__init__`. + + Args: + *args: Passed to :func:`Output.__init__`. + **kwargs: Passed to :func:`Output.__init__`. + """ + Output.__init__(self, *args, **kwargs) + self._static_value = None + + def constant(self, value, units=None): + """Set the static output value of the output. + + Args: + value (float): Value to set the output to. + units: Units, defined by the unit conversion class, the value is in. + + Raises: + LabscriptError: If static output has already been set to another value + or the value lies outside the output limits. + """ + if self._static_value is None: + # If we have units specified, convert the value + if units is not None: + # Apply the unit calibration now + value = self.apply_calibration(value, units) + # if we have limits, check the value is valid + if self.limits: + minval, maxval = self.limits + if not minval <= value <= maxval: + raise LabscriptError( + f"You cannot program the value {value} (base units) to " + f"{self.name} as it falls outside the limits " + f"({self.limits[0]} to {self.limits[1]})" + ) + self._static_value = value + else: + raise LabscriptError( + f"{self.description} {self.name} has already been set to " + f"{self._static_value} (base units). It cannot also be set to " + f"{value} ({units if units is not None else 'base units'})." + ) + + def get_change_times(self): + """Enforces no change times. + + Returns: + list: An empty list, as expected by the parent pseudoclock. + """ + # Return an empty list as the calling function at the pseudoclock level expects + # a list + return [] + + def make_timeseries(self,change_times): + """Since output is static, does nothing.""" + pass + + def expand_timeseries(self,*args,**kwargs): + """Defines the `raw_output` attribute. + """ + self.raw_output = np.array([self.static_value], dtype=self.dtype) + + @property + def static_value(self): + """float: The value of the static output.""" + if self._static_value is None: + if not compiler.suppress_mild_warnings and not compiler.suppress_all_warnings: + sys.stderr.write( + f"WARNING: {self.name} has no value set. It will be set to " + f"{self.instruction_to_string(self.default_value)}.\n" + ) + self._static_value = self.default_value + return self._static_value + + +class StaticAnalogOut(StaticAnalogQuantity): + """Static Analog Output class for use with all devices that have constant outputs.""" + description = "static analog output" + + +class DigitalQuantity(Output): + """Base class for :obj:`DigitalOut`. + + It is also used internally by other, more complex, output types. + """ + description = "digital quantity" + allowed_states = {1: "high", 0: "low"} + default_value = 0 + dtype = np.uint32 + + # Redefine __init__ so that you cannot define a limit or calibration for DO + @set_passed_properties(property_names={"connection_table_properties": ["inverted"]}) + def __init__(self, name, parent_device, connection, inverted=False, **kwargs): + """Instantiate a digital quantity. + + Args: + name (str): python variable name to assign the quantity to. + parent_device (:obj:`IntermediateDevice`): Device this quantity is attached to. + connection (str): Connection on parent device we are connected to. + inverted (bool, optional): If `True`, output is logic inverted. + **kwargs: Passed to :func:`Output.__init__`. + """ + Output.__init__(self,name,parent_device,connection, **kwargs) + self.inverted = bool(inverted) + + def go_high(self, t): + """Commands the output to go high. + + Args: + t (float): Time, in seconds, when the output goes high. + """ + self.add_instruction(t, 1) + + def go_low(self, t): + """Commands the output to go low. + + Args: + t (float): Time, in seconds, when the output goes low. + """ + self.add_instruction(t, 0) + + def enable(self, t): + """Commands the output to enable. + + If `inverted=True`, this will set the output low. + + Args: + t (float): Time, in seconds, when the output enables. + """ + if self.inverted: + self.go_low(t) + else: + self.go_high(t) + + def disable(self, t): + """Commands the output to disable. + + If `inverted=True`, this will set the output high. + + Args: + t (float): Time, in seconds, when the output disables. + """ + if self.inverted: + self.go_high(t) + else: + self.go_low(t) + + def repeat_pulse_sequence(self, t, duration, pulse_sequence, period, samplerate): + """This function only works if the DigitalQuantity is on a fast clock + + The pulse sequence specified will be repeated from time t until t+duration. + + Note 1: The samplerate should be significantly faster than the smallest time difference between + two states in the pulse sequence, or else points in your pulse sequence may never be evaluated. + + Note 2: The time points your pulse sequence is evaluated at may be different than you expect, + if another output changes state between t and t+duration. As such, you should set the samplerate + high enough that even if this rounding of tie points occurs (to fit in the update required to change the other output) + your pulse sequence will not be significantly altered) + + Args: + t (float): Time, in seconds, to start the pulse sequence. + duration (float): How long, in seconds, to repeat the sequence. + pulse_sequence (list): List of tuples, with each tuple of the form + `(time, state)`. + period (float): Defines how long the final tuple will be held for before + repeating the pulse sequence. In general, should be longer than the + entire pulse sequence. + samplerate (float): How often to update the output, in Hz. + """ + self.add_instruction( + t, + { + "function": functions.pulse_sequence(pulse_sequence, period), + "description": "pulse sequence", + "initial time":t, + "end time": t + duration, + "clock rate": samplerate, + "units": None, + } + ) + + return duration + + +class DigitalOut(DigitalQuantity): + """Digital output class for use with all devices.""" + description = "digital output" + + +class StaticDigitalQuantity(DigitalQuantity): + """Base class for :obj:`StaticDigitalOut`. + + It can also be used internally by other, more complex, output types. + """ + description = "static digital quantity" + default_value = 0 + """float: Value of output if no constant value is commanded.""" + + @set_passed_properties(property_names = {}) + def __init__(self, *args, **kwargs): + """Instatiantes the static digital quantity. + + Defines an internal tracking variable of the static output value and + calls :func:`Output.__init__`. + + Args: + *args: Passed to :func:`Output.__init__`. + **kwargs: Passed to :func:`Output.__init__`. + """ + DigitalQuantity.__init__(self, *args, **kwargs) + self._static_value = None + + def go_high(self): + """Command a static high output. + + Raises: + LabscriptError: If output has already been set low. + """ + if self._static_value is None: + self.add_instruction(0, 1) + self._static_value = 1 + else: + raise LabscriptError( + f"{self.description} {self.name} has already been set to " + f"{self.instruction_to_string(self._static_value)}. It cannot " + "also be set to 1." + ) + + def go_low(self): + """Command a static low output. + + Raises: + LabscriptError: If output has already been set high. + """ + if self._static_value is None: + self.add_instruction(0, 0) + self._static_value = 0 + else: + raise LabscriptError( + f"{self.description} {self.name} has already been set to " + f"{self.instruction_to_string(self._static_value)}. It cannot " + "also be set to 0." + ) + + def get_change_times(self): + """Enforces no change times. + + Returns: + list: An empty list, as expected by the parent pseudoclock. + """ + # Return an empty list as the calling function at the pseudoclock level expects + # a list + return [] + + def make_timeseries(self, change_times): + """Since output is static, does nothing.""" + pass + + def expand_timeseries(self, *args, **kwargs): + """Defines the `raw_output` attribute. + """ + self.raw_output = np.array([self.static_value], dtype=self.dtype) + + @property + def static_value(self): + """float: The value of the static output.""" + if self._static_value is None: + if not compiler.suppress_mild_warnings and not compiler.suppress_all_warnings: + sys.stderr.write( + f"WARNING: {self.name} has no value set. It will be set to " + f"{self.instruction_to_string(self.default_value)}.\n" + ) + self._static_value = self.default_value + return self._static_value + + +class StaticDigitalOut(StaticDigitalQuantity): + """Static Digital Output class for use with all devices that have constant outputs.""" + description = "static digital output" + + +class AnalogIn(Device): + """Analog Input for use with all devices that have an analog input.""" + description = "Analog Input" + + @set_passed_properties(property_names={}) + def __init__( + self, name, parent_device, connection, scale_factor=1.0, units="Volts", **kwargs + ): + """Instantiates an Analog Input. + + Args: + name (str): python variable to assign this input to. + parent_device (:obj:`IntermediateDevice`): Device input is connected to. + scale_factor (float, optional): Factor to scale the recorded values by. + units (str, optional): Units of the input. + **kwargs: Keyword arguments passed to :func:`Device.__init__`. + """ + self.acquisitions = [] + self.scale_factor = scale_factor + self.units=units + Device.__init__(self, name, parent_device, connection, **kwargs) + + def acquire( + self, label, start_time, end_time, wait_label="", scale_factor=None, units=None + ): + """Command an acquisition for this input. + + Args: + label (str): Unique label for the acquisition. Used to identify the saved trace. + start_time (float): Time, in seconds, when the acquisition should start. + end_time (float): Time, in seconds, when the acquisition should end. + wait_label (str, optional): + scale_factor (float): Factor to scale the saved values by. + units: Units of the input, consistent with the unit conversion class. + + Returns: + float: Duration of the acquistion, equivalent to `end_time - start_time`. + """ + if scale_factor is None: + scale_factor = self.scale_factor + if units is None: + units = self.units + self.acquisitions.append( + { + "start_time": start_time, + "end_time": end_time, + "label": label, + "wait_label": wait_label, + "scale_factor": scale_factor, + "units": units, + } + ) + return end_time - start_time + + +class Shutter(DigitalOut): + """Customized version of :obj:`DigitalOut` that accounts for the open/close + delay of a shutter automatically. + + When using the methods :meth:`open` and :meth:`close`, the shutter open + and close times are precise without haveing to track the delays. Note: + delays can be set using runmanager globals and periodically updated + via a calibration. + + .. Warning:: + + If the shutter is asked to do something at `t=0`, it cannot start + moving earlier than that. This means the initial shutter states + will have imprecise timing. + """ + description = "shutter" + + @set_passed_properties( + property_names={"connection_table_properties": ["open_state"]} + ) + def __init__( + self, name, parent_device, connection, delay=(0, 0), open_state=1, **kwargs + ): + """Instantiates a Shutter. + + Args: + name (str): python variable to assign the object to. + parent_device (:obj:`IntermediateDevice`): Parent device the + digital output is connected to. + connection (str): Physical output port of the device the digital + output is connected to. + delay (tuple, optional): Tuple of the (open, close) delays, specified + in seconds. + open_state (int, optional): Allowed values are `0` or `1`. Defines which + state of the digital output opens the shutter. + + Raises: + LabscriptError: If the `open_state` is not `0` or `1`. + """ + inverted = not bool(open_state) + DigitalOut.__init__( + self, name, parent_device, connection, inverted=inverted, **kwargs + ) + self.open_delay, self.close_delay = delay + self.open_state = open_state + if self.open_state == 1: + self.allowed_states = {0: "closed", 1: "open"} + elif self.open_state == 0: + self.allowed_states = {1: "closed", 0: "open"} + else: + raise LabscriptError( + f"Shutter {self.name} wasn't instantiated with open_state = 0 or 1." + ) + self.actual_times = {} + def open(self, t): + """Command the shutter to open at time `t`. + + Takes the open delay time into account. + + Note that the delay time will not be take into account the open delay if the + command is made at t=0 (or other times less than the open delay). No warning + will be issued for this loss of precision during compilation. + + Args: + t (float): Time, in seconds, when shutter should be open. + """ + # If a shutter is asked to do something at t=0, it cannot start moving + # earlier than that. So initial shutter states will have imprecise + # timing. Not throwing a warning here because if I did, every run + # would throw a warning for every shutter. The documentation will + # have to make a point of this. + t_calc = t-self.open_delay if t >= self.open_delay else 0 + self.actual_times[t] = {"time": t_calc, "instruction": 1} + self.enable(t_calc) + + def close(self, t): + """Command the shutter to close at time `t`. + + Takes the close delay time into account. + + Note that the delay time will not be take into account the close delay if the + command is made at t=0 (or other times less than the close delay). No warning + will be issued for this loss of precision during compilation. + + Args: + t (float): Time, in seconds, when shutter should be closed. + """ + t_calc = t-self.close_delay if t >= self.close_delay else 0 + self.actual_times[t] = {"time": t_calc, "instruction": 0} + self.disable(t_calc) + + def generate_code(self, hdf5_file): + classname = self.__class__.__name__ + calibration_table_dtypes = [ + ("name", "a256"), ("open_delay", float), ("close_delay", float) + ] + if classname not in hdf5_file["calibrations"]: + hdf5_file["calibrations"].create_dataset( + classname, (0,), dtype=calibration_table_dtypes, maxshape=(None,) + ) + metadata = (self.name, self.open_delay, self.close_delay) + dataset = hdf5_file["calibrations"][classname] + dataset.resize((len(dataset) + 1,)) + dataset[len(dataset) - 1] = metadata + + def get_change_times(self, *args, **kwargs): + retval = DigitalOut.get_change_times(self, *args, **kwargs) + + if len(self.actual_times) > 1: + sorted_times = list(self.actual_times.keys()) + sorted_times.sort() + for i in range(len(sorted_times) - 1): + time = sorted_times[i] + next_time = sorted_times[i + 1] + instruction = self.actual_times[time]["instruction"] + next_instruction = self.actual_times[next_time]["instruction"] + state = "opened" if instruction == 1 else "closed" + next_state = "open" if next_instruction == 1 else "close" + # only look at instructions that contain a state change + if instruction != next_instruction: + if self.actual_times[next_time]["time"] < self.actual_times[time]["time"]: + sys.stderr.write( + f"WARNING: The shutter '{self.name}' is requested to " + f"{next_state} too early (taking delay into account) at " + f"t={next_time:.10f}s when it is still not {state} from " + f"an earlier instruction at t={time:.10f}s\n" + ) + elif not compiler.suppress_mild_warnings and not compiler.suppress_all_warnings: + sys.stderr.write( + f"WARNING: The shutter '{self.name}' is requested to " + f"{next_state} at t={next_time:.10f}s but was never {state} " + f"after an earlier instruction at t={time:.10f}s\n" + ) + return retval + + +class Trigger(DigitalOut): + """Customized version of :obj:`DigitalOut` that tracks edge type. + """ + description = "trigger device" + allowed_children = [TriggerableDevice] + + @set_passed_properties(property_names={}) + def __init__( + self, name, parent_device, connection, trigger_edge_type="rising", **kwargs + ): + """Instantiates a DigitalOut object that tracks the trigger edge type. + + Args: + name (str): python variable name to assign the quantity to. + parent_device (:obj:`IntermediateDevice`): Device this quantity is attached to. + trigger_edge_type (str, optional): Allowed values are `'rising'` and `'falling'`. + **kwargs: Passed to :func:`Output.__init__`. + + """ + DigitalOut.__init__(self, name, parent_device, connection, **kwargs) + self.trigger_edge_type = trigger_edge_type + if self.trigger_edge_type == "rising": + self.enable = self.go_high + self.disable = self.go_low + self.allowed_states = {1: "enabled", 0: "disabled"} + elif self.trigger_edge_type == "falling": + self.enable = self.go_low + self.disable = self.go_high + self.allowed_states = {1: "disabled", 0: "enabled"} + else: + raise ValueError( + "trigger_edge_type must be 'rising' or 'falling', not " + f"'{trigger_edge_type}'." + ) + # A list of the times this trigger has been asked to trigger: + self.triggerings = [] + + def trigger(self, t, duration): + """Command a trigger pulse. + + Args: + t (float): Time, in seconds, for the trigger edge to occur. + duration (float): Duration of the trigger, in seconds. + """ + assert duration > 0, "Negative or zero trigger duration given" + if t != self.t0 and self.t0 not in self.instructions: + self.disable(self.t0) + + start = t + end = t + duration + for other_start, other_duration in self.triggerings: + other_end = other_start + other_duration + # Check for overlapping exposures: + if not (end < other_start or start > other_end): + raise LabscriptError( + f"{self.description} {self.name} has two overlapping triggerings: " + f"one at t = {start}s for {duration}s, and another at " + f"t = {other_start}s for {other_duration}s." + ) + self.enable(t) + self.disable(round(t + duration, 10)) + self.triggerings.append((t, duration)) + + def add_device(self, device): + if device.connection != "trigger": + raise LabscriptError( + f"The 'connection' string of device {device.name} " + f"to {self.name} must be 'trigger', not '{device.connection}'" + ) + DigitalOut.add_device(self, device) + + +class DDSQuantity(Device): + """Used to define a DDS output. + + It is a container class, with properties that allow access to a frequency, + amplitude, and phase of the output as :obj:`AnalogQuantity`. + It can also have a gate, which provides enable/disable control of the output + as :obj:`DigitalOut`. + + This class instantiates channels for frequency/amplitude/phase (and optionally the + gate) itself. + """ + description = 'DDS' + allowed_children = [AnalogQuantity, DigitalOut, DigitalQuantity] + + @set_passed_properties(property_names={}) + def __init__( + self, + name, + parent_device, + connection, + digital_gate=None, + freq_limits=None, + freq_conv_class=None, + freq_conv_params=None, + amp_limits=None, + amp_conv_class=None, + amp_conv_params=None, + phase_limits=None, + phase_conv_class=None, + phase_conv_params=None, + call_parents_add_device=True, + **kwargs + ): + """Instantiates a DDS quantity. + + Args: + name (str): python variable for the object created. + parent_device (:obj:`IntermediateDevice`): Device this output is + connected to. + connection (str): Output of parent device this DDS is connected to. + digital_gate (dict, optional): Configures a digital output to use as an enable/disable + gate for the output. Should contain keys `'device'` and `'connection'` + with arguments for the `parent_device` and `connection` for instantiating + the :obj:`DigitalOut`. All other (optional) keys are passed as kwargs. + freq_limits (tuple, optional): `(lower, upper)` limits for the + frequency of the output + freq_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): + Unit conversion class for the frequency of the output. + freq_conv_params (dict, optional): Keyword arguments passed to the + unit conversion class for the frequency of the output. + amp_limits (tuple, optional): `(lower, upper)` limits for the + amplitude of the output + amp_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): + Unit conversion class for the amplitude of the output. + amp_conv_params (dict, optional): Keyword arguments passed to the + unit conversion class for the amplitude of the output. + phase_limits (tuple, optional): `(lower, upper)` limits for the + phase of the output + phase_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): + Unit conversion class for the phase of the output. + phase_conv_params (dict, optional): Keyword arguments passed to the + unit conversion class for the phase of the output. + call_parents_add_device (bool, optional): Have the parent device run + its `add_device` method. + **kwargs: Keyword arguments passed to :func:`Device.__init__`. + """ + # Here we set call_parents_add_device=False so that we + # can do additional initialisation before manually calling + # self.parent_device.add_device(self). This allows the parent's + # add_device method to perform checks based on the code below, + # whilst still providing us with the checks and attributes that + # Device.__init__ gives us in the meantime. + Device.__init__( + self, name, parent_device, connection, call_parents_add_device=False, **kwargs + ) + + # Ask the parent device if it has default unit conversion classes it would like + # us to use: + if hasattr(parent_device, 'get_default_unit_conversion_classes'): + classes = self.parent_device.get_default_unit_conversion_classes(self) + default_freq_conv, default_amp_conv, default_phase_conv = classes + # If the user has not overridden, use these defaults. If + # the parent does not have a default for one or more of amp, + # freq or phase, it should return None for them. + if freq_conv_class is None: + freq_conv_class = default_freq_conv + if amp_conv_class is None: + amp_conv_class = default_amp_conv + if phase_conv_class is None: + phase_conv_class = default_phase_conv + + self.frequency = AnalogQuantity( + f"{self.name}_freq", + self, + "freq", + freq_limits, + freq_conv_class, + freq_conv_params, + ) + self.amplitude = AnalogQuantity( + f"{self.name}_amp", + self, + "amp", + amp_limits, + amp_conv_class, + amp_conv_params, + ) + self.phase = AnalogQuantity( + f"{self.name}_phase", + self, + "phase", + phase_limits, + phase_conv_class, + phase_conv_params, + ) + + self.gate = None + digital_gate = digital_gate or {} + if "device" in digital_gate and "connection" in digital_gate: + dev = digital_gate.pop("device") + conn = digital_gate.pop("connection") + self.gate = DigitalOut(f"{name}_gate", dev, conn, **digital_gate) + # Did they only put one key in the dictionary, or use the wrong keywords? + elif len(digital_gate) > 0: + raise LabscriptError( + 'You must specify the "device" and "connection" for the digital gate ' + f"of {self.name}." + ) + + # If the user has not specified a gate, and the parent device + # supports gating of DDS output, it should add a gate to this + # instance in its add_device method, which is called below. If + # they *have* specified a gate device, but the parent device + # has its own gating (such as the PulseBlaster), it should + # check this and throw an error in its add_device method. See + # labscript_devices.PulseBlaster.PulseBlaster.add_device for an + # example of this. + # In some subclasses we need to hold off on calling the parent + # device's add_device function until further code has run, + # e.g., see PulseBlasterDDS in PulseBlaster.py + if call_parents_add_device: + self.parent_device.add_device(self) + + def setamp(self, t, value, units=None): + """Set the amplitude of the output. + + Args: + t (float): Time, in seconds, when the amplitude is set. + value (float): Amplitude to set to. + units: Units that the value is defined in. + """ + self.amplitude.constant(t, value, units) + + def setfreq(self, t, value, units=None): + """Set the frequency of the output. + + Args: + t (float): Time, in seconds, when the frequency is set. + value (float): Frequency to set to. + units: Units that the value is defined in. + """ + self.frequency.constant(t, value, units) + + def setphase(self, t, value, units=None): + """Set the phase of the output. + + Args: + t (float): Time, in seconds, when the phase is set. + value (float): Phase to set to. + units: Units that the value is defined in. + """ + self.phase.constant(t, value, units) + + def enable(self, t): + """Enable the Output. + + Args: + t (float): Time, in seconds, to enable the output at. + + Raises: + LabscriptError: If the DDS is not instantiated with a digital gate. + """ + if self.gate is None: + raise LabscriptError( + f"DDS {self.name} does not have a digital gate, so you cannot use the " + "enable(t) method." + ) + self.gate.go_high(t) + + def disable(self, t): + """Disable the Output. + + Args: + t (float): Time, in seconds, to disable the output at. + + Raises: + LabscriptError: If the DDS is not instantiated with a digital gate. + """ + if self.gate is None: + raise LabscriptError( + f"DDS {self.name} does not have a digital gate, so you cannot use the " + "disable(t) method." + ) + self.gate.go_low(t) + + def pulse( + self, + t, + duration, + amplitude, + frequency, + phase=None, + amplitude_units=None, + frequency_units=None, + phase_units=None, + print_summary=False, + ): + """Pulse the output. + + Args: + t (float): Time, in seconds, to start the pulse at. + duration (float): Length of the pulse, in seconds. + amplitude (float): Amplitude to set the output to during the pulse. + frequency (float): Frequency to set the output to during the pulse. + phase (float, optional): Phase to set the output to during the pulse. + amplitude_units: Units of `amplitude`. + frequency_units: Units of `frequency`. + phase_units: Units of `phase`. + print_summary (bool, optional): Print a summary of the pulse during + compilation time. + + Returns: + float: Duration of the pulse, in seconds. + """ + if print_summary: + functions.print_time( + t, + f"{self.name} pulse at {frequency/MHz:.4f} MHz for {duration/ms:.3f} ms", + ) + self.setamp(t, amplitude, amplitude_units) + if frequency is not None: + self.setfreq(t, frequency, frequency_units) + if phase is not None: + self.setphase(t, phase, phase_units) + if amplitude != 0 and self.gate is not None: + self.enable(t) + self.disable(t + duration) + self.setamp(t + duration, 0) + return duration + + +class DDS(DDSQuantity): + """DDS class for use with all devices that have DDS-like outputs.""" + + +class StaticDDS(Device): + """Static DDS class for use with all devices that have static DDS-like outputs.""" + description = "Static RF" + allowed_children = [StaticAnalogQuantity,DigitalOut,StaticDigitalOut] + + @set_passed_properties(property_names = {}) + def __init__( + self, + name, + parent_device, + connection, + digital_gate=None, + freq_limits=None, + freq_conv_class=None, + freq_conv_params=None, + amp_limits=None, + amp_conv_class=None, + amp_conv_params=None, + phase_limits=None, + phase_conv_class=None, + phase_conv_params=None, + **kwargs, + ): + """Instantiates a Static DDS quantity. + + Args: + name (str): python variable for the object created. + parent_device (:obj:`IntermediateDevice`): Device this output is + connected to. + connection (str): Output of parent device this DDS is connected to. + digital_gate (dict, optional): Configures a digital output to use as an enable/disable + gate for the output. Should contain keys `'device'` and `'connection'` + with arguments for the `parent_device` and `connection` for instantiating + the :obj:`DigitalOut`. All other (optional) keys are passed as kwargs. + freq_limits (tuple, optional): `(lower, upper)` limits for the + frequency of the output + freq_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): + Unit conversion class for the frequency of the output. + freq_conv_params (dict, optional): Keyword arguments passed to the + unit conversion class for the frequency of the output. + amp_limits (tuple, optional): `(lower, upper)` limits for the + amplitude of the output + amp_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): + Unit conversion class for the amplitude of the output. + amp_conv_params (dict, optional): Keyword arguments passed to the + unit conversion class for the amplitude of the output. + phase_limits (tuple, optional): `(lower, upper)` limits for the + phase of the output + phase_conv_class (:obj:`labscript_utils:labscript_utils.unitconversions`, optional): + Unit conversion class for the phase of the output. + phase_conv_params (dict, optional): Keyword arguments passed to the + unit conversion class for the phase of the output. + call_parents_add_device (bool, optional): Have the parent device run + its `add_device` method. + **kwargs: Keyword arguments passed to :func:`Device.__init__`. + """ + # We tell Device.__init__ to not call + # self.parent.add_device(self), we'll do that ourselves later + # after further intitialisation, so that the parent can see the + # freq/amp/phase objects and manipulate or check them from within + # its add_device method. + Device.__init__( + self, name, parent_device, connection, call_parents_add_device=False, **kwargs + ) + + # Ask the parent device if it has default unit conversion classes it would like us to use: + if hasattr(parent_device, 'get_default_unit_conversion_classes'): + classes = parent_device.get_default_unit_conversion_classes(self) + default_freq_conv, default_amp_conv, default_phase_conv = classes + # If the user has not overridden, use these defaults. If + # the parent does not have a default for one or more of amp, + # freq or phase, it should return None for them. + if freq_conv_class is None: + freq_conv_class = default_freq_conv + if amp_conv_class is None: + amp_conv_class = default_amp_conv + if phase_conv_class is None: + phase_conv_class = default_phase_conv + + self.frequency = StaticAnalogQuantity( + f"{self.name}_freq", + self, + "freq", + freq_limits, + freq_conv_class, + freq_conv_params + ) + self.amplitude = StaticAnalogQuantity( + f"{self.name}_amp", + self, + "amp", + amp_limits, + amp_conv_class, + amp_conv_params, + ) + self.phase = StaticAnalogQuantity( + f"{self.name}_phase", + self, + "phase", + phase_limits, + phase_conv_class, + phase_conv_params, + ) + + digital_gate = digital_gate or {} + if "device" in digital_gate and "connection" in digital_gate: + dev = digital_gate.pop("device") + conn = digital_gate.pop("connection") + self.gate = DigitalOut(f"{name}_gate", dev, conn, **digital_gate) + # Did they only put one key in the dictionary, or use the wrong keywords? + elif len(digital_gate) > 0: + raise LabscriptError( + 'You must specify the "device" and "connection" for the digital gate ' + f"of {self.name}" + ) + # Now we call the parent's add_device method. This is a must, since we didn't do so earlier from Device.__init__. + self.parent_device.add_device(self) + + def setamp(self, value, units=None): + """Set the static amplitude of the output. + + Args: + value (float): Amplitude to set to. + units: Units that the value is defined in. + """ + self.amplitude.constant(value,units) + + def setfreq(self, value, units=None): + """Set the static frequency of the output. + + Args: + value (float): Frequency to set to. + units: Units that the value is defined in. + """ + self.frequency.constant(value,units) + + def setphase(self, value, units=None): + """Set the static phase of the output. + + Args: + value (float): Phase to set to. + units: Units that the value is defined in. + """ + self.phase.constant(value,units) + + def enable(self, t=None): + """Enable the Output. + + Args: + t (float, optional): Time, in seconds, to enable the output at. + + Raises: + LabscriptError: If the DDS is not instantiated with a digital gate. + """ + if self.gate: + self.gate.go_high(t) + else: + raise LabscriptError( + f"DDS {self.name} does not have a digital gate, so you cannot use the " + "enable(t) method." + ) + + def disable(self, t=None): + """Disable the Output. + + Args: + t (float, optional): Time, in seconds, to disable the output at. + + Raises: + LabscriptError: If the DDS is not instantiated with a digital gate. + """ + if self.gate: + self.gate.go_low(t) + else: + raise LabscriptError( + f"DDS {self.name} does not have a digital gate, so you cannot use the " + "disable(t) method." + ) + diff --git a/labscript/utils.py b/labscript/utils.py index 37ef294..d08c195 100644 --- a/labscript/utils.py +++ b/labscript/utils.py @@ -1,8 +1,11 @@ +import contextlib from inspect import getcallargs from functools import wraps import numpy as np +from .compiler import compiler + _RemoteConnection = None ClockLine = None PseudoClockDevice = None @@ -179,6 +182,40 @@ def bitfield(arrays,dtype): return y +@contextlib.contextmanager() +def suppress_mild_warnings(state=True): + """A context manager which modifies compiler.suppress_mild_warnings + + Allows the user to suppress (or show) mild warnings for specific lines. Useful when + you want to hide/show all warnings from specific lines. + + Arguments: + state (bool): The new state for ``compiler.suppress_mild_warnings``. Defaults to + ``True`` if not explicitly provided. + """ + previous_warning_setting = compiler.suppress_mild_warnings + compiler.suppress_mild_warnings = state + yield + compiler.suppress_mild_warnings = previous_warning_setting + + +@contextlib.contextmanager() +def suppress_all_warnings(state=True): + """A context manager which modifies compiler.suppress_all_warnings + + Allows the user to suppress (or show) all warnings for specific lines. Useful when + you want to hide/show all warnings from specific lines. + + Arguments: + state (bool): The new state for ``compiler.suppress_all_warnings``. Defaults to + ``True`` if not explicitly provided. + """ + previous_warning_setting = compiler.suppress_all_warnings + compiler.suppress_all_warnings = state + yield + compiler.suppress_all_warnings = previous_warning_setting + + class LabscriptError(Exception): """A *labscript* error. From 96b3dd3722e2919bf07b9e7d345c5498eae637a8 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Thu, 4 Jan 2024 17:28:26 +1100 Subject: [PATCH 07/19] Revert change made that tried to write to a property --- labscript/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/labscript/core.py b/labscript/core.py index bf79c8c..13dd679 100644 --- a/labscript/core.py +++ b/labscript/core.py @@ -73,9 +73,10 @@ def add_device(self, device): self._clock_limit = device.clock_limit # Update minimum clock high time if this new device requires a longer high time. if getattr(device, 'minimum_clock_high_time', None) is not None: - self.minimum_clock_high_time = max( + self._minimum_clock_high_time = max( device.minimum_clock_high_time, self.minimum_clock_high_time ) + @property def clock_limit(self): """float: Clock limit for this line, typically set by speed of child Intermediate Devices.""" From 4d0089b6e4127717f153ea3844f7d1107d9f786b Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Thu, 4 Jan 2024 17:29:52 +1100 Subject: [PATCH 08/19] Fix bug in error condition. This was an error that perviously wasn't raised (message was formed, but not raised), hence why the incorrect condition was never noticed. --- labscript/outputs.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/labscript/outputs.py b/labscript/outputs.py index 7a37545..647b787 100644 --- a/labscript/outputs.py +++ b/labscript/outputs.py @@ -347,7 +347,10 @@ def do_checks(self, trigger_times): f"t={trigger_time + self.trigger_delay}" ) # Check that there are no instructions too soon before the trigger: - if 0 < trigger_time - t < max(self.clock_limit, compiler.wait_delay): + if ( + t < trigger_time + and max(self.clock_limit, compiler.wait_delay) < trigger_time - t + ): raise LabscriptError( f"{self.description} {self.name} has an instruction at t={t}. " f"This is too soon before a trigger at t={trigger_time}, " From 5b580ad2cfcf9ec36597eb167d2228e3b8601caf Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Thu, 4 Jan 2024 17:30:11 +1100 Subject: [PATCH 09/19] Fix bug with cached imports in utils --- labscript/utils.py | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/labscript/utils.py b/labscript/utils.py index d08c195..887dc28 100644 --- a/labscript/utils.py +++ b/labscript/utils.py @@ -6,9 +6,9 @@ from .compiler import compiler -_RemoteConnection = None -ClockLine = None -PseudoClockDevice = None +cached_RemoteConnection = None +cached_ClockLine = None +cached_PseudoclockDevice = None def is_remote_connection(connection): @@ -19,9 +19,11 @@ def is_remote_connection(connection): maintaining reasonable performance (this performs better than importing each time as the lookup in the modules hash table is slower). """ - if _RemoteConnection is None: + global cached_RemoteConnection + if cached_RemoteConnection is None: from .remote import _RemoteConnection - return isinstance(connection, _RemoteConnection) + cached_RemoteConnection = _RemoteConnection + return isinstance(connection, cached_RemoteConnection) def is_clock_line(device): @@ -32,22 +34,26 @@ def is_clock_line(device): maintaining reasonable performance (this performs better than importing each time as the lookup in the modules hash table is slower). """ - if ClockLine is None: + global cached_ClockLine + if cached_ClockLine is None: from .core import ClockLine - return isinstance(device, _RemoteConnection) + cached_ClockLine = ClockLine + return isinstance(device, cached_ClockLine) def is_pseudoclock_device(device): """Returns whether the connection is an instance of ``PseudoclockDevice`` - This function defers and caches the import of ``_RemoteConnection``. This both - breaks the circular import between ``Device`` and ``_RemoteConnection``, while + This function defers and caches the import of ``PseudoclockDevice``. This both + breaks the circular import between ``Device`` and ``PseudoclockDevice``, while maintaining reasonable performance (this performs better than importing each time as the lookup in the modules hash table is slower). """ - if PseudoclockDevice is None: + global cached_PseudoclockDevice + if cached_PseudoclockDevice is None: from .core import PseudoclockDevice - return isinstance(device, PseudoclockDevice) + cached_PseudoclockDevice = PseudoclockDevice + return isinstance(device, cached_PseudoclockDevice) def set_passed_properties(property_names=None): @@ -182,7 +188,7 @@ def bitfield(arrays,dtype): return y -@contextlib.contextmanager() +@contextlib.contextmanager def suppress_mild_warnings(state=True): """A context manager which modifies compiler.suppress_mild_warnings @@ -199,7 +205,7 @@ def suppress_mild_warnings(state=True): compiler.suppress_mild_warnings = previous_warning_setting -@contextlib.contextmanager() +@contextlib.contextmanager def suppress_all_warnings(state=True): """A context manager which modifies compiler.suppress_all_warnings From 91025be54935bed9429f1f7075654100847512b7 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 12:16:47 +1100 Subject: [PATCH 10/19] Revert change to use public property in `ClockLine.add_device` internals --- labscript/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/labscript/core.py b/labscript/core.py index 13dd679..fb5e0cc 100644 --- a/labscript/core.py +++ b/labscript/core.py @@ -74,7 +74,7 @@ def add_device(self, device): # Update minimum clock high time if this new device requires a longer high time. if getattr(device, 'minimum_clock_high_time', None) is not None: self._minimum_clock_high_time = max( - device.minimum_clock_high_time, self.minimum_clock_high_time + device.minimum_clock_high_time, self._minimum_clock_high_time ) @property From 36dad35b5bf6da7e3baf7418bbfeacfd5d3c5f84 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 12:37:53 +1100 Subject: [PATCH 11/19] First pass at getting new files included in the docs --- docs/source/api/index.rst | 7 +++++++ docs/source/connection_table.rst | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index bf095f9..0da152f 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -7,5 +7,12 @@ API Reference :template: autosummary-module.rst :recursive: + labscript.core + labscript.outputs + labscript.remote + labscript.constants labscript.labscript labscript.functions + labscript.base + labscript.compiler + labscript.utils diff --git a/docs/source/connection_table.rst b/docs/source/connection_table.rst index 6f6338a..50900f4 100644 --- a/docs/source/connection_table.rst +++ b/docs/source/connection_table.rst @@ -5,7 +5,7 @@ The connection table maps out the way input/output devices are connected to each .. image:: img/connection_diagram.png :alt: Example wiring diagram. -Here we see two :py:class:`PseudoclockDevice ` instances in the top tier of the diagram. They do not have a parent device that tells them when to update their output (this is true for all :py:class:`PseudoclockDevice ` instances). However, all but one (the master pseudoclock device) must be triggered by an output clocked by the master pseudoclock device. +Here we see two :py:class:`PseudoclockDevice ` instances in the top tier of the diagram. They do not have a parent device that tells them when to update their output (this is true for all :py:class:`PseudoclockDevice ` instances). However, all but one (the master pseudoclock device) must be triggered by an output clocked by the master pseudoclock device. Each :py:class:`PseudoclockDevice ` instance should have one or more :py:class:`Pseudoclock ` children. Some :py:class:`PseudoclockDevice ` instances may automatically create these children for you (check the device specific documentation). In turn, each :py:class:`Pseudoclock ` will have one of more :py:class:`ClockLine ` instances connected to it. These :py:class:`ClockLine ` instances generally refer to physical outputs of a device which will be used to clock another device. However, in some cases, one or more :py:class:`ClockLine ` instances may be internally created for you (check the device specific documentation). From 8775e249d71334a82acfdbfdf294ae2b21ebdd4f Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 12:55:17 +1100 Subject: [PATCH 12/19] Updated connection table documentation with links to class documentation. --- docs/source/connection_table.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/source/connection_table.rst b/docs/source/connection_table.rst index 50900f4..0bd9468 100644 --- a/docs/source/connection_table.rst +++ b/docs/source/connection_table.rst @@ -5,9 +5,11 @@ The connection table maps out the way input/output devices are connected to each .. image:: img/connection_diagram.png :alt: Example wiring diagram. -Here we see two :py:class:`PseudoclockDevice ` instances in the top tier of the diagram. They do not have a parent device that tells them when to update their output (this is true for all :py:class:`PseudoclockDevice ` instances). However, all but one (the master pseudoclock device) must be triggered by an output clocked by the master pseudoclock device. +Here we see two :py:class:`PseudoclockDevice ` instances in the top tier of the diagram. They do not have a parent device that tells them when to update their output (this is true for all :py:class:`PseudoclockDevice ` instances). However, all but one (the master pseudoclock device) must be triggered by an output clocked by the master pseudoclock device. -Each :py:class:`PseudoclockDevice ` instance should have one or more :py:class:`Pseudoclock ` children. Some :py:class:`PseudoclockDevice ` instances may automatically create these children for you (check the device specific documentation). In turn, each :py:class:`Pseudoclock ` will have one of more :py:class:`ClockLine ` instances connected to it. These :py:class:`ClockLine ` instances generally refer to physical outputs of a device which will be used to clock another device. However, in some cases, one or more :py:class:`ClockLine ` instances may be internally created for you (check the device specific documentation). +Each :py:class:`PseudoclockDevice ` instance should have one or more :py:class:`Pseudoclock ` children. Some :py:class:`PseudoclockDevice ` instances may automatically create these children for you (check the device specific documentation). In turn, each :py:class:`Pseudoclock ` will have one of more :py:class:`ClockLine ` instances connected to it. These :py:class:`ClockLine ` instances generally refer to physical outputs of a device which will be used to clock another device. However, in some cases, one or more :py:class:`ClockLine ` instances may be internally created for you (check the device specific documentation). -If a device is not a :py:class:`PseudoclockDevice `, it must be connected to one via a clockline. such devices inherit from :py:class:`IntermediateDevice `. Inputs and outputs are then connected to these devices. If a :py:class:`PseudoclockDevice ` also has outputs that are not used for a :py:class:`ClockLine `, then an :py:class:`IntermediateDevice ` is internally instantiated, and should be made available through the ``PseudoclockDevice.direct_outputs`` attribute (for example see PulseBlaster implementation TODO: link!). +If a device is not a :py:class:`PseudoclockDevice `, it must be connected to one via a clockline. such devices inherit from :py:class:`IntermediateDevice `. Inputs and outputs are then connected to these devices. For example, :py:class:`DigitalOut `, :py:class:`AnalogOut `, and :py:class:`DDS `. See :py:module:`labscript.outputs` for a complete list. Note that devices determine what types of inputs and outputs can be connected, see :doc:`labscript-devices ` for device information. + +If a :py:class:`PseudoclockDevice ` also has outputs that are not used for a :py:class:`ClockLine `, then an :py:class:`IntermediateDevice ` is internally instantiated, and should be made available through the ``PseudoclockDevice.direct_outputs`` attribute (for example see the :py:class:`PulseBlaster ` implementation). From 9a9e2050ed8d968f8d04e657b27f72549908608e Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 13:00:34 +1100 Subject: [PATCH 13/19] Attempt to fix module cross reference --- docs/source/connection_table.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/connection_table.rst b/docs/source/connection_table.rst index 0bd9468..576c522 100644 --- a/docs/source/connection_table.rst +++ b/docs/source/connection_table.rst @@ -9,7 +9,7 @@ Here we see two :py:class:`PseudoclockDevice ` Each :py:class:`PseudoclockDevice ` instance should have one or more :py:class:`Pseudoclock ` children. Some :py:class:`PseudoclockDevice ` instances may automatically create these children for you (check the device specific documentation). In turn, each :py:class:`Pseudoclock ` will have one of more :py:class:`ClockLine ` instances connected to it. These :py:class:`ClockLine ` instances generally refer to physical outputs of a device which will be used to clock another device. However, in some cases, one or more :py:class:`ClockLine ` instances may be internally created for you (check the device specific documentation). -If a device is not a :py:class:`PseudoclockDevice `, it must be connected to one via a clockline. such devices inherit from :py:class:`IntermediateDevice `. Inputs and outputs are then connected to these devices. For example, :py:class:`DigitalOut `, :py:class:`AnalogOut `, and :py:class:`DDS `. See :py:module:`labscript.outputs` for a complete list. Note that devices determine what types of inputs and outputs can be connected, see :doc:`labscript-devices ` for device information. +If a device is not a :py:class:`PseudoclockDevice `, it must be connected to one via a clockline. such devices inherit from :py:class:`IntermediateDevice `. Inputs and outputs are then connected to these devices. For example, :py:class:`DigitalOut `, :py:class:`AnalogOut `, and :py:class:`DDS `. See :py:mod:`labscript.outputs` for a complete list. Note that devices determine what types of inputs and outputs can be connected, see :doc:`labscript-devices ` for device information. If a :py:class:`PseudoclockDevice ` also has outputs that are not used for a :py:class:`ClockLine `, then an :py:class:`IntermediateDevice ` is internally instantiated, and should be made available through the ``PseudoclockDevice.direct_outputs`` attribute (for example see the :py:class:`PulseBlaster ` implementation). From 59757a4d2d0519996e9a5200787c180aa9f32752 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 13:08:04 +1100 Subject: [PATCH 14/19] Added note about not using generic classes directly --- docs/source/connection_table.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/source/connection_table.rst b/docs/source/connection_table.rst index 576c522..028ff55 100644 --- a/docs/source/connection_table.rst +++ b/docs/source/connection_table.rst @@ -13,3 +13,5 @@ If a device is not a :py:class:`PseudoclockDevice ` also has outputs that are not used for a :py:class:`ClockLine `, then an :py:class:`IntermediateDevice ` is internally instantiated, and should be made available through the ``PseudoclockDevice.direct_outputs`` attribute (for example see the :py:class:`PulseBlaster ` implementation). +.. note:: + Most user's will not need to use :py:class:`PseudoclockDevice `, :py:class:`Pseudoclock `, and :py:class:`IntermediateDevice ` directly. These are generic classes that are subclassed by device implementations in :doc:`labscript-devices `. It is these device implementations that you are most likely to use. From 5c04f428470a820f1348b889a989c360c2bd1840 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 15:49:00 +1100 Subject: [PATCH 15/19] Split `AnalogIn` into it's own module so that documentation makes a little more sese --- docs/source/api/index.rst | 1 + docs/source/connection_table.rst | 2 +- labscript/inputs.py | 60 ++++++++++++++++++++++++++++++++ labscript/labscript.py | 2 +- labscript/outputs.py | 57 ++---------------------------- 5 files changed, 65 insertions(+), 57 deletions(-) create mode 100644 labscript/inputs.py diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index 0da152f..b05b455 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -9,6 +9,7 @@ API Reference labscript.core labscript.outputs + labscript.inputs labscript.remote labscript.constants labscript.labscript diff --git a/docs/source/connection_table.rst b/docs/source/connection_table.rst index 028ff55..6e14ed1 100644 --- a/docs/source/connection_table.rst +++ b/docs/source/connection_table.rst @@ -9,7 +9,7 @@ Here we see two :py:class:`PseudoclockDevice ` Each :py:class:`PseudoclockDevice ` instance should have one or more :py:class:`Pseudoclock ` children. Some :py:class:`PseudoclockDevice ` instances may automatically create these children for you (check the device specific documentation). In turn, each :py:class:`Pseudoclock ` will have one of more :py:class:`ClockLine ` instances connected to it. These :py:class:`ClockLine ` instances generally refer to physical outputs of a device which will be used to clock another device. However, in some cases, one or more :py:class:`ClockLine ` instances may be internally created for you (check the device specific documentation). -If a device is not a :py:class:`PseudoclockDevice `, it must be connected to one via a clockline. such devices inherit from :py:class:`IntermediateDevice `. Inputs and outputs are then connected to these devices. For example, :py:class:`DigitalOut `, :py:class:`AnalogOut `, and :py:class:`DDS `. See :py:mod:`labscript.outputs` for a complete list. Note that devices determine what types of inputs and outputs can be connected, see :doc:`labscript-devices ` for device information. +If a device is not a :py:class:`PseudoclockDevice `, it must be connected to one via a clockline. such devices inherit from :py:class:`IntermediateDevice `. Inputs and outputs are then connected to these devices. For example, :py:class:`DigitalOut `, :py:class:`AnalogOut `, and :py:class:`DDS `. See :py:mod:`labscript.outputs` and :py:mod:`labscript.inputs` for a complete list. Note that devices determine what types of inputs and outputs can be connected, see :doc:`labscript-devices ` for device information. If a :py:class:`PseudoclockDevice ` also has outputs that are not used for a :py:class:`ClockLine `, then an :py:class:`IntermediateDevice ` is internally instantiated, and should be made available through the ``PseudoclockDevice.direct_outputs`` attribute (for example see the :py:class:`PulseBlaster ` implementation). diff --git a/labscript/inputs.py b/labscript/inputs.py new file mode 100644 index 0000000..e95cec1 --- /dev/null +++ b/labscript/inputs.py @@ -0,0 +1,60 @@ +"""Classes for device channels that are inputs""" + +from .base import Device +from .utils import set_passed_properties + + +class AnalogIn(Device): + """Analog Input for use with all devices that have an analog input.""" + description = "Analog Input" + + @set_passed_properties(property_names={}) + def __init__( + self, name, parent_device, connection, scale_factor=1.0, units="Volts", **kwargs + ): + """Instantiates an Analog Input. + + Args: + name (str): python variable to assign this input to. + parent_device (:obj:`IntermediateDevice`): Device input is connected to. + scale_factor (float, optional): Factor to scale the recorded values by. + units (str, optional): Units of the input. + **kwargs: Keyword arguments passed to :func:`Device.__init__`. + """ + self.acquisitions = [] + self.scale_factor = scale_factor + self.units=units + Device.__init__(self, name, parent_device, connection, **kwargs) + + def acquire( + self, label, start_time, end_time, wait_label="", scale_factor=None, units=None + ): + """Command an acquisition for this input. + + Args: + label (str): Unique label for the acquisition. Used to identify the saved trace. + start_time (float): Time, in seconds, when the acquisition should start. + end_time (float): Time, in seconds, when the acquisition should end. + wait_label (str, optional): + scale_factor (float): Factor to scale the saved values by. + units: Units of the input, consistent with the unit conversion class. + + Returns: + float: Duration of the acquistion, equivalent to `end_time - start_time`. + """ + if scale_factor is None: + scale_factor = self.scale_factor + if units is None: + units = self.units + self.acquisitions.append( + { + "start_time": start_time, + "end_time": end_time, + "label": label, + "wait_label": wait_label, + "scale_factor": scale_factor, + "units": units, + } + ) + return end_time - start_time + diff --git a/labscript/labscript.py b/labscript/labscript.py index 5f87f80..7d5e664 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -62,9 +62,9 @@ PseudoclockDevice, TriggerableDevice, ) +from .inputs import AnalogIn from .outputs import ( Output, - AnalogIn, AnalogOut, AnalogQuantity, DDS, diff --git a/labscript/outputs.py b/labscript/outputs.py index 647b787..cf53e2d 100644 --- a/labscript/outputs.py +++ b/labscript/outputs.py @@ -1,3 +1,5 @@ +"""Classes for devices channels that out outputs""" + import sys import numpy as np @@ -1492,61 +1494,6 @@ class StaticDigitalOut(StaticDigitalQuantity): description = "static digital output" -class AnalogIn(Device): - """Analog Input for use with all devices that have an analog input.""" - description = "Analog Input" - - @set_passed_properties(property_names={}) - def __init__( - self, name, parent_device, connection, scale_factor=1.0, units="Volts", **kwargs - ): - """Instantiates an Analog Input. - - Args: - name (str): python variable to assign this input to. - parent_device (:obj:`IntermediateDevice`): Device input is connected to. - scale_factor (float, optional): Factor to scale the recorded values by. - units (str, optional): Units of the input. - **kwargs: Keyword arguments passed to :func:`Device.__init__`. - """ - self.acquisitions = [] - self.scale_factor = scale_factor - self.units=units - Device.__init__(self, name, parent_device, connection, **kwargs) - - def acquire( - self, label, start_time, end_time, wait_label="", scale_factor=None, units=None - ): - """Command an acquisition for this input. - - Args: - label (str): Unique label for the acquisition. Used to identify the saved trace. - start_time (float): Time, in seconds, when the acquisition should start. - end_time (float): Time, in seconds, when the acquisition should end. - wait_label (str, optional): - scale_factor (float): Factor to scale the saved values by. - units: Units of the input, consistent with the unit conversion class. - - Returns: - float: Duration of the acquistion, equivalent to `end_time - start_time`. - """ - if scale_factor is None: - scale_factor = self.scale_factor - if units is None: - units = self.units - self.acquisitions.append( - { - "start_time": start_time, - "end_time": end_time, - "label": label, - "wait_label": wait_label, - "scale_factor": scale_factor, - "units": units, - } - ) - return end_time - start_time - - class Shutter(DigitalOut): """Customized version of :obj:`DigitalOut` that accounts for the open/close delay of a shutter automatically. From 5ea15e8b15e99926b52552ea4d30bfd16d9a661c Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 16:05:28 +1100 Subject: [PATCH 16/19] Added copyright notices and docstrings to new files --- labscript/base.py | 15 +++++++++++++++ labscript/compiler.py | 16 ++++++++++++++++ labscript/constants.py | 32 +++++++++++++++++++++++++++++++- labscript/core.py | 16 ++++++++++++++++ labscript/functions.py | 3 +++ labscript/inputs.py | 13 +++++++++++++ labscript/labscript.py | 3 +++ labscript/outputs.py | 15 ++++++++++++++- labscript/remote.py | 15 +++++++++++++++ labscript/utils.py | 15 +++++++++++++++ 10 files changed, 141 insertions(+), 2 deletions(-) diff --git a/labscript/base.py b/labscript/base.py index 139c61d..cbab6b6 100644 --- a/labscript/base.py +++ b/labscript/base.py @@ -1,3 +1,18 @@ +##################################################################### +# # +# /base.py # +# # +# Copyright 2013, Monash University # +# # +# This file is part of the program labscript, in the labscript # +# suite (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +"""The labscript base class for all I/O/Device classes""" + import builtins import keyword diff --git a/labscript/compiler.py b/labscript/compiler.py index ff25d64..9328a42 100644 --- a/labscript/compiler.py +++ b/labscript/compiler.py @@ -1,3 +1,19 @@ +##################################################################### +# # +# /compiler.py # +# # +# Copyright 2013, Monash University # +# # +# This file is part of the program labscript, in the labscript # +# suite (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +"""The labscript compiler interface. This is only relevant to developers and those +interested in the labscript interface to runmanager.""" + import builtins from labscript_utils.labconfig import LabConfig diff --git a/labscript/constants.py b/labscript/constants.py index 907bc5b..9e623a1 100644 --- a/labscript/constants.py +++ b/labscript/constants.py @@ -1,8 +1,38 @@ +##################################################################### +# # +# /constants.py # +# # +# Copyright 2013, Monash University # +# # +# This file is part of the program labscript, in the labscript # +# suite (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +"""Common constant factors for time and frequency""" + ns = 1e-9 +"""Conversion factor between nanoseconds and seconds""" + us = 1e-6 +"""Conversion factor between microseconds and seconds""" + ms = 1e-3 +"""Conversion factor between milliseconds and seconds""" + s = 1 +"""Conversion factor between seconds and seconds""" + Hz = 1 +"""Conversion factor between hertz and hertz""" + kHz = 1e3 +"""Conversion factor between kilohertz and hertz""" + MHz = 1e6 -GHz = 1e9 \ No newline at end of file +"""Conversion factor between megahertz and hertz""" + +GHz = 1e9 +"""Conversion factor between gigahertz and hertz""" diff --git a/labscript/core.py b/labscript/core.py index fb5e0cc..078e992 100644 --- a/labscript/core.py +++ b/labscript/core.py @@ -1,3 +1,19 @@ +##################################################################### +# # +# /core.py # +# # +# Copyright 2013, Monash University # +# # +# This file is part of the program labscript, in the labscript # +# suite (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +"""Core classes containing common device functionality. These are used in +labscript-devices when adding support for a hardware device.""" + import sys import numpy as np diff --git a/labscript/functions.py b/labscript/functions.py index 829c3a0..81eebc2 100644 --- a/labscript/functions.py +++ b/labscript/functions.py @@ -11,6 +11,9 @@ # # ##################################################################### +"""Contains the functional forms of analog output ramps. These are not used directly, +instead see the interfaces in `AnalogQuantity`/`AnalogOut`.""" + from pylab import * import numpy as np diff --git a/labscript/inputs.py b/labscript/inputs.py index e95cec1..864bd66 100644 --- a/labscript/inputs.py +++ b/labscript/inputs.py @@ -1,3 +1,16 @@ +##################################################################### +# # +# /inputs.py # +# # +# Copyright 2013, Monash University # +# # +# This file is part of the program labscript, in the labscript # +# suite (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + """Classes for device channels that are inputs""" from .base import Device diff --git a/labscript/labscript.py b/labscript/labscript.py index 7d5e664..f8ab0ce 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -11,6 +11,9 @@ # # ##################################################################### +"""Everything else including the `start()`, `stop()`, and `wait()` functions. All other +classes are also imported here for backwards compatibility""" + import builtins import os import sys diff --git a/labscript/outputs.py b/labscript/outputs.py index cf53e2d..f0c8fe6 100644 --- a/labscript/outputs.py +++ b/labscript/outputs.py @@ -1,4 +1,17 @@ -"""Classes for devices channels that out outputs""" +##################################################################### +# # +# /outputs.py # +# # +# Copyright 2013, Monash University # +# # +# This file is part of the program labscript, in the labscript # +# suite (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +"""Classes for devices channels that are outputs""" import sys diff --git a/labscript/remote.py b/labscript/remote.py index 1d0d979..9efb585 100644 --- a/labscript/remote.py +++ b/labscript/remote.py @@ -1,3 +1,18 @@ +##################################################################### +# # +# /remote.py # +# # +# Copyright 2013, Monash University # +# # +# This file is part of the program labscript, in the labscript # +# suite (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +"""Classes for configuring remote/secondary BLACS and/or device workers""" + from .compiler import compiler from .labscript import Device, set_passed_properties diff --git a/labscript/utils.py b/labscript/utils.py index 887dc28..384ff19 100644 --- a/labscript/utils.py +++ b/labscript/utils.py @@ -1,3 +1,18 @@ +##################################################################### +# # +# /utils.py # +# # +# Copyright 2013, Monash University # +# # +# This file is part of the program labscript, in the labscript # +# suite (see http://labscriptsuite.org), and is licensed under the # +# Simplified BSD License. See the license.txt file in the root of # +# the project for the full license. # +# # +##################################################################### + +"""Utility functions""" + import contextlib from inspect import getcallargs from functools import wraps From 32b3d6ebb7972a9eab2f793c4c61bf7f347612f3 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 16:15:18 +1100 Subject: [PATCH 17/19] Updated module doc strings so they are only a single sentence and relocated `print_time()` function to utils. --- labscript/compiler.py | 2 +- labscript/core.py | 2 +- labscript/functions.py | 14 ++------------ labscript/labscript.py | 2 +- labscript/utils.py | 14 ++++++++++++++ 5 files changed, 19 insertions(+), 15 deletions(-) diff --git a/labscript/compiler.py b/labscript/compiler.py index 9328a42..8b1118f 100644 --- a/labscript/compiler.py +++ b/labscript/compiler.py @@ -11,7 +11,7 @@ # # ##################################################################### -"""The labscript compiler interface. This is only relevant to developers and those +"""The labscript compiler interface - this is only relevant to developers and those interested in the labscript interface to runmanager.""" import builtins diff --git a/labscript/core.py b/labscript/core.py index 078e992..c6c8c66 100644 --- a/labscript/core.py +++ b/labscript/core.py @@ -11,7 +11,7 @@ # # ##################################################################### -"""Core classes containing common device functionality. These are used in +"""Core classes containing common device functionality - these are used in labscript-devices when adding support for a hardware device.""" import sys diff --git a/labscript/functions.py b/labscript/functions.py index 81eebc2..8bd8df0 100644 --- a/labscript/functions.py +++ b/labscript/functions.py @@ -11,24 +11,14 @@ # # ##################################################################### -"""Contains the functional forms of analog output ramps. These are not used directly, +"""Contains the functional forms of analog output ramps - these are not used directly, instead see the interfaces in `AnalogQuantity`/`AnalogOut`.""" from pylab import * import numpy as np -def print_time(t, description): - """Print time with a descriptive string. +from .utils import print_time - Useful debug tool to print time at a specific point - in the shot, during shot compilation. Helpful when - the time is calculated. - - Args: - t (float): Time to print - description (str): Descriptive label to print with it - """ - print('t = {0:.9f} s:'.format(t),description) def ramp(duration, initial, final): """Defines a linear ramp. diff --git a/labscript/labscript.py b/labscript/labscript.py index f8ab0ce..48229d4 100644 --- a/labscript/labscript.py +++ b/labscript/labscript.py @@ -11,7 +11,7 @@ # # ##################################################################### -"""Everything else including the `start()`, `stop()`, and `wait()` functions. All other +"""Everything else including the `start()`, `stop()`, and `wait()` functions - all other classes are also imported here for backwards compatibility""" import builtins diff --git a/labscript/utils.py b/labscript/utils.py index 384ff19..39372d9 100644 --- a/labscript/utils.py +++ b/labscript/utils.py @@ -26,6 +26,20 @@ cached_PseudoclockDevice = None +def print_time(t, description): + """Print time with a descriptive string. + + Useful debug tool to print time at a specific point + in the shot, during shot compilation. Helpful when + the time is calculated. + + Args: + t (float): Time to print + description (str): Descriptive label to print with it + """ + print(f"t={t:.9f}s: {description}") + + def is_remote_connection(connection): """Returns whether the connection is an instance of ``_RemoteConnection`` From 7e0ab10ee29af1af3e072a04660af620719e0098 Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 16:20:40 +1100 Subject: [PATCH 18/19] Reamed `Compiler` class to work around an autosummary bug --- labscript/compiler.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/labscript/compiler.py b/labscript/compiler.py index 8b1118f..deffbd8 100644 --- a/labscript/compiler.py +++ b/labscript/compiler.py @@ -25,7 +25,10 @@ _SAVE_GIT_INFO = LabConfig().getboolean("labscript", "save_git_info", fallback=False) -class Compiler(object): +# Note: This class name is chosen to work around a autosummary bug, see +# https://github.com/sphinx-doc/sphinx/issues/11362 It could be reverted to just +# ``Compiler`` once the bug is fixed. +class _Compiler(object): """Compiler singleton that saves relevant parameters during compilation of each shot""" _instance = None @@ -85,5 +88,5 @@ def reset(self): self.compression = 'gzip' # set to 'gzip' for compression -compiler = Compiler() +compiler = _Compiler() """The compiler instance""" From 714d982a7d1116d2b6a4d0c90304aae6280f1c6b Mon Sep 17 00:00:00 2001 From: philipstarkey Date: Sat, 27 Jan 2024 16:24:56 +1100 Subject: [PATCH 19/19] Remove `compiler` module from autosummary documentation due to a bug that breaks documentation. --- docs/source/api/index.rst | 1 - labscript/compiler.py | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/source/api/index.rst b/docs/source/api/index.rst index b05b455..eccc4a9 100644 --- a/docs/source/api/index.rst +++ b/docs/source/api/index.rst @@ -15,5 +15,4 @@ API Reference labscript.labscript labscript.functions labscript.base - labscript.compiler labscript.utils diff --git a/labscript/compiler.py b/labscript/compiler.py index deffbd8..8b1118f 100644 --- a/labscript/compiler.py +++ b/labscript/compiler.py @@ -25,10 +25,7 @@ _SAVE_GIT_INFO = LabConfig().getboolean("labscript", "save_git_info", fallback=False) -# Note: This class name is chosen to work around a autosummary bug, see -# https://github.com/sphinx-doc/sphinx/issues/11362 It could be reverted to just -# ``Compiler`` once the bug is fixed. -class _Compiler(object): +class Compiler(object): """Compiler singleton that saves relevant parameters during compilation of each shot""" _instance = None @@ -88,5 +85,5 @@ def reset(self): self.compression = 'gzip' # set to 'gzip' for compression -compiler = _Compiler() +compiler = Compiler() """The compiler instance"""