Measurement outputs (comp.outputs)

Compiled-circuit measurement handles are compilation-local: they wire classical register (and loose clbit) outcomes from qm-qasm’s result_program into QUA variables. They are not runtime/OPNIC deployment parameters.

API reference (autodoc): QuaCircuitCompilation, MeasurementOutcomeTable, MeasurementRegisterField, QuaFieldTable, and scope guards.

See also: Workflows — hybrid programs.

Locality model

Concept

Runtime ParameterTable

comp.outputs (MeasurementOutcomeTable)

Scope

Process/session; registered in ParameterPool (runtime registry)

Per QuaCircuitCompilation; weakref-tracked, not OPNIC-emitted

Purpose

Host↔OPX knobs, OPNIC structs, input streams

Compiler output handles wired from result_program

Typical classical path

stream_back() / fetch_from_opx() on struct/table

save(state_int, stream) — stream processing on host

Name keys

User-chosen struct/table/field names

Circuit output keys (creg names + _bitN loose bits)

Key guidance: matching an OPNIC struct field name to a creg name is optional and usually unnecessary. Same string is allowed (dual namespace) but does not imply the same QUA variable. Use ParameterPool.lookup_runtime_parameter(name) for runtime knobs; use comp.outputs.get_parameter(name) for measurement fields.

Accessor contract (aligned with ParameterTable)

All QUA variable accessors require with program():.

Access

Returns

comp.outputs["c"] / comp.outputs.c

QUA bool var or array (measurement outcome)

comp.outputs.get_parameter("c")

MeasurementRegisterField handle

comp.outputs.get_variable("c")

QUA var (same as ["c"])

comp.outputs.state_ints["c"]

Lazy-packed int QUA scalar

comp.outputs.streams["c"]

Per-field declare_stream() handle

Breaking change: comp.outputs.c.state_int is invalid — comp.outputs.c is the measurement bool var. Use comp.outputs.state_ints["c"] or comp.outputs.get_parameter("c").state_int.

Worked examples

RL reward stream

from qm.qua import program, save

with program() as prog:
    comp = backend.quantum_circuit_to_qua(reward_circuit)
    save(comp.outputs.state_ints["meas"], comp.outputs.streams["meas"])

Host-side RL typically consumes the packed integer stream, not raw bool arrays. Input struct fields (actions) and output streams (rewards) are separate pipelines.

QEC — in-QUA processing first

For error correction, the OPNIC struct you stream_back() is usually not the same object as the raw measurement register from the circuit. Compiler outputs (comp.outputs) hold discriminated bits from result_program; runtime ParameterTable fields hold whatever derived classical data the host decoder needs — detection events, packed syndrome integers, histogram bins, etc. See the Error-correction guide for full walkthroughs.

Minimal pattern — derive before streaming:

from qm.qua import program, assign, declare

with program() as prog:
    comp = backend.quantum_circuit_to_qua(syndrome_circuit)
    syndrome = comp.outputs["syndrome"]          # bool array var — raw readout
    detection_events = declare(int)
    # ... QUA logic combining syndrome bits into detection_events ...
    save(detection_events, comp.outputs.streams["syndrome"])

Recommended pattern — detection events via consecutive-round XOR:

Many decoders expect per-bit changes between rounds, not absolute stabilizer values. Declare runtime tables for staging and streaming, XOR in QUA, and stream only the derived table:

from qm.qua import declare, for_, assign

def update_syndrome_streams(
    circuit,
    comp,
    previous_measurement_outcomes: ParameterTable,  # local history table
    syndrome_data: ParameterTable,  # OPNIC / stream transport
):
    """Update syndrome streams for a given circuit."""
    j = declare(int)
    for creg in circuit.cregs:
        meas_reg = comp.outputs[creg.name]
        syndrome_param = syndrome_data[creg.name]
        prev_meas = previous_measurement_outcomes[creg.name]
        with for_(j, 0, j < creg.size, j + 1):
            assign(
                syndrome_param[j],
                prev_meas[j] ^ meas_reg[j],
            )
            assign(prev_meas[j], meas_reg[j])
    syndrome_data.stream_back(reset=True)

After each comp = backend.quantum_circuit_to_qua(syndrome_circuit), pass comp to update_syndrome_streams(...). The host receives detection events from syndrome_data, not raw comp.outputs bits.

Large registers — prefer state_int for streaming:

For registers with many bits, avoid buffering full bool chains on the stream path. Use the lazy-packed integer instead:

from qm.qua import program, assign

with program() as prog:
    comp = backend.quantum_circuit_to_qua(syndrome_circuit)

    ancilla = syndrome_circuit.cregs[0].name
    assign(syndrome_data.var, comp.outputs.state_ints[ancilla])
    syndrome_data.stream_back(reset=True)

state_int collapses creg.size bits into one scalar — useful for host decoders, lookup tables, and stream_processing() buffers of size 2**creg.size. Keep per-bit bool access when you need XOR or single-stabilizer feedback; switch to state_int when the outcome is consumed as a single label. Details: Error-correction — detection events and state_int.

QEC workflows process syndrome bits in QUA before streaming a derived quantity — no 1:1 creg→OPNIC struct mapping is required.

Optional bridge to OPNIC (explicit only)

When the host must receive data via an OPNIC struct rather than a raw stream:

with program() as prog:
    comp = backend.quantum_circuit_to_qua(qc)
    reward_table.assign({"detection_events": comp.outputs.state_ints["syndrome"]})
    reward_table.stream_back()

Never automatic by name — always an explicit assign.

Re-compile and lifecycle

  • Each quantum_circuit_to_qua call creates a new QuaCircuitCompilation with fresh MeasurementRegisterField objects.

  • comp.rewire_outputs(qc, new_result) refreshes wiring on the same wrapper; size or compilation identity changes invalidate cached state_int / stream handles.

Future extensibility

Today, output keys mirror classical registers (and loose clbits) because that is what Qiskit’s OpenQASM 3 exporter emits — it can only export classical bits as output declarations. qm-qasm already supports the OpenQASM 3 output command more broadly (including non-creg types), so the bottleneck is on the Qiskit side. Once Qiskit’s exporter gains support for richer output types, comp.outputs will surface whatever keys qm-qasm exposes — the user will need to know the expected QUA type for each output key. No changes to the compiler or this provider will be required at that point.

Legacy get_measurement_outcomes remains available; it uses get_parameter() internally and accepts QuaCircuitCompilation or raw CompilationResult.