See also
A Jupyter notebook version of this tutorial can be downloaded here.

07 : Single Qubit Spectroscopy#
Tutorial Objective#
The objective of this application example is to carry out qubit spectroscopy on a single quantum dot to find the \(\lvert \downarrow \rangle \rightarrow \lvert \uparrow \rangle\) Larmor frequency.
Imports
[1]:
from __future__ import annotations
import numpy as np
import rich # noqa:F401
from dependencies.psb import OperatingPoints, PsbEdge
from qblox_scheduler import HardwareAgent, Schedule
from qblox_scheduler.operations import (
Measure,
PulseCompensation,
RampPulse,
SetClockFrequency,
SquarePulse,
)
Hardware/Device Configuration Files#
We use JSON files in order to set the configurations for different parts of the whole system.
Hardware configuration
The hardware configuration file contains the cluster IP and the type of modules in that specific cluster (by cluster slot). Options such as the output attenuations, mixer corrections or LO frequencies can be fixed inside this file and the cluster will adapt to these settings when initialized. Hardware connectivities are also described here: Each module’s output is directly connected to the corresponding device port on the chip, allowing the software to address device elements directly and eliminating an extra layer of complexity for the user.
Device configuration
The device configuration file defines each quantum element and its associated properties. In this case, the basic spin elements are qubits, whilst the charge sensor element is a sensor and edges can be defined as barriers between the dots. As can be observed in this file, each element contains several key properties that can be pre-set in the file, or from within the Jupyter notebook (e.g. sensor.measure.pulse_amp(0.5)). Please have a quick look through these properties and change them as suited to your device, if needed. Some of the typically important properties are: acq_delay, integration_time, and clock_freqs. You may also adjust the default pulse amplitudes and pulse durations for a given element here, or may define additional elements as needed.
Hardware configuration
The hardware configuration file contains the cluster IP and the type of modules in that specific cluster (by cluster slot). Options such as the output attenuations, mixer corrections or LO frequencies can be fixed inside this file and the cluster will adapt to these settings when initialized. Hardware connectivities are also described here: Each module’s output is directly connected to the corresponding device port on the chip, allowing the software to address device elements directly and eliminating an extra layer of complexity for the user.
Device configuration
The device configuration file defines each quantum element and its associated properties. In this case, the basic spin elements are qubits, whilst the charge sensor element is a sensor and edges can be defined as barriers between the dots. As can be observed in this file, each element contains several key properties that can be pre-set in the file, or from within the Jupyter notebook (e.g. sensor.measure.pulse_amp(0.5)). Please have a quick look through these properties and change them as suited to your device, if needed. Some of the typically important properties are: acq_delay, integration_time, and clock_freqs. You may also adjust the default pulse amplitudes and pulse durations for a given element here, or may define additional elements as needed.
Using the information specified in these files, we set the hardware and device configurations which determines the connectivity of our system.
[2]:
hw_config_path = "dependencies/configs/tuning_spin_coupled_pair_hardware_config.json"
device_path = "dependencies/configs/spin_with_psb_device_config_2q.yaml"
Experimental Setup#
To run the tutorial, you will need a quantum device consists of a double quantum dot array (q0 and q1), with a charge sensor (cs0) connected to reflectometry readout. The DC voltages of the quantum device also need to be properly tuned. For example, reservoir gates need to be ramped up for the accumulation devices. The charge sensor can be a quantum dot, quantum point contact (QPC), or single electron transistor (SET).
The HardwareAgent() is the main object for Qblox experiments. It provides an interface to define the quantum device, set up hardware connectivity, run experiments, and receive results.
[3]:
hw_agent = HardwareAgent(hw_config_path, device_path)
hw_agent.connect_clusters()
# Device name string should be defined as specified in the hardware configuration file
sensor_0 = hw_agent.quantum_device.get_element("cs0")
qubit_0 = hw_agent.quantum_device.get_element("q0")
qubit_1 = hw_agent.quantum_device.get_element("q1")
hw_opts = hw_agent.hardware_configuration.hardware_options
cluster = hw_agent._clusters["cluster0"].cluster
/builds/0/.venv/lib/python3.10/site-packages/qblox_scheduler/qblox/hardware_agent.py:460: UserWarning: cluster0: Trying to instantiate cluster with ip 'None'.Creating a dummy cluster.
warnings.warn(
As can be observed from the defined quantum devices and the hardware connectivies of these elements, the relevant modules and connections for this tutorial are:
QCM (Module 2):
\(\text{O}^{1}\): Gate connection line for qubit 0 (
q0).\(\text{O}^{2}\): Gate connection line for qubit 1 (
q1).
QRM (Module 4):
\(\text{O}^{1}\) and \(\text{I}^{1}\): Charge sensor (
cs0) resonator probe and readout connection.\(\text{O}^{2}\) : Charge sensor (
cs0) plunger gate connection.
QCM-RF (Slot 6)
\(\text{O}^{1}\): EDSR drive line for
q0andq1.
Qubit Spectroscopy - Schedule & Measurement#
We define all variables for the experiment schedule in one location for ease of access and modification.
[4]:
repetitions = 100
target_qubit = qubit_0
readout_pair_qubit = qubit_1
q0_q1 = PsbEdge(target_qubit, readout_pair_qubit)
center_frequency = 5.3e9
frequency_width = 10e6
frequencies = np.linspace(5.3e9, 5.4e9, 5)
sensor = sensor_0
clock = target_qubit.name + ".f_larmor"
port = target_qubit.ports.microwave
init_duration = 300e-9
idle_duration = 50e-9
wait_duration = 1e-6
control_duration = 1e-6
sensor = sensor_0
q0_q1.control.parent_voltage = 0.1
q0_q1.control.child_voltage = -0.2
q0_q1.control.barrier_voltage = 0.3
q0_q1.readout.parent_voltage = 0.0
q0_q1.readout.child_voltage = 0.0
q0_q1.readout.barrier_voltage = 0.0
q0_q1.init.parent_voltage = -0.1
q0_q1.init.child_voltage = 0.25
q0_q1.init.barrier_voltage = 0.01
hw_agent.quantum_device.add_edge(q0_q1)
We further introduce two helper functions to be able to move along or hold at certain points of the charge stability diagram:
``ramps`` Creates a schedule that smoothly ramps the voltages from one operating point to another using
RampPulse. The duration of the ramp is determined by theRampingTimessubmodule of thePSBEdge.``hold`` Creates a schedule that holds the system at a given operating point for a fixed duration using
SquarePulse. This is useful when you want to keep the system stable at a specific bias point (e.g., during readout).
Both functions loop over the parameters defined in the operating points (e.g. control and readout points and both for qubits and barrier gates), fetch the corresponding hardware ports, and then generate the appropriate pulses. This further helps with readability and streamlining of the generated schedules.
[5]:
def ramps(op_start: OperatingPoints, op_end: OperatingPoints) -> Schedule:
"""Generate a schedule to ramp gate voltages between two PSB operating points."""
parent = op_start.parent
port_dict = parent.port_dict
ramp_time = parent.ramps.to_dict()[f"{op_start.name}_to_{op_end.name}"]
ops = op_start.to_dict()
ope = op_end.to_dict()
sched = Schedule("")
ref_op = None
for key in op_start.parameters:
amplitude = ope[key] - ops[key]
offset = ops[key]
ref_op = sched.add(
RampPulse(amp=amplitude, offset=offset, duration=ramp_time, port=port_dict[key]),
ref_pt="start",
ref_op=ref_op,
)
return sched
def hold(op_point: OperatingPoints, duration: float) -> Schedule:
"""Generate a schedule to hold gate voltages at a given PSB operating point."""
parent = op_point.parent
port_dict = parent.port_dict
sched = Schedule("")
ref_op = None
for key in op_point.parameters:
ref_op = sched.add(
SquarePulse(amp=op_point.to_dict()[key], duration=duration, port=port_dict[key]),
ref_pt="start",
ref_op=ref_op,
)
return sched
Using the PSBEdge and the helper functions defined above, we generate a schedule for implementing a single qubit spectroscopy experiment.
[6]:
schedule = Schedule("schedule", repetitions=1)
compensated_schedule = Schedule("pulse_compensation", repetitions=1)
for spec_pulse_freq in frequencies:
schedule = Schedule("inner schedule")
# INITIALIZATION
ref_pulse_init = schedule.add(hold(q0_q1.init, sensor.measure.pulse_duration + init_duration))
schedule.add(
Measure(sensor.name, acq_channel=0),
ref_op=ref_pulse_init,
ref_pt="start",
rel_time=init_duration,
)
# RAMP TO READOUT POINT
# Move to readout point whilst sweeping this point via the override dict
ref_readout_ramp = schedule.add(ramps(q0_q1.init, q0_q1.readout), ref_op=ref_pulse_init)
# Wait at the readout point while pulsing the gate ports of both qubits and the barrier
ref_hold = schedule.add(hold(q0_q1.readout, wait_duration))
# Do a measurement at the readout point
ref_pulse_wait = schedule.add(
hold(
q0_q1.readout,
sensor.measure.pulse_duration + idle_duration,
),
ref_op=ref_readout_ramp,
)
schedule.add(
Measure(sensor.name, acq_channel=1),
ref_op=ref_pulse_wait,
ref_pt="start",
rel_time=idle_duration,
)
# RAMP TO CONTROL POINT
ref_control_ramp = schedule.add(
ramps(
q0_q1.readout,
q0_q1.control,
),
ref_op=ref_hold,
)
schedule.add(hold(q0_q1.control, control_duration))
# DRIVE PULSE
schedule.add(SetClockFrequency(clock=clock, clock_freq_new=spec_pulse_freq))
schedule.add(
SquarePulse(amp=0.1, duration=control_duration, port=port, clock=clock),
ref_op=ref_control_ramp,
rel_time=4e-9,
)
# RAMP BACK TO READOUT POINT
ref_readout_ramp = schedule.add(ramps(q0_q1.control, q0_q1.readout))
# Wait at the readout point while pulsing the gate ports of both qubits and the barrier
schedule.add(hold(q0_q1.readout, wait_duration))
# Do a measurement at the readout point
ref_pulse_wait = schedule.add(
hold(q0_q1.readout, sensor.measure.pulse_duration + idle_duration),
ref_op=ref_readout_ramp,
)
schedule.add(
Measure(sensor.name, acq_channel=2),
ref_op=ref_pulse_wait,
ref_pt="start",
rel_time=idle_duration,
)
compensated_schedule.add(
PulseCompensation(
schedule,
max_compensation_amp={
"q0:gt": 0.1,
"q1:gt": 0.1,
"q0_q1:gt": 0.58,
},
time_grid=4e-9,
sampling_rate=1e9,
)
)
Now, we will run the schedule. See the documentation on the QRM Module and Readout Sequencers for information on how the signal is processed upon acquisition.
[7]:
qubit_spec_ds = hw_agent.run(compensated_schedule)
qubit_spec_ds
[7]:
<xarray.Dataset> Size: 360B
Dimensions: (acq_index_0: 5, acq_index_1: 5, acq_index_2: 5)
Coordinates:
* acq_index_0 (acq_index_0) int64 40B 0 1 2 3 4
* acq_index_1 (acq_index_1) int64 40B 0 1 2 3 4
* acq_index_2 (acq_index_2) int64 40B 0 1 2 3 4
Data variables:
0 (acq_index_0) complex128 80B (nan+nanj) ... (nan+nanj)
1 (acq_index_1) complex128 80B (nan+nanj) ... (nan+nanj)
2 (acq_index_2) complex128 80B (nan+nanj) ... (nan+nanj)
Attributes:
tuid: 20251030-003728-568-9c27faThe quantum device settings can be saved after every experiment for allowing later reference into experiment settings.
[8]:
hw_agent.quantum_device.to_json_file("./dependencies/configs", add_timestamp=True)
[8]:
'./dependencies/configs/spin_with_psb_device_config_2025-10-30_00-37-28_UTC.json'