Workflow¶
When using LabOne Q, the workflow typically consist of six high level steps:
- Setting up the instruments
- Connecting to a session
- Defining an experiment
- Executing an experiment
- Accessing the results
- Changing the calibration
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",
)
Note
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 = {
## freqs are relative to local oscillator for qubit drive upconversion
"ro_freq_q0": 50e6, ## readout frequency of qubit 0 in [Hz]
"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]
}
## 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
readout_pulse_spec = pulse_library.const(
uid="readout_pulse_spec",
length=qubit_parameters["ro_len_spec"],
amplitude=qubit_parameters["ro_amp_spec"],
)
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", pulse=readout_pulse_spec)
## resonator signal readout - UHFQA in spectroscopy mode
exp_spec.acquire(signal="q0_acquire", handle="res_spec")
## holdoff time after signal acquisition
## a minimum of 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_spec)
) and then it can be run later.
my_results = my_session.run(exp_spec)
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)