open-standards/wai/wai-quantum/docs/extension-points.md
Transaction Science 223ed634bf wai-quantum docs: the example's commands as the registry workflow runs them
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]>
2026-10-31 23:30:48 -04:00

7.7 KiB
Raw Permalink Blame History

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.