Skip to content

Commit a2c80b2

Browse files
Merged in SoftwareDevice (pull request #86)
Added a 'device' that runs arbitrary functions before/after the experiment. Approved-by: Philip Starkey
2 parents 01f2a3e + 6ff17bf commit a2c80b2

File tree

7 files changed

+361
-0
lines changed

7 files changed

+361
-0
lines changed

FunctionRunner/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#####################################################################
2+
# #
3+
# /labscript_devices/FunctionRunner/__init__.py #
4+
# #
5+
# Copyright 2019, Monash University and contributors #
6+
# #
7+
# This file is part of labscript_devices, in the labscript suite #
8+
# (see http://labscriptsuite.org), and is licensed under the #
9+
# Simplified BSD License. See the license.txt file in the root of #
10+
# the project for the full license. #
11+
# #
12+
#####################################################################
13+
14+
from labscript_utils import check_version
15+
16+
import sys
17+
if sys.version_info < (3, 5):
18+
raise RuntimeError("FunctionRunner requires Python 3.5+")

FunctionRunner/blacs_tabs.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
#####################################################################
2+
# #
3+
# /labscript_devices/FunctionRunner/blacs_tabs.py #
4+
# #
5+
# Copyright 2019, Monash University and contributors #
6+
# #
7+
# This file is part of labscript_devices, in the labscript suite #
8+
# (see http://labscriptsuite.org), and is licensed under the #
9+
# Simplified BSD License. See the license.txt file in the root of #
10+
# the project for the full license. #
11+
# #
12+
#####################################################################
13+
14+
from blacs.device_base_class import DeviceTab
15+
16+
17+
class FunctionRunnerTab(DeviceTab):
18+
def restore_builtin_save_data(self, data):
19+
DeviceTab.restore_builtin_save_data(self, data)
20+
# Override restored settings and show and maximise the outputbox for this tab:
21+
self.set_terminal_visible(True)
22+
self._ui.splitter.setSizes([0, 0, 1])
23+
24+
def initialise_workers(self):
25+
self.create_worker(
26+
'main_worker',
27+
'labscript_devices.FunctionRunner.blacs_workers.FunctionRunnerWorker',
28+
{},
29+
)
30+
self.primary_worker = 'main_worker'

FunctionRunner/blacs_workers.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#####################################################################
2+
# #
3+
# /labscript_devices/FunctionRunner/blacs_worker.py #
4+
# #
5+
# Copyright 2019, Monash University and contributors #
6+
# #
7+
# This file is part of labscript_devices, in the labscript suite #
8+
# (see http://labscriptsuite.org), and is licensed under the #
9+
# Simplified BSD License. See the license.txt file in the root of #
10+
# the project for the full license. #
11+
# #
12+
#####################################################################
13+
import os
14+
from time import monotonic
15+
import numpy as np
16+
import labscript_utils.h5_lock
17+
import h5py
18+
from blacs.tab_base_classes import Worker
19+
import runmanager
20+
import runmanager.remote
21+
from zprocess import rich_print
22+
from .utils import deserialise_function
23+
24+
BLUE = '#66D9EF'
25+
PURPLE = '#AE81FF'
26+
GREEN = '#A6E22E'
27+
GREY = '#75715E'
28+
29+
def deserialise_function_table(function_table, device_name):
30+
table = []
31+
for t, name, source, args, kwargs in function_table:
32+
if t == -np.inf:
33+
t = 'start'
34+
elif t == np.inf:
35+
t = 'stop'
36+
# We deserialise the functions in a namespace with the given __name__ and
37+
# __file__ so that if the user instantiates a lyse.Run object, that the results
38+
# will automatically be saved to a results group with the name of this
39+
# FunctionRunner, since lyse.Run inspects the filename to determine this.
40+
function, args, kwargs = deserialise_function(
41+
name, source, args, kwargs, __name__=device_name, __file__=device_name
42+
)
43+
table.append((t, name, function, args, kwargs))
44+
return table
45+
46+
47+
class ShotContext(object):
48+
def __init__(self, h5_file, device_name):
49+
self.h5_file = h5_file
50+
self.device_name = device_name
51+
self.globals = runmanager.get_shot_globals(h5_file)
52+
53+
54+
class FunctionRunnerWorker(Worker):
55+
def program_manual(self, values):
56+
return {}
57+
58+
def transition_to_buffered(self, device_name, h5_file, initial_values, fresh):
59+
rich_print(f"====== new shot: {os.path.basename(h5_file)} ======", color=GREEN)
60+
with h5py.File(h5_file, 'r') as f:
61+
group = f[f'devices/{self.device_name}']
62+
if 'FUNCTION_TABLE' not in group:
63+
self.function_table = None
64+
rich_print("[no functions]", color=GREY)
65+
return {}
66+
function_table = group['FUNCTION_TABLE'][:]
67+
68+
self.function_table = deserialise_function_table(
69+
function_table, self.device_name
70+
)
71+
self.shot_context = ShotContext(h5_file, self.device_name)
72+
if self.function_table[0][0] != 'start':
73+
rich_print("no start functions", color=GREY)
74+
return {}
75+
rich_print("[running start functions]", color=PURPLE)
76+
while self.function_table:
77+
t, name, function, args, kwargs = self.function_table[0]
78+
if t != 'start':
79+
break
80+
del self.function_table[0]
81+
rich_print(f" t={t}: {name}()", color=BLUE)
82+
function(self.shot_context, t, *args, **kwargs)
83+
rich_print("[finished start functions]", color=PURPLE)
84+
return {}
85+
86+
def transition_to_manual(self):
87+
if self.function_table is None:
88+
return True
89+
elif not self.function_table:
90+
rich_print("no stop functions", color=GREY)
91+
return True
92+
rich_print("[running stop functions]", color=PURPLE)
93+
while self.function_table:
94+
t, name, function, args, kwargs = self.function_table.pop(0)
95+
assert t == 'stop'
96+
rich_print(f" t={t}: {name}()",color=BLUE)
97+
function(self.shot_context, t, *args, **kwargs)
98+
rich_print("[finished stop functions]", color=PURPLE)
99+
100+
return True
101+
102+
def shutdown(self):
103+
return
104+
105+
def abort_buffered(self):
106+
return self.transition_to_manual()
107+
108+
def abort_transition_to_buffered(self):
109+
return True
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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)

FunctionRunner/register_classes.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#####################################################################
2+
# #
3+
# /labscript_devices/FunctionRunner/register_classes.py #
4+
# #
5+
# Copyright 2019, Monash University and contributors #
6+
# #
7+
# This file is part of labscript_devices, in the labscript suite #
8+
# (see http://labscriptsuite.org), and is licensed under the #
9+
# Simplified BSD License. See the license.txt file in the root of #
10+
# the project for the full license. #
11+
# #
12+
#####################################################################
13+
from labscript_devices import register_classes
14+
15+
register_classes(
16+
'FunctionRunner',
17+
BLACS_tab='labscript_devices.FunctionRunner.blacs_tabs.FunctionRunnerTab',
18+
runviewer_parser=None,
19+
)

FunctionRunner/testing/test.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from labscript import *
2+
from labscript_devices.DummyPseudoclock.labscript_devices import DummyPseudoclock
3+
from labscript_devices.FunctionRunner.labscript_devices import FunctionRunner
4+
5+
labscript_init('test.h5', new=True, overwrite=True)
6+
7+
DummyPseudoclock('pseudoclock')
8+
FunctionRunner('function_runner')
9+
10+
11+
def foo(shot_context, t, arg):
12+
print(f"hello, {arg}!")
13+
import lyse
14+
run = lyse.Run(shot_context.h5_file)
15+
run.save_result('x', 7)
16+
17+
18+
function_runner.add_function('start', foo, 'world')
19+
20+
start()
21+
stop(1)

FunctionRunner/utils.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#####################################################################
2+
# #
3+
# /labscript_devices/FunctionRunner/utils.py #
4+
# #
5+
# Copyright 2019, Monash University and contributors #
6+
# #
7+
# This file is part of labscript_devices, in the labscript suite #
8+
# (see http://labscriptsuite.org), and is licensed under the #
9+
# Simplified BSD License. See the license.txt file in the root of #
10+
# the project for the full license. #
11+
# #
12+
#####################################################################
13+
14+
import inspect
15+
from types import FunctionType
16+
from labscript_utils import dedent
17+
from labscript_utils.properties import serialise, deserialise
18+
19+
20+
def serialise_function(function, *args, **kwargs):
21+
"""Serialise a function based on its source code, and serialise the additional args
22+
and kwargs that it will be called with. Raise an exception if the function signature
23+
does not begin with (shot_context, t) or if the additional args and kwargs are
24+
incompatible with the rest of the function signature"""
25+
signature = inspect.signature(function)
26+
if not tuple(signature.parameters)[:2] == ('shot_context', 't'):
27+
msg = """function must be defined with (shot_context, t, ...) as its first two
28+
arguments"""
29+
raise ValueError(dedent(msg))
30+
# This will raise an error if the arguments do not match the function's call
31+
# signature:
32+
_ = signature.bind(None, None, *args, **kwargs)
33+
34+
# Enure it's a bona fide function and not some other callable:
35+
if not isinstance(function, FunctionType):
36+
msg = f"""callable of type {type(function)} is not a function. Only functions
37+
can be used, not other callables"""
38+
raise TypeError(dedent(msg))
39+
40+
# Serialise the function, args and kwargs:
41+
source = inspect.getsource(function)
42+
args = serialise(args)
43+
kwargs = serialise(kwargs)
44+
45+
return function.__name__, source, args, kwargs
46+
47+
48+
def deserialise_function(
49+
name, source, args, kwargs, __name__=None, __file__='<string>'
50+
):
51+
"""Deserialise a function that was serialised by serialise_function. Optional
52+
__name__ and __file__ arguments set those attributes in the namespace that the
53+
function will be defined."""
54+
args = deserialise(args)
55+
kwargs = deserialise(kwargs)
56+
code = compile(source, '<string>', 'exec', dont_inherit=True,)
57+
namespace = {'__name__': __name__, '__file__': __file__}
58+
exec(code, namespace)
59+
return namespace[name], args, kwargs

0 commit comments

Comments
 (0)