Tutorial 2) Readout Sequences
0. Introduction
The first tutorial demonstrated sequence writing and parameterisation with configuration files. This enables users to apply and quantify arbitrary waveforms on instrument outputs.
Playing waveforms is one part of qubit experiments. Reading out the result is equally important.
This tutorial explains how ReadSequences work and how to configure and use them.
The tutorial is structured as follows:
The
ReadSequencearchitectureWriting a custom
ReadSequenceConfiguring a readout sequence
Instantiation and inspection
Compiling to QUA code
Scaling readout sequences to larger systems
1. The ReadSequence Architecture
A ReadSequence is built around three core concepts:
1.1 Signal
A Signal is a logical container for measurement results belonging to a single physical entity (e.g. a qubit, quantum dot, or SET).
A
ReadSequencecan hold multiple signalsSignals carry no configuration beyond their name
They are used purely to group related results
1.2. AbstractReadout
An AbstractReadout represents a single measurement operation.
Each subclass (e.g.
DcAverage,Difference,Threshold) defines:the QUA code to execute
the results it produces
A readout can produce one or more
GettableParameters (QCoDeS parameters)Each
AbstractReadoutis assigned to exactly oneSignal
1.3. readout_groups
Readout groups are named collections of AbstractReadout instances defined in the configuration.
Calling a group inside
qua_sequenceexecutes all contained readouts in sequenceEnables extensibility:
add new physical entities by extending the configuration
no changes required to the
ReadSequenceclass
Dependent readouts
A key feature of this design is that AbstractReadouts can depend on each other.
A readout can consume the
GettableParameterproduced by a previous readoutThis enables real-time, on-FPGA processing chains
Example:
refmeasurementreadmeasurementDifference→ subtractsreffromreadThreshold→ classifies result into a binary state
The ReadSequence author does not need to manually define QUA variables or streams — these are handled automatically.
2. Writing a Custom ReadSequence
A ReadSequence subclass needs to provide two methods.
__init__ calls the parent constructor with the parent instrument, the sequence name and the configuration dictionary. Any sequence-level setup, such as collecting the full list of QUA elements, is done here.
qua_sequence contains the QUA commands that define the physical measurement. Readout groups are invoked by iterating over self.readout_groups[group_name] and calling qua_measure() on each entry. The sequence does not need to know how many physical entities are present. Adding more entries to a group in the configuration is sufficient to extend the measurement.
Below is the ParityRead class used throughout this tutorial.
from arbok_driver.examples.sequences import ParityRead
ParityRead??
2026-04-15 10:19:48,443 - qm - INFO - Starting session: 71167a05-17ea-40f6-88eb-2c0e63578ab6
Init signature:
ParityRead(
parent: arbok_driver.sequence_base.SequenceBase,
name: str,
sequence_config: dict,
)
Source:
class ParityRead(ReadSequence):
"""
Class containing parameters and sequence for spin parity readout
Args:
param_config (dict): Dict containing all program parameters
"""
PARAMETER_CLASS= ParityReadParameters
arbok_params: ParityReadParameters
def __init__(
self,
parent: SequenceBase,
name: str,
sequence_config: dict,
):
"""
Constructor method for 'ParityRead' class
Args:
parent (SequenceBase): Parent instrument module
or instrument
name (str): name of the sequence
sequence_config (dict): Configuration for sequence
"""
super().__init__(
parent = parent,
name = name,
sequence_config = sequence_config,
)
self.elements = list(self.arbok_params.gate_elements.get())
self.elements += list(self.arbok_params.readout_elements.get())
def qua_declare(self):
"""
Declares variables before execution of the main program. All mandatory
qua variables for auto generated readouts from the config are introduced
by the parent class (super)
"""
return super().qua_declare()
def qua_sequence(self):
"""
QUA sequence to perform spin parity readout
The sequence is as follows:
1. Move to REFERENCE measurement point
2. Take physical REFERENCE measurement
3. Move to READ measurement point
4. Take physical READ measurement
"""
qua.align()
qua.align(*self.elements)
qua.wait(self.arbok_params.t_wait_home_before.qua, *self.elements)
### Move to REFERENCE measurement point
arbok.ramp(
elements = self.arbok_params.gate_elements.get(),
reference = self.arbok_params.v_home,
target = self.arbok_params.v_reference,
duration = self.t_ramp_to_reference,
operation = 'unit_ramp',
)
qua.wait(self.arbok_params.t_wait_pre_read.qua, *self.elements)
qua.align(*self.elements)
### Take physical REFERENCE measurement
for _, readout in self.readout_groups["ref"].items():
readout.qua_measure()
qua.align(*self.elements)
qua.wait(self.t_wait_post_read.qua, *self.elements)
### Move to READ measurement point
arbok.ramp(
elements = self.arbok_params.gate_elements.get(),
reference = self.arbok_params.v_reference,
target = self.arbok_params.v_read,
duration = self.arbok_params.t_ramp_to_read,
operation = 'unit_ramp',
)
qua.wait(self.arbok_params.t_wait_pre_read.qua, *self.elements)
qua.align(*self.elements)
### take physical READ measurement
for _, readout in self.readout_groups["read"].items():
readout.qua_measure()
qua.align(*self.elements)
qua.align()
### Process raw data in abstract readouts
for _, readout in self.readout_groups["diff"].items():
readout.qua_measure()
for _, readout in self.readout_groups["state"].items():
readout.qua_measure()
qua.align(*self.elements)
qua.wait(self.arbok_params.t_wait_post_read.qua, *self.elements)
### Reset elements
arbok.reset_sticky_elements(self.arbok_params.gate_elements.get())
qua.wait(self.arbok_params.t_wait_after_reset.qua, *self.elements)
qua.align(*self.elements)
### Apply feedback if given in configuration
if 'set_feedback' in self.readout_groups:
for _, readout in self.readout_groups["set_feedback"].items():
readout.qua_measure()
qua.align()
def qua_after_sequence(self):
"""Saves variables to the respective streams after the sequence"""
self.measurement.qua_check_step_requirements(self.save_variables)
def save_variables(self):
"""Saves all variables to the respective streams"""
for _, readout in self.abstract_readouts.items():
readout.qua_save_variables()
File: ~/git-repos/arbok_driver/arbok_driver/examples/sequences/parity_readout.py
Type: ABCMeta
Subclasses:
Looking at qua_sequence, the structure is straightforward. The gates are ramped to the reference voltage, the ref group fires, the gates are ramped to the read voltage, the read group fires. After both physical measurements are complete, the diff and state groups execute the on-FPGA processing. Finally an optional set_feedback group allows conditional feedback operations if it is present in the configuration.
Notice that ParityRead never mentions p1p2, SET1, or any other device-specific name. All of that lives in the configuration. The same class works unchanged for any number of signals and sensors.
3. Configuring a Readout Sequence
Now lets configure the ParityRead sequence step through all the mentioned objects with a given example. In section 6 we will use the same sequence with a configuration for an 8 qubit device, demonstrating the scaling capabilities of readouts in arbok.
3.1 The structure of the configuration dictionary
A readout sequence configuration has four top-level keys.
sequence names the ReadSequence class that will be instantiated.
parameters defines the settable QCoDeS parameters of the sequence, such as timing and voltage values. These behave identically to parameters in a plain SubSequence.
signals is a plain list of strings. Each string names a logical signal container. No further information is required here.
readout_groups is a dictionary of named groups. Each group is itself a dictionary whose keys identify individual AbstractReadout entries. Every entry specifies the readout class to use, which signal the result belongs to, any keyword arguments the readout class requires and any additional settable parameters specific to that readout.
3.2 Readout group entries and result naming
Each entry in a readout group produces one or more GettableParameters. The name of a gettable follows a fixed pattern:
<readsequence_name>.<signal_name>.<group_name>__<entry_key>
For example, an entry with key p1p2 inside the group ref belonging to a ReadSequence named parity_read and signal p1p2 produces a gettable accessible at:
parity_read.p1p2.ref__p1p2
This dotted path is also how earlier results are referenced in later readout entries. A Difference readout, for instance, takes minuend and subtrahend arguments that are these dotted path strings. The framework resolves them to the correct QUA variables at compile time. This makes it possible to build processing chains entirely within the configuration, with no manual variable management.
3.3 A complete single-signal configuration
The configuration below sets up a parity readout for one signal p1p2 measured via SET1. The readout chain has four groups that execute in sequence inside qua_sequence:
The ref group takes a DC-averaged reference measurement and stores it under p1p2.
The read group takes a DC-averaged readout measurement and stores it under p1p2.
The diff group subtracts the read result from the reference result on the FPGA in real time.
The state group applies a voltage threshold to the difference to produce a binary spin state.
from arbok_driver.parameter_types import Time, Voltage, List
from arbok_driver.examples.readout_classes import DcAverage, Difference, Threshold
parity_read_p1p2_conf = {
'sequence': ParityRead,
'parameters': {
'gate_elements': {
'type': List,
'value': ['P1', 'J1', 'P2', 'J2', 'P3']
},
'readout_elements': {
'type': List,
'value': ['SET1']
},
't_wait_home_before': {
'type': Time,
'label': 'Wait time at the home point before readout',
'value': int(1e3 / 4)
},
't_wait_pre_read': {
'type': Time,
'value': int(10e3 / 4)
},
't_wait_post_read': {
'type': Time,
'value': int(10e3 / 4)
},
't_ramp_to_reference': {
'type': Time,
'var_type': 'fixed',
'value': int(1e3 / 4)
},
't_ramp_to_read': {
'type': Time,
'value': int(0.05e3 / 4)
},
't_wait_after_reset': {
'type': Time,
'value': int(10e3 / 4)
},
'v_home': {
'type': Voltage,
'label': 'Home voltage point',
'elements': {
'P1': 0.0, 'J1': 0.0,
'P2': 0.0, 'J2': 0.0,
'P3': 0.0,
}
},
'v_reference': {
'type': Voltage,
'label': 'Reference voltage point',
'elements': {
'P1': 0.0, 'J1': 0.0,
'P2': 0.0, 'J2': 0.0,
'P3': 0.0,
}
},
'v_read': {
'type': Voltage,
'label': 'Readout voltage point',
'elements': {
'P1': 0.0, 'J1': 0.0,
'P2': 0.0, 'J2': 0.0,
'P3': 0.0,
}
},
},
'signals': ['p1p2'],
'readout_groups': {
'ref': {
'p1p2': {
'readout_class': DcAverage,
'signal': 'p1p2',
'kwargs': {
'qua_element': 'SET1'
},
},
},
'read': {
'p1p2': {
'readout_class': DcAverage,
'signal': 'p1p2',
'kwargs': {
'qua_element': 'SET1'
},
},
},
'diff': {
'p1p2': {
'readout_class': Difference,
'signal': 'p1p2',
'kwargs': {
'minuend': 'parity_read.p1p2.ref__p1p2',
'subtrahend': 'parity_read.p1p2.read__p1p2',
},
},
},
'state': {
'p1p2': {
'readout_class': Threshold,
'signal': 'p1p2',
'kwargs': {
'charge_readout': 'p1p2.diff__p1p2',
},
'parameters': {
'threshold': {'type': Voltage, 'value': 0.0}
}
},
},
},
}
3.4 The processing chain in detail
It is worth pausing on how the four groups form a chain.
DcAverage in ref measures the SET voltage and stores the result as a QUA fixed-point variable. Its gettable is registered under the path parity_read.p1p2.ref__p1p2.
DcAverage in read does the same at the readout voltage point and registers under parity_read.p1p2.read__p1p2.
Difference in diff receives the two paths above as minuend and subtrahend. It resolves them to the underlying QUA variables at compile time and subtracts them on the FPGA, producing a new gettable at parity_read.p1p2.diff__p1p2.
Threshold in state receives the output of diff as its charge_readout argument and compares it against the settable threshold parameter to produce a binary result.
No manual QUA variable declarations or stream bookkeeping are needed anywhere in this chain. Everything is resolved from the configuration.
4. Instantiation and Inspection
4.1 Setting up the driver stack
As in Tutorial 1, a Device, an ArbokDriver and a Measurement are created first. Measurement replaces the old Sequence class. The ParityRead sequence is then added to the measurement by instantiating it with the measurement as parent.
from arbok_driver import ArbokDriver, Device, Measurement
from arbok_driver.examples.configurations.hardware import (
opx1000_config, divider_config)
mock_device = Device(
name='mock_device',
opx_config=opx1000_config,
divider_config=divider_config,
)
qm_driver = ArbokDriver('qm_driver', mock_device)
mock_measurement = Measurement(qm_driver, 'mock_measurement')
parity_read = ParityRead(
parent=mock_measurement,
name='parity_read',
sequence_config=parity_read_p1p2_conf,
)
4.2 Inspecting parameters with print_readable_snapshot
The quickest way to inspect any QCoDeS instrument is print_readable_snapshot. For a ReadSequence this shows two kinds of parameters.
Settable parameters such as t_wait_pre_read or the per-element voltages appear with their current values.
Gettable parameters appear with the value Not available until the hardware has returned results. Their names encode where in the readout chain the result comes from, following the <readsequence>.<signal>.<group>__<key> convention described above.
parity_read.print_readable_snapshot()
qm_driver_mock_measurement_parity_read:
parameter value
--------------------------------------------------------------------------------
diff__p1p2 : Not available
gate_elements : ['P1', 'J1', 'P2', 'J2', 'P3'] (N/A)
read__p1p2 : Not available
readout_elements : ['SET1'] (N/A)
ref__p1p2 : Not available
state__p1p2 : Not available
state__p1p2__threshold : 0 (V)
t_ramp_to_read : 12 (s)
t_ramp_to_reference : 250 (s)
t_wait_after_reset : 2500 (s)
t_wait_home_before : 250 (s)
t_wait_post_read : 2500 (s)
t_wait_pre_read : 2500 (s)
v_home_J1 : 0 (V)
v_home_J2 : 0 (V)
v_home_P1 : 0 (V)
v_home_P2 : 0 (V)
v_home_P3 : 0 (V)
v_read_J1 : 0 (V)
v_read_J2 : 0 (V)
v_read_P1 : 0 (V)
v_read_P2 : 0 (V)
v_read_P3 : 0 (V)
v_reference_J1 : 0 (V)
v_reference_J2 : 0 (V)
v_reference_P1 : 0 (V)
v_reference_P2 : 0 (V)
v_reference_P3 : 0 (V)
4.3 Accessing gettables directly
The gettables attribute of a ReadSequence lists all GettableParameters it owns. Calling a gettable returns its current value.
parity_read.gettables
[<arbok_driver.parameters.gettable_parameter.GettableParameter: ref__p1p2 at 129173997264512>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: read__p1p2 at 129173994877520>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: diff__p1p2 at 129173994877840>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: state__p1p2 at 129173994878480>]
Individual gettables are accessed via the signal and group path directly on the ReadSequence object. Double underscores in the attribute name indicate nesting.
parity_read.p1p2.diff__p1p2
<arbok_driver.parameters.gettable_parameter.GettableParameter: diff__p1p2 at 129173994877840>
4.4 All gettables across a measurement
When a Measurement contains multiple ReadSequences, available_gettables gives a flat view of every gettable across all of them.
mock_measurement.available_gettables
[<arbok_driver.parameters.gettable_parameter.GettableParameter: ref__p1p2 at 129173997264512>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: read__p1p2 at 129173994877520>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: diff__p1p2 at 129173994877840>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: state__p1p2 at 129173994878480>]
4.5 Inspecting signals and readout groups
The signals present in a ReadSequence are accessible via its signals attribute. Readout groups are accessible via readout_groups.
parity_read.signals
{'p1p2': <arbok_driver.signal.Signal at 0x757baa389be0>}
parity_read.readout_groups
{'ref': {'p1p2': <arbok_driver.examples.readout_classes.dc_average.DcAverage at 0x757baa389d30>},
'read': {'p1p2': <arbok_driver.examples.readout_classes.dc_average.DcAverage at 0x757baa143110>},
'diff': {'p1p2': <arbok_driver.examples.readout_classes.difference.Difference at 0x757baa389fd0>},
'state': {'p1p2': <arbok_driver.examples.readout_classes.threshold.Threshold at 0x757baa38a120>}}
5. Compiling to QUA Code
Once the ReadSequence is instantiated and configured, the full QUA program can be compiled and inspected as a string. The framework inserts all variable declarations and stream definitions automatically based on the gettables that are actually used.
from rich import print as rprint
rprint(mock_measurement.get_qua_program_as_str())
# Single QUA script generated at 2026-04-15 10:21:13.091399 # 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(fixed, ) v3 = declare(fixed, ) v4 = declare(fixed, ) v5 = declare(bool, ) with infinite_loop_(): pause() assign(v1, 0) align() align("P1", "J1", "P2", "J2", "P3", "SET1") wait(250, "P1", "J1", "P2", "J2", "P3", "SET1") align("P1", "J1", "P2", "J2", "P3") align("P1", "J1", "P2", "J2", "P3") wait(2500, "P1", "J1", "P2", "J2", "P3", "SET1") align("P1", "J1", "P2", "J2", "P3", "SET1") measure("measure", "SET1", integration.full("x_const", v2, "")) align("P1", "J1", "P2", "J2", "P3", "SET1") wait(2500, "P1", "J1", "P2", "J2", "P3", "SET1") align("P1", "J1", "P2", "J2", "P3") align("P1", "J1", "P2", "J2", "P3") wait(2500, "P1", "J1", "P2", "J2", "P3", "SET1") align("P1", "J1", "P2", "J2", "P3", "SET1") measure("measure", "SET1", integration.full("x_const", v3, "")) align("P1", "J1", "P2", "J2", "P3", "SET1") align() assign(v4, (v2-v3)) assign(v5, (v4>0.0)) align("P1", "J1", "P2", "J2", "P3", "SET1") wait(2500, "P1", "J1", "P2", "J2", "P3", "SET1") ramp_to_zero("P1") ramp_to_zero("J1") ramp_to_zero("P2") ramp_to_zero("J2") ramp_to_zero("P3") align("P1", "J1", "P2", "J2", "P3") wait(2500, "P1", "J1", "P2", "J2", "P3", "SET1") align("P1", "J1", "P2", "J2", "P3", "SET1") align() r2 = declare_stream() save(v2, r2) r3 = declare_stream() save(v3, r3) r4 = declare_stream() save(v4, r4) r5 = declare_stream() save(v5, r5) align() assign(v1, (v1+1)) r1 = declare_stream() save(v1, r1) align() with stream_processing(): r1.buffer(1).save("qm_driver_mock_measurement_shots") r2.buffer(1).save("qm_driver_mock_measurement_parity_read_ref__p1p2") r3.buffer(1).save("qm_driver_mock_measurement_parity_read_read__p1p2") r4.buffer(1).save("qm_driver_mock_measurement_parity_read_diff__p1p2") r5.buffer(1).save("qm_driver_mock_measurement_parity_read_state__p1p2") config = None loaded_config = None
The output shows that variable declarations for the QUA fixed-point results and the binary state, as well as the corresponding result streams, are all present without the user having written a single line of QUA resource management code.
6. Scaling to Larger Systems
The design philosophy of arbok is that sequence classes are written once and scaled by configuration alone. To extend the single-signal parity_read_p1p2_conf to a four-sensor system with signals p1p2, p3p4, p5p6 and p7p8, the only change required is in the configuration. The ParityRead class is reused without modification.
parity_read_4signal_conf = {
'sequence': ParityRead,
'parameters': {
'gate_elements': {
'type': List,
'value': [
'P1', 'J1', 'P2', 'J2', 'P3',
'P3', 'J3', 'P4', 'J4', 'P5',
'P5', 'J5', 'P6', 'J6', 'P7',
'P7', 'J7', 'P8'
]
},
'readout_elements': {
'type': List,
'value': ['SET1', 'SET2', 'SET3', 'SET4']
},
't_wait_home_before': {'type': Time, 'value': int(1e3 / 4)},
't_wait_pre_read': {'type': Time, 'value': int(10e3 / 4)},
't_wait_post_read': {'type': Time, 'value': int(10e3 / 4)},
't_ramp_to_reference':{'type': Time, 'var_type': 'fixed', 'value': int(1e3 / 4)},
't_ramp_to_read': {'type': Time, 'value': int(0.05e3 / 4)},
't_wait_after_reset': {'type': Time, 'value': int(10e3 / 4)},
'v_home': {
'type': Voltage,
'elements': {
'P1': 0.0, 'J1': 0.0, 'P2': 0.0, 'J2': 0.0,
'P3': 0.0, 'J3': 0.0, 'P4': 0.0, 'J4': 0.0,
'P5': 0.0, 'J5': 0.0, 'P6': 0.0, 'J6': 0.0,
'P7': 0.0, 'J7': 0.0, 'P8': 0.0,
}
},
'v_reference': {
'type': Voltage,
'elements': {
'P1': 0.0, 'J1': 0.0, 'P2': 0.0, 'J2': 0.0,
'P3': 0.0, 'J3': 0.0, 'P4': 0.0, 'J4': 0.0,
'P5': 0.0, 'J5': 0.0, 'P6': 0.0, 'J6': 0.0,
'P7': 0.0, 'J7': 0.0, 'P8': 0.0,
}
},
'v_read': {
'type': Voltage,
'elements': {
'P1': 0.0, 'J1': 0.0, 'P2': 0.0, 'J2': 0.0,
'P3': 0.0, 'J3': 0.0, 'P4': 0.0, 'J4': 0.0,
'P5': 0.0, 'J5': 0.0, 'P6': 0.0, 'J6': 0.0,
'P7': 0.0, 'J7': 0.0, 'P8': 0.0,
}
},
},
'signals': ['p1p2', 'p3p4', 'p5p6', 'p7p8'],
'readout_groups': {
'ref': {
'p1p2': {'readout_class': DcAverage, 'signal': 'p1p2', 'kwargs': {'qua_element': 'SET1'}},
'p3p4': {'readout_class': DcAverage, 'signal': 'p3p4', 'kwargs': {'qua_element': 'SET2'}},
'p5p6': {'readout_class': DcAverage, 'signal': 'p5p6', 'kwargs': {'qua_element': 'SET3'}},
'p7p8': {'readout_class': DcAverage, 'signal': 'p7p8', 'kwargs': {'qua_element': 'SET4'}},
},
'read': {
'p1p2': {'readout_class': DcAverage, 'signal': 'p1p2', 'kwargs': {'qua_element': 'SET1'}},
'p3p4': {'readout_class': DcAverage, 'signal': 'p3p4', 'kwargs': {'qua_element': 'SET2'}},
'p5p6': {'readout_class': DcAverage, 'signal': 'p5p6', 'kwargs': {'qua_element': 'SET3'}},
'p7p8': {'readout_class': DcAverage, 'signal': 'p7p8', 'kwargs': {'qua_element': 'SET4'}},
},
'diff': {
'p1p2': {
'readout_class': Difference, 'signal': 'p1p2',
'kwargs': {
'minuend': 'parity_read.p1p2.ref__p1p2',
'subtrahend': 'parity_read.p1p2.read__p1p2',
},
},
'p3p4': {
'readout_class': Difference, 'signal': 'p3p4',
'kwargs': {
'minuend': 'parity_read.p3p4.ref__p3p4',
'subtrahend': 'parity_read.p3p4.read__p3p4',
},
},
'p5p6': {
'readout_class': Difference, 'signal': 'p5p6',
'kwargs': {
'minuend': 'parity_read.p5p6.ref__p5p6',
'subtrahend': 'parity_read.p5p6.read__p5p6',
},
},
'p7p8': {
'readout_class': Difference, 'signal': 'p7p8',
'kwargs': {
'minuend': 'parity_read.p7p8.ref__p7p8',
'subtrahend': 'parity_read.p7p8.read__p7p8',
},
},
},
'state': {
'p1p2': {
'readout_class': Threshold, 'signal': 'p1p2',
'kwargs': {'charge_readout': 'p1p2.diff__p1p2'},
'parameters': {'threshold': {'type': Voltage, 'value': 0.0}}
},
'p3p4': {
'readout_class': Threshold, 'signal': 'p3p4',
'kwargs': {'charge_readout': 'p3p4.diff__p3p4'},
'parameters': {'threshold': {'type': Voltage, 'value': 0.0}}
},
'p5p6': {
'readout_class': Threshold, 'signal': 'p5p6',
'kwargs': {'charge_readout': 'p5p6.diff__p5p6'},
'parameters': {'threshold': {'type': Voltage, 'value': 0.0}}
},
'p7p8': {
'readout_class': Threshold, 'signal': 'p7p8',
'kwargs': {'charge_readout': 'p7p8.diff__p7p8'},
'parameters': {'threshold': {'type': Voltage, 'value': 0.0}}
},
},
},
}
The four-signal measurement is instantiated in exactly the same way as the single-signal one. A new Measurement and a new ArbokDriver are created and a fresh ParityRead is instantiated with the extended configuration.
qm_driver.reset_measurements()
measurement_8q = Measurement(qm_driver, 'measurement_8q')
parity_read2 = ParityRead(
parent=measurement_8q,
name='parity_read',
sequence_config=parity_read_4signal_conf,
)
Deleting measurement: mock_measurement
The snapshot now shows gettable parameters for all four signals across all four processing steps, with no changes to ParityRead required.
parity_read2.print_readable_snapshot()
qm_driver_measurement_8q_parity_read:
parameter value
--------------------------------------------------------------------------------
diff__p1p2 : Not available
diff__p3p4 : Not available
diff__p5p6 : Not available
diff__p7p8 : Not available
gate_elements : ['P1', 'J1', 'P2', 'J2', 'P3', 'P3', 'J3', 'P4', 'J4...
read__p1p2 : Not available
read__p3p4 : Not available
read__p5p6 : Not available
read__p7p8 : Not available
readout_elements : ['SET1', 'SET2', 'SET3', 'SET4'] (N/A)
ref__p1p2 : Not available
ref__p3p4 : Not available
ref__p5p6 : Not available
ref__p7p8 : Not available
state__p1p2 : Not available
state__p1p2__threshold : 0 (V)
state__p3p4 : Not available
state__p3p4__threshold : 0 (V)
state__p5p6 : Not available
state__p5p6__threshold : 0 (V)
state__p7p8 : Not available
state__p7p8__threshold : 0 (V)
t_ramp_to_read : 12 (s)
t_ramp_to_reference : 250 (s)
t_wait_after_reset : 2500 (s)
t_wait_home_before : 250 (s)
t_wait_post_read : 2500 (s)
t_wait_pre_read : 2500 (s)
v_home_J1 : 0 (V)
v_home_J2 : 0 (V)
v_home_J3 : 0 (V)
v_home_J4 : 0 (V)
v_home_J5 : 0 (V)
v_home_J6 : 0 (V)
v_home_J7 : 0 (V)
v_home_P1 : 0 (V)
v_home_P2 : 0 (V)
v_home_P3 : 0 (V)
v_home_P4 : 0 (V)
v_home_P5 : 0 (V)
v_home_P6 : 0 (V)
v_home_P7 : 0 (V)
v_home_P8 : 0 (V)
v_read_J1 : 0 (V)
v_read_J2 : 0 (V)
v_read_J3 : 0 (V)
v_read_J4 : 0 (V)
v_read_J5 : 0 (V)
v_read_J6 : 0 (V)
v_read_J7 : 0 (V)
v_read_P1 : 0 (V)
v_read_P2 : 0 (V)
v_read_P3 : 0 (V)
v_read_P4 : 0 (V)
v_read_P5 : 0 (V)
v_read_P6 : 0 (V)
v_read_P7 : 0 (V)
v_read_P8 : 0 (V)
v_reference_J1 : 0 (V)
v_reference_J2 : 0 (V)
v_reference_J3 : 0 (V)
v_reference_J4 : 0 (V)
v_reference_J5 : 0 (V)
v_reference_J6 : 0 (V)
v_reference_J7 : 0 (V)
v_reference_P1 : 0 (V)
v_reference_P2 : 0 (V)
v_reference_P3 : 0 (V)
v_reference_P4 : 0 (V)
v_reference_P5 : 0 (V)
v_reference_P6 : 0 (V)
v_reference_P7 : 0 (V)
v_reference_P8 : 0 (V)
Compiling the four-signal program shows that the framework has automatically allocated all required QUA variables and streams for all four sensors.
measurement_8q.available_gettables
[<arbok_driver.parameters.gettable_parameter.GettableParameter: ref__p1p2 at 129173966203920>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: ref__p3p4 at 129173966204240>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: ref__p5p6 at 129173966204560>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: ref__p7p8 at 129173966204880>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: read__p1p2 at 129173966205200>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: read__p3p4 at 129173966205520>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: read__p5p6 at 129173966205840>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: read__p7p8 at 129173966206160>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: diff__p1p2 at 129173966206800>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: diff__p3p4 at 129173966207760>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: diff__p5p6 at 129173966208080>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: diff__p7p8 at 129173966208400>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: state__p1p2 at 129173965177232>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: state__p3p4 at 129173965178512>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: state__p5p6 at 129173965179152>,
<arbok_driver.parameters.gettable_parameter.GettableParameter: state__p7p8 at 129173965179792>]
rprint(measurement_8q.get_qua_program_as_str())
# Single QUA script generated at 2026-04-15 10:21:50.490758 # 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(fixed, ) v3 = declare(fixed, ) v4 = declare(fixed, ) v5 = declare(fixed, ) v6 = declare(fixed, ) v7 = declare(fixed, ) v8 = declare(fixed, ) v9 = declare(fixed, ) v10 = declare(fixed, ) v11 = declare(fixed, ) v12 = declare(fixed, ) v13 = declare(fixed, ) v14 = declare(bool, ) v15 = declare(bool, ) v16 = declare(bool, ) v17 = declare(bool, ) with infinite_loop_(): pause() assign(v1, 0) align() align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") wait(250, "P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8") align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8") wait(2500, "P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") measure("measure", "SET1", integration.full("x_const", v2, "")) measure("measure", "SET2", integration.full("x_const", v3, "")) measure("measure", "SET3", integration.full("x_const", v4, "")) measure("measure", "SET4", integration.full("x_const", v5, "")) align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") wait(2500, "P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8") align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8") wait(2500, "P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") measure("measure", "SET1", integration.full("x_const", v6, "")) measure("measure", "SET2", integration.full("x_const", v7, "")) measure("measure", "SET3", integration.full("x_const", v8, "")) measure("measure", "SET4", integration.full("x_const", v9, "")) align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") align() assign(v10, (v2-v6)) assign(v11, (v3-v7)) assign(v12, (v4-v8)) assign(v13, (v5-v9)) assign(v14, (v10>0.0)) assign(v15, (v11>0.0)) assign(v16, (v12>0.0)) assign(v17, (v13>0.0)) align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") wait(2500, "P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") ramp_to_zero("P1") ramp_to_zero("J1") ramp_to_zero("P2") ramp_to_zero("J2") ramp_to_zero("P3") ramp_to_zero("P3") ramp_to_zero("J3") ramp_to_zero("P4") ramp_to_zero("J4") ramp_to_zero("P5") ramp_to_zero("P5") ramp_to_zero("J5") ramp_to_zero("P6") ramp_to_zero("J6") ramp_to_zero("P7") ramp_to_zero("P7") ramp_to_zero("J7") ramp_to_zero("P8") align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8") wait(2500, "P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") align("P1", "J1", "P2", "J2", "P3", "P3", "J3", "P4", "J4", "P5", "P5", "J5", "P6", "J6", "P7", "P7", "J7", "P8", "SET1", "SET2", "SET3", "SET4") align() r2 = declare_stream() save(v2, r2) r3 = declare_stream() save(v3, r3) r4 = declare_stream() save(v4, r4) r5 = declare_stream() save(v5, r5) r6 = declare_stream() save(v6, r6) r7 = declare_stream() save(v7, r7) r8 = declare_stream() save(v8, r8) r9 = declare_stream() save(v9, r9) r10 = declare_stream() save(v10, r10) r11 = declare_stream() save(v11, r11) r12 = declare_stream() save(v12, r12) r13 = declare_stream() save(v13, r13) r14 = declare_stream() save(v14, r14) r15 = declare_stream() save(v15, r15) r16 = declare_stream() save(v16, r16) r17 = declare_stream() save(v17, r17) align() assign(v1, (v1+1)) r1 = declare_stream() save(v1, r1) align() with stream_processing(): r1.buffer(1).save("qm_driver_measurement_8q_shots") r2.buffer(1).save("qm_driver_measurement_8q_parity_read_ref__p1p2") r3.buffer(1).save("qm_driver_measurement_8q_parity_read_ref__p3p4") r4.buffer(1).save("qm_driver_measurement_8q_parity_read_ref__p5p6") r5.buffer(1).save("qm_driver_measurement_8q_parity_read_ref__p7p8") r6.buffer(1).save("qm_driver_measurement_8q_parity_read_read__p1p2") r7.buffer(1).save("qm_driver_measurement_8q_parity_read_read__p3p4") r8.buffer(1).save("qm_driver_measurement_8q_parity_read_read__p5p6") r9.buffer(1).save("qm_driver_measurement_8q_parity_read_read__p7p8") r10.buffer(1).save("qm_driver_measurement_8q_parity_read_diff__p1p2") r11.buffer(1).save("qm_driver_measurement_8q_parity_read_diff__p3p4") r12.buffer(1).save("qm_driver_measurement_8q_parity_read_diff__p5p6") r13.buffer(1).save("qm_driver_measurement_8q_parity_read_diff__p7p8") r14.buffer(1).save("qm_driver_measurement_8q_parity_read_state__p1p2") r15.buffer(1).save("qm_driver_measurement_8q_parity_read_state__p3p4") r16.buffer(1).save("qm_driver_measurement_8q_parity_read_state__p5p6") r17.buffer(1).save("qm_driver_measurement_8q_parity_read_state__p7p8") config = None loaded_config = None
Going from one sensor to four required only additions to the configuration dictionary. The signals list grew from one entry to four, each readout group gained three new entries, and the ReadSequence class was left completely untouched. This is the central scaling principle of arbok.