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

T1 Time Measurement#
Tutorial Objective#
In this tutorial, we measure the energy relaxation time \(T_1\) of a qubit. The qubit is first initialized in the ground state \(|0\rangle\) and then excited to the \(|1\rangle\) using a calibrated \(\pi\)-pulse (meaning the pulse parameters have been tuned to reliably produce a full population inversion). After a variable delay time \(\tau\), during which the qubit relaxes due to interactions with its environment, a projective measurement is performed. By repeating this procedure for different delay times and fitting the resulting exponential decay of the excited-state population, we extract the relaxation time \(T_1\).
Imports#
[1]:
from __future__ import annotations
from dependencies.psb import OperatingPoints, PsbEdge
from qblox_scheduler import HardwareAgent, Schedule
from qblox_scheduler.operations import (
Measure,
PulseCompensation,
RampPulse,
SquarePulse,
X,
)
from qblox_scheduler.operations.expressions import DType
from qblox_scheduler.operations.loop_domains import arange
Hardware/Device Configuration Files#
We use configuration files in order to describe the hardware properties (e.g. cluster ip, connected modules, output ports) and quantum device properties (charge sensors, qubits, barriers and their associated properties) in one accessible location, respectively. Check the Getting Started Guide for further information.
Based on the information specified in these files, we establish the hardware and device configurations that determine the system’s connectivity.
[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#
In order to run this application example, you will need a quantum device that 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).
Hardware Setup#
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. For more information about the HardwareAgent() you can check the Core concepts of qblox-scheduler page.
[3]:
hw_agent = HardwareAgent(hw_config_path, device_path)
hw_agent.connect_clusters()
/.venv/lib/python3.14/site-packages/qblox_scheduler/qblox/hardware_agent.py:485: UserWarning: cluster0: Trying to instantiate cluster with ip 'None'.Creating a dummy cluster.
warnings.warn(
[4]:
# 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")
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.
Note: Depending on the frequency requirements, the QRM module can be swapped with a QRC or QRM-RF module.
QCM-RF (Slot 6)
\(\text{O}^{1}\): EDSR drive line for
q0andq1.
T1 Time Determination - Schedule & Measurement#
We further introduce a custom device element and two helper functions to be able to move at controlled speeds or hold at certain points of the charge stability diagram:
PSBEdge - A Device Element to simplify moving along the charge stability diagram#
We generate a custom device element (see the Make custom device elements tutorial) to simplify the process of moving along the charge stability diagram for two qubits. The PSBEdge inherits from the SpinEdge class in order to control voltages of the plunger gate ports of the target qubits, as well as the barrier gate between them in one command. The ramp time between points can also be customized. The
functions which allow you to implement this are:
rampCreates a schedule that smoothly ramps the voltages of the plunger gates of the dots from one operating point to another usingRampPulse. The barrier in between the dots can also be ramped if so desired. The duration of the ramp is determined by theRampingTimessubmodule ofPSBEdge.holdCreates a schedule that holds the double dot system at a given operating point for a fixed duration usingSquarePulse. The barrier in between the dots can also be held at a certain voltage point if desired. This is useful when you want to keep the system stable at a specific bias point (e.g., during readout).
Both functions check the parameters defined in the operating points (e.g. control and readout points for qubit and barriers both), fetch the corresponding gate ports, and then generate the appropriate pulses. This further helps with readability and streamlining of the generated schedules.
[5]:
def ramp(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
We define all variables for the experiment schedule in one location for ease of access and modification.
[6]:
repetitions = 2
# Readout via PSB utilizes the transition between the effective (1,1) and (2,0) charge configurations. We denote the two dots as (psb_target, psb_ancilla) to distinguish their roles during the measurement process.
# Out of the two qubits involded (`q0` and `q1`) psb_target is pulsed to shift its state. This is effectively done where both electrons are on one of the dots.
# psb_ancilla participates passively in PSB readout
psb_target = qubit_0 # The qubit which will be the target of drive pulses. It is also one of the two qubits involved in PSB readout
psb_ancilla = qubit_1 # The qubit which will be used as the second partner for PSB readout
q0_q1 = PsbEdge(psb_target, psb_ancilla)
sensor = sensor_0
# We define some time slots we will use in the schedule in advance. You can examine the schedule below to see where they are implemented. Defining these values here allows for easier modification of parameters globally.
init_duration = 300e-9
wait_at_readout_duration = 1e-6
control_duration = 1e-6
# Operating points for the qubits, as extracted from charge stability diagram measurements.
# Note that Cluster output and acquisition parameters are normalized to –1.0…+1.0 in software.
# This is a unitless representation and does not directly equal the actual physical voltages.
# Consult the specific module’s hardware specs to map to real output ranges. For example, QCM output range is +-2.5V.
q0_q1.control.target_gate_voltage = 0.1
q0_q1.control.ancilla_gate_voltage = -0.2
q0_q1.control.barrier_gate_voltage = 0.3
q0_q1.readout.target_gate_voltage = 0.0
q0_q1.readout.ancilla_gate_voltage = 0.0
q0_q1.readout.barrier_gate_voltage = 0.0
q0_q1.init.target_gate_voltage = -0.1
q0_q1.init.ancilla_gate_voltage = 0.25
q0_q1.init.barrier_gate_voltage = 0.01
# Ramp times in ns
q0_q1.ramps.init_to_readout = 5e-6
q0_q1.ramps.readout_to_control = 150e-9
q0_q1.ramps.control_to_readout = 2e-6
q0_q1.ramps.control_to_init = 500e-9
# Add the edge with defined parameters to the Hardware Agent
hw_agent.quantum_device.add_edge(q0_q1)
Using the PSBEdge and the helper functions defined above, we generate a schedule for implementing T1 measurement. An overview of the entire pulse schedule can be summarized as the following:
Pulse the dots and the barrier at the initialization point
Perform an initial measurement at the initialization point. Store the data in
acq_channel0.Ramp to readout point
Perform a second measurement at the readout point. Store the data in
acq_channel1.Ramp to control point
Apply a \(\pi\) pulse to the target qubit.
Wait for a variable ‘delay_time’ after the \(\pi\) pulse.
Ramp to readout point
Perform a third measurement at the readout point. Store the data in
acq_channel2.
Finally, we add pulse compensation to the designated ports of the entire schedule. This corrects the pulse decay caused by the Bias-T, ensuring the qubit receives the intended signal. For more information, refer to the Correcting for signal distortions due to Bias Tee application example.
[7]:
compensated_schedule = Schedule("pulse_compensation")
# The following schedule sweeps over the set amount of repetitions, as well as a range of delay times (after the $\pi$ pulse) set by the user.
with (
compensated_schedule.loop(arange(0, repetitions, 1, DType.NUMBER)),
compensated_schedule.loop(arange(20e-9, 8e-6, 800e-9, dtype=DType.TIME)) as delay_time,
):
T1_schedule = Schedule("schedule")
# INITIALIZATION
ref_pulse_init = T1_schedule.add(
hold(q0_q1.init, sensor.measure.pulse_duration + init_duration)
)
T1_schedule.add(
Measure(sensor.name, acq_channel=0),
ref_op=ref_pulse_init,
ref_pt="start",
rel_time=init_duration,
)
# Note: Only acq_channel1 and acq_channel2 are required for PSB readout; acq_channel0 is mainly a diagnostic. Comparing acq_channel0 with acq_channel1 helps identify whether issues stem from improper initialization or from a mispositioned readout point.
# RAMP TO READOUT POINT
ref_readout_ramp = T1_schedule.add(ramp(q0_q1.init, q0_q1.readout))
# Wait at the readout point while pulsing the gate ports of both qubits and the barrier
ref_hold = T1_schedule.add(hold(q0_q1.readout, wait_at_readout_duration))
# Do a measurement at the readout point
ref_pulse_wait = T1_schedule.add(
hold(
q0_q1.readout,
sensor.measure.pulse_duration,
),
ref_op=ref_hold,
)
T1_schedule.add(
Measure(sensor.name, acq_channel=1),
ref_op=ref_pulse_wait,
ref_pt="start",
rel_time=50e-9,
)
# RAMP TO CONTROL POINT
ref_control_ramp = T1_schedule.add(
ramp(
q0_q1.readout,
q0_q1.control,
),
ref_op=ref_pulse_wait,
)
ref_control = T1_schedule.add(hold(q0_q1.control, control_duration))
# DRIVE PULSE
# Apply a pi pulse
T1_schedule.add(
X(psb_target.name),
ref_op=ref_control,
ref_pt="end",
)
ref_T1_delay = T1_schedule.add(hold(q0_q1.control, delay_time))
# RAMP BACK TO READOUT POINT
ref_readout_ramp = T1_schedule.add(ramp(q0_q1.control, q0_q1.readout), ref_op=ref_T1_delay)
# Wait at the readout point while pulsing the gate ports of both qubits and the barrier
ref_hold = T1_schedule.add(hold(q0_q1.readout, wait_at_readout_duration))
# Do a measurement at the readout point
ref_pulse_wait = T1_schedule.add(
hold(q0_q1.readout, sensor.measure.pulse_duration),
ref_op=ref_hold,
)
T1_schedule.add(
Measure(sensor.name, acq_channel=2),
ref_op=ref_pulse_wait,
ref_pt="start",
rel_time=50e-9,
)
compensated_schedule.add(
PulseCompensation(
T1_schedule,
max_compensation_amp={
"q0:gt": 0.1, # value depends on set-up
"q1:gt": 0.1, # value depends on set-up
"q0_q1:gt": 0.58, # value depends on set-up
},
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.
[8]:
t1_ds = hw_agent.run(compensated_schedule)
t1_ds
[8]:
<xarray.Dataset> Size: 960B
Dimensions: (acq_index_0: 10, acq_index_1: 10, acq_index_2: 10)
Coordinates:
* acq_index_0 (acq_index_0) int64 80B 0 1 2 3 4 5 6 7 8 9
loop_repetition_0 (acq_index_0) float64 80B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
* acq_index_1 (acq_index_1) int64 80B 0 1 2 3 4 5 6 7 8 9
loop_repetition_1 (acq_index_1) float64 80B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
* acq_index_2 (acq_index_2) int64 80B 0 1 2 3 4 5 6 7 8 9
loop_repetition_2 (acq_index_2) float64 80B 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0
Data variables:
0 (acq_index_0) complex128 160B (nan+nanj) ... (nan+nanj)
1 (acq_index_1) complex128 160B (nan+nanj) ... (nan+nanj)
2 (acq_index_2) complex128 160B (nan+nanj) ... (nan+nanj)
Attributes:
tuid: 20260402-064954-025-80a1c7You may plot the pulse diagram of a schedule for verification. Below, we plot a pulse diagram of 2 repetitions and a delay time (after the \(\pi\) pulse) sweep of 10 points between \(20e^{-9}\) and \(8e^{-6}\), as we have defined in the above sections of this script. In order to inspect the pulse schedule in more detail, you may reduce the number of repetitions and set a fewer number of delay_times.
For example, if you set repetitions to 1, and reduce the delay_times to a length of 1, you will observe only one cycle of the pulse sequence. In this manner, it will be easier to check and verify the details of the core pulse cycle. For example, you can check whether the timing of pulses are correct with respect to one another. Once you are convinced that your pulse sequence is correct, you can increase the number of the delay_time parameters that you will loop over in order to
check the behavior of the pulse parameter you sweep over time. As a last step, you can increase the number of repetitions to the desired value.
[9]:
hw_agent.compile(compensated_schedule).plot_pulse_diagram(plot_backend="plotly")
The quantum device settings can be saved after every experiment for allowing later reference into experiment settings.
[10]:
hw_agent.quantum_device.to_json_file("./dependencies/configs", add_timestamp=True)
[10]:
'./dependencies/configs/spin_with_psb_device_config_2026-04-02_06-49-57_UTC.json'