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)
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
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. Check out the names of the pulse functionals in our pulse library.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
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}
transition_parameters("ef")¶
drive_line_ef, parameters_ef = qubits[0].transition_parameters("ef")
drive_line_ef, parameters_ef
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}
readout_parameters()¶
measure_line, readout_parameters = qubits[0].readout_parameters()
measure_line, readout_parameters
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}
readout_integration_parameters()¶
acquire_line, readout_integration_parameters = qubits[0].readout_integration_parameters()
acquire_line, readout_integration_parameters
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}
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()
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
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. A 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 any 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)
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
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"
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
Check that this is different to the amplitude of a default x180
operation:
section = qops.x180(qubits[0])
section.children[-1].amplitude
This value comes from the $\pi$-pulse amplitude of the qubit:
qubits[0].parameters.ge_drive_amplitude_pi
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
Readout operations¶
Let's have another look at the set of TunableTransmonOperations
:
qops.keys()
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))
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))
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
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
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])
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
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
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
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
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
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["q0/measure"]
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
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.