Quantum Operations¶
Each set of quantum operations defines operations for a particular type of qubit. At the moment the library only provides operations for tunable transmon qubits. We'll introduce you to these operations and show you how to add to or modify them. You can also create your own kind of qubit and quantum operations for them but that will not be covered in this tutorial.
Let's get started.
Setting up a device and session¶
Build your LabOne Q DeviceSetup
, qubits and Session
as normal. Here we import an example from the applications library's test suite (this will change in the near future):
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.11.07 16:42:16.329] 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.11.07 16:42:16.332] INFO VERSION: laboneq 2.41.0
[2024.11.07 16:42:16.333] INFO Connecting to data server at localhost:8004
[2024.11.07 16:42:16.336] INFO Connected to Zurich Instruments LabOne Data Server version 24.10 at localhost:8004
[2024.11.07 16:42:16.350] INFO Configuring the device setup
[2024.11.07 16:42:16.386] INFO The device setup is configured
<laboneq.dsl.session.ConnectionState at 0x7af45dd106e0>
Qubits and qubit parameters¶
Inspect the qubit parameters
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, │ dc_slot=0, │ dc_voltage_parking=0.0 )
The following qubit parameters are used by the Applications Library:
- Parameters with the prefixes
ge_drive_
/ef_drive_
are used to configure the parameters for implementing a pi-pulse on the ge and ef transitions. - 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.
The remaining qubit parameters are still there for legacy reasons and have no effect. These will be cleaned up soon.
Quantum Operations¶
Quantum operations provide a means for writing DSL at a higher level of abstraction than in base LabOne Q. When writing LabOne Q DSL one works with operations on signal lines. When writing DSL with quantum operations, one works with operations on qubits.
Note:
The experiments built using quantum operations are just ordinary LabOne Q experiments. It's how the experiments are described that differs. One also uses LabOne Q DSL to define quantum operations and one can combine quantum operations with ordinary LabOne Q DSL, because they are producing the same DSL.
Building a first experiment pulse sequence¶
Let's build our first experiment pulse sequence using quantum operations. The experiment pulse sequence is described by the LabOne Q Experiment
object.
We'll need to import some things are the start. We'll explain what each of them is as we go:
import numpy as np
from laboneq.simple import dsl
from laboneq_applications.qpu_types.tunable_transmon import (
TunableTransmonOperations,
)
Let's start with a tiny experiment sequence that rotates a qubit a given angle about the x-axis and performs a measurement:
@dsl.qubit_experiment
def rotate_and_measure(qop, q, angle, count=10):
"""Rotate q by the given angle and measure it."""
with dsl.acquire_loop_rt(count=count):
qop.rx(q, angle)
qop.measure(q, "measure_q")
and break down the code line by line:
@dsl.qubit_experiment
: This decorator creates a new experiment object and makes it accessible inside therotate_and_measure
function. It also finds the qubits in the function arguments (i.e.q
) and sets the experiment calibration using them.def rotate_and_measure(qop, q, angle, count=10):
: These are ordinary function arguments, except for the detection of the qubit objects just mentioned. Theqop
argument supplies the set of quantum operations to use. The same function can be used to build an experiment for any qubit platform that provides the same operations.with dsl.acquire_loop_rt(count=count)
: This is just theacquire_loop_rt
function fromlaboneq.dsl.experiments.builtins
. Thelaboneq_applications.dsl
module is just a convenient way to access the LabOne Q DSL functionality.qop.rx(q, angle)
: Hereqop
is a set of quantum operations. Therx
operation creates a pulse that rotates the qubit by the given angle (in radians) by linearly scaling the pulse amplitude with respect to the qubit pi-pulse amplitude stored inqubit.parameters.drive_parameters_ge.amplitdue_pi
. The pulse type is specified inqubit.parameters.drive_parameters_ge.pulse.function
and it uses the length inqubit.parameters.drive_parameters_ge.length
.- To implement a pi-pulse and a pi-half pulse, we provide the operations
qop.x180
,qop.y180
,qop.x90
,qop.y90
, which use the pulse amplitdues values inqubit.parameters.drive_parameters_ge.amplitdue_pi
andqubit.parameters.drive_parameters_ge.amplitdue_pi2
, respectively,
- To implement a pi-pulse and a pi-half pulse, we provide the operations
qop.measure(q, "measure_q")
: Performs a measurement on the qubit, using the readout pulse and kernels specified by the qubit parametersqubit.parameters.readout_parameters
andqubit.parameters.readout_integration_parameters
."measure_q"
is the handle to store the results under.
To build the experiment we need some qubits and a set of quantum operations. Let's use the TunableTransmonOperations
provided by the applications library and the qubit we defined earlier:
qop = TunableTransmonOperations()
q0 = qubits[0]
exp = rotate_and_measure(qop, q0, np.pi / 2, count=10)
Here exp
is just an ordinary LabOne Q experiment:
exp
Experiment( │ uid='rotate_and_measure', │ name='unnamed', │ signals={ │ │ '/logical_signal_groups/q0/drive': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/drive', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=Oscillator( │ │ │ │ │ uid='q0_drive_ge_osc', │ │ │ │ │ frequency=100000000.0, │ │ │ │ │ modulation_type=ModulationType.AUTO, │ │ │ │ │ carrier_type=None │ │ │ │ ), │ │ │ │ local_oscillator=Oscillator( │ │ │ │ │ uid='q0_drive_local_osc', │ │ │ │ │ frequency=6400000000, │ │ │ │ │ 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=10, │ │ │ │ threshold=None, │ │ │ │ amplitude=None, │ │ │ │ amplifier_pump=None, │ │ │ │ added_outputs=None, │ │ │ │ automute=False │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/drive' │ │ ), │ │ '/logical_signal_groups/q0/drive_ef': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/drive_ef', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=Oscillator( │ │ │ │ │ uid='q0_drive_ef_osc', │ │ │ │ │ frequency=-100000000.0, │ │ │ │ │ modulation_type=ModulationType.AUTO, │ │ │ │ │ carrier_type=None │ │ │ │ ), │ │ │ │ local_oscillator=Oscillator( │ │ │ │ │ uid='q0_drive_local_osc', │ │ │ │ │ frequency=6400000000, │ │ │ │ │ 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=10, │ │ │ │ threshold=None, │ │ │ │ amplitude=None, │ │ │ │ amplifier_pump=None, │ │ │ │ added_outputs=None, │ │ │ │ automute=False │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/drive_ef' │ │ ), │ │ '/logical_signal_groups/q0/measure': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/measure', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=Oscillator( │ │ │ │ │ uid='q0_readout_acquire_osc', │ │ │ │ │ frequency=100000000.0, │ │ │ │ │ modulation_type=ModulationType.AUTO, │ │ │ │ │ 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 │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/measure' │ │ ), │ │ '/logical_signal_groups/q0/acquire': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/acquire', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=Oscillator( │ │ │ │ │ uid='q0_readout_acquire_osc', │ │ │ │ │ frequency=100000000.0, │ │ │ │ │ modulation_type=ModulationType.AUTO, │ │ │ │ │ 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=2e-08, │ │ │ │ port_mode=None, │ │ │ │ delay_signal=None, │ │ │ │ voltage_offset=None, │ │ │ │ range=10, │ │ │ │ threshold=None, │ │ │ │ amplitude=None, │ │ │ │ amplifier_pump=None, │ │ │ │ added_outputs=None, │ │ │ │ automute=False │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/acquire' │ │ ), │ │ '/logical_signal_groups/q0/flux': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/flux', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=None, │ │ │ │ local_oscillator=None, │ │ │ │ mixer_calibration=None, │ │ │ │ precompensation=None, │ │ │ │ port_delay=None, │ │ │ │ port_mode=None, │ │ │ │ delay_signal=None, │ │ │ │ voltage_offset=0, │ │ │ │ range=None, │ │ │ │ threshold=None, │ │ │ │ amplitude=None, │ │ │ │ amplifier_pump=None, │ │ │ │ added_outputs=None, │ │ │ │ automute=False │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/flux' │ │ ) │ }, │ version=DSLVersion.V3_0_0, │ epsilon=0.0, │ sections=[ │ │ AcquireLoopRt( │ │ │ uid='unnamed_0', │ │ │ name='unnamed', │ │ │ alignment=SectionAlignment.LEFT, │ │ │ execution_type=ExecutionType.REAL_TIME, │ │ │ length=None, │ │ │ play_after=None, │ │ │ children=[ │ │ │ │ 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.4, │ │ │ │ │ │ │ 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 │ │ │ │ ), │ │ │ │ Section( │ │ │ │ │ uid='measure_q0_0', │ │ │ │ │ name='measure_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/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='measure_q', │ │ │ │ │ │ │ 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 │ │ │ │ ) │ │ │ ], │ │ │ trigger={}, │ │ │ on_system_grid=False, │ │ │ acquisition_type=AcquisitionType.INTEGRATION, │ │ │ averaging_mode=AveragingMode.CYCLIC, │ │ │ count=10, │ │ │ repetition_mode=RepetitionMode.FASTEST, │ │ │ repetition_time=None, │ │ │ reset_oscillator_phase=False │ │ ) │ ] )
Have a look through the generated experiment and check that:
- the experiment signals are those for the qubit.
- the qubit calibration has been set.
- the experiment sections are those you expect.
Examining the set of operations¶
So far we've treated the quantum operations as a black box. Now let's look inside. We can start by listing the quantum operations:
qop.keys()
['acquire', 'active_reset', 'barrier', 'calibration_traces', 'delay', 'measure', 'passive_reset', 'prepare_state', 'ramsey', 'rx', 'ry', 'rz', 'set_frequency', 'set_readout_amplitude', 'spectroscopy_drive', 'x180', 'x180_ef_reset', 'x90', 'y180', 'y90', 'z180', 'z90']
The quantum operations have an attribute QUBIT_TYPES
which specifies the type of qubits support by the quantum operations object we've created. In our case, that's the TunableTransmonQubit
:
qop.QUBIT_TYPES
laboneq_applications.qpu_types.tunable_transmon.qubit_types.TunableTransmonQubit
Under the hood there is a BASE_OPS
attribute. This is an implementation detail -- it contains the original definitions of the quantum operations. We will ignore it for now except to mention that individual quantum operations can be overridden with alternative implementations if required.
Let's take a look at one of the quantum operations.
Working with a quantum operation¶
qop.rx?
qop.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 drive_line == "drive_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,
)
One can write:
qop.rx?
to view the documentation as usual, orqop.rx.src
to easily see how a quantum operation is implemented.
Take a moment to read the documentation of a few of the other operations and their source, for example:
qop.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,
)
qop.x90.src
@dsl.quantum_operation
def x90(
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 90 degrees about the X axis.
This implementation calls `rx(q, π / 2, ...)`.
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_pi2".
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_pi2"]
self.rx.omit_section(
q,
self._PI_BY_2,
transition=transition,
amplitude=amplitude,
phase=phase,
length=length,
pulse=pulse,
)
Calling a quantum operation by itself produces a LabOne Q section:
section = qop.rx(qubits[0], np.pi)
section
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 )
Some things to note about the section:
- The section name is the name of the quantum operation, followed by the UIDs of the qubits it is applied to.
- The section UID is automatically generated from the name.
- The section starts by reserving all the signal lines of the qubit it operates on so that operations acting on the same qubits never overlap.
In addition to .src
each quantum operation also has three special attributes:
.op
: This returns the function that implements the quantum operation..omit_section(...)
: This method builds the quantum operation but without a containing section and without reserving the qubit signals. This is useful if one wants to define a quantum operation in terms of another, but not have deeply nested sections..omit_reserves(...)
: The method builds the quantum operation but doesn't reserve the qubit signals. This is useful if you want to manage the reserved signals yourself.
Let's look at .op
now. We'll use .omit_section
and .omit_reserves
once we've seen how to write our own operations.
qop.rx.op
<function laboneq_applications.qpu_types.tunable_transmon.operations.TunableTransmonOperations.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'>
Writing a quantum operation¶
Often you'll want to write your own quantum operation, either to create a new operation or to replace an existing one.
Let's write our own very simple implementation of an rx
operation that varies the pulse length instead of the amplitude:
@qop.register
def simple_rx(self, q, angle):
"""A very simple implementation of an RX operation that varies pulse length."""
# Determined via rigorously calibration ;) :
amplitude = 0.6
length_for_pi = 50e-9
# Calculate the length of the pulse
length = length_for_pi * (angle / np.pi)
dsl.play(
q.signals["drive"],
amplitude=amplitude,
phase=0.0,
length=length,
pulse=dsl.pulse_library.const(),
)
Applying the decorator qop.register
wraps our function simple_rx
in a quantum operation and registers it with our current set of operations, qop
.
We can confirm that it's registered by checking that its in our set of operations, or by looking it up as an attribute or element of our operations:
"simple_rx" in qop
True
qop.simple_rx
Operation(op=<function simple_rx at 0x7af301dfc220>, op_name=simple_rx, neartime=False, supports_broadcast=True)
qop["simple_rx"]
Operation(op=<function simple_rx at 0x7af301dfc220>, op_name=simple_rx, neartime=False, supports_broadcast=True)
If an operation with the same name already exists it will be replaced.
Let's run our new operations and examine the section it produces:
section = qop.simple_rx(qubits[0], np.pi)
section
Section( │ uid='__simple_rx_q0_0', │ name='simple_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='const', │ │ │ │ uid='p0', │ │ │ │ amplitude=1.0, │ │ │ │ length=1e-07, │ │ │ │ can_compress=False, │ │ │ │ pulse_parameters=None │ │ │ ), │ │ │ amplitude=0.6, │ │ │ increment_oscillator_phase=None, │ │ │ phase=0.0, │ │ │ set_oscillator_phase=None, │ │ │ length=5e-08, │ │ │ pulse_parameters=None, │ │ │ precompensation_clear=None, │ │ │ marker=None │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
We can also create aliases for existing quantum operations that are already registered by assigning additional names for them:
qop["rx_length"] = qop.simple_rx
"rx_length" in qop
True
Using omit_section¶
Let's imagine that we'd like to write an x90_length
operation that calls our new rx_length
but always specifies an angle of $\frac{\pi}{2}$. We can write this as:
@qop.register
def x90_length(self, q):
return self.rx_length(q, np.pi / 2)
However, when we call this we will have deeply nested sections and many signal lines reserved. This obscures the structure of our experiment:
section = qop.x90_length(qubits[0])
section
Section( │ uid='__x90_length_q0_0', │ name='x90_length_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' │ │ ), │ │ Section( │ │ │ uid='__simple_rx_q0_1', │ │ │ name='simple_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='const', │ │ │ │ │ │ uid='p1', │ │ │ │ │ │ amplitude=1.0, │ │ │ │ │ │ length=1e-07, │ │ │ │ │ │ can_compress=False, │ │ │ │ │ │ pulse_parameters=None │ │ │ │ │ ), │ │ │ │ │ amplitude=0.6, │ │ │ │ │ increment_oscillator_phase=None, │ │ │ │ │ phase=0.0, │ │ │ │ │ set_oscillator_phase=None, │ │ │ │ │ length=2.5e-08, │ │ │ │ │ pulse_parameters=None, │ │ │ │ │ precompensation_clear=None, │ │ │ │ │ marker=None │ │ │ │ ) │ │ │ ], │ │ │ trigger={}, │ │ │ on_system_grid=False │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
We can remove the extra section and signal reservations by call our inner operation using .omit_section
instead:
@qop.register
def x90_length(self, q):
return self.rx_length.omit_section(q, np.pi / 2)
Note how much simpler the section structure looks now:
section = qop.x90_length(qubits[0])
section
Section( │ uid='__x90_length_q0_1', │ name='x90_length_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='const', │ │ │ │ uid='p2', │ │ │ │ amplitude=1.0, │ │ │ │ length=1e-07, │ │ │ │ can_compress=False, │ │ │ │ pulse_parameters=None │ │ │ ), │ │ │ amplitude=0.6, │ │ │ increment_oscillator_phase=None, │ │ │ phase=0.0, │ │ │ set_oscillator_phase=None, │ │ │ length=2.5e-08, │ │ │ pulse_parameters=None, │ │ │ precompensation_clear=None, │ │ │ marker=None │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
Using omit_reserves¶
By default the section created by a quantum operation reserves all of the qubit signals so that two operations on the same qubit cannot overlap. In rare circumstances one might wish to not reserve the qubit signals and to manage the avoidance of overlaps yourself.
In these cases .omit_reserves
is helpful.
Let's look at what the x90_length
section looks like with the reserves:
section = qop.x90_length.omit_reserves(qubits[0])
section
Section( │ uid='__x90_length_q0_2', │ name='x90_length_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='p3', │ │ │ │ amplitude=1.0, │ │ │ │ length=1e-07, │ │ │ │ can_compress=False, │ │ │ │ pulse_parameters=None │ │ │ ), │ │ │ amplitude=0.6, │ │ │ increment_oscillator_phase=None, │ │ │ phase=0.0, │ │ │ set_oscillator_phase=None, │ │ │ length=2.5e-08, │ │ │ pulse_parameters=None, │ │ │ precompensation_clear=None, │ │ │ marker=None │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
Broadcasting quantum operations¶
Broadcasting quantum operations is an experimental feature.
The majority of quantum operations can be broadcast which means to run them on multiple qubits in parallel.
When one broadcasts an operation over a list of qubits, it creates one operation section per qubit. The operation thus returns a list of sections. All those sections will be added to the section currently being built if there is one.
When broadcasting, other parameters of the operation may be either specified per-qubit or once for all the qubits. If a parameter is supplied as a list (or tuple) it is treated as being per-qubit. Otherwise the single value supplied is used for all the qubits.
We activate broadcasting just by supplying a list of qubits instead of a single qubit, like so:
sections = qop.x90(qubits)
It created one section for each of our qubits:
[section.name for section in sections]
['x90_q0', 'x90_q1', 'x90_q2', 'x90_q3', 'x90_q4', 'x90_q5']
Note that the sections returned are in the same order as the list of qubits we provided. This ordering is guarantted by the broadcasting machinery so you can rely on it if you need to.
If we look at one of these sections, we can see it looks just like the section created by calling the operation with the corresponding single qubit.
Here is the section for qubit q2
:
sections[2]
Section( │ uid='__x90_q2_0', │ name='x90_q2', │ alignment=SectionAlignment.LEFT, │ execution_type=None, │ length=None, │ play_after=None, │ children=[ │ │ Reserve( │ │ │ signal='/logical_signal_groups/q2/drive' │ │ ), │ │ Reserve( │ │ │ signal='/logical_signal_groups/q2/drive_ef' │ │ ), │ │ Reserve( │ │ │ signal='/logical_signal_groups/q2/measure' │ │ ), │ │ Reserve( │ │ │ signal='/logical_signal_groups/q2/acquire' │ │ ), │ │ Reserve( │ │ │ signal='/logical_signal_groups/q2/flux' │ │ ), │ │ PlayPulse( │ │ │ signal='/logical_signal_groups/q2/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.42000000000000004, │ │ │ 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 )
What about operations that take additional parameters like rx
?
In these cases you can choose whether to supply one value for the parameter for all the qubits, or one value for each qubit.
We'll try a single value for all qubits first:
sections = qop.rx(qubits, np.pi)
If we take a look at the amplitudes of the pulses we'll see that they're all very similar. They vary only because our qubit parameters vary a little:
def print_rx_amplitudes(sections):
"""Print the amplitude of rx operation pulses."""
print("Amplitudes")
print("----------")
print()
for section in sections:
print(section.children[-1].amplitude)
print_rx_amplitudes(sections)
Amplitudes ---------- 0.8 0.81 0.8200000000000001 0.8300000000000001 0.8400000000000001 0.8500000000000001
Now let's try passing a different angle for each qubit:
sections = qop.rx(qubits, [np.pi / (i + 1) for i in range(6)])
print_rx_amplitudes(sections)
Amplitudes ---------- 0.8 0.405 0.2733333333333333 0.20750000000000002 0.16800000000000004 0.14166666666666666
Here we can see the amplitudes get smaller each time because we're rotating each qubit less than the previous one.
What happens if you supply a different number of angles and qubits? You will get an error like this:
try:
# only one angle is supplied but there are six qubits
sections = qop.rx(qubits, [np.pi])
except ValueError as e:
print(e)
Quantum operation 'rx' was being broadcast with length 6 but the following positional arguments have different lengths: arg[1] has length 1
Broadcasting is powerful and a little complex. Just remember that it generates one operation section for each qubit.
It's good practice to organize all of the generated sections nicely. For example, when using many broadcast operations one after the other one should consider carefully how they should all be arranged.
When doing a series of broadcast operations followed by a broadcast measurement, its often good practice to do something like:
with dsl.section(name="operations", alignment=SectionAlignment.RIGHT):
qop.prepare_state(qubits)
qop.x180(qubits)
qop.delay(qubits, 10e9)
with dsl.section(name="measure", alignment=SectionAlignment.LEFT):
qop.measure(qubits, [dsl.handles.result_handle(q.uid) for q in qubits])
This ensures that there is a minimal gap between the end of the operations and the start of the measurements.
If you need to write a quantum operation that should never be broadcast, for example an operation such as a QFT (Quantum Fourier Transform) that already takes in a list of qubits, one can use @quantum_operation(broadcast=False)
like this:
@dsl.quantum_operation(broadcast=False)
def x90_never_broadcast(qop, qubits):
for q in qubits:
qop.x90(q)
qop.register(x90_never_broadcast)
Now when we call x90_never_broadcast
with a list of qubits it will not use the broadcast functionality but just call the operation we implemented:
section = qop.x90_never_broadcast(qubits)
section.name
'x90_never_broadcast_q0_q1_q2_q3_q4_q5'
As you can see, it returned just one section that applies X90 gates to each qubit.
Near-time quantum operations¶
Most quantum operations are real-time. That is, they are intended to be called inside a dsl.acquire_loop_rt
block.
Some operations must be called in near-time, that is, outside the dsl.acquire_loop_rt
block. In particular, operations that call near-time callbacks using dsl.call
must be declared as near-time operations.
Let's see how write such an operation:
@qop.register
@dsl.quantum_operation(neartime=True)
def set_dc_bias(qop, qubit, voltage):
dsl.call("set_dc_bias", voltage=voltage)
The @dsl.quantum_operation(neartime=True)
decorator marks the operation as near-time. The function dsl.call
makes a near-time callback to pre-defined near-time function (which has not be defined in this example).
The section created looks as follows:
section = qop.set_dc_bias(qubits[0], 1.5)
section
Section( │ uid='__set_dc_bias_q0_0', │ name='set_dc_bias_q0', │ alignment=SectionAlignment.LEFT, │ execution_type=ExecutionType.NEAR_TIME, │ length=None, │ play_after=None, │ children=[ │ │ Call( │ │ │ func_name='set_dc_bias', │ │ │ args={ │ │ │ │ 'voltage': 1.5 │ │ │ } │ │ ) │ ], │ trigger={}, │ on_system_grid=False )
Note that the execution_type
is set to ExecutionType.NEAR_TIME
. This ensures that the LabOne Q compiler will raise an error if the operation is called inside the dsl.acquire_loop_rt
block.
The section also does not reserve any signals. A near-time operation may not use any signals (since operations on signals are real-time).
Replacing a quantum operation¶
To end off our look at quantum operations, let's replace the original rx
gate with our own one and then use our existing experiment definition to produce a new experiment with the operation we've just written.
qop["rx"] = qop.simple_rx # replace the rx gate
exp = rotate_and_measure(qop, qubits[0], np.pi / 2)
exp
Experiment( │ uid='rotate_and_measure', │ name='unnamed', │ signals={ │ │ '/logical_signal_groups/q0/drive': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/drive', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=Oscillator( │ │ │ │ │ uid='q0_drive_ge_osc', │ │ │ │ │ frequency=100000000.0, │ │ │ │ │ modulation_type=ModulationType.AUTO, │ │ │ │ │ carrier_type=None │ │ │ │ ), │ │ │ │ local_oscillator=Oscillator( │ │ │ │ │ uid='q0_drive_local_osc', │ │ │ │ │ frequency=6400000000, │ │ │ │ │ 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=10, │ │ │ │ threshold=None, │ │ │ │ amplitude=None, │ │ │ │ amplifier_pump=None, │ │ │ │ added_outputs=None, │ │ │ │ automute=False │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/drive' │ │ ), │ │ '/logical_signal_groups/q0/drive_ef': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/drive_ef', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=Oscillator( │ │ │ │ │ uid='q0_drive_ef_osc', │ │ │ │ │ frequency=-100000000.0, │ │ │ │ │ modulation_type=ModulationType.AUTO, │ │ │ │ │ carrier_type=None │ │ │ │ ), │ │ │ │ local_oscillator=Oscillator( │ │ │ │ │ uid='q0_drive_local_osc', │ │ │ │ │ frequency=6400000000, │ │ │ │ │ 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=10, │ │ │ │ threshold=None, │ │ │ │ amplitude=None, │ │ │ │ amplifier_pump=None, │ │ │ │ added_outputs=None, │ │ │ │ automute=False │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/drive_ef' │ │ ), │ │ '/logical_signal_groups/q0/measure': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/measure', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=Oscillator( │ │ │ │ │ uid='q0_readout_acquire_osc', │ │ │ │ │ frequency=100000000.0, │ │ │ │ │ modulation_type=ModulationType.AUTO, │ │ │ │ │ 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 │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/measure' │ │ ), │ │ '/logical_signal_groups/q0/acquire': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/acquire', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=Oscillator( │ │ │ │ │ uid='q0_readout_acquire_osc', │ │ │ │ │ frequency=100000000.0, │ │ │ │ │ modulation_type=ModulationType.AUTO, │ │ │ │ │ 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=2e-08, │ │ │ │ port_mode=None, │ │ │ │ delay_signal=None, │ │ │ │ voltage_offset=None, │ │ │ │ range=10, │ │ │ │ threshold=None, │ │ │ │ amplitude=None, │ │ │ │ amplifier_pump=None, │ │ │ │ added_outputs=None, │ │ │ │ automute=False │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/acquire' │ │ ), │ │ '/logical_signal_groups/q0/flux': ExperimentSignal( │ │ │ uid='/logical_signal_groups/q0/flux', │ │ │ calibration=SignalCalibration( │ │ │ │ oscillator=None, │ │ │ │ local_oscillator=None, │ │ │ │ mixer_calibration=None, │ │ │ │ precompensation=None, │ │ │ │ port_delay=None, │ │ │ │ port_mode=None, │ │ │ │ delay_signal=None, │ │ │ │ voltage_offset=0, │ │ │ │ range=None, │ │ │ │ threshold=None, │ │ │ │ amplitude=None, │ │ │ │ amplifier_pump=None, │ │ │ │ added_outputs=None, │ │ │ │ automute=False │ │ │ ), │ │ │ mapped_logical_signal_path='/logical_signal_groups/q0/flux' │ │ ) │ }, │ version=DSLVersion.V3_0_0, │ epsilon=0.0, │ sections=[ │ │ AcquireLoopRt( │ │ │ uid='unnamed_0', │ │ │ name='unnamed', │ │ │ alignment=SectionAlignment.LEFT, │ │ │ execution_type=ExecutionType.REAL_TIME, │ │ │ length=None, │ │ │ play_after=None, │ │ │ children=[ │ │ │ │ Section( │ │ │ │ │ uid='simple_rx_q0_0', │ │ │ │ │ name='simple_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='const', │ │ │ │ │ │ │ │ uid='p4', │ │ │ │ │ │ │ │ amplitude=1.0, │ │ │ │ │ │ │ │ length=1e-07, │ │ │ │ │ │ │ │ can_compress=False, │ │ │ │ │ │ │ │ pulse_parameters=None │ │ │ │ │ │ │ ), │ │ │ │ │ │ │ amplitude=0.6, │ │ │ │ │ │ │ increment_oscillator_phase=None, │ │ │ │ │ │ │ phase=0.0, │ │ │ │ │ │ │ set_oscillator_phase=None, │ │ │ │ │ │ │ length=2.5e-08, │ │ │ │ │ │ │ pulse_parameters=None, │ │ │ │ │ │ │ precompensation_clear=None, │ │ │ │ │ │ │ marker=None │ │ │ │ │ │ ) │ │ │ │ │ ], │ │ │ │ │ trigger={}, │ │ │ │ │ on_system_grid=False │ │ │ │ ), │ │ │ │ Section( │ │ │ │ │ uid='measure_q0_0', │ │ │ │ │ name='measure_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/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='measure_q', │ │ │ │ │ │ │ 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 │ │ │ │ ) │ │ │ ], │ │ │ trigger={}, │ │ │ on_system_grid=False, │ │ │ acquisition_type=AcquisitionType.INTEGRATION, │ │ │ averaging_mode=AveragingMode.CYCLIC, │ │ │ count=10, │ │ │ repetition_mode=RepetitionMode.FASTEST, │ │ │ repetition_time=None, │ │ │ reset_oscillator_phase=False │ │ ) │ ] )
Confirm that the generated experiment contains the new implementation of the RX gate.
Let's put the original rx
implementation back so that we don't confuse ourselves later:
qop["rx"] = qop.BASE_OPS["rx"]
Don't worry to much about what BASE_OPS
is. It's just a place where the original quantum operations are restored.
Setting section attributes¶
Sometimes an operation will need to set special section attributes such as on_system_grid
.
This can be done by retrieving the current section and directly manipulating it.
To demonstrate, we'll create an operation whose section is required to be on the system grid:
@qop.register
def op_on_system_grid(self, q):
section = dsl.active_section()
section.on_system_grid = True
# ... play pulses, etc.
And then call it to confirm that the section has indeed been set to be on the grid:
section = qop.op_on_system_grid(qubits[0])
section.on_system_grid
True
Accessing experiment calibration¶
When a qubit experiment is created by the library its calibration is initialized from the qubits it operates on. Typically oscillator frequencies and other signal calibration are set.
Sometimes it may be useful for quantum operations to access or manipulate this configuration using experiment_calibration
which returns the calibration set for the current experiment.
Note:
The experiment calibration is only accessible if there is an experiment, so quantum operations that call
experiment_calibration
can only be called inside an experiment and will raise an exception otherwise.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.
Here is how we define a quantum operation that accesses the calibration:
@qop.register
def op_that_examines_signal_calibration(self, q):
calibration = dsl.experiment_calibration()
signal_calibration = calibration[q.signals["drive"]]
# ... examine or set calibration, play pulses, etc, e.g.:
signal_calibration.oscillator.frequency = 0.2121e9
To use it we will have to build an experiment. For now, just ignore the pieces we haven't covered. Writing a complete experiment will be covered shortly:
@dsl.qubit_experiment
def exp_for_checking_op(qop, q):
"""Simple experiment to test the operation we've just written."""
with dsl.acquire_loop_rt(count=1):
qop.op_that_examines_signal_calibration(q)
exp = exp_for_checking_op(qop, qubits[0])
exp.get_calibration().calibration_items["/logical_signal_groups/q0/drive"]
SignalCalibration( │ oscillator=Oscillator( │ │ uid='q0_drive_ge_osc', │ │ frequency=212100000.0, │ │ modulation_type=ModulationType.AUTO, │ │ carrier_type=None │ ), │ local_oscillator=Oscillator( │ │ uid='q0_drive_local_osc', │ │ frequency=6400000000, │ │ 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=10, │ threshold=None, │ amplitude=None, │ amplifier_pump=None, │ added_outputs=None, │ automute=False )
Note above that the oscillator frequency has been set to the value we specified, 0.2121e9
Hz.