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=True are 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:

  1. Heralding is added by inheritanceParityInitHeralded simply subclasses the ParityInit of Tutorial 3.

  2. One readout does both jobs — the same ParityRead heralds the state and serves as the final measurement. The control section is toggled on/off depending on heralding success.

  3. 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 the Measurement. Every sub-sequence marked check_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:

  1. 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 the p1p2 signal produced by the readout stage.

    • target_state: the readout outcome that satisfies the protocol (1).

  2. 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__p1p2 result is what ParityInitHeralded reads through feedback_result to 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 check_step_requirements=True run only when the requirement is satisfied

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 feedback_result

The heralded initialization showcased all three highlights:

  • Inheritance made heralding cheapParityInitHeralded subclasses ParityInit and reuses its physics via super().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.