See also
A Jupyter notebook version of this tutorial can be downloaded here.
Make custom device elements#
What is a DeviceElement?#
Introducing the notion of device under test (DUT): DeviceElement enable the use of the concept of DUT (typically a qubit, but not only) in the qblox-scheduler framework, in a structure where their experimental characteristics are serialized: for e.g. qubit frequencies, anharmonicities, coupling strengths… Of course, the relevant characteristics to fill in are depending of the quantum processing unit architecture that one wants to control (e.g. transmons, nv-centers, spins…).
Because the {class}DeviceElement dataclass is making use of pydantic, it inherits from many of its convenient features:
Automatic type checking and validation.
Built-in serialization/deserialization tools.
IDE-friendly auto-completion.
Self-documenting structure.
In this tutorial, we will present how to create and/or edit a {class}DeviceElement representing your a specific hardware components: for example a qubit, a resonator, or a sensor on a chip.
[1]:
from typing import Literal
from qblox_scheduler import BasicSpinElement, SpinEdge
from qblox_scheduler.device_under_test.device_element import DeviceElement
from qblox_scheduler.structure.model import Numbers, Parameter, SchedulerSubmodule
def print_operating_point_value(submodule: SchedulerSubmodule) -> None:
"""Print the values for parameters in a submodule."""
print("==" * 15)
for key, value in submodule.parameters.items():
print(f"{key} = {value}")
Existing DeviceElement#
QBS already provides ready-to-use DeviceElement libraries for several common quantum platforms, including:
Superconducting qubits
Spin qubits
NV center systems
It is also possible to complete this library of built-in DUT with custom instances of the {class}`DeviceElement class, fulfilling the needs of specific setups.
Example : BasicSpinElement#
Let’s take a look at the BasicSpinElement. A device element contains SchedulerSubmodules - a type of data class optimized to work well with the qblox scheduler compiler. To create a well organized data structure, submodules may be nested arbitrarily. For example, the rxy submodule defines key parameters used to implement a logical Rxy gate on your qubit - in this case with a Gaussian pulse.
[2]:
# Usage example of BasicSpinElement
spin = BasicSpinElement("spin")
spin.rxy.amp180 = 0.2
print(f"Amplitude of the X gate = {spin.rxy.amp180}")
print(f"Duration of the X gate = {spin.rxy.duration}s")
Amplitude of the X gate = 0.2
Duration of the X gate = 2e-08s
[3]:
# Serialize and reload
spin.to_json_file(".", add_timestamp=False)
spin = BasicSpinElement.from_json_file("spin.json")
print(f"Amplitude of the X gate = {spin.rxy.amp180}")
Amplitude of the X gate = 0.2
Completing the BasicSpinElement#
Sometimes you may want to include additional parameters or metadata specific to your experiment, for example, calibration constants or scheduling settings. To do this, you can define a custom submodule by subclassing {class}~qblox_scheduler.structure.model.SchedulerSubmodule, and then attach it to your device element (a spin qubit in our case).
Let’s look at an example:
[4]:
class ChargeHopping(SchedulerSubmodule):
"""Submodule containing Parameters for transferring a charge to the next dot."""
duration: float = Parameter(
initial_value=1e-6,
unit="s",
vals=Numbers(min_value=0, max_value=1),
)
"""Duration of Pulse."""
gain: float = Parameter(
initial_value=0,
unit="1",
vals=Numbers(min_value=-1, max_value=1),
)
"""Gain of Pulse. The gain maps the output amplitude via out_amp = gain*max_amp"""
offset: float = Parameter(
initial_value=0.1,
)
"""Additional offset that will be keep on the output at the end of the schedule."""
shape: str = "Square" # Example of a simple field without constraints
class HoppingSpin(BasicSpinElement):
"""New Device Element that represents a Spin qubit that can be transferred to a nearby dot."""
hopping: ChargeHopping
# Usage example
hopping_spin = HoppingSpin("HopSpin0")
print(f"Default hopping duration: {hopping_spin.hopping.duration:.2e} s")
hopping_spin.hopping.duration = 5e-6
print(f"Updated hopping duration: {hopping_spin.hopping.duration:.2e} s")
print(f"Type of `hopping.shape`: {type(hopping_spin.hopping.shape)}")
Default hopping duration: 1.00e-06 s
Updated hopping duration: 5.00e-06 s
Type of `hopping.shape`: <class 'str'>
In the example above, we added a ChargeHopping submodule with four parameters:
durationandgainare Parameters with validators (vals) and units.offsetis a Parameter with only an initial value.shapeis just a plain field, set to"Square".
Parameters introduced with the Parameter function offer rich functionalities: one can assign units, validators, labels, etc. On the other hand plain fields like shape are simple built-in Python attributes: simple to use but do not offer those features.
Additionally you can now also:
Convert your
HoppingSpinto a Python dictionary (.to_dict()).Serialize to JSON/YAML files (
.to_json_file(),.to_yaml_file()).Deserialize from JSON/YAML to reload the object.
[5]:
# Serialization
hopping_spin.to_json_file(".", add_timestamp=False) # Save as a JSON file
hopping_spin.to_yaml_file(".", add_timestamp=False) # Save as a YAML file
# Note: add_timestamp=True creates a new file each time.
# add_timestamp=False will overwrite the existing file.
[5]:
'./HopSpin0.yaml'
[6]:
# Deserialization
hopping_spin = ChargeHopping.from_yaml_file("HopSpin0.yaml")
Introducing Edges#
Edges describe the interface connecting two DeviceElements. Hence, after the loading (or the custom implementation) of those DeviceElement class, the description of a quantum processing unit can be completed by introducing edges between elements. Properties that are shared between elements must be stored in an {class}Edge class instance. to represent their connection.
In the example bellow, we extend the SpinEdge to add parameters for Pauli Spin Blockade (PSB) readout.
[7]:
class OperatingPoints(SchedulerSubmodule):
"""
Submodule defining the operating voltages for a double quantum dot system.
These parameters are used to control the electrostatic environment
during different operation modes (e.g. initialization, control, readout).
"""
parent_voltage: float = Parameter(
initial_value=0,
unit="v",
vals=Numbers(min_value=-1, max_value=1),
)
"""Voltage applied to the parent dot gate (relative to the edge connection)."""
child_voltage: float = Parameter(
initial_value=0,
unit="v",
vals=Numbers(min_value=-1, max_value=1),
)
"""Voltage applied to the child dot gate (relative to the edge connection)."""
barrier_voltage: float = Parameter(
initial_value=0,
unit="v",
vals=Numbers(min_value=-1, max_value=1),
)
"""Voltage applied to the inter-dot barrier gate, controlling tunnel coupling."""
class RampingTimes(SchedulerSubmodule):
"""Ramping times between different operating points."""
init_to_readout: float = Parameter(
initial_value=100e-9,
unit="s",
vals=Numbers(min_value=0),
)
"""Ramp time from initialization to readout."""
readout_to_control: float = Parameter(
initial_value=100e-9,
unit="s",
vals=Numbers(min_value=0),
)
"""Ramp time from readout to control."""
control_to_readout: float = Parameter(
initial_value=100e-9,
unit="s",
vals=Numbers(min_value=0),
)
"""Ramp time from control to readout."""
class PsbEdge(SpinEdge):
"""Custom edge for Spin qubits with PSB configuration."""
init: OperatingPoints
control: OperatingPoints
readout: OperatingPoints
ramps: RampingTimes
# Convenience method for easy access to all connected ports
@property
def port_dict(self) -> dict:
"""Generate the dict to map instrument output to device gates."""
out_dict = {
"parent_voltage": self.parent_element.ports.gate, # type: ignore[reportAttributeAccessIssue]
"child_voltage": self.child_element.ports.gate, # type: ignore[reportAttributeAccessIssue]
"barrier_voltage": self.ports.gate,
}
return out_dict
From the edge, you can now access detailed information to build your PSB schedules, both directly on the edge itself and through the connected parent and child elements.
[8]:
q0 = BasicSpinElement("q0")
q1 = BasicSpinElement("q1")
q0_q1 = PsbEdge(q0, q1)
print(f"Gate associated to the barrier : {q0_q1.ports.gate}")
print(f"Gate associated to the parent qubit : {q0_q1.parent_element.ports.gate}")
print(f"Gate associated to the child qubit : {q0_q1.child_element.ports.gate}")
print_operating_point_value(q0_q1.init) # type: ignore[reportAttributeAccessIssue]
q0_q1.init.parent_voltage = 0.2
q0_q1.init.barrier_voltage = -0.35
print_operating_point_value(q0_q1.init) # type: ignore[reportAttributeAccessIssue]
Gate associated to the barrier : q0_q1:gt
Gate associated to the parent qubit : q0:gt
Gate associated to the child qubit : q1:gt
==============================
parent_voltage = 0
child_voltage = 0
barrier_voltage = 0
==============================
parent_voltage = 0.2
child_voltage = 0
barrier_voltage = -0.35
Create Your Own DeviceElement from Scratch#
Architecture of your QPU may be very different from the pre-made options we provide. In that case, you can build your own DeviceElement completely from scratch.
To do this, you will subclass a new qubit/element from DeviceElement and add the essential submodules such as Ports, Clocks, and submodules you need to store important parameters.
In this example, let’s create a Singlet-Triplet qubit, where the information is encoded in two charges (each with its own spin) delocalized over two quantum dots.
Defining the Ports#
First, we build a submodule containing the ports to which signals will be sent (mapping to physical outputs). Note that those ports must also be added to the Hardware Config, ensuring that compilation routes signals properly.
[9]:
class Ports(SchedulerSubmodule):
"""Submodule containing the ports."""
left_gate: str = ""
"""Name of the element's microwave port."""
j_gate: str = ""
"""Name of the element's flux port."""
right_gate: str = ""
"""Name of the element's readout port."""
def _fill_defaults(self) -> None:
if self.parent:
if not self.left_gate:
self.left_gate = f"{self.parent.name}:lf_gt"
if not self.j_gate:
self.j_gate = f"{self.parent.name}:j_gt"
if not self.right_gate:
self.right_gate = f"{self.parent.name}:rg_gt"
Defining the Clocks Submodules#
In most cases, the second submodule to define is clock_freqs, which corresponds to the mapping to sequencers and modulation frequencies. Nonetheless, for Singlet-Triplet qubits, only baseband pulses are required, which is also the default for most pulses in QBS. In that particular case, this means that we can skip this submodule clock_freqs.
Defining the Other Submodules to Parametrize Schedules#
Besides ports and clocks, you often need additional submodules that capture the parameters of your qubit operations. These submodules define how logical operations (such as rotations) are translated into physical pulses during scheduling.
For Singlet-Triplet qubits, rotations around the Bloch sphere are time-dependent and require explicit calibration. We therefore create dedicated submodules for:
``Rx``: parameters to perform rotations around the X (or Y) axis by turning off the exchange interaction.
``Rz``: parameters to perform rotations around the Z axis by pulsing the exchange interaction.
Each submodule stores not only pulse-related parameters, such as gains and durations, but also key physical quantities, like detunings or magnetic field gradients, which can be used for further calculations when generating schedules.
This makes it straightforward to parametrize your schedules in a structured and reproducible way.
[10]:
class Rx(SchedulerSubmodule):
"""
Submodule containing parameters to perform an Rx(θ) rotation.
Contains all relevant information to perform a Rx($/theta$) by canceling out the exchange.
"""
exchange_off_gain: float = Parameter(
initial_value=0.4,
unit="1",
vals=Numbers(min_value=-1, max_value=1),
)
"""Gain of the Pulse Applied to the J gate in order to turn off the exchange."""
delta_bz: float = Parameter(
initial_value=0,
unit="Tesla",
vals=Numbers(),
)
"""Amplitude of the magnetic gradient in the Z direction."""
pi_duration: float = Parameter(
initial_value=0,
unit="s",
vals=Numbers(min_value=0),
)
"""Time needed to do a pi rotation around the x axis."""
class Rz(SchedulerSubmodule):
"""
Submodule containing parameters to perform an Rz(θ) rotation.
Contains all relevant information to perform a Rz($/theta$) by
doing $J>>/Delta B_z$ The control over J and the tunnel
coupling will be given by the gain applied to the pulse.
The gain will need to be calculated when generating
the schedule to determine the amplitdude t0 play.
"""
epsilon: float = Parameter(
initial_value=0,
unit="V",
vals=Numbers(),
)
"""Voltage detuning at your operating point."""
duration_180: float = Parameter(
initial_value=0,
unit="s",
vals=Numbers(min_value=0),
)
"""Duration of the Pulse to perform a 180 degree rotation."""
Putting It All Together#
Now it’s time to define your qubit! We combine the ports and rotation submodules into a new DeviceElement called SingletTripletQubit.
[11]:
class SingletTripletQubit(DeviceElement):
"""Device Element representing a Single Triplet Qubit."""
element_type: Literal["SingletTripletElement"] = "SingletTripletElement"
ports: Ports
rx: Rx
rz: Rz
def generate_device_config(
self,
) -> None: # Has to be there for the compiler. Not nice for creating your own qubit.
"""Generate the device configuration."""
pass
[12]:
st_qubit = SingletTripletQubit("st0")
st_qubit.ports.left_gate
[12]:
'st0:lf_gt'