Near-Time Callback Functions and 3rd-Party Devices¶
LabOne Q programs the full stack of instruments that are part of the Zurich Instruments QCCS family. Additionally, it can call any Python function from within the near-time section of an experiment and thus change or influence the experiment flow. In particular, this allows to integrate non-QCCS instruments, for instance, in a sweep.
Callback Functions in Near-Time Experiments¶
Near-time callback functions in LabOne Q allow interaction with the current experiment, at any point within its near-time execution (see Averaging and Sweeping for more explanation on real-time and near-time sweeps). A near-time callback function
- can be any function that is defined in Python,
- can accept input values passed on from the LabOne Q controller, and
- can return values.
Any values the function returns will be included in the result object after experiment execution.
Definition of Near-Time Callback Functions¶
Near-time callbacks are defined like any other function in Python, and
can contain named arguments and return values. Every function is
embedded in the scope of the current LabOne Q session, the first
argument being the runtime (execution) context (see RuntimeContext) which allows
to access various data from the controller.
An example of a near-time callback is the following:
def my_func(runtime_context, frequency, amplitude):
"""
Arbitrary Python function with named arguments and return value
Args:
runtime_context: LabOne Q RuntimeContext instance (passed by LabOne Q controller)
frequency: named argument 1
amplitude: named argument 2
Returns:
Product of the two input values.
"""
## arbitrary operations are allowed e.g., print a value, mathematical operation
print(f"frequency={frequency}, amplitude={amplitude}")
## returned values will be part of the result object
return frequency*amplitude
The Runtime Context in Near-Time Callbacks¶
The RuntimeContext used as the first argument of the callbacks allows
access to the experiment results. This allows them to read results during a
near-time sweep and to process these partial results inside the callback
function. It also allows changing device settings or replace pulses. RuntimeContext provides the
following methods:
.devices: Get a reference to the devices used in the experiment. Use with care, as this allows to change the settings of the devices during the experiment..results: Get a reference to the results of the experiment up to the current point in time. Use with care, as this allows to change the results of the experiment. Create a deep copy in case of doubt..abort_execution(): Abort the execution of the experiment immediately and gracefully, for example on convergence or divergence of a result or other criteria..replace_pulse(): Replace a specific pulse with new sample data on the device..replace_phase_increment(): Replace the value of a parameter that drives the phase increment value.
Warning
** Deprecation note **: For the time being, the following deprecated methods are still available but will be removed soon in a future release. Please modify existing code in case these methods are used in near-time callbacks to take the required information as function arguments.
.get_results().connection_state.device_calibration.experiment.experiment_calibration.signal_map
Using Near-Time Callbacks in a Sweep¶
Near-time callbacks can be used in the experiment at any point inside a
near-time loop using the call instruction, the function name and the
input arguments. The LabOne Q runtime context (of type RuntimeContext) is
implicitly passed as the first argument and
does not need to be stated by the user in the experiment definition.
Input arguments to the callback function can be float values but also a
LabOne Q sweep parameter. The latter also enables near-time sweeps of
settings on non-QCCS instruments, and will be discussed in more detail
in the following section on Controlling Non-QCCS
Devices.
An example call for the function defined above is the following:
exp.call("my_func", frequency = sweep_parameter, amplitude = 0.1)
Here, the frequency input is a sweep parameter, while the amplitude value is fixed.
A full example experiment definition using near-time callback functions is shown below. It shows different ways to refer to the function, as commented in the code snippet. During the experiment execution, the order of calls to user-defined functions is maintained. In this way it is possible, for instance, to have a near-time callback function before and after the real-time averaging loop.
## outer loop - near-time sweep
with exp.sweep(uid="outer_sweep", parameter=sweep_parameter):
## Only named arguments are supported.
## Arguments to `exp.call` must match those
## of the respective function definition.
## Variant 1: Use python function name as reference.
exp.call(my_func, frequency=500e6, amplitude=sweep_parameter)
## Variant 2: Use custom name as reference.
exp.call("calc_power", amplitude=sweep_parameter, gain=1.0)
## Calling same function multiple times allowed,
## results will be appended to the same result
## list in order of execution.
exp.call("calc_power", amplitude=sweep_parameter, gain=2.0)
## The same python function may be registered
## with different reference names,
## in which case it is treated as a separate function,
## producing its own result list.
exp.call("calc_power_alt", amplitude=sweep_parameter, gain=4.0)
## inner loop - near-time sweep
with exp.sweep(uid="inner_sweep", parameter=inner_arbitrary_sweep):
## Variant 2: Use custom name as reference.
exp.call("inner_neartime_callback", param=inner_arbitrary_sweep)
## innermost loop - real-time pulse sequence with averaging
with exp.acquire_loop_rt(...):
## ...
## The call order of near-time callbacks is preserved relative to the nested sections
exp.call("after_inner_neartime_callback")
Registering Near-Time Callback Functions¶
As seen in the example above, near-time callbacks can be referred to by either using the function name as defined or by a specified reference name. The latter is defined during the registration of the near-time callback function. The registration is required for every near-time callback that is used in the experiment and ensures the inclusion of these external functions in the scope of the LabOne Q session.
The following example shows how to register near-time callbacks according to the different ways that they are used in the above example experiment definition.
## Variant 1: Use python function name as reference
session.register_neartime_callback(my_func)
## Variant 2: Give the name explicitly
session.register_neartime_callback(my_power_func, "calc_power")
## Same python function may be registered multiple times with different names
session.register_neartime_callback(my_power_func, "calc_power_alt")
session.register_neartime_callback(inner_neartime_callback, "inner_neartime_callback")
session.register_neartime_callback(after_inner_neartime_callback, "after_inner_neartime_callback")
Retrieving Results from Near-Time Callback Functions¶
Near-time callbacks used inside LabOne Q can return values which are attached to the result object as a list. Every call of the function results in a new entry in the list. The key to access the results is the registered name of the callback function:
## Return values of near-time callbacks upon execution are available per function, use function name as a key.
session.results.neartime_callback_results["my_func"]
## Two calls per iteration to `calc_power` result in two adjacent entries in the results
session.results.neartime_callback_results["calc_power"]
Aborting execution¶
A near-time callback can abort the execution of the remaining near-time steps.
Simply call runtime_context.abort_execution().
The LabOne Q controller will then immediately and gracefully terminate the experiment.
Note that abort_execution() does not return, it immediately hands control back to LabOne Q. Any code in the near-time callback after it is not executed.
Controlling Non-QCCS Devices¶
The use of near-time callbacks allows one to also control instruments in an experiment loop which are not part of the QCCS, as long as there is a Python driver available. Examples are, amongst others, microwave sources, DC voltage or current sources and magnets.
Leveraging near-time callback functions, we can also incorporate Zurich Instruments lock-in amplifiers as the UHFLI or MFLI into experiments that are run with LabOne Q. To do so, we can use the Python API or Zurich Instruments Toolkit to connect to the instruments.
Zurich Instruments Toolkit¶
zhinst.toolkit
can be used to control individual devices and node values during
experiments.
Danger
Toolkit functions cannot be called during an emulated run!
Danger
When LabOne Q controls devices by running an experiment via
session.run(), never use subscribe/poll over zhinst.toolkit (neither
in the main script before/after session.run(), nor in the user
functions)
An example of a Toolkit near-time callback function is the following:
def toolkit_awg_output_amplitude(runtime_context: RuntimeContext, amplitude: float):
"""
Change HDAWG AWG 0 output 0 amplitude.
Args:
runtime_context: LabOne Q RuntimeContext instance (passed by LabOne Q controller)
amplitude: Amplitude for the selected AWG output
"""
runtime_context.devices["device_hdawg"].awgs[0].outputs[0].amplitude(amplitude)
Now the function toolkit_awg_output_amplitude can be utilized like any
other near-time callback during an experiment.