Qubits and Quantum Operations
Qubits and Quantum Operations¶
Quantum Operations are collections of Sections and pulses implementing common operations on qubits. To learn more about how quantum operations work in LabOne Q, check out this page in our manual.
Each set of quantum operations defines operations for a particular type of qubit, with the possibility to extend the set to accept more qbuit types. At the moment the library only provides TunableTransmonOperations for TunableTransmonQubits.
In this tutorial, we'll introduce you to these qubits and their corresponding operations and explain how the operations use the information from the qubit parameters to create Sections of pulses. To learn how to write your own quantum operations, check this tutorial.
Let's get started.
QPU and Device Setup¶
We generate a pre-configured, demo QuantumPlatform
containing three tunable-transmon qubits with pre-defined parameters, and a Device_Setup
consisting of a SHFQC+, HDAWG, and PQSC.
This quantum platform is meant to be used for tests in emulation mode. To learn how to get started with a real setup, check out the Getting Started tutorial. This tutorial also provides more details about the demo QuantumPlatform
we are using here.
import numpy as np
from laboneq.simple import *
from laboneq_applications.qpu_types.tunable_transmon import demo_platform
# Create a demonstration QuantumPlatform for a tunable-transmon QPU:
qt_platform = demo_platform(n_qubits=6)
# The platform contains a setup, which is an ordinary LabOne Q DeviceSetup:
setup = qt_platform.setup
# And a tunable-transmon QPU:
qpu = qt_platform.qpu
# Inside the QPU, we have qubits, which is a list of six LabOne Q Application
# Library TunableTransmonQubit qubits:
qubits = qpu.qubits
session = Session(setup)
session.connect(do_emulation=True)
[2024.12.19 17:11:47.881] INFO Logging initialized from [Default inline config in laboneq.laboneq_logging] logdir is /builds/qccs/laboneq-applications/docs/sources/tutorials/sources/laboneq_output/log
[2024.12.19 17:11:47.883] INFO VERSION: laboneq 2.43.0
[2024.12.19 17:11:47.885] INFO Connecting to data server at localhost:8004
[2024.12.19 17:11:47.887] INFO Connected to Zurich Instruments LabOne Data Server version 24.10 at localhost:8004
[2024.12.19 17:11:47.889] INFO Configuring the device setup
[2024.12.19 17:11:47.925] INFO The device setup is configured
<laboneq.dsl.session.ConnectionState at 0x7cf1e37b6930>
TunableTransmonQubitParameters¶
Let's start by inspecting the parameters of the qubits. These are used by the TunableTransmonOperations
as we will explain in the next section.
We use the first qubit in the list, but you can do the same for any of the other qubits.
qubits[0].parameters
TunableTransmonQubitParameters( │ resonance_frequency_ge=6500000000.0, │ resonance_frequency_ef=6300000000.0, │ drive_lo_frequency=6400000000, │ readout_resonator_frequency=7100000000.0, │ readout_lo_frequency=7000000000.0, │ readout_integration_delay=2e-08, │ drive_range=10, │ readout_range_out=5, │ readout_range_in=10, │ flux_offset_voltage=0, │ user_defined={}, │ ge_T1=0, │ ge_T2=0, │ ge_T2_star=0, │ ef_T1=0, │ ef_T2=0, │ ef_T2_star=0, │ ge_drive_amplitude_pi=0.8, │ ge_drive_amplitude_pi2=0.4, │ ge_drive_length=5.1e-08, │ ge_drive_pulse={ │ │ 'function': 'drag', │ │ 'beta': 0.01, │ │ 'sigma': 0.21 │ }, │ ef_drive_amplitude_pi=0.7, │ ef_drive_amplitude_pi2=0.3, │ ef_drive_length=5.2e-08, │ ef_drive_pulse={ │ │ 'function': 'drag', │ │ 'beta': 0.01, │ │ 'sigma': 0.21 │ }, │ readout_amplitude=1.0, │ readout_length=2e-06, │ readout_pulse={ │ │ 'function': 'const' │ }, │ readout_integration_length=2e-06, │ readout_integration_kernels_type='default', │ readout_integration_kernels=None, │ readout_integration_discrimination_thresholds=None, │ reset_delay_length=1e-06, │ spectroscopy_length=5e-06, │ spectroscopy_amplitude=1, │ spectroscopy_pulse={ │ │ 'function': 'const', │ │ 'can_compress': True │ }, │ dc_slot=0, │ dc_voltage_parking=0.0 )
Let's break down this list of parameters and understand their utility:
Parameters with the prefixes
ge_drive_
/ef_drive_
are used to configure the parameters of a pi-pulse on the ge and ef transitions.Parameters with the prefixe
spectroscopy_
are used to configure the parameters of a spectroscopy pulse played in a pulsed qubit spectroscopy experiment.Parameters with the prefix
readout_
are used to configure the parameters of the readout pulse.Parameters with the prefix
readout_integration_
are used to configure the parameters of the integration kernels. Setting the parameterreadout_integration_kernels=default
indicates that a constant square pulse with the length given byreadout_integration_length
will be used for the integration (created inqubit.default_integration_kernels()
). The parameterreadout_integration_kernels
can also be set to a list of pulse dictionaries of the form{"function": pulse_functional_name, "func_par1": value, "func_par2": value, ... }
.pulse_functional_name
must be the name of a function registered with thepulse_library.register_pulse_functional
decorator.reset_delay_length
: the waiting time for passive qubit reset.resonance_frequency_ge
,resonance_frequency_ef
'drive_lo_frequency
,readout_resonator_frequency
,readout_lo_frequency
,drive_range
,readout_range_out
,readout_range_in
are used to configure the qubit calibration which then ends up in theExperiment
calibration.
Note the parameters ge_drive_pulse
, ef_drive_pulse
, spectroscopy_pulse
, readout_pulse
. These parameters store a dictionary. They are used to specify the pulse shape (under the key "function"
) and any other input parameters that are needed by this pulse function. The value of "function"
must be a string corresponding to the name of a pulse functional in the LabOne Q pulse library module or a pulse functional you have defined in your kernel (as shown here).
Generic methods for getting qubit parameters¶
To make sure each of these operations is applied correctly on any TunableTransmonQubits
, the qubit parameters must be available to the implementation of the quantum operation. These parameters are accessed via a few generic qubit-class methods:
.transition_parameters(transition)
: returns the drive logical signal line of the qubit and the pulse parameters that allow exciting a given qubittransition
(currently, only "ge" or "ef"). The qubit pulse parameters are the ones previxed withge_drive_
/ef_drive_
..readout_parameters()
: returns the measure logical signal line of the qubit and the readout-pulse parameters (qubit parameter with the prefixreadout_
)..readout_integration_parameters()
: returns the acquire logical signal line of the qubit and the integration-kernel parameters (qubit parameter with the prefixreadout_integration_
)..spectroscopy_parameters()
: returns the qubit-spectroscpoy logical signal line and the qubit spectroscopy parameters (qubit parameter with the prefixspectroscopy_
).
Note: If a different qubit class has these four methods, then the TunableTransmonOperations
can be extended to accept this qubit class. This can be done by adding the qubit class to the QUBIT_TYPES
attribute of the TunableTransmonOperations
(needs modifying the source code).
The role of these methods is to abstract away the exact name of these pulse parameters defined in the qubit. Let's see what this means for each of the methods.
transition_parameters("ge")¶
drive_line_ge, parameters_ge = qubits[0].transition_parameters("ge")
drive_line_ge, parameters_ge
('drive', {'amplitude_pi': 0.8, 'amplitude_pi2': 0.4, 'length': 5.1e-08, 'pulse': {'function': 'drag', 'beta': 0.01, 'sigma': 0.21}})
The values in the dictionary parameters_ge
come from the qubit parameters:
{"amplitude_pi": qubits[0].parameters.ge_drive_amplitude_pi,
"amplitude_pi2": qubits[0].parameters.ge_drive_amplitude_pi2,
"length": qubits[0].parameters.ge_drive_length,
"pulse": qubits[0].parameters.ge_drive_pulse}
{'amplitude_pi': 0.8, 'amplitude_pi2': 0.4, 'length': 5.1e-08, 'pulse': {'function': 'drag', 'beta': 0.01, 'sigma': 0.21}}
transition_parameters("ef")¶
drive_line_ef, parameters_ef = qubits[0].transition_parameters("ef")
drive_line_ef, parameters_ef
('drive_ef', {'amplitude_pi': 0.7, 'amplitude_pi2': 0.3, 'length': 5.2e-08, 'pulse': {'function': 'drag', 'beta': 0.01, 'sigma': 0.21}})
The values in the dictionary parameters_ef
come from the qubit parameters:
{"amplitude_pi": qubits[0].parameters.ef_drive_amplitude_pi,
"amplitude_pi2": qubits[0].parameters.ef_drive_amplitude_pi2,
"length": qubits[0].parameters.ef_drive_length,
"pulse": qubits[0].parameters.ef_drive_pulse}
{'amplitude_pi': 0.7, 'amplitude_pi2': 0.3, 'length': 5.2e-08, 'pulse': {'function': 'drag', 'beta': 0.01, 'sigma': 0.21}}
readout_parameters()¶
measure_line, readout_parameters = qubits[0].readout_parameters()
measure_line, readout_parameters
('measure', {'amplitude': 1.0, 'length': 2e-06, 'pulse': {'function': 'const'}})
The values in the dictionary readout_parameters
come from the qubit parameters:
{"amplitude": qubits[0].parameters.readout_amplitude,
"length": qubits[0].parameters.readout_length,
"pulse": qubits[0].parameters.readout_pulse}
{'amplitude': 1.0, 'length': 2e-06, 'pulse': {'function': 'const'}}
readout_integration_parameters()¶
acquire_line, readout_integration_parameters = qubits[0].readout_integration_parameters()
acquire_line, readout_integration_parameters
('acquire', {'length': 2e-06, 'kernels': None, 'kernels_type': 'default', 'discrimination_thresholds': None})
The values in the dictionary readout_integration_parameters
come from the qubit parameters:
{"length": qubits[0].parameters.readout_integration_length,
"kernels": qubits[0].parameters.readout_integration_kernels,
"kernels_type": qubits[0].parameters.readout_integration_kernels_type,
"discrimination_thresholds": qubits[0].parameters.readout_integration_discrimination_thresholds}
{'length': 2e-06, 'kernels': None, 'kernels_type': 'default', 'discrimination_thresholds': None}
In the next section, we explain how the TunableTransmonOperations
use these four qubit methods to implement quantum gates.
TunableTransmonOperations¶
The demo platform we have instantiated above, created a QPU
containing six TunableTransmonQubits
and the corresponding set of TunableTransmonOperations
.
Let's examine this set of operations. To learn more about how to work with a set of quantum operations, check out this other tutorial.
qops = qpu.quantum_operations
qops.keys()
['acquire', 'active_reset', 'barrier', 'calibration_traces', 'delay', 'measure', 'passive_reset', 'prepare_state', 'qubit_spectroscopy_drive', 'ramsey', 'rx', 'ry', 'rz', 'set_frequency', 'set_readout_amplitude', 'x180', 'x180_ef_reset', 'x90', 'y180', 'y90', 'z180', 'z90']
Some basic operations¶
The TunableTransmonOperations
implements some basic operations like:
barrier(qubit)
: this operation reserves all the logical signal lines of the qubit passed to it using reserve commands. It is used to ensure that operations acting on the same qubit so not overlap.delay(qubit, delay_time)
: this operation simply adds a delay command on the drive logical signal line of the qubit. By automatically reserving all the other lines of the qubit, the this operation effectively delays any other operations acting on this qubit by thedelay_time
passed to the operation.prepare_state(qubit, state)
: this operation prepares any of the states "g", "e", or "f" of theTunableTransmonQubit
assuming the qubit is in the ground state before this operation is applied. To prepare "g", no pulse is applied. To prepare "e", the operation adds a $\pi$-pulse on the "ge" transition. To prepare the "f" state, the operation adds a $\pi$-pulse on the "ge" transition followed by a $\pi$-pulse on the "ef" transition. These $\pi$-pulse are other quantum operations from the set, which we explain in the next section.
Single-qubit gate operations¶
The operations rx
, ry
, rz
implement rotations around one of the axes of the Block sphere by an angle specified by the user.
We also have common single-qubit gates implementing rotations of 180 degrees and 90 degrees around the x, y, and z axes of the Block sphere: x180
, x90
, y180
, y90
, z180
, z90
.
Let's inspect the source code of the rx
operation to see how it is implemented and how it makes use of the qubit parameters:
qops.rx.src
@dsl.quantum_operation
def rx(
self,
q: TunableTransmonQubit,
angle: float | SweepParameter | None,
transition: str | None = None,
amplitude: float | SweepParameter | None = None,
phase: float = 0.0,
length: float | SweepParameter | None = None,
pulse: dict | None = None,
) -> None:
"""Rotate the qubit by the given angle (in radians) about the X axis.
Arguments:
q:
The qubit to rotate.
angle:
The angle to rotate by in radians.
transition:
The transition to rotate. By default this is "ge"
(i.e. the 0-1 transition).
amplitude:
The amplitude of the rotation pulse. By default this
is determined by the angle and the π pulse amplitude
qubit parameter "amplitude_pi" by linear interpolation.
phase:
The phase of the rotation pulse in radians. By default
this is 0.0.
length:
The duration of the rotation pulse. By default this
is determined by the qubit parameters.
pulse:
A dictionary of overrides for the qubit pulse parameters.
The dictionary may contain sweep parameters for the pulse
parameters other than `function`.
If the `function` parameter is different to the one
specified for the qubit, then this override dictionary
completely replaces the existing pulse parameters.
Otherwise the values override or extend the existing ones.
"""
drive_line, params = q.transition_parameters(transition)
if transition == "ef":
section = dsl.active_section()
section.on_system_grid = True
if amplitude is None:
# always do a linear scaling with respect to the pi pulse amplitude from the
# qubit
amplitude = (angle / self._PI) * params["amplitude_pi"]
if length is None:
length = params["length"]
rx_pulse = dsl.create_pulse(params["pulse"], pulse, name="rx_pulse")
dsl.play(
q.signals[drive_line],
amplitude=amplitude,
phase=phase,
length=length,
pulse=rx_pulse,
)
You can specify the transition on which to perform the rx
operation, which is forwarded to the transition_parameters(transition)
method to obtain the qubit logical signal line and pulse parameters corresponding to this transition. See the previous section for more details.
When the transition is "ef", the rx
operation makes sure that the pulses are aligned to the system grid (on_system_grid=True
). The "ge" and "ef" pulses are modulated at different frequencies but they are played back from the same physical output of the SG instrument. An change in the oscillator frequency must happen whenever there is an "ef" pulse, and this change can only happen if the pulses are aligned to the system grid.
Next in the source code, we see that, by default, the pulse parameters come from the qubit: the amplitude_pi
, the length
, and the pulse
dictionary containing information about the pulse type and nay other parameters that are special to this pulse type (see the end of the previous section for more explanation on the pulse
dictionary).
This is how the quantum operations implement the correct gates for each qubits.
Finally, we see the single play command created by this quantum operation, on the drive line of the qubit.
As explained in the tutorial on quantum operations, each operation wraps the implemented pulse commands into a Section. In addition, it adds ` all the logical signal lines of the qubit so that two operations on the same qubit cannot overlap. See how to omit these reserve commands in the tutorial on quantum operations.
Let's call the rx
operation and check that the Section
it creates is what we expect:
qops.rx(qubits[0], angle=np.pi)
Section( │ uid='__rx_q0_0', │ name='rx_q0', │ alignment=SectionAlignment.LEFT, │ execution_type=None, │ length=None, │ play_after=None, │ children=[ │ │ Reserve( │ │ │ signal='/logical_signal_groups/q0/drive' │ │ ), │ │ Reserve( │ │ │ signal='/logical_signal_groups/q0/drive_ef' │ │ ), │ │ Reserve( │ │ │ signal='/logical_signal_groups/q0/measure' │ │ ), │ │ Reserve( │ │ │ signal='/logical_signal_groups/q0/acquire' │ │ ), │ │ Reserve( │ │ │ signal='/logical_signal_groups/q0/flux' │ │ ), │ │ PlayPulse( │ │ │ signal='/logical_signal_groups/q0/drive', │ │ │ pulse=PulseFunctional( │ │ │ │ function='drag', │ │ │ │ uid='__rx_pulse_0', │ │ │ │ amplitude=1.0, │ │ │ │ length=1e-07, │ │ │ │ can_compress=False, │ │ │ │ pulse_parameters={ │ │ │ │ │ 'beta': 0.01, │ │ │ │ │ 'sigma': 0.21 │ │ │ │ } │ │ │ ), │ │ │ amplitude=0.8, │ │ │ increment_oscillator_phase=None, │ │ │ phase=0.0, │ │ │ set_oscillator_phase=None, │ │ │ length=5.1e-08, │ │ │ pulse_parameters=None, │ │ │ precompensation_clear=None, │ │ │ marker=None │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
The rx
, ry
, rz
operations implement rotations of a given angle. So these operations require the input parameter angle
, which is converted into a pulse amplitude using a linear scaling with respect to the $\pi$-pulse amplitude of the qubit.
The remaining single qubit gates (x180
, x90
, y180
, y90
, z180
, z90
) are implemented by calling the rx
, ry
, rz
operations with the correct angle. Let's look at the source code of the x180
operation to see this:
qops.x180.src
@dsl.quantum_operation
def x180(
self,
q: TunableTransmonQubit,
transition: str | None = None,
amplitude: float | None = None,
phase: float = 0.0,
length: float | None = None,
pulse: dict | None = None,
) -> None:
"""Rotate the qubit by 180 degrees about the X axis.
This implementation calls `rx(q, π, ...)`.
Arguments:
q:
The qubit to rotate.
transition:
The transition to rotate. By default this is "ge"
(i.e. the 0-1 transition).
amplitude:
The amplitude of the rotation pulse. By default this
is determined from the qubit parameter "amplitude_pi".
phase:
The phase of the rotation pulse in radians. By default
this is 0.0.
length:
The duration of the rotation pulse. By default this
is determined by the qubit parameters.
pulse:
A dictionary of overrides for the qubit pulse parameters.
The dictionary may contain sweep parameters for the pulse
parameters other than `function`.
If the `function` parameter is different to the one
specified for the qubit, then this override dictionary
completely replaces the existing pulse parameters.
Otherwise the values override or extend the existing ones.
"""
if amplitude is None:
_, params = q.transition_parameters(transition)
amplitude = params["amplitude_pi"]
self.rx.omit_section(
q,
self._PI,
transition=transition,
amplitude=amplitude,
phase=phase,
length=length,
pulse=pulse,
)
Hence, when calling any of these single-qubit gates, only the qubit needs to be specified:
section = qops.x180(qubits[0])
section.name == "x180_q0"
True
As mentioned above, the pulse parameters come from the qubit parameters by default. But you have the possibility to override any of these pulse parameters by passing them in when calling these single-qubit operations.
Let's create an x180
-operation section where we override the amplitude
:
section = qops.x180(qubits[0], amplitude=1)
section.children[-1].amplitude
1
Check that this is different to the amplitude of a default x180
operation:
section = qops.x180(qubits[0])
section.children[-1].amplitude
0.8
This value comes from the $\pi$-pulse amplitude of the qubit:
qubits[0].parameters.ge_drive_amplitude_pi
0.8
In addition, you can sweep any of these pulse parameters in an experiment by passing a SweepParameter:
section = qops.x180(qubits[0], amplitude=SweepParameter("amp_sweep", np.linspace(0, 1, 5)))
section.children[-1].amplitude
SweepParameter( │ uid='amp_sweep', │ values=array([0. , 0.25, 0.5 , 0.75, 1. ]), │ axis_name=None, │ driven_by=None )
Readout operations¶
Let's have another look at the set of TunableTransmonOperations
:
qops.keys()
['acquire', 'active_reset', 'barrier', 'calibration_traces', 'delay', 'measure', 'passive_reset', 'prepare_state', 'qubit_spectroscopy_drive', 'ramsey', 'rx', 'ry', 'rz', 'set_frequency', 'set_readout_amplitude', 'x180', 'x180_ef_reset', 'x90', 'y180', 'y90', 'z180', 'z90']
The operations measure
and acquire
are used for qubit readout.
The measure
operation creates a section that plays a readout pulse and acquires the result under the handle name passed to the operation. Below, we call this operation with the options .omit_reserves
for better readability.
qops.measure.omit_reserves(qubits[0], handle=dsl.handles.result_handle(qubits[0].uid))
Section( │ uid='__measure_q0_0', │ name='measure_q0', │ alignment=SectionAlignment.LEFT, │ execution_type=None, │ length=None, │ play_after=None, │ children=[ │ │ PlayPulse( │ │ │ signal='/logical_signal_groups/q0/measure', │ │ │ pulse=PulseFunctional( │ │ │ │ function='const', │ │ │ │ uid='__readout_pulse_0', │ │ │ │ amplitude=1.0, │ │ │ │ length=1e-07, │ │ │ │ can_compress=False, │ │ │ │ pulse_parameters=None │ │ │ ), │ │ │ amplitude=1.0, │ │ │ increment_oscillator_phase=None, │ │ │ phase=None, │ │ │ set_oscillator_phase=None, │ │ │ length=2e-06, │ │ │ pulse_parameters=None, │ │ │ precompensation_clear=None, │ │ │ marker=None │ │ ), │ │ Acquire( │ │ │ signal='/logical_signal_groups/q0/acquire', │ │ │ handle='q0/result', │ │ │ kernel=[ │ │ │ │ PulseFunctional( │ │ │ │ │ function='const', │ │ │ │ │ uid='__integration_kernel_q0_0', │ │ │ │ │ amplitude=1.0, │ │ │ │ │ length=2e-06, │ │ │ │ │ can_compress=False, │ │ │ │ │ pulse_parameters=None │ │ │ │ ) │ │ │ ], │ │ │ length=2e-06, │ │ │ pulse_parameters=None │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
The acquire
operation simply performs an acquisition without playing a readout pulse. This is useful for a continuous-wave resonator spectrsocopy, for example.
qops.acquire.omit_reserves(qubits[0], handle=dsl.handles.result_handle(qubits[0].uid))
Section( │ uid='__acquire_q0_0', │ name='acquire_q0', │ alignment=SectionAlignment.LEFT, │ execution_type=None, │ length=None, │ play_after=None, │ children=[ │ │ Acquire( │ │ │ signal='/logical_signal_groups/q0/acquire', │ │ │ handle='q0/result', │ │ │ kernel=[ │ │ │ │ PulseFunctional( │ │ │ │ │ function='const', │ │ │ │ │ uid='__integration_kernel_q0_0', │ │ │ │ │ amplitude=1.0, │ │ │ │ │ length=2e-06, │ │ │ │ │ can_compress=False, │ │ │ │ │ pulse_parameters=None │ │ │ │ ) │ │ │ ], │ │ │ length=2e-06, │ │ │ pulse_parameters=None │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
The parameters of the readout pulse as well as the name of the measure logical signal line are taken from the qubit by calling the method .readout_parameters
, as explained above).
Similarly, the parameters of the integration kernels and the name of the acquire line are taken from the qubit by calling the method .readout_integration_parameters
, as explained above).
Let's inspect the source code of measure
to see this:
qops.measure.src
@dsl.quantum_operation
def measure(
self,
q: TunableTransmonQubit,
handle: str,
readout_pulse: dict | None = None,
kernel_pulses: list[dict] | Literal["default"] | None = None,
) -> None:
"""Perform a measurement on the qubit.
The measurement operation plays a readout pulse and performs an acquisition.
If you wish to perform only an acquisition, use the `acquire` operation.
Arguments:
q:
The qubit to measure.
handle:
The handle to store the acquisition results in.
readout_pulse:
A dictionary of overrides for the readout pulse parameters.
The dictionary may contain sweep parameters for the pulse
parameters other than `function`.
If the `function` parameter is different to the one
specified for the qubit, then this override dictionary
completely replaces the existing pulse parameters.
Otherwise the values override or extend the existing ones.
kernel_pulses:
A list of dictionaries describing the pulse parameters for
the integration kernels, or "default" or `None`.
If a list of dictionaries is past, each dictionary must
completely specify a kernel pulse and its parameters (i.e.
they must include the `function` parameter and all of its
arguments).
If the string "default" is passed, a constant integration
kernel of length equal to the qubit's
`readout_integration_length` parameter is used.
If not specified or `None`, the kernels specified in the
qubit's `readout_integration_kernels` are used.
"""
measure_line, ro_params = q.readout_parameters()
acquire_line, ro_int_params = q.readout_integration_parameters()
ro_pulse = dsl.create_pulse(
ro_params["pulse"], readout_pulse, name="readout_pulse"
)
kernels = q.get_integration_kernels(kernel_pulses)
dsl.measure(
measure_signal=q.signals[measure_line],
measure_pulse_amplitude=ro_params["amplitude"],
measure_pulse_length=ro_params["length"],
measure_pulse=ro_pulse,
handle=handle,
acquire_signal=q.signals[acquire_line],
integration_kernel=kernels,
integration_length=ro_int_params["length"],
reset_delay=None,
)
Note that only the length
parameter of the readout integration kernels is used inside the operations. The pulse functionalse of the integration kernels are obtained from the qubit method .get_integration_kernels()
. This method returns the default square-shaped integration kernels if the qubit parameter readout_integration_kernels_type == "default"
, or, if readout_integration_kernels_type == "optimal"
, it returns the optimal kernels stored under the qubit parameter readout_integration_kernels
.
qubits[0].get_integration_kernels() # default kernels
[PulseFunctional( │ function='const', │ uid='__integration_kernel_q0_0', │ amplitude=1.0, │ length=2e-06, │ can_compress=False, │ pulse_parameters=None ) ]
As was the case for the single-qubit gate operations, both measure
and acquire
allow the possibility to override the readout-pulse and integration-kernels parameters by passing the corresponding pulse
dictionaries.
Qubit-reset operations¶
TunableTransmonOperations
contains two operations for resetting the qubit to the ground state: passive_reset
and active_reset
. The latter uses the special operation x180_ef_reset
.
The passive_reset
operation simply applies a delay operation with the delay time taken from the qubit parameter reset_delay_length
. Thus, the qubit controls its own passive reset delay. Ideally, this should be around $3T_1$.
The active_reset
operation implements real-time-feedback-based reset of the qubit state back to the ground state. The qubit is measured, the result is classified into one of the qubit states, and a feedback pulse is applied to reset the qubit to its ground state. Multi-state discrimination must be tuned-up before using this operation. We explain this tune-up procedure and the active reset protocol in great detail in the active reset tune-up guide. The x180_ef_reset
operation is also explained in this tune-up guide.
Qubit-spectroscopy operation¶
The qubit_spectroscopy_drive
operation is meant to be used in pulsed qubit spectroscopy measurements. The operation contains a single play
command on the spectrsocopy logical signal line of the qubit. For a TunableTransmonQubit
, this is the same as the drive line.
Note: this operation does not change the amplitude in the experiment calibration. This is done by the set_readout_amplitude
operation, which we discuss in the section Operations that access experiment calibration.
Let's look at the source code of the qubit_spectroscopy_drive
operation:
qops.qubit_spectroscopy_drive.omit_reserves(qubits[0])
Section( │ uid='__qubit_spectroscopy_drive_q0_0', │ name='qubit_spectroscopy_drive_q0', │ alignment=SectionAlignment.LEFT, │ execution_type=None, │ length=None, │ play_after=None, │ children=[ │ │ PlayPulse( │ │ │ signal='/logical_signal_groups/q0/drive', │ │ │ pulse=PulseFunctional( │ │ │ │ function='const', │ │ │ │ uid='__qubit_spectroscopy_pulse_0', │ │ │ │ amplitude=1.0, │ │ │ │ length=1e-07, │ │ │ │ can_compress=True, │ │ │ │ pulse_parameters=None │ │ │ ), │ │ │ amplitude=1, │ │ │ increment_oscillator_phase=None, │ │ │ phase=0.0, │ │ │ set_oscillator_phase=None, │ │ │ length=5e-06, │ │ │ pulse_parameters=None, │ │ │ precompensation_clear=None, │ │ │ marker=None │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
Note that we have omitted the reserve commands here for better readability.
When inspecting the source code of this operation, we see that it uses the qubit method .spectroscopy_parameters()
to obtain the spectroscopy logical signal line and the spectroscopy parameters:
qops.qubit_spectroscopy_drive.src
@dsl.quantum_operation
def qubit_spectroscopy_drive(
self,
q: TunableTransmonQubit,
amplitude: float | SweepParameter | None = None,
phase: float = 0.0,
length: float | SweepParameter | None = None,
pulse: dict | None = None,
) -> None:
"""Long pulse used for qubit spectroscopy that emulates a coherent field.
Arguments:
q:
The qubit to apply the spectroscopy drive.
amplitude:
The amplitude of the pulse. By default, the
qubit parameter "spectroscopy_amplitude".
phase:
The phase of the pulse in radians. By default,
this is 0.0.
length:
The duration of the rotation pulse. By default, this
is determined by the qubit parameters.
pulse:
A dictionary of overrides for the qubit-spectroscopy pulse parameters.
The dictionary may contain sweep parameters for the pulse
parameters other than `function`.
If the `function` parameter is different to the one
specified for the qubit, then this override dictionary
completely replaces the existing pulse parameters.
Otherwise, the values override or extend the existing ones.
"""
spec_line, params = q.spectroscopy_parameters()
if amplitude is None:
amplitude = params["amplitude"]
if length is None:
length = params["length"]
spectroscopy_pulse = dsl.create_pulse(
params["pulse"], pulse, name="qubit_spectroscopy_pulse"
)
dsl.play(
q.signals[spec_line],
amplitude=amplitude,
phase=phase,
length=length,
pulse=spectroscopy_pulse,
)
As was the case for the single-qubit gate operations and the readout operations, the qubit_spectroscopy_operation
allows the possibility to override the spectroscopy-pulse parameters by passing the amplitude
, length
, phase
, and the pulse
dictionary.
Special operations¶
ramsey
, calibration_traces
These operations are special in that they implement a snippet of code using several other operations.
ramsey¶
The ramsey
operation implements the typical phase-measurement sequence of two x90
pulses separated by a delay
used by ramsey-like experiments. Optionally, an additional $\pi$-pulse (either x180
or y180
) can be added half-way through the delay time to turn this into a Hahn echo sequence.Let's inspect the source code to see this logic:
qops.ramsey.src
@dsl.quantum_operation(broadcast=False)
def ramsey(
self,
q: TunableTransmonQubit,
delay: float,
phase: float,
echo_pulse: Literal["x180", "y180"] | None = None,
transition: str | None = None,
) -> None:
"""Performs a Ramsey operation on a qubit.
This operation consists of the following steps:
x90 - delay/2 - [x180] or [y180] - delay/2 - x90
Arguments:
q:
The qubit to rotate
delay:
The duration between two rotations, excluding the
echo pulse length if an echo pulse is included.
phase:
The phase of the second rotation
echo_pulse:
The echo pulse to include.
transition:
The transition to rotate. By default this is "ge"
(i.e. the 0-1 transition).
Raise:
ValueError:
If the transition is not "ge" nor "ef".
ValueError:
If the echo pulse is not None and not x180 or y180.
"""
transition = "ge" if transition is None else transition
if transition == "ef":
on_system_grid = True
elif transition == "ge":
on_system_grid = False
else:
raise ValueError(f"Support only ge or ef transitions, not {transition!r}")
if echo_pulse is not None and echo_pulse not in ("x180", "y180"):
raise ValueError(
f"Support only x180 or y180 for echo pulse, not {echo_pulse}"
)
with dsl.section(
name=f"ramsey_{q.uid}",
on_system_grid=on_system_grid,
alignment=SectionAlignment.RIGHT,
):
sec_x90_1 = self.x90(q, transition=transition)
sec_x90_1.alignment = SectionAlignment.RIGHT
if echo_pulse is not None:
self.delay(q, time=delay / 2)
sec_echo = self[echo_pulse](q, transition=transition)
sec_echo.alignment = SectionAlignment.RIGHT
self.delay(q, time=delay / 2)
else:
self.delay(q, time=delay)
sec_x90_2 = self.x90(q, phase=phase, transition=transition)
sec_x90_2.alignment = SectionAlignment.RIGHT
# to remove the gap due to oscillator switching for driving ef transitions.
if echo_pulse is not None:
sec_echo.on_system_grid = False
sec_x90_1.on_system_grid = False
sec_x90_2.on_system_grid = False
This operation can be performed on either the "ge" or "ef" transition. It expects a delay
and a phase
of the second x90
, both of which can be specified as either numerical values or SweepParameters
. You can also optionally specify an echo pulse.
The source code of this operation looks a bit complicated. This is because we want to ensure that there are no unwanted gaps due to the switching of the oscillator when applying this operation on the "ef" transition. The time between the two x90
pulses needs to be precisely known because the success of the Ramsey/Echo calibration or any other phase measurement relies on knowing the phase that is accumulated during this time. See our Ramsey and Echo how-to guides for more details about these calibration measurements.
Note: This operation should only be used with one qubit. Notice that the automatic broadcasting feature is disabled for this operation (broadcast=False
). Because the ramsey
operation uses several other quantum operations, the automatic broadcasting feature would result in wrong timing when the operation is applied on multiple qubits in parallel following the logic of the broadcasting feature:
for q in qubits:
qops.ramsey(q, ...)
calibration_traces¶
The second special operation is the calibration_traces
. This operation is added at the end of a tune-up experiment to measure qubit calibration states, i.e. points in the IQ plane of the acquired signal where we know what state the qubit is in because we've prepared it to be in that state. This reference points are then used to interpret the rest of the data into qubit population.
Hence, the calibration_traces
operation prepares a set of qubit states and measures them. Let's see this in the source code:
qops.calibration_traces.src
@dsl.quantum_operation(broadcast=False)
def calibration_traces(
self,
qubits: Qubits,
states: str | tuple = "ge",
active_reset: bool = False, # noqa: FBT001, FBT002
active_reset_states: str | tuple = "ge",
active_reset_repetitions: int = 1,
feedback_processing_delay: float = 0.0,
measure_section_length: float | None = None,
) -> None:
"""Add calibration-trace measurements.
Arguments:
qubits:
The qubits to reset.
states:
The calibration states to prepare. Can be any combination of
("g", "e", "f"). The same states are prepared for each qubit.
Default: "ge"
active_reset: whether to use active reset to prepare the qubit in g before
every calibration state preparation
active_reset_states:
The qubit states to reset. Can be any combination of ("g", "e", "f").
Default: "ge"
active_reset_repetitions:
The number of active reset rounds to apply
feedback_processing_delay:
Feedback processing time.
Default: 300ns
measure_section_length:
The length of the measure section. If multiple qubits are passed, the
measure section must have the same length for each qubit.
Default: None.
"""
if isinstance(qubits, TunableTransmonQubit):
qubits = [qubits]
for state in states:
if active_reset:
active_reset_handles = [
dsl.handles.active_reset_calibration_trace_handle(q.uid, state)
for q in qubits
]
self.active_reset(
qubits,
active_reset_states=active_reset_states,
number_resets=active_reset_repetitions,
feedback_processing_delay=feedback_processing_delay,
handles=active_reset_handles,
measure_section_length=measure_section_length,
)
with dsl.section(
name=f"cal_{state}",
alignment=SectionAlignment.RIGHT,
):
with dsl.section(
name=f"cal_prep_{state}", alignment=SectionAlignment.RIGHT
):
for q in qubits:
self.prepare_state.omit_section(q, state=state)
with dsl.section(
name=f"cal_measure_{state}", alignment=SectionAlignment.LEFT
):
for q in qubits:
sec = self.measure(
q, dsl.handles.calibration_trace_handle(q.uid, state)
)
# Fix the length of the measure section
sec.length = measure_section_length
self.passive_reset(q)
The calibration
traces operation calls several other operations: active_reset
(optionally), prepare_state
, measure
, passive_reset
. It can be applied on multiple qubits in parallel by passing to it a list of qubits instead of a single instance.
However, notice that the automatic broadcasting feature is disabled for this operation (broadcast=False
). Because the calibration_traces
operation uses several other quantum operations, the automatic broadcasting feature would result in wrong timing when the operation is applied on multiple qubits in parallel. Automatic boradcasting implements the logic:
for q in qubits:
qops.calibration_traces(q, ...)
This results in completely different timing compared to the current implementation, where we take care to iterate over the qubits once when creating the drive section containing the qubit state preparation, and then again when creating the readout section, containing the measurement and passive reset. Doing this ensures that the preparation pulses are played back-to-back with the readout pulses, even if the qubit have different lengths of the drive pulses and readout pulses. See the tutorial on writing an experiment workflow to learn more about this alignment.
Operations that access experiment calibration¶
When a qubit experiment is created in the Applications Library, its calibration is initialized from the qubits used in the experiment, setting the oscillator frequencies and other properties of the SignalCalibration. See the tutorial on how to write experiment workflows for more information about this.
The TunableTransmonOperations
contains two operations that set the experiment calibration, set_frequency
and set_readout_amplitude
.
Both these operations do the following:
get the experiment calibration by calling
dsl.experiment_calibration()
;modify the relevant entries in the
SignalCalibration
of one or several logical signal lines of the qubit.
Let's see how this is done in the set_readout_amplitude
operation by inspecting its source code:
qops.set_readout_amplitude.src
@dsl.quantum_operation
def set_readout_amplitude(
self,
q: TunableTransmonQubit,
amplitude: float | SweepParameter,
*,
calibration: Calibration | None = None,
) -> None:
"""Sets the readout amplitude of the given qubit's measure line.
Arguments:
q:
The qubit to set the readout amplitude of.
amplitude:
The amplitude to set for the measure line
in units from 0 (no power) to 1 (full scale).
calibration:
The experiment calibration to update (see the note below).
By default, the calibration from the currently active
experiment context is used. If no experiment context is
active, for example when using
`@qubit_experiment(context=False)`, the calibration
object may be passed explicitly.
Raises:
RuntimeError:
If there is an attempt to call `set_readout_amplitude` more than
once on the same signal. See notes below for details.
Notes:
Currently `set_readout_amplitude` is implemented by setting the
amplitude of the measure line signal in the experiment calibration.
This has two important consequences:
* Each experiment may only set one amplitude per readout line,
although this may be a parameter sweep.
* The set readout amplitude or sweep applies for the whole experiment
regardless of where in the experiment the amplitude is set.
This will be improved in a future release.
"""
if calibration is None:
calibration = dsl.experiment_calibration()
measure_line, _ = q.readout_parameters()
signal_calibration = calibration[q.signals[measure_line]]
if getattr(calibration, "_set_readout_amplitude", False):
# We mark the oscillator with a _set_readout_amplitude attribute to ensure
# that set_readout_amplitude isn't performed on the same signal twice.
# Ideally LabOne Q DSL provide a more direct method that removes the
# need for setting amplitude on the experiment calibration.
raise RuntimeError(
f"Readout amplitude of qubit {q.uid}"
f" measure line was set multiple times"
f" using the set_readout_amplitude operation.",
)
calibration._set_readout_amplitude = True
signal_calibration.amplitude = amplitude
The signal calibration of the qubit measure line is extracted and its amplitude
property is modified to the value passed by the user, which can be either a numerical value or a SweepParameter
.
The set_frequency
operation works in a similar way, but it is a little more complicated because it offers more options for the user. Let's look at its source code:
qops.set_frequency.src
@dsl.quantum_operation
def set_frequency(
self,
q: TunableTransmonQubit,
frequency: float | SweepParameter,
*,
transition: str | None = None,
readout: bool = False,
rf: bool = True,
calibration: Calibration | None = None,
) -> None:
"""Sets the frequency of the given qubit drive line or readout line.
Arguments:
q:
The qubit to set the transition or readout frequency of.
frequency:
The frequency to set in Hz.
By default the frequency specified is the RF frequency.
The oscillator frequency may be set directly instead
by passing `rf=False`.
transition:
The transition to rotate. By default this is "ge"
(i.e. the 0-1 transition).
readout:
If true, the frequency of the readout line is set
instead. Setting the readout frequency to a sweep parameter
is only supported in spectroscopy mode. The LabOne Q compiler
will raise an error in other modes.
rf:
If True, set the RF frequency of the transition.
If False, set the oscillator frequency directly instead.
The default is to set the RF frequency.
calibration:
The experiment calibration to update (see the note below).
By default, the calibration from the currently active
experiment context is used. If no experiment context is
active, for example when using
`@qubit_experiment(context=False)`, the calibration
object may be passed explicitly.
Raises:
RuntimeError:
If there is an attempt to call `set_frequency` more than
once on the same signal. See notes below for details.
Notes:
Currently `set_frequency` is implemented by setting the
appropriate oscillator frequencies in the experiment calibration.
This has two important consequences:
* Each experiment may only set one frequency per signal line,
although this may be a parameter sweep.
* The set frequency or sweep applies for the whole experiment
regardless of where in the experiment the frequency is set.
This will be improved in a future release.
"""
if readout:
signal_line, _ = q.readout_parameters()
lo_frequency = q.parameters.readout_lo_frequency
else:
signal_line, _ = q.transition_parameters(transition)
lo_frequency = q.parameters.drive_lo_frequency
if rf:
# This subtraction works for both numbers and SweepParameters
frequency -= lo_frequency
if calibration is None:
calibration = dsl.experiment_calibration()
signal_calibration = calibration[q.signals[signal_line]]
oscillator = signal_calibration.oscillator
if oscillator is None:
oscillator = signal_calibration.oscillator = Oscillator(frequency=frequency)
if getattr(oscillator, "_set_frequency", False):
# We mark the oscillator with a _set_frequency attribute to ensure that
# set_frequency isn't performed on the same oscillator twice. Ideally
# LabOne Q would provide a set_frequency DSL method that removes the
# need for setting the frequency on the experiment calibration.
raise RuntimeError(
f"Frequency of qubit {q.uid} {signal_line} line was set multiple times"
f" using the set_frequency operation.",
)
oscillator._set_frequency = True
oscillator.frequency = frequency
if readout:
# LabOne Q does not support software modulation of measurement
# signal sweeps because it results in multiple readout waveforms
# on the same readout signal. Ideally the LabOne Q compiler would
# sort this out for us when the modulation type is AUTO, but currently
# it does not.
oscillator.modulation_type = ModulationType.HARDWARE
This operation modifies the oscillator frequency of either the qubit measure line or its drive line, depending on the readout=True/False
. The frequency
parameter that is passed can be either the IF frequency of the qubit readout/drive (in units of MHz) or the qubit readout frequency or resonance frequency (in units of GHz). In the latter case, the qubit readout/drive LO frequency is subtracted from the value that was passed in. This feature is very useful in practice because you can directly pass a sweep around qubit frequencies that are physically intuitive such as the readout resonator frequency or the qubit transition frequency, and this operation takes care to convert these values to the ones expected by the instruments.
Note:
There is only a single experiment calibration per experiment, so if multiple quantum operations modify the same calibration items, only the last modification will be retained.
The experiment calibration is only accessible if there is an
Experiment
, so quantum operations that calldsl.experiment_calibration
can only be called inside an experiment and will raise an exception otherwise. Let's write a quickExperiment
to see how these operations work in practice. We use the@dsl.qubit_experiment
decorator; see Writing a New Experiment Workflow to learn more about how to use this decorator to writeExperiment
pulse sequences.
@dsl.qubit_experiment
def exp_for_checking_op(q, frequencies, qops):
"""Simple experiment to test the operation we've just written."""
with dsl.acquire_loop_rt(count=1):
with dsl.sweep(
name="readout_frequency_sweep",
parameter=SweepParameter("readout_frequency_sweep", frequencies)
) as freq_sweep:
qops.set_frequency(q, frequency=freq_sweep, readout=True, rf=True)
frequencies = qubits[0].parameters.readout_resonator_frequency + np.linspace(-30e6, 30e6, 11)
exp = exp_for_checking_op(qubits[0], frequencies, qops)
exp.get_calibration().calibration_items["/logical_signal_groups/q0/measure"]
SignalCalibration( │ oscillator=Oscillator( │ │ uid='q0_readout_acquire_osc', │ │ frequency=SweepParameter( │ │ │ uid='par0', │ │ │ values=array([7.00e+07, 7.60e+07, 8.20e+07, 8.80e+07, 9.40e+07, 1.00e+08, │ 1.06e+08, 1.12e+08, 1.18e+08, 1.24e+08, 1.30e+08]), │ │ │ axis_name=None, │ │ │ driven_by=[ │ │ │ │ SweepParameter( │ │ │ │ │ uid='readout_frequency_sweep', │ │ │ │ │ values=array([7.070e+09, 7.076e+09, 7.082e+09, 7.088e+09, 7.094e+09, 7.100e+09, │ 7.106e+09, 7.112e+09, 7.118e+09, 7.124e+09, 7.130e+09]), │ │ │ │ │ axis_name=None, │ │ │ │ │ driven_by=None │ │ │ │ ) │ │ │ ] │ │ ), │ │ modulation_type=ModulationType.HARDWARE, │ │ carrier_type=None │ ), │ local_oscillator=Oscillator( │ │ uid='q0_readout_local_osc', │ │ frequency=7000000000.0, │ │ modulation_type=ModulationType.AUTO, │ │ carrier_type=None │ ), │ mixer_calibration=None, │ precompensation=None, │ port_delay=None, │ port_mode=None, │ delay_signal=None, │ voltage_offset=None, │ range=5, │ threshold=None, │ amplitude=None, │ amplifier_pump=None, │ added_outputs=None, │ automute=False )
The oscillator frequency has been set to the IF frequency calculated from subtracting the qubit readout LO frequency from the sweep values we have specified:
frequencies - qubits[0].parameters.readout_lo_frequency
array([7.00e+07, 7.60e+07, 8.20e+07, 8.80e+07, 9.40e+07, 1.00e+08, 1.06e+08, 1.12e+08, 1.18e+08, 1.24e+08, 1.30e+08])
You have learnt how qubits are used together with quantum operations to create snippets of sections and pulses.
Learn how to use qubits and quantum operations to create Experiment
pulses sequences in our next tutorial on writing an experiment workflows.