# Workflow

When using LabOne Q, the workflow typically consist of six high level steps:

In each of these steps the data of your experiment can be fully serialized (saved) and deserialized (loaded) for storage, e.g. in a database, or for retrieval to repeat any experiment, because all references are kept persistent. In a standard use case, the instruments only need to be set up once, and then you would connect to a session on a daily basis, define and execute your experiments and access their results constantly, and update the calibration once you are happy with a result.

## Import Packages

Please make sure that you have followed the installation instructions. You can import all required packages of LabOne Q with a simple import statement. If you are working with Jupyter, you might want to activate the IPCompleter to enhance your experience with autocomplete.

%config IPCompleter.greedy=True

from laboneq.simple import *

## Setting up Instruments

In a next step, you will use a descriptor to specify how your instruments are connected to each other and to the qubits. The details of how to compose it are given in this chapter. Then, you will calibrate the devices and signal lines, e.g. with oscillator frequencies or mixer calibration matrices. This leaves you with a DeviceSetup ready to be handed to a session and run your experiments on.

### The Descriptor

The descriptor defines which instruments are used in a setup, and how they are connected to each other. Together with a Zurich Instruments data server connected to your instruments, you can define the device_setup:

device_setup = DeviceSetup.from_descriptor(
descriptor,
server_host="111.22.33.44",
server_port="8004",
setup_name="ZI_QCCS",
)
 The descriptor lists all used devices, their serial numbers, and a user-defined unique id. Furthermore, it contains the mapping between logical signal lines and physical instrument ports.

### Logical Signal Lines

LabOne Q abstracts instruments, physical channels, and hardware settings into logical signal lines which can then be combined into groups which resemble a qubit. You can read about the details here.

### Calibrating Devices and Signal Lines

Many parameters that have to be calibrated are related to the quantum hardware you perform your experiments on. It is convenient to set up a dictionary that holds these parameters.

After these qubit parameters have been defined, they can be fed to a user function create_calibration that returns a Calibration object, which, in turn, can be applied to the device_setup defined above.

# an example of some qubit paramters
qubit_parameters = {
'ro_freq_q0' :  50e6,       # readout frequency of qubit 0 in [Hz] - relative to local oscillator for readout drive upconversion
'ro_amp' : 1.0,             # readout amplitude
'ro_len' : 2.1e-6,          # readout pulse length in [s]
'qb0_freq': 100e6,          # qubit 0 drive frequency in [Hz] - relative to local oscillator for qubit drive upconversion
}

# local oscillator settings - to convert between IF and RF frequencies
lo_settings = {
'qb_lo': 5.3e9,               # qubit LO frequency in [Hz]
'ro_lo': 6.4e9                # readout LO frequency in [Hz]
}

We then define a user function that accepts the parameters and returns a Calibration object:

def define_calibration(parameters):
my_calibration = Calibration()

my_calibration["/logical_signal_groups/q0/drive_line"] = \
SignalCalibration(
oscillator=Oscillator(
uid = "q0_drive_osc",
frequency=parameters['qb0_freq'],
modulation_type=ModulationType.HARDWARE
),
mixer_calibration=MixerCalibration(
voltage_offsets=[0.0, 0.0],
correction_matrix = [
[1.0, 0.0],
[0.0, 1.0],
]
)
)
# common oscillator for readout pulse and signal acquisition
my_calibration["/logical_signal_groups/q0/measure_line"] = \
SignalCalibration(
oscillator=Oscillator(
uid = "q0_measure_osc",
frequency=parameters['ro_freq_q0'],
modulation_type=ModulationType.SOFTWARE
),
mixer_calibration=MixerCalibration(
voltage_offsets=[0.0, 0.0],
correction_matrix = [
[1.0, 0.0],
[0.0, 1.0],
]
)
)
my_calibration["/logical_signal_groups/q0/acquire_line"] = \
SignalCalibration(
oscillator=Oscillator(
uid = "q0_acquire_osc",
frequency=parameters['ro_freq_q0'],
modulation_type=ModulationType.SOFTWARE
),
)
return my_calibration

Now this function can be used to set the calibration on our device setup:

# define Calibration object
my_calibration = define_calibration(parameters=qubit_parameters)
# apply calibration to device setup
device_setup.set_calibration(my_calibration)

In a typical workflow, you might begin with a baseline calibration for your instruments, along with some guesses of your qubit parameters. After acquiring data, you may want to update your calibration and use an experiment calibration kept separate from your baseline one.

## Connecting to a Session

After the setup has been defined, we can connect to a session. The session takes the information from your calibrate device setup, and then it handles the experiment execution. It also gives you access to the results.

The emulate setting controls whether the session uses the instruments defined above (emulate=False), or runs on an emulated environment (emulate=True, in which case the results will also be emulated).

emulate = False

my_session = Session(device_setup)
my_session.connect(do_emulation=emulate)

## Defining an Experiment

We’ll use a resonator spectroscopy experiment to demonstrate the typical LabOne Q workflow. The Experiment chapter gives a brief overview of the complete experimental functionality. Additionally, it is recommended to look through the experiments to characterize single qubits, given in the chapter Single Qubit Calibration.

For our resonator spectroscopy experiment, we first need to define some additional experimental parameters, such as the frequency range of the spectroscopy scan, how many sweep steps we want to use, and how often we want to average the result. Furthermore, we use the pulse_library supplied with LabOne Q to define a square pulse.

# frequency range of spectroscopy scan - around centre frequency
spec_range = 100e6
# how many frequency points to measure
spec_num = 101
# how many averages per point: 2^n_average
n_average = 12

# spectroscopy excitation pulse
)

After that, we define the experiment, "connect" the lines, define a LinearSweepParameter, and assign it to the oscillator frequency.

The experiment itself uses a near-time sweep that holds near-time acquisition loops. In real-time, readout pulses are played, and the UHFQA acquisition is triggered in SPECTROSCOPY mode. It is best practice to grant a 1 μs hold-off time between consecutive shots to give the UHFQA enough time for data processing.

# Create resonator spectroscopy experiment - uses only readout drive and signal acquisition
exp_spec = Experiment(
uid = "Resonator Spectroscopy",
signals=[
ExperimentSignal("q0_measure"),
ExperimentSignal("q0_acquire"),
]
)
# Connect experiment signals to logical signals
exp_spec.map_signal(
"q0_measure",
lsg["measure_line"]
)
exp_spec.map_signal(
"q0_acquire",
lsg["acquire_line"]
)

# define sweep parameters
freq_sweep = LinearSweepParameter(
uid = "res_freq",
start = qubit_parameters['ro_freq_q0'] - spec_range / 2,
stop=qubit_parameters['ro_freq_q0'] + spec_range / 2,
count=spec_num
)

# change setup configuration to use hardware oscillator for spectroscopy measurement and sweep its frequency
lsg["measure_line"].oscillator.frequency = freq_sweep
lsg["measure_line"].oscillator.modulation_type = ModulationType.HARDWARE

## define experimental sequence
# outer loop - vary drive frequency
with exp_spec.sweep("res_freq", parameter=freq_sweep):
# inner loop - average for each frequency
with exp_spec.acquire_loop_rt(
uid="shots",
count=pow(2, n_average),
averaging_mode=AveragingMode.SEQUENTIAL,
acquisition_type=AcquisitionType.SPECTROSCOPY
):
# readout pulse and data acquisition
with exp_spec.section(uid = "spectroscopy"):
# resonator excitation pulse
exp_spec.play(
signal="q0_measure",
)
# resonator signal readout - UHFQA in spectroscopy mode
exp_spec.acquire(signal="q0_acquire", handle="res_spec")
# holdoff time after signal acquisition - min 1us required for data processing
exp_spec.delay(signal="q0_measure", time=1e-6)

## Executing an Experiment

To execute the experiment we hand it over to our session, where it is compiled and run. Compilation can be done without running the experiment with (session.compile(exp)) and then it can be run later.

my_results = my_session.run(exp)

## Accessing the Data

The data of the experiment can be retrieved from the session, where the unique id defined in the acquire command is used as a handle. Read about how to access and use results here.

spec_res = my_results.get_data('res_spec')

## Changing the Calibration

You can use your results to update the calibration and re-apply it to your setup:

my_calibration = define_calibration(parameters=new_qubit_parameters)
device_setup.set_calibration(my_calibration)