|
| 1 | +import numpy as np |
| 2 | +from labscript import Device |
| 3 | +from labscript_utils import dedent |
| 4 | +import labscript_utils.h5_lock, h5py |
| 5 | +from .utils import serialise_function |
| 6 | + |
| 7 | + |
| 8 | +class FunctionRunner(Device): |
| 9 | + """A labscript device to run custom functions before, after, or during (not yet |
| 10 | + implemented) the experiment in software time""" |
| 11 | + |
| 12 | + def __init__(self, name, **kwargs): |
| 13 | + Device.__init__(self, name=name, parent_device=None, connection=None, **kwargs) |
| 14 | + self.functions = [] |
| 15 | + self.BLACS_connection = name |
| 16 | + |
| 17 | + def add_function(self, t, function, *args, **kwargs): |
| 18 | + """Add a function to be run at time t. If t='start', then the function will run |
| 19 | + prior to the shot beginning, and if t='stop', it will run after the experiment |
| 20 | + has completed. Tip: use `start_order` and `stop_order` keyword arguments when |
| 21 | + instantiating this device to control the relative order that its 'start' and |
| 22 | + 'stop' functions run compared to the transition_to_manual and |
| 23 | + transition_to_buffered functions of other devices. Multiple functions added to |
| 24 | + run at the same time will be run in the order added. Running functions mid-shot |
| 25 | + in software time is yet to be implemented. |
| 26 | +
|
| 27 | + The function must have a call signature like the following: |
| 28 | +
|
| 29 | + def func(shot_context, t, ...): |
| 30 | + ... |
| 31 | +
|
| 32 | + When it is called, a ShotContext instance will be passed in as the first |
| 33 | + argument, and the time at which the function was requested to run as the second |
| 34 | + argument. The ShotContext instance will be the same for all calls for the same |
| 35 | + shot, so it can be used to store state for that shot (but not from one shot to |
| 36 | + the next), the same way you would use the 'self' argument of a method to store |
| 37 | + state in an instance. As an example, you might set shot_context.serial to be an |
| 38 | + open serial connection to a device during a function set to run at t='start', |
| 39 | + and refer back to it in subsequent functions to read and write data. Other than |
| 40 | + state stored in shot_context, the functions must be self-contained, containing |
| 41 | + any imports that they need. |
| 42 | +
|
| 43 | + This object has a number of attributes: |
| 44 | +
|
| 45 | + - self.globals: the shot globals |
| 46 | + - self.h5_file: the filepath to the shot's HDF5 file |
| 47 | + - self.device_name: the name of this FunctionRunner |
| 48 | +
|
| 49 | + If you want to save raw data to the HDF5 file at the end of a shot, the |
| 50 | + recommended place to do it is within the group 'data/<device_name>', for |
| 51 | + example: |
| 52 | +
|
| 53 | + with h5py.File(self.h5_file) as f: |
| 54 | + data_group = f['data'].create_group(self.device_name) |
| 55 | + # save datasets/attributes within this group |
| 56 | +
|
| 57 | + Or, if you are doing analysis and want to save results that will be accessible |
| 58 | + to lyse analysis routines in the usual way, you can instantiate a lyse.Run |
| 59 | + object and call Run.save_result() etc: |
| 60 | +
|
| 61 | + import lyse |
| 62 | + run = lyse.Run(shot_context.h5_file) |
| 63 | + run.save_result('x', 7) |
| 64 | +
|
| 65 | + The group that the results will be saved to, which is usually the filename of |
| 66 | + the lyse analysis routine, will instead be the device name of the |
| 67 | + FunctionRunner. |
| 68 | +
|
| 69 | + The use case for which this device was implemented was to update runmanager's |
| 70 | + globals immediately after a shot, based on measurement data, such that |
| 71 | + just-in-time compiled shots imme. This is done by calling the runmanager remote API |
| 72 | + from within a function to be run at the end of a shot, like so: |
| 73 | +
|
| 74 | + import runmanager.remote |
| 75 | + runmanager.remote.set_globals({'x': 7}) |
| 76 | +
|
| 77 | + """ |
| 78 | + name, source, args, kwargs = serialise_function(function, *args, **kwargs) |
| 79 | + if t == 'start': |
| 80 | + t = -np.inf |
| 81 | + elif t == 'stop': |
| 82 | + t = np.inf |
| 83 | + else: |
| 84 | + t = float(t) |
| 85 | + msg = """Running functions mid-experiment not yet implemented. For now, t |
| 86 | + must be "start" or "stop".""" |
| 87 | + raise NotImplementedError(dedent(msg)) |
| 88 | + self.functions.append((t, name, source, args, kwargs)) |
| 89 | + |
| 90 | + def generate_code(self, hdf5_file): |
| 91 | + # Python's sorting is stable, so items with equal times will remain in the order |
| 92 | + # they were added |
| 93 | + self.functions.sort() |
| 94 | + vlenstr = h5py.special_dtype(vlen=str) |
| 95 | + table_dtypes = [ |
| 96 | + ('t', float), |
| 97 | + ('name', vlenstr), |
| 98 | + ('source', vlenstr), |
| 99 | + ('args', vlenstr), |
| 100 | + ('kwargs', vlenstr), |
| 101 | + ] |
| 102 | + function_table = np.array(self.functions, dtype=table_dtypes) |
| 103 | + group = self.init_device_group(hdf5_file) |
| 104 | + if self.functions: |
| 105 | + group.create_dataset('FUNCTION_TABLE', data=function_table) |
0 commit comments