Parameter Table¶
ParameterTable is the contract for classical-quantum data — a single definition shared by Python host logic and QUA program logic, avoiding name and shape mismatches in streaming-heavy workflows.
For method signatures, see the Parameter Table API reference.
Purpose¶
Qiskit assumes parameters are bound per circuit run. QUA assumes long-running programs with nested loops, streaming, and explicit buffers. Hybrid workflows (feedback, error correction, QUARC/OPNIC control) need a stable mapping between:
Qiskit circuit parameters and classical input variables, and
QUA variables loaded from streams, IO, or QUARC-backed OPNIC.
ParameterTable and Parameter provide that mapping.
Two parameter categories¶
Both ParameterTable and Parameter share the same canonical verb names, so callers never need to branch on the concrete type:
Concept |
Canonical method |
Deprecated alias |
|---|---|---|
Declare QUA variables |
|
|
Receive / load input |
|
|
Declare output streams |
|
|
Zero-reset QUA variables |
|
|
declare(pause_program=False)Declares all QUA variables in the table. Call this within a QUA program.
Symbolic circuit parameters → real-time QUA variables — build with
ParameterTable.from_qiskit()from a circuit’s symbolic parameters. Values can be updated in real time (phase, amplitude, frame rotation).Custom streamed classical data — define
Parameterobjects directly withInputTypeandDirectionfor host↔device data (e.g. syndrome integers).
rcv(filter_function=None)Loads values from the configured input mechanism (Input Stream, IO, OPNIC).
ASCII parameter names: QOP rejects non-ASCII names. Use theta, phi, not Greek symbols, when building tables from Qiskit circuits.
Lifecycle¶
QUA side:
declare()— create QUA variables insidewith program():(works for bothParameterandParameterTable).Pass the table to
quantum_circuit_to_qua(qc, param_table=...).rcv()— read streamed parameters from the host.stream_back()— push values to output streams.
Python side:
push_to_opx(param_dict, job, qm)— send values to the OPX.fetch_from_opx(job, ...)— retrieve streamed results.
Constructing a ParameterTable¶
We recommend building explicit Parameter objects first and passing a list to ParameterTable. That keeps input_type, direction, and qua_type visible at each field and matches how OPNIC / Quarc emission inspects the table.
You can also pass a dictionary when a quick inline spec is enough. Every key is the parameter name; each value follows one of the shapes below.
Dictionary value shapes¶
Value |
Meaning |
|---|---|
|
Initial value only; |
|
Explicit QUA type: |
|
Adds how the host talks to the OPX: |
|
Required fourth field when |
Rules:
Every parameter in one table must share the same
input_type(and the samedirectionfor OPNIC).A list or 1D numpy array as
valuedefines an array parameter; length islen(value).OPNIC tables cannot use
add_parameters/remove_parameterafter the struct has been emitted (declare()in Flow A, or a pre-bound handle in Flow B).
Recommended — list of Parameter objects¶
from qm.qua import fixed
from qiskit_qm_provider import Parameter, ParameterTable, Direction, InputType
mu = Parameter("mu", [0.0, 0.0], qua_type=fixed, input_type=InputType.OPNIC, direction=Direction.INCOMING)
sigma = Parameter("sigma", [0.1, 0.1], qua_type=fixed, input_type=InputType.OPNIC, direction=Direction.INCOMING)
policy_params = ParameterTable([mu, sigma], name="PolicyParams")
Compile-time angles (no streaming — assigned in QUA or bound from calibration):
gate_params = ParameterTable(
[
Parameter("theta", 0.5, qua_type=fixed),
Parameter("phi", 0.0, qua_type=fixed),
],
name="gate_params",
)
Dictionary shorthand — scalar compile-time¶
# qua_type inferred: 0.0 → fixed, 1 → int, False → bool
cal_table = ParameterTable(
{
"theta": 0.5,
"n_reps": 100,
"flag": False,
},
name="cal",
)
Dictionary shorthand — explicit type¶
from qm.qua import fixed
cal_table = ParameterTable(
{
"theta": (0.5, fixed),
"n_reps": (100, int),
"flag": (False, bool),
},
name="cal",
)
Dictionary shorthand — input stream / IO¶
# Host pushes values before each rcv() in QUA
stream_table = ParameterTable(
{
"input_state_0": (0, int, InputType.INPUT_STREAM),
"observable_0": (0, int, InputType.INPUT_STREAM),
},
name="input_state_vars",
)
# IO registers (job must be paused; see Parameter.push_to_opx)
io_table = ParameterTable(
{
"sync_flag": (False, bool, InputType.IO1),
"status": (0, int, InputType.IO2),
},
name="io_flags",
)
String literals work the same as enums: "INPUT_STREAM", "IO1", "IO2".
Dictionary shorthand — OPNIC packet (four-tuple)¶
policy_params = ParameterTable(
{
"mu": ([0.0, 0.0], "fixed", "OPNIC", "INCOMING"),
"sigma": ([0.1, 0.1], "fixed", "OPNIC", "INCOMING"),
},
name="PolicyParams",
)
Growing a table before declare() — add_parameters¶
Build the core fields first, then attach more Parameter instances before the table is emitted (before declare() for OPNIC, or before any Flow-B handle is bound):
base = ParameterTable(
[Parameter("mu", [0.0, 0.0], input_type=InputType.OPNIC, direction=Direction.INCOMING)],
name="PolicyParams",
)
base.add_parameters(
Parameter("sigma", [0.1, 0.1], input_type=InputType.OPNIC, direction=Direction.INCOMING)
)
# Or merge another table's fields:
extra = ParameterTable(
[Parameter("bias", 0.0, input_type=InputType.OPNIC, direction=Direction.INCOMING)],
name="bias_only",
)
base.add_table(extra) # adds Parameter("bias", ...)
After declare() (or once a Quarc struct handle is bound), add_parameters and remove_parameter raise — Quarc struct layout is append-only.
Other constructors¶
ParameterTable.from_qiskit(qc, ...)— split circuitParameter/Varinputs into tables (see tomography example below).ParameterTable.from_spec(spec_dict)— round-trip from serialized module state.
Minimal example¶
from qiskit_qm_provider import Parameter, ParameterTable, Direction, InputType
Represents a single parameter. Shares the same canonical interface as ParameterTable (see table above).
# Streamed syndrome integer (host ← device)
syndrome_data = Parameter("syndrome_data", 0, input_type=InputType.INPUT_STREAM)
Parameter __init__ arguments:
name: Parameter name.value: Initial Python-side value (also used bypush_to_opx()when called with no argument).qua_type:int,float,bool, orfixed.input_type:InputTypeenum.direction:Directionenum (for OPNIC).
# Recovery circuit parameters (host → device)
recovery_vars = ParameterTable.from_qiskit(
recovery_circuit,
input_type=InputType.INPUT_STREAM,
)
Inside QUA:
declare(pause_program=False): Declares the QUA variable. Alias:declare_variable().rcv(): Loads one value from the input mechanism. Alias:load_input_value().assign(value): Assigns a value (can be QUA variable or constant) to this parameter.save_to_stream(): Saves the parameter’s current value to its declared stream.push_to_opx(value=self.value, ...): Sends a value to the OPX. When called with no positional argument, usesself.value— set it once and callpush_to_opx()without repeating the value.
recovery_vars.declare()
syndrome_data.declare()
syndrome_data.declare_stream()
For the full error-correction loop using these tables, see Error-Correction Workflow.
Splitting one circuit into multiple tables (filter_function)¶
Quantum process tomography and direct fidelity estimation repeat the same pulse program many times while varying two independent choices: which input state to prepare (e.g. the six Pauli eigenstates on the Bloch sphere) and which measurement observable to read out (e.g. Pauli X, Y, Z). A compact real-time circuit encodes both choices as switch blocks driven by classical input variables, but the host must not load every index at once — the observable can stay fixed while input states are swept, and only then move to the next observable.
ParameterTable.from_qiskit() walks both symbolic Parameter objects (gate angles under calibration) and real-time Var inputs (via qc.add_input(...)). The optional filter_function selects which objects belong in a given table:
filter_function: Callable[[Parameter | Var], bool] | None
Call from_qiskit several times on the same circuit with different filters to obtain disjoint tables that share one compiled circuit but are rcv()’d at different depths in the QUA control flow. The same filter can be passed to ParameterTable.rcv() to load only a subset of an already-declared table (not supported for OPNIC tables).
Typical split for tomography:
Table |
Filter |
Loaded when |
|---|---|---|
|
|
Start of each observable group (outer loop) |
|
|
Start of each input-state group (inner loop) |
|
|
Assigned in QUA before the shot loop (calibration angles) |
The host streams one (observable_0, input_state_0) pair per inner iteration, sweeping all 3 × 6 = 18 combinations — without storing the full index grid on the FPGA.
Single-qubit process tomography (Qiskit side)¶
The circuit below is a minimal tomography cell for one qubit: a Pauli-6 input-state switch, a parametrized gate under test (x_cal), a 3-way measurement-basis switch, then measure and reset. Real-time indices enter through add_input; the gate angle is a single symbolic Parameter named a.
from qiskit.circuit import QuantumCircuit, Gate, ClassicalRegister, Parameter as QiskitParameter
from qiskit.circuit.classical import types
from qiskit_qm_provider import Parameter, ParameterTable, InputType
# --- Real-time circuit (one qubit) -----------------------------------------
a = QiskitParameter("a")
x_cal = Gate("x_cal", 1, [a])
qc = QuantumCircuit(1, name="single_qubit_tomography")
meas = ClassicalRegister(1, name="meas_target_0")
qc.add_register(meas)
input_state_0 = qc.add_input("input_state_0", types.Uint(32))
observable_0 = qc.add_input("observable_0", types.Uint(32))
# Pauli-6 input-state preparation (indices 0 … 5)
with qc.switch(input_state_0) as case_input:
with case_input(0):
qc.delay(16, 0) # |0⟩
with case_input(1):
qc.x(0) # |1⟩
with case_input(2):
qc.h(0) # |+⟩
with case_input(3):
qc.h(0)
qc.z(0) # |−⟩
with case_input(4):
qc.h(0)
qc.s(0) # |+i⟩
with case_input(5):
qc.h(0)
qc.sdg(0) # |−i⟩
qc.append(x_cal, [0])
# Pauli measurement basis (0: Z, 1: X, 2: Y)
with qc.switch(observable_0) as case_obs:
with case_obs(0):
qc.delay(16, 0)
with case_obs(1):
qc.h(0)
with case_obs(2):
qc.sdg(0)
qc.h(0)
qc.measure(0, meas[0])
qc.reset(0)
# --- Split one circuit into role-specific tables ---------------------------
input_state_vars = ParameterTable.from_qiskit(
qc,
input_type=InputType.INPUT_STREAM,
filter_function=lambda x: "input" in x.name,
name="input_state_vars",
)
observable_vars = ParameterTable.from_qiskit(
qc,
input_type=InputType.INPUT_STREAM,
filter_function=lambda x: "observable" in x.name,
name="observable_vars",
)
gate_params = ParameterTable.from_qiskit(
qc,
input_type=None, # assigned in QUA (calibration angle)
filter_function=lambda x: isinstance(x, QiskitParameter),
name="gate_params",
)
n_shots = Parameter("n_shots", 100, input_type=InputType.INPUT_STREAM)
On the Python host, push_to_opx is called in lockstep with the QUA loops: for each of the 3 observables and 6 input states, push observable_0, input_state_0, and n_shots. Histograms from each pair are combined offline into expectation values or a process matrix.
Matching QUA program (nested loops, staged loading)¶
The template below exhausts all 3 × 6 tomography settings while keeping only two integer control-flow variables live (input_state_0, observable_0) plus one gate angle — not 18 copies of every index stored on the FPGA at once.
from qm.qua import program, declare, for_, fixed
from qiskit_qm_provider import QMBackend
backend = ... # QMBackend with the transpiled qc above
calibration_angle = declare(fixed) # set once, or loaded from the host before the sweep
with program() as tomography_prog:
input_state_vars.declare()
observable_vars.declare()
gate_params.declare()
n_shots.declare()
o_idx = declare(int)
i_idx = declare(int)
shots = declare(int)
backend.init_macro()
with for_(o_idx, 0, o_idx < 3, o_idx + 1):
observable_vars.rcv() # host pushes observable_0 ∈ {0, 1, 2}
with for_(i_idx, 0, i_idx < 6, i_idx + 1):
input_state_vars.rcv() # host pushes input_state_0 ∈ {0, …, 5}
n_shots.rcv()
gate_params.get_parameter("a").assign(calibration_angle)
with for_(shots, 0, shots < n_shots.var, shots + 1):
backend.quantum_circuit_to_qua(qc, circuit_variables)
# accumulate counts into histogram for this (observable, input_state) pair …
Why the filters matter: a single unfiltered ParameterTable.from_qiskit(qc) would merge input_state_0, observable_0, and a. One rcv() would advance every input stream at once, breaking the nested-loop schedule. Splitting by filter_function keeps each stream aligned with the control-flow depth where its Qiskit switch is evaluated — the natural structure for tomography experiments on long-running QUA programs.
ParameterPool¶
ParameterPool manages unique ids, the global registry of all Parameter and ParameterTable objects, and the single bound Quarc BaseModule slot.
1-field struct promotion in from_quarc_module()¶
When deserializing a Quarc module via ParameterPool.from_quarc_module(module), any struct with exactly one field is automatically promoted to a standalone Parameter instead of a ParameterTable. The wrapper ParameterTable is still created internally and marked as synthetic-standalone, so OPNIC transport (declare, rcv, push_to_opx) delegates through it correctly.
tables = ParameterPool.from_quarc_module(module)
# Scalar quantities (1 field) → Parameter
n_shots = tables["n_shots"] # Parameter, not ParameterTable
n_shots.declare() # works — delegates via synthetic table
n_shots.rcv() # works — calls table._var.recv()
n_shots.value = 1000
n_shots.push_to_opx() # streams n_shots.value = 1000
# Multi-field quantities → ParameterTable (unchanged)
policy = tables["policy_params"] # ParameterTable
policy.declare()
policy.rcv()
This eliminates any manual downconversion of 1-element ParameterTable objects back to Parameter on the deserializing side.
Two pipelines¶
Pipeline 1 — module-first: call
ParameterPool.from_quarc_module(my_module)with a pre-builtBaseModule. The pool wraps each struct as aParameterTable(or promotes toParameterfor 1-field structs) and bindsmy_moduleas the pool’s accumulator.Pipeline 2 — parameters-first: declare
Parameter/ParameterTableobjects withinput_type=OPNIC, then callParameterPool.to_quarc_module()to flush all pending tables onto a freshly-createdBaseModule.
InputType & Direction¶
Supporting types¶
InputType¶
Enum for input mechanisms:
OPNIC: OPNIC (classical host) packet communication.INPUT_STREAM: Standard QOP Input Stream.IO1,IO2: GPIO inputs.ParameterPool— coordinate multiple tables in one program.QUA2DArray/QUAArray— multi-index parameter memory (flattened QUA arrays).ParameterVectornote: OpenQASM 3 exportsParameterVectorelements as individual parameters;from_qiskithandles this transparently.