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]>
125 lines
7.7 KiB
Markdown
125 lines
7.7 KiB
Markdown
# 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.
|