Quickstart
This quickstart introduces the Experiment workflow.
It assumes you already have configuration and parameter files for your system.
Note
Qubex loads configuration and parameter files that describe your chip, wiring, and control settings.
- Prefer
system_idas the selector for one concrete instrument setup - Pass both
config_dirandparams_direxplicitly when creating anExperiment - Create these files by following System configuration
- Base units in
Experiment: time-like values usens, and frequency-like values useGHz
1. Create an experiment
Create an Experiment by specifying the system, target qubits, and the
configuration and parameter directories to use. Once you have exp, you can
connect to the instruments, run measurements, execute pulse schedules, and
sweep parameters through its methods.
import numpy as np
import qubex as qx
exp = qx.Experiment(
system_id="SYSTEM_A",
qubits=[0, 1],
config_dir="/path/to/qubex-config/config",
params_dir="/path/to/qubex-config/params/SYSTEM_A",
)
Q0, Q1 = exp.qubit_labels[:2]
RQ0, RQ1 = exp.resonator_labels[:2]
2. Connect to instruments
Use connect() to connect to the configured instruments before running
measurements or schedules. It establishes communication, checks link status,
and pulls the current instrument-side settings into the session.
exp.connect()
3. Optionally update instrument settings
Use configure() only when you want to push the current configuration and
parameter settings to the instruments.
exp.configure()
Caution
This operation changes the state of the instruments. On shared systems, it can affect other users who are using the same instruments.
4. Run a basic measurement with measure
Use measure() when you want to provide control waveforms directly and let
Qubex append the readout automatically.
waveform = np.array([
0.01 + 0.01j,
0.01 + 0.01j,
0.01 + 0.01j,
0.01 + 0.01j,
])
sequence = {
Q0: waveform,
Q1: waveform,
}
result = exp.measure(
sequence=sequence,
mode="avg",
n_shots=1024,
)
result.plot()
print("avg:", result.data[Q0].kerneled)
result = exp.measure(
sequence=sequence,
mode="single",
n_shots=1024,
)
result.plot()
print("single:", result.data[Q0].kerneled)
For each qubit in sequence, Qubex applies the control waveform, sends a
readout pulse to the corresponding readout resonator, and returns the
reflected signal. kerneled is the time-integrated reflected signal expressed
as complex I/Q data: in avg mode it is a single complex value, and in
single mode it is a complex array with one value per shot.
5. Build a pulse sequence with PulseSchedule
Create a PulseSchedule when you want to build a control sequence explicitly
from reusable pulse objects.
For a workflow-agnostic introduction to schedule construction, see Build pulse sequences with PulseSchedule.
pulse = qx.pulse.Gaussian(duration=64, amplitude=0.05, sigma=16)
pulse.plot()
schedule = qx.PulseSchedule()
with schedule as s:
s.add(Q0, pulse)
s.add(Q0, pulse.scaled(2))
s.barrier()
s.add(Q1, pulse.shifted(np.pi / 6))
schedule.plot()
After creating a PulseSchedule, add pulses inside the with block by calling
add() on each channel. Use barrier() to align channels before the next
block, and when the block exits, Qubex pads all channels to the same length
automatically. This example only builds the control sequence, and it also
shows how to derive related pulses from the same base pulse with scaled()
and shifted().
6. Sweep a parameter with sweep_parameter
Use sweep_parameter() when you want to rerun the same sequence while
changing one parameter across a range of values. For each point in
sweep_range, Qubex evaluates the sequence and stores the measured response in
result.data[target].
result = exp.sweep_parameter(
sequence=lambda amplitude: {
Q0: qx.pulse.Rect(duration=64, amplitude=amplitude),
},
sweep_range=np.linspace(0.0, 0.1, 21),
n_shots=1024,
xlabel="Drive amplitude",
ylabel="Readout response",
)
result.plot()
print("sweep_range:", result.data[Q0].sweep_range)
print("data:", result.data[Q0].data)
You can also return a PulseSchedule from a factory function and sweep that
schedule directly. This is useful for wait-time sweeps in T1-like sequences,
where the blank duration should follow the pulse sampling period. Here,
the log-spaced wait values are discretized onto the valid time grid before the
sweep. The example below uses 2 ns as one concrete value, but you should
replace it with the sampling period required by the hardware you are using.
wait_range = exp.util.discretize_time_range(
np.geomspace(100, 100e3, 51),
sampling_period=2,
)
def t1_sequence(wait: float) -> qx.PulseSchedule:
schedule = qx.PulseSchedule()
with schedule as s:
s.add(Q0, qx.pulse.Gaussian(duration=64, amplitude=0.05, sigma=16))
s.add(Q0, qx.pulse.Blank(duration=wait))
return schedule
result = exp.sweep_parameter(
sequence=t1_sequence,
sweep_range=wait_range,
n_shots=1024,
xlabel="Wait duration (ns)",
ylabel="Readout response",
xaxis_type="log",
)
result.plot()
print("sweep_range:", result.data[Q0].sweep_range)
print("data:", result.data[Q0].data)
7. Execute a schedule with execute
Use execute() when you want to run a PulseSchedule as written and place
custom readout pulses directly on resonator channels. This is useful when one
schedule should contain multiple readout events.
control_pulse = qx.pulse.Gaussian(duration=64, amplitude=0.05, sigma=16)
readout_pulse = qx.pulse.FlatTop(duration=256, amplitude=0.1, tau=32)
schedule = qx.PulseSchedule()
with schedule as s:
s.add(RQ0, readout_pulse)
s.barrier()
s.add(Q0, qx.pulse.Blank(duration=128))
s.barrier()
s.add(Q0, control_pulse)
s.barrier()
s.add(RQ0, readout_pulse.scaled(0.8))
schedule.plot()
result = exp.execute(
schedule=schedule,
mode="avg",
n_shots=1024,
)
result.plot()
print("n_captures:", len(result.data[Q0]))
This example reuses control_pulse and readout_pulse inside the schedule.
It first performs a readout, then inserts a blank interval and a control
pulse, and finally performs a second readout. Because RQ0 is read out twice,
result.data[Q0] contains two capture results.
8. Experimental async run_* methods
Experiment also exposes async-first measurement entry points such as
run_measurement(), run_sweep_measurement(), and run_ndsweep_measurement().
Treat them as Experimental features: they are public, but their signature and
behavior may still change in future releases.
Use them when your application is already async. In scripts, wrap them with
asyncio.run(...). In notebooks, call them with await.
schedule = qx.PulseSchedule()
with schedule as s:
s.add(Q0, control_pulse)
result = await exp.run_measurement(
schedule=schedule,
n_shots=1024,
shot_averaging=False,
time_integration=True,
)
result.plot()
If you want the most stable path today, prefer the legacy synchronous methods
introduced above (measure(), execute(), sweep_parameter()).
Next steps
- Explore the broader
Experimentworkflow:Experiment - Continue with curated notebooks: Experiment example workflows
- Use advanced contrib routines when needed: Community-contributed workflows