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

Coulomb Peaks#
Objectives#
In this tutorial, we measure charge sensor’s Coulomb peaks. The sharp conductance gradient of Coulomb peaks makes them ideal sensors for the electrostatic state of nearby quantum dots.
Imports
[1]:
from __future__ import annotations
import numpy as np
from dependencies.analysis_utils import CoulombPeaksAnalysis
from dependencies.simulated_data import get_coulomb_peaks_data
from qblox_scheduler import HardwareAgent, Schedule
from qblox_scheduler.operations import Measure, PulseCompensation, 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()
cluster = hw_agent.get_clusters()["cluster0"]
# 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")
/.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(
Modules are connected to the quantum device via:
QCM (Module 2):
\(\text{O}^{1}\): plunger gate of qubit 0 (
q0).\(\text{O}^{2}\): plunger gate of qubit 1 (
q1).
QRM (Module 4):
\(\text{O}^{1}\) and \(\text{I}^{1}\): resonator of the charge sensor (
cs0).
Schedule & Measurement#
We will now build the schedule for a 1D gate voltage sweep. The goal is to find Coulomb peaks by applying a pulse with a varying amplitude to target element and recording the cs0’s response.
The schedule’s behavior changes depending on what you define as the target_element: To find a dot’s peaks (Cross-Capacitance): If the target_element is a quantum dot (e.g., q0), the schedule does two things for each amplitude: Pulse: Applies a square pulse to the dot’s gate. Measure: After a short delay, it sends a separate pulse to the sensor to measure its response.
To calibrate the sensor (Self-Sweep): If the target_element is the sensor itself, the logic is simpler. For each amplitude, the schedule applies a single measurement pulse to the sensor, using the swept amplitude as its gate_pulse_amp. This “self-sweep” is the method we use to find the sensor’s own Coulomb peaks and identify its most sensitive working point.
We define all variables for the experiment schedule in one location for ease of access and modification.
[4]:
repetitions = 101
sensor = sensor_0
target_element = sensor_0
pulse_duration = 800e-9
amplitudes = np.linspace(0.02, 0.9, 500)
[5]:
coulomb_peaks_schedule = Schedule("pulse_compensation", repetitions=repetitions)
# Make sure the element that is assigned to obtain data is a charge sensor
if not sensor.__class__.__name__ == "ChargeSensor":
raise ValueError("The measure object is not a charge sensor object.")
# Sweep over pulse amplitude of the gate port of the target element in the given amplitude range, and obtain the charge sensor response for every amplitude value.
for amp in amplitudes:
inner_schedule = Schedule("schedule", repetitions=1)
if target_element == sensor:
inner_schedule.add(Measure(sensor.name, gate_pulse_amp=amp, pulse_duration=pulse_duration))
else:
inner_schedule.add(
SquarePulse(amp=amp, duration=pulse_duration, port=target_element.ports.gate)
)
inner_schedule.add(
Measure(
sensor.name,
gate_pulse_amp=sensor.measure.pulse_amp,
),
rel_time=250e-9,
) # rel_time defines the time between this Measure() operation and the previous SquarePulse() operation
# Add pulse compensation
coulomb_peaks_schedule.add(
PulseCompensation(
inner_schedule,
max_compensation_amp={"q0:gt": 0.110, "q1:gt": 0.120, "cs0:gt": 0.15},
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.
Before running this schedule, please make sure that the measurement pulse amplitude, duration, acquisition delay and integration time parameters are adjusted to your needs in the device configuration file. These will be defined via the sensor device (in this case, named “cs0”) since the measurement is performed at the device level.
Note that this schedule can be adapted to perform a sweep of the gate connection of any quantum device element via changing the target element.
[6]:
coulomb_peaks_ds = hw_agent.run(coulomb_peaks_schedule)
coulomb_peaks_ds
[6]:
<xarray.Dataset> Size: 12kB
Dimensions: (acq_index_0: 500)
Coordinates:
* acq_index_0 (acq_index_0) int64 4kB 0 1 2 3 4 5 ... 494 495 496 497 498 499
Data variables:
0 (acq_index_0) complex128 8kB (nan+nanj) ... (nan+nanj)
Attributes:
tuid: 20260415-120432-869-19af7dAnalysis#
[7]:
if cluster.is_dummy:
coulomb_peaks_ds[0].data = get_coulomb_peaks_data(amplitudes)[1]
[8]:
analysis = CoulombPeaksAnalysis(
dataset=coulomb_peaks_ds,
) # settings_overwrite={"mpl_transparent_background": False}
analysis.display_figs_mpl()
The quantum device settings can be saved after every experiment for allowing later reference into experiment settings.
[9]:
hw_agent.quantum_device.to_json_file("./dependencies/configs", add_timestamp=True)
[9]:
'./dependencies/configs/spin_with_psb_device_config_2026-04-15_12-04-33_UTC.json'