# 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

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 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 Callback Functions

Callback functions 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, thus the first argument is always the current session.

An example of a callback function is the following:

def my_func(session, frequency, amplitude):
"""
Arbitrary Python function with named arguments and return value

Args:
session: LabOne Q session instance (forwarded 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

Since callback functions have access to the current session, they also have access to the session results. This allows them to read results during a near-time sweep and to process these partial results inside the callback function.

### Using Callback Functions in a Sweep

Callback functions 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 session object is implicitly forwarded 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 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 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_user_func", param=inner_arbitrary_sweep)

# innermost loop - real-time pulse sequence with averaging
with exp.acquire_loop_rt(...):
# ...

# The call order of user functions is preserved relative to the nested sections
exp.call("after_inner_user_func")

### Registering Callback Functions

As seen in the example above, callback functions 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 callback function. The registration is required for every callback function 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 the callback function 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_user_function(my_func)

# Variant 2: Give the name explicitly
session.register_user_function(my_power_func, "calc_power")

# Same python function may be registered multiple times with different names
session.register_user_function(my_power_func, "calc_power_alt")

session.register_user_function(inner_user_func, "inner_user_func")
session.register_user_function(after_inner_user_func, "after_inner_user_func")

### Retrieving Results from Callback Functions

Callback functions used inside LabOne Q can return values which are attached to the result object of the session 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 user functions upon execution are available per function, use function name as a key.
session.results.user_func_results["my_func"]
# Two calls per iteration to calc_power result in two adjacent entries in the results
session.results.user_func_results["calc_power"]

## Controlling Non-QCCS Devices

The use of callback functions 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 the 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.

 Toolkit functions cannot be called during an emulated run!
 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 callback function is the following:

def toolkit_awg_output_amplitude(session, amplitude: float):
"""
Change HDAWG AWG 0 output 0 amplitude.

Args:
session: LabOne Q session instance (forwarded by LabOne Q controller)
amplitude: Amplitude for the selected AWG output
"""
session.devices["device_hdawg"].awgs[0].outputs[0].amplitude(amplitude)

Now the function toolkit_awg_output_amplitude can be utilized like any other callback function during an experiment.