See also

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

banner

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
from dependencies.analysis_utils import PSBQubitSpectroscopyAnalysis
from dependencies.psb import OperatingPoints, PsbEdge
from dependencies.simulated_data import get_qubit_spectroscopy_data

from qblox_scheduler import HardwareAgent, Schedule
from qblox_scheduler.enums import BinMode
from qblox_scheduler.operations import (
    Measure,
    PulseCompensation,
    RampPulse,
    SetClockFrequency,
    SquarePulse,
)
/.venv/lib/python3.14/site-packages/quantify_core/utilities/general.py:13: QCoDeSDeprecationWarning: The `qcodes.utils.helpers` module is deprecated. Please consult the api documentation at https://microsoft.github.io/Qcodes/api/index.html for alternatives.
  from qcodes.utils.helpers import NumpyJSONEncoder

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()

# 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
/.venv/lib/python3.14/site-packages/qblox_scheduler/qblox/hardware_agent.py:499: 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 q0 and q1.

Qubit Spectroscopy - Schedule & Measurement#

We define all variables for the experiment schedule in one location for ease of access and modification.

[4]:
repetitions = 2000
target_qubit = qubit_0
readout_pair_qubit = qubit_1
q0_q1 = PsbEdge(target_qubit, readout_pair_qubit)
frequencies = np.linspace(4.95e9, 5.05e9, 31)
sensor = sensor_0
clock = target_qubit.name + ".f_larmor"
port = target_qubit.ports.microwave
init_duration = 300e-9
wait_at_readout_duration = 1e-6
control_duration = 1e-6
sensor = sensor_0

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
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

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:

  1. ``ramp`` Creates a schedule that smoothly ramps the voltages from one operating point to another using RampPulse. The duration of the ramp is determined by the RampingTimes submodule of the PSBEdge.

  2. ``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 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

Using the PSBEdge and the helper functions defined above, we generate a schedule for implementing a single qubit spectroscopy experiment.

[6]:
compensated_schedule = Schedule("pulse_compensation", repetitions=repetitions)

for spec_pulse_freq in frequencies:
    sqs_schedule = Schedule("single qubit spectroscopy schedule")

    # INITIALIZATION
    ref_pulse_init = sqs_schedule.add(
        hold(q0_q1.init, sensor.measure.pulse_duration + init_duration)
    )
    sqs_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

    # Move to readout point whilst sweeping this point via the override dict
    ref_readout_ramp = sqs_schedule.add(ramp(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 = sqs_schedule.add(hold(q0_q1.readout, wait_at_readout_duration))

    # Do a measurement at the readout point
    ref_pulse_wait = sqs_schedule.add(
        hold(
            q0_q1.readout,
            sensor.measure.pulse_duration,
        ),
        ref_op=ref_hold,
        ref_pt="end",
    )
    sqs_schedule.add(
        Measure(sensor.name, acq_channel=1, bin_mode=BinMode.APPEND),
        ref_op=ref_pulse_wait,
        ref_pt="start",
    )

    # RAMP TO CONTROL POINT
    ref_control_ramp = sqs_schedule.add(
        ramp(
            q0_q1.readout,
            q0_q1.control,
        ),
        ref_op=ref_pulse_wait,
    )

    sqs_schedule.add(hold(q0_q1.control, control_duration))
    # DRIVE PULSE
    sqs_schedule.add(SetClockFrequency(clock=clock, clock_freq_new=spec_pulse_freq))
    sqs_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
    sqs_schedule.add(ramp(q0_q1.control, q0_q1.readout))

    # Wait at the readout point while pulsing the gate ports of both qubits and the barrier
    ref_hold = sqs_schedule.add(hold(q0_q1.readout, wait_at_readout_duration))

    # Do a measurement at the readout point
    ref_pulse_wait = sqs_schedule.add(
        hold(q0_q1.readout, sensor.measure.pulse_duration),
        ref_op=ref_hold,
    )
    sqs_schedule.add(
        Measure(sensor.name, acq_channel=2, bin_mode=BinMode.APPEND),
        ref_op=ref_pulse_wait,
        ref_pt="start",
    )

    compensated_schedule.add(
        PulseCompensation(
            sqs_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,
        )
    )
/tmp/ipykernel_8456/3763333451.py:32: FutureWarning: amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.
  SquarePulse(amp=op_point.to_dict()[key], duration=duration, port=port_dict[key]),
/tmp/ipykernel_8456/3763333451.py:16: FutureWarning: amp is deprecated as an argument to RampPulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.
  RampPulse(amp=amplitude, offset=offset, duration=ramp_time, port=port_dict[key]),
/tmp/ipykernel_8456/3763333451.py:16: FutureWarning: offset is deprecated as an argument to RampPulse and will be removed in qblox-scheduler >= 2.0; use voltage_offset instead.
  RampPulse(amp=amplitude, offset=offset, duration=ramp_time, port=port_dict[key]),
/tmp/ipykernel_8456/3523779974.py:52: FutureWarning: clock_freq_new is deprecated as an argument to SetClockFrequency and will be removed in qblox-scheduler >= 2.0; use frequency instead.
  sqs_schedule.add(SetClockFrequency(clock=clock, clock_freq_new=spec_pulse_freq))
/tmp/ipykernel_8456/3523779974.py:54: FutureWarning: amp is deprecated as an argument to SquarePulse and will be removed in qblox-scheduler >= 2.0; use amplitude instead.
  SquarePulse(amp=0.1, duration=control_duration, port=port, clock=clock),

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: 2MB
Dimensions:      (acq_index_0: 31, acq_index_1: 31, repetition: 2000,
                  acq_index_2: 31)
Coordinates:
  * acq_index_0  (acq_index_0) int64 248B 0 1 2 3 4 5 6 ... 24 25 26 27 28 29 30
  * acq_index_1  (acq_index_1) int64 248B 0 1 2 3 4 5 6 ... 24 25 26 27 28 29 30
  * repetition   (repetition) int64 16kB 0 1 2 3 4 ... 1995 1996 1997 1998 1999
  * acq_index_2  (acq_index_2) int64 248B 0 1 2 3 4 5 6 ... 24 25 26 27 28 29 30
Data variables:
    0            (acq_index_0) complex128 496B (nan+nanj) ... (nan+nanj)
    1            (repetition, acq_index_1) complex128 992kB (nan+nanj) ... (n...
    2            (repetition, acq_index_2) complex128 992kB (nan+nanj) ... (n...
Attributes:
    tuid:     20260415-121106-560-f375a9

Analysis#

[8]:
if cluster.is_dummy:
    qubit_spec_ds[1].data = get_qubit_spectroscopy_data(frequencies, repetitions)[0]
    qubit_spec_ds[2].data = get_qubit_spectroscopy_data(frequencies, repetitions)[1]
[9]:
analysis = PSBQubitSpectroscopyAnalysis(
    frequency_list=frequencies,
    flip_proability=get_qubit_spectroscopy_data(frequencies, repetitions)[2],
    dataset=qubit_spec_ds,
)
analysis.display_figs_mpl()
../../../_images/applications_spin_spin_007_SingleQubitSpectroscopy_21_0.png
../../../_images/applications_spin_spin_007_SingleQubitSpectroscopy_21_1.png

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-15_12-11-07_UTC.json'