Errors¶
QProgram has a single-rooted exception hierarchy. Everything the library raises
about a program, and everything a platform raises through the contract, is a
subclass of QProgramError, so one except covers the lot. Catch the level of
granularity you need.
Argument types are the exception, and they raise a plain TypeError on purpose,
because a value with no expression or waveform form is a Python type error
rather than a fact about a program. Two families are documented. Expression
construction rejects an operand it cannot represent, so a bool where an
Expression belongs, or a Variable in a context that calls bool() on it,
comes back as a TypeError whose message names the alternative to write; see
Comparisons and logical combination.
A malformed inline waveform constructor in a .qp file escapes as the waveform
class's own TypeError, described under
Parse-time errors.
The hierarchy¶
QProgramError
ValidationError
InvalidVariableIdError (also ValueError)
UnassignedVariableError (also ValueError)
ParseError
SerializationError
VendorActivationError
UnsupportedOperationError
BusNotAvailableError
WaveformResolutionError
CompilationError
HardwareError
Every class here is defined in src/qprogram/errors.py except ParseError,
which lives in qprogram.serialization.parser because it is part of the
parser's own surface. All twelve are re-exported at the top level, so
qp.ValidationError and qp.ParseError both resolve. qp.loads, qp.load,
and qp.ParseError come through a module-level __getattr__ on first
attribute access rather than at import time, because the parser imports
QProgram and importing it eagerly from qprogram/__init__.py would close a
cycle. qp.errors reaches the module holding the other eleven;
qp.errors.ParseError does not exist.
Choosing what to catch¶
| What you write | What it catches |
|---|---|
except qp.QProgramError |
Anything QProgram-related, core or platform. |
except qp.ValidationError |
Construction-time validation, including its two subclasses. |
except qp.ParseError |
A .qp or .wfl document that does not parse. |
except qp.SerializationError |
A program or waveform library the writer cannot represent. |
except qp.VendorActivationError |
A vendor extension that is installed but fails to activate. |
except qp.UnsupportedOperationError |
An error diagnostic, or an operation a backend cannot lower. |
except (qp.CompilationError, qp.HardwareError) |
Backend compile failures and instrument failures. |
except ValueError |
InvalidVariableIdError and UnassignedVariableError, nothing else. |
Construction-time validation¶
ValidationError¶
Raised while a program is being assembled, whenever an operation, block, or sweep source rejects its arguments. The checks run in the constructor or the builder method, not in a later pass, so the traceback points at the line that built the offending node.
| Module | What it rejects |
|---|---|
qprogram.qprogram |
A waveform whose channel count does not match the bus, measure() on a bus with acquires=False, a duplicate variable id, a BusRef from a different BusSchema, an empty or duplicate measurement name, a sweep() that never picked values, sync([]), a call() that is a self-call or names a fragment built against another schema, a broken if_/elif_/else_ chain, a conditional condition that is not a comparison over a measurement-state ref and int literals, and rebind(naming=...) on a program with no schema or a rebind() that leaves raw-string buses unported |
qprogram.fragments |
A fragment name that is malformed or reserved, a parameter colliding with a local variable, the wrong number of call arguments, an unknown or duplicated keyword, an unsupported argument type, a call cycle, and an expansion result that is not a bus or a waveform |
qprogram.sweeps.builtin |
Non-numeric or non-finite bounds, num < 1, a zero step, a step pointing away from stop, non-positive Logspace bounds, and a Values or File array that is empty or not 1-D |
qprogram.sweeps.combinators |
A Repeat count below 1, a non-integer Rotate offset, Concat given a single source or none, and any combinator argument that is a callable rather than a SweepSource |
qprogram.blocks |
A Sweep source that is a callable or not a 1-D sequence, a Parallel with fewer than two loops or with mismatched iteration counts, Average(shots) below 1, and appending directly to a Conditional |
qprogram.operations.operation |
A fields= that is a bare string, not iterable, empty, or names a field no capability token registers |
qprogram.waveforms.iq_pair |
An IQPair whose I and Q channels have different concrete durations |
qprogram.result |
An empty MeasurementHandle name, and QProgramResult.get(field=None) |
qprogram.waveform_library |
An empty waveform name, and a WaveformLibrary.set() whose element/idx/kind combination matches none of the three tiers |
A message names the offending value and the fix rather than the rule that was broken:
import qprogram as qp
schema = qp.BusSchema.flux_tunable_transmon()
q = schema.q
program = qp.QProgram(schema=schema)
program.play(q[0].drive, qp.waveforms.Square(0.5, 100))
# ValidationError: Bus 'q0/drive' is an IQ channel but received a
# single-channel Waveform (Square). Use an IQWaveform (e.g. IQPair, IQDrag)
# instead.
Three more, from a measure() on a drive bus, a | composition whose loops
run different numbers of iterations, and an unknown measurement field:
ValidationError: Bus 'q0/drive' does not support acquisition
(acquires=False). measure() can only be called on buses with an ADC
(e.g. readout buses).
ValidationError: parallel loops must have the same number of iterations to
advance in lockstep; got Sweep('a'): 11, Sweep('b'): 4
ValidationError: unknown measurement field(s) ['bogus']. Known fields:
['iq', 'raw', 'state']. A vendor extension adds its own by registering
`measure.fields.<name>` via qprogram.protocol.register_capability_tokens.
The base ValidationError does not extend ValueError. Construction
validation is common enough in this library that inheriting ValueError
would turn a generic except ValueError into an accidental catch-all for it.
Catch qp.ValidationError or qp.QProgramError instead.
InvalidVariableIdError¶
A Variable.id fails the identifier rules. Two flavors share the class: a
pattern failure, where the id does not match [A-Za-z_][A-Za-z0-9_]*, and a
reserved keyword, where the id matches the pattern but is one of
the reserved keywords. The reserved attribute distinguishes
them, and id carries the offending string:
import qprogram as qp
program = qp.QProgram()
try:
program.variable("if")
except qp.InvalidVariableIdError as e:
print((e.id, e.reserved)) # ('if', True)
Both messages suggest the fix. The reserved one proposes appending _var and
points at the optional label argument for the human-readable name; the
pattern one spells the regular expression out:
Variable id 'if' is reserved for future QProgram syntax (see
qprogram.RESERVED_KEYWORDS). Pick a non-reserved id such as 'if_var', or
carry the original name in the optional `label` argument.
Variable id '1freq' is invalid: must match [A-Za-z_][A-Za-z0-9_]* (letters,
digits, underscores only; cannot start with a digit, no spaces or special
characters). Use the optional `label` for human-readable names.
The class also subclasses ValueError, so except ValueError around
variable construction catches an invalid identifier as well.
UnassignedVariableError¶
Expression.evaluate_or_raise() ran while at least one variable in the
expression was still unbound. The error carries the expression and the set of
free variables:
import qprogram as qp
program = qp.QProgram()
freq = program.variable("freq")
expr = freq * 2 + 100
try:
expr.evaluate_or_raise()
except qp.UnassignedVariableError as e:
e.expression # the offending expression
e.free_variables # {freq}
The message reads
Cannot evaluate expression <repr>: unassigned variable(s) <set>. The same
error comes out of qp.simulate when an operation holds an expression that no
enclosing loop binds, since the reference executor evaluates every operand
before it runs the operation. Like InvalidVariableIdError, this class
subclasses ValueError too.
Write-time errors¶
QProgram writes two formats, and both go through the same exception.
qp.dumps and qp.save write a program as .qp;
WaveformLibrary has its own
dumps and save, which write a calibration library as .wfl.
SerializationError¶
Raised instead of emitting output that is lossy or would not parse back. On
the .qp side that covers an operation or block class that was never
registered with the serialization registry, a vendor operation whose
extension never called register_vendor_version, an attribute value with no
.qp representation (a dict with non-string keys, an array with more than
one dimension), a fragment passed to dumps directly instead of the program
that calls it, a fragment call with an unbound parameter, two different
fragments with the same name reachable from one program, and a measurement
name that cannot survive the unquoted name.field wire form of a conditional
reference. On the .wfl side it covers an entry whose waveform is not
concrete.
Arrays are never truncated: Arbitrary samples and Values sweeps are
written in full, and the text reparses to an equal program.
SerializationError: Cannot serialize operation class 'MyOp': it is not
registered with the .qp serializer. Core ops register in
qprogram.serialization._specs; vendor ops must call
register_vendor_operation(...) at import time.
SerializationError: measurement name 'my readout' is referenced in a
conditional but contains characters that don't survive the unquoted
`<name>.<field>` wire form (whitespace, quotes, dots, brackets, '#', or ',').
Pass a token-safe name= to measure() when you intend to branch on the result.
SerializationError: cannot serialize the waveform stored under name 'pi': a
WaveformLibrary must hold concrete waveforms (no Variables / symbolic
parameters). Underlying error: 'v'
A clean dumps is not by itself a promise that the text parses back. The
writer emits an expression wherever the AST holds one, including inside a
waveform or sweep-source constructor argument, and the parser does not accept
an expression in that position:
import qprogram as qp
program = qp.QProgram()
phi = program.variable("phi")
pulse = qp.waveforms.Gaussian(amplitude=qp.sin(phi), duration=40, sigma=8)
program.play("drive_q0", pulse)
text = qp.dumps(program)
# play "drive_q0" Gaussian(amplitude=sin(phi), duration=40, sigma=8)
qp.loads(text)
# ParseError: Unknown waveform or sweep source type: sin
Gaussian(amplitude=(phi * 2), ...) fails in the same place, with an empty
class name in the message: any argument containing a ( is routed to the
constructor parser, and the class name it reads is the text before the
opening bracket, which here is nothing at all. Keep constructor
arguments to numbers, quoted strings, and bare variable references, the
shapes the format documents, and
a file that writes without a SerializationError parses back into an equal
program. Nothing in the writer checks this for you.
Parse-time errors¶
ParseError is what qp.load and qp.loads raise on a .qp document that
does not follow the grammar or fails a compatibility check, and what
WaveformLibrary.load and WaveformLibrary.loads raise on a .wfl
document. Compatibility accounts for the first group of .qp cases: a
missing #!QProgram header, a header whose major version differs from the
parser's, a require declaration that cannot be satisfied, and require
lines that do not sit directly after the header. The rest are grammar:
a second schema: declaration, a schema with no elements or a malformed
info= value, a bus path that does not resolve against the schema, a
duplicate var id, a fragment defined after body: or called before it is
defined, an elif or else without a matching if, an unknown operation or
block keyword, an unknown sweep source, and an argument list that does not fit
the signature.
ParseError: Line 1: Missing #!QProgram header
ParseError: Line 1: Unsupported format version 9.0
ParseError: Line 5: duplicate variable id 'x'
ParseError: Line 2: file requires vendor 'nosuchvendor' 1.0 but no matching
extension is registered in this environment — install the package that
declares the 'qprogram.vendors' entry point for 'nosuchvendor', or import the
extension before loading
ParseError: Line 2: file requires myvendor 99.0 (major 99); installed
myvendor is 1.2.0 (major 1) — major versions must match
ParseError: Line 2: file requires myvendor 1.9 or compatible; installed
myvendor is 1.2.0 — minor version too old
Majors must match exactly, the file's minor must be no newer than the installed extension's, and a patch component is read but ignored.
An id declared in a .qp file is checked twice, and the two failures come
back differently. A malformed id is rejected by the parser's own pattern
check, so var 1x raises a ParseError carrying the line number. A
reserved id passes that check and is rejected by the Variable constructor
instead, so var if raises InvalidVariableIdError with no line
information:
import qprogram as qp
qp.loads("#!QProgram 1.0\n\nbody:\n var 1x\n")
# ParseError: Line 4: variable id '1x' is invalid: must match
# [A-Za-z_][A-Za-z0-9_]* (no spaces or special characters)
qp.loads("#!QProgram 1.0\n\nbody:\n var if\n")
# InvalidVariableIdError: Variable id 'if' is reserved for future QProgram
# syntax ...
Inline constructors are where an argument-list mistake leaves the hierarchy
entirely. The parser hands the arguments it read straight to the waveform
class, so a missing or misspelled constructor argument surfaces as that
class's own TypeError:
import qprogram as qp
qp.loads('#!QProgram 1.0\n\nbody:\n play "b" Gaussian(amplitude=0.5)\n')
# TypeError: Gaussian.__init__() missing 2 required positional arguments:
# 'duration' and 'sigma'
A sweep source nested inside a combinator's argument list escapes the same
way, because it reaches the class through the same argument parser. Only the
outermost sweep-source constructor has its TypeError wrapped, so
for x in Range(start=0): is a ParseError carrying the line number while
for x in Concat(sources=[Range(start=0)]): is a bare TypeError naming the
missing stop argument.
Catch (qp.ParseError, TypeError) around load and loads if you are
parsing files you did not write.
Most messages name the 1-based line number and carry it separately as
ParseError.line_num, and the string form gains a Line N: prefix when
line_num is non-zero. Two raise sites in the parser omit it, both in
helpers that run below the line loop and have no view of the cursor: the
unknown-class check in _parse_waveform_expr, and the operand promotion in
_to_expression. Everything else does carry a line, including the sweep-source
lookup on a for header, which is the near twin of the waveform lookup. So two
almost identical mistakes read differently:
import qprogram as qp
qp.loads('#!QProgram 1.0\n\nbody:\n play "b" Bogus(amplitude=0.5)\n')
# ParseError: Unknown waveform or sweep source type: Bogus
# ... with line_num == 0, even though the offending line is line 4
qp.loads("#!QProgram 1.0\n\nbody:\n var x\n for x in Bogus(start=1):\n sync\n")
# ParseError: Line 5: unknown sweep source 'Bogus'; registered sources are
# ['Concat', 'File', 'Linspace', 'Logspace', 'Range', 'Repeat', 'Rotate',
# 'Values']
# ... with line_num == 5
qp.loads('#!QProgram 1.0\n\nbody:\n var x\n set_phase "b" ("a" + x)\n')
# ParseError: cannot use 'a' (_QuotedStr) as an expression operand
# ... with line_num == 0
So treat line_num == 0 as "no line attributed", not as "whole-file error".
Vendor extension activation¶
VendorActivationError is raised by qp.try_activate_vendor(name) when a
qprogram.vendors entry point claims name but its import target raises, or
imports without calling register_vendor_version. The extension is installed
and broken, which is a different failure from not being installed at all:
try_activate_vendor returns False in that case and leaves the decision to
the caller.
VendorActivationError: vendor extension for 'myvendor' is installed (entry
point 'qprogram_myvendor:activate') but failed to import: ImportError: ...
VendorActivationError: vendor extension for 'myvendor' imported from entry
point 'qprogram_myvendor:activate' but did not register a protocol version;
the package must call register_vendor_version('myvendor', '<x.y.z>') on
import
Reading a .qp file whose require line names a vendor triggers activation
by default, and the parser wraps any VendorActivationError in a
ParseError carrying the require line's number. Passing
auto_activate=False to qp.loads or qp.load turns the discovery off, in
which case an unregistered vendor is a ParseError whose hint asks you to
import the extension yourself.
Platform-side errors¶
These five classes give platforms one hierarchy to report failures through,
so the catch surface is uniform across backends. Four of them
(BusNotAvailableError, WaveformResolutionError, CompilationError, and
HardwareError) are defined in qprogram and raised only by platforms; no
core code path raises them. UnsupportedOperationError is the exception:
core raises it too.
UnsupportedOperationError¶
The platform cannot run an operation as written. Core raises it from
ReferencePlatform.execute(), the engine behind qp.simulate, on any
severity="error" diagnostic the validator reports, listing every one in the
message:
import qprogram as qp
square = qp.waveforms.Square(0.5, 100)
pulse = qp.waveforms.IQPair(square, qp.waveforms.Square(0.0, 100))
program = qp.QProgram()
handle = program.measure("readout_q0", pulse, pulse, fields=[qp.MeasurementField.IQ])
with program.if_(handle.state == 0):
program.sync()
qp.simulate(program)
# UnsupportedOperationError: program is not executable on the reference
# platform:
# [error] missing-classification: Conditional references m0.state, but the
# measurement does not request state classification (add
# MeasurementField.STATE to fields=) (at body[1])
Each line is a Diagnostic rendered as [severity] code: message (at path);
the ten codes the validator emits are tabulated with their severities and the
condition that produces each under
Diagnostics.
A hardware backend raises it for the same reason, and for anything it cannot lower: a vendor operation it does not implement, a control-flow construct its compiler does not support.
BusNotAvailableError¶
The program references a bus name the backend does not expose. The program is
structurally well-formed; it just does not fit this particular platform. A
bus problem caught while the program is being built is a ValidationError
instead.
WaveformResolutionError¶
A string waveform alias reached execution without a concrete waveform behind
it, usually a name missing from QProgram.with_waveforms or from the
WaveformLibrary that fed it. The reference platform does not raise this,
because it models measurements only and never reads waveform content:
qp.simulate on a program full of unresolved aliases returns a
QProgramResult as usual.
CompilationError¶
A backend-internal failure produced an invalid lowered representation: timing constraints not satisfied, resource over-allocation, code-generation bugs, anything that surfaces during compilation but does not fit the other classes.
HardwareError¶
Runtime failure at the instrument level: driver errors, SCPI failures, lost trigger pulses. Anything raised during execution rather than at compile or validate time.
Which error to expect¶
| Situation | Error |
|---|---|
| Wrong waveform channel count for the bus | ValidationError |
measure() on a bus with acquires=False |
ValidationError |
Duplicate variable id, or a BusRef from another schema |
ValidationError |
| Sweep bounds, step, or length that yield no iterations | ValidationError |
qp.Variable("if") or qp.Variable("1freq") |
InvalidVariableIdError |
expr.evaluate_or_raise() with an unbound variable |
UnassignedVariableError |
qp.simulate on an operation no loop binds a variable for |
UnassignedVariableError |
var if in a .qp file |
InvalidVariableIdError, no line number |
var 1x in a .qp file |
ParseError with the line number |
require myvendor 1.0 with nothing installed |
ParseError |
require myvendor 99.0 against an older install |
ParseError |
| Vendor extension installed but raising on import | VendorActivationError, wrapped in ParseError during loads |
Bad argument list in an inline .qp waveform constructor |
TypeError, outside the hierarchy |
qp.and_(cond, True), or a Variable where Python calls bool() |
TypeError, outside the hierarchy |
Unregistered operation or block class in qp.dumps |
SerializationError |
WaveformLibrary entry holding a Variable |
SerializationError |
Any error diagnostic under qp.simulate or a platform execute |
UnsupportedOperationError |
| Platform missing the bus you wrote | BusNotAvailableError (platform side) |
| A waveform alias still unresolved when execution starts | WaveformResolutionError (platform side) |
| Anything else from a platform you do not recognize | QProgramError |
Why two parents on some classes¶
InvalidVariableIdError and UnassignedVariableError inherit from both
ValidationError and ValueError. Each reports a value that is wrong on its
own terms: an identifier that is not a legal identifier, an expression with
no number to compute. That is exactly what ValueError means in Python, so
both spellings catch them. Every other class in the hierarchy descends from
QProgramError alone.
Diagnostic is not an exception¶
The validator surface lives next door, but it is not part of this hierarchy.
qp.validate(program, caps) returns a tuple (list[Diagnostic],
ExecutionPlan) rather than raising. The list comes back, the caller decides
what to do. A Diagnostic is a frozen dataclass with severity
("error", "warning", or "info"), code, message, node, path,
capability, limit, and domain fields.
Platforms typically translate any severity="error" diagnostic into one of
the platform-side exceptions above, UnsupportedOperationError being the
usual choice, so end users see one consistent error class regardless of which
axis tripped. A severity="warning" diagnostic means the program runs but in
a degraded way; ReferencePlatform.execute passes those to
warnings.warn as a qp.ExecutionWarning rather than raising, which is how
the "forced-host" notice on a block that lost real-time dispatch reaches
the caller. severity="info" diagnostics, such as the
"reorderable-averaging" hint, are neither raised nor warned; they come back
in the list as advisory output.
See Capabilities, diagnostics, and profiles for the validator walkthrough, and Diagnostics for the ten codes with their severities and producing conditions.