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

125 lines
7.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.
```sh runnable
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.
```rust runnable features=quantum_engine
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:
```sh norun=ci:wai-quantum-registry.yml
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](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.