Skip to content

API reference

Every entry below is generated from a docstring in src/ by mkdocstrings. Signatures, defaults, and type annotations are read from the code rather than written out here, and each heading carries a fold with the source it came from. Members are listed in the order they appear in their file rather than alphabetically, and private names are hidden apart from __init__, whose parameters are folded into the class heading. Every heading is anchored by dotted path, so another page can link to a single member: api-qprogram.md#qprogram.QProgram.play.

The supported surface is qprogram.__all__, the names that resolve directly on the package after import qprogram as qp. Two other kinds of name appear here under a longer dotted path. The waveform, operation, and block classes live in submodules the top level does not re-export, so they are written qp.waveforms.Gaussian, qp.operations.Play, and qp.blocks.Sweep; Call and MeasurementField are the two names from qprogram.operations that the top level re-exports as well. The rest are extension points an integrator needs and a program author does not, reached through their submodule: qp.serialization.register_operation, qp.protocol.validate_tokens, qp.sweeps.validate_source. For the reasoning behind any of these names, read the user guide; this page is the lookup.

Top-level

QProgram

QProgram(
    label: str = "",
    description: str | None = None,
    schema: BusSchema | None = None,
)

Top-level container for a pulse-level quantum program.

The fluent builder for the QProgram AST. Methods like play, measure, and the control-flow context managers (sweep, average, if_) append typed operation and block nodes to the current active block.

Parameters:

  • label (str, default: '' ) –

    Short identifier for the program; surfaced in result metadata and .qp headers.

  • description (str | None, default: None ) –

    Longer description of what the program does.

  • schema (BusSchema | None, default: None ) –

    Schema backing typed bus references. Passing one turns on schema-aware bus validation and lets the .qp writer emit compact bus paths.

Source code in src/qprogram/qprogram.py
def __init__(
    self,
    label: str = "",
    description: str | None = None,
    schema: BusSchema | None = None,
) -> None:
    self.label = label
    self.description = description
    self._body = Block()
    self._block_stack: deque[Block] = deque([self._body])
    self._variables: list[Variable] = []
    self._schema = schema
    # Fragments used by this program, keyed by name. Populated by `call` (transitively,
    # dependencies first — so iteration order is topological) and by the ``.qp`` parser (file
    # order, which is topological too since fragments must be defined before use).
    self._fragments: dict[str, Fragment] = {}
    # Structural-path → 1-based ``.qp`` line for every body node, filled by ``loads()``/
    # ``load()``. Empty for programs built in Python; cleared by `expand` (the
    # expansion restructures the tree, invalidating the recorded paths).
    self._qp_source_map: dict[tuple[int | str, ...], int] = {}
    # Holds the open if_/elif_/else_ chain so a following elif_/else_ can find the right
    # Conditional. The tuple is (open Conditional, parent block); it's cleared whenever something
    # else is appended at that parent level by `_append_to_active`.
    self._pending_conditional: tuple[Conditional, Block] | None = None

body property

body: Block

The root Block containing every operation appended to this program.

schema property

schema: BusSchema | None

The attached BusSchema, or None for a program built from raw-string buses.

At most one schema per program — it defines the chip's elements and bus kinds. The .qp writer uses it to emit bus paths (q[0].drive) rather than quoted strings; plain-string buses keep working either way.

buses property

buses: set[str]

Every bus name referenced anywhere in the program tree.

variables property

variables: list[Variable]

The Variable s declared on this program, in declaration order.

fragments property

fragments: dict[str, Fragment]

The Fragment s used by this program, keyed by name.

Populated by call (including each fragment's own dependencies, registered first) and by loads() for every fragment section in a .qp file. Iteration order is topological: a fragment always appears before any fragment that calls it.

source_map property

source_map: dict[tuple[int | str, ...], int]

The .qp source map: structural path → 1-based line in the parsed file.

Filled by loads() / load() for every node in the body: section (paths follow qprogram.paths() is the body, ints index elements, arm:<i> / else / loop:<i> address conditional arms and parallel loop headers). Empty for programs built in Python and after expand. Because the .qp round-trip preserves structure, a path computed against a built program looks up directly in loads(dumps(p)).source_map. Fragment-internal statements are not mapped (diagnostics always target the expanded body).

variable

variable(
    id: str,
    *,
    label: str | None = None,
    units: str | None = None,
    description: str | None = None,
) -> Variable

Declare a new Variable on this program.

Parameters:

  • id (str) –

    Short identifier matching [A-Za-z_][A-Za-z0-9_]*. Doubles as the .qp identifier and must be unique within the program.

  • label (str | None, default: None ) –

    Human-readable name for plots and results.

  • units (str | None, default: None ) –

    Unit string (e.g. "Hz", "ns").

  • description (str | None, default: None ) –

    Longer free-form description.

Returns:

Raises:

Source code in src/qprogram/qprogram.py
def variable(
    self,
    id: str,  # ruff: ignore[builtin-argument-shadowing]
    *,
    label: str | None = None,
    units: str | None = None,
    description: str | None = None,
) -> Variable:
    """Declare a new [`Variable`][qprogram.Variable] on this program.

    Args:
        id (str): Short identifier matching ``[A-Za-z_][A-Za-z0-9_]*``. Doubles as the ``.qp``
            identifier and must be unique within the program.
        label (str | None): Human-readable name for plots and results.
        units (str | None): Unit string (e.g. ``"Hz"``, ``"ns"``).
        description (str | None): Longer free-form description.

    Returns:
        The new [`Variable`][qprogram.Variable].

    Raises:
        ValidationError: If ``id`` already exists on this program.
    """
    if any(v.id == id for v in self._variables):
        msg = f"Variable {id!r} is already declared on this QProgram"
        raise ValidationError(msg)
    var = Variable(id, label=label, units=units, description=description)
    self._variables.append(var)
    return var

measurement_handles

measurement_handles() -> list[MeasurementHandle]

Return the canonical MeasurementHandle for every measurement in the AST.

Walks the body in declaration order and returns op.handle for each MeasurementOperation. The returned handles are the same Python instances the AST stores — writing per-measurement values via handle._set_value(...) is immediately visible to every MeasurementRef, whether the program was built in Python or loaded from a .qp file.

Returns:

Source code in src/qprogram/qprogram.py
def measurement_handles(self) -> list[MeasurementHandle]:
    """Return the canonical [`MeasurementHandle`][qprogram.MeasurementHandle] for every measurement in the AST.

    Walks the body in declaration order and returns ``op.handle`` for each
    `MeasurementOperation`. The returned handles are the *same Python instances* the AST
    stores — writing per-measurement values via ``handle._set_value(...)`` is immediately visible
    to every [`MeasurementRef`][qprogram.MeasurementRef], whether the program was built in Python or loaded from a
    ``.qp`` file.

    Returns:
        One handle per measurement, in declaration order.
    """
    return [op.handle for op in _walk_measurement_ops(self._body)]

register_vendor classmethod

register_vendor(
    name: str, namespace_cls: type[VendorNamespace]
) -> None

Register a VendorNamespace subclass under name.

After registration, program.<name> returns the namespace on any QProgram instance.

Re-registering the same namespace class under the same name is a no-op (import-time side-effect modules may run twice); registering a different class under a taken name is an error — silently replacing another vendor's namespace would be a supply-chain hazard.

Parameters:

  • name (str) –

    Vendor identifier (also used as the dot-prefix in .qp operation names). Must not be a reserved keyword, the "core" sentinel, or the name of any QProgram attribute (which would make the namespace unreachable — vendor lookup happens in __getattr__, after normal attribute resolution).

  • namespace_cls (type[VendorNamespace]) –

    The VendorNamespace subclass to instantiate lazily.

Raises:

  • ValueError

    If name is reserved, shadows a QProgram attribute, or is already registered to a different namespace class.

Source code in src/qprogram/qprogram.py
@classmethod
def register_vendor(cls, name: str, namespace_cls: type[VendorNamespace]) -> None:
    """Register a [`VendorNamespace`][qprogram.VendorNamespace] subclass under ``name``.

    After registration, ``program.<name>`` returns the namespace on any [`QProgram`][qprogram.QProgram] instance.

    Re-registering the *same* namespace class under the same name is a no-op (import-time
    side-effect modules may run twice); registering a different class under a taken name is
    an error — silently replacing another vendor's namespace would be a supply-chain hazard.

    Args:
        name (str): Vendor identifier (also used as the dot-prefix in ``.qp`` operation names).
            Must not be a reserved keyword, the ``"core"`` sentinel, or the name of any
            [`QProgram`][qprogram.QProgram] attribute (which would make the namespace unreachable — vendor
            lookup happens in ``__getattr__``, after normal attribute resolution).
        namespace_cls (type[VendorNamespace]): The [`VendorNamespace`][qprogram.VendorNamespace] subclass
            to instantiate lazily.

    Raises:
        ValueError: If ``name`` is reserved, shadows a ``QProgram`` attribute, or is already
            registered to a different namespace class.
    """
    existing = cls._vendor_registry.get(name)
    if existing is namespace_cls:
        # idempotent re-registration
        return
    if is_reserved_vendor(name):
        msg = (
            f"vendor name {name!r} is reserved (see qprogram.RESERVED_KEYWORDS plus the "
            f"'core' sentinel); pick a different namespace for this vendor extension"
        )
        raise ValueError(msg)
    # ``hasattr`` covers methods and properties; the frozenset covers the public *instance*
    # attributes assigned in ``__init__`` (invisible on the class but they shadow vendor
    # dispatch on every instance, since ``__getattr__`` only runs after normal lookup fails).
    if hasattr(cls, name) or name in _PUBLIC_INSTANCE_ATTRS:
        msg = (
            f"vendor name {name!r} collides with a QProgram attribute; the namespace would "
            f"be unreachable because normal attribute lookup wins over vendor dispatch"
        )
        raise ValueError(msg)
    if existing is not None:
        msg = (
            f"vendor name {name!r} is already registered to "
            f"{existing.__module__}.{existing.__qualname__}; refusing to replace it"
        )
        raise ValueError(msg)
    cls._vendor_registry[name] = namespace_cls

play

play(
    bus: str, waveform: Waveform | IQWaveform | str
) -> None

Append a Play op — play a waveform on a bus.

Parameters:

  • bus (str) –

    Bus to play on.

  • waveform (Waveform | IQWaveform | str) –

    Concrete waveform, or a string alias resolved later by with_waveforms.

Raises:

  • ValidationError

    If bus comes from another schema, or a concrete waveform's channel count does not match the bus's (an IQ pulse on a single-channel bus, or vice versa).

Source code in src/qprogram/qprogram.py
def play(self, bus: str, waveform: Waveform | IQWaveform | str) -> None:
    """Append a [`Play`][qprogram.operations.Play] op — play a waveform on a bus.

    Args:
        bus (str): Bus to play on.
        waveform (Waveform | IQWaveform | str): Concrete waveform, or a string alias resolved
            later by `with_waveforms`.

    Raises:
        ValidationError: If ``bus`` comes from another schema, or a concrete waveform's channel
            count does not match the bus's (an IQ pulse on a single-channel bus, or vice versa).
    """
    self._validate_bus(bus)
    _validate_waveform_channel(bus, waveform)
    self._append_to_active(Play(bus=bus, waveform=waveform))

measure

measure(
    bus: str,
    waveform: IQWaveform | str,
    weights: IQWaveform | str,
    *,
    name: str | None = None,
    fields: Iterable[MeasurementField] = (
        MeasurementField.IQ,
    ),
) -> MeasurementHandle

Play a readout pulse, acquire the result, and return a stable handle.

Parameters:

  • bus (str) –

    Readout bus (must have acquires=True).

  • waveform (IQWaveform | str) –

    Readout pulse — concrete IQWaveform or a string alias.

  • weights (IQWaveform | str) –

    Integration weights — same shape options as waveform.

  • name (str | None, default: None ) –

    Explicit handle name. When omitted, an auto-name is allocated using the convention described on _allocate_measurement_name.

  • fields (Iterable[MeasurementField], default: (IQ,) ) –

    Which measurement fields to produce — an iterable of MeasurementField members (registered field-name strings are also accepted, which is how vendors extend the set). Default (MeasurementField.IQ,); STATE requests classification, RAW the raw ADC trace. Order and duplicates don't matter — the stored tuple is canonical. An unknown field name raises ValidationError here, at the call site.

Returns:

Raises:

  • ValidationError

    If bus has no ADC, comes from another schema, a waveform's channel count does not match the bus's, name collides with another measurement, or fields is a bare string, is not iterable, requests nothing, or names something other than a registered field.

Source code in src/qprogram/qprogram.py
def measure(
    self,
    bus: str,
    waveform: IQWaveform | str,
    weights: IQWaveform | str,
    *,
    name: str | None = None,
    fields: Iterable[MeasurementField] = (MeasurementField.IQ,),
) -> MeasurementHandle:
    """Play a readout pulse, acquire the result, and return a stable handle.

    Args:
        bus (str): Readout bus (must have ``acquires=True``).
        waveform (IQWaveform | str): Readout pulse — concrete
            [`IQWaveform`][qprogram.waveforms.IQWaveform] or a string alias.
        weights (IQWaveform | str): Integration weights — same shape options as ``waveform``.
        name (str | None): Explicit handle name. When omitted, an auto-name is allocated using
            the convention described on `_allocate_measurement_name`.
        fields (Iterable[MeasurementField], optional): Which measurement fields to produce — an
            iterable of `MeasurementField` members (registered field-name
            strings are also accepted, which is how vendors extend the set). Default
            ``(MeasurementField.IQ,)``; `STATE` requests
            classification, `RAW` the raw ADC trace. Order and
            duplicates don't matter — the stored tuple is canonical. An unknown field name
            raises [`ValidationError`][qprogram.ValidationError] here, at the call site.

    Returns:
        The [`MeasurementHandle`][qprogram.MeasurementHandle] identifying this measurement; pass it to
        ``result.get(...)``.

    Raises:
        ValidationError: If ``bus`` has no ADC, comes from another schema, a waveform's channel
            count does not match the bus's, ``name`` collides with another measurement, or
            ``fields`` is a bare string, is not iterable, requests nothing, or names something
            other than a registered field.
    """
    self._validate_bus(bus)
    _validate_acquires(bus)
    _validate_waveform_channel(bus, waveform)
    _validate_waveform_channel(bus, weights)
    allocated = self._allocate_measurement_name(bus, requested=name)
    handle = MeasurementHandle(allocated)
    handle._auto_named = name is None
    self._append_to_active(
        Measure(bus=bus, waveform=waveform, weights=weights, handle=handle, fields=fields),
    )
    return handle

wait

wait(bus: str, duration: int | Expression) -> None

Append a Wait — idle on bus for duration ns.

Parameters:

  • bus (str) –

    Bus to idle on.

  • duration (int | Expression) –

    Wait duration in nanoseconds. Accepts an Expression for sweeps.

Raises:

Source code in src/qprogram/qprogram.py
def wait(self, bus: str, duration: int | Expression) -> None:
    """Append a [`Wait`][qprogram.operations.Wait] — idle on ``bus`` for ``duration`` ns.

    Args:
        bus (str): Bus to idle on.
        duration (int | Expression): Wait duration in nanoseconds. Accepts an
            [`Expression`][qprogram.Expression] for sweeps.

    Raises:
        ValidationError: If ``bus`` comes from another schema.
    """
    self._validate_bus(bus)
    self._append_to_active(Wait(bus=bus, duration=duration))

sync

sync(buses: list[str] | None = None) -> None

Append a Sync — synchronize buses to a common time reference.

Parameters:

  • buses (list[str] | None, default: None ) –

    Buses to sync, or None to sync every bus currently active in the program.

Raises:

  • ValidationError

    If buses is an empty list — ambiguous between "sync nothing" and "sync everything"; pass None for the sync-all form. Also if a listed bus comes from another schema.

Source code in src/qprogram/qprogram.py
def sync(self, buses: list[str] | None = None) -> None:
    """Append a [`Sync`][qprogram.operations.Sync] — synchronize buses to a common time reference.

    Args:
        buses (list[str] | None): Buses to sync, or ``None`` to sync every bus currently active
            in the program.

    Raises:
        ValidationError: If ``buses`` is an empty list — ambiguous between "sync nothing"
            and "sync everything"; pass ``None`` for the sync-all form. Also if a listed bus
            comes from another schema.
    """
    # The user-facing keyword is ``buses`` for readability; the AST attribute is ``targets``
    # (see `Sync`).
    if buses is not None and len(buses) == 0:
        msg = "sync([]) is ambiguous; pass None (or no argument) to sync all buses"
        raise ValidationError(msg)
    if buses:
        for b in buses:
            self._validate_bus(b)
    self._append_to_active(Sync(targets=buses))

set_frequency

set_frequency(
    bus: str, frequency: float | Expression
) -> None

Append a SetFrequency — retune the NCO on bus.

Parameters:

  • bus (str) –

    Bus whose oscillator to retune.

  • frequency (float | Expression) –

    New frequency in Hz. Accepts an Expression for sweeps.

Raises:

Source code in src/qprogram/qprogram.py
def set_frequency(self, bus: str, frequency: float | Expression) -> None:
    """Append a [`SetFrequency`][qprogram.operations.SetFrequency] — retune the NCO on ``bus``.

    Args:
        bus (str): Bus whose oscillator to retune.
        frequency (float | Expression): New frequency in Hz. Accepts an
            [`Expression`][qprogram.Expression] for sweeps.

    Raises:
        ValidationError: If ``bus`` comes from another schema.
    """
    self._validate_bus(bus)
    self._append_to_active(SetFrequency(bus=bus, frequency=frequency))

set_phase

set_phase(bus: str, phase: float | Expression) -> None

Append a SetPhase — set the NCO phase on bus.

Parameters:

  • bus (str) –

    Bus whose oscillator phase to set.

  • phase (float | Expression) –

    Phase in radians. Accepts an Expression for sweeps.

Raises:

Source code in src/qprogram/qprogram.py
def set_phase(self, bus: str, phase: float | Expression) -> None:
    """Append a [`SetPhase`][qprogram.operations.SetPhase] — set the NCO phase on ``bus``.

    Args:
        bus (str): Bus whose oscillator phase to set.
        phase (float | Expression): Phase in radians. Accepts an
            [`Expression`][qprogram.Expression] for sweeps.

    Raises:
        ValidationError: If ``bus`` comes from another schema.
    """
    self._validate_bus(bus)
    self._append_to_active(SetPhase(bus=bus, phase=phase))

reset_phase

reset_phase(bus: str) -> None

Append a ResetPhase — reset the NCO phase on bus to zero.

Parameters:

  • bus (str) –

    Bus whose oscillator phase to reset.

Raises:

Source code in src/qprogram/qprogram.py
def reset_phase(self, bus: str) -> None:
    """Append a [`ResetPhase`][qprogram.operations.ResetPhase] — reset the NCO phase on ``bus`` to zero.

    Args:
        bus (str): Bus whose oscillator phase to reset.

    Raises:
        ValidationError: If ``bus`` comes from another schema.
    """
    self._validate_bus(bus)
    self._append_to_active(ResetPhase(bus=bus))

set_gain

set_gain(bus: str, gain: float | Expression) -> None

Append a SetGain — set the output gain on bus.

Parameters:

  • bus (str) –

    Bus whose output gain to set.

  • gain (float | Expression) –

    New gain. Accepts an Expression for sweeps.

Raises:

Source code in src/qprogram/qprogram.py
def set_gain(self, bus: str, gain: float | Expression) -> None:
    """Append a [`SetGain`][qprogram.operations.SetGain] — set the output gain on ``bus``.

    Args:
        bus (str): Bus whose output gain to set.
        gain (float | Expression): New gain. Accepts an [`Expression`][qprogram.Expression] for
            sweeps.

    Raises:
        ValidationError: If ``bus`` comes from another schema.
    """
    self._validate_bus(bus)
    self._append_to_active(SetGain(bus=bus, gain=gain))

set_offset

set_offset(
    bus: str,
    offset_path0: float | Expression,
    offset_path1: float | Expression | None = None,
) -> None

Append a SetOffset — set DC offset on one or both paths of bus.

Parameters:

  • bus (str) –

    Bus whose DC offset to set.

  • offset_path0 (float | Expression) –

    Offset on path 0 (the only path for single-channel buses, I for IQ buses).

  • offset_path1 (float | Expression | None, default: None ) –

    Offset on path 1 (Q for IQ buses). None leaves that path's offset unchanged.

Raises:

Source code in src/qprogram/qprogram.py
def set_offset(
    self,
    bus: str,
    offset_path0: float | Expression,
    offset_path1: float | Expression | None = None,
) -> None:
    """Append a [`SetOffset`][qprogram.operations.SetOffset] — set DC offset on one or both paths of ``bus``.

    Args:
        bus (str): Bus whose DC offset to set.
        offset_path0 (float | Expression): Offset on path 0 (the only path for single-channel
            buses, I for IQ buses).
        offset_path1 (float | Expression | None): Offset on path 1 (Q for IQ buses). ``None``
            leaves that path's offset unchanged.

    Raises:
        ValidationError: If ``bus`` comes from another schema.
    """
    self._validate_bus(bus)
    self._append_to_active(SetOffset(bus=bus, offset_path0=offset_path0, offset_path1=offset_path1))

set_parameter

set_parameter(
    bus: str, parameter: str, value: float | Expression
) -> None

Append a SetParameter — write a bus-scoped parameter.

A parameter write is platform configuration rather than a real-time instruction, so platforms expose it host-side only.

Parameters:

  • bus (str) –

    The bus whose parameter is written.

  • parameter (str) –

    Name of the parameter to write.

  • value (float | Expression) –

    New value. Accepts an Expression for sweeps.

Raises:

Source code in src/qprogram/qprogram.py
def set_parameter(
    self,
    bus: str,
    parameter: str,
    value: float | Expression,
) -> None:
    """Append a [`SetParameter`][qprogram.operations.SetParameter] — write a bus-scoped parameter.

    A parameter write is platform configuration rather than a real-time instruction, so
    platforms expose it host-side only.

    Args:
        bus (str): The bus whose parameter is written.
        parameter (str): Name of the parameter to write.
        value (float | Expression): New value. Accepts an [`Expression`][qprogram.Expression] for
            sweeps.

    Raises:
        ValidationError: If ``bus`` comes from another schema.
    """
    self._validate_bus(bus)
    self._append_to_active(SetParameter(bus=bus, parameter=parameter, value=value))

get_parameter

get_parameter(bus: str, parameter: str) -> Variable

Append a GetParameter and return the freshly-declared variable.

Derives a unique variable id from f"{bus}_{parameter}", replacing non-word characters with underscores and appending a numeric suffix on collision; the original bus.parameter form is kept on the variable's label for traceability.

Parameters:

  • bus (str) –

    The bus whose parameter is read.

  • parameter (str) –

    Name of the parameter to read.

Returns:

Raises:

  • ValidationError

    If bus comes from another schema, or the derived id is not a valid Variable id — which is what a bus or parameter name carrying letters or digits outside ASCII produces.

Source code in src/qprogram/qprogram.py
def get_parameter(self, bus: str, parameter: str) -> Variable:
    """Append a [`GetParameter`][qprogram.operations.GetParameter] and return the freshly-declared variable.

    Derives a unique variable id from ``f"{bus}_{parameter}"``, replacing non-word characters
    with underscores and appending a numeric suffix on collision; the original
    ``bus.parameter`` form is kept on the variable's `label` for traceability.

    Args:
        bus (str): The bus whose parameter is read.
        parameter (str): Name of the parameter to read.

    Returns:
        The [`Variable`][qprogram.Variable] the runtime populates with the read value.

    Raises:
        ValidationError: If ``bus`` comes from another schema, or the derived id is not a valid
            [`Variable`][qprogram.Variable] id — which is what a bus or parameter name carrying letters or
            digits outside ASCII produces.
    """
    self._validate_bus(bus)
    base = _sanitize_id(f"{bus}_{parameter}")
    existing = {v.id for v in self._variables}
    var_id = base
    n = 2
    while var_id in existing:
        var_id = f"{base}_{n}"
        n += 1
    var = self.variable(var_id, label=f"{bus}.{parameter}")
    self._append_to_active(GetParameter(variable=var, bus=bus, parameter=parameter))
    return var

sweep

sweep(variable: Variable) -> _SweepBuilder
sweep(
    variable: Variable, source: SweepSource
) -> _LoopContext
sweep(
    variable: Variable,
    source: SweepSource | _Unset = _UNSET,
) -> _SweepBuilder | _LoopContext

Open a Sweep binding variable to a source's values.

The DSL's only loop. What varies between a linear ramp, an explicit table, a log-spaced set and a composed pattern is the source, not the block — and there are two equal-billing ways to say which source.

Pick the values fluently, which is the shorter spelling and needs no source class in scope::

with program.sweep(freq).from_range(4e9, 6e9, 1e6):
    ...
with program.sweep(amp).from_linspace(0.0, 1.0, num=101):
    ...
with program.sweep(det).from_logspace(1e6, 1e9, num=50):
    ...
with program.sweep(phi).from_values(calibrated_phases):
    ...
with program.sweep(phi).from_file("phases.npy"):
    ...
with program.sweep(phi).from_values(base).rotate(by=1).repeat(3):
    ...

Or pass the source object, which is what you want when you are computing the source rather than writing it out — and the form that reaches combinator nestings the fluent rotate / repeat shortcuts don't::

with program.sweep(freq, qp.Range(4e9, 6e9, 1e6)):
    ...
with program.sweep(phi, qp.Concat(qp.Rotate(base, by=i) for i in range(4))):
    ...
with program.sweep(freq, source):  # held in a variable, from a scan spec, ...
    ...

Both build the identical Sweep node and serialize to the identical .qp line. Every registered source has a fluent builder, vendor sources included — see _SweepBuilder.

Use | on the returned context manager to compose several sweeps into a Parallel block that advances them in lockstep.

Parameters:

  • variable (Variable) –

    The Variable rebound each iteration.

  • source (SweepSource, default: _UNSET ) –

    A SweepSource. A bare 1-D sequence is accepted as shorthand for Values. Omit it to get a _SweepBuilder and pick the values with a from_* method instead.

Returns:

Raises:

  • ValidationError

    If source is given but is neither a sweep source nor a 1-D sequence of values.

Source code in src/qprogram/qprogram.py
def sweep(self, variable: Variable, source: SweepSource | _Unset = _UNSET) -> _SweepBuilder | _LoopContext:
    """Open a [`Sweep`][qprogram.blocks.Sweep] binding ``variable`` to a source's values.

    The DSL's only loop. What varies between a linear ramp, an explicit table, a log-spaced set
    and a composed pattern is the *source*, not the block — and there are two equal-billing ways
    to say which source.

    Pick the values fluently, which is the shorter spelling and needs no source class in scope::

        with program.sweep(freq).from_range(4e9, 6e9, 1e6):
            ...
        with program.sweep(amp).from_linspace(0.0, 1.0, num=101):
            ...
        with program.sweep(det).from_logspace(1e6, 1e9, num=50):
            ...
        with program.sweep(phi).from_values(calibrated_phases):
            ...
        with program.sweep(phi).from_file("phases.npy"):
            ...
        with program.sweep(phi).from_values(base).rotate(by=1).repeat(3):
            ...

    Or pass the source object, which is what you want when you are *computing* the source rather
    than writing it out — and the form that reaches combinator nestings the fluent
    `rotate` / `repeat` shortcuts don't::

        with program.sweep(freq, qp.Range(4e9, 6e9, 1e6)):
            ...
        with program.sweep(phi, qp.Concat(qp.Rotate(base, by=i) for i in range(4))):
            ...
        with program.sweep(freq, source):  # held in a variable, from a scan spec, ...
            ...

    Both build the identical [`Sweep`][qprogram.blocks.Sweep] node and serialize to the identical
    ``.qp`` line. Every registered source has a fluent builder, vendor sources included — see
    `_SweepBuilder`.

    Use ``|`` on the returned context manager to compose several sweeps into a
    [`Parallel`][qprogram.blocks.Parallel] block that advances them in lockstep.

    Args:
        variable (Variable): The [`Variable`][qprogram.Variable] rebound each iteration.
        source (SweepSource, optional): A [`SweepSource`][qprogram.SweepSource]. A bare 1-D
            sequence is accepted as shorthand for [`Values`][qprogram.Values]. Omit it to
            get a `_SweepBuilder` and pick the values with a ``from_*`` method instead.

    Returns:
        A context manager opening the sweep block, or — when ``source`` is omitted — the
        `_SweepBuilder` that produces one.

    Raises:
        ValidationError: If ``source`` is given but is neither a sweep source nor a 1-D sequence
            of values.
    """
    if isinstance(source, _Unset):
        return _SweepBuilder(self, variable)
    return _LoopContext(self, Sweep(variable=variable, source=source))

average

average(shots: int) -> _AverageContext

Open an averaging block that repeats its body shots times and averages the results.

Parameters:

  • shots (int) –

    How many times to repeat the body.

Returns:

  • _AverageContext

    The context manager that opens the Average block.

Source code in src/qprogram/qprogram.py
def average(self, shots: int) -> _AverageContext:
    """Open an averaging block that repeats its body ``shots`` times and averages the results.

    Args:
        shots (int): How many times to repeat the body.

    Returns:
        The context manager that opens the [`Average`][qprogram.blocks.Average] block.
    """
    return _AverageContext(self, shots)

block

block() -> _BlockContext

Open a generic grouping block — a container that carries no semantics of its own.

Returns:

  • _BlockContext

    The context manager that opens the Block.

Source code in src/qprogram/qprogram.py
def block(self) -> _BlockContext:
    """Open a generic grouping block — a container that carries no semantics of its own.

    Returns:
        The context manager that opens the [`Block`][qprogram.blocks.Block].
    """
    return _BlockContext(self)

if_

if_(condition: Expression) -> _IfContext

Open an if arm gated on a measurement-state predicate.

Build chains with sequential with blocks::

with program.if_(m.state == 0):
    program.play(q[0].drive, "id_pulse")
with program.elif_(m.state == 1):
    program.play(q[0].drive, "pi_pulse")
with program.else_():
    pass

The producing measurement must request state classification (fields must include STATE); the validator emits missing-classification otherwise.

Parameters:

Returns:

  • _IfContext

    The context manager that opens the conditional's first arm.

Raises:

  • ValidationError

    If condition is anything other than a comparison of a measurement-state reference against an int literal.

Source code in src/qprogram/qprogram.py
def if_(self, condition: Expression) -> _IfContext:
    """Open an ``if`` arm gated on a measurement-state predicate.

    Build chains with sequential ``with`` blocks::

        with program.if_(m.state == 0):
            program.play(q[0].drive, "id_pulse")
        with program.elif_(m.state == 1):
            program.play(q[0].drive, "pi_pulse")
        with program.else_():
            pass

    The producing measurement **must** request state classification (``fields`` must include
    `STATE`); the validator emits ``missing-classification``
    otherwise.

    Args:
        condition (Expression): A [`Comparison`][qprogram.Comparison] between a
            [`MeasurementRef`][qprogram.MeasurementRef] (from ``handle.state``) and an ``int`` literal.
            That is the only accepted shape.

    Returns:
        The context manager that opens the conditional's first arm.

    Raises:
        ValidationError: If ``condition`` is anything other than a comparison of a
            measurement-state reference against an ``int`` literal.
    """
    self._validate_conditional_condition(condition, where="if_")
    return _IfContext(self, condition)

elif_

elif_(condition: Expression) -> _ElifContext

Extend the open if_ chain with another arm.

Must appear immediately after the matching if_() / elif_() at the same nesting level; any other append in between closes the chain. Condition shape is the same as if_.

Parameters:

  • condition (Expression) –

    The arm's condition, in the shape if_ documents.

Returns:

  • _ElifContext

    The context manager that opens the new arm.

Raises:

  • ValidationError

    If condition has the wrong shape, no conditional chain is open at this nesting level, or the chain already has an else_() arm.

Source code in src/qprogram/qprogram.py
def elif_(self, condition: Expression) -> _ElifContext:
    """Extend the open ``if_`` chain with another arm.

    Must appear immediately after the matching ``if_()`` / ``elif_()`` at the same nesting level;
    any other append in between closes the chain. Condition shape is the same as `if_`.

    Args:
        condition (Expression): The arm's condition, in the shape `if_` documents.

    Returns:
        The context manager that opens the new arm.

    Raises:
        ValidationError: If ``condition`` has the wrong shape, no conditional chain is open at
            this nesting level, or the chain already has an ``else_()`` arm.
    """
    self._validate_conditional_condition(condition, where="elif_")
    return _ElifContext(self, condition)

else_

else_() -> _ElseContext

Close the open if_ chain with an unconditional arm.

Must appear immediately after the matching if_() / elif_() at the same nesting level. At most one else_() per chain.

Returns:

  • _ElseContext

    The context manager that opens the else body.

Raises:

  • ValidationError

    If no conditional chain is open at this nesting level, or the chain already has an else_() arm.

Source code in src/qprogram/qprogram.py
def else_(self) -> _ElseContext:
    """Close the open ``if_`` chain with an unconditional arm.

    Must appear immediately after the matching ``if_()`` / ``elif_()`` at the same nesting level.
    At most one ``else_()`` per chain.

    Returns:
        The context manager that opens the ``else`` body.

    Raises:
        ValidationError: If no conditional chain is open at this nesting level, or the chain
            already has an ``else_()`` arm.
    """
    return _ElseContext(self)

call

call(
    fragment: Fragment, *args: object, **kwargs: object
) -> None

Instantiate a Fragment at the current position.

Appends a first-class Call node — the fragment definition and the call site both survive serialization and round-trip through .qp. Use expand to lower every call into the substituted fragment body.

Arguments bind to the fragment's parameters with the Python calling convention (positional in declaration order, then keywords). Accepted values: numbers, expressions/variables, buses (strings or BusRef), and waveforms.

The fragment (and, transitively, any fragment it calls) is registered on this program so the .qp writer can emit its definition.

Parameters:

  • fragment (Fragment) –

    The fragment to call.

  • *args (object, default: () ) –

    Positional arguments, bound in parameter declaration order.

  • **kwargs (object, default: {} ) –

    Keyword arguments, bound by parameter name.

Raises:

  • ValidationError

    On a non-Fragment argument, a binding error (missing/extra/duplicate parameter, unsupported value type), a name clash with a different fragment already used by this program, a schema mismatch, or a call cycle.

Source code in src/qprogram/qprogram.py
def call(self, fragment: Fragment, *args: object, **kwargs: object) -> None:
    """Instantiate a [`Fragment`][qprogram.Fragment] at the current position.

    Appends a first-class [`Call`][qprogram.operations.Call] node — the fragment definition and
    the call site both survive serialization and round-trip through ``.qp``. Use
    `expand` to lower every call into the substituted fragment body.

    Arguments bind to the fragment's parameters with the Python calling convention (positional
    in declaration order, then keywords). Accepted values: numbers, expressions/variables,
    buses (strings or [`BusRef`][qprogram.BusRef]), and waveforms.

    The fragment (and, transitively, any fragment it calls) is registered on this program so
    the ``.qp`` writer can emit its definition.

    Args:
        fragment (Fragment): The fragment to call.
        *args (object): Positional arguments, bound in parameter declaration order.
        **kwargs (object): Keyword arguments, bound by parameter name.

    Raises:
        ValidationError: On a non-Fragment argument, a binding error (missing/extra/duplicate
            parameter, unsupported value type), a name clash with a different fragment already
            used by this program, a schema mismatch, or a call cycle.
    """
    from qprogram.fragments import Fragment, bind_arguments  # ruff: ignore[import-outside-top-level]
    from qprogram.operations.call import Call  # ruff: ignore[import-outside-top-level]

    if not isinstance(fragment, Fragment):
        msg = f"call() expects a Fragment, got {type(fragment).__name__}"
        raise ValidationError(msg)
    if fragment is self:
        msg = f"fragment {fragment.name!r} cannot call itself"
        raise ValidationError(msg)
    bound = bind_arguments(fragment, args, kwargs)
    for value in bound.values():
        if isinstance(value, BusRef):
            self._validate_bus(value)
    self._reconcile_fragment_schema(fragment)
    self._register_fragment(fragment, _stack=())
    self._append_to_active(Call(fragment=fragment, arguments=bound))

expand

expand() -> QProgram

Return a deep copy with every fragment Call inlined.

The canonical lowering from the composed form to a fragment-free program: each call site is replaced by a plain Block containing the fragment body with parameters substituted by the bound arguments. Fragment-local variables are hygienically renamed onto this program ({fragment}_{id}, numeric suffix on collision); colliding measurement names gain a _2 / _3 suffix (the shared handle is renamed, keeping handle.state conditionals consistent). Nested calls expand recursively; expansion is deterministic, so expanding twice yields structurally equal programs.

A program with no calls is deep-copied and returned unchanged in structure.

Returns:

  • QProgram

    A new, fragment-free QProgram; the original is untouched. Its fragment

  • QProgram

    registry and its .qp source map are both empty either way, because expansion

  • QProgram

    restructures the tree the recorded paths address.

Raises:

  • ValidationError

    On a fragment call cycle or a binding used in an incompatible position (e.g. a waveform bound to a parameter used inside arithmetic).

Source code in src/qprogram/qprogram.py
def expand(self) -> QProgram:
    """Return a deep copy with every fragment [`Call`][qprogram.operations.Call] inlined.

    The canonical lowering from the composed form to a fragment-free program: each call site is
    replaced by a plain [`Block`][qprogram.blocks.Block] containing the fragment body with parameters substituted
    by the bound arguments. Fragment-local variables are hygienically renamed onto this program
    (``{fragment}_{id}``, numeric suffix on collision); colliding measurement names gain a
    ``_2`` / ``_3`` suffix (the shared handle is renamed, keeping ``handle.state`` conditionals
    consistent). Nested calls expand recursively; expansion is deterministic, so expanding twice
    yields structurally equal programs.

    A program with no calls is deep-copied and returned unchanged in structure.

    Returns:
        A new, fragment-free [`QProgram`][qprogram.QProgram]; the original is untouched. Its fragment
        registry and its ``.qp`` source map are both empty either way, because expansion
        restructures the tree the recorded paths address.

    Raises:
        ValidationError: On a fragment call cycle or a binding used in an incompatible
            position (e.g. a waveform bound to a parameter used inside arithmetic).
    """
    from qprogram.fragments import expand_program  # ruff: ignore[import-outside-top-level]

    return expand_program(self)

rebind

rebind(
    *,
    schema: BusSchema | None = None,
    elements: Mapping[
        tuple[str, int | tuple[int, ...]],
        tuple[str, int | tuple[int, ...]],
    ]
    | None = None,
    naming: BusNaming | None = None,
    strings: Mapping[str, str] | None = None,
    allow_unported_strings: bool = False,
) -> QProgram

Return a copy of this program with its bus references re-resolved structurally.

Rather than rewriting bus strings, rebind re-resolves every schema-backed BusRef through a schema factory, so the result stays a typed BusRef (serializing as a q[1].drive path, not a quoted string) and can re-index a qubit, move to a different element, swap naming conventions, or move onto another chip's schema — all checked against the schema (an absent bus kind raises AttributeError).

Auto-allocated measurement names embed the bus (q0/readout/m0); rebind re-derives them for the rebound buses while leaving user-supplied names untouched (see MeasurementHandle). Fragment calls are expanded first.

Parameters:

  • schema (BusSchema | None, default: None ) –

    Target schema. Defaults to the program's current schema (re-index within one chip).

  • elements (Mapping[tuple[str, int | tuple[int, ...]], tuple[str, int | tuple[int, ...]]] | None, default: None ) –

    Maps (element, idx) to (element, idx) — e.g. {("q", 0): ("q", 1)} to port qubit 0's operations onto qubit 1. Unlisted (element, idx) pairs pass through.

  • naming (BusNaming | None, default: None ) –

    Re-resolve every ref under a new BusNaming (cross-platform names).

  • strings (Mapping[str, str] | None, default: None ) –

    Escape hatch for raw-string buses (which carry no schema metadata): an old→new map. Map a string to itself to mark it intentionally untouched.

  • allow_unported_strings (bool, default: False ) –

    When False (default), a raw-string bus not covered by strings raises — a partial port is a loud choice, not a silent accident. Set True to leave uncovered raw-string buses in place.

Returns:

Raises:

  • ValidationError

    If naming is given without a schema, or raw-string buses are left unported without allow_unported_strings.

  • AttributeError

    If a rebound (element, idx, kind) does not resolve against the target schema (e.g. the target element lacks that bus kind).

  • KeyError

    If the target schema's naming pattern names a placeholder other than {element}, {index} or {kind}.

  • ValueError

    If the target schema's naming pattern is not a well-formed format string.

Source code in src/qprogram/qprogram.py
def rebind(
    self,
    *,
    schema: BusSchema | None = None,
    elements: Mapping[tuple[str, int | tuple[int, ...]], tuple[str, int | tuple[int, ...]]] | None = None,
    naming: BusNaming | None = None,
    strings: Mapping[str, str] | None = None,
    allow_unported_strings: bool = False,
) -> QProgram:
    """Return a copy of this program with its bus references re-resolved structurally.

    Rather than rewriting bus *strings*, ``rebind`` re-resolves every schema-backed
    [`BusRef`][qprogram.BusRef] through a schema factory, so the result stays a typed ``BusRef``
    (serializing as a ``q[1].drive`` path, not a quoted string) and can re-index a qubit, move to a
    different element, swap naming conventions, or move onto another chip's schema — all checked
    against the schema (an absent bus kind raises ``AttributeError``).

    Auto-allocated measurement names embed the bus (``q0/readout/m0``); ``rebind`` re-derives them for
    the rebound buses while leaving user-supplied names untouched (see
    [`MeasurementHandle`][qprogram.MeasurementHandle]). Fragment calls are expanded first.

    Args:
        schema (BusSchema | None): Target schema. Defaults to the program's current schema
            (re-index within one chip).
        elements (Mapping[tuple[str, int | tuple[int, ...]], tuple[str, int | tuple[int, ...]]] | None):
            Maps ``(element, idx)`` to ``(element, idx)`` — e.g. ``{("q", 0): ("q", 1)}`` to port
            qubit 0's operations onto qubit 1. Unlisted ``(element, idx)`` pairs pass through.
        naming (BusNaming | None): Re-resolve every ref under a new
            [`BusNaming`][qprogram.BusNaming] (cross-platform names).
        strings (Mapping[str, str] | None): Escape hatch for raw-string buses (which carry no
            schema metadata): an old→new map. Map a string to itself to mark it intentionally
            untouched.
        allow_unported_strings (bool, optional): When ``False`` (default), a raw-string bus not
            covered by ``strings`` raises — a partial port is a loud choice, not a silent
            accident. Set ``True`` to leave uncovered raw-string buses in place.

    Returns:
        A new [`QProgram`][qprogram.QProgram]; the original is untouched.

    Raises:
        ValidationError: If ``naming`` is given without a schema, or raw-string buses are left
            unported without ``allow_unported_strings``.
        AttributeError: If a rebound ``(element, idx, kind)`` does not resolve against the target
            schema (e.g. the target element lacks that bus kind).
        KeyError: If the target schema's naming pattern names a placeholder other than
            ``{element}``, ``{index}`` or ``{kind}``.
        ValueError: If the target schema's naming pattern is not a well-formed format string.
    """
    program = self.expand() if self.fragments else copy.deepcopy(self)
    target_schema = schema if schema is not None else program._schema
    if naming is not None:
        if target_schema is None:
            msg = "rebind(naming=...) requires the program to have a schema to re-resolve against"
            raise ValidationError(msg)
        target_schema = naming_substituted_schema(target_schema, naming)
    element_map = dict(elements or {})
    string_map = dict(strings or {})
    unported: set[str] = set()

    # swap in lockstep; None stays None for raw-string programs
    program._schema = target_schema

    _map_bus_attrs(
        program._body,
        lambda bus: _rebind_bus(bus, target_schema, element_map, string_map, unported),
    )

    if unported and not allow_unported_strings:
        names = ", ".join(repr(b) for b in sorted(unported))
        msg = (
            f"rebind left raw-string bus(es) unported: {names}. Raw strings carry no schema metadata "
            f"to re-resolve — map them via strings={{...}} (map a name to itself to keep it), or pass "
            f"allow_unported_strings=True to leave them in place."
        )
        raise ValidationError(msg)

    program._rederive_auto_measurement_names()

    for bus in _iter_bus_attrs(program._body):
        program._validate_bus(bus)

    return program

with_waveforms

with_waveforms(
    waveforms: WaveformLibrary
    | Mapping[str, Waveform | IQWaveform],
) -> QProgram

Return a copy with string waveform names resolved to concrete waveforms, scoped per bus.

For each operation whose waveform attribute is still a string, the name is looked up against waveforms for that operation's bus — so a shared name like "pi_pulse" can resolve to a different concrete pulse on q[0].drive than on q[1].drive. Concrete waveforms and names with no matching entry pass through unchanged. Each replacement re-runs the channel-type check, so an IQ pulse landing on a single-channel bus is caught here rather than at the hardware compiler.

Parameters:

Returns:

  • QProgram

    A new QProgram with matching names replaced; the original is untouched.

Raises:

  • ValidationError

    If a resolved waveform's channel count does not match its bus's, or if waveforms is a mapping keyed by anything other than non-empty strings.

Source code in src/qprogram/qprogram.py
def with_waveforms(
    self,
    waveforms: WaveformLibrary | Mapping[str, Waveform | IQWaveform],
) -> QProgram:
    """Return a copy with string waveform names resolved to concrete waveforms, scoped per bus.

    For each operation whose waveform attribute is still a string, the name is looked up against
    ``waveforms`` *for that operation's bus* — so a shared name like ``"pi_pulse"`` can resolve to a
    different concrete pulse on ``q[0].drive`` than on ``q[1].drive``. Concrete waveforms and names
    with no matching entry pass through unchanged. Each replacement re-runs the channel-type check,
    so an IQ pulse landing on a single-channel bus is caught here rather than at the hardware compiler.

    Args:
        waveforms (WaveformLibrary | Mapping[str, Waveform | IQWaveform]): A
            [`WaveformLibrary`][qprogram.WaveformLibrary] (resolved per bus), or a plain
            ``{name: waveform}`` mapping (one global tier, resolved on every bus).

    Returns:
        A new [`QProgram`][qprogram.QProgram] with matching names replaced; the original is untouched.

    Raises:
        ValidationError: If a resolved waveform's channel count does not match its bus's, or if
            ``waveforms`` is a mapping keyed by anything other than non-empty strings.
    """
    library = waveforms if isinstance(waveforms, WaveformLibrary) else WaveformLibrary.from_mapping(waveforms)
    new_program = copy.deepcopy(self)
    _resolve_waveforms(new_program._body, library)
    return new_program

Sweep builders

program.sweep(variable), with the source left out, returns a source builder; program.sweep(variable, source) returns the loop context straight away. Both are private classes that user code never constructs, but their methods are part of the public surface, so they are documented here. Entering a builder before a from_* call has picked any values raises ValidationError rather than sweeping nothing. The context managers behind average, block, if_, elif_, and else_ add nothing to the context-manager protocol, so they have no entries of their own.

_SweepBuilder

_SweepBuilder(program: QProgram, variable: Variable)

Source picker returned by program.sweep(variable) when no source is passed.

Each from_* builds one SweepSource and returns exactly what the two-argument sweep(variable, source) form returns — the same Sweep node, the same .qp line, the same | composition. The only thing it changes is the call site, which does not have to name a source class::

with program.sweep(freq).from_range(4e9, 6e9, 1e6):
    ...
with program.sweep(freq).from_range(4e9, 6e9, 1e6) | program.sweep(amp).from_values(table):
    ...

Both spellings are supported on purpose. Reach for from_* when writing a sweep by hand; pass the source object when computing one — holding it in a variable, building it in a comprehension, or composing combinators more deeply than _LoopContext.rotate and _LoopContext.repeat reach.

Every registered source is reachable here, not just the built-ins: an unknown from_<name> attribute is resolved against the live sweep-source registry (see __getattr__), so a vendor source gets its builder with no core change. The five built-ins are additionally written out as real methods, so editors complete and type-check them.

A builder is not a context manager — it has no values yet. with program.sweep(v): raises instead of quietly sweeping nothing.

Source code in src/qprogram/qprogram.py
def __init__(self, program: QProgram, variable: Variable) -> None:
    self._program = program
    self._variable = variable

from_range

from_range(
    start: float, stop: float, step: float = 1
) -> _LoopContext

Sweep a ramp from start to stop in increments of step, both ends inclusive.

Builds Range, which is where the validation rules live.

Parameters:

  • start (float) –

    First value (inclusive).

  • stop (float) –

    Final value (inclusive).

  • step (float, default: 1 ) –

    Increment between consecutive points. Defaults to 1.

Returns:

  • _LoopContext

    The context manager that opens the sweep block.

Source code in src/qprogram/qprogram.py
def from_range(self, start: float, stop: float, step: float = 1) -> _LoopContext:
    """Sweep a ramp from ``start`` to ``stop`` in increments of ``step``, both ends inclusive.

    Builds [`Range`][qprogram.Range], which is where the validation rules live.

    Args:
        start (float): First value (inclusive).
        stop (float): Final value (inclusive).
        step (float, optional): Increment between consecutive points. Defaults to ``1``.

    Returns:
        The context manager that opens the sweep block.
    """
    return self._bind(Range(start, stop, step))

from_linspace

from_linspace(
    start: float, stop: float, num: int
) -> _LoopContext

Sweep num evenly spaced points from start to stop, both ends inclusive.

Builds Linspace — the ramp to prefer when you know the point count rather than the spacing.

Parameters:

  • start (float) –

    First value (inclusive).

  • stop (float) –

    Final value (inclusive).

  • num (int) –

    Number of points. 1 yields [start].

Returns:

  • _LoopContext

    The context manager that opens the sweep block.

Source code in src/qprogram/qprogram.py
def from_linspace(self, start: float, stop: float, num: int) -> _LoopContext:
    """Sweep ``num`` evenly spaced points from ``start`` to ``stop``, both ends inclusive.

    Builds [`Linspace`][qprogram.Linspace] — the ramp to prefer when you know the point count
    rather than the spacing.

    Args:
        start (float): First value (inclusive).
        stop (float): Final value (inclusive).
        num (int): Number of points. ``1`` yields ``[start]``.

    Returns:
        The context manager that opens the sweep block.
    """
    return self._bind(Linspace(start, stop, num))

from_logspace

from_logspace(
    start: float, stop: float, num: int
) -> _LoopContext

Sweep num points spaced evenly on a log scale between start and stop.

Builds Logspace. Both bounds are actual values, not exponents.

Parameters:

  • start (float) –

    First value (inclusive). Must be strictly positive.

  • stop (float) –

    Final value (inclusive). Must be strictly positive.

  • num (int) –

    Number of points.

Returns:

  • _LoopContext

    The context manager that opens the sweep block.

Source code in src/qprogram/qprogram.py
def from_logspace(self, start: float, stop: float, num: int) -> _LoopContext:
    """Sweep ``num`` points spaced evenly on a log scale between ``start`` and ``stop``.

    Builds [`Logspace`][qprogram.Logspace]. Both bounds are actual values, not exponents.

    Args:
        start (float): First value (inclusive). Must be strictly positive.
        stop (float): Final value (inclusive). Must be strictly positive.
        num (int): Number of points.

    Returns:
        The context manager that opens the sweep block.
    """
    return self._bind(Logspace(start, stop, num))

from_values

from_values(points: ArrayLike) -> _LoopContext

Sweep an explicit list of points.

Builds Values, which is KIND = "arbitrary" even when the points happen to be evenly spaced — use from_range or from_linspace when the sweep really is a ramp and you want a platform to be able to compile it as one.

Parameters:

  • points (ArrayLike) –

    Sequence of values to iterate through. Anything numpy.asarray accepts.

Returns:

  • _LoopContext

    The context manager that opens the sweep block.

Source code in src/qprogram/qprogram.py
def from_values(self, points: npt.ArrayLike) -> _LoopContext:
    """Sweep an explicit list of points.

    Builds [`Values`][qprogram.Values], which is ``KIND = "arbitrary"`` even when the points
    happen to be evenly spaced — use `from_range` or `from_linspace` when the sweep
    really is a ramp and you want a platform to be able to compile it as one.

    Args:
        points (ArrayLike): Sequence of values to iterate through. Anything
            `numpy.asarray` accepts.

    Returns:
        The context manager that opens the sweep block.
    """
    return self._bind(Values(points))

from_file

from_file(path: str) -> _LoopContext

Sweep the points held in the .npy file at path.

Builds File, which stores the path rather than the values — the file must be readable wherever the program is validated or run.

Parameters:

  • path (str) –

    Path to a .npy file holding a 1-D array.

Returns:

  • _LoopContext

    The context manager that opens the sweep block.

Source code in src/qprogram/qprogram.py
def from_file(self, path: str) -> _LoopContext:
    """Sweep the points held in the ``.npy`` file at ``path``.

    Builds [`File`][qprogram.File], which stores the path rather than the values — the
    file must be readable wherever the program is validated or run.

    Args:
        path (str): Path to a ``.npy`` file holding a 1-D array.

    Returns:
        The context manager that opens the sweep block.
    """
    return self._bind(File(path))

__getattr__

__getattr__(attribute: str) -> Callable[..., _LoopContext]

Resolve an unknown from_<source> attribute against the live sweep-source registry.

This is what keeps the fluent form open: whatever register_sweep_source knows about is spellable here — vendor sources and the combinators included — matched on the class name with underscores and case ignored, so from_iq_table finds IQTable. The returned callable forwards its arguments to the source's constructor, so the source itself still does the validating.

Parameters:

  • attribute (str) –

    The attribute being looked up.

Returns:

  • Callable[..., _LoopContext]

    A callable that constructs the named source from the arguments it is given and returns

  • Callable[..., _LoopContext]

    the loop context for the resulting sweep.

Raises:

  • AttributeError

    For a from_* attribute no registered source answers (with a did-you-mean list), for a shaping method that belongs on the sweep rather than on the builder, and for any other attribute, as usual.

Source code in src/qprogram/qprogram.py
def __getattr__(self, attribute: str) -> Callable[..., _LoopContext]:
    """Resolve an unknown ``from_<source>`` attribute against the live sweep-source registry.

    This is what keeps the fluent form open: whatever
    `register_sweep_source` knows about is spellable here — vendor sources and
    the combinators included — matched on the class name with underscores and case ignored, so
    ``from_iq_table`` finds ``IQTable``. The returned callable forwards its arguments to the
    source's constructor, so the source itself still does the validating.

    Args:
        attribute (str): The attribute being looked up.

    Returns:
        A callable that constructs the named source from the arguments it is given and returns
        the loop context for the resulting sweep.

    Raises:
        AttributeError: For a ``from_*`` attribute no registered source answers (with a
            did-you-mean list), for a shaping method that belongs on the sweep rather than on
            the builder, and for any other attribute, as usual.
    """
    if attribute in _SHAPING_METHODS:
        msg = (
            f"{attribute}() shapes a sweep that already has values, so it lives on the sweep, "
            f"not on the builder: pick the values first — "
            f"sweep(variable).from_values([...]).{attribute}(...)."
        )
        raise AttributeError(msg)
    if attribute.startswith("__") or not attribute.startswith(_FROM_PREFIX):
        msg = f"{type(self).__name__!r} object has no attribute {attribute!r}"
        raise AttributeError(msg)
    source_cls = _sweep_source_for_builder(attribute[len(_FROM_PREFIX) :])
    if source_cls is None:
        raise AttributeError(_unknown_source_message(attribute))

    def build(*args: Any, **kwargs: Any) -> _LoopContext:
        return self._bind(source_cls(*args, **kwargs))

    build.__name__ = attribute
    build.__qualname__ = f"{type(self).__name__}.{attribute}"
    build.__doc__ = f"Sweep the values of {source_cls.__name__}(...), resolved from the sweep-source registry."
    return build

_LoopContext

_LoopContext(program: QProgram, block: Sweep)

Context manager returned by QProgram.sweep.

Supports | to compose multiple sweeps into a Parallel block. __or__ is pure — it returns a fresh context with the concatenated list and touches the program only on __enter__ — so a list of sweeps can be folded programmatically::

functools.reduce(operator.or_, [program.sweep(v, src) for v, src in specs])

repeat and rotate are pure in the same way. They wrap the bound source in the matching combinator and hand back a fresh context, which is what lets a sweep be shaped inline without naming Repeat / Rotate::

with program.sweep(phi).from_values(base).rotate(by=1).repeat(3):
    ...
Source code in src/qprogram/qprogram.py
def __init__(self, program: QProgram, block: Sweep) -> None:
    self._program = program
    self._block = block
    self._parallel_blocks: list[Sweep] = [block]

__or__

__or__(other: _LoopContext) -> _LoopContext

Compose this sweep with other into a Parallel block.

Parameters:

  • other (_LoopContext) –

    The sweep context to advance in lockstep with this one.

Returns:

  • _LoopContext

    A fresh context carrying both operands' sweeps; neither operand is modified.

Source code in src/qprogram/qprogram.py
def __or__(self, other: _LoopContext) -> _LoopContext:
    """Compose this sweep with ``other`` into a [`Parallel`][qprogram.blocks.Parallel] block.

    Args:
        other (_LoopContext): The sweep context to advance in lockstep with this one.

    Returns:
        A fresh context carrying both operands' sweeps; neither operand is modified.
    """
    ctx = _LoopContext(self._program, self._block)
    ctx._parallel_blocks = self._parallel_blocks + other._parallel_blocks
    return ctx

repeat

repeat(times: int) -> _LoopContext

Run the bound source's points times times back to back — Repeat.

Each repetition is a distinct sweep point with its own result entry; reach for QProgram.average when you want the repetitions collapsed instead.

Parameters:

  • times (int) –

    How many times to run the source. Must be at least 1.

Returns:

  • _LoopContext

    A fresh context bound to the wrapped source; this one is left untouched.

Raises:

  • ValidationError

    If this context is already a | composition, which gives the combinator no single source to wrap.

Source code in src/qprogram/qprogram.py
def repeat(self, times: int) -> _LoopContext:
    """Run the bound source's points ``times`` times back to back — [`Repeat`][qprogram.Repeat].

    Each repetition is a distinct sweep point with its own result entry; reach for
    [`QProgram.average`][qprogram.QProgram.average] when you want the repetitions collapsed instead.

    Args:
        times (int): How many times to run the source. Must be at least 1.

    Returns:
        A fresh context bound to the wrapped source; this one is left untouched.

    Raises:
        ValidationError: If this context is already a ``|`` composition, which gives the
            combinator no single source to wrap.
    """
    return self._wrapped(lambda source: Repeat(source, times), method="repeat")

rotate

rotate(by: int = 1) -> _LoopContext

Cyclically shift the bound source's points left by byRotate.

Parameters:

  • by (int, default: 1 ) –

    Positions to shift left. May be negative (shifts right) or exceed the point count (wraps, as numpy.roll does).

Returns:

  • _LoopContext

    A fresh context bound to the wrapped source; this one is left untouched.

Raises:

Source code in src/qprogram/qprogram.py
def rotate(self, by: int = 1) -> _LoopContext:
    """Cyclically shift the bound source's points left by ``by`` — [`Rotate`][qprogram.Rotate].

    Args:
        by (int, optional): Positions to shift left. May be negative (shifts right) or exceed
            the point count (wraps, as `numpy.roll` does).

    Returns:
        A fresh context bound to the wrapped source; this one is left untouched.

    Raises:
        ValidationError: If this context is already a ``|`` composition.
    """
    return self._wrapped(lambda source: Rotate(source, by), method="rotate")

Sweep sources

What a Sweep iterates over: a description of the values, never a producer of them. Every source answers length() and values() without the program running, declares a KIND of "linear" or "arbitrary" along with its own sweep.<name> capability token, and compares and hashes structurally over its public attributes, which are treated as immutable once the source is in a program. register_sweep_source puts a subclass in the registry under its own class name, which is what makes it parseable from a .qp file and spellable as sweep(variable).from_<name>(...); validate_source checks the length() and values() invariants for a new one.

SweepSource

Bases: ABC

How a Sweep generates the values it binds to its variable.

Subclasses declare KIND and TOKEN, implement length and values, and store their parameters as public instance attributes (which is what makes the .qp serialization signature-driven and free).

Equality and hashing are structural over those attributes, so two sources built the same way compare equal and a program survives deepcopy / loads(dumps(...)) comparison. Sources are conceptually immutable: once one has been used in a program, do not mutate its attributes.

KIND class-attribute

KIND: SweepKind

Whether the values form an exact start + step * i ramp ("linear") or not ("arbitrary").

This is a claim about compilability, not a description of the numbers: a platform may compile a linear sweep to a loop register with an increment, while an arbitrary one needs a value table or a host-side dispatch per point. Two sources can produce identical values and still differ here — Values listing an even ramp is still "arbitrary", because nothing about the source proves the regularity to a compiler.

TOKEN class-attribute

TOKEN: str

This source's own sweep.<name> capability token, registered with the global registry when the class is registered. A platform that can generate this source natively declares the token; one that cannot omits it and the validator reports it, rather than the platform silently materializing the points into a table.

length abstractmethod

length() -> int

Return the number of sweep points.

Must be answerable statically — without running the program, and without side effects beyond reading this source's own parameters. It must also be at least one: a sweep with no points never executes its body, so a built-in source rejects an empty parameterization as soon as it can see one, at construction for the sources that carry their values and on first read for File, which learns the length only when it loads the array. validate_source holds a subclass to the same rule.

Returns:

  • int

    The number of points the sweep iterates through.

Source code in src/qprogram/sweeps/source.py
@abstractmethod
def length(self) -> int:
    """Return the number of sweep points.

    Must be answerable statically — without running the program, and without side effects beyond
    reading this source's own parameters. It must also be at least one: a sweep with no points
    never executes its body, so a built-in source rejects an empty parameterization as soon as
    it can see one, at construction for the sources that carry their values and on first read
    for [`File`][qprogram.File], which learns the length only when it loads the array.
    `validate_source` holds a subclass to the same rule.

    Returns:
        The number of points the sweep iterates through.
    """

values abstractmethod

values() -> np.ndarray

Return the 1-D array of values this source sweeps, in iteration order.

len(values()) == length() is an invariant; validate_source checks it for the built-ins' tests and any subclass that wants the same guard.

Returns:

  • ndarray

    A 1-D array of length values, in the order the sweep binds them.

Source code in src/qprogram/sweeps/source.py
@abstractmethod
def values(self) -> np.ndarray:
    """Return the 1-D array of values this source sweeps, in iteration order.

    ``len(values()) == length()`` is an invariant; `validate_source` checks it for the
    built-ins' tests and any subclass that wants the same guard.

    Returns:
        A 1-D array of `length` values, in the order the sweep binds them.
    """

tokens

tokens() -> set[str]

Return every capability token this source requires.

The source's own TOKEN plus its sweep.<kind> token. Combinators override this to union their wrapped sources' tokens too — a platform that cannot generate Logspace also cannot generate a rotation of one.

Returns:

  • set[str]

    The capability tokens a platform must declare to generate this source.

Source code in src/qprogram/sweeps/source.py
def tokens(self) -> set[str]:
    """Return every capability token this source requires.

    The source's own `TOKEN` plus its ``sweep.<kind>`` token. Combinators override this to
    union their wrapped sources' tokens too — a platform that cannot generate
    [`Logspace`][qprogram.Logspace] also cannot generate a rotation of one.

    Returns:
        The capability tokens a platform must declare to generate this source.
    """
    return {self.TOKEN, f"sweep.{self.KIND}"}

Range

Range(start: float, stop: float, step: float = 1)

Bases: SweepSource

A linear ramp from start toward stop in increments of step.

The source a hardware sequencer can usually run without a value table: one loop register plus an increment. Prefer Linspace when you know the number of points rather than the spacing.

The ramp always begins at start and holds round((stop - start) / step) + 1 points, so it lands on stop only when step divides stop - start evenly. Otherwise the last point stops short of stopRange(0, 1, 0.3) ends at 0.9, Range(0, 0.4, 1) is the single point 0.0 — or steps past it, as Range(0, 1, 0.6) does by ending at 1.2. Reach for Linspace when the last point has to land exactly on stop.

Parameters:

  • start (float) –

    First value, always produced.

  • stop (float) –

    The value the ramp runs toward, produced only when step divides stop - start evenly.

  • step (float, default: 1 ) –

    Increment between consecutive points. Defaults to 1.

Raises:

  • ValidationError

    If any bound is non-numeric or non-finite, if step is zero (an infinite sweep), or if step points away from stop (an empty sweep).

Source code in src/qprogram/sweeps/builtin.py
def __init__(self, start: float, stop: float, step: float = 1) -> None:
    cls_name = type(self).__name__
    start = _require_finite_number(start, cls_name=cls_name, name="start")
    stop = _require_finite_number(stop, cls_name=cls_name, name="stop")
    step = _require_finite_number(step, cls_name=cls_name, name="step")
    if step == 0:
        msg = f"{cls_name} step must be non-zero (a zero step never reaches stop)"
        raise ValidationError(msg)
    if (stop - start) * step < 0:
        msg = (
            f"{cls_name} step {step!r} moves away from stop ({start!r} -> {stop!r}); "
            f"flip the step sign or swap the bounds"
        )
        raise ValidationError(msg)
    self.start = start
    self.stop = stop
    self.step = step

length

length() -> int

Return the number of points in the ramp.

The count is round((stop - start) / step) + 1 — the rounding absorbs floating-point division noise for ranges like (0.0, 1.0, 0.01), and it is also what lets the last point stop short of stop or step past it when step does not divide stop - start evenly.

Returns:

  • int

    The number of points in the ramp, counting the one at start.

Source code in src/qprogram/sweeps/builtin.py
def length(self) -> int:
    """Return the number of points in the ramp.

    The count is ``round((stop - start) / step) + 1`` — the rounding absorbs floating-point
    division noise for ranges like ``(0.0, 1.0, 0.01)``, and it is also what lets the last point
    stop short of ``stop`` or step past it when ``step`` does not divide ``stop - start`` evenly.

    Returns:
        The number of points in the ramp, counting the one at ``start``.
    """
    return round((self.stop - self.start) / self.step) + 1

values

values() -> np.ndarray

Return the ramp's points, in iteration order.

Returns:

  • ndarray

    start + step * arange(length()) — consistent with length by construction.

Source code in src/qprogram/sweeps/builtin.py
def values(self) -> np.ndarray:
    """Return the ramp's points, in iteration order.

    Returns:
        ``start + step * arange(length())`` — consistent with `length` by construction.
    """
    return self.start + self.step * np.arange(self.length())

Linspace

Linspace(start: float, stop: float, num: int)

Bases: SweepSource

num evenly spaced points from start to stop, both ends inclusive.

The shape most sweeps actually want — you usually know how many points you can afford, not the spacing that lands on them. Linear, so a platform can compile it as a ramp: the derived step is (stop - start) / (num - 1).

Parameters:

  • start (float) –

    First value (inclusive).

  • stop (float) –

    Final value (inclusive).

  • num (int) –

    Number of points. 1 yields [start].

Raises:

  • ValidationError

    If a bound is non-numeric or non-finite, or num is not an int >= 1.

Source code in src/qprogram/sweeps/builtin.py
def __init__(self, start: float, stop: float, num: int) -> None:
    cls_name = type(self).__name__
    self.start = _require_finite_number(start, cls_name=cls_name, name="start")
    self.stop = _require_finite_number(stop, cls_name=cls_name, name="stop")
    self.num = _require_positive_count(num, cls_name=cls_name)

length

length() -> int

Return the number of points.

Returns:

  • int

    num, as given.

Source code in src/qprogram/sweeps/builtin.py
def length(self) -> int:
    """Return the number of points.

    Returns:
        ``num``, as given.
    """
    return self.num

values

values() -> np.ndarray

Return the evenly spaced points.

Returns:

  • ndarray

    numpy.linspace over the closed interval [start, stop].

Source code in src/qprogram/sweeps/builtin.py
def values(self) -> np.ndarray:
    """Return the evenly spaced points.

    Returns:
        `numpy.linspace` over the closed interval ``[start, stop]``.
    """
    return np.linspace(self.start, self.stop, self.num)

step

step() -> float

Return the spacing this sweep resolves to, for a compiler that wants start/step form.

Returns:

  • float

    (stop - start) / (num - 1), or 0.0 for a single-point sweep, where the spacing is

  • float

    undefined.

Source code in src/qprogram/sweeps/builtin.py
def step(self) -> float:
    """Return the spacing this sweep resolves to, for a compiler that wants ``start``/``step`` form.

    Returns:
        ``(stop - start) / (num - 1)``, or ``0.0`` for a single-point sweep, where the spacing is
        undefined.
    """
    if self.num == 1:
        return 0.0
    return (self.stop - self.start) / (self.num - 1)

Logspace

Logspace(start: float, stop: float, num: int)

Bases: SweepSource

num points spaced evenly on a log scale between start and stop (both linear values).

Note the argument convention: start and stop are the actual first and last values, not the exponents numpy.logspace takes — a frequency sweep reads Logspace(1e6, 1e9, num=50) rather than Logspace(6, 9, num=50).

Parameters:

  • start (float) –

    First value (inclusive). Must be strictly positive.

  • stop (float) –

    Final value (inclusive). Must be strictly positive.

  • num (int) –

    Number of points.

Raises:

  • ValidationError

    If a bound is non-positive, non-numeric or non-finite, or num is not an int >= 1.

Source code in src/qprogram/sweeps/builtin.py
def __init__(self, start: float, stop: float, num: int) -> None:
    cls_name = type(self).__name__
    start = _require_finite_number(start, cls_name=cls_name, name="start")
    stop = _require_finite_number(stop, cls_name=cls_name, name="stop")
    if start <= 0 or stop <= 0:
        msg = (
            f"{cls_name} bounds must be strictly positive (log of a non-positive value), got {start!r} -> {stop!r}"
        )
        raise ValidationError(msg)
    self.start = start
    self.stop = stop
    self.num = _require_positive_count(num, cls_name=cls_name)

length

length() -> int

Return the number of points.

Returns:

  • int

    num, as given.

Source code in src/qprogram/sweeps/builtin.py
def length(self) -> int:
    """Return the number of points.

    Returns:
        ``num``, as given.
    """
    return self.num

values

values() -> np.ndarray

Return the log-spaced points.

Returns:

  • ndarray

    numpy.geomspace over [start, stop] — evenly spaced in log space, inclusive of

  • ndarray

    both bounds.

Source code in src/qprogram/sweeps/builtin.py
def values(self) -> np.ndarray:
    """Return the log-spaced points.

    Returns:
        `numpy.geomspace` over ``[start, stop]`` — evenly spaced in log space, inclusive of
        both bounds.
    """
    return np.geomspace(self.start, self.stop, self.num)

Values

Values(points: ArrayLike)

Bases: SweepSource

An explicit list of sweep points.

Use it when the points don't fit a regular pattern: calibrated values, a measured table, the output of a computation you ran yourself. Note that it is KIND "arbitrary" even when the values happen to be evenly spaced — the source proves nothing about their regularity to a compiler, so reach for Range or Linspace when the sweep really is a ramp and you want a platform to be able to compile it as one.

Parameters:

  • points (ArrayLike) –

    Sequence of values to iterate through. Anything numpy.asarray accepts. Named points rather than values so it doesn't collide with values; on the wire it is almost always written as the bracket literal [...] anyway.

Raises:

Source code in src/qprogram/sweeps/builtin.py
def __init__(self, points: npt.ArrayLike) -> None:
    array = np.asarray(points, dtype=float)
    if array.ndim != 1:
        msg = f"Values must be a 1-D sequence, got a {array.ndim}-D array"
        raise ValidationError(msg)
    if array.size == 0:
        msg = "Values must be non-empty (an empty sweep never executes its body)"
        raise ValidationError(msg)
    self.points = array

length

length() -> int

Return the number of points, one per element of the stored array.

Returns:

  • int

    The size of the stored array.

Source code in src/qprogram/sweeps/builtin.py
def length(self) -> int:
    """Return the number of points, one per element of the stored array.

    Returns:
        The size of the stored array.
    """
    return int(self.points.size)

values

values() -> np.ndarray

Return the points given at construction.

Returns:

  • ndarray

    The stored float array itself, not a copy — treat it as read-only, since the source

  • ndarray

    is a value object whose equality and hash are derived from it.

Source code in src/qprogram/sweeps/builtin.py
def values(self) -> np.ndarray:
    """Return the points given at construction.

    Returns:
        The stored ``float`` array itself, not a copy — treat it as read-only, since the source
        is a value object whose equality and hash are derived from it.
    """
    return self.points

File

File(path: str)

Bases: SweepSource

Sweep points loaded from a .npy file at the given path.

The path is what the AST stores, so a .qp file records where the points came from rather than inlining them — which keeps the file small and the intent legible. The cost is that the file must be readable wherever the program is validated or run: length and values both load it, and neither caches (caching would put the loaded array into the structural equality of the source, so an already-loaded instance would stop comparing equal to a fresh one).

Parameters:

  • path (str) –

    Path to a .npy file holding a 1-D array.

Raises:

Source code in src/qprogram/sweeps/builtin.py
def __init__(self, path: str) -> None:
    if not isinstance(path, str) or not path:
        msg = f"File path must be a non-empty string, got {path!r}"
        raise ValidationError(msg)
    self.path = path

length

length() -> int

Load the file and report its length.

Returns:

  • int

    The number of values the file holds.

Raises:

  • ValidationError

    If the file holds an array that is not 1-D, or holds no values.

  • OSError

    If the path does not exist or cannot be read.

Source code in src/qprogram/sweeps/builtin.py
def length(self) -> int:
    """Load the file and report its length.

    Returns:
        The number of values the file holds.

    Raises:
        ValidationError: If the file holds an array that is not 1-D, or holds no values.
        OSError: If the path does not exist or cannot be read.
    """
    return int(self._load().size)

values

values() -> np.ndarray

Load the file and return its contents.

Returns:

  • ndarray

    The file's contents as a 1-D float array.

Raises:

  • ValidationError

    If the file holds an array that is not 1-D, or holds no values.

  • OSError

    If the path does not exist or cannot be read.

Source code in src/qprogram/sweeps/builtin.py
def values(self) -> np.ndarray:
    """Load the file and return its contents.

    Returns:
        The file's contents as a 1-D ``float`` array.

    Raises:
        ValidationError: If the file holds an array that is not 1-D, or holds no values.
        OSError: If the path does not exist or cannot be read.
    """
    return self._load()

Repeat

Repeat(source: SweepSource, times: int)

Bases: SweepSource

An inner source's points repeated times times, back to back.

Repeat(Values([0, 1]), times=3) sweeps 0, 1, 0, 1, 0, 1. Note this is not averaging: each repetition is a distinct sweep point with its own result entry. Use average when you want the repetitions collapsed.

Reports KIND = "arbitrary" even around a linear source — see the module docstring for why the conservative direction is the correct one.

Parameters:

  • source (SweepSource) –

    The source to repeat. A bare sequence is accepted and wrapped in Values.

  • times (int) –

    How many times to run it. Must be an int >= 1.

Raises:

  • ValidationError

    If source isn't a source or sequence, or if times is not an int, is a bool, or is less than 1.

Source code in src/qprogram/sweeps/combinators.py
def __init__(self, source: SweepSource, times: int) -> None:
    cls_name = type(self).__name__
    self.source = _require_source(source, cls_name=cls_name)
    if not isinstance(times, int) or isinstance(times, bool) or times < 1:
        msg = f"{cls_name} times must be an int >= 1, got {times!r}"
        raise ValidationError(msg)
    self.times = times

length

length() -> int

Return the number of points across every repetition.

Returns:

  • int

    source.length() * times.

Source code in src/qprogram/sweeps/combinators.py
def length(self) -> int:
    """Return the number of points across every repetition.

    Returns:
        ``source.length() * times``.
    """
    return self.source.length() * self.times

values

values() -> np.ndarray

Return the repeated points.

Returns:

  • ndarray

    The wrapped source's values tiled times times.

Source code in src/qprogram/sweeps/combinators.py
def values(self) -> np.ndarray:
    """Return the repeated points.

    Returns:
        The wrapped source's values tiled ``times`` times.
    """
    return np.tile(self.source.values(), self.times)

tokens

tokens() -> set[str]

Return every capability token this source requires.

Returns:

  • set[str]

    sweep.repeat and sweep.arbitrary, plus everything the wrapped source needs.

Source code in src/qprogram/sweeps/combinators.py
def tokens(self) -> set[str]:
    """Return every capability token this source requires.

    Returns:
        ``sweep.repeat`` and ``sweep.arbitrary``, plus everything the wrapped source needs.
    """
    return {self.TOKEN, f"sweep.{self.KIND}"} | self.source.tokens()

Rotate

Rotate(source: SweepSource, by: int = 1)

Bases: SweepSource

An inner source's points cyclically shifted left by by positions.

Rotate(Values([0, 1, 2, 3]), by=1) sweeps 1, 2, 3, 0. The point count is unchanged. Use it with Concat to build the phase-cycling pattern where a sequence is swept once per starting offset::

Concat(Rotate(base, by=i) for i in range(base.length()))

Parameters:

  • source (SweepSource) –

    The source to rotate. A bare sequence is accepted and wrapped in Values.

  • by (int, default: 1 ) –

    Number of positions to shift left. May be negative (shifts right) or exceed the length (wraps, as numpy.roll does). Defaults to 1.

Raises:

  • ValidationError

    If source isn't a source or sequence, or if by is not an int or is a bool.

Source code in src/qprogram/sweeps/combinators.py
def __init__(self, source: SweepSource, by: int = 1) -> None:
    cls_name = type(self).__name__
    self.source = _require_source(source, cls_name=cls_name)
    if not isinstance(by, int) or isinstance(by, bool):
        msg = f"{cls_name} by must be an int, got {type(by).__name__}"
        raise ValidationError(msg)
    self.by = by

length

length() -> int

Return the number of points, which rotation leaves unchanged.

Returns:

  • int

    The wrapped source's length.

Source code in src/qprogram/sweeps/combinators.py
def length(self) -> int:
    """Return the number of points, which rotation leaves unchanged.

    Returns:
        The wrapped source's length.
    """
    return self.source.length()

values

values() -> np.ndarray

Return the rotated points.

Returns:

  • ndarray

    The wrapped source's values under numpy.roll by -by — a left shift, so

  • ndarray

    by=1 starts at the second point.

Source code in src/qprogram/sweeps/combinators.py
def values(self) -> np.ndarray:
    """Return the rotated points.

    Returns:
        The wrapped source's values under `numpy.roll` by ``-by`` — a *left* shift, so
        ``by=1`` starts at the second point.
    """
    return np.roll(self.source.values(), -self.by)

tokens

tokens() -> set[str]

Return every capability token this source requires.

Returns:

  • set[str]

    sweep.rotate and sweep.arbitrary, plus everything the wrapped source needs.

Source code in src/qprogram/sweeps/combinators.py
def tokens(self) -> set[str]:
    """Return every capability token this source requires.

    Returns:
        ``sweep.rotate`` and ``sweep.arbitrary``, plus everything the wrapped source needs.
    """
    return {self.TOKEN, f"sweep.{self.KIND}"} | self.source.tokens()

Concat

Concat(sources: Iterable[SweepSource])

Bases: SweepSource

Several sources joined end to end into a single sweep.

Concat([Range(0, 1, 0.5), Values([10, 20])]) sweeps 0, 0.5, 1, 10, 20. Accepts any iterable, so a comprehension or generator expression works directly.

Parameters:

  • sources (Iterable[SweepSource]) –

    The sources to concatenate, in order. At least one. Bare sequences among them are wrapped in Values.

Raises:

  • ValidationError

    If sources is a single source rather than an iterable of them, is empty, or holds something that is neither a source nor a sequence of values.

Source code in src/qprogram/sweeps/combinators.py
def __init__(self, sources: Iterable[SweepSource]) -> None:
    cls_name = type(self).__name__
    if isinstance(sources, SweepSource):
        msg = (
            f"{cls_name} takes an iterable of sources, not a single source. "
            f"Write Concat([a, b]) or Concat(gen_expr)."
        )
        raise ValidationError(msg)
    resolved = [_require_source(s, cls_name=cls_name, name=f"sources[{i}]") for i, s in enumerate(sources)]
    if not resolved:
        msg = f"{cls_name} needs at least one source (an empty sweep never executes its body)"
        raise ValidationError(msg)
    self.sources = resolved

length

length() -> int

Return the number of points across every part.

Returns:

  • int

    The sum of the wrapped sources' lengths.

Source code in src/qprogram/sweeps/combinators.py
def length(self) -> int:
    """Return the number of points across every part.

    Returns:
        The sum of the wrapped sources' lengths.
    """
    return sum(source.length() for source in self.sources)

values

values() -> np.ndarray

Return the concatenated points.

Returns:

  • ndarray

    The parts' values joined in order.

Source code in src/qprogram/sweeps/combinators.py
def values(self) -> np.ndarray:
    """Return the concatenated points.

    Returns:
        The parts' values joined in order.
    """
    return np.concatenate([source.values() for source in self.sources])

tokens

tokens() -> set[str]

Return every capability token this source requires.

Returns:

  • set[str]

    sweep.concat and sweep.arbitrary, plus everything every wrapped source needs.

Source code in src/qprogram/sweeps/combinators.py
def tokens(self) -> set[str]:
    """Return every capability token this source requires.

    Returns:
        ``sweep.concat`` and ``sweep.arbitrary``, plus everything every wrapped source needs.
    """
    out = {self.TOKEN, f"sweep.{self.KIND}"}
    for source in self.sources:
        out |= source.tokens()
    return out

register_sweep_source

register_sweep_source(
    cls: type[SweepSource],
) -> type[SweepSource]

Register a sweep-source class for .qp serialization, keyed by its class name.

The sweep analogue of register_waveform, and the whole extension step for a new source: once registered, for <var> in <ClassName>(...) parses and writes itself from the constructor signature, exactly as a waveform constructor does.

Also registers the class's TOKEN in the capability registry, so a profile may list it without a separate register_capability_tokens call — mirroring register_waveform_token.

Same-class re-registration is a no-op; registering a different class under an already-taken name raises — it would silently change how every existing file parses that constructor.

Parameters:

  • cls (type[SweepSource]) –

    Sweep-source class to register. Its __name__ is the constructor name on the wire, and its TOKEN is added to the capability registry.

Returns:

  • type[SweepSource]

    cls, so the function can be used as a decorator.

Raises:

  • ValueError

    If cls.__name__ is already registered to a different class.

Source code in src/qprogram/serialization/registry.py
def register_sweep_source(cls: type[SweepSource]) -> type[SweepSource]:
    """Register a sweep-source class for ``.qp`` serialization, keyed by its class name.

    The sweep analogue of `register_waveform`, and the whole extension step for a new source:
    once registered, ``for <var> in <ClassName>(...)`` parses and writes itself from the constructor
    signature, exactly as a waveform constructor does.

    Also registers the class's `TOKEN` in the capability registry,
    so a profile may list it without a separate ``register_capability_tokens`` call — mirroring
    [`register_waveform_token`][qprogram.register_waveform_token].

    Same-class re-registration is a no-op; registering a different class under an already-taken name
    raises — it would silently change how every existing file parses that constructor.

    Args:
        cls (type[SweepSource]): Sweep-source class to register. Its ``__name__`` is the constructor
            name on the wire, and its ``TOKEN`` is added to the capability registry.

    Returns:
        ``cls``, so the function can be used as a decorator.

    Raises:
        ValueError: If ``cls.__name__`` is already registered to a different class.
    """
    # Imported here rather than at module load, which would be a cycle.
    from qprogram.protocol import register_capability_tokens  # ruff: ignore[import-outside-top-level]

    existing = _sweep_source_registry.get(cls.__name__)
    if existing is not None and existing is not cls:
        msg = (
            f"sweep source name {cls.__name__!r} is already registered to "
            f"{existing.__module__}.{existing.__qualname__}; rename the class or unregister first"
        )
        raise ValueError(msg)
    _sweep_source_registry[cls.__name__] = cls
    register_capability_tokens(cls.TOKEN)
    return cls

known_sweep_sources

known_sweep_sources() -> set[str]

Return the class name of every registered sweep source.

The live registry, so a vendor source registered at import time is included immediately. Both did-you-mean lists read it: the parser's, for an unknown for x in Name(...) constructor, and the fluent builder's, for an unknown sweep(var).from_* attribute.

Returns:

  • set[str]

    The class name of every sweep source currently registered.

Source code in src/qprogram/serialization/registry.py
def known_sweep_sources() -> set[str]:
    """Return the class name of every registered sweep source.

    The live registry, so a vendor source registered at import time is included immediately. Both
    did-you-mean lists read it: the parser's, for an unknown ``for x in Name(...)`` constructor, and
    the fluent builder's, for an unknown ``sweep(var).from_*`` attribute.

    Returns:
        The class name of every sweep source currently registered.
    """
    return set(_sweep_source_registry)

validate_source

validate_source(source: SweepSource) -> None

Assert the SweepSource.length / SweepSource.values invariants.

Not called on the hot path — it materializes the values. Tests and source authors use it to check a new subclass honors the contract.

Parameters:

Raises:

  • AssertionError

    If the values are not a non-empty 1-D array whose length matches SweepSource.length.

Source code in src/qprogram/sweeps/source.py
def validate_source(source: SweepSource) -> None:
    """Assert the [`SweepSource.length`][qprogram.SweepSource.length] / [`SweepSource.values`][qprogram.SweepSource.values] invariants.

    Not called on the hot path — it materializes the values. Tests and source authors use it to check
    a new subclass honors the contract.

    Args:
        source (SweepSource): The source to check.

    Raises:
        AssertionError: If the values are not a non-empty 1-D array whose length matches
            [`SweepSource.length`][qprogram.SweepSource.length].
    """
    array = np.asarray(source.values())
    assert array.ndim == 1, f"{type(source).__name__}.values() must be 1-D, got {array.ndim}-D"  # ruff: ignore[assert]
    assert array.size > 0, f"{type(source).__name__}.values() must be non-empty"  # ruff: ignore[assert]
    assert array.size == source.length(), (  # ruff: ignore[assert]
        f"{type(source).__name__}.length() is {source.length()} but values() has {array.size} points"
    )

Bus schemas

BusSchema

BusSchema(naming: BusNaming | None = None)

The bus kinds each element kind on a chip exposes.

Three construction modes:

  1. Presetstransmon, fluxonium, etc. return fully-typed subclasses with IDE autocomplete.
  2. Dynamic — instantiate BusSchema directly and call add_element for custom topologies. Bus access via schema.q[0].drive works at runtime but has no static type.
  3. Custom typed — subclass BusSchema to expose your own typed accessors; see the user guide for the template.

Schemas compose: schema_a + schema_b (or combine for three or more, or for naming control) returns a new schema with the union of both element families. Either operand may be a schema instance or a schema class, e.g. FluxTunableTransmonSchema + RFSwitchSchema. The result is a plain (dynamic) BusSchema — runtime access like combined.q[0].drive works, but it carries no static typing (the same trade-off as add_element). Build refs from the combined schema, not the originals, so their BusRef.schema back-pointer matches the schema you attach to a program.

Each QProgram holds at most one schema — passed at construction, or adopted from the first schema-backed ref the program sees. The .qp writer reads program.schema — not the individual refs' BusRef.schema back-pointers — both to emit the schema: block and to decide that a bus renders as an element[idx].kind path. A ref's own back-pointer records which schema produced it, which is what lets a program refuse a ref from a foreign schema and what routes the bus to its per-bus capability profile.

Parameters:

  • naming (BusNaming | None, default: None ) –

    Naming convention for the bus strings this schema resolves. When omitted, a default BusNaming is used.

Attributes:

  • KIND (str) –

    Class-level identifier set by built-in presets ("transmon", "fluxonium", ...). Informational; user subclasses may set their own. A combined schema keeps the base "".

Source code in src/qprogram/buses.py
def __init__(self, naming: BusNaming | None = None) -> None:
    self._naming = naming or BusNaming()
    self._elements: dict[str, ElementSchema] = {}

naming property

naming: BusNaming

The BusNaming this schema resolves bus strings through.

elements property

elements: dict[str, ElementSchema]

The registered element schemas, keyed by element name.

add_element

add_element(
    name: str, buses: dict[str, tuple[ChannelType, bool]]
) -> None

Register an element type and its available bus kinds.

For statically-typed schemas, subclass BusSchema and expose @property accessors rather than using this method.

Parameters:

  • name (str) –

    Element name (e.g. "q", "resonator"). Registering a name twice replaces the earlier declaration.

  • buses (dict[str, tuple[ChannelType, bool]]) –

    Mapping of bus kind to (channel, acquires). For example::

    {"drive": ("IQ", False), "readout": ("IQ", True), "flux": ("single", False)}

Source code in src/qprogram/buses.py
def add_element(self, name: str, buses: dict[str, tuple[ChannelType, bool]]) -> None:
    """Register an element type and its available bus kinds.

    For statically-typed schemas, subclass [`BusSchema`][qprogram.BusSchema] and expose ``@property`` accessors
    rather than using this method.

    Args:
        name (str): Element name (e.g. ``"q"``, ``"resonator"``). Registering a name twice
            replaces the earlier declaration.
        buses (dict[str, tuple[ChannelType, bool]]): Mapping of bus kind to
            ``(channel, acquires)``. For example::

            {"drive": ("IQ", False), "readout": ("IQ", True), "flux": ("single", False)}
    """
    self._elements[name] = ElementSchema(name=name, buses=buses, naming=self._naming)

combine staticmethod

combine(
    *schemas: BusSchema, naming: BusNaming | None = None
) -> BusSchema

Merge schemas into a new dynamic BusSchema.

The result holds the union of every input schema's elements, so passing a single schema yields a dynamic copy of it. It is a plain BusSchema (not a typed subclass), so combined.q[0].drive resolves at runtime but without static typing — the same trade-off as building a schema with add_element. The + operator (__add__, and the class-level form via the metaclass) delegates here; use combine directly when joining three or more schemas in one call or when you need to pick the naming convention explicitly.

Parameters:

  • *schemas (BusSchema, default: () ) –

    The schemas to merge (at least one). Each must be a BusSchema instance.

  • naming (BusNaming | None, default: None ) –

    Naming convention for the combined schema. When omitted, every input schema must share the same naming pattern (a combined schema can carry only one) — they usually do, since the default is universal. Pass an explicit naming to resolve a clash.

Returns:

Raises:

  • ValueError

    If no schemas are given, if the inputs disagree on naming and none is given, or if two inputs define the same element name with different buses (an ambiguous merge — rename one element). Re-declaring an identical element is allowed (idempotent).

Source code in src/qprogram/buses.py
@staticmethod
def combine(*schemas: BusSchema, naming: BusNaming | None = None) -> BusSchema:
    """Merge schemas into a new dynamic [`BusSchema`][qprogram.BusSchema].

    The result holds the **union** of every input schema's elements, so passing a single schema
    yields a dynamic copy of it. It is a plain
    [`BusSchema`][qprogram.BusSchema] (not a typed subclass), so ``combined.q[0].drive`` resolves at runtime but
    without static typing — the same trade-off as building a schema with `add_element`.
    The ``+`` operator (`__add__`, and the class-level form via the metaclass) delegates
    here; use ``combine`` directly when joining three or more schemas in one call or when you need
    to pick the naming convention explicitly.

    Args:
        *schemas (BusSchema): The schemas to merge (at least one). Each must be a
            [`BusSchema`][qprogram.BusSchema] instance.
        naming (BusNaming | None): Naming convention for the combined schema. When omitted,
            every input schema must share the same naming pattern (a combined schema can carry
            only one) — they usually do, since the default is universal. Pass an explicit
            ``naming`` to resolve a clash.

    Returns:
        A new [`BusSchema`][qprogram.BusSchema] whose ``elements`` are the union of the inputs'.

    Raises:
        ValueError: If no schemas are given, if the inputs disagree on naming and none is given,
            or if two inputs define the *same* element name with *different* buses (an ambiguous
            merge — rename one element). Re-declaring an identical element is allowed (idempotent).
    """
    if not schemas:
        msg = "BusSchema.combine() requires at least one schema"
        raise ValueError(msg)
    if naming is None:
        patterns = {s.naming.pattern for s in schemas}
        if len(patterns) > 1:
            msg = (
                f"cannot combine schemas with different naming patterns {sorted(patterns)}; "
                f"pass naming=BusNaming(...) to BusSchema.combine() to choose one explicitly"
            )
            raise ValueError(msg)
        naming = BusNaming(next(iter(patterns)))
    combined = BusSchema(naming=naming)
    for schema in schemas:
        for name, element in schema.elements.items():
            existing = combined.elements.get(name)
            if existing is not None:
                if existing.buses != element.buses:
                    msg = (
                        f"cannot combine schemas: element {name!r} is defined differently "
                        f"({existing.buses} vs {element.buses}); rename one element before combining"
                    )
                    raise ValueError(msg)
                # identical element already merged — idempotent
                continue
            combined.add_element(name, dict(element.buses))
    return combined

transmon classmethod

transmon(naming: BusNaming | None = None) -> TransmonSchema

Return a schema for fixed-frequency transmon qubits (no couplers).

Qubit buses: drive (IQ), readout (IQ, acquires).

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Returns:

Source code in src/qprogram/buses.py
@classmethod
def transmon(cls, naming: BusNaming | None = None) -> TransmonSchema:
    """Return a schema for fixed-frequency transmon qubits (no couplers).

    Qubit buses: ``drive`` (IQ), ``readout`` (IQ, acquires).

    Args:
        naming (BusNaming | None): Custom naming convention for the resolved bus strings.

    Returns:
        A typed schema exposing a ``q`` qubit accessor.
    """
    return TransmonSchema(naming=naming)

transmon_coupled classmethod

transmon_coupled(
    naming: BusNaming | None = None,
) -> TransmonCoupledSchema

Return a schema for fixed-frequency transmon qubits with couplers.

Qubit buses: drive (IQ), readout (IQ, acquires). Coupler buses: flux (single).

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Returns:

Source code in src/qprogram/buses.py
@classmethod
def transmon_coupled(cls, naming: BusNaming | None = None) -> TransmonCoupledSchema:
    """Return a schema for fixed-frequency transmon qubits with couplers.

    Qubit buses: ``drive`` (IQ), ``readout`` (IQ, acquires). Coupler buses: ``flux`` (single).

    Args:
        naming (BusNaming | None): Custom naming convention for the resolved bus strings.

    Returns:
        A typed schema exposing ``q`` qubit and ``c`` coupler accessors.
    """
    return TransmonCoupledSchema(naming=naming)

flux_tunable_transmon classmethod

flux_tunable_transmon(
    naming: BusNaming | None = None,
) -> FluxTunableTransmonSchema

Return a schema for flux-tunable transmon qubits (no couplers).

Qubit buses: drive (IQ), readout (IQ, acquires), flux (single).

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Returns:

Source code in src/qprogram/buses.py
@classmethod
def flux_tunable_transmon(cls, naming: BusNaming | None = None) -> FluxTunableTransmonSchema:
    """Return a schema for flux-tunable transmon qubits (no couplers).

    Qubit buses: ``drive`` (IQ), ``readout`` (IQ, acquires), ``flux`` (single).

    Args:
        naming (BusNaming | None): Custom naming convention for the resolved bus strings.

    Returns:
        A typed schema exposing a ``q`` qubit accessor.
    """
    return FluxTunableTransmonSchema(naming=naming)

flux_tunable_transmon_coupled classmethod

flux_tunable_transmon_coupled(
    naming: BusNaming | None = None,
) -> FluxTunableTransmonCoupledSchema

Return a schema for flux-tunable transmon qubits with couplers.

Qubit buses: drive (IQ), readout (IQ, acquires), flux (single). Coupler buses: flux (single).

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Returns:

Source code in src/qprogram/buses.py
@classmethod
def flux_tunable_transmon_coupled(cls, naming: BusNaming | None = None) -> FluxTunableTransmonCoupledSchema:
    """Return a schema for flux-tunable transmon qubits with couplers.

    Qubit buses: ``drive`` (IQ), ``readout`` (IQ, acquires), ``flux`` (single).
    Coupler buses: ``flux`` (single).

    Args:
        naming (BusNaming | None): Custom naming convention for the resolved bus strings.

    Returns:
        A typed schema exposing ``q`` qubit and ``c`` coupler accessors.
    """
    return FluxTunableTransmonCoupledSchema(naming=naming)

fluxonium classmethod

fluxonium(
    naming: BusNaming | None = None,
) -> FluxoniumSchema

Return a schema for fluxonium qubits (no couplers).

Qubit buses: drive (IQ), readout (IQ, acquires), flux_x (single), flux_z (single).

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Returns:

Source code in src/qprogram/buses.py
@classmethod
def fluxonium(cls, naming: BusNaming | None = None) -> FluxoniumSchema:
    """Return a schema for fluxonium qubits (no couplers).

    Qubit buses: ``drive`` (IQ), ``readout`` (IQ, acquires), ``flux_x`` (single), ``flux_z`` (single).

    Args:
        naming (BusNaming | None): Custom naming convention for the resolved bus strings.

    Returns:
        A typed schema exposing a ``q`` qubit accessor.
    """
    return FluxoniumSchema(naming=naming)

fluxonium_coupled classmethod

fluxonium_coupled(
    naming: BusNaming | None = None,
) -> FluxoniumCoupledSchema

Return a schema for fluxonium qubits with couplers.

Qubit buses: drive (IQ), readout (IQ, acquires), flux_x (single), flux_z (single). Coupler buses: flux (single).

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Returns:

Source code in src/qprogram/buses.py
@classmethod
def fluxonium_coupled(cls, naming: BusNaming | None = None) -> FluxoniumCoupledSchema:
    """Return a schema for fluxonium qubits with couplers.

    Qubit buses: ``drive`` (IQ), ``readout`` (IQ, acquires), ``flux_x`` (single), ``flux_z`` (single).
    Coupler buses: ``flux`` (single).

    Args:
        naming (BusNaming | None): Custom naming convention for the resolved bus strings.

    Returns:
        A typed schema exposing ``q`` qubit and ``c`` coupler accessors.
    """
    return FluxoniumCoupledSchema(naming=naming)

BusNaming

BusNaming(pattern: str = DEFAULT_PATTERN)

Configurable bus-name format string.

The default "{element}{index}/{kind}" produces "q0/drive", "coupler0_1/flux", etc. Platforms with entrenched naming conventions can supply their own, e.g. "{kind}_{element}{index}_bus" for "drive_q0_bus".

Parameters:

  • pattern (str, default: DEFAULT_PATTERN ) –

    Format string. Supported placeholders: {element}, {index}, {kind}.

Source code in src/qprogram/buses.py
def __init__(self, pattern: str = DEFAULT_PATTERN) -> None:
    self.pattern = pattern

resolve

resolve(element: str, index: int | tuple, kind: str) -> str

Format a bus name from its component pieces.

Parameters:

  • element (str) –

    Element name, substituted for {element}.

  • index (int | tuple) –

    Element index, substituted for {index}. A tuple is joined with underscores, so (0, 1) becomes 0_1.

  • kind (str) –

    Bus kind name, substituted for {kind}.

Returns:

  • str

    The bus name produced by this naming's pattern.

Raises:

  • KeyError

    If the pattern names a placeholder other than {element}, {index} or {kind}.

  • ValueError

    If the pattern is not a well-formed format string, or applies a format specification the substituted text cannot satisfy — every piece is substituted as text, so {index:d} fails.

  • IndexError

    If the pattern uses a positional placeholder such as {0}; the three pieces are supplied by keyword only.

Source code in src/qprogram/buses.py
def resolve(self, element: str, index: int | tuple, kind: str) -> str:
    """Format a bus name from its component pieces.

    Args:
        element (str): Element name, substituted for ``{element}``.
        index (int | tuple): Element index, substituted for ``{index}``. A tuple is joined with
            underscores, so ``(0, 1)`` becomes ``0_1``.
        kind (str): Bus kind name, substituted for ``{kind}``.

    Returns:
        The bus name produced by this naming's pattern.

    Raises:
        KeyError: If the pattern names a placeholder other than ``{element}``, ``{index}`` or
            ``{kind}``.
        ValueError: If the pattern is not a well-formed format string, or applies a format
            specification the substituted text cannot satisfy — every piece is substituted as
            text, so ``{index:d}`` fails.
        IndexError: If the pattern uses a positional placeholder such as ``{0}``; the three
            pieces are supplied by keyword only.
    """
    idx_str = "_".join(str(i) for i in index) if isinstance(index, tuple) else str(index)
    return self.pattern.format(element=element, index=idx_str, kind=kind)

BusRef

Bases: str

A string that also carries structured bus metadata.

A BusRef is a real str everywhere downstream — operations, serialization, compiler — but exposes metadata attributes for tooling and validation. The idx attribute is named that way rather than index to avoid shadowing the inherited str.index method.

The constructor takes the resolved bus name as the string value, followed by the metadata fields below. Refs normally come from a schema accessor (schema.q[0].drive) or from resolve_ref; constructing one directly is for buses that live outside any schema.

Attributes:

  • element (str) –

    Element name (e.g. "q", "coupler").

  • idx (int | tuple[int, ...]) –

    Element index (e.g. 0 or (0, 1)).

  • kind (str) –

    Bus kind name (e.g. "drive", "flux", "readout").

  • channel (ChannelType) –

    "single" for real-valued waveforms, "IQ" for complex I/Q.

  • acquires (bool) –

    True if the bus has an ADC and supports QProgram.measure.

  • schema (BusSchema | None) –

    The BusSchema that produced this ref, or None for manually-built refs. Used by QProgram._validate_bus to reject buses from a different schema than the one attached to the program.

Typed schemas

Each preset factory on BusSchema returns one of these subclasses, whose element properties are declared rather than resolved through __getattr__, so an editor can complete the bus kinds. The classes are reachable under their own names for a type annotation or for combine, which takes a class as readily as an instance.

TransmonSchema

TransmonSchema(naming: BusNaming | None = None)

Bases: BusSchema

Typed schema for fixed-frequency transmon qubits — exposes a typed q accessor.

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Source code in src/qprogram/buses.py
def __init__(self, naming: BusNaming | None = None) -> None:
    super().__init__(naming=naming)
    self.add_element("q", {"drive": ("IQ", False), "readout": ("IQ", True)})

q property

q: TransmonQubitFactory

The qubit factory: schema.q[0] exposes that qubit's drive and readout buses.

TransmonCoupledSchema

TransmonCoupledSchema(naming: BusNaming | None = None)

Bases: TransmonSchema

Typed schema for transmon qubits plus couplers — adds a typed c accessor.

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Source code in src/qprogram/buses.py
def __init__(self, naming: BusNaming | None = None) -> None:
    super().__init__(naming=naming)
    self.add_element("c", {"flux": ("single", False)})

c property

c: CouplerFactory

The coupler factory: schema.c[0, 1] exposes that coupler's flux bus.

FluxTunableTransmonSchema

FluxTunableTransmonSchema(naming: BusNaming | None = None)

Bases: BusSchema

Typed schema for flux-tunable transmon qubits — exposes a typed q accessor.

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Source code in src/qprogram/buses.py
def __init__(self, naming: BusNaming | None = None) -> None:
    super().__init__(naming=naming)
    self.add_element("q", {"drive": ("IQ", False), "readout": ("IQ", True), "flux": ("single", False)})

q property

q: FluxTunableTransmonQubitFactory

The qubit factory: schema.q[0] exposes that qubit's drive, readout and flux buses.

FluxTunableTransmonCoupledSchema

FluxTunableTransmonCoupledSchema(
    naming: BusNaming | None = None,
)

Bases: FluxTunableTransmonSchema

Typed schema for flux-tunable transmon qubits plus couplers — adds a typed c accessor.

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Source code in src/qprogram/buses.py
def __init__(self, naming: BusNaming | None = None) -> None:
    super().__init__(naming=naming)
    self.add_element("c", {"flux": ("single", False)})

c property

c: CouplerFactory

The coupler factory: schema.c[0, 1] exposes that coupler's flux bus.

FluxoniumSchema

FluxoniumSchema(naming: BusNaming | None = None)

Bases: BusSchema

Typed schema for fluxonium qubits — exposes a typed q accessor.

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Source code in src/qprogram/buses.py
def __init__(self, naming: BusNaming | None = None) -> None:
    super().__init__(naming=naming)
    self.add_element(
        "q",
        {
            "drive": ("IQ", False),
            "readout": ("IQ", True),
            "flux_x": ("single", False),
            "flux_z": ("single", False),
        },
    )

q property

q: FluxoniumQubitFactory

The qubit factory: schema.q[0] exposes that qubit's drive, readout and two flux buses.

FluxoniumCoupledSchema

FluxoniumCoupledSchema(naming: BusNaming | None = None)

Bases: FluxoniumSchema

Typed schema for fluxonium qubits plus couplers — adds a typed c accessor.

Parameters:

  • naming (BusNaming | None, default: None ) –

    Custom naming convention for the resolved bus strings.

Source code in src/qprogram/buses.py
def __init__(self, naming: BusNaming | None = None) -> None:
    super().__init__(naming=naming)
    self.add_element("c", {"flux": ("single", False)})

c property

c: CouplerFactory

The coupler factory: schema.c[0, 1] exposes that coupler's flux bus.

Typed schema base classes

A chip type no preset covers gets a subclass built from the same three pieces the presets use: an accessor carrying one property per bus kind, a factory that turns an index into an accessor, and the schema carrying one property per element. The two base classes hold the machinery for the first two, and CouplerFactory is reusable as it stands, because a coupler's single flux bus is the same in every preset that has one. Defining your own typed schema walks through a complete class.

_TypedElementAccessor

_TypedElementAccessor(
    element: str,
    index: int | tuple,
    naming: BusNaming,
    parent: BusSchema,
)

Base class for typed element accessors. Concrete subclasses add per-bus @property accessors.

Source code in src/qprogram/buses.py
def __init__(self, element: str, index: int | tuple, naming: BusNaming, parent: BusSchema) -> None:
    self._element = element
    self._index = index
    self._naming = naming
    self._parent = parent

_ref

_ref(
    kind: str,
    channel: ChannelType,
    *,
    acquires: bool = False,
) -> BusRef
Source code in src/qprogram/buses.py
def _ref(self, kind: str, channel: ChannelType, *, acquires: bool = False) -> BusRef:
    raw = self._naming.resolve(self._element, self._index, kind)
    return BusRef(
        raw,
        element=self._element,
        idx=self._index,
        kind=kind,
        channel=channel,
        acquires=acquires,
        schema=self._parent,
    )

_TypedElementFactory

_TypedElementFactory(
    element: str, naming: BusNaming, parent: BusSchema
)

Base class for typed element factories. Concrete subclasses specify _accessor_cls.

Source code in src/qprogram/buses.py
def __init__(self, element: str, naming: BusNaming, parent: BusSchema) -> None:
    self._element = element
    self._naming = naming
    self._parent = parent

__getitem__

__getitem__(index: int) -> _TypedElementAccessor
Source code in src/qprogram/buses.py
def __getitem__(self, index: int) -> _TypedElementAccessor:
    return self._accessor_cls(self._element, index, self._naming, self._parent)

CouplerFactory

CouplerFactory(
    element: str, naming: BusNaming, parent: BusSchema
)

Bases: _TypedElementFactory

Subscriptable factory returning CouplerBuses instances. Indices may be tuples.

Source code in src/qprogram/buses.py
def __init__(self, element: str, naming: BusNaming, parent: BusSchema) -> None:
    self._element = element
    self._naming = naming
    self._parent = parent

Re-resolving a coordinate

resolve_ref is the one place an (element, index, kind) coordinate becomes a BusRef: the .qp parser calls it for every element[i].kind path it reads, and QProgram.rebind calls it for every ref it rewrites, which is what keeps a re-indexed or ported program checked against the schema it lands on. naming_substituted_schema covers the naming-only port, returning a dynamic copy of the schema with the same elements declared under a new BusNaming.

resolve_ref

resolve_ref(
    schema: BusSchema,
    element: str,
    index: int | tuple[int, ...],
    kind: str,
) -> BusRef

Re-resolve an (element, index, kind) coordinate into a typed BusRef.

The single source of truth for turning a structural bus coordinate into a BusRef under schema's naming. Used both when loading element[i].kind paths (_resolve_bus_path) and when porting a program to new indices or a new schema (rebind).

Parameters:

  • schema (BusSchema) –

    The schema to resolve the coordinate against; it becomes the ref's BusRef.schema back-pointer.

  • element (str) –

    Element name, as registered on schema.

  • index (int | tuple[int, ...]) –

    Element index, a tuple for multi-index elements such as couplers.

  • kind (str) –

    Bus kind name declared for that element.

Returns:

  • BusRef

    A typed BusRef whose string form follows schema's naming pattern.

Raises:

  • AttributeError

    If element is not an element of schema or kind is not one of that element's bus kinds.

  • KeyError

    If schema's naming pattern names a placeholder other than {element}, {index} or {kind}.

  • ValueError

    If schema's naming pattern is not a well-formed format string.

  • IndexError

    If schema's naming pattern uses a positional placeholder such as {0}.

Source code in src/qprogram/buses.py
def resolve_ref(schema: BusSchema, element: str, index: int | tuple[int, ...], kind: str) -> BusRef:
    """Re-resolve an ``(element, index, kind)`` coordinate into a typed [`BusRef`][qprogram.BusRef].

    The single source of truth for turning a structural bus coordinate into a [`BusRef`][qprogram.BusRef] under
    ``schema``'s naming. Used both when loading ``element[i].kind`` paths
    (`_resolve_bus_path`) and when porting a program to
    new indices or a new schema ([`rebind`][qprogram.QProgram.rebind]).

    Args:
        schema (BusSchema): The schema to resolve the coordinate against; it becomes the ref's
            `BusRef.schema` back-pointer.
        element (str): Element name, as registered on ``schema``.
        index (int | tuple[int, ...]): Element index, a tuple for multi-index elements such as
            couplers.
        kind (str): Bus kind name declared for that element.

    Returns:
        A typed [`BusRef`][qprogram.BusRef] whose string form follows ``schema``'s naming pattern.

    Raises:
        AttributeError: If ``element`` is not an element of ``schema`` or ``kind`` is not one of that
            element's bus kinds.
        KeyError: If ``schema``'s naming pattern names a placeholder other than ``{element}``,
            ``{index}`` or ``{kind}``.
        ValueError: If ``schema``'s naming pattern is not a well-formed format string.
        IndexError: If ``schema``'s naming pattern uses a positional placeholder such as ``{0}``.
    """
    factory = getattr(schema, element)
    accessor = factory[index]
    return getattr(accessor, kind)

naming_substituted_schema

naming_substituted_schema(
    schema: BusSchema, naming: BusNaming
) -> BusSchema

Return a dynamic copy of schema with every element re-declared under naming.

Used by rebind for naming-only ports: the structural element/bus content is preserved but BusRef strings (and the serialized schema: block) adopt the new pattern. The result is a plain (untyped) BusSchema — the same trade-off as BusSchema.combine.

Parameters:

  • schema (BusSchema) –

    The schema whose elements and bus kinds are carried over.

  • naming (BusNaming) –

    The naming convention the copy resolves bus strings through.

Returns:

Source code in src/qprogram/buses.py
def naming_substituted_schema(schema: BusSchema, naming: BusNaming) -> BusSchema:
    """Return a dynamic copy of ``schema`` with every element re-declared under ``naming``.

    Used by [`rebind`][qprogram.QProgram.rebind] for naming-only ports: the structural element/bus content
    is preserved but [`BusRef`][qprogram.BusRef] strings (and the serialized ``schema:`` block) adopt the new
    pattern. The result is a plain (untyped) [`BusSchema`][qprogram.BusSchema] — the same trade-off as
    [`BusSchema.combine`][qprogram.BusSchema.combine].

    Args:
        schema (BusSchema): The schema whose elements and bus kinds are carried over.
        naming (BusNaming): The naming convention the copy resolves bus strings through.

    Returns:
        A new dynamic [`BusSchema`][qprogram.BusSchema] with the same elements under the given naming.
    """
    new_schema = BusSchema(naming=naming)
    for name, element in schema.elements.items():
        new_schema.add_element(name, dict(element.buses))
    return new_schema

Variables and expressions

Variable

Variable(
    id: str,
    *,
    label: str | None = None,
    units: str | None = None,
    description: str | None = None,
)

Bases: Expression

A symbolic variable.

Leaf node of the expression AST, holding a value that starts out UNASSIGNED.

Equality and hashing are structural over id: two variables compare equal when their id matches. Within one program ids are unique (QProgram.variable rejects duplicates), so id-equality coincides with identity there; across programs it is what lets a program survive deepcopy / qp.loads(qp.dumps(...)) and still compare equal to the original.

The runtime sets the value via set_value per loop iteration; reading value (or calling evaluate) returns the current value or UNASSIGNED.

Parameters:

  • id (str) –

    Short name matching [A-Za-z_][A-Za-z0-9_]*. Doubles as the identifier in the .qp format, so no spaces or punctuation.

  • label (str | None, default: None ) –

    Human-readable name for axis labels, plot titles, and the like.

  • units (str | None, default: None ) –

    Unit string, such as "Hz", "ns", or "V".

  • description (str | None, default: None ) –

    Longer free-form description.

Raises:

Source code in src/qprogram/variable.py
def __init__(
    self,
    id: str,  # ruff: ignore[builtin-argument-shadowing]
    *,
    label: str | None = None,
    units: str | None = None,
    description: str | None = None,
) -> None:
    if not _ID_RE.match(id):
        raise InvalidVariableIdError(id)
    if id in RESERVED_KEYWORDS:
        raise InvalidVariableIdError(id, reserved=True)
    self._id: str = id
    self._label: str | None = label
    self._units: str | None = units
    self._description: str | None = description
    self._value: int | float | _UnassignedType = UNASSIGNED

id property

id: str

The identifier, emitted verbatim as the variable's token in the .qp format.

label property

label: str | None

The human-readable name for axis labels and plot titles, or None.

units property

units: str | None

The unit the variable's values are expressed in, or None.

description property

description: str | None

The free-form description, or None.

value property

value: int | float | _UnassignedType

The current value, or UNASSIGNED if no value has been set.

set_value

set_value(value: float) -> None

Set the variable's current value.

Parameters:

  • value (float) –

    The value to bind. The runtime writes it once per loop iteration.

Source code in src/qprogram/variable.py
def set_value(self, value: float) -> None:
    """Set the variable's current value.

    Args:
        value (float): The value to bind. The runtime writes it once per loop iteration.
    """
    self._value = value

reset

reset() -> None

Clear the variable's value, returning it to UNASSIGNED.

Source code in src/qprogram/variable.py
def reset(self) -> None:
    """Clear the variable's value, returning it to `UNASSIGNED`."""
    self._value = UNASSIGNED

evaluate

evaluate() -> int | float | _UnassignedType

Return the variable's current value.

Returns:

  • int | float | _UnassignedType

    The bound value, or UNASSIGNED while nothing is bound.

Source code in src/qprogram/variable.py
def evaluate(self) -> int | float | _UnassignedType:
    """Return the variable's current value.

    Returns:
        The bound value, or `UNASSIGNED` while nothing is bound.
    """
    return self._value

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

  • set[Variable]

    A single-element set holding this variable.

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        A single-element set holding this variable.
    """
    return {self}

Expression

Bases: ABC

Abstract base for symbolic expressions.

Subclasses split into four families:

Operators that map cleanly onto Python syntax build the corresponding AST nodes uniformly across every subclass. The exceptions are == / != (use eq / ne so Variable can keep a plain-bool __eq__ for set / dict-key use) and and / or / not (Python keywords, unoverloadable — use & | ~ or the named helpers and_ / or_ / not_).

Why __bool__ raises: if freq < 5e9: would otherwise silently test the truthiness of the Comparison instance (always true) instead of building the conditional the user almost certainly meant. Mirrors SymPy.

evaluate abstractmethod

evaluate() -> int | float | _UnassignedType

Compute the value of the expression.

Returns:

  • int | float | _UnassignedType

    A numeric result for arithmetic / math expressions, a bool for comparisons and logical

  • int | float | _UnassignedType

    expressions (bool is an int subclass), or UNASSIGNED when any referenced

  • int | float | _UnassignedType

    variable is currently unassigned.

Source code in src/qprogram/variable.py
@abstractmethod
def evaluate(self) -> int | float | _UnassignedType:
    """Compute the value of the expression.

    Returns:
        A numeric result for arithmetic / math expressions, a ``bool`` for comparisons and logical
        expressions (``bool`` is an ``int`` subclass), or `UNASSIGNED` when any referenced
        variable is currently unassigned.
    """
    ...

evaluate_or_raise

evaluate_or_raise() -> int | float

Compute the value of the expression, raising instead of returning UNASSIGNED.

Returns:

  • int | float

    The numeric value.

Raises:

  • UnassignedVariableError

    When any referenced variable is unassigned. Use this when the caller (e.g. waveform envelope computation) requires a concrete value.

  • ZeroDivisionError

    When the expression divides by an operand that evaluates to zero.

Source code in src/qprogram/variable.py
def evaluate_or_raise(self) -> int | float:
    """Compute the value of the expression, raising instead of returning `UNASSIGNED`.

    Returns:
        The numeric value.

    Raises:
        UnassignedVariableError: When any referenced variable is unassigned. Use this when the
            caller (e.g. waveform envelope computation) requires a concrete value.
        ZeroDivisionError: When the expression divides by an operand that evaluates to zero.
    """
    result = self.evaluate()
    if isinstance(result, _UnassignedType):
        raise UnassignedVariableError(self)
    return result

variables abstractmethod

variables() -> set[Variable]

Return the free Variable s appearing in this expression.

Every node unions the sets reported by its children, so one call covers the whole subtree.

Returns:

  • set[Variable]

    The variables reachable from this node; an empty set for leaves that hold none.

Source code in src/qprogram/variable.py
@abstractmethod
def variables(self) -> set[Variable]:
    """Return the free [`Variable`][qprogram.Variable] s appearing in this expression.

    Every node unions the sets reported by its children, so one call covers the whole subtree.

    Returns:
        The variables reachable from this node; an empty set for leaves that hold none.
    """
    ...

Constant

Constant(value: float)

Bases: Expression

Concrete numeric leaf with structural equality (Constant(5) == Constant(5)).

Numeric literals appearing in expressions are auto-wrapped to Constant by the operator methods on Expression.

Parameters:

  • value (float) –

    The number to hold; an int is kept as an int. bool is rejected — booleans would silently coerce to 0/1 and obscure intent.

Raises:

  • TypeError

    If value is not an int or float (or is a bool).

Source code in src/qprogram/variable.py
def __init__(self, value: float) -> None:
    if not isinstance(value, (int, float)) or isinstance(value, bool):
        msg = f"Constant value must be int or float, got {type(value).__name__}"
        raise TypeError(msg)
    self.value: int | float = value

evaluate

evaluate() -> int | float

Return the constant's value.

Returns:

  • int | float

    The stored number. A constant is always bound, so this is never UNASSIGNED.

Source code in src/qprogram/variable.py
def evaluate(self) -> int | float:
    """Return the constant's value.

    Returns:
        The stored number. A constant is always bound, so this is never `UNASSIGNED`.
    """
    return self.value

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        An empty set.
    """
    return set()

BinaryOp

BinaryOp(
    op: BinaryOperator, left: Expression, right: Expression
)

Bases: Expression

Binary arithmetic node — left op right for one of + - * /.

Constructed by the arithmetic operators on Expression. Numeric literals are auto-wrapped to Constant, so both operands are always Expression instances.

Parameters:

  • op (BinaryOperator) –

    Operator symbol — one of +, -, *, /.

  • left (Expression) –

    Left operand.

  • right (Expression) –

    Right operand.

Source code in src/qprogram/variable.py
def __init__(self, op: BinaryOperator, left: Expression, right: Expression) -> None:
    self.op: BinaryOperator = op
    self.left: Expression = left
    self.right: Expression = right

evaluate

evaluate() -> int | float | _UnassignedType

Apply the arithmetic operator to both operands.

Returns:

  • int | float | _UnassignedType

    The numeric result, or UNASSIGNED when either operand is unassigned.

Raises:

  • ZeroDivisionError

    If the operator is / and the right operand evaluates to zero.

Source code in src/qprogram/variable.py
def evaluate(self) -> int | float | _UnassignedType:
    """Apply the arithmetic operator to both operands.

    Returns:
        The numeric result, or `UNASSIGNED` when either operand is unassigned.

    Raises:
        ZeroDivisionError: If the operator is ``/`` and the right operand evaluates to zero.
    """
    left_value = self.left.evaluate()
    if isinstance(left_value, _UnassignedType):
        return UNASSIGNED
    right_value = self.right.evaluate()
    if isinstance(right_value, _UnassignedType):
        return UNASSIGNED
    if self.op == "+":
        return left_value + right_value
    if self.op == "-":
        return left_value - right_value
    if self.op == "*":
        return left_value * right_value
    return left_value / right_value

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

  • set[Variable]

    The union of both operands' variables.

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        The union of both operands' variables.
    """
    return self.left.variables() | self.right.variables()

UnaryOp

UnaryOp(op: UnaryOperator, operand: Expression)

Bases: Expression

Unary arithmetic node — op operand for one of - +.

Parameters:

  • op (UnaryOperator) –

    Operator symbol — - or +.

  • operand (Expression) –

    The operand.

Source code in src/qprogram/variable.py
def __init__(self, op: UnaryOperator, operand: Expression) -> None:
    self.op: UnaryOperator = op
    self.operand: Expression = operand

evaluate

evaluate() -> int | float | _UnassignedType

Apply the sign operator to the operand.

Returns:

  • int | float | _UnassignedType

    The negated value for - and the operand's own value for +, or UNASSIGNED

  • int | float | _UnassignedType

    when the operand is unassigned.

Source code in src/qprogram/variable.py
def evaluate(self) -> int | float | _UnassignedType:
    """Apply the sign operator to the operand.

    Returns:
        The negated value for ``-`` and the operand's own value for ``+``, or `UNASSIGNED`
        when the operand is unassigned.
    """
    operand_value = self.operand.evaluate()
    if isinstance(operand_value, _UnassignedType):
        return UNASSIGNED
    return -operand_value if self.op == "-" else +operand_value

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

  • set[Variable]

    The operand's variables.

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        The operand's variables.
    """
    return self.operand.variables()

Comparison

Comparison(
    op: ComparisonOperator,
    left: Expression,
    right: Expression,
)

Bases: Expression

Binary comparison node — left op right for one of == != < <= > >=.

Constructed by the comparison operators on Expression (< <= > >=) and by the helper functions eq / ne (== !=; see Expression for why these are not overloaded directly).

evaluate returns bool (an int subclass) or UNASSIGNED if either operand is unassigned.

Parameters:

  • op (ComparisonOperator) –

    Operator symbol — one of ==, !=, <, <=, >, >=.

  • left (Expression) –

    Left operand.

  • right (Expression) –

    Right operand.

Raises:

  • ValueError

    If op is not a recognized comparison operator (defensive — callers should pass a Literal).

Source code in src/qprogram/variable.py
def __init__(self, op: ComparisonOperator, left: Expression, right: Expression) -> None:
    # defensive: callers should pass a Literal
    if op not in self._OPS:
        msg = f"Comparison op must be one of {sorted(self._OPS)}, got {op!r}"
        raise ValueError(msg)
    self.op: ComparisonOperator = op
    self.left: Expression = left
    self.right: Expression = right

evaluate

evaluate() -> bool | _UnassignedType

Compare the two operands.

Returns:

  • bool | _UnassignedType

    The boolean outcome of the comparison, or UNASSIGNED when either operand is

  • bool | _UnassignedType

    unassigned.

Source code in src/qprogram/variable.py
def evaluate(self) -> bool | _UnassignedType:
    """Compare the two operands.

    Returns:
        The boolean outcome of the comparison, or `UNASSIGNED` when either operand is
        unassigned.
    """
    left_value = self.left.evaluate()
    if isinstance(left_value, _UnassignedType):
        return UNASSIGNED
    right_value = self.right.evaluate()
    if isinstance(right_value, _UnassignedType):
        return UNASSIGNED
    if self.op == "==":
        return left_value == right_value
    if self.op == "!=":
        return left_value != right_value
    if self.op == "<":
        return left_value < right_value
    if self.op == "<=":
        return left_value <= right_value
    if self.op == ">":
        return left_value > right_value
    return left_value >= right_value

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

  • set[Variable]

    The union of both operands' variables.

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        The union of both operands' variables.
    """
    return self.left.variables() | self.right.variables()

LogicalBinaryOp

LogicalBinaryOp(
    op: LogicalBinaryOperator,
    left: Expression,
    right: Expression,
)

Bases: Expression

Binary logical node — left op right for and or or.

Constructed by & and | on Expression, or by the named helpers and_ / or_.

Why this never short-circuits: any UNASSIGNED operand propagates upwards. Matches the rest of the evaluator and makes unbound-variable diagnostics deterministic rather than position-dependent.

Parameters:

  • op (LogicalBinaryOperator) –

    Operator symbol — and or or.

  • left (Expression) –

    Left operand.

  • right (Expression) –

    Right operand.

Raises:

  • ValueError

    If op is not a recognized logical operator (defensive).

  • TypeError

    If either operand is not an Expression.

Source code in src/qprogram/variable.py
def __init__(self, op: LogicalBinaryOperator, left: Expression, right: Expression) -> None:
    if op not in self._OPS:  # defensive
        msg = f"LogicalBinaryOp op must be one of {sorted(self._OPS)}, got {op!r}"
        raise ValueError(msg)
    _require_expression(left, where="LogicalBinaryOp left operand")
    _require_expression(right, where="LogicalBinaryOp right operand")
    self.op: LogicalBinaryOperator = op
    self.left: Expression = left
    self.right: Expression = right

evaluate

evaluate() -> bool | _UnassignedType

Combine the truthiness of both operands.

Returns:

  • bool | _UnassignedType

    The boolean outcome of and / or, or UNASSIGNED when either operand is

  • bool | _UnassignedType

    unassigned. Both operands are always evaluated.

Source code in src/qprogram/variable.py
def evaluate(self) -> bool | _UnassignedType:
    """Combine the truthiness of both operands.

    Returns:
        The boolean outcome of ``and`` / ``or``, or `UNASSIGNED` when either operand is
        unassigned. Both operands are always evaluated.
    """
    left_value = self.left.evaluate()
    if isinstance(left_value, _UnassignedType):
        return UNASSIGNED
    right_value = self.right.evaluate()
    if isinstance(right_value, _UnassignedType):
        return UNASSIGNED
    if self.op == "and":
        return bool(left_value) and bool(right_value)
    return bool(left_value) or bool(right_value)

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

  • set[Variable]

    The union of both operands' variables.

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        The union of both operands' variables.
    """
    return self.left.variables() | self.right.variables()

LogicalNot

LogicalNot(operand: Expression)

Bases: Expression

Logical negation node — not operand.

Constructed by ~ on Expression or by not_.

Parameters:

  • operand (Expression) –

    The expression to negate.

Raises:

Source code in src/qprogram/variable.py
def __init__(self, operand: Expression) -> None:
    _require_expression(operand, where="LogicalNot operand")
    self.operand: Expression = operand

evaluate

evaluate() -> bool | _UnassignedType

Negate the truthiness of the operand.

Returns:

  • bool | _UnassignedType

    True when the operand is falsy and False when it is truthy, or UNASSIGNED

  • bool | _UnassignedType

    when the operand is unassigned.

Source code in src/qprogram/variable.py
def evaluate(self) -> bool | _UnassignedType:
    """Negate the truthiness of the operand.

    Returns:
        ``True`` when the operand is falsy and ``False`` when it is truthy, or `UNASSIGNED`
        when the operand is unassigned.
    """
    operand_value = self.operand.evaluate()
    if isinstance(operand_value, _UnassignedType):
        return UNASSIGNED
    return not bool(operand_value)

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

  • set[Variable]

    The operand's variables.

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        The operand's variables.
    """
    return self.operand.variables()

MathFunc

MathFunc(name: str, operands: tuple[Expression, ...])

Bases: Expression

Math-function application — name(arg1, arg2, ...).

The function name selects the numeric implementation. Recognized names are listed in _MATH_FUNCTIONS. Each is dispatched via the lazy lookup in _math_eval, keeping this module free of a direct numpy/math import at the AST layer.

Arity is not enforced beyond requiring at least one operand: minimum and maximum fold every operand, and every other function reads the first and ignores the rest. The module-level builders (sin, minimum, ...) are the arity-checked way in.

Parameters:

  • name (str) –

    Function name, such as "sin" or "minimum".

  • operands (tuple[Expression, ...]) –

    Operand expressions.

Raises:

  • ValueError

    If name is not a recognized math function, or if operands is empty.

Source code in src/qprogram/variable.py
def __init__(self, name: str, operands: tuple[Expression, ...]) -> None:
    if name not in _MATH_FUNCTIONS:
        msg = f"Unknown math function {name!r}; known: {sorted(_MATH_FUNCTIONS)}"
        raise ValueError(msg)
    if not operands:
        msg = f"Math function {name!r} requires at least one operand"
        raise ValueError(msg)
    self.name: str = name
    self.operands: tuple[Expression, ...] = operands

evaluate

evaluate() -> int | float | _UnassignedType

Evaluate every operand and apply the named function.

Returns:

  • int | float | _UnassignedType

    The numeric result, or UNASSIGNED as soon as any operand is unassigned.

Source code in src/qprogram/variable.py
def evaluate(self) -> int | float | _UnassignedType:
    """Evaluate every operand and apply the named function.

    Returns:
        The numeric result, or `UNASSIGNED` as soon as any operand is unassigned.
    """
    values: list[int | float] = []
    for op in self.operands:
        v = op.evaluate()
        if isinstance(v, _UnassignedType):
            return UNASSIGNED
        values.append(v)
    return _math_eval(self.name, values)

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

  • set[Variable]

    The union of every operand's variables.

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        The union of every operand's variables.
    """
    out: set[Variable] = set()
    for op in self.operands:
        out |= op.variables()
    return out

Where

Where(
    condition: Expression,
    then: Expression,
    else_: Expression,
)

Bases: Expression

Ternary conditional expression — where(condition, then, else_).

Short-circuits: the unchosen branch is not evaluated, so it may safely reference unassigned variables. If condition itself is unassigned the whole expression evaluates to UNASSIGNED.

Users normally construct via the where helper.

Parameters:

  • condition (Expression) –

    The predicate expression.

  • then (Expression) –

    Returned when condition evaluates to truthy.

  • else_ (Expression) –

    Returned when condition evaluates to falsy.

Raises:

  • TypeError

    If any argument is not an Expression.

Source code in src/qprogram/variable.py
def __init__(self, condition: Expression, then: Expression, else_: Expression) -> None:
    _require_expression(condition, where="Where condition")
    _require_expression(then, where="Where 'then' branch")
    _require_expression(else_, where="Where 'else_' branch")
    self.condition: Expression = condition
    self.then: Expression = then
    self.else_: Expression = else_

evaluate

evaluate() -> int | float | _UnassignedType

Evaluate the condition, then the one branch it selects.

Returns:

  • int | float | _UnassignedType

    The chosen branch's value. UNASSIGNED when the condition is unassigned, or when

  • int | float | _UnassignedType

    the chosen branch is; the branch that is not taken is never evaluated.

Source code in src/qprogram/variable.py
def evaluate(self) -> int | float | _UnassignedType:
    """Evaluate the condition, then the one branch it selects.

    Returns:
        The chosen branch's value. `UNASSIGNED` when the condition is unassigned, or when
        the chosen branch is; the branch that is not taken is never evaluated.
    """
    cond_value = self.condition.evaluate()
    if isinstance(cond_value, _UnassignedType):
        return UNASSIGNED
    chosen = self.then if cond_value else self.else_
    return chosen.evaluate()

variables

variables() -> set[Variable]

Return the free variables in this expression.

Returns:

  • set[Variable]

    The union of the condition's and both branches' variables — the branch that evaluation

  • set[Variable]

    skips still contributes.

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    Returns:
        The union of the condition's and both branches' variables — the branch that evaluation
        skips still contributes.
    """
    return self.condition.variables() | self.then.variables() | self.else_.variables()

MeasurementRef

MeasurementRef(handle: MeasurementHandle, field: str)

Bases: Expression

Reference to a field of a measurement result, used inside conditional expressions.

Built implicitly by the proxy that MeasurementHandle.state returns when the user writes handle.state == 0; direct construction is rarely needed.

Equality is structural over (handle.name, field): distinct instances built at different sites refer to the same logical reference whenever their names match. Mirrors Variable's cross-program equality story so programs survive deepcopy / qp.loads(qp.dumps(...)).

Parameters:

Raises:

  • ValueError

    If field is not in the allowed set.

Source code in src/qprogram/variable.py
def __init__(self, handle: MeasurementHandle, field: str) -> None:
    if field not in self._ALLOWED_FIELDS:
        msg = f"MeasurementRef field must be one of {sorted(self._ALLOWED_FIELDS)}, got {field!r}"
        raise ValueError(msg)
    self.handle: MeasurementHandle = handle
    self.field: str = field

evaluate

evaluate() -> int | float | _UnassignedType

Return the current value of the referenced measurement field.

Returns:

  • int | float | _UnassignedType

    The value the runtime recorded on the handle for this field, or UNASSIGNED before

  • int | float | _UnassignedType

    the measurement has produced one.

Source code in src/qprogram/variable.py
def evaluate(self) -> int | float | _UnassignedType:
    """Return the current value of the referenced measurement field.

    Returns:
        The value the runtime recorded on the handle for this field, or `UNASSIGNED` before
        the measurement has produced one.
    """
    return self.handle._value_for(self.field)  # ruff: ignore[private-member-access]

variables

variables() -> set[Variable]

Return the free variables in this expression.

A measurement reference is its own binding kind, distinct from Variable, so it takes no part in the loop-counter variable walk the rest of the AST feeds.

Returns:

Source code in src/qprogram/variable.py
def variables(self) -> set[Variable]:
    """Return the free variables in this expression.

    A measurement reference is its own binding kind, distinct from [`Variable`][qprogram.Variable], so it takes
    no part in the loop-counter variable walk the rest of the AST feeds.

    Returns:
        An empty set.
    """
    return set()

UNASSIGNED module-attribute

UNASSIGNED: Final[_UnassignedType] = _UnassignedType()

Sentinel returned by evaluate() when a variable in the expression has no value.

Helper functions

Free functions that build expression nodes, all reached as qp.eq, qp.sin, and so on. eq and ne are the only way to compare two expressions for equality: Variable.__eq__ compares ids and has to keep returning a bool so that variables stay usable in sets and as dictionary keys. and_, or_, and not_ are function forms of &, |, and ~, which Expression does overload. The math functions, minimum, maximum, and where have no operator form at all.

eq

eq(
    left: Expression | float | _HandleFieldAccess,
    right: Expression | float | _HandleFieldAccess,
) -> Comparison

Build an equality Comparison.

Why this exists rather than overloading == on Variable: Variable.__eq__ compares ids and must keep returning a plain bool, so that variables stay usable in sets (notably the expression.variables() walk) and as dict keys — which rules out building a Comparison from ==. Numeric operands are wrapped as Constant; handle.<field> proxies resolve to MeasurementRef. Equivalent to handle.state == 0 when one operand is a field-access proxy.

Parameters:

  • left (Expression | float | _HandleFieldAccess) –

    Left operand — an expression, a number, or a handle.<field> proxy.

  • right (Expression | float | _HandleFieldAccess) –

    Right operand, same forms as left.

Returns:

Raises:

  • TypeError

    If either operand is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def eq(
    left: Expression | float | _HandleFieldAccess,
    right: Expression | float | _HandleFieldAccess,
) -> Comparison:
    """Build an equality [`Comparison`][qprogram.Comparison].

    Why this exists rather than overloading ``==`` on [`Variable`][qprogram.Variable]: ``Variable.__eq__`` compares
    ids and must keep returning a plain ``bool``, so that variables stay usable in sets (notably the
    ``expression.variables()`` walk) and as dict keys — which rules out building a
    [`Comparison`][qprogram.Comparison] from ``==``. Numeric operands are wrapped as [`Constant`][qprogram.Constant];
    ``handle.<field>`` proxies resolve to [`MeasurementRef`][qprogram.MeasurementRef]. Equivalent to
    ``handle.state == 0`` when one operand is a field-access proxy.

    Args:
        left (Expression | float | _HandleFieldAccess): Left operand — an expression, a number, or a
            ``handle.<field>`` proxy.
        right (Expression | float | _HandleFieldAccess): Right operand, same forms as ``left``.

    Returns:
        A [`Comparison`][qprogram.Comparison] node for ``left == right``.

    Raises:
        TypeError: If either operand is a ``bool`` or any other type with no expression form.
    """
    return Comparison("==", _wrap(left), _wrap(right))

ne

ne(
    left: Expression | float | _HandleFieldAccess,
    right: Expression | float | _HandleFieldAccess,
) -> Comparison

Build an inequality Comparison — counterpart of eq.

Parameters:

  • left (Expression | float | _HandleFieldAccess) –

    Left operand — an expression, a number, or a handle.<field> proxy.

  • right (Expression | float | _HandleFieldAccess) –

    Right operand, same forms as left.

Returns:

Raises:

  • TypeError

    If either operand is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def ne(
    left: Expression | float | _HandleFieldAccess,
    right: Expression | float | _HandleFieldAccess,
) -> Comparison:
    """Build an inequality [`Comparison`][qprogram.Comparison] — counterpart of `eq`.

    Args:
        left (Expression | float | _HandleFieldAccess): Left operand — an expression, a number, or a
            ``handle.<field>`` proxy.
        right (Expression | float | _HandleFieldAccess): Right operand, same forms as ``left``.

    Returns:
        A [`Comparison`][qprogram.Comparison] node for ``left != right``.

    Raises:
        TypeError: If either operand is a ``bool`` or any other type with no expression form.
    """
    return Comparison("!=", _wrap(left), _wrap(right))

and_

and_(
    left: Expression, right: Expression
) -> LogicalBinaryOp

Build left and right — function form of the & operator.

Precedence reminder: in Python & binds tighter than comparison operators, so a < b & c < d parses as a < (b & c) < d. Parenthesize comparisons explicitly: (a < b) & (c < d).

Parameters:

Returns:

Raises:

  • TypeError

    If either operand is not an Expression. Numbers are not coerced here — a logical operand must already be an expression.

Source code in src/qprogram/variable.py
def and_(left: Expression, right: Expression) -> LogicalBinaryOp:
    """Build ``left and right`` — function form of the ``&`` operator.

    Precedence reminder: in Python ``&`` binds tighter than comparison operators, so
    ``a < b & c < d`` parses as ``a < (b & c) < d``. Parenthesize comparisons explicitly:
    ``(a < b) & (c < d)``.

    Args:
        left (Expression): Left operand.
        right (Expression): Right operand.

    Returns:
        A [`LogicalBinaryOp`][qprogram.LogicalBinaryOp] node for ``left and right``.

    Raises:
        TypeError: If either operand is not an [`Expression`][qprogram.Expression]. Numbers are not coerced here —
            a logical operand must already be an expression.
    """
    return LogicalBinaryOp("and", left, right)

or_

or_(left: Expression, right: Expression) -> LogicalBinaryOp

Build left or right — function form of the | operator.

Parameters:

Returns:

Raises:

  • TypeError

    If either operand is not an Expression.

Source code in src/qprogram/variable.py
def or_(left: Expression, right: Expression) -> LogicalBinaryOp:
    """Build ``left or right`` — function form of the ``|`` operator.

    Args:
        left (Expression): Left operand.
        right (Expression): Right operand.

    Returns:
        A [`LogicalBinaryOp`][qprogram.LogicalBinaryOp] node for ``left or right``.

    Raises:
        TypeError: If either operand is not an [`Expression`][qprogram.Expression].
    """
    return LogicalBinaryOp("or", left, right)

not_

not_(operand: Expression) -> LogicalNot

Build not operand — function form of the ~ operator.

Parameters:

  • operand (Expression) –

    The expression to negate.

Returns:

Raises:

Source code in src/qprogram/variable.py
def not_(operand: Expression) -> LogicalNot:
    """Build ``not operand`` — function form of the ``~`` operator.

    Args:
        operand (Expression): The expression to negate.

    Returns:
        A [`LogicalNot`][qprogram.LogicalNot] node wrapping ``operand``.

    Raises:
        TypeError: If ``operand`` is not an [`Expression`][qprogram.Expression].
    """
    return LogicalNot(operand)

sin

sin(x: Expression | float) -> MathFunc

Build a symbolic sin(x).

Parameters:

Returns:

Raises:

  • TypeError

    If x is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def sin(x: Expression | float) -> MathFunc:
    """Build a symbolic ``sin(x)``.

    Args:
        x (Expression | float): The operand. A number is wrapped as a [`Constant`][qprogram.Constant].

    Returns:
        A [`MathFunc`][qprogram.MathFunc] node for ``sin(x)``.

    Raises:
        TypeError: If ``x`` is a ``bool`` or any other type with no expression form.
    """
    return MathFunc("sin", (_wrap(x),))

cos

cos(x: Expression | float) -> MathFunc

Build a symbolic cos(x).

Parameters:

Returns:

Raises:

  • TypeError

    If x is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def cos(x: Expression | float) -> MathFunc:
    """Build a symbolic ``cos(x)``.

    Args:
        x (Expression | float): The operand. A number is wrapped as a [`Constant`][qprogram.Constant].

    Returns:
        A [`MathFunc`][qprogram.MathFunc] node for ``cos(x)``.

    Raises:
        TypeError: If ``x`` is a ``bool`` or any other type with no expression form.
    """
    return MathFunc("cos", (_wrap(x),))

tan

tan(x: Expression | float) -> MathFunc

Build a symbolic tan(x).

Parameters:

Returns:

Raises:

  • TypeError

    If x is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def tan(x: Expression | float) -> MathFunc:
    """Build a symbolic ``tan(x)``.

    Args:
        x (Expression | float): The operand. A number is wrapped as a [`Constant`][qprogram.Constant].

    Returns:
        A [`MathFunc`][qprogram.MathFunc] node for ``tan(x)``.

    Raises:
        TypeError: If ``x`` is a ``bool`` or any other type with no expression form.
    """
    return MathFunc("tan", (_wrap(x),))

exp

exp(x: Expression | float) -> MathFunc

Build a symbolic exp(x).

Parameters:

Returns:

Raises:

  • TypeError

    If x is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def exp(x: Expression | float) -> MathFunc:
    """Build a symbolic ``exp(x)``.

    Args:
        x (Expression | float): The operand. A number is wrapped as a [`Constant`][qprogram.Constant].

    Returns:
        A [`MathFunc`][qprogram.MathFunc] node for ``exp(x)``.

    Raises:
        TypeError: If ``x`` is a ``bool`` or any other type with no expression form.
    """
    return MathFunc("exp", (_wrap(x),))

log

log(x: Expression | float) -> MathFunc

Build a symbolic natural log log(x).

Parameters:

Returns:

Raises:

  • TypeError

    If x is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def log(x: Expression | float) -> MathFunc:
    """Build a symbolic natural log ``log(x)``.

    Args:
        x (Expression | float): The operand. A number is wrapped as a [`Constant`][qprogram.Constant].

    Returns:
        A [`MathFunc`][qprogram.MathFunc] node for the natural logarithm ``log(x)``.

    Raises:
        TypeError: If ``x`` is a ``bool`` or any other type with no expression form.
    """
    return MathFunc("log", (_wrap(x),))

sqrt

sqrt(x: Expression | float) -> MathFunc

Build a symbolic sqrt(x).

Parameters:

Returns:

Raises:

  • TypeError

    If x is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def sqrt(x: Expression | float) -> MathFunc:
    """Build a symbolic ``sqrt(x)``.

    Args:
        x (Expression | float): The operand. A number is wrapped as a [`Constant`][qprogram.Constant].

    Returns:
        A [`MathFunc`][qprogram.MathFunc] node for ``sqrt(x)``.

    Raises:
        TypeError: If ``x`` is a ``bool`` or any other type with no expression form.
    """
    return MathFunc("sqrt", (_wrap(x),))

minimum

minimum(*args: Expression | float) -> MathFunc

Build a symbolic minimum(a, b, ...).

Why this exists separately from min: the built-in compares with <, which on an Expression builds a Comparison node instead of a bool, so min(var_a, var_b) raises TypeError from Expression.__bool__ rather than building the symbolic minimum.

Parameters:

  • *args (Expression | float, default: () ) –

    At least two operands. Numbers are wrapped as Constant.

Returns:

Raises:

  • TypeError

    If fewer than two arguments are passed, or if an argument is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def minimum(*args: Expression | float) -> MathFunc:
    """Build a symbolic ``minimum(a, b, ...)``.

    Why this exists separately from `min`: the built-in compares with ``<``, which on an
    [`Expression`][qprogram.Expression] builds a [`Comparison`][qprogram.Comparison] node instead of a ``bool``, so
    ``min(var_a, var_b)`` raises `TypeError` from `Expression.__bool__` rather than
    building the symbolic minimum.

    Args:
        *args (Expression | float): At least two operands. Numbers are wrapped as [`Constant`][qprogram.Constant].

    Returns:
        A [`MathFunc`][qprogram.MathFunc] node for ``minimum(a, b, ...)``.

    Raises:
        TypeError: If fewer than two arguments are passed, or if an argument is a ``bool`` or any
            other type with no expression form.
    """
    if len(args) < 2:
        msg = "minimum() requires at least two arguments"
        raise TypeError(msg)
    return MathFunc("minimum", tuple(_wrap(a) for a in args))

maximum

maximum(*args: Expression | float) -> MathFunc

Build a symbolic maximum(a, b, ...) — same rationale as minimum.

Parameters:

  • *args (Expression | float, default: () ) –

    At least two operands. Numbers are wrapped as Constant.

Returns:

Raises:

  • TypeError

    If fewer than two arguments are passed, or if an argument is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def maximum(*args: Expression | float) -> MathFunc:
    """Build a symbolic ``maximum(a, b, ...)`` — same rationale as `minimum`.

    Args:
        *args (Expression | float): At least two operands. Numbers are wrapped as [`Constant`][qprogram.Constant].

    Returns:
        A [`MathFunc`][qprogram.MathFunc] node for ``maximum(a, b, ...)``.

    Raises:
        TypeError: If fewer than two arguments are passed, or if an argument is a ``bool`` or any
            other type with no expression form.
    """
    if len(args) < 2:
        msg = "maximum() requires at least two arguments"
        raise TypeError(msg)
    return MathFunc("maximum", tuple(_wrap(a) for a in args))

where

where(
    condition: Expression,
    then: Expression | float,
    else_: Expression | float,
) -> Where

Build a ternary Where expression — where(condition, then, else_).

Only the chosen branch is evaluated, so the unchosen branch may reference variables that happen to be unassigned at evaluation time.

Parameters:

Returns:

  • Where

    A Where node over the condition and the two branches.

Raises:

  • TypeError

    If condition is not an Expression, or if a branch is a bool or any other type with no expression form.

Source code in src/qprogram/variable.py
def where(condition: Expression, then: Expression | float, else_: Expression | float) -> Where:
    """Build a ternary [`Where`][qprogram.Where] expression — ``where(condition, then, else_)``.

    Only the chosen branch is evaluated, so the unchosen branch may reference variables that happen to
    be unassigned at evaluation time.

    Args:
        condition (Expression): Boolean expression, typically a [`Comparison`][qprogram.Comparison] or a
            [`LogicalBinaryOp`][qprogram.LogicalBinaryOp].
        then (Expression | float): Returned when ``condition`` is truthy. Numeric literals are wrapped
            as [`Constant`][qprogram.Constant].
        else_ (Expression | float): Returned when ``condition`` is falsy, wrapped the same way.

    Returns:
        A [`Where`][qprogram.Where] node over the condition and the two branches.

    Raises:
        TypeError: If ``condition`` is not an [`Expression`][qprogram.Expression], or if a branch is a ``bool`` or any
            other type with no expression form.
    """
    return Where(condition, _wrap(then), _wrap(else_))

Waveforms

waveforms

Built-in waveform shapes for pulse-level programs.

Single-channel waveforms subclass Waveform; complex (I/Q) waveforms subclass IQWaveform. Both bases expose envelope(resolution) / get_I() / get_Q() and get_duration() for platforms that need to render samples.

A shape parameter annotated float | Expression or int | Expression accepts an Expression in place of a number, so a swept variable can parameterize the shape; rendering samples then requires every referenced variable to hold a value. Parameters annotated with a plain numeric type take a number only.

Waveform

Bases: _StructuralEqMixin, ABC

Abstract base for single-channel (real-valued) waveforms.

A concrete shape supplies envelope and get_duration; the analysis and plotting helpers here are derived from the sampled envelope, so they work for any subclass. Equality and hashing are structural over the constructor attributes, which lets a waveform be used as a dictionary key and lets two independently built shapes compare equal.

envelope abstractmethod

envelope(resolution: int = 1) -> np.ndarray

Return the pulse envelope sampled at resolution-ns steps.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per ns.

Returns:

  • ndarray

    A 1-D array of duration / resolution samples. A shape whose parameters are all

  • ndarray

    floats yields a float array; one built from integers may yield an integer array.

Source code in src/qprogram/waveforms/waveform.py
@abstractmethod
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the pulse envelope sampled at ``resolution``-ns steps.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per ns.

    Returns:
        A 1-D array of ``duration / resolution`` samples. A shape whose parameters are all
        floats yields a float array; one built from integers may yield an integer array.
    """
    ...

get_duration abstractmethod

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration in nanoseconds.

Source code in src/qprogram/waveforms/waveform.py
@abstractmethod
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration in nanoseconds.
    """
    ...

peak_amplitude

peak_amplitude(resolution: int = 1) -> float

Return max(|envelope|) at the given sample resolution.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

Returns:

  • float

    The largest absolute sample value of the envelope.

Raises:

Source code in src/qprogram/waveforms/waveform.py
def peak_amplitude(self, resolution: int = 1) -> float:
    """Return ``max(|envelope|)`` at the given sample resolution.

    Args:
        resolution (int, optional): Sample period in nanoseconds.

    Returns:
        The largest absolute sample value of the envelope.

    Raises:
        UnassignedVariableError: If the envelope depends on a variable that has no value.
    """
    return float(np.max(np.abs(self.envelope(resolution=resolution))))

rms_amplitude

rms_amplitude(resolution: int = 1) -> float

Return the root-mean-square amplitude of the envelope at the given resolution.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

Returns:

  • float

    The root mean square of the envelope samples.

Raises:

Source code in src/qprogram/waveforms/waveform.py
def rms_amplitude(self, resolution: int = 1) -> float:
    """Return the root-mean-square amplitude of the envelope at the given resolution.

    Args:
        resolution (int, optional): Sample period in nanoseconds.

    Returns:
        The root mean square of the envelope samples.

    Raises:
        UnassignedVariableError: If the envelope depends on a variable that has no value.
    """
    env = self.envelope(resolution=resolution)
    return float(np.sqrt(np.mean(env**2)))

area

area(resolution: int = 1) -> float

Return the integrated envelope ∫ envelope(t) dt in nanosecond-amplitude units.

Useful for pulse-area calibration: a π rotation on a transmon corresponds to a fixed area independent of pulse shape (modulo nonlinear corrections).

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

Returns:

  • float

    The trapezoidal integral of the envelope over the pulse.

Raises:

Source code in src/qprogram/waveforms/waveform.py
def area(self, resolution: int = 1) -> float:
    """Return the integrated envelope ``∫ envelope(t) dt`` in nanosecond-amplitude units.

    Useful for pulse-area calibration: a π rotation on a transmon corresponds to a fixed area
    independent of pulse shape (modulo nonlinear corrections).

    Args:
        resolution (int, optional): Sample period in nanoseconds.

    Returns:
        The trapezoidal integral of the envelope over the pulse.

    Raises:
        UnassignedVariableError: If the envelope depends on a variable that has no value.
    """
    env = self.envelope(resolution=resolution)
    return float(np.trapezoid(env, dx=resolution))

spectrum

spectrum(
    resolution: int = 1,
) -> tuple[np.ndarray, np.ndarray]

Return the one-sided frequency spectrum of the envelope.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

Returns:

  • tuple[ndarray, ndarray]

    (frequencies_hz, complex_spectrum) from numpy.fft.rfft.

Raises:

Source code in src/qprogram/waveforms/waveform.py
def spectrum(self, resolution: int = 1) -> tuple[np.ndarray, np.ndarray]:
    """Return the one-sided frequency spectrum of the envelope.

    Args:
        resolution (int, optional): Sample period in nanoseconds.

    Returns:
        ``(frequencies_hz, complex_spectrum)`` from `numpy.fft.rfft`.

    Raises:
        UnassignedVariableError: If the envelope depends on a variable that has no value.
    """
    env = self.envelope(resolution=resolution)
    freqs = np.fft.rfftfreq(len(env), d=resolution * 1e-9)
    spectrum = np.fft.rfft(env)
    return freqs, spectrum

plot

plot(resolution: int = 1, ax: Axes | None = None) -> Axes

Plot the envelope on a matplotlib Axes.

Requires matplotlib, which ships in the viz extra; it is imported inside the call so the rest of the package stays importable without it.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

  • ax (Axes | None, default: None ) –

    Axes to draw on. A fresh figure+axes is created when None.

Returns:

  • Axes

    The Axes containing the plot.

Raises:

  • ModuleNotFoundError

    When matplotlib is not installed — install qprogram[viz].

  • UnassignedVariableError

    If the envelope depends on a variable that has no value.

Source code in src/qprogram/waveforms/waveform.py
def plot(self, resolution: int = 1, ax: Axes | None = None) -> Axes:
    """Plot the envelope on a matplotlib `Axes`.

    Requires ``matplotlib``, which ships in the ``viz`` extra; it is imported inside the call so the
    rest of the package stays importable without it.

    Args:
        resolution (int, optional): Sample period in nanoseconds.
        ax (Axes | None): Axes to draw on. A fresh figure+axes is created when ``None``.

    Returns:
        The `Axes` containing the plot.

    Raises:
        ModuleNotFoundError: When ``matplotlib`` is not installed — install ``qprogram[viz]``.
        UnassignedVariableError: If the envelope depends on a variable that has no value.
    """
    import matplotlib.pyplot as plt  # ruff: ignore[import-outside-top-level]

    if ax is None:
        _, ax = plt.subplots(figsize=(6, 2))
    env = self.envelope(resolution=resolution)
    t = np.arange(len(env)) * resolution
    ax.plot(t, env)
    ax.set_xlabel("Time (ns)")
    ax.set_ylabel("Amplitude")
    ax.set_title(type(self).__name__)
    return ax

IQWaveform

Bases: _StructuralEqMixin, ABC

Abstract base for IQ (two-channel, complex-valued) waveforms.

A concrete shape supplies get_I, get_Q and get_duration; the analysis and plotting helpers here work on the complex envelope I + jQ. Equality and hashing are structural over the constructor attributes, recursing into the component waveforms.

get_I abstractmethod

get_I() -> Waveform

Return the in-phase component as a single-channel Waveform.

Returns:

  • Waveform

    The waveform played on the I channel.

Source code in src/qprogram/waveforms/waveform.py
@abstractmethod
def get_I(self) -> Waveform:
    """Return the in-phase component as a single-channel [`Waveform`][qprogram.waveforms.Waveform].

    Returns:
        The waveform played on the I channel.
    """
    ...

get_Q abstractmethod

get_Q() -> Waveform

Return the quadrature component as a single-channel Waveform.

Returns:

  • Waveform

    The waveform played on the Q channel.

Source code in src/qprogram/waveforms/waveform.py
@abstractmethod
def get_Q(self) -> Waveform:
    """Return the quadrature component as a single-channel [`Waveform`][qprogram.waveforms.Waveform].

    Returns:
        The waveform played on the Q channel.
    """
    ...

get_duration abstractmethod

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration in nanoseconds.

Source code in src/qprogram/waveforms/waveform.py
@abstractmethod
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration in nanoseconds.
    """
    ...

peak_amplitude

peak_amplitude(resolution: int = 1) -> float

Return the peak magnitude max(|I + jQ|) at the given sample resolution.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

Returns:

  • float

    The largest magnitude of the complex envelope.

Raises:

Source code in src/qprogram/waveforms/waveform.py
def peak_amplitude(self, resolution: int = 1) -> float:
    """Return the peak magnitude ``max(|I + jQ|)`` at the given sample resolution.

    Args:
        resolution (int, optional): Sample period in nanoseconds.

    Returns:
        The largest magnitude of the complex envelope.

    Raises:
        UnassignedVariableError: If either channel's envelope depends on a variable that has no
            value.
    """
    return float(np.max(np.abs(self._complex_envelope(resolution=resolution))))

rms_amplitude

rms_amplitude(resolution: int = 1) -> float

Return the RMS magnitude of the complex envelope at the given resolution.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

Returns:

  • float

    The root mean square of the complex envelope magnitudes.

Raises:

Source code in src/qprogram/waveforms/waveform.py
def rms_amplitude(self, resolution: int = 1) -> float:
    """Return the RMS magnitude of the complex envelope at the given resolution.

    Args:
        resolution (int, optional): Sample period in nanoseconds.

    Returns:
        The root mean square of the complex envelope magnitudes.

    Raises:
        UnassignedVariableError: If either channel's envelope depends on a variable that has no
            value.
    """
    env = self._complex_envelope(resolution=resolution)
    return float(np.sqrt(np.mean(np.abs(env) ** 2)))

area

area(resolution: int = 1) -> float

Return the integrated magnitude ∫ |I + jQ| dt in nanosecond-amplitude units.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

Returns:

  • float

    The trapezoidal integral of the complex envelope magnitude over the pulse.

Raises:

Source code in src/qprogram/waveforms/waveform.py
def area(self, resolution: int = 1) -> float:
    """Return the integrated magnitude ``∫ |I + jQ| dt`` in nanosecond-amplitude units.

    Args:
        resolution (int, optional): Sample period in nanoseconds.

    Returns:
        The trapezoidal integral of the complex envelope magnitude over the pulse.

    Raises:
        UnassignedVariableError: If either channel's envelope depends on a variable that has no
            value.
    """
    env = self._complex_envelope(resolution=resolution)
    return float(np.trapezoid(np.abs(env), dx=resolution))

spectrum

spectrum(
    resolution: int = 1,
) -> tuple[np.ndarray, np.ndarray]

Return the two-sided frequency spectrum of the complex envelope.

IQ waveforms are complex, so a two-sided numpy.fft.fft is more informative than a real-only one-sided transform.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

Returns:

  • tuple[ndarray, ndarray]

    (frequencies_hz, complex_spectrum), both shifted so zero frequency sits in the middle.

Raises:

Source code in src/qprogram/waveforms/waveform.py
def spectrum(self, resolution: int = 1) -> tuple[np.ndarray, np.ndarray]:
    """Return the two-sided frequency spectrum of the complex envelope.

    IQ waveforms are complex, so a two-sided `numpy.fft.fft` is more informative than a
    real-only one-sided transform.

    Args:
        resolution (int, optional): Sample period in nanoseconds.

    Returns:
        ``(frequencies_hz, complex_spectrum)``, both shifted so zero frequency sits in the middle.

    Raises:
        UnassignedVariableError: If either channel's envelope depends on a variable that has no
            value.
    """
    env = self._complex_envelope(resolution=resolution)
    freqs = np.fft.fftshift(np.fft.fftfreq(len(env), d=resolution * 1e-9))
    spectrum = np.fft.fftshift(np.fft.fft(env))
    return freqs, spectrum

plot

plot(
    resolution: int = 1,
    axes: tuple[Axes, Axes] | None = None,
) -> tuple[Axes, Axes]

Plot the I and Q channels on two stacked matplotlib axes.

Requires matplotlib, which ships in the viz extra; it is imported inside the call so the rest of the package stays importable without it.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds.

  • axes (tuple[Axes, Axes] | None, default: None ) –

    Pair of axes to draw on. A fresh figure with two stacked axes is created when None.

Returns:

  • tuple[Axes, Axes]

    The (I_axes, Q_axes) pair used for the plot.

Raises:

  • ModuleNotFoundError

    When matplotlib is not installed — install qprogram[viz].

  • UnassignedVariableError

    If either channel's envelope depends on a variable that has no value.

Source code in src/qprogram/waveforms/waveform.py
def plot(
    self,
    resolution: int = 1,
    axes: tuple[Axes, Axes] | None = None,
) -> tuple[Axes, Axes]:
    """Plot the I and Q channels on two stacked matplotlib axes.

    Requires ``matplotlib``, which ships in the ``viz`` extra; it is imported inside the call so the
    rest of the package stays importable without it.

    Args:
        resolution (int, optional): Sample period in nanoseconds.
        axes (tuple[Axes, Axes] | None): Pair of axes to draw on. A fresh figure with two stacked axes
            is created when ``None``.

    Returns:
        The ``(I_axes, Q_axes)`` pair used for the plot.

    Raises:
        ModuleNotFoundError: When ``matplotlib`` is not installed — install ``qprogram[viz]``.
        UnassignedVariableError: If either channel's envelope depends on a variable that has no
            value.
    """
    import matplotlib.pyplot as plt  # ruff: ignore[import-outside-top-level]

    if axes is None:
        _, ax_pair = plt.subplots(2, 1, sharex=True, figsize=(6, 3))
        axes = (ax_pair[0], ax_pair[1])
    i_env = self.get_I().envelope(resolution=resolution)
    q_env = self.get_Q().envelope(resolution=resolution)
    t = np.arange(len(i_env)) * resolution
    axes[0].plot(t, i_env)
    axes[0].set_ylabel("I")
    axes[0].set_title(type(self).__name__)
    axes[1].plot(t, q_env)
    axes[1].set_ylabel("Q")
    axes[1].set_xlabel("Time (ns)")
    return axes

Square

Square(
    amplitude: float | Expression,
    duration: int | Expression,
)

Bases: Waveform

Constant-amplitude rectangular pulse.

Parameters:

Source code in src/qprogram/waveforms/square.py
def __init__(self, amplitude: float | Expression, duration: int | Expression) -> None:
    self.amplitude = amplitude
    self.duration = duration

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the rectangular envelope sampled at resolution-ns steps.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per ns.

Returns:

  • ndarray

    A 1-D array of length duration / resolution, every sample at amplitude. The dtype

  • ndarray

    follows amplitude, so an integer amplitude yields an integer array.

Raises:

Source code in src/qprogram/waveforms/square.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the rectangular envelope sampled at ``resolution``-ns steps.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per ns.

    Returns:
        A 1-D array of length ``duration / resolution``, every sample at ``amplitude``. The dtype
        follows ``amplitude``, so an integer amplitude yields an integer array.

    Raises:
        UnassignedVariableError: If a parameter is an [`Expression`][qprogram.Expression] whose variables
            have no value.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return np.full(int(duration / resolution), amplitude)

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/square.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is an [`Expression`][qprogram.Expression] whose variables
            have no value.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

Gaussian

Gaussian(
    amplitude: float | Expression,
    duration: int | Expression,
    sigma: float | Expression,
)

Bases: Waveform

Gaussian-shaped pulse, peaked at the midpoint of the duration window.

The envelope is not truncation-corrected: the tails are clipped wherever the window ends, so the first and last samples sit at whatever amplitude the Gaussian reaches there rather than at zero.

Parameters:

  • amplitude (float | Expression) –

    Peak amplitude. Accepts an Expression.

  • duration (int | Expression) –

    Pulse duration in nanoseconds. Accepts an Expression.

  • sigma (float | Expression) –

    Standard deviation in nanoseconds. The truncation ratio duration / sigma controls how steeply the tails are clipped at the window edges. Accepts an Expression.

Source code in src/qprogram/waveforms/gaussian.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    sigma: float | Expression,
) -> None:
    self.amplitude = amplitude
    self.duration = duration
    self.sigma = sigma

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the Gaussian envelope sampled at resolution-ns steps.

The peak sits at the center of the sample window, so an even sample count straddles it between the two middle samples and the largest sample falls a little below amplitude. sigma is converted to samples, which keeps the shape the same at any resolution.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per nanosecond.

Returns:

  • ndarray

    A 1-D float array of duration / resolution samples.

Raises:

  • UnassignedVariableError

    If amplitude, duration, or sigma is a symbolic expression whose variables are unassigned.

Source code in src/qprogram/waveforms/gaussian.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the Gaussian envelope sampled at ``resolution``-ns steps.

    The peak sits at the center of the sample window, so an even sample count straddles it between
    the two middle samples and the largest sample falls a little below ``amplitude``. ``sigma`` is
    converted to samples, which keeps the shape the same at any resolution.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per nanosecond.

    Returns:
        A 1-D float array of ``duration / resolution`` samples.

    Raises:
        UnassignedVariableError: If ``amplitude``, ``duration``, or ``sigma`` is a symbolic expression
            whose variables are unassigned.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    sigma = self.sigma.evaluate_or_raise() if isinstance(self.sigma, Expression) else self.sigma
    n_samples = int(duration / resolution)
    sigma_samples = sigma / resolution
    center = (n_samples - 1) / 2
    t = np.arange(n_samples)
    return amplitude * np.exp(-0.5 * ((t - center) / sigma_samples) ** 2)

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration, truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/gaussian.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration, truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is a symbolic expression whose variables are
            unassigned.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

GaussianDragCorrection

GaussianDragCorrection(
    amplitude: float | Expression,
    duration: int | Expression,
    sigma: float | Expression,
    beta: float | Expression,
)

Bases: Gaussian

Derivative-of-Gaussian envelope used as the Q-channel of a DRAG pulse.

On its own, this waveform is rarely emitted directly; it is the Q-channel partner produced by get_Q.

Only the envelope differs from Gaussian: the duration, and the meaning of amplitude, duration, and sigma, are inherited unchanged, so the correction always spans the same window as the Gaussian it partners.

Parameters:

  • amplitude (float | Expression) –

    Peak amplitude of the underlying Gaussian. Accepts an Expression.

  • duration (int | Expression) –

    Pulse duration in nanoseconds. Accepts an Expression.

  • sigma (float | Expression) –

    Standard deviation of the underlying Gaussian in nanoseconds.

  • beta (float | Expression) –

    DRAG scaling (β in the Motzoi et al. parameterization). Multiplicative weight on the derivative term.

Source code in src/qprogram/waveforms/gaussian_drag_correction.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    sigma: float | Expression,
    beta: float | Expression,
) -> None:
    super().__init__(amplitude=amplitude, duration=duration, sigma=sigma)
    self.beta = beta

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the scaled Gaussian derivative sampled at resolution-ns steps.

The derivative is taken with respect to sample index rather than time, so the correction's amplitude scales with resolution while the underlying Gaussian's does not. It is antisymmetric about the pulse center, where it crosses zero.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per nanosecond.

Returns:

  • ndarray

    A 1-D float array of duration / resolution samples.

Raises:

  • UnassignedVariableError

    If amplitude, duration, sigma, or beta is a symbolic expression whose variables are unassigned.

Source code in src/qprogram/waveforms/gaussian_drag_correction.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the scaled Gaussian derivative sampled at ``resolution``-ns steps.

    The derivative is taken with respect to sample index rather than time, so the correction's
    amplitude scales with ``resolution`` while the underlying Gaussian's does not. It is antisymmetric
    about the pulse center, where it crosses zero.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per nanosecond.

    Returns:
        A 1-D float array of ``duration / resolution`` samples.

    Raises:
        UnassignedVariableError: If ``amplitude``, ``duration``, ``sigma``, or ``beta`` is a symbolic
            expression whose variables are unassigned.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    sigma = self.sigma.evaluate_or_raise() if isinstance(self.sigma, Expression) else self.sigma
    beta = self.beta.evaluate_or_raise() if isinstance(self.beta, Expression) else self.beta
    n_samples = int(duration / resolution)
    sigma_samples = sigma / resolution
    center = (n_samples - 1) / 2
    t = np.arange(n_samples)
    gaussian = amplitude * np.exp(-0.5 * ((t - center) / sigma_samples) ** 2)
    return beta * -(t - center) / (sigma_samples**2) * gaussian

Ramp

Ramp(
    from_amplitude: float | Expression,
    to_amplitude: float | Expression,
    duration: int | Expression,
)

Bases: Waveform

Linearly-interpolated ramp between two amplitudes.

Parameters:

Source code in src/qprogram/waveforms/ramp.py
def __init__(
    self,
    from_amplitude: float | Expression,
    to_amplitude: float | Expression,
    duration: int | Expression,
) -> None:
    self.from_amplitude = from_amplitude
    self.to_amplitude = to_amplitude
    self.duration = duration

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the ramp sampled at resolution-ns steps.

Samples run linearly from from_amplitude to to_amplitude, so the step between samples is set by the sample count rather than by resolution alone. Both endpoints are included once the window holds at least two samples: a one-sample window yields from_amplitude alone, and a window that holds less than one sample yields an empty array.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per nanosecond.

Returns:

  • ndarray

    A 1-D float array of duration / resolution samples running linearly from

  • ndarray

    from_amplitude to to_amplitude.

Raises:

  • UnassignedVariableError

    If either amplitude or the duration is a symbolic expression whose variables are still unassigned.

  • ValueError

    If the duration is negative, which asks for a negative number of samples.

Source code in src/qprogram/waveforms/ramp.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the ramp sampled at ``resolution``-ns steps.

    Samples run linearly from ``from_amplitude`` to ``to_amplitude``, so the step between samples is
    set by the sample count rather than by ``resolution`` alone. Both endpoints are included once the
    window holds at least two samples: a one-sample window yields ``from_amplitude`` alone, and a
    window that holds less than one sample yields an empty array.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per nanosecond.

    Returns:
        A 1-D float array of ``duration / resolution`` samples running linearly from
        ``from_amplitude`` to ``to_amplitude``.

    Raises:
        UnassignedVariableError: If either amplitude or the duration is a symbolic expression whose
            variables are still unassigned.
        ValueError: If the duration is negative, which asks for a negative number of samples.
    """
    from_amplitude = (
        self.from_amplitude.evaluate_or_raise()
        if isinstance(self.from_amplitude, Expression)
        else self.from_amplitude
    )
    to_amplitude = (
        self.to_amplitude.evaluate_or_raise() if isinstance(self.to_amplitude, Expression) else self.to_amplitude
    )
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    n_samples = int(duration / resolution)
    return np.linspace(from_amplitude, to_amplitude, n_samples)

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The ramp duration, truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/ramp.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The ramp duration, truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is a symbolic expression whose variables are still
            unassigned.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

FlatTop

FlatTop(
    amplitude: float | Expression,
    duration: int | Expression,
    smooth_duration: int | Expression,
    buffer: int = 0,
)

Bases: Waveform

Rectangular pulse with erf-smoothed rising and falling edges.

Useful for fast-acting flat pulses where instantaneous square edges would inject high-frequency content outside the control electronics' bandwidth.

Each edge is an error function of width smooth_duration / 3: the rise crosses half amplitude smooth_duration ns into the pulse and is flat to within a part in 10⁵ by twice that, with the fall mirrored about the pulse center. A duration that is not comfortably longer than 2 * smooth_duration therefore never reaches full amplitude.

Parameters:

  • amplitude (float | Expression) –

    Pulse amplitude. Accepts an Expression.

  • duration (int | Expression) –

    Pulse duration in nanoseconds, including the smoothed edges but excluding the buffer padding. Accepts an Expression.

  • smooth_duration (int | Expression) –

    Length of each edge (rise and fall) in nanoseconds. Accepts an Expression.

  • buffer (int, default: 0 ) –

    Zero-amplitude padding added on each side of the pulse, in nanoseconds. The total duration is duration + 2 * buffer.

Source code in src/qprogram/waveforms/flat_top.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    smooth_duration: int | Expression,
    buffer: int = 0,
) -> None:
    self.amplitude = amplitude
    self.duration = duration
    self.smooth_duration = smooth_duration
    self.buffer = buffer

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the smoothed rectangular envelope sampled at resolution-ns steps.

The rising and falling error functions are multiplied together rather than spliced, which keeps the envelope smooth even when the two edges overlap. Padding is emitted as buffer / resolution zero samples on each side.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per nanosecond.

Returns:

  • ndarray

    A 1-D float array of (duration + 2 * buffer) / resolution samples.

Raises:

  • UnassignedVariableError

    If amplitude, duration, or smooth_duration is a symbolic expression whose variables are unassigned.

Source code in src/qprogram/waveforms/flat_top.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the smoothed rectangular envelope sampled at ``resolution``-ns steps.

    The rising and falling error functions are multiplied together rather than spliced, which keeps the
    envelope smooth even when the two edges overlap. Padding is emitted as ``buffer / resolution`` zero
    samples on each side.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per nanosecond.

    Returns:
        A 1-D float array of ``(duration + 2 * buffer) / resolution`` samples.

    Raises:
        UnassignedVariableError: If ``amplitude``, ``duration``, or ``smooth_duration`` is a symbolic
            expression whose variables are unassigned.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    smooth_duration = (
        self.smooth_duration.evaluate_or_raise()
        if isinstance(self.smooth_duration, Expression)
        else self.smooth_duration
    )
    n_samples = int(duration / resolution)
    smooth = int(smooth_duration / resolution)
    t = np.arange(n_samples)
    rise = 0.5 * (1 + np.vectorize(erf)((t - smooth) / (smooth / 3)))
    fall = 0.5 * (1 + np.vectorize(erf)((n_samples - 1 - smooth - t) / (smooth / 3)))
    pulse = amplitude * rise * fall
    if self.buffer:
        pad = np.zeros(int(self.buffer / resolution))
        return np.concatenate([pad, pulse, pad])
    return pulse

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds, padding included.

Returns:

  • int

    duration + 2 * buffer, with duration truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/flat_top.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds, padding included.

    Returns:
        ``duration + 2 * buffer``, with ``duration`` truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is a symbolic expression whose variables are
            unassigned.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration) + 2 * self.buffer

SuddenNetZero

SuddenNetZero(
    amplitude: float | Expression,
    duration: int | Expression,
    b: float | Expression,
    t_phi: int | Expression,
)

Bases: Waveform

Sudden Net Zero (SNZ) pulse for fast, leakage-suppressed two-qubit gates.

The shape is a positive square segment, a zero hold of width t_phi, then a negative square segment scaled by b. The two segments are meant to cancel, leaving zero net integrated flux: the cancellation is exact when b is 1 and the samples left over after the hold divide evenly between the segments, and b is detuned from 1 to null whatever residual the flux line adds.

Parameters:

  • amplitude (float | Expression) –

    Amplitude of the positive segment. Accepts an Expression.

  • duration (int | Expression) –

    Total pulse duration in nanoseconds. Accepts an Expression.

  • b (float | Expression) –

    Ratio of negative-to-positive amplitudes (typically near 1.0). Accepts an Expression.

  • t_phi (int | Expression) –

    Width of the zero hold between the two segments in nanoseconds. Accepts an Expression.

Source code in src/qprogram/waveforms/snz.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    b: float | Expression,
    t_phi: int | Expression,
) -> None:
    self.amplitude = amplitude
    self.duration = duration
    self.b = b
    self.t_phi = t_phi

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the SNZ envelope sampled at resolution-ns steps.

The two square segments split what is left of the pulse once the zero hold is taken out, rounding the positive segment down — so the negative segment carries the extra sample when that remainder is an odd number of samples.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per ns.

Returns:

  • ndarray

    A 1-D float array of length duration / resolution.

Raises:

Source code in src/qprogram/waveforms/snz.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the SNZ envelope sampled at ``resolution``-ns steps.

    The two square segments split what is left of the pulse once the zero hold is taken out, rounding
    the positive segment down — so the negative segment carries the extra sample when that remainder is
    an odd number of samples.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per ns.

    Returns:
        A 1-D float array of length ``duration / resolution``.

    Raises:
        UnassignedVariableError: If a parameter is an [`Expression`][qprogram.Expression] whose variables
            have no value.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    b = self.b.evaluate_or_raise() if isinstance(self.b, Expression) else self.b
    t_phi = self.t_phi.evaluate_or_raise() if isinstance(self.t_phi, Expression) else self.t_phi
    n_samples = int(duration / resolution)
    t_phi_samples = int(t_phi / resolution)
    half = (n_samples - t_phi_samples) // 2

    envelope = np.zeros(n_samples)
    envelope[:half] = amplitude
    envelope[half : half + t_phi_samples] = 0.0
    envelope[half + t_phi_samples :] = -amplitude * b
    return envelope

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/snz.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is an [`Expression`][qprogram.Expression] whose variables
            have no value.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

Sine

Sine(
    amplitude: float | Expression,
    duration: int | Expression,
    frequency: float | Expression,
    phase: float | Expression = 0.0,
)

Bases: Waveform

Sinusoidal envelope amplitude · sin(2π·frequency·t + phase).

Useful for parametric drives, sideband cooling tones, and as a building block for amplitude-modulated waveforms. The envelope does not taper to zero at the endpoints; pair with a window function (e.g. Tukey) when continuous-wave artifacts matter.

Parameters:

  • amplitude (float | Expression) –

    Peak amplitude.

  • duration (int | Expression) –

    Pulse duration in nanoseconds.

  • frequency (float | Expression) –

    Oscillation frequency in Hz.

  • phase (float | Expression, default: 0.0 ) –

    Phase offset in radians. Defaults to zero.

Source code in src/qprogram/waveforms/sine.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    frequency: float | Expression,
    phase: float | Expression = 0.0,
) -> None:
    self.amplitude = amplitude
    self.duration = duration
    self.frequency = frequency
    self.phase = phase

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the sine envelope sampled at resolution-ns steps.

The sample times are expressed in seconds, so frequency is read as a frequency in Hz.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per ns.

Returns:

  • ndarray

    A 1-D float array of length duration / resolution.

Raises:

Source code in src/qprogram/waveforms/sine.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the sine envelope sampled at ``resolution``-ns steps.

    The sample times are expressed in seconds, so ``frequency`` is read as a frequency in Hz.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per ns.

    Returns:
        A 1-D float array of length ``duration / resolution``.

    Raises:
        UnassignedVariableError: If a parameter is an [`Expression`][qprogram.Expression] whose variables
            have no value.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    frequency = self.frequency.evaluate_or_raise() if isinstance(self.frequency, Expression) else self.frequency
    phase = self.phase.evaluate_or_raise() if isinstance(self.phase, Expression) else self.phase
    n_samples = int(duration / resolution)
    t = np.arange(n_samples) * resolution * 1e-9
    return amplitude * np.sin(2 * np.pi * frequency * t + phase)

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/sine.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is an [`Expression`][qprogram.Expression] whose variables
            have no value.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

Cosine

Cosine(
    amplitude: float | Expression,
    duration: int | Expression,
    frequency: float | Expression,
    phase: float | Expression = 0.0,
)

Bases: Waveform

Cosine envelope amplitude · cos(2π·frequency·t + phase).

See Sine for the analogous sine variant and the same caveats.

Parameters:

  • amplitude (float | Expression) –

    Peak amplitude.

  • duration (int | Expression) –

    Pulse duration in nanoseconds.

  • frequency (float | Expression) –

    Oscillation frequency in Hz.

  • phase (float | Expression, default: 0.0 ) –

    Phase offset in radians. Defaults to zero.

Source code in src/qprogram/waveforms/cosine.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    frequency: float | Expression,
    phase: float | Expression = 0.0,
) -> None:
    self.amplitude = amplitude
    self.duration = duration
    self.frequency = frequency
    self.phase = phase

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the cosine envelope sampled at resolution-ns steps.

Every symbolic parameter is resolved to a number first, so an enclosing sweep must have bound its variables before samples can be rendered. Sample times run from zero, in seconds, which is what pairs with frequency in Hz.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per nanosecond.

Returns:

  • ndarray

    A 1-D float array of duration / resolution samples.

Raises:

  • UnassignedVariableError

    If amplitude, duration, frequency, or phase is a symbolic expression whose variables are unassigned.

Source code in src/qprogram/waveforms/cosine.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the cosine envelope sampled at ``resolution``-ns steps.

    Every symbolic parameter is resolved to a number first, so an enclosing sweep must have bound its
    variables before samples can be rendered. Sample times run from zero, in seconds, which is what
    pairs with ``frequency`` in Hz.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per nanosecond.

    Returns:
        A 1-D float array of ``duration / resolution`` samples.

    Raises:
        UnassignedVariableError: If ``amplitude``, ``duration``, ``frequency``, or ``phase`` is a
            symbolic expression whose variables are unassigned.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    frequency = self.frequency.evaluate_or_raise() if isinstance(self.frequency, Expression) else self.frequency
    phase = self.phase.evaluate_or_raise() if isinstance(self.phase, Expression) else self.phase
    n_samples = int(duration / resolution)
    t = np.arange(n_samples) * resolution * 1e-9
    return amplitude * np.cos(2 * np.pi * frequency * t + phase)

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration, truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/cosine.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration, truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is a symbolic expression whose variables are
            unassigned.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

Sech

Sech(
    amplitude: float | Expression,
    duration: int | Expression,
    tau: float | Expression,
)

Bases: Waveform

Hyperbolic-secant envelope, centered at the midpoint of the duration window.

The envelope is amplitude * sech((t - center) / tau). sech pulses are the canonical envelope for chirped adiabatic-passage gates: paired with a quadratic phase ramp they yield analytically-solvable population transfer.

Parameters:

  • amplitude (float | Expression) –

    Peak amplitude (at the midpoint).

  • duration (int | Expression) –

    Pulse duration in nanoseconds.

  • tau (float | Expression) –

    Width parameter in nanoseconds — analogous to sigma on Gaussian.

Source code in src/qprogram/waveforms/sech.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    tau: float | Expression,
) -> None:
    self.amplitude = amplitude
    self.duration = duration
    self.tau = tau

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the pulse envelope sampled at resolution-ns steps.

The peak sits at the center of the sample window, and tau is scaled into samples so the shape is independent of resolution. A window that holds less than one sample yields an empty array.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per nanosecond.

Returns:

  • ndarray

    A 1-D float array of duration / resolution samples.

Raises:

  • UnassignedVariableError

    If amplitude, duration, or tau is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/sech.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the pulse envelope sampled at ``resolution``-ns steps.

    The peak sits at the center of the sample window, and ``tau`` is scaled into samples so the shape
    is independent of ``resolution``. A window that holds less than one sample yields an empty array.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per nanosecond.

    Returns:
        A 1-D float array of ``duration / resolution`` samples.

    Raises:
        UnassignedVariableError: If ``amplitude``, ``duration``, or ``tau`` is a symbolic expression
            whose variables are still unassigned.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    tau = self.tau.evaluate_or_raise() if isinstance(self.tau, Expression) else self.tau
    n_samples = int(duration / resolution)
    if n_samples == 0:
        return np.zeros(0)
    tau_samples = tau / resolution
    center = (n_samples - 1) / 2
    t = np.arange(n_samples)
    return amplitude / np.cosh((t - center) / tau_samples)

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The pulse duration, truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/sech.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The pulse duration, truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is a symbolic expression whose variables are still
            unassigned.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

Tukey

Tukey(
    amplitude: float | Expression,
    duration: int | Expression,
    alpha: float | Expression = 0.5,
)

Bases: Waveform

Tukey window: rectangular pulse with cosine-tapered edges.

The alpha parameter controls the fraction of the pulse occupied by the rising and falling edges combined: alpha=0 produces a pure rectangle, alpha=1 produces a Hann window, and intermediate values yield a flat top of width (1 - alpha) * duration with cosine ramps on each side of width (alpha / 2) * duration. Matches the alpha parameter of scipy.signal.windows.tukey.

Cheaper to compile than FlatTop (no erf evaluation) and commonly used as a smoothing window in pulse calibration.

Parameters:

  • amplitude (float | Expression) –

    Peak amplitude (achieved across the flat region).

  • duration (int | Expression) –

    Total pulse duration in nanoseconds.

  • alpha (float | Expression, default: 0.5 ) –

    Fraction of the duration occupied by the combined rise + fall, in [0, 1]. Defaults to 0.5.

Source code in src/qprogram/waveforms/tukey.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    alpha: float | Expression = 0.5,
) -> None:
    self.amplitude = amplitude
    self.duration = duration
    self.alpha = alpha

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the Tukey-windowed envelope sampled at resolution-ns steps.

Three cases skip the taper arithmetic: a window of fewer than two samples and alpha <= 0 are flat at amplitude, and alpha >= 1 is a full Hann window. Otherwise the taper spans alpha * (n - 1) / 2 samples at each end, where n is the sample count.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds. 1 returns one sample per ns.

Returns:

  • ndarray

    A 1-D array of length duration / resolution. The tapered forms are float; the two flat

  • ndarray

    forms take their dtype from amplitude, so an integer amplitude yields an integer array.

Raises:

Source code in src/qprogram/waveforms/tukey.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the Tukey-windowed envelope sampled at ``resolution``-ns steps.

    Three cases skip the taper arithmetic: a window of fewer than two samples and ``alpha <= 0`` are
    flat at ``amplitude``, and ``alpha >= 1`` is a full Hann window. Otherwise the taper spans
    ``alpha * (n - 1) / 2`` samples at each end, where ``n`` is the sample count.

    Args:
        resolution (int, optional): Sample period in nanoseconds. ``1`` returns one sample per ns.

    Returns:
        A 1-D array of length ``duration / resolution``. The tapered forms are float; the two flat
        forms take their dtype from ``amplitude``, so an integer amplitude yields an integer array.

    Raises:
        UnassignedVariableError: If a parameter is an [`Expression`][qprogram.Expression] whose variables
            have no value.
    """
    amplitude = self.amplitude.evaluate_or_raise() if isinstance(self.amplitude, Expression) else self.amplitude
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    alpha = self.alpha.evaluate_or_raise() if isinstance(self.alpha, Expression) else self.alpha

    n = int(duration / resolution)
    if n <= 1:
        return np.full(max(n, 0), amplitude)
    if alpha <= 0:
        return np.full(n, amplitude)
    if alpha >= 1:
        # The taper spans the whole pulse, leaving no flat top: a Hann window.
        t = np.arange(n)
        return amplitude * 0.5 * (1 - np.cos(2 * np.pi * t / (n - 1)))

    t = np.arange(n)
    half_alpha = alpha * (n - 1) / 2
    out = np.full(n, float(amplitude))
    rising = t < half_alpha
    falling = t > (n - 1) - half_alpha
    out[rising] = amplitude * 0.5 * (1 + np.cos(np.pi * (t[rising] / half_alpha - 1)))
    out[falling] = amplitude * 0.5 * (1 + np.cos(np.pi * ((t[falling] - (n - 1)) / half_alpha + 1)))
    return out

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration truncated to a whole number of nanoseconds.

Raises:

Source code in src/qprogram/waveforms/tukey.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration truncated to a whole number of nanoseconds.

    Raises:
        UnassignedVariableError: If ``duration`` is an [`Expression`][qprogram.Expression] whose variables
            have no value.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

Arbitrary

Arbitrary(samples: Sequence[float] | ndarray)

Bases: Waveform

Waveform defined by a user-provided 1-D sample array.

The resolution argument to envelope is ignored — the samples are taken as-is, one per ns.

An array that is already an numpy.ndarray is adopted rather than copied, so a caller holding a reference to it must not mutate it: waveforms are values, compared and hashed structurally. envelope hands back a copy for the same reason.

Parameters:

  • samples (Sequence[float] | ndarray) –

    1-D sequence (list, tuple, or np.ndarray) of sample values. Converted with numpy.asarray, so the stored dtype follows the input.

Source code in src/qprogram/waveforms/arbitrary.py
def __init__(self, samples: Sequence[float] | np.ndarray) -> None:
    self.samples = np.asarray(samples)

envelope

envelope(resolution: int = 1) -> np.ndarray

Return a copy of the stored samples.

Parameters:

  • resolution (int, default: 1 ) –

    Ignored. Accepted so the signature matches Waveform.envelope; the stored samples are the envelope already, one per nanosecond.

Returns:

  • ndarray

    A copy of the sample array, so writing to it cannot alter the waveform.

Source code in src/qprogram/waveforms/arbitrary.py
def envelope(self, resolution: int = 1) -> np.ndarray:  # ruff: ignore[unused-method-argument]
    """Return a copy of the stored samples.

    Args:
        resolution (int, optional): Ignored. Accepted so the signature matches `Waveform.envelope`; the
            stored samples are the envelope already, one per nanosecond.

    Returns:
        A copy of the sample array, so writing to it cannot alter the waveform.
    """
    return self.samples.copy()

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The number of samples, one sample spanning one nanosecond.

Source code in src/qprogram/waveforms/arbitrary.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The number of samples, one sample spanning one nanosecond.
    """
    return len(self.samples)

Chained

Chained(waveforms: list[Waveform])

Bases: Waveform

Single-channel waveform built by concatenating other single-channel waveforms in time.

Waveform.__add__ builds these and flattens as it goes, so a + b + c yields one three-element chain rather than nested pairs. Every child is sampled at whatever resolution the chain is asked for, and the chain's duration is the sum of the children's.

Parameters:

  • waveforms (list[Waveform]) –

    Waveforms to play back-to-back, in order.

Source code in src/qprogram/waveforms/chained.py
def __init__(self, waveforms: list[Waveform]) -> None:
    self.waveforms = waveforms

envelope

envelope(resolution: int = 1) -> np.ndarray

Return the children's envelopes concatenated in order.

Parameters:

  • resolution (int, default: 1 ) –

    Sample period in nanoseconds, passed through to every child.

Returns:

  • ndarray

    A 1-D array holding each child's envelope in turn.

Raises:

  • ValueError

    If the chain holds no waveforms, leaving nothing to concatenate.

  • UnassignedVariableError

    If a child's envelope depends on a variable that has no value.

Source code in src/qprogram/waveforms/chained.py
def envelope(self, resolution: int = 1) -> np.ndarray:
    """Return the children's envelopes concatenated in order.

    Args:
        resolution (int, optional): Sample period in nanoseconds, passed through to every child.

    Returns:
        A 1-D array holding each child's envelope in turn.

    Raises:
        ValueError: If the chain holds no waveforms, leaving nothing to concatenate.
        UnassignedVariableError: If a child's envelope depends on a variable that has no value.
    """
    return np.concatenate([w.envelope(resolution) for w in self.waveforms])

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The sum of the children's durations, or zero for an empty chain.

Raises:

Source code in src/qprogram/waveforms/chained.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The sum of the children's durations, or zero for an empty chain.

    Raises:
        UnassignedVariableError: If a child's duration is a symbolic expression whose variables are
            unassigned.
    """
    return sum(w.get_duration() for w in self.waveforms)

IQPair

IQPair(I: Waveform, Q: Waveform)

Bases: IQWaveform

An IQ waveform assembled from two independent single-channel envelopes.

Useful when the desired I and Q envelopes don't fit one of the named DRAG-style classes — for example, a square-on-I, zero-on-Q readout pulse.

Parameters:

  • I (Waveform) –

    In-phase channel.

  • Q (Waveform) –

    Quadrature channel. Must have the same duration as I.

Raises:

  • TypeError

    If either argument is not a Waveform instance.

  • ValidationError

    If the two channels have different (concretely-known) durations. The check is best-effort — symbolic durations whose variables are still unassigned are accepted here and left to the platform compiler to verify once values are bound.

Source code in src/qprogram/waveforms/iq_pair.py
def __init__(self, I: Waveform, Q: Waveform) -> None:
    if not isinstance(I, Waveform) or not isinstance(Q, Waveform):
        msg = "I and Q must be Waveform instances"
        raise TypeError(msg)
    try:
        i_duration, q_duration = I.get_duration(), Q.get_duration()
    except UnassignedVariableError:
        # symbolic durations — defer the check
        i_duration = q_duration = None
    if i_duration != q_duration:
        msg = f"IQPair channels must have equal durations; got I={i_duration} ns, Q={q_duration} ns"
        raise ValidationError(msg)
    self.I = I
    self.Q = Q

get_I

get_I() -> Waveform

Return the in-phase channel.

Returns:

  • Waveform

    The waveform given as I, unchanged.

Source code in src/qprogram/waveforms/iq_pair.py
def get_I(self) -> Waveform:
    """Return the in-phase channel.

    Returns:
        The waveform given as ``I``, unchanged.
    """
    return self.I

get_Q

get_Q() -> Waveform

Return the quadrature channel.

Returns:

  • Waveform

    The waveform given as Q, unchanged.

Source code in src/qprogram/waveforms/iq_pair.py
def get_Q(self) -> Waveform:
    """Return the quadrature channel.

    Returns:
        The waveform given as ``Q``, unchanged.
    """
    return self.Q

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Both channels are required to share a duration, so the I channel answers for the pair.

Returns:

  • int

    The duration of the I channel in nanoseconds.

Raises:

  • UnassignedVariableError

    If the I channel's duration is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/iq_pair.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Both channels are required to share a duration, so the I channel answers for the pair.

    Returns:
        The duration of the I channel in nanoseconds.

    Raises:
        UnassignedVariableError: If the I channel's duration is a symbolic expression whose
            variables are still unassigned.
    """
    return self.I.get_duration()

IQDrag

IQDrag(
    amplitude: float | Expression,
    duration: int | Expression,
    sigma: float | Expression,
    beta: float | Expression,
)

Bases: IQWaveform

DRAG (Derivative Removal by Adiabatic Gate) pulse.

The I channel carries a Gaussian; the Q channel carries the derivative correction (GaussianDragCorrection) scaled by beta. Suppresses leakage to the |2⟩ state during single-qubit rotations on weakly-anharmonic transmons.

The two channels are built on demand from the four stored parameters, so they cannot drift out of step with each other or with get_duration.

Parameters:

  • amplitude (float | Expression) –

    Peak amplitude of the I-channel Gaussian. Accepts an Expression.

  • duration (int | Expression) –

    Pulse duration in nanoseconds. Accepts an Expression.

  • sigma (float | Expression) –

    Standard deviation of the I-channel Gaussian in nanoseconds.

  • beta (float | Expression) –

    DRAG scaling (β in the Motzoi et al. parameterization). Typically a small (< 0.5) value tuned per qubit.

Source code in src/qprogram/waveforms/iq_drag.py
def __init__(
    self,
    amplitude: float | Expression,
    duration: int | Expression,
    sigma: float | Expression,
    beta: float | Expression,
) -> None:
    self.amplitude = amplitude
    self.duration = duration
    self.sigma = sigma
    self.beta = beta

get_I

get_I() -> Waveform

Return the in-phase component as a single-channel Waveform.

Returns:

Source code in src/qprogram/waveforms/iq_drag.py
def get_I(self) -> Waveform:
    """Return the in-phase component as a single-channel [`Waveform`][qprogram.waveforms.Waveform].

    Returns:
        A fresh [`Gaussian`][qprogram.waveforms.Gaussian] carrying this pulse's amplitude, duration, and
        sigma.
    """
    return Gaussian(amplitude=self.amplitude, duration=self.duration, sigma=self.sigma)

get_Q

get_Q() -> Waveform

Return the quadrature component as a single-channel Waveform.

Returns:

Source code in src/qprogram/waveforms/iq_drag.py
def get_Q(self) -> Waveform:
    """Return the quadrature component as a single-channel [`Waveform`][qprogram.waveforms.Waveform].

    Returns:
        A fresh [`GaussianDragCorrection`][qprogram.waveforms.GaussianDragCorrection] carrying this pulse's parameters
        together with ``beta``.
    """
    return GaussianDragCorrection(
        amplitude=self.amplitude,
        duration=self.duration,
        sigma=self.sigma,
        beta=self.beta,
    )

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration, truncated to a whole number of nanoseconds. Both channels span it.

Raises:

Source code in src/qprogram/waveforms/iq_drag.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration, truncated to a whole number of nanoseconds. Both channels span it.

    Raises:
        UnassignedVariableError: If ``duration`` is a symbolic expression whose variables are
            unassigned.
    """
    duration = self.duration.evaluate_or_raise() if isinstance(self.duration, Expression) else self.duration
    return int(duration)

IQRotation

IQRotation(base: IQWaveform, phase: float | Expression)

Bases: IQWaveform

An existing IQWaveform rotated in the I/Q plane by phase radians.

Applies the 2x2 rotation::

I_out = I * cos(phase) - Q * sin(phase)
Q_out = I * sin(phase) + Q * cos(phase)

Useful for virtual-Z gates and for applying a software-side phase offset to a calibrated pulse without resampling the envelope. Materializes both channels as Arbitrary waveforms; for purely-symbolic rotation, prefer carrying the phase through the underlying envelope's parameters.

Parameters:

Raises:

  • TypeError

    If base is not an IQWaveform instance.

Source code in src/qprogram/waveforms/iq_rotation.py
def __init__(self, base: IQWaveform, phase: float | Expression) -> None:
    if not isinstance(base, IQWaveform):
        msg = f"IQRotation base must be an IQWaveform, got {type(base).__name__}"
        raise TypeError(msg)
    self.base = base
    self.phase = phase

get_I

get_I() -> Waveform

Return the rotated in-phase channel.

Returns:

  • Waveform

    An Arbitrary waveform holding I·cos(phase) - Q·sin(phase), sampled from the base

  • Waveform

    channels at 1-ns steps.

Raises:

  • UnassignedVariableError

    If phase or any parameter of the base channels is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/iq_rotation.py
def get_I(self) -> Waveform:
    """Return the rotated in-phase channel.

    Returns:
        An [`Arbitrary`][qprogram.waveforms.Arbitrary] waveform holding ``I·cos(phase) - Q·sin(phase)``, sampled from the base
        channels at 1-ns steps.

    Raises:
        UnassignedVariableError: If ``phase`` or any parameter of the base channels is a symbolic
            expression whose variables are still unassigned.
    """
    phase = self._resolved_phase()
    i_env = self.base.get_I().envelope()
    q_env = self.base.get_Q().envelope()
    return Arbitrary(i_env * np.cos(phase) - q_env * np.sin(phase))

get_Q

get_Q() -> Waveform

Return the rotated quadrature channel.

Returns:

  • Waveform

    An Arbitrary waveform holding I·sin(phase) + Q·cos(phase), sampled from the base

  • Waveform

    channels at 1-ns steps.

Raises:

  • UnassignedVariableError

    If phase or any parameter of the base channels is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/iq_rotation.py
def get_Q(self) -> Waveform:
    """Return the rotated quadrature channel.

    Returns:
        An [`Arbitrary`][qprogram.waveforms.Arbitrary] waveform holding ``I·sin(phase) + Q·cos(phase)``, sampled from the base
        channels at 1-ns steps.

    Raises:
        UnassignedVariableError: If ``phase`` or any parameter of the base channels is a symbolic
            expression whose variables are still unassigned.
    """
    phase = self._resolved_phase()
    i_env = self.base.get_I().envelope()
    q_env = self.base.get_Q().envelope()
    return Arbitrary(i_env * np.sin(phase) + q_env * np.cos(phase))

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

A rotation mixes the two channels sample by sample, so the length is the base waveform's.

Returns:

  • int

    The duration of the base waveform in nanoseconds.

Raises:

  • UnassignedVariableError

    If the base waveform's duration is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/iq_rotation.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    A rotation mixes the two channels sample by sample, so the length is the base waveform's.

    Returns:
        The duration of the base waveform in nanoseconds.

    Raises:
        UnassignedVariableError: If the base waveform's duration is a symbolic expression whose
            variables are still unassigned.
    """
    return self.base.get_duration()

IQZero

IQZero(envelope: Waveform)

Bases: IQWaveform

A real-valued Waveform presented on an IQ bus with a silent Q channel.

Convenience wrapper for IQPair(I=envelope, Q=Square(0.0, duration)). Useful when a calibrated single-channel pulse has to drive an IQ-typed bus without rewriting the rest of the program.

Parameters:

Raises:

  • TypeError

    If envelope is not a Waveform instance.

Source code in src/qprogram/waveforms/iq_zero.py
def __init__(self, envelope: Waveform) -> None:
    if not isinstance(envelope, Waveform):
        msg = f"IQZero envelope must be a Waveform, got {type(envelope).__name__}"
        raise TypeError(msg)
    self.envelope = envelope

get_I

get_I() -> Waveform

Return the in-phase channel.

Returns:

  • Waveform

    The wrapped envelope, unchanged.

Source code in src/qprogram/waveforms/iq_zero.py
def get_I(self) -> Waveform:
    """Return the in-phase channel.

    Returns:
        The wrapped envelope, unchanged.
    """
    return self.envelope

get_Q

get_Q() -> Waveform

Return the silent quadrature channel.

Returns:

  • Waveform

    An Arbitrary waveform of zeros, one sample per nanosecond of the envelope's duration.

Raises:

  • UnassignedVariableError

    If the envelope's duration is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/iq_zero.py
def get_Q(self) -> Waveform:
    """Return the silent quadrature channel.

    Returns:
        An [`Arbitrary`][qprogram.waveforms.Arbitrary] waveform of zeros, one sample per nanosecond of the envelope's duration.

    Raises:
        UnassignedVariableError: If the envelope's duration is a symbolic expression whose variables
            are still unassigned.
    """
    return Arbitrary(np.zeros(self.envelope.get_duration()))

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Returns:

  • int

    The duration of the wrapped envelope in nanoseconds.

Raises:

  • UnassignedVariableError

    If the envelope's duration is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/iq_zero.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Returns:
        The duration of the wrapped envelope in nanoseconds.

    Raises:
        UnassignedVariableError: If the envelope's duration is a symbolic expression whose variables
            are still unassigned.
    """
    return self.envelope.get_duration()

Modulated

Modulated(
    envelope: Waveform,
    frequency: float | Expression,
    phase: float | Expression = 0.0,
)

Bases: IQWaveform

IQ pulse formed by intermediate-frequency modulation of a real envelope.

Produces I = envelope · cos(2π·frequency·t + phase) and Q = envelope · sin(2π·frequency·t + phase), evaluated at 1-ns resolution. Use this to lift any single-channel envelope onto an IQ bus for sideband-modulated drive without writing an IQPair by hand.

The materialized I and Q channels are Arbitrary waveforms — modulation collapses the enclosing envelope's parametric structure to concrete samples.

Parameters:

  • envelope (Waveform) –

    Underlying single-channel Waveform shaping the pulse.

  • frequency (float | Expression) –

    Modulation frequency in Hz.

  • phase (float | Expression, default: 0.0 ) –

    Phase offset in radians. Defaults to zero.

Raises:

  • TypeError

    If envelope is not a Waveform instance.

Source code in src/qprogram/waveforms/modulated.py
def __init__(
    self,
    envelope: Waveform,
    frequency: float | Expression,
    phase: float | Expression = 0.0,
) -> None:
    if not isinstance(envelope, Waveform):
        msg = f"Modulated envelope must be a Waveform, got {type(envelope).__name__}"
        raise TypeError(msg)
    self.envelope = envelope
    self.frequency = frequency
    self.phase = phase

get_I

get_I() -> Waveform

Return the in-phase channel.

Returns:

  • Waveform

    An Arbitrary waveform holding envelope · cos(2π·frequency·t + phase), with t

  • Waveform

    running over the envelope's samples in seconds at 1-ns steps.

Raises:

  • UnassignedVariableError

    If frequency, phase, or any envelope parameter is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/modulated.py
def get_I(self) -> Waveform:
    """Return the in-phase channel.

    Returns:
        An [`Arbitrary`][qprogram.waveforms.Arbitrary] waveform holding ``envelope · cos(2π·frequency·t + phase)``, with ``t``
        running over the envelope's samples in seconds at 1-ns steps.

    Raises:
        UnassignedVariableError: If ``frequency``, ``phase``, or any envelope parameter is a symbolic
            expression whose variables are still unassigned.
    """
    env = self.envelope.envelope()
    frequency, phase = self._resolved_params()
    t = np.arange(len(env)) * 1e-9
    return Arbitrary(env * np.cos(2 * np.pi * frequency * t + phase))

get_Q

get_Q() -> Waveform

Return the quadrature channel.

Returns:

  • Waveform

    An Arbitrary waveform holding envelope · sin(2π·frequency·t + phase), with t

  • Waveform

    running over the envelope's samples in seconds at 1-ns steps.

Raises:

  • UnassignedVariableError

    If frequency, phase, or any envelope parameter is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/modulated.py
def get_Q(self) -> Waveform:
    """Return the quadrature channel.

    Returns:
        An [`Arbitrary`][qprogram.waveforms.Arbitrary] waveform holding ``envelope · sin(2π·frequency·t + phase)``, with ``t``
        running over the envelope's samples in seconds at 1-ns steps.

    Raises:
        UnassignedVariableError: If ``frequency``, ``phase``, or any envelope parameter is a symbolic
            expression whose variables are still unassigned.
    """
    env = self.envelope.envelope()
    frequency, phase = self._resolved_params()
    t = np.arange(len(env)) * 1e-9
    return Arbitrary(env * np.sin(2 * np.pi * frequency * t + phase))

get_duration

get_duration() -> int

Return the pulse duration in nanoseconds.

Modulation is sample-wise, so the length is the underlying envelope's.

Returns:

  • int

    The duration of the underlying envelope in nanoseconds.

Raises:

  • UnassignedVariableError

    If the envelope's duration is a symbolic expression whose variables are still unassigned.

Source code in src/qprogram/waveforms/modulated.py
def get_duration(self) -> int:
    """Return the pulse duration in nanoseconds.

    Modulation is sample-wise, so the length is the underlying envelope's.

    Returns:
        The duration of the underlying envelope in nanoseconds.

    Raises:
        UnassignedVariableError: If the envelope's duration is a symbolic expression whose variables
            are still unassigned.
    """
    return self.envelope.get_duration()

WaveformLibrary

WaveformLibrary()

A per-bus store mapping waveform names to concrete waveforms.

Entries are keyed at one of three tiers; get tries them most-specific first:

  1. exactset(name, wf, element="q", idx=0, kind="drive") — only q[0].drive.
  2. familyset(name, wf, element="q", kind="drive") — any q[*].drive (idx unspecified).
  3. globalset(name, wf) — any bus. This tier is the only one a raw-string bus can reach, and a bare dict passed to QProgram.with_waveforms lands here.

More specific entries shadow less specific ones for a given bus.

Source code in src/qprogram/waveform_library.py
def __init__(self) -> None:
    self._entries: dict[_LibraryKey, Waveform | IQWaveform] = {}

from_mapping classmethod

from_mapping(
    mapping: Mapping[str, Waveform | IQWaveform],
) -> WaveformLibrary

Build a global-tier-only library from a plain {name: waveform} mapping.

This is what makes a bare dict usable with QProgram.with_waveforms: every name in the mapping resolves on every bus, whatever its (element, idx, kind) coordinate.

Parameters:

  • mapping (Mapping[str, Waveform | IQWaveform]) –

    Waveform names mapped to concrete waveforms.

Returns:

  • WaveformLibrary

    A library holding one global-tier entry per mapping key.

Raises:

Source code in src/qprogram/waveform_library.py
@classmethod
def from_mapping(cls, mapping: Mapping[str, Waveform | IQWaveform]) -> WaveformLibrary:
    """Build a global-tier-only library from a plain ``{name: waveform}`` mapping.

    This is what makes a bare ``dict`` usable with [`QProgram.with_waveforms`][qprogram.QProgram.with_waveforms]:
    every name in the mapping resolves on every bus, whatever its ``(element, idx, kind)`` coordinate.

    Args:
        mapping (Mapping[str, Waveform | IQWaveform]): Waveform names mapped to concrete
            waveforms.

    Returns:
        A library holding one global-tier entry per mapping key.

    Raises:
        ValidationError: If a key is not a non-empty string.
    """
    library = cls()
    for name, waveform in mapping.items():
        library.set(name, waveform)
    return library

set

set(
    name: str,
    waveform: Waveform | IQWaveform,
    *,
    element: str | None = None,
    idx: int | tuple[int, ...] | None = None,
    kind: str | None = None,
) -> None

Register waveform under name at the tier implied by the keyword arguments.

Parameters:

  • name (str) –

    The waveform name as referenced in the program (the string alias).

  • waveform (Waveform | IQWaveform) –

    The concrete waveform to resolve to.

  • element (str | None, default: None ) –

    Element kind (e.g. "q"). Required for the exact and family tiers.

  • idx (int | tuple[int, ...] | None, default: None ) –

    Element index — a tuple for a multi-index element such as a coupler. Given (with element and kind) → exact tier.

  • kind (str | None, default: None ) –

    Bus kind (e.g. "drive"). Required for the exact and family tiers.

Raises:

  • ValidationError

    If name is not a non-empty string, or the element/idx/kind combination does not match one of the three tiers (exact = all three; family = element + kind; global = none).

Source code in src/qprogram/waveform_library.py
def set(
    self,
    name: str,
    waveform: Waveform | IQWaveform,
    *,
    element: str | None = None,
    idx: int | tuple[int, ...] | None = None,
    kind: str | None = None,
) -> None:
    """Register ``waveform`` under ``name`` at the tier implied by the keyword arguments.

    Args:
        name (str): The waveform name as referenced in the program (the string alias).
        waveform (Waveform | IQWaveform): The concrete waveform to resolve to.
        element (str | None): Element kind (e.g. ``"q"``). Required for the exact and family
            tiers.
        idx (int | tuple[int, ...] | None): Element index — a tuple for a multi-index element
            such as a coupler. Given (with ``element`` and ``kind``) → exact tier.
        kind (str | None): Bus kind (e.g. ``"drive"``). Required for the exact and family tiers.

    Raises:
        ValidationError: If ``name`` is not a non-empty string, or the ``element``/``idx``/``kind``
            combination does not match one of the three tiers (exact = all three; family =
            element + kind; global = none).
    """
    if not isinstance(name, str) or not name:
        msg = f"waveform name must be a non-empty string, got {name!r}"
        raise ValidationError(msg)
    if element is not None and kind is not None and idx is not None:
        key: _LibraryKey = (element, idx, kind, name)
    elif element is not None and kind is not None and idx is None:
        key = (element, None, kind, name)
    elif element is None and idx is None and kind is None:
        key = (None, None, None, name)
    else:
        msg = (
            "WaveformLibrary.set: specify (element, idx, kind) for an exact entry, "
            "(element, kind) for a family default, or none of them for a global entry; "
            f"got element={element!r}, idx={idx!r}, kind={kind!r}"
        )
        raise ValidationError(msg)
    self._entries[key] = waveform

get

get(bus: str, name: str) -> Waveform | IQWaveform | None

Resolve name for bus, trying exact → family → global; None if no entry matches.

A schema-backed BusRef can match all three tiers; a raw-string bus carries no (element, idx, kind) metadata, so only the global tier is reachable.

Parameters:

  • bus (str) –

    The bus the name is played on — a BusRef to reach the exact and family tiers, any string for the global tier alone.

  • name (str) –

    The waveform name to resolve.

Returns:

Source code in src/qprogram/waveform_library.py
def get(self, bus: str, name: str) -> Waveform | IQWaveform | None:
    """Resolve ``name`` for ``bus``, trying exact → family → global; ``None`` if no entry matches.

    A schema-backed [`BusRef`][qprogram.BusRef] can match all three tiers; a raw-string bus carries no
    ``(element, idx, kind)`` metadata, so only the global tier is reachable.

    Args:
        bus (str): The bus the name is played on — a [`BusRef`][qprogram.BusRef] to reach the
            exact and family tiers, any string for the global tier alone.
        name (str): The waveform name to resolve.

    Returns:
        The most specific waveform registered for ``(bus, name)``, or ``None`` when no tier
        matches.
    """
    if isinstance(bus, BusRef) and bus.element and bus.kind:
        for key in (
            (bus.element, bus.idx, bus.kind, name),
            (bus.element, None, bus.kind, name),
            (None, None, None, name),
        ):
            if key in self._entries:
                return self._entries[key]
        return None
    return self._entries.get((None, None, None, name))

apply

apply(program: QProgram) -> QProgram

Return a copy of program with string waveform names resolved against this library.

Platform-free convenience identical to program.with_waveforms(self) — useful for tooling and tests that resolve without going through a platform.

Parameters:

  • program (QProgram) –

    The program whose string waveform aliases are resolved. Never mutated.

Returns:

  • QProgram

    A copy of program with each resolvable alias replaced by its concrete waveform.

Raises:

  • ValidationError

    If a resolved waveform does not match the channel kind of the bus it lands on.

Source code in src/qprogram/waveform_library.py
def apply(self, program: QProgram) -> QProgram:
    """Return a copy of ``program`` with string waveform names resolved against this library.

    Platform-free convenience identical to ``program.with_waveforms(self)`` — useful for tooling and
    tests that resolve without going through a platform.

    Args:
        program (QProgram): The program whose string waveform aliases are resolved. Never
            mutated.

    Returns:
        A copy of ``program`` with each resolvable alias replaced by its concrete waveform.

    Raises:
        ValidationError: If a resolved waveform does not match the channel kind of the bus it
            lands on.
    """
    return program.with_waveforms(self)

dumps

dumps() -> str

Serialize the library to the portable .wfl text format.

Each entry is one line — "<name>" [<coord>] = <waveform> — where <coord> is element[idx].kind (exact tier), element[*].kind (family tier), or absent (global tier), and <waveform> reuses the same constructor syntax as .qp (e.g. IQDrag(amplitude=0.5, duration=40, sigma=8, beta=0.1)). Entries are emitted in insertion order, so loads(dumps(lib)) reproduces the library exactly.

Returns:

  • str

    The complete .wfl document, header line included, ending in a newline.

Raises:

  • SerializationError

    If a stored waveform is not concrete (e.g. carries a Variable) — a calibration library must hold uploadable pulses, not symbolic ones.

Source code in src/qprogram/waveform_library.py
def dumps(self) -> str:
    """Serialize the library to the portable ``.wfl`` text format.

    Each entry is one line — ``"<name>" [<coord>] = <waveform>`` — where ``<coord>`` is
    ``element[idx].kind`` (exact tier), ``element[*].kind`` (family tier), or absent (global tier),
    and ``<waveform>`` reuses the same constructor syntax as ``.qp`` (e.g.
    ``IQDrag(amplitude=0.5, duration=40, sigma=8, beta=0.1)``). Entries are emitted in insertion
    order, so ``loads(dumps(lib))`` reproduces the library exactly.

    Returns:
        The complete ``.wfl`` document, header line included, ending in a newline.

    Raises:
        SerializationError: If a stored waveform is not concrete (e.g. carries a ``Variable``) — a
            calibration library must hold uploadable pulses, not symbolic ones.
    """
    from qprogram.errors import SerializationError  # ruff: ignore[import-outside-top-level]
    from qprogram.qprogram import QProgram  # ruff: ignore[import-outside-top-level]
    from qprogram.serialization.writer import _escape_str, _Writer  # ruff: ignore[import-outside-top-level]

    writer = _Writer(QProgram())
    lines = [f"#!WaveformLibrary {WAVEFORM_LIBRARY_FORMAT_VERSION}"]
    for (element, idx, kind, name), waveform in self._entries.items():
        try:
            wf_str = writer.serialize_waveform(waveform)
        except (KeyError, SerializationError) as e:
            msg = (
                f"cannot serialize the waveform stored under name {name!r}: a WaveformLibrary must "
                f"hold concrete waveforms (no Variables / symbolic parameters). Underlying error: {e}"
            )
            raise SerializationError(msg) from e
        coord = _format_coord(element, idx, kind)
        prefix = f'"{_escape_str(name)}"'
        lines.append(f"{prefix} {coord} = {wf_str}" if coord else f"{prefix} = {wf_str}")
    return "\n".join(lines) + "\n"

save

save(path: str) -> None

Write the library to path as UTF-8 text.

The .wfl extension is the convention.

Parameters:

  • path (str) –

    Filesystem path to write. An existing file is overwritten.

Raises:

  • SerializationError

    If a stored waveform is not concrete (see dumps).

  • OSError

    If the path cannot be written.

Source code in src/qprogram/waveform_library.py
def save(self, path: str) -> None:
    """Write the library to ``path`` as UTF-8 text.

    The ``.wfl`` extension is the convention.

    Args:
        path (str): Filesystem path to write. An existing file is overwritten.

    Raises:
        SerializationError: If a stored waveform is not concrete (see `dumps`).
        OSError: If the path cannot be written.
    """
    Path(path).write_text(self.dumps(), encoding="utf-8")

loads classmethod

loads(text: str) -> WaveformLibrary

Parse a .wfl text document into a WaveformLibrary.

The #!WaveformLibrary header must come first, preceded by blank lines at most — a comment ahead of it is an error. After the header, blank lines and # comment lines are skipped and every other line must be an entry.

Parameters:

  • text (str) –

    The .wfl document to parse.

Returns:

Raises:

  • ParseError

    On a missing or incompatible header, a malformed entry, or an unknown waveform type. Waveform types are looked up in the global serialization registry, so every built-in is always available while a vendor waveform needs its package imported first.

  • ValidationError

    If an entry names an empty waveform name.

Source code in src/qprogram/waveform_library.py
@classmethod
def loads(cls, text: str) -> WaveformLibrary:
    """Parse a ``.wfl`` text document into a [`WaveformLibrary`][qprogram.WaveformLibrary].

    The ``#!WaveformLibrary`` header must come first, preceded by blank lines at most — a
    comment ahead of it is an error. After the header, blank lines and ``#`` comment lines are
    skipped and every other line must be an entry.

    Args:
        text (str): The ``.wfl`` document to parse.

    Returns:
        The reconstructed library, entries in file order.

    Raises:
        ParseError: On a missing or incompatible header, a malformed entry, or an unknown
            waveform type. Waveform types are looked up in the global serialization registry, so
            every built-in is always available while a vendor waveform needs its package
            imported first.
        ValidationError: If an entry names an empty waveform name.
    """
    from qprogram.serialization.parser import (  # ruff: ignore[import-outside-top-level]
        ParseError,
        _parse_waveform_expr,
        _split_lines,
        _tokenize,
        _unescape_str,
    )

    library = cls()
    lines = _split_lines(text)
    pos = 0
    while pos < len(lines) and not lines[pos].strip():
        pos += 1
    if pos >= len(lines) or not lines[pos].strip().startswith("#!WaveformLibrary"):
        msg = "Missing #!WaveformLibrary header"
        raise ParseError(msg, pos + 1)
    header = lines[pos].split()
    version = header[-1] if len(header) > 1 else "unknown"
    if version.split(".", maxsplit=1)[0] != WAVEFORM_LIBRARY_FORMAT_VERSION.split(".", maxsplit=1)[0]:
        msg = f"Unsupported WaveformLibrary format version {version}"
        raise ParseError(msg, pos + 1)
    pos += 1

    for offset, raw in enumerate(lines[pos:], start=pos):
        stripped = raw.strip()
        if not stripped or stripped.startswith("#"):
            continue
        line_num = offset + 1
        tokens = _tokenize(stripped)
        if "=" not in tokens:
            msg = f"WaveformLibrary entry must contain '=': {stripped!r}"
            raise ParseError(msg, line_num)
        eq = tokens.index("=")
        left, right = tokens[:eq], tokens[eq + 1 :]
        if len(right) != 1:
            msg = "expected exactly one waveform after '='"
            raise ParseError(msg, line_num)
        if not left or not (left[0].startswith('"') and left[0].endswith('"') and len(left[0]) >= 2):
            msg = "entry must start with a quoted waveform name"
            raise ParseError(msg, line_num)
        name = _unescape_str(left[0][1:-1])
        if len(left) == 1:
            element, idx, kind = None, None, None
        elif len(left) == 2:
            element, idx, kind = _parse_coord(left[1], line_num)
        else:
            msg = f"unexpected tokens before '=': {left[1:-1]!r}"
            raise ParseError(msg, line_num)
        try:
            waveform = cast("Waveform | IQWaveform", _parse_waveform_expr(right[0]))
        except (ParseError, ValueError) as e:
            msg = f"invalid waveform: {e}"
            raise ParseError(msg, line_num) from e
        library.set(name, waveform, element=element, idx=idx, kind=kind)
    return library

load classmethod

load(path: str) -> WaveformLibrary

Read and parse a .wfl file encoded as UTF-8.

Parameters:

  • path (str) –

    Filesystem path of the document to read.

Returns:

Raises:

  • ParseError

    If the file's contents are not a valid .wfl document (see loads).

  • OSError

    If the path cannot be read.

Source code in src/qprogram/waveform_library.py
@classmethod
def load(cls, path: str) -> WaveformLibrary:
    """Read and parse a ``.wfl`` file encoded as UTF-8.

    Args:
        path (str): Filesystem path of the document to read.

    Returns:
        The reconstructed library.

    Raises:
        ParseError: If the file's contents are not a valid ``.wfl`` document (see
            `loads`).
        OSError: If the path cannot be read.
    """
    return cls.loads(Path(path).read_text(encoding="utf-8"))

Operations

The AST leaves, each appended by the matching builder method on QProgram rather than constructed at a call site. MeasurementField and normalize_fields are documented here because they live in the same module: the first is a StrEnum of the field names a measurement can request, and the second sorts a fields argument into the canonical order those names are compared, hashed, and serialized in.

operations

Built-in operations — the AST leaf nodes a QProgram is composed of.

Operations are typed nodes appended to the program's active block by the builder methods on QProgram. Each subclass declares which constructor params hold buses (BUS_ATTRS) and waveforms (WAVEFORM_ATTRS) so the shared introspection contract works without per-class overrides.

Operation

Base class for all operations in the QProgram AST.

Subclasses customize introspection behavior through two class-attribute conventions:

  • BUS_ATTRS lists which __init__ parameter names hold bus references. The default ("bus",) matches every core op except Sync (which holds a list under targets) and Call (which lists none — buses reach a call site only as bound argument values).
  • WAVEFORM_ATTRS lists which __init__ parameters carry waveform values. Default empty.

Equality and hashing are structural; once an instance has been used as a set / dict key, do not mutate its attributes. Callers like QProgram.rebind that rewrite operations do so on a fresh deepcopy.

BROADCASTS_WHEN_NO_BUS class-attribute

BROADCASTS_WHEN_NO_BUS: bool = False

When True and the op's BUS_ATTRS resolve to no buses, the op semantically touches every bus in the program (e.g. Sync(targets=None)). The validator then routes it across all program buses instead of the default-bus profile.

AFFECTS_AVERAGING class-attribute

AFFECTS_AVERAGING: bool = False

Whether this op participates in what an Average block averages.

An average block repeats its body and accumulates measurement results; only the ops that produce those results determine whether the averaging itself can run as a real-time hardware feature. Ops with AFFECTS_AVERAGING = False are still validated and still execute inside the body — they just don't gate the Average block's execution domain (see qprogram.validation.validate). Defaults to False; MeasurementOperation sets it True, so core measure and vendor acquire opt in automatically. A vendor adding another averaging-relevant op sets it on its own class.

variables

variables() -> set[Variable]

Return every Variable referenced by this op, transitively.

Walks every public instance attribute, descending into Expression nodes, waveform parameters, and nested lists. Ops with data hidden in private fields or computed lazily can override this method.

Source code in src/qprogram/operations/operation.py
def variables(self) -> set[Variable]:
    """Return every [`Variable`][qprogram.Variable] referenced by this op, transitively.

    Walks every public instance attribute, descending into [`Expression`][qprogram.Expression] nodes, waveform
    parameters, and nested lists. Ops with data hidden in private fields or computed lazily can
    override this method.
    """
    out: set[Variable] = set()
    for name, value in vars(self).items():
        if name.startswith("_"):
            continue
        out |= _collect_variables(value)
    return out

buses

buses() -> set[str]

Return every bus name this op touches.

Reads the attributes listed in BUS_ATTRS. Plain strings, BusRef instances, and lists of either are collected.

Source code in src/qprogram/operations/operation.py
def buses(self) -> set[str]:
    """Return every bus name this op touches.

    Reads the attributes listed in `BUS_ATTRS`. Plain strings, [`BusRef`][qprogram.BusRef]
    instances, and lists of either are collected.
    """
    out: set[str] = set()
    for attr_name in self.BUS_ATTRS:
        value = getattr(self, attr_name, None)
        if isinstance(value, str):
            out.add(value)
        elif isinstance(value, list):
            out.update(v for v in value if isinstance(v, str))
    return out

waveforms

waveforms() -> set[Waveform | IQWaveform | str]

Return every waveform (concrete or string alias) the op carries.

Reads the attributes listed in WAVEFORM_ATTRS. None values (from optional waveform params) are skipped.

Source code in src/qprogram/operations/operation.py
def waveforms(self) -> set[Waveform | IQWaveform | str]:
    """Return every waveform (concrete or string alias) the op carries.

    Reads the attributes listed in `WAVEFORM_ATTRS`. ``None`` values (from optional waveform
    params) are skipped.
    """
    out: set[Waveform | IQWaveform | str] = set()
    for attr_name in self.WAVEFORM_ATTRS:
        value = getattr(self, attr_name, None)
        if value is not None:
            out.add(value)
    return out

walk

walk() -> Iterator[Operation | Block]

Yield self — operations are AST leaves.

Pairs with Block.walk, which recurses through children.

Source code in src/qprogram/operations/operation.py
def walk(self) -> Iterator[Operation | Block]:
    """Yield ``self`` — operations are AST leaves.

    Pairs with `Block.walk`, which recurses through children.
    """
    yield self

required_capabilities

required_capabilities() -> set[str]

Return the capability tokens this op needs, in isolation.

Non-recursive: the validator walks the AST and unions per-node sets. Subclasses override to add their identity token (op.<name>) plus any state-dependent refinement tokens (waveform kind, expression shape, measure.fields.<name> for the fields a measurement requests, ...).

Source code in src/qprogram/operations/operation.py
def required_capabilities(self) -> set[str]:
    """Return the capability tokens this op needs, in isolation.

    Non-recursive: the validator walks the AST and unions per-node sets. Subclasses override to add
    their identity token (``op.<name>``) plus any state-dependent refinement tokens (waveform kind,
    expression shape, ``measure.fields.<name>`` for the fields a measurement requests, ...).
    """
    return set()

Play

Play(bus: str, waveform: Waveform | IQWaveform | str)

Bases: Operation

A waveform played on a bus.

Parameters:

Source code in src/qprogram/operations/play.py
def __init__(self, bus: str, waveform: Waveform | IQWaveform | str) -> None:
    self.bus = bus
    self.waveform = waveform

required_capabilities

required_capabilities() -> set[str]

Return op.play plus the tokens describing the waveform.

A string alias contributes waveform.alias; a concrete waveform contributes its channel kind (waveform.iq or waveform.single) and, when its class is registered, the per-class token from qprogram.protocol.waveform_token.

Source code in src/qprogram/operations/play.py
def required_capabilities(self) -> set[str]:
    """Return ``op.play`` plus the tokens describing the waveform.

    A string alias contributes ``waveform.alias``; a concrete waveform contributes its channel
    kind (``waveform.iq`` or ``waveform.single``) and, when its class is registered, the
    per-class token from [`qprogram.protocol.waveform_token`][].
    """
    from qprogram.protocol import waveform_token  # ruff: ignore[import-outside-top-level]

    caps = {"op.play"}
    if isinstance(self.waveform, str):
        caps.add("waveform.alias")
    else:
        caps.add("waveform.iq" if isinstance(self.waveform, IQWaveform) else "waveform.single")
        tok = waveform_token(self.waveform)
        if tok is not None:
            caps.add(tok)
    return caps

Measure

Measure(
    bus: str,
    waveform: IQWaveform | str,
    weights: IQWaveform | str,
    handle: MeasurementHandle,
    fields: Iterable[MeasurementField] = (
        MeasurementField.IQ,
    ),
)

Bases: MeasurementOperation

A readout pulse played on a bus, together with the acquisition of the response.

Parameters:

  • bus (str) –

    Readout bus (must have acquires=True).

  • waveform (IQWaveform | str) –

    Readout pulse — concrete IQWaveform or a string alias.

  • weights (IQWaveform | str) –

    Integration weights — concrete IQWaveform or a string alias.

  • handle (MeasurementHandle) –

    The canonical MeasurementHandle for this measurement. The same Python instance is returned to user code, referenced by any MeasurementRef in conditionals, and listed by QProgram.measurement_handles — the runtime writes per-measurement values onto this single object and every reader sees them.

  • fields (Iterable[MeasurementField], default: (IQ,) ) –

    Which measurement fields to produce, as an iterable of MeasurementField members. Default (MeasurementField.IQ,). RAW requests the raw ADC trace; STATE requests a classified outcome (required when the program references handle.state in a conditional). Stored canonically ordered and deduplicated — see normalize_fields.

Raises:

  • ValidationError

    If fields is a bare string, is not iterable, requests no field at all, or names a field that is not registered.

Source code in src/qprogram/operations/measure.py
def __init__(
    self,
    bus: str,
    waveform: IQWaveform | str,
    weights: IQWaveform | str,
    handle: MeasurementHandle,
    fields: Iterable[MeasurementField] = (MeasurementField.IQ,),
) -> None:
    self.bus = bus
    self.waveform = waveform
    self.weights = weights
    self.handle = handle
    self.fields: tuple[str, ...] = normalize_fields(fields)

required_capabilities

required_capabilities() -> set[str]

Return op.measure plus the waveform and requested-field tokens.

waveform.iq is always required — a readout drives an IQ bus. The pulse and the integration weights each contribute waveform.alias when given as a string alias, and a concrete one contributes the per-class token from qprogram.protocol.waveform_token when its class is registered. The measure.fields.<name> tokens come from required_capabilities.

Source code in src/qprogram/operations/measure.py
def required_capabilities(self) -> set[str]:
    """Return ``op.measure`` plus the waveform and requested-field tokens.

    ``waveform.iq`` is always required — a readout drives an IQ bus. The pulse and the
    integration weights each contribute ``waveform.alias`` when given as a string alias, and a
    concrete one contributes the per-class token from [`qprogram.protocol.waveform_token`][]
    when its class is registered. The ``measure.fields.<name>`` tokens come from
    [`required_capabilities`][qprogram.operations.operation.MeasurementOperation.required_capabilities].
    """
    from qprogram.protocol import waveform_token  # ruff: ignore[import-outside-top-level]

    caps = super().required_capabilities() | {"op.measure", "waveform.iq"}
    for attr in (self.waveform, self.weights):
        if isinstance(attr, str):
            caps.add("waveform.alias")
        else:
            tok = waveform_token(attr)
            if tok is not None:
                caps.add(tok)
    return caps

Wait

Wait(bus: str, duration: int | Expression)

Bases: Operation

An idle period of duration nanoseconds on bus.

Parameters:

  • bus (str) –

    Bus to idle on.

  • duration (int | Expression) –

    Wait duration in nanoseconds. Accepts an Expression for sweeps.

Source code in src/qprogram/operations/wait.py
def __init__(self, bus: str, duration: int | Expression) -> None:
    self.bus = bus
    self.duration = duration

required_capabilities

required_capabilities() -> set[str]

Return op.wait plus the tokens contributed by the duration expression.

Source code in src/qprogram/operations/wait.py
def required_capabilities(self) -> set[str]:
    """Return ``op.wait`` plus the tokens contributed by the ``duration`` expression."""
    from qprogram.protocol import expression_tokens  # ruff: ignore[import-outside-top-level]

    return {"op.wait"} | expression_tokens(self.duration)

Sync

Sync(targets: list[str] | None = None)

Bases: Operation

An alignment of buses to a common time reference.

The user-facing QProgram.sync exposes a buses= keyword; the AST attribute is named targets to avoid shadowing Operation.buses (a list attribute named buses would silently hide the introspection method).

Parameters:

  • targets (list[str] | None, default: None ) –

    Bus names to sync, or None to sync every bus currently active in the program. With no explicit targets the op broadcasts (Operation.BROADCASTS_WHEN_NO_BUS), so the validator intersects the capabilities of every bus the program touches.

Source code in src/qprogram/operations/sync.py
def __init__(self, targets: list[str] | None = None) -> None:
    self.targets = targets

required_capabilities

required_capabilities() -> set[str]

Return the single op.sync token.

Source code in src/qprogram/operations/sync.py
def required_capabilities(self) -> set[str]:
    """Return the single ``op.sync`` token."""
    return {"op.sync"}

SetFrequency

SetFrequency(bus: str, frequency: float | Expression)

Bases: Operation

A new NCO / oscillator frequency for a bus.

Parameters:

  • bus (str) –

    Bus whose oscillator to retune.

  • frequency (float | Expression) –

    New frequency in Hz. Accepts an Expression for sweeps.

Source code in src/qprogram/operations/set_frequency.py
def __init__(self, bus: str, frequency: float | Expression) -> None:
    self.bus = bus
    self.frequency = frequency

required_capabilities

required_capabilities() -> set[str]

Return op.set_frequency plus the tokens contributed by the frequency expression.

Source code in src/qprogram/operations/set_frequency.py
def required_capabilities(self) -> set[str]:
    """Return ``op.set_frequency`` plus the tokens contributed by the ``frequency`` expression."""
    from qprogram.protocol import expression_tokens  # ruff: ignore[import-outside-top-level]

    return {"op.set_frequency"} | expression_tokens(self.frequency)

SetPhase

SetPhase(bus: str, phase: float | Expression)

Bases: Operation

A new NCO phase for a bus.

Parameters:

  • bus (str) –

    Bus whose oscillator phase to set.

  • phase (float | Expression) –

    Phase in radians. Accepts an Expression.

Source code in src/qprogram/operations/set_phase.py
def __init__(self, bus: str, phase: float | Expression) -> None:
    self.bus = bus
    self.phase = phase

required_capabilities

required_capabilities() -> set[str]

Return op.set_phase plus the tokens contributed by the phase expression.

Source code in src/qprogram/operations/set_phase.py
def required_capabilities(self) -> set[str]:
    """Return ``op.set_phase`` plus the tokens contributed by the ``phase`` expression."""
    from qprogram.protocol import expression_tokens  # ruff: ignore[import-outside-top-level]

    return {"op.set_phase"} | expression_tokens(self.phase)

ResetPhase

ResetPhase(bus: str)

Bases: Operation

A reset of the NCO phase on a bus to zero.

Parameters:

  • bus (str) –

    Bus whose oscillator phase to reset.

Source code in src/qprogram/operations/reset_phase.py
def __init__(self, bus: str) -> None:
    self.bus = bus

required_capabilities

required_capabilities() -> set[str]

Return the single op.reset_phase token.

Source code in src/qprogram/operations/reset_phase.py
def required_capabilities(self) -> set[str]:
    """Return the single ``op.reset_phase`` token."""
    return {"op.reset_phase"}

SetGain

SetGain(bus: str, gain: float | Expression)

Bases: Operation

A new output gain for a bus.

Parameters:

  • bus (str) –

    Bus whose output gain to set.

  • gain (float | Expression) –

    New gain. Accepts an Expression for sweeps.

Source code in src/qprogram/operations/set_gain.py
def __init__(self, bus: str, gain: float | Expression) -> None:
    self.bus = bus
    self.gain = gain

required_capabilities

required_capabilities() -> set[str]

Return op.set_gain plus the tokens contributed by the gain expression.

Source code in src/qprogram/operations/set_gain.py
def required_capabilities(self) -> set[str]:
    """Return ``op.set_gain`` plus the tokens contributed by the ``gain`` expression."""
    from qprogram.protocol import expression_tokens  # ruff: ignore[import-outside-top-level]

    return {"op.set_gain"} | expression_tokens(self.gain)

SetOffset

SetOffset(
    bus: str,
    offset_path0: float | Expression,
    offset_path1: float | Expression | None = None,
)

Bases: Operation

A new DC offset on one or both signal paths of a bus.

Parameters:

  • bus (str) –

    Bus whose DC offset to set.

  • offset_path0 (float | Expression) –

    Offset on path 0 (the only path for single-channel buses, I for IQ buses).

  • offset_path1 (float | Expression | None, default: None ) –

    Offset on path 1 (Q for IQ buses). None leaves the path's offset unchanged.

Source code in src/qprogram/operations/set_offset.py
def __init__(
    self,
    bus: str,
    offset_path0: float | Expression,
    offset_path1: float | Expression | None = None,
) -> None:
    self.bus = bus
    self.offset_path0 = offset_path0
    self.offset_path1 = offset_path1

required_capabilities

required_capabilities() -> set[str]

Return op.set_offset plus the tokens contributed by the offset expressions.

An unset offset_path1 contributes nothing.

Source code in src/qprogram/operations/set_offset.py
def required_capabilities(self) -> set[str]:
    """Return ``op.set_offset`` plus the tokens contributed by the offset expressions.

    An unset ``offset_path1`` contributes nothing.
    """
    from qprogram.protocol import expression_tokens  # ruff: ignore[import-outside-top-level]

    caps = {"op.set_offset"} | expression_tokens(self.offset_path0)
    if self.offset_path1 is not None:
        caps |= expression_tokens(self.offset_path1)
    return caps

SetParameter

SetParameter(
    bus: str, parameter: str, value: float | Expression
)

Bases: Operation

A write to a bus-scoped parameter named by string.

Targets a bus (like SetFrequency and the other set_* ops), so BUS_ATTRS is ("bus",) and the op routes to that bus's capability slot. Unlike the real-time set_* ops, a parameter write is a platform-configuration action — not real-time — so platforms expose op.set_parameter only in a bus slot's host half, making it host-side-only (a swept value additionally forces its binding loop to host via a predicate).

Parameters:

  • bus (str) –

    The bus whose parameter is written.

  • parameter (str) –

    Name of the parameter to set.

  • value (float | Expression) –

    New value. Accepts an Expression for sweeps.

Source code in src/qprogram/operations/set_parameter.py
def __init__(
    self,
    bus: str,
    parameter: str,
    value: float | Expression,
) -> None:
    self.bus = bus
    self.parameter = parameter
    self.value = value

required_capabilities

required_capabilities() -> set[str]

Return op.set_parameter plus the tokens contributed by the value expression.

Source code in src/qprogram/operations/set_parameter.py
def required_capabilities(self) -> set[str]:
    """Return ``op.set_parameter`` plus the tokens contributed by the ``value`` expression."""
    from qprogram.protocol import expression_tokens  # ruff: ignore[import-outside-top-level]

    return {"op.set_parameter"} | expression_tokens(self.value)

GetParameter

GetParameter(variable: Variable, bus: str, parameter: str)

Bases: Operation

A read of a bus-scoped parameter into a Variable at runtime.

Targets a bus (BUS_ATTRS = ("bus",)), so the op routes to that bus's capability slot. Like SetParameter, reading a parameter is a platform-configuration action — platforms expose op.get_parameter only in a bus slot's host half, making it host-side-only.

Parameters:

  • variable (Variable) –

    Destination Variable for the read value.

  • bus (str) –

    The bus whose parameter is read.

  • parameter (str) –

    Name of the parameter to read.

Source code in src/qprogram/operations/get_parameter.py
def __init__(self, variable: Variable, bus: str, parameter: str) -> None:
    self.variable = variable
    self.bus = bus
    self.parameter = parameter

required_capabilities

required_capabilities() -> set[str]

Return the single op.get_parameter token.

Source code in src/qprogram/operations/get_parameter.py
def required_capabilities(self) -> set[str]:
    """Return the single ``op.get_parameter`` token."""
    return {"op.get_parameter"}

Call

Call(fragment: Fragment, arguments: dict[str, object])

Bases: Operation

Instantiation of a Fragment at a specific site with bound arguments.

A first-class AST leaf: definitions and call sites survive serialization (.qp emits a fragment <name>(...): section and a bare <name>(<args>) statement) and structural equality. QProgram.expand replaces every Call with the substituted fragment body — compilers and validators consume the expanded, fragment-free program.

Built by QProgram.call; direct construction is rarely needed.

Parameters:

  • fragment (Fragment) –

    The called Fragment.

  • arguments (dict[str, object]) –

    Fully-bound {param_id: value} mapping (one entry per fragment parameter), produced by qprogram.fragments.bind_arguments.

Source code in src/qprogram/operations/call.py
def __init__(self, fragment: Fragment, arguments: dict[str, object]) -> None:
    self.fragment = fragment
    self.arguments = arguments

variables

variables() -> set[Variable]

Return host variables referenced by the bound arguments.

The fragment body's own parameters/locals are not reported — they are placeholders that exist only until expansion.

Source code in src/qprogram/operations/call.py
def variables(self) -> set[Variable]:
    """Return host variables referenced by the bound arguments.

    The fragment body's own parameters/locals are *not* reported — they are placeholders that
    exist only until expansion.
    """
    out: set[Variable] = set()
    for value in self.arguments.values():
        out |= _collect_variables(value)
    return out

buses

buses() -> set[str]

Return every string-valued argument bound at this call site.

A Parameter is untyped — only the position an argument lands in inside the fragment body decides whether it is a bus, a waveform alias, or a plain value — so a string argument of any kind is reported here and the result over-approximates the buses the call touches. Buses named inside the fragment body become visible once QProgram.expand has substituted the arguments.

Source code in src/qprogram/operations/call.py
def buses(self) -> set[str]:
    """Return every string-valued argument bound at this call site.

    A [`Parameter`][qprogram.Parameter] is untyped — only the position an argument lands in inside
    the fragment body decides whether it is a bus, a waveform alias, or a plain value — so a
    string argument of any kind is reported here and the result over-approximates the buses the
    call touches. Buses named inside the fragment body become visible once
    [`QProgram.expand`][qprogram.QProgram.expand] has substituted the arguments.
    """
    return {value for value in self.arguments.values() if isinstance(value, str)}

required_capabilities

required_capabilities() -> set[str]

Return the empty set — validate() expands calls before checking capabilities.

Source code in src/qprogram/operations/call.py
def required_capabilities(self) -> set[str]:
    """Return the empty set — ``validate()`` expands calls before checking capabilities."""
    return set()

MeasurementField

Bases: StrEnum

Canonical names for the data a measurement can produce.

Members are strings (MeasurementField.IQ == "iq" is True), so they travel unchanged through capability tokens, the .qp writer, and fields keys. The enum exists for discoverability (type MeasurementField. and let the editor list the options) and for static checking — it is not a new value type.

Declaration order is the canonical field order: normalize_fields sorts every fields tuple this way, so (IQ, STATE) and (STATE, IQ) build the same AST, hash the same, and serialize to the same .qp line. Vendor-defined fields are plain strings (a vendor ships its own StrEnum if it wants the same ergonomics) and sort after the core members, alphabetically among themselves.

Attributes:

  • STATE

    Classified outcome — required to reference handle.state in a conditional.

  • IQ

    Demodulated, integrated in-phase/quadrature pair. The default, and the data attribute of a MeasurementResult whenever it is requested.

  • RAW

    Raw ADC trace.

MeasurementOperation

Bases: Operation

Marker base for operations that produce a referenceable measurement.

Why a marker base (rather than duck-typing on the handle attribute): vendor authors opt in deliberately, and tooling that wants to enumerate measurements has a single isinstance to check.

Why store the canonical MeasurementHandle rather than a name string: every reference to the measurement — the user's variable, the AST node, every MeasurementRef in conditionals, the value returned by QProgram.measurement_handles — becomes the same Python instance, and the runtime writes per-measurement values onto that single object.

Concrete subclasses must set handle and fields.

fields instance-attribute

fields: tuple[str, ...]

Requested measurement fields, canonically ordered by normalize_fields. Typed as str rather than MeasurementField because vendors may register their own field names; core fields always compare equal to their enum member.

AFFECTS_AVERAGING class-attribute

AFFECTS_AVERAGING: bool = True

Measurements are exactly what an average block accumulates — see Operation.AFFECTS_AVERAGING.

name property

name: str

The measurement name — a proxy for self.handle.name.

required_capabilities

required_capabilities() -> set[str]

Return one measure.fields.<name> token per entry in fields.

Concrete subclasses union this with their own identity-token set.

Source code in src/qprogram/operations/operation.py
def required_capabilities(self) -> set[str]:
    """Return one ``measure.fields.<name>`` token per entry in `fields`.

    Concrete subclasses union this with their own identity-token set.
    """
    from qprogram.protocol import measurement_field_token  # ruff: ignore[import-outside-top-level]

    return {measurement_field_token(f) for f in self.fields}

normalize_fields

normalize_fields(
    value: Iterable[MeasurementField | str],
) -> tuple[str, ...]

Coerce a fields argument into the canonical, sorted tuple[str, ...].

Accepts any iterable of MeasurementField members or registered field-name strings — (MeasurementField.IQ, MeasurementField.STATE) and ["iq", "state"] are equivalent. A bare string is rejected: iterating one would yield its characters, and there is no comma-separated "iq,state" spelling.

Every name is checked against the live capability registry, so a typo raises here — at the measure(...) call site — instead of surfacing later as an unsupported-capability diagnostic. Vendors widen the accepted set by registering measure.fields.<name> (see qprogram.protocol.register_capability_tokens).

The result is deduplicated and sorted into canonical order (MeasurementField declaration order, then vendor names alphabetically), so two measurements requesting the same data compare equal, hash equal, and serialize identically regardless of argument order.

Parameters:

  • value (Iterable[MeasurementField | str]) –

    Iterable of MeasurementField members or registered field-name strings.

Returns:

  • tuple[str, ...]

    Canonical, deduplicated, sorted tuple of field names.

Raises:

  • ValidationError

    If value is a bare string or not iterable, if any entry is not a string or is empty, if any name is not registered, or if no fields are requested.

Source code in src/qprogram/operations/operation.py
def normalize_fields(value: Iterable[MeasurementField | str]) -> tuple[str, ...]:
    """Coerce a ``fields`` argument into the canonical, sorted ``tuple[str, ...]``.

    Accepts any iterable of `MeasurementField` members or registered field-name strings —
    ``(MeasurementField.IQ, MeasurementField.STATE)`` and ``["iq", "state"]`` are equivalent. A bare
    string is **rejected**: iterating one would yield its characters, and there is no comma-separated
    ``"iq,state"`` spelling.

    Every name is checked against the live capability registry, so a typo raises *here* — at the
    ``measure(...)`` call site — instead of surfacing later as an unsupported-capability diagnostic.
    Vendors widen the accepted set by registering ``measure.fields.<name>`` (see
    [`qprogram.protocol.register_capability_tokens`][qprogram.register_capability_tokens]).

    The result is deduplicated and sorted into canonical order (`MeasurementField`
    declaration order, then vendor names alphabetically), so two measurements requesting the same
    data compare equal, hash equal, and serialize identically regardless of argument order.

    Args:
        value (Iterable[MeasurementField | str]): Iterable of `MeasurementField` members or
            registered field-name strings.

    Returns:
        Canonical, deduplicated, sorted tuple of field names.

    Raises:
        ValidationError: If ``value`` is a bare string or not iterable, if any entry is not a
            string or is empty, if any name is not registered, or if no fields are requested.
    """
    if isinstance(value, str):
        raise ValidationError(_bare_string_message(value))
    try:
        items = list(value)
    except TypeError as e:
        msg = f"`fields` must be an iterable of MeasurementField values, got {value!r}"
        raise ValidationError(msg) from e

    names: list[str] = []
    for item in items:
        if not isinstance(item, str):
            msg = (
                f"`fields` entries must be MeasurementField values or field-name strings, got "
                f"{item!r} of type {type(item).__name__!r}"
            )
            raise ValidationError(msg)
        # ``str(item)`` flattens both StrEnum members and the parser's _QuotedStr marker down to a
        # plain ``str``, so storage is uniform no matter how the caller spelled the field.
        name = str(item)
        if not name:
            msg = "`fields` entries must be non-empty field names"
            raise ValidationError(msg)
        names.append(name)

    if not names:
        msg = (
            f"`fields` must request at least one measurement field; valid values are "
            f"{[str(f) for f in MeasurementField]} (plus any vendor-registered names)"
        )
        raise ValidationError(msg)

    _reject_unknown_fields(names)
    return tuple(sorted(dict.fromkeys(names), key=_field_sort_key))

Blocks

blocks

AST block types — containers that group operations and other blocks together.

The base Block is a simple ordered container; concrete subclasses (Average, Conditional, Sweep, Parallel) carry extra structure that the runtime, validator, and serializer understand.

Sweep is the only loop: it binds a variable to whatever a SweepSource produces, so a linear ramp, an explicit table, a log-spaced set and any composition of those are all the same block with a different source.

Block

Block()

Generic sequential container for operations and nested blocks.

The base Block is what QProgram.block produces — an unstructured grouping with no extra semantics. Repeating blocks (Sweep, Average, Parallel) and Conditional subclass it to add structure the runtime understands.

Equality and hashing are structural (same class + same children); once a block has been used as a set / dict key, do not append to it.

Source code in src/qprogram/blocks/block.py
def __init__(self) -> None:
    self._elements: list[Block | Operation] = []

REPEATS class-attribute

REPEATS: bool = False

Whether this block re-runs its body, i.e. occupies a repetition level on the sequencer.

True on the three core repeating blocks (Sweep, Parallel, Average — the last because averaging is repetition), False on the plain grouping Block and on Conditional (branching selects a body, it doesn't iterate).

qprogram.validation.validate reads this to compute max_loop_nesting — the only reason the attribute exists rather than the validator testing concrete classes. A vendor or platform package contributing a repeating block of its own (see the .qp block registry, register_vendor_block) sets it True on its subclass and is then counted correctly against a platform's loop-depth limit, with no core change.

A Parallel counts as one level in total, not one per composed loop: its loop headers live on loops rather than among its children, and they advance in lockstep.

elements property

elements: list[Block | Operation]

The contained operations and sub-blocks, in declaration order.

The block's own list rather than a copy; append is the sanctioned way to extend it.

append

append(element: Block | Operation) -> None

Append an operation or sub-block to the end of this block.

Parameters:

  • element (Block | Operation) –

    The node to add as this block's next child.

Source code in src/qprogram/blocks/block.py
def append(self, element: Block | Operation) -> None:
    """Append an operation or sub-block to the end of this block.

    Args:
        element (Block | Operation): The node to add as this block's next child.
    """
    self._elements.append(element)

variables

variables() -> set[Variable]

Return every Variable referenced by any child.

Loop subclasses override to also include the loop-counter variable they bind.

Returns:

  • set[Variable]

    The union of every child's variables.

Source code in src/qprogram/blocks/block.py
def variables(self) -> set[Variable]:
    """Return every [`Variable`][qprogram.Variable] referenced by any child.

    Loop subclasses override to also include the loop-counter variable they bind.

    Returns:
        The union of every child's variables.
    """
    out: set[Variable] = set()
    for el in self._elements:
        out |= el.variables()
    return out

buses

buses() -> set[str]

Return every bus name referenced by any child.

Returns:

  • set[str]

    The union of every child's bus names. A BusRef is a str, so

  • set[str]

    schema-backed references appear alongside raw string buses.

Source code in src/qprogram/blocks/block.py
def buses(self) -> set[str]:
    """Return every bus name referenced by any child.

    Returns:
        The union of every child's bus names. A [`BusRef`][qprogram.BusRef] is a ``str``, so
        schema-backed references appear alongside raw string buses.
    """
    out: set[str] = set()
    for el in self._elements:
        out |= el.buses()
    return out

waveforms

waveforms() -> set[Waveform | IQWaveform | str]

Return every waveform (concrete or string alias) referenced by any child.

Returns:

Source code in src/qprogram/blocks/block.py
def waveforms(self) -> set[Waveform | IQWaveform | str]:
    """Return every waveform (concrete or string alias) referenced by any child.

    Returns:
        The union of every child's waveforms.
    """
    out: set[Waveform | IQWaveform | str] = set()
    for el in self._elements:
        out |= el.waveforms()
    return out

walk

walk() -> Iterator[Block | Operation]

Yield this block, then each descendant in pre-order.

Pairs with Operation.walk (which yields just the leaf) so callers can write for node in program.body.walk(): without recursion.

Yields:

  • Block | Operation

    This block first, then every descendant operation and sub-block, in declaration order.

Source code in src/qprogram/blocks/block.py
def walk(self) -> Iterator[Block | Operation]:
    """Yield this block, then each descendant in pre-order.

    Pairs with `Operation.walk` (which yields just the leaf) so callers can write
    ``for node in program.body.walk():`` without recursion.

    Yields:
        This block first, then every descendant operation and sub-block, in declaration order.
    """
    yield self
    for el in self._elements:
        yield from el.walk()

required_capabilities

required_capabilities() -> set[str]

Return the capability tokens this block needs, in isolation.

Non-recursive by design — qprogram.validation.validate visits every node and checks each one's own token set against the slot that node routes to. Recursing here would double-count. Subclasses override to add their own identity token (block.<name>) and refinement tokens (sweep shape, etc.).

Returns:

  • set[str]

    The identity token block.block of a plain grouping block.

Source code in src/qprogram/blocks/block.py
def required_capabilities(self) -> set[str]:
    """Return the capability tokens this block needs, in isolation.

    Non-recursive by design — [`qprogram.validation.validate`][qprogram.validate] visits every node and checks
    each one's own token set against the slot that node routes to. Recursing here would
    double-count. Subclasses override to add their own identity token (``block.<name>``) and
    refinement tokens (sweep shape, etc.).

    Returns:
        The identity token ``block.block`` of a plain grouping block.
    """
    return {"block.block"}

Average

Average(shots: int)

Bases: Block

A body run shots times, with measurement results averaged across the repetitions.

Averaging is repetition, so the block occupies a repetition level on the sequencer (Block.REPEATS). Unlike a Sweep it contributes no dimension to the results: the shots are accumulated and divided out, so a state field arrives as the excited-state population over the shots.

Parameters:

  • shots (int) –

    Number of times to execute the block body. Must be a positive integer; a bool is rejected even though it is an int.

Raises:

Source code in src/qprogram/blocks/average.py
def __init__(self, shots: int) -> None:
    super().__init__()
    if not isinstance(shots, int) or isinstance(shots, bool) or shots < 1:
        msg = f"Average shots must be an integer >= 1, got {shots!r}"
        raise ValidationError(msg)
    self.shots = shots

REPEATS class-attribute

REPEATS: bool = True

This block re-runs its body — it occupies a repetition level (see Block.REPEATS).

required_capabilities

required_capabilities() -> set[str]

Return the capability tokens this block needs, in isolation.

Returns:

  • set[str]

    The identity token block.average.

Source code in src/qprogram/blocks/average.py
def required_capabilities(self) -> set[str]:
    """Return the capability tokens this block needs, in isolation.

    Returns:
        The identity token ``block.average``.
    """
    return {"block.average"}

Sweep

Sweep(variable: Variable, source: SweepSource)

Bases: Block

A loop binding variable to each value a SweepSource produces.

The DSL's single sweep construct. It carries no notion of how the values are generated — that is entirely the source's business, which is what lets Range, Values, Logspace, a file, and any composition of those be peers rather than separate block types.

Parameters:

Raises:

  • ValidationError

    If source is neither a source nor a sequence of values.

Source code in src/qprogram/blocks/sweep.py
def __init__(self, variable: Variable, source: SweepSource) -> None:
    super().__init__()
    self.variable = variable
    self.source = _coerce_source(source)

REPEATS class-attribute

REPEATS: bool = True

This block re-runs its body — it occupies a repetition level (see Block.REPEATS).

num_iterations

num_iterations() -> int

Return the number of sweep points, delegated to the source.

Answerable without executing the program, which is what lets Parallel check lockstep lengths at construction time and the executor size a composed parallel axis before the first shot. Cheap for every built-in source except File, which reads its array to answer.

Returns:

  • int

    The number of points the bound source produces.

Raises:

  • ValidationError

    If the bound source cannot describe its points — a File holding an array that is not 1-D, or holding none.

  • OSError

    If the bound source reads a file whose path does not exist or cannot be read.

Source code in src/qprogram/blocks/sweep.py
def num_iterations(self) -> int:
    """Return the number of sweep points, delegated to the source.

    Answerable without executing the program, which is what lets
    [`Parallel`][qprogram.blocks.Parallel] check lockstep lengths at construction time and the executor
    size a composed parallel axis before the first shot. Cheap for every built-in source except
    [`File`][qprogram.File], which reads its array to answer.

    Returns:
        The number of points the bound source produces.

    Raises:
        ValidationError: If the bound source cannot describe its points — a
            [`File`][qprogram.File] holding an array that is not 1-D, or holding none.
        OSError: If the bound source reads a file whose path does not exist or cannot be read.
    """
    return self.source.length()

variables

variables() -> set[Variable]

Return every Variable referenced by the body, plus the swept one.

Returns:

  • set[Variable]

    The body's variables together with variable.

Source code in src/qprogram/blocks/sweep.py
def variables(self) -> set[Variable]:
    """Return every [`Variable`][qprogram.Variable] referenced by the body, plus the swept one.

    Returns:
        The body's variables together with `variable`.
    """
    return super().variables() | {self.variable}

required_capabilities

required_capabilities() -> set[str]

Return block.sweep plus the bound source's own tokens.

A platform therefore declares both the loop and the specific value source it is asked for: the source's class token and its sweep.<kind>, unioned across everything a combinator wraps.

Returns:

Source code in src/qprogram/blocks/sweep.py
def required_capabilities(self) -> set[str]:
    """Return ``block.sweep`` plus the bound source's own tokens.

    A platform therefore declares both the loop and the specific value source it is asked for: the
    source's class token and its ``sweep.<kind>``, unioned across everything a combinator wraps.

    Returns:
        The identity token ``block.sweep`` together with [`SweepSource.tokens`][qprogram.SweepSource.tokens].
    """
    return {"block.sweep"} | self.source.tokens()

Parallel

Parallel(loops: Iterable[Sweep])

Bases: Block

Several loops advanced in lockstep, sharing one body.

Created via the | operator on sweep contexts (with sweep(a, src) | sweep(b, src) as p:). Note the structural quirk: composed loop headers live on self.loops, not in self._elements. Body operations live in _elements as usual. The introspection overrides below thread loop variables back into the unioned views so analyzers see them.

The composition occupies a single repetition level (see Block.REPEATS), not one per composed loop, because the headers advance together rather than nesting.

Parameters:

  • loops (Iterable[Sweep]) –

    The Sweep instances to advance in lockstep. At least two, and all with the same number of iterations — which every source can report statically, so the check happens here at construction rather than at run time.

Raises:

  • ValidationError

    If fewer than two loops are given, if their iteration counts differ, or if a bound source cannot describe its points — a File holding an array that is not 1-D, or holding none.

  • OSError

    If a bound source reads a file whose path does not exist or cannot be read.

Source code in src/qprogram/blocks/parallel.py
def __init__(self, loops: Iterable[Sweep]) -> None:
    super().__init__()
    loop_list: list[Sweep] = list(loops)
    if len(loop_list) < 2:
        msg = f"Parallel requires at least two loops, got {len(loop_list)}"
        raise ValidationError(msg)
    iteration_counts = [lp.num_iterations() for lp in loop_list]
    if len(set(iteration_counts)) > 1:
        described = ", ".join(
            f"{type(lp).__name__}({lp.variable.id!r}): {n}"
            for lp, n in zip(loop_list, iteration_counts, strict=True)
        )
        msg = f"parallel loops must have the same number of iterations to advance in lockstep; got {described}"
        raise ValidationError(msg)
    self.loops: list[Sweep] = loop_list

REPEATS class-attribute

REPEATS: bool = True

This block re-runs its body — it occupies a repetition level (see Block.REPEATS).

variables

variables() -> set[Variable]

Return every Variable in the shared body, plus the ones the loops bind.

The loop headers sit outside _elements, so the inherited walk over the body misses them and they are unioned in here.

Returns:

  • set[Variable]

    The body's variables together with each composed loop's own.

Source code in src/qprogram/blocks/parallel.py
def variables(self) -> set[Variable]:
    """Return every [`Variable`][qprogram.Variable] in the shared body, plus the ones the loops bind.

    The loop headers sit outside ``_elements``, so the inherited walk over the body misses them and
    they are unioned in here.

    Returns:
        The body's variables together with each composed loop's own.
    """
    out = super().variables()
    for lp in self.loops:
        out |= lp.variables()
    return out

walk

walk() -> Iterator[Block | Operation]

Yield this block, then each composed loop header, then the shared body in pre-order.

The headers come first so a consumer meets the loops that bind the variables before the operations that read them.

Yields:

Source code in src/qprogram/blocks/parallel.py
def walk(self) -> Iterator[Block | Operation]:
    """Yield this block, then each composed loop header, then the shared body in pre-order.

    The headers come first so a consumer meets the loops that bind the variables before the
    operations that read them.

    Yields:
        This block, then each composed [`Sweep`][qprogram.blocks.Sweep] with its own descendants,
        then every node of the shared body.
    """
    yield self
    for lp in self.loops:
        yield from lp.walk()
    for el in self._elements:
        yield from el.walk()

required_capabilities

required_capabilities() -> set[str]

Return the capability tokens this block needs, in isolation.

qprogram.validation.validate classifies each composed header as a child block in its own right and checks its tokens there, so the headers' sweep.* tokens are not repeated here.

Returns:

  • set[str]

    The identity token block.parallel.

Source code in src/qprogram/blocks/parallel.py
def required_capabilities(self) -> set[str]:
    """Return the capability tokens this block needs, in isolation.

    [`qprogram.validation.validate`][qprogram.validate] classifies each composed header as a child block in its
    own right and checks its tokens there, so the headers' ``sweep.*`` tokens are not repeated
    here.

    Returns:
        The identity token ``block.parallel``.
    """
    return {"block.parallel"}

Conditional

Conditional()

Bases: Block

A chain of if / elif / else arms.

Why this doesn't reuse the inherited _elements list: arms each carry an independent body and a condition expression, so a single ordered list would conflate "arm body" with "shared body" — there is no shared body in a conditional. append therefore raises; populate via the builder methods on QProgram.

Attributes:

  • arms (list[tuple[Expression, Block]]) –

    The (condition, body) pairs, in source order. The first condition that evaluates truthy selects its body.

  • else_body (Block | None) –

    The terminal else body, or None when the chain has none.

Source code in src/qprogram/blocks/conditional.py
def __init__(self) -> None:
    super().__init__()
    self.arms: list[tuple[Expression, Block]] = []
    self.else_body: Block | None = None

append

append(element: Block | Operation) -> None

Raise ValidationError — populate via QProgram.if_ / elif_ / else_ instead.

Parameters:

Raises:

Source code in src/qprogram/blocks/conditional.py
def append(self, element: Block | Operation) -> None:  # ruff: ignore[unused-method-argument]
    """Raise ``ValidationError`` — populate via [`QProgram.if_`][qprogram.QProgram.if_] / `elif_` / `else_` instead.

    Args:
        element (Block | Operation): Ignored; the call never succeeds.

    Raises:
        ValidationError: Always.
    """
    msg = (
        "Cannot append directly to a Conditional. "
        "Use the arm body returned by program.if_() / elif_() / else_() instead."
    )
    raise ValidationError(msg)

walk

walk() -> Iterator[Block | Operation]

Yield this block, then every node of each arm body and of the else body.

Arm conditions are expressions rather than AST nodes, so they are not yielded; a consumer that needs them reads arms directly.

Yields:

Source code in src/qprogram/blocks/conditional.py
def walk(self) -> Iterator[Block | Operation]:
    """Yield this block, then every node of each arm body and of the ``else`` body.

    Arm conditions are expressions rather than AST nodes, so they are not yielded; a consumer that
    needs them reads `arms` directly.

    Yields:
        This conditional first, then each arm body's nodes in source order, then the ``else``
        body's.
    """
    yield self
    for _, body in self.arms:
        yield from body.walk()
    if self.else_body is not None:
        yield from self.else_body.walk()

variables

variables() -> set[Variable]

Return every Variable referenced by an arm condition or an arm body.

The conditions count: a branch taken on a swept threshold makes that variable part of the conditional even when no operation inside it reads the variable.

Returns:

  • set[Variable]

    The union over every arm's condition and body, plus the else body.

Source code in src/qprogram/blocks/conditional.py
def variables(self) -> set[Variable]:
    """Return every [`Variable`][qprogram.Variable] referenced by an arm condition or an arm body.

    The conditions count: a branch taken on a swept threshold makes that variable part of the
    conditional even when no operation inside it reads the variable.

    Returns:
        The union over every arm's condition and body, plus the ``else`` body.
    """
    out: set[Variable] = set()
    for cond, body in self.arms:
        out |= cond.variables() | body.variables()
    if self.else_body is not None:
        out |= self.else_body.variables()
    return out

buses

buses() -> set[str]

Return every bus name referenced from any arm body or from the else body.

Returns:

  • set[str]

    The union of the bus names of every arm body and the else body.

Source code in src/qprogram/blocks/conditional.py
def buses(self) -> set[str]:
    """Return every bus name referenced from any arm body or from the ``else`` body.

    Returns:
        The union of the bus names of every arm body and the ``else`` body.
    """
    out: set[str] = set()
    for _, body in self.arms:
        out |= body.buses()
    if self.else_body is not None:
        out |= self.else_body.buses()
    return out

waveforms

waveforms() -> set[Waveform | IQWaveform | str]

Return every waveform (concrete or string alias) referenced by any arm body.

Returns:

  • set[Waveform | IQWaveform | str]

    The union of the waveforms of every arm body and the else body.

Source code in src/qprogram/blocks/conditional.py
def waveforms(self) -> set[Waveform | IQWaveform | str]:
    """Return every waveform (concrete or string alias) referenced by any arm body.

    Returns:
        The union of the waveforms of every arm body and the ``else`` body.
    """
    out: set[Waveform | IQWaveform | str] = set()
    for _, body in self.arms:
        out |= body.waveforms()
    if self.else_body is not None:
        out |= self.else_body.waveforms()
    return out

required_capabilities

required_capabilities() -> set[str]

Return block.conditional plus the expression tokens of every arm condition.

A platform that branches must also evaluate the conditions, so each arm contributes the expr.* tokens of its condition tree — the comparison, the measurement reference it reads, and any arithmetic around them. The else arm has no condition and contributes none. Like every other node's token set this is non-recursive: the arm bodies' own tokens are collected as the validator walks them.

Returns:

  • set[str]

    The identity token block.conditional together with the expr.* tokens of the arm

  • set[str]

    conditions.

Source code in src/qprogram/blocks/conditional.py
def required_capabilities(self) -> set[str]:
    """Return ``block.conditional`` plus the expression tokens of every arm condition.

    A platform that branches must also evaluate the conditions, so each arm contributes the
    ``expr.*`` tokens of its condition tree — the comparison, the measurement reference it reads,
    and any arithmetic around them. The ``else`` arm has no condition and contributes none. Like
    every other node's token set this is non-recursive: the arm bodies' own tokens are collected as
    the validator walks them.

    Returns:
        The identity token ``block.conditional`` together with the ``expr.*`` tokens of the arm
        conditions.
    """
    from qprogram.protocol import expression_tokens  # ruff: ignore[import-outside-top-level]

    caps = {"block.conditional"}
    for cond, _ in self.arms:
        caps |= expression_tokens(cond)
    return caps

Fragments

fragment

fragment(func: Callable[..., None]) -> Fragment

Build a Fragment from a function — the signature is the parameter list.

The first parameter receives the fragment builder; each remaining parameter becomes a Parameter (in order); the fragment name is the function's __name__. The body runs once, at decoration time, to record the AST — Python-level control flow inside it is evaluated at definition, not per call.

Example::

@fragment
def x_pulse(f, drive, amp):
    f.play(drive, Gaussian(amplitude=amp, duration=40, sigma=8))


program.call(x_pulse, "drive_q0", 0.5)

Parameters:

  • func (Callable[..., None]) –

    The definition function. *args / **kwargs / defaults / keyword-only parameters are rejected — the .qp grammar has no representation for them.

Returns:

  • Fragment

    The recorded Fragment (the decorated name is the fragment object).

Raises:

  • ValidationError

    If the object has no __name__, takes no builder parameter, carries a __name__ that is not a valid fragment identifier or is reserved (a lambda's "<lambda>" fails here), or declares a parameter that is not plain positional or that carries a default.

  • InvalidVariableIdError

    If a parameter's name is reserved (see RESERVED_KEYWORDS) or falls outside [A-Za-z_][A-Za-z0-9_]*, which a non-ASCII Python identifier does.

Source code in src/qprogram/fragments.py
def fragment(func: Callable[..., None]) -> Fragment:
    """Build a [`Fragment`][qprogram.Fragment] from a function — the signature *is* the parameter list.

    The first parameter receives the fragment builder; each remaining parameter becomes a
    [`Parameter`][qprogram.Parameter] (in order); the fragment name is the function's ``__name__``. The body runs
    **once**, at decoration time, to record the AST — Python-level control flow inside it is
    evaluated at definition, not per call.

    Example::

        @fragment
        def x_pulse(f, drive, amp):
            f.play(drive, Gaussian(amplitude=amp, duration=40, sigma=8))


        program.call(x_pulse, "drive_q0", 0.5)

    Args:
        func (Callable[..., None]): The definition function. ``*args`` / ``**kwargs`` / defaults /
            keyword-only parameters are rejected — the ``.qp`` grammar has no representation for
            them.

    Returns:
        The recorded [`Fragment`][qprogram.Fragment] (the decorated name *is* the fragment object).

    Raises:
        ValidationError: If the object has no ``__name__``, takes no builder parameter, carries a
            ``__name__`` that is not a valid fragment identifier or is reserved (a lambda's
            ``"<lambda>"`` fails here), or declares a parameter that is not plain positional or that
            carries a default.
        InvalidVariableIdError: If a parameter's name is reserved (see
            [`RESERVED_KEYWORDS`][qprogram.RESERVED_KEYWORDS]) or falls outside ``[A-Za-z_][A-Za-z0-9_]*``, which
            a non-ASCII Python identifier does.
    """
    func_name = getattr(func, "__name__", None)
    if not isinstance(func_name, str):
        msg = "@fragment requires a named function (the object has no __name__ to use as the fragment name)"
        raise ValidationError(msg)
    sig = inspect.signature(func)
    sig_params = list(sig.parameters.values())
    if not sig_params:
        msg = f"@fragment function {func_name!r} must take the fragment builder as its first parameter"
        raise ValidationError(msg)
    positional_kinds = (inspect.Parameter.POSITIONAL_ONLY, inspect.Parameter.POSITIONAL_OR_KEYWORD)
    frag = Fragment(func_name)
    handles: list[Parameter] = []
    for p in sig_params[1:]:
        if p.kind not in positional_kinds:
            msg = (
                f"@fragment {func_name!r}: parameter {p.name!r} is "
                f"{p.kind.description}; only plain positional parameters are supported"
            )
            raise ValidationError(msg)
        if p.default is not inspect.Parameter.empty:
            msg = f"@fragment {func_name!r}: parameter {p.name!r} has a default value; defaults are not supported"
            raise ValidationError(msg)
        handles.append(frag.parameter(p.name))
    func(frag, *handles)
    return frag

Fragment

Fragment(
    name: str,
    label: str = "",
    description: str | None = None,
)

Bases: QProgram

A named, parameterized sub-program — define once, call many times.

Inherits the entire QProgram builder (operations, control flow, vendor namespaces), so a fragment body is built exactly like a program body. A fragment may call another fragment that is already defined; cycles are rejected at registration and at expansion.

Parameters:

  • name (str) –

    Fragment identifier — must match [A-Za-z_][A-Za-z0-9_]* and not be a reserved keyword. Used verbatim as the .qp definition and call name.

  • label (str, default: '' ) –

    Human-readable label. A .qp fragment section is headed by the name and the parameter list alone, so the label does not survive serialization.

  • description (str | None, default: None ) –

    Longer free-form description. Not serialized either.

Raises:

Source code in src/qprogram/fragments.py
def __init__(self, name: str, label: str = "", description: str | None = None) -> None:
    if not isinstance(name, str) or not _ID_RE.match(name):
        msg = f"fragment name {name!r} is invalid: must match [A-Za-z_][A-Za-z0-9_]* (no spaces or punctuation)"
        raise ValidationError(msg)
    if name in RESERVED_KEYWORDS:
        msg = f"fragment name {name!r} is a reserved keyword (see qprogram.RESERVED_KEYWORDS)"
        raise ValidationError(msg)
    super().__init__(label=label, description=description)
    self._name = name
    self._params: list[Parameter] = []

name property

name: str

The fragment's identifier, used verbatim as its .qp definition and call name.

params property

params: tuple[Parameter, ...]

The declared parameters, in declaration order — the order positional arguments bind in.

parameter

parameter(
    id: str, *, label: str | None = None
) -> Parameter

Declare a new Parameter on this fragment.

Parameters:

  • id (str) –

    Identifier matching [A-Za-z_][A-Za-z0-9_]*; unique among the fragment's parameters and local variables (they share the identifier namespace inside the body).

  • label (str | None, default: None ) –

    Human-readable name for plots and results.

Returns:

Raises:

Source code in src/qprogram/fragments.py
def parameter(self, id: str, *, label: str | None = None) -> Parameter:  # ruff: ignore[builtin-argument-shadowing]
    """Declare a new [`Parameter`][qprogram.Parameter] on this fragment.

    Args:
        id (str): Identifier matching ``[A-Za-z_][A-Za-z0-9_]*``; unique among the fragment's
            parameters *and* local variables (they share the identifier namespace inside the
            body).
        label (str | None): Human-readable name for plots and results.

    Returns:
        The new [`Parameter`][qprogram.Parameter], usable anywhere in the fragment body a value, bus, or
        waveform is accepted.

    Raises:
        ValidationError: If ``id`` collides with an existing parameter or local variable.
        InvalidVariableIdError: If ``id`` violates the identifier pattern or is reserved
            (see [`RESERVED_KEYWORDS`][qprogram.RESERVED_KEYWORDS]).
    """
    if any(p.id == id for p in self._params):
        msg = f"Parameter {id!r} is already declared on fragment {self._name!r}"
        raise ValidationError(msg)
    if any(v.id == id for v in self._variables):
        msg = f"Parameter {id!r} collides with a local variable of fragment {self._name!r}"
        raise ValidationError(msg)
    param = Parameter(id, label=label)
    self._params.append(param)
    return param

variable

variable(
    id: str,
    *,
    label: str | None = None,
    units: str | None = None,
    description: str | None = None,
) -> Variable

Declare a fragment-local Variable.

Local variables are renamed onto the host program at expansion ({fragment}_{id}, with a numeric suffix on collision) so repeated calls never clash.

Parameters:

  • id (str) –

    Short identifier matching [A-Za-z_][A-Za-z0-9_]*; unique among the fragment's parameters and local variables.

  • label (str | None, default: None ) –

    Human-readable name for plots and results.

  • units (str | None, default: None ) –

    Unit string (e.g. "Hz", "ns").

  • description (str | None, default: None ) –

    Longer free-form description.

Returns:

Raises:

Source code in src/qprogram/fragments.py
def variable(
    self,
    id: str,  # ruff: ignore[builtin-argument-shadowing]
    *,
    label: str | None = None,
    units: str | None = None,
    description: str | None = None,
) -> Variable:
    """Declare a fragment-local [`Variable`][qprogram.Variable].

    Local variables are renamed onto the host program at expansion (``{fragment}_{id}``, with a
    numeric suffix on collision) so repeated calls never clash.

    Args:
        id (str): Short identifier matching ``[A-Za-z_][A-Za-z0-9_]*``; unique among the
            fragment's parameters *and* local variables.
        label (str | None): Human-readable name for plots and results.
        units (str | None): Unit string (e.g. ``"Hz"``, ``"ns"``).
        description (str | None): Longer free-form description.

    Returns:
        The new [`Variable`][qprogram.Variable], scoped to this fragment.

    Raises:
        ValidationError: If ``id`` collides with a parameter or an existing local variable.
        InvalidVariableIdError: If ``id`` violates the identifier pattern or is reserved
            (see [`RESERVED_KEYWORDS`][qprogram.RESERVED_KEYWORDS]).
    """
    if any(p.id == id for p in self._params):
        msg = f"Variable {id!r} collides with a parameter of fragment {self._name!r}"
        raise ValidationError(msg)
    return super().variable(id, label=label, units=units, description=description)

Parameter

Parameter(
    id: str,
    *,
    label: str | None = None,
    units: str | None = None,
    description: str | None = None,
)

Bases: Variable

A fragment parameter — a placeholder substituted with the bound argument at expansion.

Subclasses Variable, so a parameter participates in expressions (amp * 2), serializes as a bare identifier, and follows the same id rules. Parameters are untyped: the binding determines the kind (number/expression, bus, or waveform), checked at expansion with a clear error when a binding is used in an incompatible position.

Source code in src/qprogram/variable.py
def __init__(
    self,
    id: str,  # ruff: ignore[builtin-argument-shadowing]
    *,
    label: str | None = None,
    units: str | None = None,
    description: str | None = None,
) -> None:
    if not _ID_RE.match(id):
        raise InvalidVariableIdError(id)
    if id in RESERVED_KEYWORDS:
        raise InvalidVariableIdError(id, reserved=True)
    self._id: str = id
    self._label: str | None = label
    self._units: str | None = units
    self._description: str | None = description
    self._value: int | float | _UnassignedType = UNASSIGNED

Results

MeasurementHandle

MeasurementHandle(name: str)

A reference to a measurement performed by a QProgram.

Equality is structural: two handles with the same name refer to the same measurement. Why this matters: after a .qp round-trip the original Python objects are gone but names survive in the AST, so a freshly-constructed MeasurementHandle("q0_m0") compares equal to the original.

Runtime-supplied values (e.g. the classified qubit state when the measurement's fields includes STATE) live in a private _values dict keyed by field name. They participate in MeasurementRef evaluation but do not contribute to handle identity.

Parameters:

  • name (str) –

    Stable identifier for the measurement. Auto-assigned by QProgram.measure (q0/readout/m0, m0, ...) or user-supplied. Emitted verbatim into .qp.

Raises:

Note

_auto_named records whether QProgram.measure allocated the name (True) or the user supplied it (False). It distinguishes a bus-derived auto-name (q0/readout/m0) that QProgram.rebind must re-derive when the bus changes from a deliberate user label that must be preserved — the two are byte-identical once serialized, so the flag is the only sound signal. It is in-memory state (like the platform parameter store): it is not serialized, so it defaults to False on a handle reconstructed from .qp (rebind before dumping).

Source code in src/qprogram/result.py
def __init__(self, name: str) -> None:
    if not isinstance(name, str) or not name:
        msg = f"MeasurementHandle name must be a non-empty string, got {name!r}"
        raise ValidationError(msg)
    self.name = name
    self._values: dict[str, int | float] = {}
    self._auto_named: bool = False

state property

state: _HandleFieldAccess

A proxy for referencing this measurement's classified state in a conditional.

The returned proxy is a throwaway whose == and != operators build Comparison AST nodes::

with program.if_(handle.state == 0):
    ...

The producing measurement op must request state classification (its fields must include STATE); the validator emits missing-classification otherwise.

MeasurementResult dataclass

MeasurementResult(
    bus: str,
    name: str,
    data: DataArray,
    fields: dict[str, DataArray] = dict(),
)

One measurement record produced by the runtime.

Attributes:

  • bus (str) –

    The bus the measurement was taken on.

  • name (str) –

    The measurement handle's name, as assigned at program construction.

  • data (DataArray) –

    The primary result array — the "iq" field when the measurement requested it, else the first requested field in canonical order (see MeasurementField). Use it when you want whatever the measurement produced; QProgramResult.get names a field explicitly instead.

  • fields (dict[str, DataArray]) –

    One array per requested measurement field, keyed by field name. Shapes per spec §8: iq(*sweeps, "IQ"); state(*sweeps) (excited-state population under averaging); raw(*sweeps, "time", "IQ"). This is what QProgramResult.get reads.

QProgramResult

QProgramResult()

In-memory result of executing a QProgram.

Each measurement contributes one MeasurementResult holding an xarray.DataArray per requested field. Dimensions are named after the enclosing loops, outermost first; the iq field carries a trailing "IQ" dimension with coordinates ["I", "Q"].

Results are stored in construction order and addressable by handle, by name string, or by integer position via get, which returns the IQ field unless a different one is named.

Source code in src/qprogram/result.py
def __init__(self) -> None:
    self._measurements: list[MeasurementResult] = []

measurements property

measurements: list[MeasurementResult]

All measurement records in construction order.

append_measurement

append_measurement(
    bus: str,
    name: str,
    data: DataArray,
    fields: dict[str, DataArray] | None = None,
) -> None

Append a measurement record.

Parameters:

  • bus (str) –

    The bus the measurement was taken on.

  • name (str) –

    The measurement handle name as it appears in the AST.

  • data (DataArray) –

    The primary result data (the "iq" field when requested).

  • fields (dict[str, DataArray] | None, default: None ) –

    Per-measurement-field arrays, keyed by field name. Omitting it records data as the IQ field — the field a measurement requests when fields= is omitted, and the one get returns by default. A record whose primary array is not iq must pass the mapping explicitly, so that get never hands back an array under the wrong field name.

Source code in src/qprogram/result.py
def append_measurement(
    self,
    bus: str,
    name: str,
    data: xr.DataArray,
    fields: dict[str, xr.DataArray] | None = None,
) -> None:
    """Append a measurement record.

    Args:
        bus (str): The bus the measurement was taken on.
        name (str): The measurement handle name as it appears in the AST.
        data (xarray.DataArray): The primary result data (the ``"iq"`` field when requested).
        fields (dict[str, xarray.DataArray] | None): Per-measurement-field arrays, keyed by field
            name. Omitting it records ``data`` as the `IQ`
            field — the field a measurement requests when ``fields=`` is omitted, and the one
            `get` returns by default. A record whose primary array is *not* ``iq`` must
            pass the mapping explicitly, so that `get` never hands back an array under the
            wrong field name.
    """
    if not fields:
        fields = {MeasurementField.IQ.value: data}
    self._measurements.append(MeasurementResult(bus=bus, name=name, data=data, fields=dict(fields)))

get

get(
    measurement: MeasurementHandle | str | int = 0,
    bus: str | None = None,
    field: MeasurementField | str = MeasurementField.IQ,
) -> xr.DataArray

Retrieve one measurement field's data.

Parameters:

  • measurement (MeasurementHandle | str | int, default: 0 ) –

    Which measurement to retrieve.

    • MeasurementHandle: looked up by name.
    • str: looked up by name. Useful after a loads() round-trip when the original handle objects are gone but handles can be reconstructed via QProgram.measurement_handles.
    • int: positional sugar; returns the N-th measurement in declaration order, or N-th on bus when the filter is given. A handle or a name says what it means and survives reordering, so prefer either to a position.
  • bus (str | None, default: None ) –

    Bus name filter — narrows the search before the handle / name / position lookup.

  • field (MeasurementField | str, default: IQ ) –

    Which measurement field to return — a MeasurementField member or a registered vendor field name (the members are strings, so both spell the same thing). Defaults to IQ, matching the default of measure(..., fields=); a measurement that did not request the field raises KeyError rather than silently substituting another one. Reach for MeasurementResult.data when you want the record's primary array whatever the requested fields were.

Returns:

  • DataArray

    The field's xarray.DataArray.

Raises:

  • KeyError

    When measurement is a handle or name with no match in scope, or field names a measurement field the measurement did not request.

  • IndexError

    When measurement is an integer position outside the range in scope.

  • ValidationError

    When field is None. There is no spelling of "give me the primary array" here — read MeasurementResult.data for that.

Source code in src/qprogram/result.py
def get(
    self,
    measurement: MeasurementHandle | str | int = 0,
    bus: str | None = None,
    field: MeasurementField | str = MeasurementField.IQ,
) -> xr.DataArray:
    """Retrieve one measurement field's data.

    Args:
        measurement (MeasurementHandle | str | int): Which measurement to retrieve.

            - [`MeasurementHandle`][qprogram.MeasurementHandle]: looked up by name.
            - ``str``: looked up by name. Useful after a ``loads()`` round-trip when the original
              handle objects are gone but handles can be reconstructed via
              [`QProgram.measurement_handles`][qprogram.QProgram.measurement_handles].
            - ``int``: positional sugar; returns the N-th measurement in declaration order, or N-th
              on ``bus`` when the filter is given. A handle or a name says what it means and
              survives reordering, so prefer either to a position.

        bus (str | None): Bus name filter — narrows the search before the handle / name /
            position lookup.
        field (MeasurementField | str): Which measurement field to return — a
            `MeasurementField` member or a registered vendor field name (the
            members *are* strings, so both spell the same thing). Defaults to
            `IQ`, matching the default of
            ``measure(..., fields=)``; a measurement that did not request the field raises
            ``KeyError`` rather than silently substituting another one. Reach for
            `MeasurementResult.data` when you want the record's primary array whatever the
            requested fields were.

    Returns:
        The field's `xarray.DataArray`.

    Raises:
        KeyError: When ``measurement`` is a handle or name with no match in scope, or ``field``
            names a measurement field the measurement did not request.
        IndexError: When ``measurement`` is an integer position outside the range in scope.
        ValidationError: When ``field`` is ``None``. There is no spelling of "give me the primary
            array" here — read `MeasurementResult.data` for that.
    """
    if field is None:
        msg = (
            "field=None is not a valid field: get() returns the "
            f"{MeasurementField.IQ.value!r} field by default. Name the field explicitly "
            "(field=MeasurementField.STATE), or read MeasurementResult.data for the record's "
            "primary array."
        )
        raise ValidationError(msg)

    candidates = self._measurements if bus is None else [m for m in self._measurements if m.bus == bus]

    if isinstance(measurement, MeasurementHandle):
        record = self._lookup_by_name(candidates, measurement.name, bus)
    elif isinstance(measurement, str):
        record = self._lookup_by_name(candidates, measurement, bus)
    else:
        # int — positional sugar.
        if measurement >= len(candidates):
            scope = f" for bus '{bus}'" if bus is not None else ""
            msg = f"Measurement index {measurement} out of range{scope} ({len(candidates)} measurements)"
            raise IndexError(msg)
        record = candidates[measurement]
    # ``str(...)`` normalizes a MeasurementField member to its wire name, so the lookup and the
    # error message read the same whichever spelling the caller used.
    name = str(field)
    if name not in record.fields:
        available = ", ".join(sorted(record.fields)) or "none"
        msg = f"Measurement {record.name!r} has no field {name!r}; available: {available}"
        raise KeyError(msg)
    return record.fields[name]

Vendor protocol

A vendor extension groups its operations as methods on a VendorNamespace subclass, registers that subclass under a namespace name with QProgram.register_vendor, and reaches the program through the two protected helpers below; program.<vendor>.<operation>(...) then works without core QProgram knowing the vendor exists.

The registration calls below all run at the extension's import time. register_operation and register_block add core names unless given a vendor=, while register_vendor_operation and register_vendor_block forward to them with the vendor prefix already filled in; register_waveform keys a waveform class by its own __name__ with no prefix at all. register_vendor_version records the extension's protocol version, and the writer raises SerializationError on vendor content whose extension never called it. try_activate_vendor is the discovery step: it imports the package behind the qprogram.vendors entry point for a name and returns False when no installed package claims that name.

VendorNamespace

VendorNamespace(program: QProgram)

Base class for vendor operation namespaces.

Vendors subclass this and add typed methods that construct Operation instances and append them to the program via _append (or _append_measurement for measurement ops).

Parameters:

  • program (QProgram) –

    The program whose currently active block receives the appended operations. Held for the namespace's lifetime, which the program ties to its own.

Source code in src/qprogram/vendor.py
def __init__(self, program: QProgram) -> None:
    self._program = program

_append

_append(operation: Operation) -> None

Append a vendor operation to the program's active block.

BusRef attributes (and lists thereof) are run through QProgram._validate_bus so vendor ops can't sneak in a bus from a different schema. Plain-string attributes are not validated.

Parameters:

  • operation (Operation) –

    The vendor operation instance to append.

Raises:

  • ValidationError

    When one of the operation's bus references belongs to a different BusSchema than the one attached to the program.

Source code in src/qprogram/vendor.py
def _append(self, operation: Operation) -> None:
    """Append a vendor operation to the program's active block.

    [`BusRef`][qprogram.BusRef] attributes (and lists thereof) are run through
    `QProgram._validate_bus` so vendor ops can't sneak in a bus from a different schema.
    Plain-string attributes are not validated.

    Args:
        operation (Operation): The vendor operation instance to append.

    Raises:
        ValidationError: When one of the operation's bus references belongs to a different
            [`BusSchema`][qprogram.BusSchema] than the one attached to the program.
    """
    for value in vars(operation).values():
        if isinstance(value, BusRef):
            self._program._validate_bus(value)  # ruff: ignore[private-member-access]
        elif isinstance(value, list):
            for item in value:
                if isinstance(item, BusRef):
                    self._program._validate_bus(item)  # ruff: ignore[private-member-access]
    self._program._append_to_active(operation)  # ruff: ignore[private-member-access]

_append_measurement

_append_measurement(
    op_cls: type[MeasurementOperation],
    *,
    bus: str,
    name: str | None = None,
    **kwargs: Any,
) -> MeasurementHandle

Allocate a handle, build a vendor measurement op, append it, and return the handle.

Shares the per-bus name counter with QProgram.measure so vendor and core measurements on the same bus never collide. Vendor measurement methods call this in place of _append so users receive a usable MeasurementHandle.

Parameters:

  • op_cls (type[MeasurementOperation]) –

    The concrete MeasurementOperation subclass to instantiate.

  • bus (str) –

    Bus the measurement runs on.

  • name (str | None, default: None ) –

    Explicit handle name; auto-allocated when omitted.

  • **kwargs (Any, default: {} ) –

    Remaining keyword arguments forwarded to op_cls(...).

Returns:

Raises:

  • ValidationError

    When name is empty, not a string, or already used by another measurement in the program, or when the operation carries a bus reference from a foreign BusSchema.

Example::

def acquire(self, bus, weights, *, name=None):
    return self._append_measurement(Acquire, bus=bus, weights=weights, name=name)
Source code in src/qprogram/vendor.py
def _append_measurement(
    self,
    op_cls: type[MeasurementOperation],
    *,
    bus: str,
    name: str | None = None,
    **kwargs: Any,
) -> MeasurementHandle:
    """Allocate a handle, build a vendor measurement op, append it, and return the handle.

    Shares the per-bus name counter with [`QProgram.measure`][qprogram.QProgram.measure] so vendor and core
    measurements on the same bus never collide. Vendor measurement methods call this in place of `_append` so users
    receive a usable [`MeasurementHandle`][qprogram.MeasurementHandle].

    Args:
        op_cls (type[MeasurementOperation]): The concrete `MeasurementOperation` subclass
            to instantiate.
        bus (str): Bus the measurement runs on.
        name (str | None): Explicit handle name; auto-allocated when omitted.
        **kwargs (Any): Remaining keyword arguments forwarded to ``op_cls(...)``.

    Returns:
        The freshly-allocated [`MeasurementHandle`][qprogram.MeasurementHandle].

    Raises:
        ValidationError: When ``name`` is empty, not a string, or already used by another
            measurement in the program, or when the operation carries a bus reference from a
            foreign [`BusSchema`][qprogram.BusSchema].

    Example::

        def acquire(self, bus, weights, *, name=None):
            return self._append_measurement(Acquire, bus=bus, weights=weights, name=name)
    """
    allocated = self._program._allocate_measurement_name(bus, requested=name)  # ruff: ignore[private-member-access]
    handle = MeasurementHandle(allocated)
    # MeasurementOperation is a marker base — the cast lets the dynamic constructor go through
    # without ty falling back to the empty object.__init__ signature.
    factory = cast("Callable[..., MeasurementOperation]", op_cls)
    op = factory(bus=bus, handle=handle, **kwargs)
    self._append(op)
    return handle

register_operation

register_operation(
    name: str,
    cls: type[Operation],
    *,
    vendor: str | None = None,
    serialize: OperationSerializeFn | None = None,
    parse: OperationParseFn | None = None,
) -> type[Operation]

Register an operation for .qp serialization.

Re-registering the same class under the same (vendor, name) is allowed (the owner may refresh its callbacks; import-time side-effect modules may run twice). Registering a different class under a taken (vendor, name) raises — silently replacing another package's operation would corrupt every file using that keyword.

Parameters:

  • name (str) –

    Keyword used in .qp (play, measure, ...). Operation names are unrestricted; keyword-led block headers register on the block registry instead.

  • cls (type[Operation]) –

    The Operation subclass.

  • vendor (str | None, default: None ) –

    Vendor namespace. None registers a core op (emitted without prefix). Cannot be "core" or any RESERVED_KEYWORDS.

  • serialize (OperationSerializeFn | None, default: None ) –

    Optional override; default uses signature-driven serialization.

  • parse (OperationParseFn | None, default: None ) –

    Optional override; default uses signature-driven parsing.

Returns:

  • type[Operation]

    cls unchanged, so the call can stand in for the class at the point of registration. A

  • type[Operation]

    bare @register_operation decoration does not work — cls is the second positional

  • type[Operation]

    parameter, not the first.

Raises:

  • ValueError

    If vendor is reserved, or (vendor, name) is already registered to a different class.

Source code in src/qprogram/serialization/registry.py
def register_operation(
    name: str,
    cls: type[Operation],
    *,
    vendor: str | None = None,
    serialize: OperationSerializeFn | None = None,
    parse: OperationParseFn | None = None,
) -> type[Operation]:
    """Register an operation for ``.qp`` serialization.

    Re-registering the **same class** under the same ``(vendor, name)`` is allowed (the owner may
    refresh its callbacks; import-time side-effect modules may run twice). Registering a
    **different class** under a taken ``(vendor, name)`` raises — silently replacing another
    package's operation would corrupt every file using that keyword.

    Args:
        name (str): Keyword used in ``.qp`` (``play``, ``measure``, ...). Operation names are
            unrestricted; keyword-led block headers register on the block registry instead.
        cls (type[Operation]): The [`Operation`][qprogram.operations.Operation] subclass.
        vendor (str | None): Vendor namespace. ``None`` registers a core op (emitted without
            prefix). Cannot be ``"core"`` or any [`RESERVED_KEYWORDS`][qprogram.RESERVED_KEYWORDS].
        serialize (OperationSerializeFn | None): Optional override; default uses signature-driven
            serialization.
        parse (OperationParseFn | None): Optional override; default uses signature-driven parsing.

    Returns:
        ``cls`` unchanged, so the call can stand in for the class at the point of registration. A
        bare ``@register_operation`` decoration does not work — ``cls`` is the second positional
        parameter, not the first.

    Raises:
        ValueError: If ``vendor`` is reserved, or ``(vendor, name)`` is already registered to a
            different class.
    """
    if vendor is not None and vendor in RESERVED_VENDOR_NAMES:
        msg = (
            f"vendor name {vendor!r} is reserved (see qprogram.RESERVED_KEYWORDS "
            f"plus the 'core' sentinel); pick a different namespace for this "
            f"vendor extension"
        )
        raise ValueError(msg)
    existing = _operation_specs_by_qualified.get((vendor, name))
    if existing is not None and existing.cls is not cls:
        qualified = f"{vendor}.{name}" if vendor else name
        msg = (
            f"operation {qualified!r} is already registered to "
            f"{existing.cls.__module__}.{existing.cls.__qualname__}; refusing to replace it "
            f"with {cls.__module__}.{cls.__qualname__}"
        )
        raise ValueError(msg)
    spec = OperationSpec(name=name, vendor=vendor, cls=cls, serialize=serialize, parse=parse)
    _operation_specs_by_qualified[(vendor, name)] = spec
    _operation_specs_by_class[cls] = spec
    return cls

register_vendor_operation

register_vendor_operation(
    vendor: str,
    name: str,
    cls: type[Operation],
    *,
    serialize: OperationSerializeFn | None = None,
    parse: OperationParseFn | None = None,
) -> None

Register an operation under a vendor namespace.

Equivalent to register_operation(name, cls, vendor=vendor, ...) — the spelling vendor extension packages use at import time. The optional serialize / parse callbacks are forwarded unchanged: a vendor shipping a measurement operation passes qprogram.serialization._specs.measurement_op_serialize and qprogram.serialization._specs.make_measurement_op_parse, so the parser produces the canonical handle instance shared with every MeasurementRef naming it.

Parameters:

  • vendor (str) –

    Vendor namespace the operation is emitted under (<vendor>.<name>).

  • name (str) –

    Operation keyword within that namespace.

  • cls (type[Operation]) –

    The Operation subclass.

  • serialize (OperationSerializeFn | None, default: None ) –

    Optional override; default uses signature-driven serialization.

  • parse (OperationParseFn | None, default: None ) –

    Optional override; default uses signature-driven parsing.

Raises:

  • ValueError

    If vendor is reserved, or (vendor, name) is already registered to a different class.

Source code in src/qprogram/serialization/registry.py
def register_vendor_operation(
    vendor: str,
    name: str,
    cls: type[Operation],
    *,
    serialize: OperationSerializeFn | None = None,
    parse: OperationParseFn | None = None,
) -> None:
    """Register an operation under a vendor namespace.

    Equivalent to ``register_operation(name, cls, vendor=vendor, ...)`` — the spelling vendor
    extension packages use at import time. The optional ``serialize`` / ``parse`` callbacks are
    forwarded unchanged: a vendor shipping a measurement operation passes
    `qprogram.serialization._specs.measurement_op_serialize` and
    `qprogram.serialization._specs.make_measurement_op_parse`, so the parser produces the
    canonical handle instance shared with every [`MeasurementRef`][qprogram.MeasurementRef] naming it.

    Args:
        vendor (str): Vendor namespace the operation is emitted under (``<vendor>.<name>``).
        name (str): Operation keyword within that namespace.
        cls (type[Operation]): The [`Operation`][qprogram.operations.Operation] subclass.
        serialize (OperationSerializeFn | None): Optional override; default uses signature-driven
            serialization.
        parse (OperationParseFn | None): Optional override; default uses signature-driven parsing.

    Raises:
        ValueError: If ``vendor`` is reserved, or ``(vendor, name)`` is already registered to a
            different class.
    """
    register_operation(name, cls, vendor=vendor, serialize=serialize, parse=parse)

register_block

register_block(
    name: str,
    cls: type[Block],
    *,
    vendor: str | None = None,
    serialize_header: BlockSerializeHeaderFn | None = None,
    parse_header: BlockParseHeaderFn | None = None,
) -> type[Block]

Register a keyword-led control-flow block for .qp serialization.

The body parser handles indentation and child statements uniformly. Loops are not registered here — they use the sweep-source registry instead. Same-class re-registration is allowed; claiming a taken keyword with a different class raises.

The registry is keyed by the qualified keyword, so a vendor block is looked up by the same dotted token the parser reads off the line (myplatform.infinite_loop) and can never collide with a core keyword.

Parameters:

  • name (str) –

    Leading keyword in the block header (average, block, ...), without any vendor prefix.

  • cls (type[Block]) –

    The Block subclass.

  • vendor (str | None, default: None ) –

    Vendor namespace. None registers a core block (emitted without prefix). Cannot be "core" or any RESERVED_KEYWORDS. Prefer the register_vendor_block wrapper from a vendor package.

  • serialize_header (BlockSerializeHeaderFn | None, default: None ) –

    Optional header-text override.

  • parse_header (BlockParseHeaderFn | None, default: None ) –

    Optional header-token override.

Returns:

  • type[Block]

    cls unchanged, so the call can stand in for the class at the point of registration. A

  • type[Block]

    bare @register_block decoration does not work — cls is the second positional

  • type[Block]

    parameter, not the first.

Raises:

  • ValueError

    If vendor is reserved, or the qualified keyword is already registered to a different class.

Source code in src/qprogram/serialization/registry.py
def register_block(
    name: str,
    cls: type[Block],
    *,
    vendor: str | None = None,
    serialize_header: BlockSerializeHeaderFn | None = None,
    parse_header: BlockParseHeaderFn | None = None,
) -> type[Block]:
    """Register a keyword-led control-flow block for ``.qp`` serialization.

    The body parser handles indentation and child statements uniformly. Loops are not registered
    here — they use the sweep-source registry instead. Same-class re-registration is allowed;
    claiming a taken keyword with a different class raises.

    The registry is keyed by the **qualified** keyword, so a vendor block is looked up by the same
    dotted token the parser reads off the line (``myplatform.infinite_loop``) and can never collide
    with a core keyword.

    Args:
        name (str): Leading keyword in the block header (``average``, ``block``, ...), without any
            vendor prefix.
        cls (type[Block]): The [`Block`][qprogram.blocks.Block] subclass.
        vendor (str | None): Vendor namespace. ``None`` registers a core block (emitted without
            prefix). Cannot be ``"core"`` or any [`RESERVED_KEYWORDS`][qprogram.RESERVED_KEYWORDS]. Prefer the
            `register_vendor_block` wrapper from a vendor package.
        serialize_header (BlockSerializeHeaderFn | None): Optional header-text override.
        parse_header (BlockParseHeaderFn | None): Optional header-token override.

    Returns:
        ``cls`` unchanged, so the call can stand in for the class at the point of registration. A
        bare ``@register_block`` decoration does not work — ``cls`` is the second positional
        parameter, not the first.

    Raises:
        ValueError: If ``vendor`` is reserved, or the qualified keyword is already registered to a
            different class.
    """
    if vendor is not None and vendor in RESERVED_VENDOR_NAMES:
        msg = (
            f"vendor name {vendor!r} is reserved (see qprogram.RESERVED_KEYWORDS "
            f"plus the 'core' sentinel); pick a different namespace for this "
            f"vendor extension"
        )
        raise ValueError(msg)
    spec = BlockSpec(
        name=name,
        cls=cls,
        vendor=vendor,
        serialize_header=serialize_header,
        parse_header=parse_header,
    )
    keyword = spec.qualified_name
    existing = _block_specs_by_name.get(keyword)
    if existing is not None and existing.cls is not cls:
        msg = (
            f"block keyword {keyword!r} is already registered to "
            f"{existing.cls.__module__}.{existing.cls.__qualname__}; refusing to replace it"
        )
        raise ValueError(msg)
    _block_specs_by_name[keyword] = spec
    _block_specs_by_class[cls] = spec
    return cls

register_vendor_block

register_vendor_block(
    vendor: str,
    name: str,
    cls: type[Block],
    *,
    serialize_header: BlockSerializeHeaderFn | None = None,
    parse_header: BlockParseHeaderFn | None = None,
) -> None

Register a vendor control-flow block — the block analogue of register_vendor_operation.

Equivalent to register_block(name, cls, vendor=vendor, ...). The block's wire form becomes <vendor>.<name>: followed by an indented suite, and — because the spec records the vendor — a program containing the block gets a require <vendor> <x.y> line even when it contains no vendor operations.

A block that repeats its body should also set REPEATS True on the class so it counts toward max_loop_nesting.

Parameters:

  • vendor (str) –

    Vendor namespace the block header is emitted under (<vendor>.<name>:).

  • name (str) –

    Header keyword within that namespace.

  • cls (type[Block]) –

    The Block subclass.

  • serialize_header (BlockSerializeHeaderFn | None, default: None ) –

    Optional header-text override.

  • parse_header (BlockParseHeaderFn | None, default: None ) –

    Optional header-token override.

Raises:

  • ValueError

    If vendor is reserved, or the qualified keyword is already registered to a different class.

Source code in src/qprogram/serialization/registry.py
def register_vendor_block(
    vendor: str,
    name: str,
    cls: type[Block],
    *,
    serialize_header: BlockSerializeHeaderFn | None = None,
    parse_header: BlockParseHeaderFn | None = None,
) -> None:
    """Register a vendor control-flow block — the block analogue of `register_vendor_operation`.

    Equivalent to ``register_block(name, cls, vendor=vendor, ...)``. The block's wire form becomes
    ``<vendor>.<name>:`` followed by an indented suite, and — because the spec records the vendor —
    a program containing the block gets a ``require <vendor> <x.y>`` line even when it contains no
    vendor *operations*.

    A block that repeats its body should also set `REPEATS` ``True`` on
    the class so it counts toward ``max_loop_nesting``.

    Args:
        vendor (str): Vendor namespace the block header is emitted under (``<vendor>.<name>:``).
        name (str): Header keyword within that namespace.
        cls (type[Block]): The [`Block`][qprogram.blocks.Block] subclass.
        serialize_header (BlockSerializeHeaderFn | None): Optional header-text override.
        parse_header (BlockParseHeaderFn | None): Optional header-token override.

    Raises:
        ValueError: If ``vendor`` is reserved, or the qualified keyword is already registered to a
            different class.
    """
    register_block(name, cls, vendor=vendor, serialize_header=serialize_header, parse_header=parse_header)

register_vendor_version

register_vendor_version(vendor: str, version: str) -> None

Record the protocol version of an installed vendor extension.

Parameters:

  • vendor (str) –

    Vendor name as used in the dot-notation operations. Cannot be "core" or any RESERVED_KEYWORDS.

  • version (str) –

    Semver string with at least major.minor integer components. Major.minor governs compatibility; patch is informational.

Raises:

  • ValueError

    If vendor is reserved or version does not parse as major.minor.

Source code in src/qprogram/serialization/registry.py
def register_vendor_version(vendor: str, version: str) -> None:
    """Record the protocol version of an installed vendor extension.

    Args:
        vendor (str): Vendor name as used in the dot-notation operations. Cannot be ``"core"`` or
            any [`RESERVED_KEYWORDS`][qprogram.RESERVED_KEYWORDS].
        version (str): Semver string with at least ``major.minor`` integer components. Major.minor
            governs compatibility; patch is informational.

    Raises:
        ValueError: If ``vendor`` is reserved or ``version`` does not parse as ``major.minor``.
    """
    if vendor in RESERVED_VENDOR_NAMES:
        msg = (
            f"vendor name {vendor!r} is reserved (see qprogram.RESERVED_KEYWORDS plus the "
            f"'core' sentinel); pick a different namespace for this vendor extension"
        )
        raise ValueError(msg)
    parts = version.split(".")
    minimum_parts = 2
    if len(parts) < minimum_parts:
        msg = f"vendor version {version!r} must have at least major.minor components"
        raise ValueError(msg)
    try:
        _major, _minor = int(parts[0]), int(parts[1])
    except ValueError as e:
        msg = f"vendor version {version!r} has non-integer major/minor components"
        raise ValueError(msg) from e
    _vendor_versions[vendor] = version

register_waveform

register_waveform(cls: type[Waveform | IQWaveform]) -> type

Register a waveform class for .qp serialization, keyed by its class name.

Same-class re-registration is a no-op; registering a different class under an already-taken name raises — it would silently change how every existing file parses that constructor.

Parameters:

  • cls (type[Waveform | IQWaveform]) –

    Waveform class to register. Its __name__ is the constructor name on the wire.

Returns:

  • type

    cls, so the function can be used as a decorator.

Raises:

  • ValueError

    If cls.__name__ is already registered to a different class.

Source code in src/qprogram/serialization/registry.py
def register_waveform(cls: type[Waveform | IQWaveform]) -> type:
    """Register a waveform class for ``.qp`` serialization, keyed by its class name.

    Same-class re-registration is a no-op; registering a different class under an already-taken
    name raises — it would silently change how every existing file parses that constructor.

    Args:
        cls (type[Waveform | IQWaveform]): Waveform class to register. Its ``__name__`` is the
            constructor name on the wire.

    Returns:
        ``cls``, so the function can be used as a decorator.

    Raises:
        ValueError: If ``cls.__name__`` is already registered to a different class.
    """
    existing = _waveform_registry.get(cls.__name__)
    if existing is not None and existing is not cls:
        msg = (
            f"waveform name {cls.__name__!r} is already registered to "
            f"{existing.__module__}.{existing.__qualname__}; rename the class or unregister first"
        )
        raise ValueError(msg)
    _waveform_registry[cls.__name__] = cls
    return cls

try_activate_vendor

try_activate_vendor(vendor: str) -> bool

Ensure vendor is registered, importing its extension package on demand if needed.

Importing the entry-point target runs the package's registration side effects (register_vendor / register_vendor_version / register_vendor_operation / register_profile). Python caches imports, so repeat calls are cheap and idempotent.

Parameters:

  • vendor (str) –

    Vendor namespace, e.g. "qblox".

Returns:

  • bool

    True when the vendor is registered after the call — either it already was, or its

  • bool

    qprogram.vendors entry point was found and imported successfully. False when no

  • bool

    installed package claims vendor; the caller decides whether that is an error.

Raises:

  • VendorActivationError

    If an entry point claims vendor but its import raises, or it imports without registering a protocol version (a packaging bug in the extension).

Source code in src/qprogram/serialization/registry.py
def try_activate_vendor(vendor: str) -> bool:
    """Ensure ``vendor`` is registered, importing its extension package on demand if needed.

    Importing the entry-point target runs the package's registration side effects
    (``register_vendor`` / ``register_vendor_version`` / ``register_vendor_operation`` /
    ``register_profile``). Python caches imports, so repeat calls are cheap and idempotent.

    Args:
        vendor (str): Vendor namespace, e.g. ``"qblox"``.

    Returns:
        ``True`` when the vendor is registered after the call — either it already was, or its
        ``qprogram.vendors`` entry point was found and imported successfully. ``False`` when no
        installed package claims ``vendor``; the caller decides whether that is an error.

    Raises:
        VendorActivationError: If an entry point claims ``vendor`` but its import raises, or it
            imports without registering a protocol version (a packaging bug in the extension).
    """
    if get_vendor_version(vendor) is not None:
        return True
    ep = _vendor_entry_points().get(vendor)
    if ep is None:
        return False
    try:
        ep.load()
    except Exception as e:
        # collapse any import-time failure into one clear error
        msg = (
            f"vendor extension for {vendor!r} is installed (entry point {ep.value!r}) but failed "
            f"to import: {type(e).__name__}: {e}"
        )
        raise VendorActivationError(msg) from e
    if get_vendor_version(vendor) is None:
        msg = (
            f"vendor extension for {vendor!r} imported from entry point {ep.value!r} but did not "
            f"register a protocol version; the package must call "
            f"register_vendor_version({vendor!r}, '<x.y.z>') on import"
        )
        raise VendorActivationError(msg)
    return True

OperationSpec dataclass

OperationSpec(
    name: str,
    vendor: str | None,
    cls: type[Operation],
    serialize: OperationSerializeFn | None = None,
    parse: OperationParseFn | None = None,
)

Serialization metadata for one operation, core or vendor.

Attributes:

  • name (str) –

    Operation name as it appears in .qp.

  • vendor (str | None) –

    Vendor name for the dot-prefix (<vendor>.<name>), or None for core.

  • cls (type[Operation]) –

    The Operation subclass.

  • serialize (OperationSerializeFn | None) –

    Optional override; default uses inspect.signature-driven serialization.

  • parse (OperationParseFn | None) –

    Optional override; default uses inspect.signature-driven parsing.

qualified_name property

qualified_name: str

The keyword this operation is written as.

A core operation keeps its bare name; a vendor operation is dotted as <vendor>.<name>.

BlockSpec dataclass

BlockSpec(
    name: str,
    cls: type[Block],
    vendor: str | None = None,
    serialize_header: BlockSerializeHeaderFn | None = None,
    parse_header: BlockParseHeaderFn | None = None,
)

Serialization metadata for a keyword-led control-flow block, core or vendor.

Sweeps are not registered here — for <var> in <Source>(...) is driven by the sweep-source registry (register_sweep_source) instead.

Attributes:

  • name (str) –

    Header keyword as it appears in .qp (e.g. "average", "block"), without any vendor prefix.

  • cls (type[Block]) –

    The Block subclass.

  • vendor (str | None) –

    Vendor name for the dot-prefix (<vendor>.<name>), or None for a core block. Mirrors OperationSpec.vendor, and is what lets the writer emit a require line for a file whose only vendor content is a block.

  • serialize_header (BlockSerializeHeaderFn | None) –

    Optional override returning the header text without the trailing : (e.g. "average 1000"). Default emits the bare (qualified) keyword.

  • parse_header (BlockParseHeaderFn | None) –

    Optional override taking the post-keyword tokens; default invokes cls() with no arguments.

qualified_name property

qualified_name: str

The keyword this block header is written as.

A core block keeps its bare name; a vendor block is dotted as <vendor>.<name>, which is also the key the block registry stores it under.

Serialization

dumps and save write .qp; loads and load read it. Those two readers and ParseError are resolved by the package's module-level __getattr__ on first access rather than at import time, because the parser imports QProgram and importing them eagerly would close a cycle. Nothing about that shows at the call site: qp.loads is read off the package like any other attribute.

dumps

dumps(program: QProgram) -> str

Serialize a QProgram to a .qp-format string.

Parameters:

  • program (QProgram) –

    Program to serialize.

Returns:

  • str

    The full .qp text: header, require lines, metadata, schema, fragments, body.

Raises:

  • SerializationError

    If the program contains a node or value the format cannot represent faithfully (unregistered operation/block class, vendor without a registered version, attribute value of an unsupported type), or if program is itself a Fragment — fragments serialize as sections of the host program that calls them. The writer never emits lossy output.

Source code in src/qprogram/serialization/writer.py
def dumps(program: QProgram) -> str:
    """Serialize a [`QProgram`][qprogram.QProgram] to a ``.qp``-format string.

    Args:
        program (QProgram): Program to serialize.

    Returns:
        The full ``.qp`` text: header, ``require`` lines, metadata, schema, fragments, body.

    Raises:
        SerializationError: If the program contains a node or value the format cannot represent
            faithfully (unregistered operation/block class, vendor without a registered version,
            attribute value of an unsupported type), or if ``program`` is itself a
            [`Fragment`][qprogram.Fragment] — fragments serialize as sections of the host program
            that calls them. The writer never emits lossy output.
    """
    from qprogram.fragments import Fragment  # ruff: ignore[import-outside-top-level]

    if isinstance(program, Fragment):
        msg = (
            f"cannot serialize Fragment {program.name!r} directly; fragments are emitted as "
            f"`fragment ...:` sections of the host QProgram that calls them — serialize that program"
        )
        raise SerializationError(msg)
    return _Writer(program).dump()

save

save(program: QProgram, path: str) -> None

Serialize program and write the result to path.

.qp files are always UTF-8, independent of the platform's locale.

Parameters:

  • program (QProgram) –

    Program to serialize.

  • path (str) –

    Destination file path.

Raises:

Source code in src/qprogram/serialization/writer.py
def save(program: QProgram, path: str) -> None:
    """Serialize ``program`` and write the result to ``path``.

    ``.qp`` files are always UTF-8, independent of the platform's locale.

    Args:
        program (QProgram): Program to serialize.
        path (str): Destination file path.

    Raises:
        SerializationError: See `dumps`.
    """
    Path(path).write_text(dumps(program), encoding="utf-8")

loads

loads(text: str, *, auto_activate: bool = True) -> QProgram

Parse a .qp-format string into a QProgram.

Parameters:

  • text (str) –

    The .qp document to parse.

  • auto_activate (bool, default: True ) –

    Whether a require <vendor> line whose extension is not imported yet triggers entry-point discovery (the qprogram.vendors group) — the installed package is imported on demand so the file is self-contained. Set False to require that vendors be imported explicitly beforehand (no implicit imports).

Returns:

Raises:

  • ParseError

    On malformed input, unknown registry entries, or a required vendor that is neither registered nor discoverable (and, when its extension is installed but broken, the wrapped VendorActivationError).

  • ValidationError

    If a declaration the grammar accepts is rejected by the program it builds, such as a variable id that is a reserved .qp keyword.

  • TypeError

    If a constructor call in the file does not fit its class's signature — an inline waveform, or a sweep source nested inside a combinator's argument list.

Source code in src/qprogram/serialization/parser.py
def loads(text: str, *, auto_activate: bool = True) -> QProgram:
    """Parse a ``.qp``-format string into a [`QProgram`][qprogram.QProgram].

    Args:
        text (str): The ``.qp`` document to parse.
        auto_activate (bool, optional): Whether a ``require <vendor>`` line whose extension is not
            imported yet triggers entry-point discovery (the ``qprogram.vendors`` group) — the
            installed package is imported on demand so the file is self-contained. Set ``False``
            to require that vendors be imported explicitly beforehand (no implicit imports).

    Returns:
        The reconstructed [`QProgram`][qprogram.QProgram], with its source map populated.

    Raises:
        ParseError: On malformed input, unknown registry entries, or a required vendor that is
            neither registered nor discoverable (and, when its extension is installed but broken,
            the wrapped [`VendorActivationError`][qprogram.VendorActivationError]).
        ValidationError: If a declaration the grammar accepts is rejected by the program it builds,
            such as a variable id that is a reserved ``.qp`` keyword.
        TypeError: If a constructor call in the file does not fit its class's signature — an inline
            waveform, or a sweep source nested inside a combinator's argument list.
    """
    return _Parser(text, auto_activate=auto_activate).parse()

load

load(path: str, *, auto_activate: bool = True) -> QProgram

Read a .qp file and parse it into a QProgram.

.qp files are always UTF-8, independent of the platform's locale.

Parameters:

  • path (str) –

    Path to the .qp file.

  • auto_activate (bool, default: True ) –

    See loads.

Returns:

Raises:

  • ParseError

    On malformed input or unknown registry entries.

  • OSError

    If the file cannot be read.

  • ValidationError

    If a declaration the grammar accepts is rejected by the program it builds, such as a variable id that is a reserved .qp keyword.

  • TypeError

    If a constructor call in the file does not fit its class's signature — an inline waveform, or a sweep source nested inside a combinator's argument list.

Source code in src/qprogram/serialization/parser.py
def load(path: str, *, auto_activate: bool = True) -> QProgram:
    """Read a ``.qp`` file and parse it into a [`QProgram`][qprogram.QProgram].

    ``.qp`` files are always UTF-8, independent of the platform's locale.

    Args:
        path (str): Path to the ``.qp`` file.
        auto_activate (bool, optional): See `loads`.

    Returns:
        The reconstructed [`QProgram`][qprogram.QProgram].

    Raises:
        ParseError: On malformed input or unknown registry entries.
        OSError: If the file cannot be read.
        ValidationError: If a declaration the grammar accepts is rejected by the program it builds,
            such as a variable id that is a reserved ``.qp`` keyword.
        TypeError: If a constructor call in the file does not fit its class's signature — an inline
            waveform, or a sweep source nested inside a combinator's argument list.
    """
    return loads(Path(path).read_text(encoding="utf-8"), auto_activate=auto_activate)

Platform protocol

PlatformProtocol

Bases: ABC

Abstract interface that execution platforms must implement.

Splits into resource discovery (get_*) and capability + execution (capabilities, validate, plan, explain, execute). The convention is that execute calls validate first, raises UnsupportedOperationError on any severity="error" diagnostic, and surfaces "warning" / "info" diagnostics without raising — concrete platforms aren't forced to follow it, but skipping the check means cryptic compiler errors in place of structured diagnostics.

capabilities abstractmethod property

capabilities: PlatformCapabilities

The capability descriptor for this platform.

A PlatformCapabilities carries per-(element, bus_kind) bus profiles, a platform-level profile (block / expression / bus-less ops), and a default-bus-profile fallback for raw-string buses. Each slot is a BusCapabilities with rt / host halves. Users introspect this to know what the platform supports; the validator consumes the same object.

get_bus_schema abstractmethod

get_bus_schema() -> BusSchema

Return the BusSchema for this platform's chip.

Returns:

  • BusSchema

    The schema naming the chip's elements and the bus kinds each one exposes.

Source code in src/qprogram/platform.py
@abstractmethod
def get_bus_schema(self) -> BusSchema:
    """Return the [`BusSchema`][qprogram.BusSchema] for this platform's chip.

    Returns:
        The schema naming the chip's elements and the bus kinds each one exposes.
    """
    ...

get_buses abstractmethod

get_buses() -> list[str]

Return the names of every bus this platform exposes.

Returns:

  • list[str]

    Every bus name, spelled the way a program would reference it.

Source code in src/qprogram/platform.py
@abstractmethod
def get_buses(self) -> list[str]:
    """Return the names of every bus this platform exposes.

    Returns:
        Every bus name, spelled the way a program would reference it.
    """
    ...

get_parameters abstractmethod

get_parameters(bus: str) -> list[str]

Return the parameter names supported on bus.

Parameters:

  • bus (str) –

    Bus whose parameters to list.

Returns:

  • list[str]

    The parameter names set_parameter / get_parameter accept for that bus.

Source code in src/qprogram/platform.py
@abstractmethod
def get_parameters(self, bus: str) -> list[str]:
    """Return the parameter names supported on ``bus``.

    Args:
        bus (str): Bus whose parameters to list.

    Returns:
        The parameter names ``set_parameter`` / ``get_parameter`` accept for that bus.
    """
    ...

get_global_parameters abstractmethod

get_global_parameters() -> list[str]

Return the parameter names that are not bound to any specific bus.

Returns:

  • list[str]

    The platform-wide parameter names.

Source code in src/qprogram/platform.py
@abstractmethod
def get_global_parameters(self) -> list[str]:
    """Return the parameter names that are not bound to any specific bus.

    Returns:
        The platform-wide parameter names.
    """
    ...

validate

validate(qprogram: QProgram) -> list[Diagnostic]

Validate a program against this platform's capabilities.

Default delegates to qprogram.validation.validate and discards the execution-plan half of the return value. Platforms may override to prepend device-specific predicates or short-circuit on the first error.

Parameters:

  • qprogram (QProgram) –

    Program to validate.

Returns:

  • list[Diagnostic]

    List of diagnostics; empty when the program is fully supported with no forced-host

  • list[Diagnostic]

    fallback events.

Source code in src/qprogram/platform.py
def validate(self, qprogram: QProgram) -> list[Diagnostic]:
    """Validate a program against this platform's capabilities.

    Default delegates to [`qprogram.validation.validate`][qprogram.validate] and discards the execution-plan
    half of the return value. Platforms may override to prepend device-specific predicates or
    short-circuit on the first error.

    Args:
        qprogram (QProgram): Program to validate.

    Returns:
        List of diagnostics; empty when the program is fully supported with no forced-host
        fallback events.
    """
    from qprogram.validation import validate as _validate  # ruff: ignore[import-outside-top-level]

    diagnostics, _ = _validate(qprogram, self.capabilities)
    return diagnostics

plan

plan(qprogram: QProgram) -> ExecutionPlan

Return the execution-domain plan for qprogram.

Default delegates to qprogram.validation.validate and discards the diagnostic half. Callers who want both — e.g. execute() implementations that gate on diagnostics and then compile against the plan — should call qprogram.validation.validate directly to avoid the duplicated walk.

Parameters:

  • qprogram (QProgram) –

    Program to classify.

Returns:

  • ExecutionPlan

    Mapping from each AST node to its final domain set.

Source code in src/qprogram/platform.py
def plan(self, qprogram: QProgram) -> ExecutionPlan:
    """Return the execution-domain plan for ``qprogram``.

    Default delegates to [`qprogram.validation.validate`][qprogram.validate] and discards the diagnostic half.
    Callers who want both — e.g. ``execute()`` implementations that gate on diagnostics and then
    compile against the plan — should call [`qprogram.validation.validate`][qprogram.validate] directly to
    avoid the duplicated walk.

    Args:
        qprogram (QProgram): Program to classify.

    Returns:
        Mapping from each AST node to its final domain set.
    """
    from qprogram.validation import validate as _validate  # ruff: ignore[import-outside-top-level]

    _, plan = _validate(qprogram, self.capabilities)
    return plan

explain

explain(qprogram: QProgram) -> str

Render the execution plan for qprogram as a human-readable tree.

Default delegates to qprogram.explain: every body node is shown as its .qp text with the domain set it will execute in ([rt|host] / [rt] / [host] / [--]), with errors, warnings (notably forced-host and its reasons), and info annotated inline. Programs with fragment calls are expanded first.

Parameters:

  • qprogram (QProgram) –

    Program to classify and render.

Returns:

  • str

    The rendered tree.

Source code in src/qprogram/platform.py
def explain(self, qprogram: QProgram) -> str:
    """Render the execution plan for ``qprogram`` as a human-readable tree.

    Default delegates to `qprogram.explain`: every body node is shown as its ``.qp``
    text with the domain set it will execute in (``[rt|host]`` / ``[rt]`` / ``[host]`` /
    ``[--]``), with errors, warnings (notably ``forced-host`` and its reasons), and info
    annotated inline. Programs with fragment calls are expanded first.

    Args:
        qprogram (QProgram): Program to classify and render.

    Returns:
        The rendered tree.
    """
    from qprogram.explain import explain as _explain  # ruff: ignore[import-outside-top-level]

    return _explain(qprogram, self.capabilities)

execute abstractmethod

execute(qprogram: QProgram) -> QProgramResult

Execute a program and return its results.

By convention the implementation calls validate first and raises UnsupportedOperationError on any severity="error" diagnostic.

Parameters:

  • qprogram (QProgram) –

    Program to run.

Returns:

Source code in src/qprogram/platform.py
@abstractmethod
def execute(self, qprogram: QProgram) -> QProgramResult:
    """Execute a program and return its results.

    By convention the implementation calls [`validate`][qprogram.validate] first and raises
    [`UnsupportedOperationError`][qprogram.UnsupportedOperationError] on any ``severity="error"`` diagnostic.

    Args:
        qprogram (QProgram): Program to run.

    Returns:
        One record per measurement in the program.
    """
    ...

stream

stream(
    qprogram: QProgram, **kwargs
) -> Iterator[QProgramResult]

Execute and yield partial results as they become available.

Optional — the default raises NotImplementedError. Platforms that don't support streaming can leave this alone.

Parameters:

  • qprogram (QProgram) –

    Program to run.

  • **kwargs (Any, default: {} ) –

    Platform-specific streaming options.

Raises:

  • NotImplementedError

    Always, in the default implementation.

Source code in src/qprogram/platform.py
def stream(self, qprogram: QProgram, **kwargs) -> Iterator[QProgramResult]:
    """Execute and yield partial results as they become available.

    Optional — the default raises `NotImplementedError`. Platforms that don't support
    streaming can leave this alone.

    Args:
        qprogram (QProgram): Program to run.
        **kwargs (Any): Platform-specific streaming options.

    Raises:
        NotImplementedError: Always, in the default implementation.
    """
    msg = "Streaming not supported by this platform"
    raise NotImplementedError(msg)

Reference platform

The software platform in this repository. It validates a program, interprets the AST, and returns xarray.DataArray results with one dimension per enclosing sweep (a Parallel composition contributing one shared dimension) and none for an averaging block, and it is the semantics a vendor compiler is tested against: an error diagnostic becomes UnsupportedOperationError, a warning is raised through warnings.warn with category ExecutionWarning, and info diagnostics pass silently. simulate(program) is the one-call form, running the program on a throwaway platform whose measurement model defaults to a deterministic, all-zero MockMeasurementModel. See Running programs for the walkthrough.

simulate

simulate(
    program: QProgram,
    *,
    model: MeasurementModel | None = None,
    schema: BusSchema | None = None,
    parameters: dict[str, float] | None = None,
) -> QProgramResult

Execute program on a one-off ReferencePlatform — the quickest path to results.

The program should already be concrete; resolve any string waveform names first with program.with_waveforms(library) (or library.apply(program)).

Parameters:

  • program (QProgram) –

    Program to run.

  • model (MeasurementModel | None, default: None ) –

    Measurement model; None uses a deterministic, all-zero MockMeasurementModel.

  • schema (BusSchema | None, default: None ) –

    Bus schema for the throwaway platform.

  • parameters (dict[str, float] | None, default: None ) –

    Initial parameter store, keyed "bus.parameter". Copied, so the caller's dict is left untouched.

Returns:

Raises:

  • UnsupportedOperationError

    When the program uses something the reference platform cannot run.

  • UnassignedVariableError

    When an operation's expression references a variable no enclosing loop binds.

  • ValueError

    When a measurement requests raw and the model returns a trace whose shape is not (raw_samples, 2).

Source code in src/qprogram/executor.py
def simulate(
    program: QProgram,
    *,
    model: MeasurementModel | None = None,
    schema: BusSchema | None = None,
    parameters: dict[str, float] | None = None,
) -> QProgramResult:
    """Execute ``program`` on a one-off [`ReferencePlatform`][qprogram.ReferencePlatform] — the quickest path to results.

    The program should already be concrete; resolve any string waveform names first with
    ``program.with_waveforms(library)`` (or ``library.apply(program)``).

    Args:
        program (QProgram): Program to run.
        model (MeasurementModel | None): Measurement model; ``None`` uses a deterministic, all-zero
            [`MockMeasurementModel`][qprogram.MockMeasurementModel].
        schema (BusSchema | None): Bus schema for the throwaway platform.
        parameters (dict[str, float] | None): Initial parameter store, keyed ``"bus.parameter"``.
            Copied, so the caller's dict is left untouched.

    Returns:
        One record per measurement in the program.

    Raises:
        UnsupportedOperationError: When the program uses something the reference platform cannot
            run.
        UnassignedVariableError: When an operation's expression references a variable no enclosing
            loop binds.
        ValueError: When a measurement requests ``raw`` and the model returns a trace whose shape is
            not ``(raw_samples, 2)``.
    """
    return ReferencePlatform(schema=schema, model=model, parameters=parameters).execute(program)

ReferencePlatform

ReferencePlatform(
    schema: BusSchema | None = None,
    model: MeasurementModel | None = None,
    parameters: dict[str, float] | None = None,
    vendor_op_handlers: Mapping[
        type[Operation], VendorOpHandler
    ]
    | None = None,
)

Bases: PlatformProtocol

The in-tree software platform: validates, interprets, and returns real result xarrays.

Follows the documented convention exactly: execute raises UnsupportedOperationError on any error diagnostic, surfaces warnings via warnings (category ExecutionWarning), and passes info through silently. Fragment calls are expanded before execution. This is the reference semantics vendor compilers are tested against.

Parameters:

  • schema (BusSchema | None, default: None ) –

    Bus schema reported by get_bus_schema. None makes that method raise and get_buses return nothing.

  • model (MeasurementModel | None, default: None ) –

    Measurement model; None builds a fresh MockMeasurementModel (all-zero response, no noise, ground state).

  • parameters (dict[str, float] | None, default: None ) –

    Initial platform parameter store, keyed "bus.parameter". Copied once, then that copy is read by get_parameter, written by set_parameter, and exposed to the model — so a run's writes persist across calls to execute on the same platform.

  • vendor_op_handlers (Mapping[type[Operation], VendorOpHandler] | None, default: None ) –

    Map of vendor Operation class to a VendorOpHandler invoked when that op executes — the seam a platform uses to give its own vendor ops runtime effects on the parameter store (a vendor's set_parameter / get_parameter operations, which target an alias rather than a bus). Ops without a handler execute generically (expressions evaluated, then no-op).

Source code in src/qprogram/executor.py
def __init__(
    self,
    schema: BusSchema | None = None,
    model: MeasurementModel | None = None,
    parameters: dict[str, float] | None = None,
    vendor_op_handlers: Mapping[type[Operation], VendorOpHandler] | None = None,
) -> None:
    self._schema = schema
    self._model: MeasurementModel = model if model is not None else MockMeasurementModel()
    self.parameters: dict[str, float] = dict(parameters or {})
    self._vendor_op_handlers: dict[type[Operation], VendorOpHandler] = dict(vendor_op_handlers or {})

capabilities property

capabilities: PlatformCapabilities

The permissive descriptor built by reference_capabilities, recomputed per access.

Recomputing is what lets a vendor extension imported after the platform was constructed have its tokens honored.

get_bus_schema

get_bus_schema() -> BusSchema

Return the configured schema.

Returns:

  • BusSchema

    The schema this platform was constructed with.

Raises:

  • ValueError

    When the platform was built without one.

Source code in src/qprogram/executor.py
def get_bus_schema(self) -> BusSchema:
    """Return the configured schema.

    Returns:
        The schema this platform was constructed with.

    Raises:
        ValueError: When the platform was built without one.
    """
    if self._schema is None:
        msg = "this ReferencePlatform was created without a BusSchema"
        raise ValueError(msg)
    return self._schema

get_buses

get_buses() -> list[str]

Return the schema's bus names, or an empty list without a schema.

Returns:

  • list[str]

    One name per (element, bus kind) pair the schema declares, with the index position

  • list[str]

    left as * because the schema names kinds rather than enumerating indices.

Source code in src/qprogram/executor.py
def get_buses(self) -> list[str]:
    """Return the schema's bus names, or an empty list without a schema.

    Returns:
        One name per ``(element, bus kind)`` pair the schema declares, with the index position
        left as ``*`` because the schema names kinds rather than enumerating indices.
    """
    if self._schema is None:
        return []
    return [
        self._schema.naming.pattern.format(element=element, index="*", kind=kind)
        for element, element_schema in self._schema.elements.items()
        for kind in element_schema.buses
    ]

get_parameters

get_parameters(bus: str) -> list[str]

Return parameter names stored under bus (keys shaped "bus.parameter").

Parameters:

  • bus (str) –

    Bus whose parameters to list.

Returns:

  • list[str]

    The parameter names currently present in the store for that bus. The store grows as

  • list[str]

    set_parameter runs, so this reflects what has been set, not what the bus accepts.

Source code in src/qprogram/executor.py
def get_parameters(self, bus: str) -> list[str]:
    """Return parameter names stored under ``bus`` (keys shaped ``"bus.parameter"``).

    Args:
        bus (str): Bus whose parameters to list.

    Returns:
        The parameter names currently present in the store for that bus. The store grows as
        ``set_parameter`` runs, so this reflects what has been set, not what the bus accepts.
    """
    return [key.split(".", 1)[1] for key in self.parameters if key.split(".", 1)[0] == bus]

get_global_parameters

get_global_parameters() -> list[str]

Return every known bus.parameter key.

Returns:

  • list[str]

    The store's keys, sorted. These are fully qualified, not the bus-less parameters the

  • list[str]

    name might suggest — the reference platform keeps one flat store.

Source code in src/qprogram/executor.py
def get_global_parameters(self) -> list[str]:
    """Return every known ``bus.parameter`` key.

    Returns:
        The store's keys, sorted. These are fully qualified, not the bus-less parameters the
        name might suggest — the reference platform keeps one flat store.
    """
    return sorted(self.parameters)

execute

execute(
    qprogram: QProgram, **kwargs: object
) -> QProgramResult

Validate and run qprogram, returning its QProgramResult.

Warning-severity diagnostics are re-emitted through warnings as ExecutionWarning and do not stop the run; info-severity ones are dropped.

Parameters:

  • qprogram (QProgram) –

    Program to run. Fragment calls are expanded first, on a copy.

  • **kwargs (object, default: {} ) –

    Accepted and ignored, so callers can pass the platform-specific options a real back-end would take.

Returns:

Raises:

  • UnsupportedOperationError

    When validation produces any severity="error" diagnostic (all of them are listed in the message).

  • UnassignedVariableError

    When an operation's expression references a variable no enclosing loop binds.

  • ValueError

    When a measurement requests raw and the model returns a trace whose shape is not (raw_samples, 2).

Source code in src/qprogram/executor.py
def execute(self, qprogram: QProgram, **kwargs: object) -> QProgramResult:  # ruff: ignore[unused-method-argument]
    """Validate and run ``qprogram``, returning its [`QProgramResult`][qprogram.QProgramResult].

    Warning-severity diagnostics are re-emitted through `warnings` as
    [`ExecutionWarning`][qprogram.ExecutionWarning] and do not stop the run; info-severity ones are dropped.

    Args:
        qprogram (QProgram): Program to run. Fragment calls are expanded first, on a copy.
        **kwargs (object): Accepted and ignored, so callers can pass the platform-specific
            options a real back-end would take.

    Returns:
        One record per measurement in the program.

    Raises:
        UnsupportedOperationError: When validation produces any ``severity="error"``
            diagnostic (all of them are listed in the message).
        UnassignedVariableError: When an operation's expression references a variable no
            enclosing loop binds.
        ValueError: When a measurement requests ``raw`` and the model returns a trace whose
            shape is not ``(raw_samples, 2)``.
    """
    if qprogram.fragments:
        qprogram = qprogram.expand()
    diagnostics, _plan = validate(qprogram, self.capabilities)
    errors = [d for d in diagnostics if d.severity == "error"]
    if errors:
        msg = "program is not executable on the reference platform:\n" + "\n".join(str(d) for d in errors)
        raise UnsupportedOperationError(msg)
    for diag in diagnostics:
        if diag.severity == "warning":
            warnings.warn(str(diag), ExecutionWarning, stacklevel=2)
    return _Interpreter(qprogram, self._model, self.parameters, self._vendor_op_handlers).run()

reference_capabilities

reference_capabilities() -> PlatformCapabilities

Build the reference platform's permissive capability descriptor.

Every token in the live CAPABILITY_REGISTRY is supported — core and vendor tokens (the reference executor runs vendor operations generically, so importing a vendor extension makes its programs executable here). Computed fresh on each call so late-registered vendor tokens are picked up. Each bus slot supports everything host-side while its rt half excludes the bus-scoped parameter ops (set_parameter / get_parameter), so those stay host-side-only on every bus — mirroring real platforms, so plans, forced-host warnings, and explain are meaningful against the reference platform too.

Returns:

Source code in src/qprogram/executor.py
def reference_capabilities() -> PlatformCapabilities:
    """Build the reference platform's permissive capability descriptor.

    Every token in the live `CAPABILITY_REGISTRY` is supported — core
    *and* vendor tokens (the reference executor runs vendor operations generically, so importing
    a vendor extension makes its programs executable here). Computed fresh on each call so
    late-registered vendor tokens are picked up. Each bus slot supports everything host-side while its
    **rt half excludes the bus-scoped parameter ops** (``set_parameter`` / ``get_parameter``), so those
    stay host-side-only on every bus — mirroring real platforms, so plans, ``forced-host`` warnings,
    and `explain` are meaningful against the reference platform too.

    Returns:
        A descriptor with an empty per-bus map, a platform slot for blocks and expressions, and a
        default bus profile every bus falls back to.
    """
    from qprogram.protocol import CAPABILITY_REGISTRY  # ruff: ignore[import-outside-top-level] — live, mutable registry

    tokens = frozenset(CAPABILITY_REGISTRY)

    def cc(profile: str, capability_tokens: frozenset[str]) -> CompilerCapabilities:
        return CompilerCapabilities(
            profile=profile,
            version=(0, 1, 0),
            capabilities=capability_tokens,
            limits={},
            predicates=(_swept_parameter_forces_host,),
            vendor_versions={},
        )

    # Parameter ops are bus-scoped but host-side-only: present in each bus slot's host half, absent
    # from its rt half. The platform slot (blocks / expressions) never carries them.
    bus_slot = BusCapabilities(
        rt=cc("qprogram-reference-bus", tokens - _PARAM_OPS),
        host=cc("qprogram-reference-bus", tokens),
    )
    platform_slot = BusCapabilities(
        rt=cc("qprogram-reference-platform", tokens - _PARAM_OPS),
        host=cc("qprogram-reference-platform", tokens - _PARAM_OPS),
    )
    return PlatformCapabilities(bus={}, platform=platform_slot, default_bus_profile=bus_slot)

MeasurementModel

Bases: Protocol

What the executor asks of a measurement back-end — one sample per shot.

env carries the currently bound loop variables (by id) and the platform parameters (by "bus.parameter"), so a model can shape its response as a function of the sweep.

A model that simulates an ADC also declares raw_samples, an int the executor reads once at the start of a run (default 16), and returns a raw trace of shape (raw_samples, 2) from every sample. One that does not can leave MeasurementSample.raw at its default; a measurement requesting MeasurementField.RAW then raises rather than accumulating a broadcast of the wrong trace.

sample

sample(
    bus: str, env: Mapping[str, float]
) -> MeasurementSample

Return one shot's outcomes for a measurement on bus under env.

Parameters:

  • bus (str) –

    The bus the measurement runs on, as a plain string ("" for a measurement op with no bus attribute).

  • env (Mapping[str, float]) –

    Bound loop variables by id, plus platform parameters keyed "bus.parameter".

Returns:

Source code in src/qprogram/executor.py
def sample(self, bus: str, env: Mapping[str, float]) -> MeasurementSample:
    """Return one shot's outcomes for a measurement on ``bus`` under ``env``.

    Args:
        bus (str): The bus the measurement runs on, as a plain string (``""`` for a measurement
            op with no bus attribute).
        env (Mapping[str, float]): Bound loop variables by id, plus platform parameters keyed
            ``"bus.parameter"``.

    Returns:
        The outcomes for a single shot.
    """
    ...

MockMeasurementModel

MockMeasurementModel(
    response: Callable[[str, Mapping[str, float]], complex]
    | None = None,
    p_excited: Callable[[str, Mapping[str, float]], float]
    | None = None,
    noise: float = 0.0,
    raw_samples: int = 16,
    seed: int = 0,
)

Deterministic mock measurement model — the executor's default.

The noiseless IQ point comes from response (default 0j); per-shot gaussian noise is added on both quadratures. The classified state is a Bernoulli sample of p_excited (default 0.0). The raw trace replicates the IQ point over raw_samples time samples with per-sample noise. All randomness flows from one seeded generator, so identical programs produce identical results.

Parameters:

  • response (Callable[[str, Mapping[str, float]], complex] | None, default: None ) –

    (bus, env) -> complex noiseless IQ response. env holds the bound loop variables and platform parameters, so e.g. a Rabi oscillation is lambda bus, env: np.sin(np.pi * env["g"] / 2) ** 2 + 0j. None responds 0j.

  • p_excited (Callable[[str, Mapping[str, float]], float] | None, default: None ) –

    (bus, env) -> float excited-state probability for the classified outcome. None keeps every shot in the ground state.

  • noise (float, default: 0.0 ) –

    Standard deviation of the gaussian noise added per quadrature, per shot. Zero skips the noise draws entirely rather than drawing zero-width ones.

  • raw_samples (int, default: 16 ) –

    Number of time samples in the raw trace.

  • seed (int, default: 0 ) –

    Seed for the model's private numpy.random.default_rng.

Source code in src/qprogram/executor.py
def __init__(
    self,
    response: Callable[[str, Mapping[str, float]], complex] | None = None,
    p_excited: Callable[[str, Mapping[str, float]], float] | None = None,
    noise: float = 0.0,
    raw_samples: int = 16,
    seed: int = 0,
) -> None:
    self._response = response
    self._p_excited = p_excited
    self._noise = noise
    self.raw_samples = raw_samples
    self._rng = np.random.default_rng(seed)

sample

sample(
    bus: str, env: Mapping[str, float]
) -> MeasurementSample

Return one deterministic-given-the-seed shot for bus under env.

Parameters:

  • bus (str) –

    The bus the measurement runs on; forwarded to response and p_excited.

  • env (Mapping[str, float]) –

    Bound loop variables by id, plus platform parameters keyed "bus.parameter"; forwarded to response and p_excited.

Returns:

Source code in src/qprogram/executor.py
def sample(self, bus: str, env: Mapping[str, float]) -> MeasurementSample:
    """Return one deterministic-given-the-seed shot for ``bus`` under ``env``.

    Args:
        bus (str): The bus the measurement runs on; forwarded to ``response`` and ``p_excited``.
        env (Mapping[str, float]): Bound loop variables by id, plus platform parameters keyed
            ``"bus.parameter"``; forwarded to ``response`` and ``p_excited``.

    Returns:
        The shot's IQ point, classified state, and raw trace.
    """
    center = complex(self._response(bus, env)) if self._response is not None else 0j
    i = center.real + (self._rng.normal(0.0, self._noise) if self._noise else 0.0)
    q = center.imag + (self._rng.normal(0.0, self._noise) if self._noise else 0.0)
    p1 = float(self._p_excited(bus, env)) if self._p_excited is not None else 0.0
    state = int(self._rng.random() < p1)
    raw = np.empty((self.raw_samples, 2), dtype=float)
    raw[:, 0] = center.real
    raw[:, 1] = center.imag
    if self._noise:
        raw += self._rng.normal(0.0, self._noise, size=raw.shape)
    return MeasurementSample(i=i, q=q, state=state, raw=raw)

MeasurementSample dataclass

MeasurementSample(
    i: float,
    q: float,
    state: int,
    raw: ndarray = (lambda: np.empty((0, 2)))(),
)

One shot's worth of simulated measurement outcomes.

Attributes:

  • i (float) –

    In-phase value.

  • q (float) –

    Quadrature value.

  • state (int) –

    Classified outcome, 0 or 1.

  • raw (ndarray) –

    Raw trace of shape (raw_samples, 2) (I and Q per time sample). Defaults to an empty (0, 2) array, which is what a model that simulates no ADC wants; it is read only by a measurement that requests MeasurementField.RAW, and such a measurement rejects a trace of the wrong shape rather than broadcasting it.

ExecutionWarning

Bases: UserWarning

Category for warning-severity diagnostics surfaced during ReferencePlatform.execute.

Capability protocol

The data types and helpers that platforms use to declare which DSL features they support. See Capabilities, diagnostics, and profiles for the narrative tour.

Descriptors and bundles

PlatformCapabilities dataclass

PlatformCapabilities(
    bus: Mapping[BusSelector, BusCapabilities],
    platform: BusCapabilities,
    default_bus_profile: BusCapabilities,
)

The capability descriptor returned by PlatformProtocol.capabilities.

Capabilities split along two grains — per-bus and platform-wide — plus a fallback slot for buses the per-bus mapping does not cover.

Attributes:

  • bus (Mapping[BusSelector, BusCapabilities]) –

    One slot per (element_kind, bus_kind). Bus-touching ops (play, wait, measure, ...) route here via for_bus, and so do the tokens that travel with them: waveform.* and measure.fields.*.

  • platform (BusCapabilities) –

    The platform-wide slot. Holds block-structure tokens, expression tokens, and bus-less ops.

  • default_bus_profile (BusCapabilities) –

    Fallback for raw-string buses lacking schema metadata, and for bus-touching ops whose (element, kind) key is missing from bus.

for_bus

for_bus(bus: str | BusRef) -> BusCapabilities

Resolve the BusCapabilities that applies to bus.

A BusRef carrying schema metadata routes to bus[(element, kind)] when present, otherwise to default_bus_profile. A plain str or schema-less BusRef always routes to default_bus_profile.

Parameters:

  • bus (str | BusRef) –

    The bus a node touches.

Returns:

Source code in src/qprogram/protocol.py
def for_bus(self, bus: str | BusRef) -> BusCapabilities:
    """Resolve the [`BusCapabilities`][qprogram.BusCapabilities] that applies to ``bus``.

    A [`BusRef`][qprogram.BusRef] carrying schema metadata routes to ``bus[(element, kind)]`` when present,
    otherwise to `default_bus_profile`. A plain ``str`` or schema-less ``BusRef`` always
    routes to `default_bus_profile`.

    Args:
        bus (str | BusRef): The bus a node touches.

    Returns:
        The slot whose capabilities apply to that bus.
    """
    from qprogram.buses import BusRef as _BusRef  # ruff: ignore[import-outside-top-level] — break the import cycle

    if isinstance(bus, _BusRef) and bus.schema is not None:
        key: BusSelector = (bus.element, bus.kind)
        if key in self.bus:
            return self.bus[key]
    return self.default_bus_profile

BusCapabilities dataclass

BusCapabilities(
    rt: CompilerCapabilities | None,
    host: CompilerCapabilities | None,
)

Two stacked CompilerCapabilities for a single bus or platform slot.

Each half describes what the slot supports in that execution domain. Either may be None when the bus or platform lacks an engine for that domain — e.g. a flux bus driven only by a slow DAC has rt=None; a real-time-only bus has host=None.

Attributes:

get

get(domain: Domain) -> CompilerCapabilities | None

Return the CompilerCapabilities for domain, or None if unsupported.

Parameters:

  • domain (Domain) –

    The execution domain to read.

Returns:

  • CompilerCapabilities | None

    The half describing that domain, or None when the slot has no engine for it.

Source code in src/qprogram/protocol.py
def get(self, domain: Domain) -> CompilerCapabilities | None:
    """Return the [`CompilerCapabilities`][qprogram.CompilerCapabilities] for ``domain``, or ``None`` if unsupported.

    Args:
        domain (Domain): The execution domain to read.

    Returns:
        The half describing that domain, or ``None`` when the slot has no engine for it.
    """
    return self.rt if domain == "rt" else self.host

supported_domains

supported_domains() -> frozenset[Domain]

Return the set of domains this slot has a non-None descriptor for.

Returns:

  • frozenset[Domain]

    A subset of {"rt", "host"}; empty when the slot can run nothing at all.

Source code in src/qprogram/protocol.py
def supported_domains(self) -> frozenset[Domain]:
    """Return the set of domains this slot has a non-``None`` descriptor for.

    Returns:
        A subset of ``{"rt", "host"}``; empty when the slot can run nothing at all.
    """
    out: set[Domain] = set()
    if self.rt is not None:
        out.add("rt")
    if self.host is not None:
        out.add("host")
    return frozenset(out)

CompilerCapabilities dataclass

CompilerCapabilities(
    profile: str,
    version: tuple[int, int, int],
    capabilities: frozenset[str],
    limits: Mapping[str, float],
    predicates: tuple[Predicate, ...],
    vendor_versions: Mapping[str, tuple[int, int, int]],
)

The capability descriptor that PlatformProtocol exposes via .capabilities.

Materialized by from_profile, which walks extends and merges parent → child: capabilities/predicates union, limits replace. A live device may pass limit_overrides= to further tighten any limit. The same object is what the validator consumes and what users introspect — there is no separate "advertised vs. enforced" surface.

Attributes:

  • profile (str) –

    Name of the source profile.

  • version (tuple[int, int, int]) –

    Source profile version.

  • capabilities (frozenset[str]) –

    Merged set of capability tokens.

  • limits (Mapping[str, float]) –

    Merged numeric limits.

  • predicates (tuple[Predicate, ...]) –

    Merged predicates, parent's first.

  • vendor_versions (Mapping[str, tuple[int, int, int]]) –

    Merged vendor-extension version expectations.

supports

supports(token: str) -> bool

Return whether token is in this descriptor's capability set.

Parameters:

  • token (str) –

    The capability token to look for.

Returns:

  • bool

    True when this descriptor advertises the token.

Source code in src/qprogram/protocol.py
def supports(self, token: str) -> bool:
    """Return whether ``token`` is in this descriptor's capability set.

    Args:
        token (str): The capability token to look for.

    Returns:
        ``True`` when this descriptor advertises the token.
    """
    return token in self.capabilities

from_profile classmethod

from_profile(
    profile_name: str,
    *,
    limit_overrides: Mapping[str, float] | None = None,
    extra_predicates: tuple[Predicate, ...] = (),
) -> CompilerCapabilities

Resolve a registered profile and merge it into a capability descriptor.

Parameters:

  • profile_name (str) –

    Name of a profile registered with register_profile.

  • limit_overrides (Mapping[str, float] | None, default: None ) –

    Per-limit replacements for the merged values — typically supplied by a device that knows its hardware is tighter than the profile defaults.

  • extra_predicates (tuple[Predicate, ...], default: () ) –

    Site-specific predicates run on top of the profile's. Useful for rack-level constraints that don't belong in the vendor-shipped profile.

Returns:

Raises:

  • KeyError

    If profile_name, or any profile named by an extends link, is not registered.

  • ValueError

    If the extends chain forms a cycle.

Source code in src/qprogram/protocol.py
@classmethod
def from_profile(
    cls,
    profile_name: str,
    *,
    limit_overrides: Mapping[str, float] | None = None,
    extra_predicates: tuple[Predicate, ...] = (),
) -> CompilerCapabilities:
    """Resolve a registered profile and merge it into a capability descriptor.

    Args:
        profile_name (str): Name of a profile registered with [`register_profile`][qprogram.register_profile].
        limit_overrides (Mapping[str, float] | None): Per-limit replacements for the merged
            values — typically supplied by a device that knows its hardware is tighter than the
            profile defaults.
        extra_predicates (tuple[Predicate, ...], optional): Site-specific predicates run on
            top of the profile's. Useful for rack-level constraints that don't belong in the
            vendor-shipped profile.

    Returns:
        The materialized [`CompilerCapabilities`][qprogram.CompilerCapabilities].

    Raises:
        KeyError: If ``profile_name``, or any profile named by an ``extends`` link, is not
            registered.
        ValueError: If the ``extends`` chain forms a cycle.
    """
    profile = resolve_profile(profile_name)
    chain = _profile_chain(profile)
    merged_caps: set[str] = set()
    merged_limits: dict[str, float] = {}
    merged_predicates: list[Predicate] = []
    merged_vendor_versions: dict[str, tuple[int, int, int]] = {}
    for p in chain:
        merged_caps |= p.capabilities
        merged_limits.update(p.limits)
        merged_predicates.extend(p.predicates)
        # Root-first like limits: a child's expectation for a vendor overrides the parent's.
        merged_vendor_versions.update(p.vendor_versions)
    if limit_overrides:
        merged_limits.update(limit_overrides)
    merged_predicates.extend(extra_predicates)
    return cls(
        profile=profile.name,
        version=profile.version,
        capabilities=frozenset(merged_caps),
        limits=dict(merged_limits),
        predicates=tuple(merged_predicates),
        vendor_versions=merged_vendor_versions,
    )

Profile dataclass

Profile(
    name: str,
    version: tuple[int, int, int],
    extends: str | None,
    capabilities: frozenset[str],
    limits: Mapping[str, float] = dict(),
    predicates: tuple[Predicate, ...] = (),
    vendor_versions: Mapping[
        str, tuple[int, int, int]
    ] = dict(),
)

A named, versioned bundle of capabilities, limits, and predicates.

Vendors register one or more profiles via register_profile. Profiles may extends another by name — capabilities and predicates accumulate (parent → child), limits inherit and may be overridden by the child.

Attributes:

  • name (str) –

    Unique profile name (e.g. "myvendor-default-v1").

  • version (tuple[int, int, int]) –

    Profile version as (major, minor, patch).

  • extends (str | None) –

    Name of a parent profile, or None for a root profile.

  • capabilities (frozenset[str]) –

    Capability tokens this profile advertises. Validated against CAPABILITY_REGISTRY at construction.

  • limits (Mapping[str, float]) –

    Numeric thresholds. The validator ignores keys it does not know, so a profile may declare a limit an older validator has no check for.

  • predicates (tuple[Predicate, ...]) –

    Predicates run against every visited node.

  • vendor_versions (Mapping[str, tuple[int, int, int]]) –

    Informational record of which vendor extension versions this profile was designed for; mirrors the .qp require <vendor> <version> line.

Diagnostic dataclass

Diagnostic(
    severity: Literal["error", "warning", "info"],
    code: str,
    message: str,
    node: Operation | Block | None = None,
    path: tuple[int | str, ...] | None = None,
    capability: str | None = None,
    limit: tuple[str, float] | None = None,
    domain: Domain | None = None,
)

One issue found by the validator.

Attributes:

  • severity (Literal['error', 'warning', 'info']) –

    "error" for hard failures the program cannot execute with (missing capability, exceeded limit, empty execution domain); "warning" for programs that will run but in a degraded or surprising way callers should surface prominently (notably the forced-host notice attached to the highest block that lost "rt" from its execution domain); "info" for purely advisory output. The execution convention: execute() raises on errors, surfaces warnings without raising, and passes info through.

  • code (str) –

    Short machine-readable identifier ("missing-capability", "limit-exceeded", "empty-domain", "forced-host", or a vendor-prefixed code).

  • message (str) –

    Human-readable explanation.

  • node (Operation | Block | None) –

    The offending AST node when one is available. Capability-missing diagnostics always have one; whole-program checks (total-measurement-count, ...) do not.

  • path (tuple[int | str, ...] | None) –

    Structural address of node under the validated program's body (see qprogram.paths). Stamped by validate(); None when there is no node. Because the .qp round-trip preserves structure, the same path resolves against loads(dumps(p)) — whose source_map then maps it to a 1-based .qp line.

  • capability (str | None) –

    The token that was missing, when applicable.

  • limit (tuple[str, float] | None) –

    (name, observed_value) when a numeric limit was exceeded. The threshold itself lives in CompilerCapabilities.limits.

  • domain (Domain | None) –

    Populated on "forced-host" diagnostics with the domain the node ended up running in (typically "host").

DomainConstraint dataclass

DomainConstraint(
    node: Operation | Block,
    exclude: frozenset[Domain],
    reason: str,
)

A predicate's soft outcome: this node would be supported, except in the listed domains.

The classifier collects these and subtracts exclude from the node's per-domain support set. Compare to Diagnostic, which is a hard outcome (the node is unsupported outright in the slot being validated). A predicate may yield zero or more of each type from a single call.

Attributes:

  • node (Operation | Block) –

    The AST node the constraint applies to. Must be a Block — the classifier reports an op-targeted constraint as bad-domain-constraint.

  • exclude (frozenset[Domain]) –

    Domains the node cannot run in. Usually a single-element frozenset ({"rt"}).

  • reason (str) –

    Human-readable explanation surfaced in the eventual forced-host diagnostic, or in the empty-domain error if the exclusion leaves nothing.

Domain module-attribute

Domain = Literal['rt', 'host']

Execution domain for an AST node — real-time hardware sequencer ("rt") or host-side orchestration on the lab server ("host").

BusSelector module-attribute

BusSelector = tuple[str, str]

(element_kind, bus_kind) key into PlatformCapabilities.bus. ("q", "drive") selects every transmon drive bus on the platform.

ExecutionPlan module-attribute

ExecutionPlan = Mapping[
    "Operation | Block", frozenset[Domain]
]

Classifier output: each AST node mapped to the set of domains it may execute in.

A frozenset({"rt"}) entry means a real-time hardware path; frozenset({"host"}) means host-side dispatch; frozenset({"rt", "host"}) means the platform may pick either at compile time. Delivered by PlatformProtocol.plan.

ValidationContext

ValidationContext(
    *,
    variable_bindings: Mapping[Variable, Block],
    sweep_kinds: Mapping[Variable, SweepKind],
    max_loop_nesting: int,
    max_parallel_arity: int,
    measurement_count: int,
    measurement_fields: Mapping[str, tuple[str, ...]]
    | None = None,
    program_buses: frozenset[str] = frozenset(),
)

Read-only view of program-wide data-flow facts, built once per validate() call.

Predicates use the queries here to answer "in this AST, is X legal?" without re-walking the tree. New queries are added here so predicate authors have a single, discoverable surface; predicates must treat the context as immutable.

Parameters:

  • variable_bindings (Mapping[Variable, Block]) –

    Each loop-bound variable mapped to the block that binds it.

  • sweep_kinds (Mapping[Variable, SweepKind]) –

    Each loop-bound variable mapped to its sweep source's KIND.

  • max_loop_nesting (int) –

    Deepest repetition-level count observed in the program.

  • max_parallel_arity (int) –

    Largest number of loops composed by any Parallel block.

  • measurement_count (int) –

    Total number of measurement operations in the program.

  • measurement_fields (Mapping[str, tuple[str, ...]] | None, default: None ) –

    Requested fields per measurement name; None is read as an empty mapping.

  • program_buses (frozenset[str], default: frozenset() ) –

    Every bus referenced anywhere in the program.

Source code in src/qprogram/protocol.py
def __init__(  # ruff: ignore[too-many-arguments]  # all-keyword constructor for a small data carrier
    self,
    *,
    variable_bindings: Mapping[Variable, Block],
    sweep_kinds: Mapping[Variable, SweepKind],
    max_loop_nesting: int,
    max_parallel_arity: int,
    measurement_count: int,
    measurement_fields: Mapping[str, tuple[str, ...]] | None = None,
    program_buses: frozenset[str] = frozenset(),
) -> None:
    self._variable_bindings = dict(variable_bindings)
    self._sweep_kinds = dict(sweep_kinds)
    self._max_loop_nesting = max_loop_nesting
    self._max_parallel_arity = max_parallel_arity
    self._measurement_count = measurement_count
    self._measurement_fields: dict[str, tuple[str, ...]] = dict(measurement_fields or {})
    self._program_buses = frozenset(program_buses)

max_loop_nesting property

max_loop_nesting: int

Deepest repetition-level count observed in the program.

A block adds a level when it declares REPEATS, so a Parallel counts as one level however many loops it composes — its headers advance in lockstep rather than nesting.

max_parallel_arity property

max_parallel_arity: int

Largest len(parallel.loops) observed across any Parallel block.

measurement_count property

measurement_count: int

Total number of MeasurementOperation instances in the program.

program_buses property

program_buses: frozenset[str]

Every bus name referenced anywhere in the program (QProgram.buses at build time).

Elements may be BusRef instances (which subclass str), so per-bus routing through PlatformCapabilities.for_bus keeps its schema awareness. Used by the validator to route broadcast ops (Sync(targets=None)) across every touched bus.

sweep_kind_of

sweep_kind_of(var: Variable) -> SweepKind | None

Return how var is loop-bound.

Parameters:

  • var (Variable) –

    The variable to look up.

Returns:

  • SweepKind | None

    The binding Sweep's source KIND"linear" for an

  • SweepKind | None

    exact start + step * i ramp, "arbitrary" otherwise — or None when the

  • SweepKind | None

    variable is not loop-bound (set externally, or unused). "averaged" belongs to the

  • SweepKind | None

    vocabulary but no built-in source declares it.

Source code in src/qprogram/protocol.py
def sweep_kind_of(self, var: Variable) -> SweepKind | None:
    """Return how ``var`` is loop-bound.

    Args:
        var (Variable): The variable to look up.

    Returns:
        The binding [`Sweep`][qprogram.blocks.Sweep]'s source ``KIND`` — ``"linear"`` for an
        exact ``start + step * i`` ramp, ``"arbitrary"`` otherwise — or ``None`` when the
        variable is not loop-bound (set externally, or unused). ``"averaged"`` belongs to the
        vocabulary but no built-in source declares it.
    """
    return self._sweep_kinds.get(var)

binding_loop_of

binding_loop_of(var: Variable) -> Block | None

Return the loop block that binds var, or None if it has no binding.

Parameters:

  • var (Variable) –

    The variable to look up.

Returns:

  • Block | None

    The Sweep that binds var — a standalone loop or one of a

  • Block | None

    Parallel's composed headers — or None when nothing binds

  • Block | None

    it. This is the node a DomainConstraint about var must target.

Source code in src/qprogram/protocol.py
def binding_loop_of(self, var: Variable) -> Block | None:
    """Return the loop block that binds ``var``, or ``None`` if it has no binding.

    Args:
        var (Variable): The variable to look up.

    Returns:
        The [`Sweep`][qprogram.blocks.Sweep] that binds ``var`` — a standalone loop or one of a
        [`Parallel`][qprogram.blocks.Parallel]'s composed headers — or ``None`` when nothing binds
        it. This is the node a [`DomainConstraint`][qprogram.DomainConstraint] about ``var`` must target.
    """
    return self._variable_bindings.get(var)

measurement_fields

measurement_fields(name: str) -> tuple[str, ...] | None

Return the fields tuple of the named measurement, or None if it doesn't exist.

Predicates use this to check that a referenced measurement requested the data they care about — e.g. that a handle.state reference's source measurement requested STATE classification.

Parameters:

  • name (str) –

    Measurement name to look up.

Returns:

  • tuple[str, ...] | None

    The measurement's requested field names in canonical order, or None when the

  • tuple[str, ...] | None

    program holds no measurement with that name.

Source code in src/qprogram/protocol.py
def measurement_fields(self, name: str) -> tuple[str, ...] | None:
    """Return the ``fields`` tuple of the named measurement, or ``None`` if it doesn't exist.

    Predicates use this to check that a referenced measurement requested the data they care
    about — e.g. that a ``handle.state`` reference's source measurement requested
    `STATE` classification.

    Args:
        name (str): Measurement name to look up.

    Returns:
        The measurement's requested field names in canonical order, or ``None`` when the
        program holds no measurement with that name.
    """
    return self._measurement_fields.get(name)

known_measurement_names

known_measurement_names() -> set[str]

Return the set of every measurement name in the program.

Returns:

  • set[str]

    Every name a measurement in the program carries, whether the author spelled it or the

  • set[str]

    builder allocated it.

Source code in src/qprogram/protocol.py
def known_measurement_names(self) -> set[str]:
    """Return the set of every measurement name in the program.

    Returns:
        Every name a measurement in the program carries, whether the author spelled it or the
        builder allocated it.
    """
    return set(self._measurement_fields)

SweepKind module-attribute

SweepKind = Literal['linear', 'arbitrary', 'averaged']

Predicate

Bases: Protocol

Per-node validation predicate consulted while validate() walks the program.

Receives a ValidationContext with cross-op data-flow facts. Returns zero or more Diagnostic or DomainConstraint objects.

A node is judged against each domain half of each slot it routes to, so a predicate carried by both the rt and the host half of a profile runs once per (domain, bus) pair: twice for a single-bus node, and twice more for every additional bus a multi-bus operation such as Sync touches. The validator discards duplicate outputs, so a predicate should be a cheap, side-effect-free function of (node, ctx).

Motivating examples:

  • Flagging a Wait whose duration is bound by an arbitrary-valued sweep — qblox can't run it at all, so the predicate emits a Diagnostic.
  • Flagging an IQDrag whose sigma is loop-bound — qblox can't realtime-update sigma, but the platform can still dispatch one shot per iteration host-side, so the predicate emits a DomainConstraint excluding "rt".

PredicateFn module-attribute

PredicateFn = Callable[
    ["Operation | Block", "ValidationContext"],
    Iterable["Diagnostic | DomainConstraint"],
]

QPROGRAM_BASE_V1 module-attribute

QPROGRAM_BASE_V1 = Profile(
    name="qprogram-base-v1",
    version=(0, 1, 0),
    extends=None,
    capabilities=_BLOCKS
    | _EXPRS
    | _SWEEP_KINDS
    | _SWEEP_SOURCES,
    limits={},
    predicates=(),
    vendor_versions={},
)

Validator

validate(program, caps) returns a list of Diagnostics and an ExecutionPlan covering every visited node except the root body, keyed by node identity rather than by structural equality. explain(program, caps) renders that same classification as a string: a header with the severity counts, then one row per node carrying its .qp text, its domain, and any diagnostic on it. optimize(program, caps) applies the one rewrite the validator only reports as an info hint, lifting a host-side sweep out of an averaging block so that the averaging itself can run in real time.

validate and explain expand fragment Call nodes before walking, so their diagnostics reference nodes of that expansion rather than nodes the caller holds. optimize expands only when the program's own body holds an average; otherwise it returns a deep copy with the Call nodes intact.

validate

validate(
    qprogram: QProgram, caps: PlatformCapabilities
) -> tuple[list[Diagnostic], ExecutionPlan]

Run capability validation + execution-domain classification.

Algorithm:

  1. Pre-walk to build a ValidationContext (variable bindings, sweep kinds, …).
  2. Single recursive post-order walk that, per node, computes:

  3. For operations: the available domain set is the slots where the op's required tokens are present (and any predicate-emitted Diagnostic is absent). The op's support equals its availableDomainConstraint outputs are not applied to the op; they are routed to the block they target (typically the binding loop of a swept variable).

  4. For blocks: support = own_available & natural_from_ops - exclude_from_constraints, where natural_from_ops is the op-children consensus directly (an all-real-time block is shifted to host-side by the (e2) fallback, not by widening the consensus). For an Average only the averaging-relevant op-children enter the consensus. Mixed op-children produce a mixed-domain error. Block-children act as units; they don't constrain the parent's domain but the parent's domain constrains them (no host-side block inside a real-time block).
  5. Whole-program limit checks (loop nesting, parallel arity, measurement count) against the platform slot's limits; min_wait_duration_ns checks against the bus slot's limits.
  6. Universal Conditional checks (unknown measurement, missing state classification).
  7. Emit one "forced-host" warning per highest-block whose support was reduced from {rt, host} to {host}, with the subtree's constraint reasons in the message.
  8. Stamp each node-bearing diagnostic with its structural Diagnostic.path.

Parameters:

Returns:

  • list[Diagnostic]

    (diagnostics, plan). The plan covers every visited AST node (excluding the root

  • ExecutionPlan

    body) and is identity-keyed: each node instance gets its own entry, even when two

  • tuple[list[Diagnostic], ExecutionPlan]

    nodes are structurally identical (plan[node] looks up by id, and iterating the

  • tuple[list[Diagnostic], ExecutionPlan]

    plan yields every instance).

Note

Programs containing fragment Call nodes are expanded first (QProgram.expand) — capabilities are checked against the substituted fragment bodies, and diagnostics reference nodes of that internal expansion. Callers that need the identity-keyed plan for nodes they hold should expand explicitly and validate the expanded program.

Source code in src/qprogram/validation.py
def validate(
    qprogram: QProgram,
    caps: PlatformCapabilities,
) -> tuple[list[Diagnostic], ExecutionPlan]:
    """Run capability validation + execution-domain classification.

    Algorithm:

    1. Pre-walk to build a [`ValidationContext`][qprogram.ValidationContext] (variable bindings, sweep kinds, …).
    2. Single recursive post-order walk that, per node, computes:

       - For operations: the *available* domain set is the slots where the op's required tokens
         are present (and any predicate-emitted [`Diagnostic`][qprogram.Diagnostic] is absent). The op's
         ``support`` equals its ``available`` — [`DomainConstraint`][qprogram.DomainConstraint] outputs are *not*
         applied to the op; they are routed to the **block** they target (typically the binding
         loop of a swept variable).
       - For blocks: ``support = own_available & natural_from_ops - exclude_from_constraints``,
         where ``natural_from_ops`` is the op-children consensus directly (an all-real-time block is
         shifted to host-side by the (e2) fallback, not by widening the consensus). For an ``Average``
         only the averaging-relevant op-children enter the consensus. Mixed op-children produce a
         ``mixed-domain`` error. Block-children act as units; they don't constrain the parent's
         domain but the parent's domain constrains them (no host-side block inside a real-time block).
    3. Whole-program limit checks (loop nesting, parallel arity, measurement count) against the
       platform slot's limits; ``min_wait_duration_ns`` checks against the bus slot's limits.
    4. Universal Conditional checks (unknown measurement, missing state classification).
    5. Emit one ``"forced-host"`` warning per highest-block whose ``support`` was reduced
       from ``{rt, host}`` to ``{host}``, with the subtree's constraint reasons in the message.
    6. Stamp each node-bearing diagnostic with its structural `Diagnostic.path`.

    Args:
        qprogram (QProgram): Program to validate.
        caps (PlatformCapabilities): Platform capability descriptor to check the program against.

    Returns:
        ``(diagnostics, plan)``. The plan covers every visited AST node (excluding the root
        body) and is **identity-keyed**: each node *instance* gets its own entry, even when two
        nodes are structurally identical (``plan[node]`` looks up by ``id``, and iterating the
        plan yields every instance).

    Note:
        Programs containing fragment [`Call`][qprogram.operations.Call] nodes are **expanded
        first** ([`QProgram.expand`][qprogram.QProgram.expand]) — capabilities are checked against the substituted
        fragment bodies, and diagnostics reference nodes of that internal expansion. Callers
        that need the identity-keyed plan for nodes they hold should expand explicitly and
        validate the expanded program.
    """
    from qprogram.operations.call import Call  # ruff: ignore[import-outside-top-level]

    if any(isinstance(node, Call) for node in qprogram.body.walk()):
        qprogram = qprogram.expand()
    ctx = _build_context(qprogram)
    diagnostics: list[Diagnostic] = []
    available: _IdentityNodeMap[frozenset[Domain]] = _IdentityNodeMap()
    support: _IdentityNodeMap[frozenset[Domain]] = _IdentityNodeMap()
    parent: _IdentityNodeMap[Block | None] = _IdentityNodeMap()
    constraints_by_block: dict[int, list[DomainConstraint]] = defaultdict(list)

    for child in qprogram.body.elements:
        _classify_node(
            child,
            qprogram.body,
            caps,
            ctx,
            diagnostics,
            available,
            support,
            parent,
            constraints_by_block,
        )

    diagnostics.extend(_check_limits(qprogram, ctx, caps))
    diagnostics.extend(_check_conditional_classification(qprogram, ctx))
    _emit_forced_host(diagnostics, available, support, parent, constraints_by_block)
    _emit_averaging_hints(diagnostics, support)

    return _stamp_paths(diagnostics, qprogram), support

explain

explain(
    program: QProgram, caps: PlatformCapabilities
) -> str

Render the execution plan of program under caps as a tree.

Programs containing fragment calls are expanded first (the header says so) — the plan always describes what would actually execute.

Parameters:

  • program (QProgram) –

    The program to classify and render.

  • caps (PlatformCapabilities) –

    The platform capability descriptor to validate against.

Returns:

  • str

    A multi-line string: header with severity counts, the body tree with one row per node

  • str

    (.qp text, domain column, inline diagnostics), and a footer for whole-program

  • str

    diagnostics that have no node.

Source code in src/qprogram/explain.py
def explain(program: QProgram, caps: PlatformCapabilities) -> str:
    """Render the execution plan of ``program`` under ``caps`` as a tree.

    Programs containing fragment calls are expanded first (the header says so) — the plan always
    describes what would actually execute.

    Args:
        program (QProgram): The program to classify and render.
        caps (PlatformCapabilities): The platform capability descriptor to validate against.

    Returns:
        A multi-line string: header with severity counts, the body tree with one row per node
        (``.qp`` text, domain column, inline diagnostics), and a footer for whole-program
        diagnostics that have no node.
    """
    # Imported here rather than at module load, which would be a cycle.
    from qprogram.operations.call import Call  # ruff: ignore[import-outside-top-level]

    expanded = False
    if any(isinstance(node, Call) for node in program.body.walk()):
        program = program.expand()
        expanded = True
    diagnostics, plan = validate(program, caps)

    by_node: dict[int, list[Diagnostic]] = {}
    floating: list[Diagnostic] = []
    for diag in diagnostics:
        if diag.node is None:
            floating.append(diag)
        else:
            by_node.setdefault(id(diag.node), []).append(diag)

    rows = _render_rows(program, plan, by_node)

    counts = {"error": 0, "warning": 0, "info": 0}
    for diag in diagnostics:
        counts[diag.severity] = counts.get(diag.severity, 0) + 1
    title = f"plan for {program.label!r}" if program.label else "plan"
    if expanded:
        title += " (fragments expanded)"
    header = f"{title} — errors: {counts['error']} · warnings: {counts['warning']} · info: {counts['info']}"

    lines = [header, "body"]
    if rows:
        width = max(len(text) for text, _, _ in rows)
        for text, domain, anns in rows:
            line = f"{text:<{width}}  {domain:<9}"
            if anns:
                line = f"{line}  {'  '.join(anns)}"
            lines.append(line.rstrip())
    else:
        lines.append("(empty)")
    if floating:
        lines.append("")
        lines.extend(_annotation(diag) for diag in floating)
    return "\n".join(lines)

optimize

optimize(
    qprogram: QProgram, capabilities: PlatformCapabilities
) -> QProgram

Return a copy of qprogram rewritten to run better under capabilities.

Applies the rewrite the validator only suggests (the "reorderable-averaging" info hint). For every average that is host-side-only solely because it encloses a host-side sweep — while its measurement sequence supports real-time hardware — the sweep is lifted to become the outer loop and the host-side-only setup ops are hoisted out of the average, so the averaging itself runs as a real-time hardware feature::

average(N):                     for v in sweep:           # host-side (slow DAC step)
    for v in sweep:                 set_offset(flux, v)   # hoisted setup
        set_offset(flux, v)   -->     average(N):         # REAL-TIME
        play(drive, ...)                  play(drive, ...)
        measure(readout)                  measure(readout)

Only the well-defined pattern above is rewritten — an average whose sole child is a single flat sweep loop carrying host-side-only setup plus a real-time-capable measurement sequence. Anything else is left untouched. (Whether a given op is host-side-only is decided by capabilities: the example assumes a flux bus with no real-time engine; on a platform whose flux bus has a real-time half, set_offset would not be hoisted and the average would already be real-time.)

This is opt-in because the reorder is not unconditionally semantics-preserving:

  • It groups all shots of one sweep point together rather than interleaving sweep passes; the averaged result is identical for a stationary system but differs under drift.
  • Each hoisted op runs once per sweep point instead of once per shot. That is the point of hoisting "setup" (DC-offset / parameter writes are idempotent, so the count change is harmless), but the rewrite hoists every host-side-only op in the leading run — so only apply it when those ops are genuinely idempotent setup. To stay safe, the rewrite refuses to reorder: it only hoists a leading contiguous run of host-side-only ops, never one that sits after a kept op (which would move it past that op and could change results).

Parameters:

  • qprogram (QProgram) –

    The program to optimize. Never mutated.

  • capabilities (PlatformCapabilities) –

    The platform descriptor used to classify which ops are host-side-only.

Returns:

  • QProgram

    A new QProgram with the rewrite applied; the original is untouched. The

  • QProgram

    search for an average scans the program's own body and does not look inside the

  • QProgram

    fragments it calls, so a program whose body holds no average block comes back a plain

  • QProgram

    deep copy with its Call nodes intact — including a program

  • QProgram

    whose only average sits in a fragment body. A program whose body does hold an average

  • QProgram

    is validated against capabilities to classify its ops, which expands any Call nodes

  • QProgram

    first, so such a program is returned in expanded form even if no average ends up rewritten.

Raises:

  • ValidationError

    If the program's body holds an average and its fragment calls cannot be expanded (a call cycle, or a binding used in an incompatible position).

Source code in src/qprogram/optimization.py
def optimize(qprogram: QProgram, capabilities: PlatformCapabilities) -> QProgram:
    """Return a copy of ``qprogram`` rewritten to run better under ``capabilities``.

    Applies the rewrite the validator only *suggests* (the ``"reorderable-averaging"`` info hint).
    For every ``average`` that is host-side-only **solely because it encloses a host-side sweep** —
    while its measurement sequence supports real-time hardware — the sweep is lifted to become the
    outer loop and the host-side-only setup ops are hoisted out of the average, so the averaging
    itself runs as a real-time hardware feature::

        average(N):                     for v in sweep:           # host-side (slow DAC step)
            for v in sweep:                 set_offset(flux, v)   # hoisted setup
                set_offset(flux, v)   -->     average(N):         # REAL-TIME
                play(drive, ...)                  play(drive, ...)
                measure(readout)                  measure(readout)

    Only the well-defined pattern above is rewritten — an ``average`` whose sole child is a single
    flat sweep loop carrying host-side-only setup plus a real-time-capable measurement sequence.
    Anything else is left untouched. (Whether a given op is host-side-only is decided by
    ``capabilities``: the example assumes a flux bus with no real-time engine; on a platform whose
    flux bus has a real-time half, ``set_offset`` would not be hoisted and the average would already
    be real-time.)

    This is **opt-in** because the reorder is not unconditionally semantics-preserving:

    - It **groups** all shots of one sweep point together rather than interleaving sweep passes; the
      averaged result is identical for a stationary system but differs under drift.
    - Each hoisted op runs **once per sweep point** instead of once per shot. That is the point of
      hoisting "setup" (DC-offset / parameter writes are idempotent, so the count change is
      harmless), but the rewrite hoists *every* host-side-only op in the leading run — so only apply
      it when those ops are genuinely idempotent setup. To stay safe, the rewrite refuses to
      reorder: it only hoists a *leading contiguous run* of host-side-only ops, never one that sits
      after a kept op (which would move it past that op and could change results).

    Args:
        qprogram (QProgram): The program to optimize. Never mutated.
        capabilities (PlatformCapabilities): The platform descriptor used to classify which ops are
            host-side-only.

    Returns:
        A new [`QProgram`][qprogram.QProgram] with the rewrite applied; the original is untouched. The
        search for an ``average`` scans the program's own body and does not look inside the
        fragments it calls, so a program whose body holds no ``average`` block comes back a plain
        deep copy with its [`Call`][qprogram.operations.Call] nodes intact — including a program
        whose only ``average`` sits in a fragment body. A program whose body *does* hold an average
        is validated against ``capabilities`` to classify its ops, which expands any ``Call`` nodes
        first, so such a program is returned in expanded form even if no average ends up rewritten.

    Raises:
        ValidationError: If the program's body holds an average and its fragment calls cannot be
            expanded (a call cycle, or a binding used in an incompatible position).
    """
    # No average in the program's own body → nothing to do; return a faithful copy without the
    # (lossy) fragment expansion that classification would otherwise force.
    if not any(isinstance(node, Average) for node in qprogram.body.walk()):
        return copy.deepcopy(qprogram)
    new_program = qprogram.expand() if qprogram.fragments else copy.deepcopy(qprogram)
    _, plan = validate(new_program, capabilities)
    _reorder_rt_averages(new_program.body, plan)
    return new_program

Diagnostic paths

Every node-bearing Diagnostic carries a structural path. AstPath is the type, node_path builds one for a node, resolve_path walks one back to the node it names, format_path renders one for display, and iter_child_edges is the single ordered traversal that node_path, resolve_path, and the validator all read the AST through.

AstPath module-attribute

AstPath: TypeAlias = tuple[int | str, ...]

A structural node address: () is program.body; see the module docstring for segments.

node_path

node_path(
    root: QProgram | Block, node: Block | Operation
) -> AstPath | None

Return the path of node under root, or None if it isn't in the tree.

Matching is by object identity — pass the same instance the tree holds (e.g. a Diagnostic.node), not a structural twin.

Parameters:

Returns:

  • AstPath | None

    The structural address of node() when node is the root itself — or None

  • AstPath | None

    when the tree holds no such instance.

Source code in src/qprogram/paths.py
def node_path(root: QProgram | Block, node: Block | Operation) -> AstPath | None:
    """Return the path of ``node`` under ``root``, or ``None`` if it isn't in the tree.

    Matching is by object **identity** — pass the same instance the tree holds (e.g. a
    ``Diagnostic.node``), not a structural twin.

    Args:
        root (QProgram | Block): A [`QProgram`][qprogram.QProgram] (paths root at its body) or a
            [`Block`][qprogram.blocks.Block].
        node (Block | Operation): The node instance to locate.

    Returns:
        The structural address of ``node`` — ``()`` when ``node`` is the root itself — or ``None``
        when the tree holds no such instance.
    """
    base = root.body if isinstance(root, QProgram) else root
    if node is base:
        return ()

    def search(current: Block | Operation, prefix: AstPath) -> AstPath | None:
        for segment, child in iter_child_edges(current):
            child_path = (*prefix, segment)
            if child is node:
                return child_path
            found = search(child, child_path)
            if found is not None:
                return found
        return None

    return search(base, ())

resolve_path

resolve_path(
    root: QProgram | Block, path: AstPath
) -> Block | Operation

Return the node addressed by path under root — the inverse of node_path.

Parameters:

Returns:

  • Block | Operation

    The addressed block or operation — the root node itself (a program's body, when root is

  • Block | Operation

    a program) for the empty path.

Raises:

  • KeyError

    When any segment doesn't exist on the node reached so far (a dangling path — typically a path computed against a structurally different program).

Source code in src/qprogram/paths.py
def resolve_path(root: QProgram | Block, path: AstPath) -> Block | Operation:
    """Return the node addressed by ``path`` under ``root`` — the inverse of [`node_path`][qprogram.node_path].

    Args:
        root (QProgram | Block): A [`QProgram`][qprogram.QProgram] (paths root at its body) or a
            [`Block`][qprogram.blocks.Block].
        path (AstPath): The structural address to follow.

    Returns:
        The addressed block or operation — the root node itself (a program's body, when ``root`` is
        a program) for the empty path.

    Raises:
        KeyError: When any segment doesn't exist on the node reached so far (a dangling path —
            typically a path computed against a structurally different program).
    """
    current: Block | Operation = root.body if isinstance(root, QProgram) else root
    for depth, segment in enumerate(path):
        for child_segment, child in iter_child_edges(current):
            if child_segment == segment:
                current = child
                break
        else:
            taken = format_path(path[:depth])
            msg = f"path segment {segment!r} does not exist under {taken} ({type(current).__name__})"
            raise KeyError(msg)
    return current

format_path

format_path(path: AstPath) -> str

Render a path for humans: ()"body"; (1, "arm:0", 2)"body[1].arm:0[2]".

Parameters:

  • path (AstPath) –

    The structural address to render.

Returns:

  • str

    The path as a single line, always rooted at body: integer segments in brackets, string

  • str

    segments after a dot.

Source code in src/qprogram/paths.py
def format_path(path: AstPath) -> str:
    """Render a path for humans: ``()`` → ``"body"``; ``(1, "arm:0", 2)`` → ``"body[1].arm:0[2]"``.

    Args:
        path (AstPath): The structural address to render.

    Returns:
        The path as a single line, always rooted at ``body``: integer segments in brackets, string
        segments after a dot.
    """
    out = "body"
    for segment in path:
        out += f"[{segment}]" if isinstance(segment, int) else f".{segment}"
    return out

iter_child_edges

iter_child_edges(
    node: Block | Operation,
) -> Iterator[tuple[int | str, Block | Operation]]

Yield (segment, child) for every structural child of node, in document order.

The single canonical child enumeration behind node_path / resolve_path — Conditional keeps its arm bodies on .arms / .else_body and Parallel its loop headers on .loops, so a plain elements walk would miss them.

Parameters:

  • node (Block | Operation) –

    The node whose children to enumerate. An operation is a leaf and yields nothing.

Yields:

  • int | str

    A (segment, child) pair per child, where segment is the path segment that addresses

  • Block | Operation

    that child under node.

Source code in src/qprogram/paths.py
def iter_child_edges(node: Block | Operation) -> Iterator[tuple[int | str, Block | Operation]]:
    """Yield ``(segment, child)`` for every structural child of ``node``, in document order.

    The single canonical child enumeration behind [`node_path`][qprogram.node_path] /
    [`resolve_path`][qprogram.resolve_path] — Conditional keeps its arm bodies on ``.arms`` / ``.else_body`` and
    Parallel its loop headers on ``.loops``, so a plain ``elements`` walk would miss them.

    Args:
        node (Block | Operation): The node whose children to enumerate. An operation is a leaf and
            yields nothing.

    Yields:
        A ``(segment, child)`` pair per child, where ``segment`` is the path segment that addresses
        that child under ``node``.
    """
    if isinstance(node, Conditional):
        for i, (_, body) in enumerate(node.arms):
            yield f"arm:{i}", body
        if node.else_body is not None:
            yield "else", node.else_body
        return
    if isinstance(node, Parallel):
        for i, loop in enumerate(node.loops):
            yield f"loop:{i}", loop
    if isinstance(node, Block):
        for i, child in enumerate(node.elements):
            yield i, child

Registries and helpers

Registration and lookup for the capability tokens and the named profiles. Profile runs every token it is given through validate_tokens in its __post_init__, so a token no core definition or registration call has put in CAPABILITY_REGISTRY is rejected where the profile is defined rather than surfacing later as a feature the platform silently lacks.

CAPABILITY_REGISTRY module-attribute

CAPABILITY_REGISTRY: set[str] = set(_BASE_TOKENS)

Mutable token registry. Core tokens are added at import time; vendor packages extend it via register_capability_tokens.

register_profile

register_profile(profile: Profile) -> None

Register a profile in PROFILE_REGISTRY under its name.

Idempotent for an equal profile, so an import-time side effect that runs twice is safe even when it rebuilds the bundle each time rather than holding it as a module constant. Only a profile whose content differs raises.

Of an equal pair the registry keeps the first object, so the profile just passed in is not necessarily the one resolve_profile returns; mutating limits on the object you built will not be visible through the registry.

Equality is Profile's own, which compares every field with that field's __eq__. Predicates count as the objects they are, so a profile whose predicates are rebuilt on each construction — a lambda, a closure, a functools.partial — is never equal to a second construction of itself and still raises. Hold them as module-level functions, or give them a type with value equality, to get the idempotency.

Parameters:

  • profile (Profile) –

    The bundle to register under its own name.

Raises:

  • ValueError

    If a profile with different content is already registered under profile.name.

Source code in src/qprogram/protocol.py
def register_profile(profile: Profile) -> None:
    """Register a profile in `PROFILE_REGISTRY` under its name.

    Idempotent for an *equal* profile, so an import-time side effect that runs twice is safe even
    when it rebuilds the bundle each time rather than holding it as a module constant. Only a
    profile whose content differs raises.

    Of an equal pair the registry keeps the first object, so the profile just passed in is not
    necessarily the one `resolve_profile` returns; mutating ``limits`` on the object you built will
    not be visible through the registry.

    Equality is [`Profile`][qprogram.Profile]'s own, which compares every field with that field's
    ``__eq__``. Predicates count as the objects they are, so a profile whose predicates are rebuilt
    on each construction — a ``lambda``, a closure, a ``functools.partial`` — is never equal to a
    second construction of itself and still raises. Hold them as module-level functions, or give
    them a type with value equality, to get the idempotency.

    Args:
        profile (Profile): The bundle to register under its own ``name``.

    Raises:
        ValueError: If a profile with different content is already registered under
            ``profile.name``.
    """
    existing = PROFILE_REGISTRY.get(profile.name)
    if existing is not None:
        if existing == profile:
            return
        msg = f"Profile {profile.name!r} is already registered with different content"
        raise ValueError(msg)
    PROFILE_REGISTRY[profile.name] = profile

resolve_profile

resolve_profile(name: str) -> Profile

Look up a profile by name.

Parameters:

  • name (str) –

    The registered profile name.

Returns:

Raises:

  • KeyError

    If name is not registered. The message lists the currently-known names.

Source code in src/qprogram/protocol.py
def resolve_profile(name: str) -> Profile:
    """Look up a profile by name.

    Args:
        name (str): The registered profile name.

    Returns:
        The registered [`Profile`][qprogram.Profile].

    Raises:
        KeyError: If ``name`` is not registered. The message lists the currently-known names.
    """
    if name not in PROFILE_REGISTRY:
        available = ", ".join(sorted(PROFILE_REGISTRY)) or "(none registered)"
        msg = f"Unknown profile {name!r}. Available: {available}"
        raise KeyError(msg)
    return PROFILE_REGISTRY[name]

register_capability_tokens

register_capability_tokens(*tokens: str) -> None

Register vendor-extension capability tokens.

Idempotent (re-registration is a no-op). Validates the shape of each token but does not enforce a namespace policy beyond rejecting empty segments and stray dots — each vendor owns its own vendor.<name>.* prefix.

Parameters:

  • *tokens (str, default: () ) –

    Tokens to register.

Raises:

  • ValueError

    If any token is empty, starts/ends with ., or contains ...

Source code in src/qprogram/protocol.py
def register_capability_tokens(*tokens: str) -> None:
    """Register vendor-extension capability tokens.

    Idempotent (re-registration is a no-op). Validates the shape of each token but does not enforce
    a namespace policy beyond rejecting empty segments and stray dots — each vendor owns its own
    ``vendor.<name>.*`` prefix.

    Args:
        *tokens (str): Tokens to register.

    Raises:
        ValueError: If any token is empty, starts/ends with ``.``, or contains ``..``.
    """
    for token in tokens:
        if not token or token.startswith(".") or token.endswith(".") or ".." in token:
            msg = f"Invalid capability token {token!r} (empty / leading-dot / trailing-dot / doubled dot)"
            raise ValueError(msg)
        CAPABILITY_REGISTRY.add(token)

register_waveform_token

register_waveform_token(cls: type, token: str) -> None

Register a waveform class → token mapping.

Also registers token in CAPABILITY_REGISTRY so profiles that list the token don't have to call both functions.

Parameters:

  • cls (type) –

    Waveform class to register.

  • token (str) –

    Canonical capability token (e.g. "waveform.iq_drag").

Raises:

  • ValueError

    If token is empty, starts or ends with ., or contains ...

Source code in src/qprogram/protocol.py
def register_waveform_token(cls: type, token: str) -> None:
    """Register a waveform class → token mapping.

    Also registers ``token`` in `CAPABILITY_REGISTRY` so profiles that list the token don't
    have to call both functions.

    Args:
        cls (type): Waveform class to register.
        token (str): Canonical capability token (e.g. ``"waveform.iq_drag"``).

    Raises:
        ValueError: If ``token`` is empty, starts or ends with ``.``, or contains ``..``.
    """
    WAVEFORM_TOKEN[cls] = token
    register_capability_tokens(token)

validate_tokens

validate_tokens(tokens: Iterable[str]) -> None

Validate that every token in tokens is registered.

Called from Profile's __post_init__, so an unknown token — a typo, or a feature this build does not have — is rejected at registration rather than during validation.

Parameters:

  • tokens (Iterable[str]) –

    Capability tokens to check.

Raises:

  • ValueError

    If any token is not in CAPABILITY_REGISTRY.

Source code in src/qprogram/protocol.py
def validate_tokens(tokens: Iterable[str]) -> None:
    """Validate that every token in ``tokens`` is registered.

    Called from [`Profile`][qprogram.Profile]'s ``__post_init__``, so an unknown token — a typo, or a feature this
    build does not have — is rejected at registration rather than during validation.

    Args:
        tokens (Iterable[str]): Capability tokens to check.

    Raises:
        ValueError: If any token is not in `CAPABILITY_REGISTRY`.
    """
    unknown = [t for t in tokens if t not in CAPABILITY_REGISTRY]
    if unknown:
        msg = (
            f"Unknown capability token(s): {sorted(unknown)}. "
            f"Register via qprogram.protocol.register_capability_tokens before use."
        )
        raise ValueError(msg)

waveform_token

waveform_token(wf: object) -> str | None

Return the canonical capability token for a waveform value, or None.

String aliases return None (callers add waveform.alias directly). Unknown concrete classes also return None; the validator skips per-class refinement for them. Channel-kind tokens (waveform.single / waveform.iq) come from Operation.required_capabilities via isinstance checks, so they remain present even when no per-class token is registered.

Parameters:

  • wf (object) –

    The waveform value to classify. Typed object because the dispatch is purely class-keyed, and vendor packages register their own classes here without subclassing Waveform / IQWaveform.

Returns:

  • str | None

    The token registered for the value's class, or None for a string alias or an

  • str | None

    unregistered class.

Source code in src/qprogram/protocol.py
def waveform_token(wf: object) -> str | None:
    """Return the canonical capability token for a waveform value, or ``None``.

    String aliases return ``None`` (callers add ``waveform.alias`` directly). Unknown concrete classes
    also return ``None``; the validator skips per-class refinement for them. Channel-kind tokens
    (``waveform.single`` / ``waveform.iq``) come from `Operation.required_capabilities` via
    ``isinstance`` checks, so they remain present even when no per-class token is registered.

    Args:
        wf (object): The waveform value to classify. Typed ``object`` because the dispatch is purely
            class-keyed, and vendor packages register their own classes here without subclassing
            [`Waveform`][qprogram.waveforms.Waveform] / [`IQWaveform`][qprogram.waveforms.IQWaveform].

    Returns:
        The token registered for the value's class, or ``None`` for a string alias or an
        unregistered class.
    """
    _register_builtin_waveform_tokens()
    if isinstance(wf, str):
        return None
    return WAVEFORM_TOKEN.get(type(wf))

expression_tokens

expression_tokens(value: object) -> set[str]

Recursively collect capability tokens contributed by an expression value.

The returned set always describes the Expression node type (and operator name for MathFunc), not the value. Plain numeric literals contribute nothing — they're not Expression nodes. Operations call this on each Expression-typed instance attribute they carry.

Parameters:

  • value (object) –

    Anything that can appear as an expression operand. A value that is not an Expression contributes nothing.

Returns:

  • set[str]

    Set of capability tokens contributed by the value and its descendants.

Source code in src/qprogram/protocol.py
def expression_tokens(value: object) -> set[str]:
    """Recursively collect capability tokens contributed by an expression value.

    The returned set always describes the ``Expression`` node type (and operator name for
    [`MathFunc`][qprogram.MathFunc]), not the value. Plain numeric literals contribute nothing — they're not
    Expression nodes. Operations call this on each Expression-typed instance attribute they carry.

    Args:
        value (object): Anything that can appear as an expression operand. A value that is not an
            [`Expression`][qprogram.Expression] contributes nothing.

    Returns:
        Set of capability tokens contributed by the value and its descendants.
    """
    from qprogram.variable import (  # ruff: ignore[import-outside-top-level]
        BinaryOp,
        Comparison,
        Constant,
        Expression,
        LogicalBinaryOp,
        LogicalNot,
        MathFunc,
        MeasurementRef,
        UnaryOp,
        Variable,
        Where,
    )

    if isinstance(value, Constant):
        return {"expr.constant"}
    if isinstance(value, Variable):
        return {"expr.variable"}
    if isinstance(value, MeasurementRef):
        return {"expr.measurement_ref"}
    if isinstance(value, BinaryOp):
        return {"expr.binary_op"} | expression_tokens(value.left) | expression_tokens(value.right)
    if isinstance(value, UnaryOp):
        return {"expr.unary_op"} | expression_tokens(value.operand)
    if isinstance(value, Comparison):
        return {"expr.comparison"} | expression_tokens(value.left) | expression_tokens(value.right)
    if isinstance(value, LogicalBinaryOp):
        return {"expr.logical_and_or"} | expression_tokens(value.left) | expression_tokens(value.right)
    if isinstance(value, LogicalNot):
        return {"expr.logical_not"} | expression_tokens(value.operand)
    if isinstance(value, Where):
        return (
            {"expr.where"}
            | expression_tokens(value.condition)
            | expression_tokens(value.then)
            | expression_tokens(value.else_)
        )
    if isinstance(value, MathFunc):
        tokens = {f"expr.math.{value.name}"}
        for op in value.operands:
            tokens |= expression_tokens(op)
        return tokens
    # forward-compat: unknown Expression subclass
    if isinstance(value, Expression):
        return set()
    return set()

measurement_field_token

measurement_field_token(field: str) -> str

Return the capability token for a measurement field name ("iq""measure.fields.iq").

Parameters:

  • field (str) –

    A measurement field name, core or vendor-registered.

Returns:

  • str

    The token a profile lists to advertise support for that field.

Source code in src/qprogram/protocol.py
def measurement_field_token(field: str) -> str:
    """Return the capability token for a measurement field name (``"iq"`` → ``"measure.fields.iq"``).

    Args:
        field (str): A measurement field name, core or vendor-registered.

    Returns:
        The token a profile lists to advertise support for that field.
    """
    return f"{MEASUREMENT_FIELD_TOKEN_PREFIX}{field}"

known_measurement_fields

known_measurement_fields() -> set[str]

Return every currently registered measurement field name (core plus vendor).

Derived from CAPABILITY_REGISTRY rather than from MeasurementField, so vendor fields are included the moment their token is registered. Used by qprogram.operations.operation.normalize_fields to reject typos at the measure(...) call site, and by editor tooling to offer completions.

Returns:

  • set[str]

    Every field name that currently has a measure.fields.<name> token registered.

Source code in src/qprogram/protocol.py
def known_measurement_fields() -> set[str]:
    """Return every currently registered measurement field name (core plus vendor).

    Derived from `CAPABILITY_REGISTRY` rather than from `MeasurementField`, so vendor
    fields are included the moment their token is registered. Used by
    [`qprogram.operations.operation.normalize_fields`][] to reject typos at the ``measure(...)``
    call site, and by editor tooling to offer completions.

    Returns:
        Every field name that currently has a ``measure.fields.<name>`` token registered.
    """
    return {
        t.removeprefix(MEASUREMENT_FIELD_TOKEN_PREFIX)
        for t in CAPABILITY_REGISTRY
        if t.startswith(MEASUREMENT_FIELD_TOKEN_PREFIX)
    }

Reserved keywords

A frozenset of the identifiers the .qp grammar reserves. Variable ids, fragment names, and vendor namespaces are all checked against it, and Reserved keywords lists the words with what each one is kept for.

RESERVED_KEYWORDS module-attribute

RESERVED_KEYWORDS: Final[frozenset[str]] = frozenset(
    {
        "var",
        "for",
        "in",
        "and",
        "or",
        "not",
        "if",
        "else",
        "elif",
        "while",
        "until",
        "break",
        "continue",
        "return",
        "fragment",
        "def",
        "gate",
        "case",
        "match",
        "repeat",
        "where",
        "let",
        "const",
        "import",
        "from",
        "as",
        "true",
        "false",
        "null",
    }
)

Identifiers reserved for future QProgram syntax — rejected as Variable ids, Fragment names, and vendor namespace names.

Errors

Every error QProgram raises while a program is built, parsed, or run derives from QProgramError, so one except covers all of it. UnsupportedOperationError, BusNotAvailableError, WaveformResolutionError, CompilationError, and HardwareError are the platform-side half of the hierarchy, defined here so that user code can catch one class per failure mode whichever backend is in use; of the five, only UnsupportedOperationError is raised in this repository, by ReferencePlatform.execute. Errors covers when each one fires and what it carries.

QProgramError

Bases: Exception

Root of the QProgram exception hierarchy.

Catch this for "QProgram raised something"; prefer a more specific subclass when you do care why.

ValidationError

Bases: QProgramError

A construction-time validation failure.

Raised when a program is being assembled and an operation rejects its arguments — e.g. an IQ waveform on a single-channel bus, a measure() on a bus without ADC, duplicate variable ids, or a measurement-handle name collision.

Deliberately not a ValueError: construction validation deserves a catch of its own, and inheriting ValueError would turn except ValueError into an accidental catch-all for it. Two subclasses do extend ValueErrorInvalidVariableIdError and UnassignedVariableError — because a malformed identifier and an expression that has no value really are bad-value errors in the ordinary Python sense.

InvalidVariableIdError

InvalidVariableIdError(id: str, *, reserved: bool = False)

Bases: ValidationError, ValueError

A variable id rejected on its pattern or because it is reserved.

Also a ValueError, since a malformed identifier is a bad value in the ordinary Python sense — except ValueError around variable construction catches it.

Parameters:

  • id (str) –

    The offending identifier. Available at catch-time as id.

  • reserved (bool, default: False ) –

    True if id matches the identifier pattern but is one of RESERVED_KEYWORDS; False for an outright pattern violation. Available at catch-time as reserved.

Source code in src/qprogram/errors.py
def __init__(self, id: str, *, reserved: bool = False) -> None:  # ruff: ignore[builtin-argument-shadowing]
    if reserved:
        message = (
            f"Variable id {id!r} is reserved for future QProgram syntax "
            f"(see qprogram.RESERVED_KEYWORDS). Pick a non-reserved id "
            f"such as {id + '_var'!r}, or carry the original name in the "
            f"optional `label` argument."
        )
    else:
        message = (
            f"Variable id {id!r} is invalid: must match "
            f"[A-Za-z_][A-Za-z0-9_]* (letters, digits, underscores only; "
            f"cannot start with a digit, no spaces or special characters). "
            f"Use the optional `label` for human-readable names."
        )
    super().__init__(message)
    self.id = id
    self.reserved = reserved

UnassignedVariableError

UnassignedVariableError(expression: Expression)

Bases: ValidationError, ValueError

An Expression evaluated while it still references unbound Variable s.

Raised by Expression.evaluate_or_raise. Also a ValueError, since an expression over unbound variables has no value to give.

Parameters:

  • expression (Expression) –

    The expression that failed to evaluate. Kept as expression.

Attributes:

  • free_variables (set[Variable]) –

    The unbound variables that caused the failure, collected from expression at construction.

Source code in src/qprogram/errors.py
def __init__(self, expression: Expression) -> None:
    free = expression.variables()
    super().__init__(
        f"Cannot evaluate expression {expression!r}: unassigned variable(s) {free!r}",
    )
    self.expression: Expression = expression
    self.free_variables: set[Variable] = free

ParseError

ParseError(message: str, line_num: int = 0)

Bases: QProgramError

Error during .qp file parsing.

Direct child of QProgramError, distinct from ValidationError — validation runs on in-memory programs, parsing fails on malformed input text.

Parameters:

  • message (str) –

    Human-readable error description.

  • line_num (int, default: 0 ) –

    1-based line number of the offending input, or 0 for whole-file errors.

Source code in src/qprogram/serialization/parser.py
def __init__(self, message: str, line_num: int = 0) -> None:
    self.line_num = line_num
    super().__init__(f"Line {line_num}: {message}" if line_num else message)

SerializationError

Bases: QProgramError

A program the .qp writer cannot faithfully serialize.

Raised instead of emitting lossy or unparseable output — e.g. an operation or block class that was never registered with the serialization registry, a vendor operation whose extension forgot to call register_vendor_version, or an attribute value of a type the format has no representation for. The write-side counterpart of ParseError, but not a guarantee against one: the writer only refuses what it can see is unrepresentable, so a program can serialize cleanly and still fail to load. An inline waveform argument holding a math function is the known case, since Gaussian(amplitude=sin(phi), ...) writes as-is and the grammar accepts no function call in a constructor argument.

VendorActivationError

Bases: QProgramError

A discovered vendor extension that could not be activated.

Raised by try_activate_vendor (and, during .qp parsing, wrapped into a ParseError) when a qprogram.vendors entry point's import target raises, or imports without calling register_vendor_version — i.e. the package is installed but broken. A vendor that is not installed at all is not this error: discovery reports "no matching extension".

UnsupportedOperationError

Bases: QProgramError

Platform-side: an operation the backend cannot lower to its hardware.

Typical causes: a vendor op the backend doesn't implement, or a control-flow construct outside the platform's supported feature set.

BusNotAvailableError

Bases: QProgramError

Platform-side: a bus the program references but this backend doesn't expose.

The program is structurally well-formed; the incompatibility is with this particular platform. Use ValidationError for construction-time bus issues.

WaveformResolutionError

Bases: QProgramError

Platform-side: a string waveform name that reached execution without a concrete waveform.

Resolve names before execution with QProgram.with_waveforms (or WaveformLibrary.apply); this is raised when one was missed or the WaveformLibrary used had no entry for it.

CompilationError

Bases: QProgramError

Platform-side: a lowered representation that failed to compile.

Catch-all for backend-internal failures that don't fit the other platform errors (timing constraints, resource over-allocation, code-generation bugs).

HardwareError

Bases: QProgramError

Platform-side: an instrument-level failure during execution.

Covers driver errors, SCPI failures, lost trigger pulses, and anything else surfacing during execution rather than at compile or validate time.