Tutorial 6) Asynchronous Control
This tutorial covers asynchronous control in arbok-driver, executing parts of a sequence conditionally and independently of the main parameter sweep, rather than running every repetition through an identical, strictly linear set of steps.
Many experiments need this. Slow drifts in qubit frequency or microwave power can be tracked between sweep points, or a quantum state can be prepared by measurement in a repeat-until-success protocol, where the sequence only continues once a desired outcome has been obtained.
arbok-driver supports this through a step requirement mechanism. Any
sub-sequence can register a step requirement (a boolean QUA variable) on its
parent Measurement. This variable is evaluated at runtime before each sequence
iteration:
While the step requirement is not satisfied, no data is saved to the stream-processing buffers.
Sub-sequences defined with
check_step_requirements=Trueare not executed while the requirement is unmet.
This lets asynchronous logic such as feedback loops or heralding protocols run continuously alongside the main sequence, without a separate program or manual host/FPGA coordination.
We demonstrate this with a heralded quantum state initialization protocol, building directly on the parity initialization from Tutorial 3.
This tutorial assumes you have completed Tutorials 1–3.
1. The Idea: Heralded Initialization
In Tutorial 3 the measurement flowed strictly linearly:
parity_init → control → parity_read
Every repetition initialized the qubit pair, applied control, and read out — regardless of whether the initialization actually produced the desired state.
Heralded initialization inserts a verification layer. We measure the state before accepting it for quantum control:
┌─────────────────────────── repeat until success ──┐
│ │
parity_init (heralded) → control → parity_read ───────────┘
(skipped once (only (heralds the state AND,
heralded) when once accepted, is the
heralded) actual measurement)
The sequence keeps re-initializing and reading out until the readout reports the target state. Only then does it accept the state, run the quantum control, and save the measurement data. This has a nice physical bonus: the heralding measurement itself collapses the wavefunction into the correct state.
Real-time heralding of this kind is widely used in silicon spin-qubit experiments to prepare high-fidelity initial states by measurement [1][2][3].
Three features make this cheap to build in arbok-driver, and we highlight each below:
Heralding is added by inheritance —
ParityInitHeraldedsimply subclasses theParityInitof Tutorial 3.One readout does both jobs — the same
ParityReadheralds the state and serves as the final measurement. The control section is toggled on/off depending on heralding success.Data is saved conditionally — measurement results are only streamed on iterations where control was actually executed.
References
[1] Huang, J.Y., Su, R.Y., Lim, W.H. et al. High-fidelity spin qubit operation and algorithmic initialization above 1 K. Nature 627, 772–777 (2024). https://doi.org/10.1038/s41586-024-07160-2
[2] Nickl, A., Dumoulin Stuyck, N., Steinacker, P. et al. Eight-qubit operation of a 300 mm SiMOS foundry-fabricated device. Nature Communications 17, 5878 (2026). https://doi.org/10.1038/s41467-026-74597-6
[3] Steinacker, P., Dumoulin Stuyck, N., Lim, W.H. et al. Industry-compatible silicon spin-qubit unit cells exceeding 99% fidelity. Nature 646, 81–87 (2025). https://doi.org/10.1038/s41586-025-09531-9
2. Heralding by Inheritance
The heralded initialization lives in
parity_initialization_heralded.py.
It is a thin subclass of the ParityInit from Tutorial 3 — the actual voltage
ramps that prepare the qubit pair are inherited unchanged. All the subclass adds
is the repeat-until-success bookkeeping.
The essential logic is compact enough to show in full:
class ParityInitHeralded(ParityInit):
def qua_declare(self):
# remembers the previous readout outcome
self.successful_init = qua.declare(bool, value=False)
# the step requirement gating everything downstream
self.step_requirement = qua.declare(bool, value=False)
self.measurement.add_step_requirement(self.step_requirement)
...
def qua_sequence(self):
# resolve the readout result we herald on
self.feedback_var = self.measurement.find_parameter_from_sub_sequence(
self.feedback_result)
with qua.if_(self.successful_init):
# already heralded -> allow downstream stages, skip re-init
qua.assign(self.step_requirement, True)
qua.wait(self.arbok_params.t_wait_post_init.qua, *self.elements)
with qua.else_():
# not yet heralded -> block downstream stages, re-initialize
qua.assign(self.step_requirement, False)
super().qua_sequence() # <- the inherited ParityInit ramps
def qua_after_sequence(self):
# update the flag from the latest readout for the next iteration
with qua.if_(~self.successful_init):
qua.assign(self.successful_init, self.feedback_var) # target_state = 1
with qua.else_():
qua.assign(self.successful_init, False)
Two things are worth noting:
super().qua_sequence()re-runs the entire parity initialization inherited from Tutorial 3. Adding heralding did not require re-implementing any of the physics.add_step_requirement(...)registers the boolean on theMeasurement. Every sub-sequence markedcheck_step_requirements=True, and all data saving, is gated on it.
2.1 A config for the heralded initialization
The heralded sequence reuses every voltage point and timing of the bare parity
initialization and adds a single new parameter: t_wait_post_init, the settle
time applied once initialization has been heralded. The config
parity_init_heralded_conf
is therefore just a copy of parity_init_conf with that one parameter added and
the sequence class swapped to ParityInitHeralded.
from arbok_driver import ArbokDriver, Device, Measurement
from arbok_driver.examples.configurations.hardware import opx1000_config
from arbok_driver.examples.configurations.sequence import (
device_config,
parity_init_heralded_conf,
parity_read_conf,
)
from arbok_driver.examples.sequences import Xstrict
# The device master_config provides shared parameters such as the qubit
# elements used by the control sub-sequence (see Tutorial 3).
device = Device(
'device_8q',
opx_config=opx1000_config,
divider_config={},
master_config=device_config,
)
my_driver = ArbokDriver('my_driver', device)
2026-07-23 01:05:36,054 - qm - INFO - Starting session: 6f3cfa78-54fc-437e-ad00-c8e193e76ca6
3. Composing the Heralded Measurement
We build the measurement with the dictionary-based composition from Tutorial 3. Compared to the linear CPMG example, only two things change:
The initialization stage uses the heralded config and receives two extra
kwargs:feedback_result: the path to the readout gettable that heralds the state. Here'parity_read.p1p2.state__p1p2.qua_result_var'points at the thresholded state of thep1p2signal produced by the readout stage.target_state: the readout outcome that satisfies the protocol (1).
The control stage is marked
check_step_requirements=True, so it is only executed once the initialization has been heralded.
As the quantum control we use the simplest possible operation: a single
Xstrict gate playing one control_pi pulse on Q1.
heralded_conf = {
# Initialization stage — heralded, repeat-until-success
'parity_init': {
'config': parity_init_heralded_conf,
'kwargs': {
# readout result used to herald the initialized state
'feedback_result': 'parity_read.p1p2.state__p1p2.qua_result_var',
# accept the state once the readout reports a 1
'target_state': 1,
},
},
# Quantum control stage — a single X gate, toggled by heralding success
'spin_control': {
'sub_sequences': {
'x_gate': {
'sequence': Xstrict,
'kwargs': {
'target_qubit': 'Q1',
'control_pulse': 'control_pi',
},
},
},
# only execute this stage once the step requirement is satisfied
'kwargs': {'check_step_requirements': True},
},
# Readout stage — heralds the state AND is the actual measurement
'parity_read': {'config': parity_read_conf},
}
heralded_meas = Measurement(my_driver, 'heralded_meas')
heralded_meas.add_subsequences_from_dict(heralded_conf)
3.1 One readout, two jobs
Notice that parity_read appears once in the dictionary, yet it plays two
roles:
Heralding measurement. On every iteration it measures the state. Its thresholded
state__p1p2result is whatParityInitHeraldedreads throughfeedback_resultto decide whether the target state was reached.Actual measurement. On the iteration where the state has been heralded and control has been applied, the very same readout provides the measurement data we care about.
The readout runs unconditionally every iteration — but its data saving is
gated on the step requirement (this is handled inside ParityRead, which wraps
its save_variables in measurement.qua_check_step_requirements). So results
are only streamed on iterations where control was executed.
4. Compiling to QUA
Everything compiles to a standard QUA program. Let’s generate it and inspect the key parts of the loop body.
qua_str = heralded_meas.get_qua_program_as_str()
print(qua_str)
# Single QUA script generated at 2026-07-23 01:05:38.031989
# QUA library version: 1.2.5
from qm import CompilerOptionArguments
from qm.qua import *
with program() as prog:
v1 = declare(int, value=0)
v2 = declare(bool, value=False)
v3 = declare(bool, value=False)
v4 = declare(int, value=0)
v5 = declare(fixed, )
v6 = declare(fixed, )
v7 = declare(fixed, )
v8 = declare(fixed, )
v9 = declare(fixed, )
v10 = declare(fixed, )
v11 = declare(bool, )
v12 = declare(bool, )
with infinite_loop_():
pause()
assign(v1, 0)
with if_(v2):
assign(v3, True)
r2 = declare_stream()
save(v4, r2)
assign(v4, 0)
align("P1", "J1", "P2", "P7", "J7", "P8")
wait(250, "P1", "J1", "P2", "P7", "J7", "P8")
with else_():
assign(v3, False)
assign(v4, (v4+1))
align("P1", "J1", "P2", "P7", "J7", "P8")
play("unit_ramp"*amp(0.06), "P1", duration=250)
play("unit_ramp"*amp(-0.085), "J1", duration=250)
play("unit_ramp"*amp(-0.06), "P2", duration=250)
play("unit_ramp"*amp(-0.07), "P7", duration=250)
play("unit_ramp"*amp(-0.05), "J7", duration=250)
play("unit_ramp"*amp(0.07), "P8", duration=250)
align("P1", "J1", "P2", "P7", "J7", "P8")
wait(50000, "P1", "J1", "P2", "P7", "J7", "P8")
align("P1", "J1", "P2", "P7", "J7", "P8")
play("unit_ramp"*amp(-0.0165), "P1", duration=250)
play("unit_ramp"*amp(0.035), "J1", duration=250)
play("unit_ramp"*amp(0.0165), "P2", duration=250)
play("unit_ramp"*amp(0.099), "P7", duration=250)
play("unit_ramp"*amp(0.117), "J7", duration=250)
play("unit_ramp"*amp(-0.099), "P8", duration=250)
align("P1", "J1", "P2", "P7", "J7", "P8")
wait(9571, "P1", "J1", "P2", "P7", "J7", "P8")
align("P1", "J1", "P2", "P7", "J7", "P8")
play("unit_ramp"*amp(-0.03), "P1", duration=141)
play("unit_ramp"*amp(0.03), "P2", duration=141)
play("unit_ramp"*amp(-0.02), "P7", duration=141)
play("unit_ramp"*amp(0.02), "P8", duration=141)
align("P1", "J1", "P2", "P7", "J7", "P8")
wait(250, "P1", "J1", "P2", "P7", "J7", "P8")
ramp_to_zero("P1")
ramp_to_zero("J1")
ramp_to_zero("P2")
ramp_to_zero("P7")
ramp_to_zero("J7")
ramp_to_zero("P8")
align("P1", "J1", "P2", "P7", "J7", "P8")
with if_(v3):
play("control_pi", "Q1")
align()
align("P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
wait(25000, "P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
align("P1", "J1", "P2", "P7", "J7", "P8")
play("unit_ramp"*amp(0.023), "P1", duration=6250)
play("unit_ramp"*amp(-0.02), "J1", duration=6250)
play("unit_ramp"*amp(-0.023), "P2", duration=6250)
play("unit_ramp"*amp(-0.0325), "P7", duration=6250)
play("unit_ramp"*amp(0.005), "J7", duration=6250)
play("unit_ramp"*amp(0.0325), "P8", duration=6250)
align("P1", "J1", "P2", "P7", "J7", "P8")
wait(25000, "P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
align("P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
measure("measure", "SET1", integration.full("x_const", v5, ""))
measure("measure", "SET2", integration.full("x_const", v6, ""))
align("P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
wait(5000, "P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
align("P1", "J1", "P2", "P7", "J7", "P8")
play("unit_ramp"*amp(0.0020000000000000018), "P1", duration=250)
play("unit_ramp"*amp(-0.0020000000000000018), "P2", duration=250)
play("unit_ramp"*amp(-0.0025000000000000022), "P7", duration=250)
play("unit_ramp"*amp(0.0025000000000000022), "P8", duration=250)
align("P1", "J1", "P2", "P7", "J7", "P8")
wait(25000, "P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
align("P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
measure("measure", "SET1", integration.full("x_const", v7, ""))
measure("measure", "SET2", integration.full("x_const", v8, ""))
align("P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
align()
assign(v9, (v5-v7))
assign(v10, (v6-v8))
assign(v11, (v9>0.001))
assign(v12, (v10>-0.001))
align("P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
wait(5000, "P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
ramp_to_zero("P1")
ramp_to_zero("J1")
ramp_to_zero("P2")
ramp_to_zero("P7")
ramp_to_zero("J7")
ramp_to_zero("P8")
align("P1", "J1", "P2", "P7", "J7", "P8")
wait(1000, "P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
align("P1", "J1", "P2", "P7", "J7", "P8", "SET1", "SET2")
align()
with if_((v2^True)):
assign(v2, v11)
with else_():
assign(v2, False)
with if_(v3):
r3 = declare_stream()
save(v5, r3)
r4 = declare_stream()
save(v6, r4)
r5 = declare_stream()
save(v7, r5)
r6 = declare_stream()
save(v8, r6)
r7 = declare_stream()
save(v9, r7)
r8 = declare_stream()
save(v10, r8)
r9 = declare_stream()
save(v11, r9)
r10 = declare_stream()
save(v12, r10)
align()
with if_(v3):
assign(v1, (v1+1))
r1 = declare_stream()
save(v1, r1)
align()
with stream_processing():
r1.buffer(1).save("my_driver_heralded_meas_shots")
r2.save_all("heralded_attempts")
r3.buffer(1).save("my_driver_heralded_meas_parity_read_ref__p1p2")
r4.buffer(1).save("my_driver_heralded_meas_parity_read_ref__p7p8")
r5.buffer(1).save("my_driver_heralded_meas_parity_read_read__p1p2")
r6.buffer(1).save("my_driver_heralded_meas_parity_read_read__p7p8")
r7.buffer(1).save("my_driver_heralded_meas_parity_read_diff__p1p2")
r8.buffer(1).save("my_driver_heralded_meas_parity_read_diff__p7p8")
r9.buffer(1).save("my_driver_heralded_meas_parity_read_state__p1p2")
r10.buffer(1).save("my_driver_heralded_meas_parity_read_state__p7p8")
config = None
loaded_config = None
4.1 Reading the compiled program
Look for the following structures in the output above — they are the direct
consequences of the step requirement mechanism. (v2 is successful_init, v3
is step_requirement.)
Repeat-until-success initialization. The heralding flag chooses between skipping re-init and running the full inherited parity initialization:
with if_(v2): # already heralded
assign(v3, True) # -> raise the step requirement
wait(250, "P1", "J1", ...) # -> just settle
with else_(): # not yet heralded
assign(v3, False) # -> lower the step requirement
play("unit_ramp"*amp(0.06), "P1", duration=250) # full ParityInit ramps
...
Control gated by heralding success. The single X gate is wrapped in the step requirement, so it only fires once the state has been accepted:
with if_(v3):
play("control_pi", "Q1")
Conditional data saving. The readout runs unconditionally, but the streams are only written — and the shot counter only advanced — when control ran:
with if_(v3):
save(v5, r3) # readout results
save(v6, r4)
...
with if_(v3):
assign(v1, (v1+1)) # shot counter
save(v1, r1)
Flag update for the next iteration. After the readout, the heralding flag is
refreshed from the latest thresholded state (v11):
with if_((v2^True)): # if this iteration was an init attempt
assign(v2, v11) # herald from the fresh readout
with else_(): # control ran this iteration
assign(v2, False) # reset -> re-initialize next shot
This is a complete repeat-until-success feedback loop running entirely on the
FPGA, expressed as a handful of QUA if_/else_ blocks driven by one boolean.
5. Summary
Asynchronous control in arbok-driver rests on a single, simple primitive: a
boolean step requirement registered on the Measurement.
Concern |
How it is handled |
|---|---|
Conditional execution |
Sub-sequences with |
Conditional data saving |
Stream writes and the shot counter are gated on the requirement automatically |
Feedback / heralding logic |
A sub-sequence sets the requirement based on a readout result resolved via |
The heralded initialization showcased all three highlights:
Inheritance made heralding cheap —
ParityInitHeraldedsubclassesParityInitand reuses its physics viasuper().qua_sequence().One readout served two purposes — heralding the state and, once accepted, acting as the actual measurement, with the control section toggled by heralding success.
Data was saved conditionally — only on iterations where control executed.
Because this all lives in ordinary QUA control flow, the same pattern extends to richer feedback protocols: drift tracking between sweep points, adaptive calibration, or multi-stage repeat-until-success routines — all running continuously alongside the main parameter sweep.