Quantum Elements¶
It is useful to think of the quantum device we are controlling in terms of components such as qubits, couplers or TWPAs (Travelling Wave Parametric Amplifiers). This allows us to define experiments in terms of operations on these components, rather than having to always think in terms of actions on individual signal lines.
In LabOne Q, these components are modelled using the dsl.QuantumElement class.
A QuantumElement consists of:
- a set of logical signal lines used to control and/or measure the component, and
- a set of parameters for controlling the component
Each logical signal line is associated with a name that specifies its function.
For example, a transmon qubit might have a signal named drive that is mapped to the logical signal q0/drive that is used to drive the G-E transition of the qubit and a parameter named resonance_frequency_ge that specifies the frequency of the G-E transition in Hz.
QuantumElement is a base class that contains only the core functionality needed to describe a quantum component. Each group using LabOne Q will likely want create their own sub-class of QuantumElement that describes their own components.
In this tutorial, we'll go through everything provided by the QuantumElement base class and show you how to go about writing your own.
In addition to the QuantumElement class, LabOne Q provides two sub-classes:
Transmon: A demonstration transmon component.TunableTransmon: A tunable transmon component regularly tested on real tunable transmons.
You should not use either of these classes in your own experiments but you are welcome to copy them and use them as the starting point for defining your own quantum components.
No tunable transmons were harmed during the writing of this tutorial.
Imports¶
from __future__ import annotations
# Import required packages
from laboneq.contrib.example_helpers.generate_device_setup import (
generate_device_setup,
)
import laboneq.serializers
A first look at a quantum element¶
Let's start by taking a look at the demonstration Transmon quantum element.
from laboneq.simple import Transmon
To create a Transmon we need to specify a uid and the map from the signal roles to the corresponding logical signal paths for the particular qubit:
q0 = Transmon(
uid="q0",
signals={
"drive": "q0/drive",
"measure": "q0/measure",
"acquire": "q0/acquire",
},
)
The list of required signal roles is available via the REQUIRED_SIGNALS attribute of the class. Required signal roles must be supplied when the qubit is created:
Transmon.REQUIRED_SIGNALS
There is also a list of optional signal roles. For Transmon these are:
Transmon.OPTIONAL_SIGNALS
Let's print out the qubit we just created. We didn't supply any parameters when we created q0 so the parameter values are the defaults:
q0
We can also access the parameters individually:
q0.parameters.drive_range
Or print out just the parameters
q0.parameters
The uid and signals can also be accessed directly:
q0.uid
q0.signals
Parameter values can be supplied when a qubit is created:
q0 = Transmon(
uid="q0",
signals={
"drive": "q0/drive",
"measure": "q0/measure",
"acquire": "q0/acquire",
},
parameters={
"resonance_frequency_ge": 5.0e9,
},
)
q0
Or replaced, which creates a new qubit with the updated parameters:
q0_custom = q0.replace(resonance_frequency_ge=5.1e9)
q0_custom
If you need a copy of the qubit with the same parameters, you can call .copy:
q0_copy = q0.copy()
q0_copy
Or, if needed, you can create a copy of just the parameters:
q0_parameters_copy = q0.parameters.copy()
q0_parameters_copy
Lastly, qubit parameters can be modified in-place using .update. Using .replace is preferred because places where a reference to a qubit is held might not be expecting it to change, but sometimes one wants to really modify an existing qubit. For example, one might wish to update the parameters of the quantum elements in a QPU. One uses .update as follows:
q0.update(resonance_frequency_ge=5.2e9)
q0
Lastly, if one requires a parameters object by itself, one may be created using .create_parameters either on the quantum element class:
params = Transmon.create_parameters(resonance_frequency_ef=6.7e9)
params
Or directly on a quantum element instance:
params = q0.create_parameters(resonance_frequency_ef=6.7e9)
params
Saving and loading quantum elements¶
Quantum elements may be saved and loaded using LabOne Q's serializers from laboneq.serializers. Let's import the serializers and save our transmon qubit:
laboneq.serializers.save(q0, "q0.json")
And we can load it back using .load:
q0_loaded = laboneq.serializers.load("q0.json")
q0_loaded
If you write your own quantum element class, you will still be able to save and load it using .save and .load. See the section on writing your own QuantumElement below.
Loading quantum elements from before LabOne Q 2.44¶
Prior to LabOne Q 2.44, quantum elements were saved and loaded using LabOne Q's previous serializer which did not support saving and loading custom QuantumElements. It only supported saving and loading the built-in Qubit and Transmon classes.
If you saved Qubit or Transmon instances with LabOne Q 2.43 or earlier, you can load them as follows:
- Install LabOne Q 2.43.
- Load the
QubitorTransmoninstance using, e.g.,q = Transmon.load(...). - Save the
QubitorTransmoninstance usinglaboneq.serializers.save(q, ...). - Install LabOne Q 2.44 or later.
- Load the
QubitorTransmoninstance you just saved usingq = laboneq.serializers.load(...).
Creating quantum elements from a device setup¶
It is common to define a logical signal group for each quantum element in one's device setup using the following convention:
- The UID of the logical signal group is the UID of the quantum element, e.g.
q0. - For each logical signal in the group, the name of the logical signal is the signal's role within the quantum element, e.g.
drive,measure,acquire.
If you follow the above convention, the QuantumElement base class provides two methods to allow you to create your quantum elements from the logical signal groups:
- from_device_setup(...): Returns a
QuantumElementfor each logical signal group in the device setup. - from_logical_signal_group(...): Returns a single
QuantumElementfor the given logical signal group.
We'll see how to use these two methods below, but first we need to create a device setup:
# specify the number of qubits you want to use
number_of_qubits = 2
# generate the device setup and the qubit objects using a helper function
device_setup = generate_device_setup(
number_qubits=number_of_qubits,
shfqc=[
{
"serial": "DEV12001",
"zsync": 1,
"number_of_channels": 6,
"readout_multiplex": 6,
"options": None,
}
],
include_flux_lines=False,
server_host="localhost",
setup_name=f"my_{number_of_qubits}_fixed_qubit_setup",
)
Now that we have a device setup, we can load all the qubits from it:
qubits = Transmon.from_device_setup(device_setup)
qubits
Note that the class used, in this case Transmon, must match the kind of quantum element described by your device setup.
If you wish, you may specify parameters for each qubit using the parameters argument to from_device_setup. The parameters argument accepts a dictionary that maps quantum element UIDs to the parameters for that element, like so:
qubits = Transmon.from_device_setup(
device_setup,
parameters={
"q0": {
"resonance_frequency_ge": 5.1e9,
"drive_lo_frequency": 5.0e9,
},
"q1": {
"resonance_frequency_ge": 5.2e9,
"drive_lo_frequency": 5.0e9,
},
},
)
qubits
Alternatively, you might wish to load qubits from the devices setup individually using from_logical_signal_group:
q1_signal_group = device_setup.logical_signal_groups["q1"]
q1 = Transmon.from_logical_signal_group(q1_signal_group.uid, q1_signal_group)
Here too the class used, i.e. Transmon, must match the kind of quantum element described by the logical signal group.
The first parameter specifies the UID of the qubit. You may choose to give it a UID that is different to that of the logical signal group.
You may also choose to pass parameters for the quantum element using the parameters argument.
Experiment signals and calibration¶
Once we have quantum element objects, we need to use them in our experiments. We saw in the previous tutorial how we can write quantum operations.
QuantumElement classes also provide a list of experiment signals and qubit calibration (which can be used either as experiment signal calibration or device calibration).
The .experiment_signals method lists the experiment signals used by the qubit and the logical signal they are mapped to:
q0.experiment_signals()
The .calibration method needs to be implemented by each kind of QuantumElement. The default method on the QuantumElement class returns an empty set of calibration. The calibration returned typically depends on the quantum element parameters. Here is the default calibration returned by the Transmon class:
q0.calibration()
Writing your own QuantumElement¶
LabOne Q includes the QuantumElement base class and the Transmon class, but you'll want to write your own QuantumElement sub-class for your own qubits. In this section we'll show you how.
Before starting to code your class, you should think about:
- What signals are connected to the quantum element?
- What parameters are needed to calibrate it?
When thinking about the signals and parameters, it might be useful to think about what operations you'd like to perform on these elements and how they will be calibrated.
Once you know the set of signals each element will have, you can start writing your quantum element class:
Specifying the signal roles¶
Once you know the set of signals each element will have, you can start writing your quantum element class:
import attrs
from laboneq.simple import QuantumElement
@attrs.define()
class MiniTransmon(QuantumElement):
REQUIRED_SIGNALS = (
"acquire",
"drive",
"measure",
)
OPTIONAL_SIGNALS = ()
SIGNAL_ALIASES = {}
Let's go through what we've written above. First, the boilerplate:
import attrs:QuantumElements are written using the attrs library. Theattrslibrary was the inspiration for Python'sdataclasses. It addition it provides validation for the fields of the class.@attrs.define(): This marks our new class as anattrsclass.class MiniTransmon(QuantumElement): Our class,MiniTransform, inherits fromQuantumElement.
The boilerplate will remain the same. You need to decide on the signals:
REQUIRED_SIGNALS: This tuple lists the names of the signal line roles that must always be present when your quantum element is instantiated.OPTIONAL_SIGNALS: And this lists the optional signal roles. These may or may not be present.
In our example, the required signal roles are acquire, drive and measure. For simplicity we've left the list of optional signals blank.
We can also add SIGNAL_ALIASES, which provide alternative names for the signal roles. These allow for backward compatibility with existing signal names. For example, if in the past we have called the drive role drive_line we could add:
SIGNAL_ALIASES = {
"drive_line": "drive",
}
Unless you specifically need such aliases, just leave them blank as we did above.
Define the parameters¶
With the signals defined, the next step is to define our parameters. Here we will only define two parameters. In practice there may be many more.
The parameters are specified on a separate class that inherits from QuantumParameters. We will then attach it to the class we wrote above by adding PARAMETERS_TYPE = MiniTransmonParameters to the class.
Here is our MiniTransmonParameters class:
from laboneq.simple import QuantumParameters
@attrs.define(kw_only=True)
class MiniTransmonParameters(QuantumParameters):
"""MiniTransmon parameters.
Attributes
----------
resonance_frequency_ge:
The resonance frequency of the 0-1 transition (Hz).
drive_lo_frequency:
The frequency of the drive signal local oscillator (Hz).
"""
resonance_frequency_ge: float | None = None
drive_lo_frequency: float | None = None
We'll go through it in detail as we did for the MiniTransmon class above:
@attrs.define(kw_only=True): The quantum parameters are also anattrsclass. Here we passkw_only=Truewhich prevents parameters being passed as positional arguments when the class is instantiated. This prevents accidentally relying on the parameter ordering.class MiniTransmonParameters(QuantumParameters): OurMiniTransformParametersinherit fromQuantumParameters.
We've nicely documented our parameters in the class docstring (highly recommended) and then defined them in the class body using:
resonance_frequency_ge: float | None = None
drive_lo_frequency: float | None = None
Since these have type annotations, attrs will automatically add them to the parameter class it creates.
To bring everything together we add the PARAMETERS_TYPE = MiniTransmonParameters to the definition of our TransmonParameters class:
@attrs.define()
class MiniTransmon(QuantumElement):
PARAMETERS_TYPE = MiniTransmonParameters
REQUIRED_SIGNALS = (
"acquire",
"drive",
"measure",
)
OPTIONAL_SIGNALS = ()
SIGNAL_ALIASES = {}
Now let's try it out:
t0 = MiniTransmon(
uid="t0",
signals={
"acquire": "t0/acquire",
"drive": "t0/drive",
"measure": "t0/measure",
},
parameters={
"resonance_frequency_ge": 5.1e9,
"drive_lo_frequency": 5.0e9,
},
)
t0
Our MiniTransmon has the following experiment signals and parameters:
t0.experiment_signals()
t0.parameters
Notice that there is an additional parameter called custom. This is automatically provided by the QuantumParameters class as a way of storing on the fly a dictionary of custom parameters with attribute-style access. This provides additional flexibility for prototyping and testing. You can simply add a new custom parameter as follows:
t0.parameters.custom.my_new_parameter = 10
t0.parameters
Notice also that the calibration is still empty because we haven't defined it yet:
t0.calibration()
Let's write a calibration method.
Writing a calibration method¶
The only method you need to write yourself for your quantum element class is .calibration(). This should return a Calibration holding the required calibration for each signal line used by the quantum element.
In the example below, we return just calibration for the drive line. A completely implementation would likely also return calibration for the other signal lines.
from laboneq.simple import Calibration, ModulationType, Oscillator, SignalCalibration
@attrs.define()
class MiniTransmon(QuantumElement):
PARAMETERS_TYPE = MiniTransmonParameters
REQUIRED_SIGNALS = (
"acquire",
"drive",
"measure",
)
OPTIONAL_SIGNALS = ()
SIGNAL_ALIASES = {}
def calibration(self) -> Calibration:
"""Calibration for the MiniTransmon."""
# define the local oscillator if `drive_lo_frequency` was specified:
if self.parameters.drive_lo_frequency is not None:
drive_lo = Oscillator(
uid=f"{self.uid}_drive_local_osc",
frequency=self.parameters.drive_lo_frequency,
)
else:
drive_lo = None
# calculate the drive line RF frequency:
if (
self.parameters.drive_lo_frequency is not None
and self.parameters.resonance_frequency_ge is not None
):
drive_rf_frequency = (
self.parameters.resonance_frequency_ge
- self.parameters.drive_lo_frequency
)
else:
drive_rf_frequency = None
calibration = {}
# define the drive signal calibration:
sig_cal = SignalCalibration()
if drive_rf_frequency is not None:
sig_cal.oscillator = Oscillator(
uid=f"{self.uid}_drive_ge_osc",
frequency=drive_rf_frequency,
modulation_type=ModulationType.AUTO,
)
sig_cal.local_oscillator = drive_lo
calibration[self.signals["drive"]] = sig_cal
return Calibration(calibration)
Things to note in the implementation above:
- If parameters are optional, we only create the corresponding calibration entries if the parameters are present.
- We build up a set of calibration in the
calibrationdictionary. The keys of this dictionary are the logical signal names. That is, for example,self.signals["drive"]and not simply"drive". The values are instances ofSignalCalibration.
Let's create an instance of our new MiniTransmon with calibration:
t0 = MiniTransmon(
uid="t0",
signals={
"acquire": "t0/acquire",
"drive": "t0/drive",
"measure": "t0/measure",
},
parameters={
"resonance_frequency_ge": 5.1e9,
"drive_lo_frequency": 5.0e9,
},
)
t0
Add examine the calibration:
t0.calibration()
If we remove the drive_lo_frequency from the parameters, the calibration for the drive line will no longer be defined:
t0_without_lo = t0.replace(drive_lo_frequency=None)
t0_without_lo.calibration()
Parameter validation¶
If you wish to provide validation of your parameters, the attrs library provides great support for adding it. You can learn how to do this in the attrs validation guide.