The docs check holds a block that cannot run here to the workflow named: the example's test and run, with --locked, as wai-quantum-registry.yml runs them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
7.7 KiB
Extension points: methods, passes, noise channels and formats
The engine plans, runs, compiles and converts through one registry
(engine::registry). Each simulation method, compiler pass, noise channel
and program format in it is a component behind a trait, and every built-in
is registered through the same trait a host's own implements. A host that
embeds the engine as a Rust library registers its own beside them; they run
where the built-ins run, and the engine checks what they give.
wai-quantum components # every component here, its version and digest
wai-quantum components --json # with each one's descriptor
The command line, both MCP servers (list_components) and the job service
(GET /v1, quantum_service_info) list the built-ins. None of them loads a
component from a file, a path or a request: registration happens in the
process of the host that embeds the engine, in Rust.
The four traits
| Trait | It declares | The engine calls | The engine checks |
|---|---|---|---|
Method |
its name, version, accuracy classes, what circuits and answers it takes (its descriptor), whether it models noise, where auto tries it |
plan: admit the job (or refuse it) and estimate its memory and work; run: carry the plan out |
a job with noise is refused for a method that models none; every channel the job names must be one it takes; its result must carry an accuracy class it declared and leave the members the engine writes as they are |
Pass |
its name, version, the gates it takes and gives, and how its result is checked: by the equivalence ladder or by a distance bound (Check::Ladder, Check::Fidelity) |
run, after the level's own passes and before twirling and decoupling, when a compile's passes names it |
its gates on the program's qubits and reading back; its output held to its input on the whole circuit by the check it declares (never by a hash); a distance within its bound and within what is left of the compile's ε budget |
Channel |
its name, version, parameters with their ranges, the qubits it acts on, its Kraus operators and, where it is one, its Pauli form | kraus and pauli at a job's values; taken_by, for how each noisy method takes it |
its operators completely positive and trace preserving (their sum of K†K within 1e-9 of the identity) at every job's values, and its Pauli form a distribution whose channel is its operators' |
FrontEnd |
its name, version, whether it is binary, its file extensions and profiles | read (a program, as OpenQASM 3) and write |
what it reads must read as OpenQASM 3; a conversion holds what it writes to the program by the equivalence ladder, as for the built-in formats |
A registered pass is checked by the engine on the whole circuit. A built-in pass checks itself where it acts, as its record in the compile certificate says, and the tests hold every built-in to the gates it declares it gives.
How a built-in is identified, and a registered one
Every component has an identity: its kind, its name, its version and the BLAKE3 digest of its descriptor in canonical JSON (keys sorted, no whitespace), and whether it is built in. A built-in's version is the engine's.
A result made by built-ins alone is the result it always was, byte for byte:
it names its method, its compile's passes, its noise's channels and the
engine's version, and the receipt seals them. The digests for that version
are the ones wai-quantum components lists. A result that a registered
component took part in also carries components: the identity of every
component that made it (its program's format, its compile's passes, its
noise channels, its method), each registered one marked "origin": "registered" and "certified": false. The receipt seals it with the rest of
the result, so a registered component cannot be passed off as a built-in, and
a name a built-in holds is never registered.
Writing one
A noise channel, registered and run: X with probability p after each
gate, which the density matrix takes through its Kraus operators.
use wai_quantum::engine::registry::{Channel, Component, Param, Registry, Value, json};
use wai_quantum::gates::C;
struct BitFlip;
impl Channel for BitFlip {
fn name(&self) -> &str { "bit_flip" }
fn version(&self) -> &str { "1.0.0" }
fn title(&self) -> &str { "bit flip" }
fn descriptor(&self) -> Value { json!({ "does": "X with probability p after each gate" }) }
fn params(&self) -> Vec<Param> { vec![Param::new("p", 0.0, 1.0, "a probability")] }
fn arity(&self) -> u32 { 1 }
fn kraus(&self, v: &[f64]) -> Vec<Vec<C>> {
let (a, b) = ((1.0 - v[0]).sqrt(), v[0].sqrt());
let c = |re| C { re, im: 0.0 };
vec![vec![c(a), c(0.0), c(0.0), c(a)], vec![c(0.0), c(b), c(b), c(0.0)]]
}
fn pauli(&self, v: &[f64]) -> Option<Vec<(f64, String)>> {
Some(vec![(1.0 - v[0], "I".into()), (v[0], "X".into())])
}
}
fn main() {
let mut r = Registry::with_builtins();
let id = r.register(Component::channel(BitFlip)).expect("registers");
let job = json!({ "qasm": "OPENQASM 3.0;\nqubit q;\nx q;\n", "observables": ["Z0"], "noise": { "bit_flip": 0.1 } });
let (plan, result) = r.plan_and_run(&job, 1 << 30).expect("runs");
assert_eq!(plan.method, "density_matrix");
let z = result["expectations"][0]["value"].as_f64().unwrap();
assert!((z - (-0.8)).abs() < 1e-12, "⟨Z⟩ = −1 + 2p");
assert_eq!(result["components"][1]["digest"], json!(id.digest));
println!("{}", result["components"]);
}
examples/extension_points.rs has one of each, a method (basis_table, for
circuits that permute basis states), a pass (cancel_pairs), a channel
(bit_flip) and a format (gate_list), written against the public traits
alone, registered, and run end to end with their tests:
cargo test --locked --release --features quantum_engine,quantum_receipt --example extension_points
cargo run --locked --release --features quantum_engine,quantum_receipt --example extension_points
What is refused, and how
| Code | When |
|---|---|
J060 |
a compile's passes names a pass this host does not hold |
J061 |
a noise channel the method cannot take: a method that models no noise, amplitude damping on the stabilizer's Pauli frames, a registered channel on a method that takes the built-in channels alone |
J062 |
a channel whose operators fail the validity check at the job's values |
J063 |
a registration the registry does not take: a name its kind holds already (a built-in's among them), a name or version that is not plain text, a descriptor that is not a JSON object, a channel on other than one or two qubits, a pass whose fidelity check states no finite bound |
J064 |
a registered component's answer that breaks the contract: a method's result without a class it declared, or one that writes a member the engine writes; a pass's gates off the program's qubits or that do not read back |
J065 |
an intent on a job whose noise names a registered channel: the intent checks model the built-in channels alone |
A method named for a job it cannot take is refused with J011, as a built-in
is, and auto passes over it to the next. job-refusals.md
has every code.
Determinism
Components are kept in the order they are registered, the built-ins first in
a fixed order, and every listing and every choice follows that order: auto
tries methods by their rank (the built-ins hold 10, 20, 30, …, in the order
they have always been tried), equal ranks in the order of registration. A
registered component's results are the same bytes for the same job, as the
built-ins' are, when the component itself is deterministic. Device-target
jobs, sets and sweeps run on the built-in components.