From 56b47afae126834f79429c067cc46b42d954e3e6 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 31 Oct 2026 02:44:56 -0400 Subject: [PATCH 1/4] wai-quantum engine::registry: methods, passes, noise channels and formats behind traits, the built-ins registered through them Method, Pass, Channel and FrontEnd, and one registry the planner, the run, the compile, the conversions and the verdict go through. Every built-in registers through the same trait a host's own implements: the twelve methods and the pulse simulator, the eight passes, the five channels and the three formats. Registry::with_builtins() and register(...) are explicit and stay in the process. The engine checks what a registered component gives: a method's class and the members the engine writes, a pass's output against its input on the whole circuit by the ladder or a distance bound, a channel's Kraus operators completely positive and trace preserving at each job's values, a format's program reading. A result a registered component took part in carries `components`, each identity (name, version, descriptor digest) marked registered and certified false, sealed by its receipt; a result of built-ins alone gives the bytes it gave, the eight pinned job digests holding through any registry. Refusal codes J060 to J065. The example examples/extension_points.rs, docs/extension-points.md, and the CI leg wai-quantum-registry.yml. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/wai-quantum-registry.yml | 169 +++ wai/tools/quantum-docs/docs.json | 3 +- wai/wai-quantum/Cargo.toml | 5 + wai/wai-quantum/README.md | 30 + wai/wai-quantum/README.zh-CN.md | 8 + wai/wai-quantum/ci/check_schemas.py | 1 + wai/wai-quantum/docs/extension-points.md | 125 ++ wai/wai-quantum/docs/job-refusals.md | 6 + wai/wai-quantum/examples/extension_points.rs | 332 +++++ .../schemas/compile-result.v1.json | 54 + wai/wai-quantum/schemas/job-result.v1.json | 50 + wai/wai-quantum/schemas/tools.v1.json | 162 +++ wai/wai-quantum/src/engine/catalog.rs | 264 +++- wai/wai-quantum/src/engine/compile.rs | 205 ++- wai/wai-quantum/src/engine/fabric_measured.rs | 3 + wai/wai-quantum/src/engine/formats/mod.rs | 126 +- wai/wai-quantum/src/engine/job.rs | 9 +- wai/wai-quantum/src/engine/mod.rs | 9 + wai/wai-quantum/src/engine/refusal.rs | 8 + .../src/engine/registry/builtin.rs | 394 ++++++ wai/wai-quantum/src/engine/registry/mod.rs | 1259 +++++++++++++++++ wai/wai-quantum/src/engine/registry/tests.rs | 673 +++++++++ wai/wai-quantum/src/engine/run.rs | 211 ++- wai/wai-quantum/src/engine/verdict.rs | 25 +- wai/wai-quantum/src/quantum_noisy.rs | 2 +- 25 files changed, 3932 insertions(+), 201 deletions(-) create mode 100644 .github/workflows/wai-quantum-registry.yml create mode 100644 wai/wai-quantum/docs/extension-points.md create mode 100644 wai/wai-quantum/examples/extension_points.rs create mode 100644 wai/wai-quantum/src/engine/registry/builtin.rs create mode 100644 wai/wai-quantum/src/engine/registry/mod.rs create mode 100644 wai/wai-quantum/src/engine/registry/tests.rs diff --git a/.github/workflows/wai-quantum-registry.yml b/.github/workflows/wai-quantum-registry.yml new file mode 100644 index 000000000..63bf81343 --- /dev/null +++ b/.github/workflows/wai-quantum-registry.yml @@ -0,0 +1,169 @@ +name: wai-quantum extension points + +# The registry (wai/wai-quantum/src/engine/registry/): the traits a simulation +# method, a compiler pass, a noise channel and a program format implement, the +# built-ins registered through them, and a host's own registered beside them. +# The registry's tests hold every built-in to the bytes it gave (the pinned +# job digests, through the built-in registry and through one holding a host's +# components), auto to the order it always tried, the built-in channels and +# passes to what they declare, and a host's method, pass, channel and format +# to what the engine checks of them: each refusal with its code, and its +# identity sealed in the receipt of every result it took part in, marked as +# registered. They run under the full feature set and under the contract +# workflow's, natively on both Linux architectures and under wasm32-wasip2 by +# wasmtime. The example written outside the engine against its public traits +# (wai/wai-quantum/examples/extension_points.rs) is tested and run end to end. + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +on: + push: + branches: [main] + paths: + - 'wai/wai-quantum/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-registry.yml' + pull_request: + paths: + - 'wai/wai-quantum/**' + - 'rust-toolchain.toml' + - '.github/workflows/wai-quantum-registry.yml' + +jobs: + relevant: + name: does this change touch it + runs-on: linux-arm64 + outputs: + run: ${{ steps.gate.outputs.run }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - id: gate + run: | + set -euo pipefail + r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-registry.yml \ + "${{ github.event.pull_request.base.sha }}" \ + "${{ github.event.pull_request.head.sha }}") + echo "this change touches the registry: $r" + echo "run=$r" >> "$GITHUB_OUTPUT" + + native: + name: the registry and the example · ${{ matrix.runner }} + needs: relevant + if: needs.relevant.outputs.run == 'true' + strategy: + fail-fast: false + matrix: + runner: [linux, linux-arm64] + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # A module filter is satisfied by any one test of the module, so each + # test is required by name to have run. + - name: The registry's tests, under the full feature set and the contract's + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features full --lib engine::registry:: 2>&1 | tee "$RUNNER_TEMP/full.log" + cargo test --locked --release --no-default-features --features quantum_engine,quantum_receipt,quantum_qec,quantum_noise --lib engine::registry:: 2>&1 | tee "$RUNNER_TEMP/contract.log" + for log in full contract; do + grep -qE '^test result: ok\. 12 passed; 0 failed' "$RUNNER_TEMP/$log.log" || { echo "::error::the registry's tests did not all pass ($log)"; exit 1; } + for t in every_built_in_registers_through_its_trait_in_a_fixed_order \ + auto_tries_the_built_ins_in_the_order_it_did \ + the_pinned_jobs_give_their_bytes_through_any_registry \ + the_built_in_channels_are_valid_channels \ + the_built_in_passes_give_what_they_declare \ + a_registered_method_is_planned_run_and_named_in_its_result \ + the_receipt_seals_a_registered_component_s_identity \ + registration_refuses_what_it_cannot_hold \ + a_registered_method_s_result_is_held_to_the_contract \ + a_registered_channel_runs_on_the_density_matrix_and_is_refused_elsewhere \ + a_registered_pass_is_checked_by_the_engine \ + a_registered_front_end_converts_and_feeds_a_job; do + grep -q "^test engine::registry::tests::$t \.\.\. ok$" "$RUNNER_TEMP/$log.log" || { echo "::error::$t did not run ($log)"; exit 1; } + done + done + - name: The built-ins' bytes, the refusal table and the job layer beside them + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features full --lib -- engine::job:: engine::refusal:: engine::catalog:: engine::compile:: engine::formats:: 2>&1 | tee "$RUNNER_TEMP/engine.log" + grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/engine.log" || { echo "::error::the engine's tests did not pass"; exit 1; } + for t in engine::job::tests::a_job_that_asks_for_no_verdict_gives_the_bytes_it_gave \ + engine::refusal::tests::the_documented_table_is_the_table; do + grep -q "^test $t \.\.\. ok$" "$RUNNER_TEMP/engine.log" || { echo "::error::$t did not run"; exit 1; } + done + - name: The example, tested and run + shell: bash + working-directory: wai/wai-quantum + run: | + set -euo pipefail + cargo test --locked --release --features quantum_engine,quantum_receipt --example extension_points 2>&1 | tee "$RUNNER_TEMP/example.log" + grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/example.log" || { echo "::error::the example's tests did not pass"; exit 1; } + cargo run --locked --release --features quantum_engine,quantum_receipt --example extension_points > "$RUNNER_TEMP/walk.txt" + cat "$RUNNER_TEMP/walk.txt" + for want in 'the job planned on "basis_table"' 'counts {"100":100}' '"origin": "registered"'; do + grep -qF "$want" "$RUNNER_TEMP/walk.txt" || { echo "::error::the example's walk-through does not say: $want"; exit 1; } + done + + wasip2: + name: the registry under wasm32-wasip2 (wasmtime) + needs: relevant + if: needs.relevant.outputs.run == 'true' + runs-on: linux + timeout-minutes: 60 + steps: + - uses: actions/checkout@v5 + - name: Install Rust 1.98.0 with the wasm32-wasip2 target + shell: bash + run: | + export PATH="$HOME/.cargo/bin:$PATH" + command -v rustup >/dev/null 2>&1 || \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal --default-toolchain 1.98.0 + rustup toolchain install 1.98.0 --profile minimal + rustup default 1.98.0 + rustup target add --toolchain 1.98.0 wasm32-wasip2 + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + # The release binary, pinned by version and SHA-256, rather than a build from source. + - name: Install wasmtime 49.0.2 + shell: bash + run: | + set -euo pipefail + [ "$(uname -m)" = x86_64 ] || { echo "::error::the pinned wasmtime archive is x86_64; this host is $(uname -m)"; exit 1; } + if ! command -v xz >/dev/null 2>&1; then + SUDO=""; [ "$(id -u)" -ne 0 ] && SUDO="sudo" + $SUDO apt-get update -qq + $SUDO apt-get install -y -qq xz-utils + fi + v=49.0.2 + f="wasmtime-v$v-x86_64-linux.tar.xz" + curl --proto '=https' --tlsv1.2 -sSfL -o "$RUNNER_TEMP/$f" \ + "https://github.com/bytecodealliance/wasmtime/releases/download/v$v/$f" + echo "a4d6e9e3a5a60f527cf7793d674c48930c80c2e8977995b8a275cad3254b9322 $RUNNER_TEMP/$f" | sha256sum -c - + tar -xJf "$RUNNER_TEMP/$f" -C "$RUNNER_TEMP" + echo "$RUNNER_TEMP/wasmtime-v$v-x86_64-linux" >> "$GITHUB_PATH" + # A debug build, so integer overflow panics instead of wrapping. + - name: The registry's tests under wasmtime + shell: bash + working-directory: wai/wai-quantum + env: + CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime + run: | + set -euo pipefail + cargo test --locked --target wasm32-wasip2 --features full --lib engine::registry:: 2>&1 | tee "$RUNNER_TEMP/wasip2.log" + grep -qE '^test result: ok\. 12 passed; 0 failed' "$RUNNER_TEMP/wasip2.log" || { echo "::error::the registry's tests did not all pass under wasip2"; exit 1; } diff --git a/wai/tools/quantum-docs/docs.json b/wai/tools/quantum-docs/docs.json index 678cfa927..f20198347 100644 --- a/wai/tools/quantum-docs/docs.json +++ b/wai/tools/quantum-docs/docs.json @@ -1,6 +1,6 @@ { "schema": "wai.quantum.docs-manifest/1", - "about": "The documents check_docs.py runs: the WAI Quantum READMEs and docs pages (engine, command line, service, Python, Studio, desktop, editors, providers, the compiler, estimates, verdicts, error correction, other formats, the variational and algorithm library, the feature matrix and the version policy; the service's self-hosting, scheduler and provenance pages; the views' engine), the agent skill, the course's lessons, and their translations.", + "about": "The documents check_docs.py runs: the WAI Quantum READMEs and docs pages (engine, command line, service, Python, Studio, desktop, editors, providers, the compiler, estimates, verdicts, error correction, other formats, the variational and algorithm library, the extension points, the feature matrix and the version policy; the service's self-hosting, scheduler and provenance pages; the views' engine), the agent skill, the course's lessons, and their translations.", "docs": [ "wai/wai-quantum/README.md", "wai/wai-quantum/README.zh-CN.md", @@ -10,6 +10,7 @@ "wai/wai-quantum/docs/estimate.md", "wai/wai-quantum/docs/verdicts.md", "wai/wai-quantum/docs/job-refusals.md", + "wai/wai-quantum/docs/extension-points.md", "wai/wai-quantum/docs/formats.md", "wai/wai-quantum/docs/qasm-diagnostics.md", "wai/wai-quantum/docs/qasm-feature-matrix.md", diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index b2e7f679d..4388a8ac0 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -341,6 +341,11 @@ required-features = ["quantum_engine"] name = "qec_text_conformance" required-features = ["quantum_frame", "quantum_ldpc"] +[[example]] +name = "extension_points" +required-features = ["quantum_engine", "quantum_receipt"] +test = true + [[example]] name = "stablehlo_gate" required-features = ["quantum_fabric"] diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index c4f54b8c0..d91d78530 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -897,6 +897,36 @@ implementation: and two-mode statistics. Those are computable for any number of modes, and every sampler of the state must reproduce them. +## Extension points: your own method, pass, noise channel or format + +```sh runnable +wai-quantum components # every component the engine runs through here, with its identity +``` + +`engine::registry` holds every simulation method, compiler pass, noise +channel and program format the engine plans, runs, compiles and converts +through, each behind a trait (`Method`, `Pass`, `Channel`, `FrontEnd`). The +built-ins register through the same traits, and a host that embeds the +engine registers its own beside them with `Registry::with_builtins()` and +`register(...)`, in its own process: nothing is loaded from a file or a +request, and the command line, the MCP servers and the job service run the +built-ins alone. A registered component runs where a built-in runs (a job's +plan and run, `auto`'s choice, a compile's `passes`, a job's `noise`, a +conversion or a job's `source`), and the engine checks what it gives: a +method's result carries a class it declared and leaves the engine's members +alone, a pass is held to its input by the equivalence ladder or a distance +bound on the whole circuit, a channel's Kraus operators are checked +completely positive and trace preserving at each job's values, and a +format's program must read. Each component's identity is its name, its +version and the BLAKE3 digest of its descriptor. A result made by built-ins +alone is the result it always was, byte for byte; one a registered +component took part in carries `components`, every component that made it, +the registered ones marked `registered` and `certified: false`, and its +receipt seals them. [docs/extension-points.md](docs/extension-points.md) +says how to write one, with a channel registered and run, and +[examples/extension_points.rs](examples/extension_points.rs) has a method, a +pass, a channel and a format run end to end. + ## Determinism Simulation is **byte-exact**: amplitudes are dyadic fixed-point, so a circuit diff --git a/wai/wai-quantum/README.zh-CN.md b/wai/wai-quantum/README.zh-CN.md index 4a66677f0..d758f3283 100644 --- a/wai/wai-quantum/README.zh-CN.md +++ b/wai/wai-quantum/README.zh-CN.md @@ -479,6 +479,14 @@ wai-quantum lab gbs --squeezing 0.9,0.7,0.8,0.6 --eta 0.5 --seed 3 --check s.txt `--check` 用该态精确的单模与双模统计来检验一个观测点击模式文件。这些统计对任意模式数都可以计算,该态的每一个采样器都必须复现它们。 +## 扩展点:你自己的方法、编译遍、噪声信道或格式 + +```sh runnable +wai-quantum components # 引擎在本机所经过的每个组件,及其身份 +``` + +`engine::registry` 持有引擎规划、运行、编译和转换所经过的每个模拟方法、编译遍、噪声信道和程序格式,各在一个 trait 之后(`Method`、`Pass`、`Channel`、`FrontEnd`)。内置组件通过同样的 trait 注册;嵌入引擎的宿主在自己的进程中用 `Registry::with_builtins()` 和 `register(...)` 在它们旁边注册自己的组件:不从文件或请求加载任何东西,命令行、MCP 服务器和作业服务只运行内置组件。注册的组件在内置组件运行的地方运行(作业的规划与运行、`auto` 的选择、编译的 `passes`、作业的 `noise`、转换或作业的 `source`),引擎检查它给出的东西:方法的结果带有它声明的精度等级,并且不改动引擎写入的成员;编译遍在整条线路上按等价阶梯或距离界与其输入对照;信道的 Kraus 算子在每个作业的取值下检查为完全正且保迹;格式读出的程序必须可读。每个组件的身份是它的名称、版本及其描述符的 BLAKE3 摘要。只由内置组件做出的结果与以往逐字节相同;有注册组件参与的结果带有 `components`,即做出它的每个组件,注册的组件标为 `registered` 和 `certified: false`,并由收据封存。[docs/extension-points.md](docs/extension-points.md) 说明如何编写,附一个注册并运行的信道;[examples/extension_points.rs](examples/extension_points.rs) 有一个方法、一个编译遍、一个信道和一个格式的端到端运行。 + ## 确定性 模拟是**字节级一致**的:振幅是二进定点数,因此一条线路在每台机器、每个目标上都重构出相同的 `statevector_hash`。近似方法(MPS 截断、稀疏 Pauli 动力学、相量/机器学习层)是*可复现的 f64*:严格遵循 IEEE-754,在每个平台上给定相同的输入和种子时结果完全相同。文档如实地这样描述它们,而不声称它们字节级一致。它们的 `sin`、`cos`、`ln` 和 `atan2` 都在 crate 内部由 `+ − × ÷` 计算得出,因为各平台的数学库在最后一位上存在差异——这种差异足以让其中四个层曾经在原生运行和作为 WASI 组件运行时给出不同的位。`tests/bit_identity.rs` 为每个层固定一个结果,因此任何算术不同的平台都会在那里失败。 diff --git a/wai/wai-quantum/ci/check_schemas.py b/wai/wai-quantum/ci/check_schemas.py index af886ba05..aa28ec2be 100644 --- a/wai/wai-quantum/ci/check_schemas.py +++ b/wai/wai-quantum/ci/check_schemas.py @@ -173,6 +173,7 @@ def main() -> int: ("draw_circuit", {"qasm": bell}), ("verify_quantum_result", {"job": run}), ("list_methods", {}), + ("list_components", {}), ("novelty_qasm", {"qasm": bell}), ("rewrite_qasm", {"qasm": bell, "op": "options"}), ("rewrite_qasm", {"qasm": bell, "op": "simplify"}), diff --git a/wai/wai-quantum/docs/extension-points.md b/wai/wai-quantum/docs/extension-points.md new file mode 100644 index 000000000..f98c829f7 --- /dev/null +++ b/wai/wai-quantum/docs/extension-points.md @@ -0,0 +1,125 @@ +# 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 { vec![Param::new("p", 0.0, 1.0, "a probability")] } + fn arity(&self) -> u32 { 1 } + fn kraus(&self, v: &[f64]) -> Vec> { + 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> { + 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 run --release --features quantum_engine,quantum_receipt --example extension_points +cargo test --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. diff --git a/wai/wai-quantum/docs/job-refusals.md b/wai/wai-quantum/docs/job-refusals.md index 7f34f56ee..d7abe0b34 100644 --- a/wai/wai-quantum/docs/job-refusals.md +++ b/wai/wai-quantum/docs/job-refusals.md @@ -78,6 +78,12 @@ browser engine. A test holds this page to the code's table. | `J051` | refused | A gradient that cannot be taken: a differentiation method that does not exist, a parameter the reader cannot carry to the gates (its cause `Q129`), a program that measures in the middle, noise, no Hamiltonian, or more qubits or work than it takes (the message says which). | | `J052` | refused | An optimisation, a variational eigensolver or a QAOA run that does not read or cannot be done: an unknown optimiser, settings or steps past their limits, a Hamiltonian or a graph that does not read (the message says which). | | `J053` | refused | An algorithm builder's request that does not read: an unknown algorithm, sizes past its limits, or arguments it does not take (the message says which). | +| `J060` | refused | A compiler pass a request names (`compile.passes`) that this host does not hold: the components list names every method, pass, noise channel and front end here, and no host loads a component from a request. | +| `J061` | refused | A noise channel the method cannot take: a method that models no noise, or a channel the method has no form for (amplitude damping on the stabilizer's Pauli frames, a registered channel on a method that takes only the built-in ones); the message names what does take it. | +| `J062` | refused | A noise channel whose parameters do not read or whose operators fail its validity check: a value outside its range, Kraus operators that are not completely positive and trace preserving (their sum of K†K off the identity), or a Pauli form whose probabilities are negative, do not sum to 1, or are not the channel its operators make (the message says by how much). | +| `J063` | refused | A component the registry does not take: a name its kind holds already (a built-in's among them), a name or version that is empty or not plain text, a descriptor that is not a JSON object, a channel on other than one or two qubits, or a pass whose check states no bound. | +| `J064` | refused | A registered component's answer that breaks the engine's contract: a method's result without an accuracy class it declared, or one that writes a member the engine writes (method, qubits, gates, compiled, repaired, engine, schema, components, receipt, verdict); a pass's gates on qubits the program does not have, or that do not read back; a front end's program that does not read. Nothing it gave is returned. | +| `J065` | refused | An intent on a job whose noise names a registered channel: the intent checks model the built-in channels alone, so the check is refused rather than made against another model. | | `J100` | dropped | A line of an answer that is not a statement (`name = View(...)`), a program (a fenced `qasm` block with its name), a comment or a marker: left out. | | `J101` | dropped | A view the answer layer does not have: left out (the views are listed in the layer's specification). | | `J102` | dropped | An `@` call the engine does not have: the statement is left out. | diff --git a/wai/wai-quantum/examples/extension_points.rs b/wai/wai-quantum/examples/extension_points.rs new file mode 100644 index 000000000..3b400b9d7 --- /dev/null +++ b/wai/wai-quantum/examples/extension_points.rs @@ -0,0 +1,332 @@ +//! A host's own components, written outside the engine against its public +//! traits, registered beside the built-ins and run end to end: a simulation +//! method, a compiler pass, a noise channel and a program format. +//! +//! ```sh +//! cargo run --release --example extension_points +//! cargo test --release --example extension_points +//! ``` +//! +//! - `basis_table`, a method: a circuit of `x`, `cx`, `ccx` and `swap` gates +//! keeps one basis state, so its answer is that state, followed bit by bit +//! and exact. `auto` tries it first for counts; it refuses every other +//! circuit, and `auto` goes on to the built-ins. +//! - `cancel_pairs`, a pass: adjacent pairs of one self-inverse gate on the +//! same qubits removed. The engine checks its output against its input by +//! the equivalence ladder, as it declares. +//! - `bit_flip`, a noise channel: `X` with probability `p` after each gate, +//! given by its Kraus operators and its Pauli form, which the engine checks +//! at every job's value; the density matrix takes it. +//! - `gate_list`, a format: `qubits N`, then a gate and its qubits a line, +//! and `measure`; read to OpenQASM 3 and written from the engine's program. +//! +//! Every result one of them made names them in `components`, each marked +//! `registered` and `certified: false`, and the receipt seals it. + +use serde_json::{Value, json}; +use wai_quantum::engine::catalog::Plan; +use wai_quantum::engine::formats::{Exported, Imported}; +use wai_quantum::engine::registry::{Channel, Check, Component, FrontEnd, Job, Method, Param, Pass, PassInput, PassOutput, Question, Registry}; +use wai_quantum::gates::C; +use wai_quantum::qasm::diag::{Diagnostic, FORMAT_UNREAD, WRITE_CANNOT}; +use wai_quantum::qasm::{Kind, Op, Program}; + +/// A method for circuits that permute basis states. +pub struct BasisTable; + +fn permutes(p: &Program) -> bool { + p.ops.iter().all(|o| matches!(o.kind, Kind::X | Kind::Cx | Kind::Ccx | Kind::Swap | Kind::Id)) +} + +/// The basis state the circuit leaves, qubit by qubit. +fn follow(p: &Program) -> Vec { + let mut b = vec![false; p.n_qubits as usize]; + for o in &p.ops { + let q: Vec = o.qubits.iter().map(|&q| q as usize).collect(); + match o.kind { + Kind::X => b[q[0]] = !b[q[0]], + Kind::Cx if b[q[0]] => b[q[1]] = !b[q[1]], + Kind::Ccx if b[q[0]] && b[q[1]] => b[q[2]] = !b[q[2]], + Kind::Swap => b.swap(q[0], q[1]), + _ => {} + } + } + b +} + +impl Method for BasisTable { + fn name(&self) -> &str { + "basis_table" + } + fn version(&self) -> &str { + "1.0.0" + } + fn classes(&self) -> Vec { + vec!["exact".into()] + } + fn descriptor(&self) -> Value { + json!({ "circuits": "x, cx, ccx and swap alone: a permutation of basis states", "answers": ["counts", "amplitudes"] }) + } + fn auto_rank(&self, question: Question) -> Option { + // Before every built-in, for counts alone. + (question == Question::Counts).then_some(5) + } + fn plan(&self, job: &Job<'_>) -> Result { + let r = job.request()?; + if !permutes(&r.program) { + return Err("basis_table takes x, cx, ccx and swap alone".into()); + } + if !r.observables.is_empty() { + return Err("basis_table answers counts and amplitudes".into()); + } + let n = r.program.n_qubits; + Ok(Plan { method: "basis_table".into(), why: format!("a permutation of basis states on {n} qubits, followed bit by bit"), memory_bytes: 1 << 20, work: r.program.ops.len() as u64, qubits: n, target: None }) + } + fn run(&self, job: &Job<'_>, _: &Plan, out: &mut Value) -> Result<(), String> { + let r = job.request()?; + let b = follow(&r.program); + out["accuracy"] = json!({ "class": "exact", "note": "the one basis state a permutation leaves, followed bit by bit" }); + if r.shots > 0 { + let mut counts = serde_json::Map::new(); + counts.insert(r.program.bitstring(|q| b[q as usize]), json!(r.shots)); + out["counts"] = Value::Object(counts); + } + if !r.amplitudes.is_empty() { + let here: String = (0..b.len()).rev().map(|q| if b[q] { '1' } else { '0' }).collect(); + out["amplitudes"] = r.amplitudes.iter().map(|a| json!({ "bits": a, "re": if *a == here { 1.0 } else { 0.0 }, "im": 0.0 })).collect(); + } + Ok(()) + } +} + +/// A pass: adjacent pairs of one self-inverse gate on the same qubits removed. +pub struct CancelPairs; + +impl Pass for CancelPairs { + fn name(&self) -> &str { + "cancel_pairs" + } + fn version(&self) -> &str { + "1.0.0" + } + fn descriptor(&self) -> Value { + json!({ "does": "adjacent pairs of one self-inverse gate on the same qubits removed" }) + } + fn check(&self) -> Check { + Check::Ladder + } + fn run(&self, input: &PassInput<'_>) -> Result { + let inverse = |k: Kind| matches!(k, Kind::X | Kind::Y | Kind::Z | Kind::H | Kind::Cx | Kind::Cz | Kind::Swap | Kind::Ccx); + let mut out: Vec = Vec::new(); + let mut dropped = 0; + for o in input.ops { + if inverse(o.kind) && out.last().is_some_and(|l| l.kind == o.kind && l.qubits == o.qubits) { + out.pop(); + dropped += 1; + } else { + out.push(o.clone()); + } + } + Ok(PassOutput::new(out).with_note(json!({ "how": "adjacent pairs dropped", "pairs": dropped }))) + } +} + +/// A channel: the bit flip. +pub struct BitFlip; + +fn re(x: f64) -> C { + C { re: x, im: 0.0 } +} + +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, on each qubit it touched" }) + } + fn params(&self) -> Vec { + vec![Param::new("p", 0.0, 1.0, "a probability")] + } + fn arity(&self) -> u32 { + 1 + } + fn kraus(&self, v: &[f64]) -> Vec> { + let (a, b) = ((1.0 - v[0]).sqrt(), v[0].sqrt()); + vec![vec![re(a), re(0.0), re(0.0), re(a)], vec![re(0.0), re(b), re(b), re(0.0)]] + } + fn pauli(&self, v: &[f64]) -> Option> { + Some(vec![(1.0 - v[0], "I".into()), (v[0], "X".into())]) + } +} + +/// A format: a list of gates. +pub struct GateList; + +impl FrontEnd for GateList { + fn name(&self) -> &str { + "gate_list" + } + fn version(&self) -> &str { + "1.0.0" + } + fn title(&self) -> &str { + "a gate list" + } + fn descriptor(&self) -> Value { + json!({ "reads": "qubits N, then a gate and its qubits a line, and measure", "writes": "the same, for gates without parameters" }) + } + fn extensions(&self) -> Vec { + vec!["gates".into()] + } + fn read(&self, input: &[u8]) -> Result { + let text = std::str::from_utf8(input).map_err(|_| Diagnostic::new(FORMAT_UNREAD, None, "a gate list is text"))?; + let mut q = String::from("OPENQASM 3.0;\n"); + let mut declared = false; + for (i, line) in text.lines().map(str::trim).filter(|l| !l.is_empty()).enumerate() { + let w: Vec<&str> = line.split_whitespace().collect(); + match (i, w.as_slice()) { + (0, ["qubits", k]) => { + let k: u32 = k.parse().map_err(|_| Diagnostic::new(FORMAT_UNREAD, None, "the first line is qubits N"))?; + q.push_str(&format!("qubit[{k}] q;\nbit[{k}] c;\n")); + declared = true; + } + (0, _) => return Err(Diagnostic::new(FORMAT_UNREAD, None, "a gate list starts with qubits N")), + (_, ["measure"]) => q.push_str("c = measure q;\n"), + (_, [g, qs @ ..]) => q.push_str(&format!("{g} {};\n", qs.iter().map(|x| format!("q[{x}]")).collect::>().join(", "))), + _ => {} + } + } + if !declared { + return Err(Diagnostic::new(FORMAT_UNREAD, None, "an empty gate list")); + } + Ok(Imported { qasm: q, notes: Vec::new(), summary: json!({ "format": "gate_list" }) }) + } + fn write(&self, p: &Program, _: Option<&str>) -> Result { + let mut s = format!("qubits {}\n", p.n_qubits); + for o in &p.ops { + if !o.params.is_empty() { + return Err(Diagnostic::new(WRITE_CANNOT, None, "a gate list holds gates without parameters")); + } + let qs: Vec = o.qubits.iter().map(u32::to_string).collect(); + s.push_str(&format!("{} {}\n", format!("{:?}", o.kind).to_lowercase(), qs.join(" "))); + } + if !p.measured.is_empty() { + s.push_str("measure\n"); + } + Ok(Exported { bytes: s.into_bytes(), notes: Vec::new(), summary: json!({ "format": "gate_list" }) }) + } +} + +/// The built-ins, and the four components above after them. +pub fn registry() -> Registry { + let mut r = Registry::with_builtins(); + for c in [Component::method(BasisTable), Component::pass(CancelPairs), Component::channel(BitFlip), Component::front_end(GateList)] { + let id = r.register(c).expect("registers"); + assert!(!id.is_builtin()); + } + r +} + +/// A gate list: `x` on each qubit, the pair on qubit 2 a pair the pass +/// cancels, then `cx` and `swap`. +pub const LIST: &str = "qubits 3\nx 0\nx 1\nx 2\nx 2\ncx 0 1\nswap 0 2\nmeasure\n"; + +/// The four end to end: a gate list read as a job's source, compiled with +/// the pass, run on the method, and the same program under the channel. +pub fn walk_through(r: &Registry) -> Result { + let list = LIST; + let job = r.expand(&json!({ "source": { "format": "gate_list", "program": list }, "shots": 100, "compile": { "level": 0, "basis": "standard", "passes": ["cancel_pairs"] } }))?.ok_or("a job with a source expands")?; + let (plan, result) = r.plan_and_run(&job, 1 << 30)?; + let noisy = json!({ "qasm": job["qasm"], "observables": ["Z0", "Z2"], "noise": { "bit_flip": 0.05 } }); + let (noisy_plan, noisy_result) = r.plan_and_run(&noisy, 1 << 30)?; + let written = r.convert_json(&json!({ "from": "qasm", "to": "gate_list", "program": job["qasm"] }))?; + Ok(json!({ "plan": plan.method, "why": plan.why, "result": result, "noisy_plan": noisy_plan.method, "noisy": noisy_result, "written": written })) +} + +fn main() { + let r = registry(); + let c = r.components(); + println!("{} components, {} registered:", r.identities().len(), c["registered"]); + for id in r.identities().into_iter().filter(|i| !i.is_builtin()) { + println!(" {:<10} {:<14} {:<7} {}", id.kind.as_str(), id.name, id.version, id.digest); + } + match walk_through(&r) { + Ok(w) => { + println!("\nthe job planned on {}: {}", w["plan"], w["why"]); + println!("counts {}", w["result"]["counts"]); + println!("compiled by {}", w["result"]["compiled"]["source"]); + println!("under bit_flip on {}: {}", w["noisy_plan"], w["noisy"]["expectations"]); + println!("written back as a gate list:\n{}", w["written"]["program"].as_str().unwrap_or("")); + println!("made by: {}", serde_json::to_string_pretty(&w["result"]["components"]).unwrap_or_default()); + } + Err(e) => { + eprintln!("{e}"); + std::process::exit(1); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn the_four_run_end_to_end_and_are_named_in_what_they_make() { + let r = registry(); + let w = walk_through(&r).unwrap(); + // x on every qubit, the pair on q[2] cancelled, then cx and swap: q[2] alone reads 1. + assert_eq!(w["plan"], "basis_table", "{}", w["why"]); + assert_eq!(w["result"]["counts"], json!({ "100": 100 }), "{w}"); + let names: Vec<(&str, &str)> = w["result"]["components"].as_array().unwrap().iter().map(|c| (c["name"].as_str().unwrap(), c["origin"].as_str().unwrap())).collect(); + assert_eq!(names, [("gate_list", "registered"), ("lower", "built-in"), ("cancel_pairs", "registered"), ("basis_table", "registered")]); + assert!(w["result"]["components"].as_array().unwrap().iter().filter(|c| c["origin"] == "registered").all(|c| c["certified"] == false)); + // The same counts as the bit-exact method, a built-in, gives. + let mut exact = r.expand(&json!({ "source": { "format": "gate_list", "program": LIST }, "shots": 100 })).unwrap().unwrap(); + exact["method"] = json!("exact"); + assert_eq!(r.plan_and_run(&exact, 1 << 30).unwrap().1["counts"], w["result"]["counts"]); + // Under the bit flip, on the density matrix: ⟨Z⟩ of a flipped qubit + // after the gates that touch it, each flip with probability 0.05. + assert_eq!(w["noisy_plan"], "density_matrix"); + let z: Vec = w["noisy"]["expectations"].as_array().unwrap().iter().map(|e| e["value"].as_f64().unwrap()).collect(); + assert!(z.iter().all(|v| v.abs() < 1.0 && v.abs() > 0.5), "{z:?}"); + // Written back as a gate list, read again and held to the program. + assert_eq!(w["written"]["check"]["verdict"]["verdict"], "equivalent", "{}", w["written"]); + } + + #[test] + fn the_receipt_seals_them() { + let r = registry(); + let key = ed25519_dalek_key(); + let job = json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nx q[0];\ncx q[0], q[1];\nc = measure q;\n", "shots": 10 }); + let body = r.job(&job, 1 << 30, &key, json!({ "unmetered": "an example" })).unwrap(); + assert!(wai_quantum::engine::job::verify(&body).unwrap().0); + let mut forged = body.clone(); + forged["result"]["components"][1]["origin"] = json!("built-in"); + assert!(!wai_quantum::engine::job::verify(&forged).unwrap().0, "a registered method cannot pass for a built-in"); + } + + fn ed25519_dalek_key() -> ed25519_dalek::SigningKey { + ed25519_dalek::SigningKey::from_bytes(&[9; 32]) + } + + #[test] + fn what_they_do_not_take_is_refused_with_its_code() { + let r = registry(); + let codes = |e: String| wai_quantum::engine::refusal::codes_in(&e).iter().map(|c| c.to_string()).collect::>(); + let bell = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\n"; + assert_eq!(codes(r.plan(&json!({ "qasm": bell, "shots": 1, "method": "basis_table" }), 1 << 30).unwrap_err()), ["J011"]); + assert_eq!(codes(r.plan(&json!({ "qasm": bell, "shots": 1, "method": "stabilizer", "noise": { "bit_flip": 0.1 } }), 1 << 30).unwrap_err()), ["J061"]); + assert_eq!(codes(r.plan(&json!({ "qasm": bell, "shots": 1, "compile": { "passes": ["no_such_pass"] } }), 1 << 30).unwrap_err()), ["J060"]); + let mut again = registry(); + assert!(again.register(Component::method(BasisTable)).unwrap_err().ends_with("[J063]")); + // The built-in registry holds none of them. + assert!(Registry::builtins().plan(&json!({ "qasm": bell, "shots": 1, "method": "basis_table" }), 1 << 30).unwrap_err().ends_with("[J010]")); + } +} diff --git a/wai/wai-quantum/schemas/compile-result.v1.json b/wai/wai-quantum/schemas/compile-result.v1.json index eff6109cc..58a92b425 100644 --- a/wai/wai-quantum/schemas/compile-result.v1.json +++ b/wai/wai-quantum/schemas/compile-result.v1.json @@ -289,6 +289,13 @@ }, "receipt": { "$ref": "receipt.v2.json" + }, + "components": { + "type": "array", + "description": "The identity of every component that made the compile, when a registered pass took part (engine::registry): the program's format and each pass, each registered one marked so.", + "items": { + "$ref": "#/$defs/component" + } } } }, @@ -382,6 +389,10 @@ }, "check": { "type": "object" + }, + "component": { + "$ref": "#/$defs/component", + "description": "Beside a registered pass's record: its identity. Its check is the engine's, on the whole circuit (by: engine)." } } } @@ -406,6 +417,49 @@ "description": "What the certificate says of the output, in words." } } + }, + "component": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } } } } diff --git a/wai/wai-quantum/schemas/job-result.v1.json b/wai/wai-quantum/schemas/job-result.v1.json index 23d4156a7..d3bd353d4 100644 --- a/wai/wai-quantum/schemas/job-result.v1.json +++ b/wai/wai-quantum/schemas/job-result.v1.json @@ -32,6 +32,13 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "$ref": "#/$defs/component" + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -1362,6 +1369,49 @@ "type": "string" } } + }, + "component": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } } } } diff --git a/wai/wai-quantum/schemas/tools.v1.json b/wai/wai-quantum/schemas/tools.v1.json index 2a89d287f..b20c99dd1 100644 --- a/wai/wai-quantum/schemas/tools.v1.json +++ b/wai/wai-quantum/schemas/tools.v1.json @@ -2225,6 +2225,13 @@ } ], "description": "The output read back and held to the program read (or, for a program read into OpenQASM, written back to its format and read again), or null when no check was asked." + }, + "components": { + "type": "array", + "description": "The identities of the two formats, when a registered front end took part (engine::registry).", + "items": { + "$ref": "#/$defs/component" + } } } }, @@ -2579,6 +2586,161 @@ } } } + }, + "component": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + }, + "component_entry": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + }, + "components": { + "type": "object", + "description": "Every component the engine plans, runs, compiles and converts through (engine::registry::Registry::components): the simulation methods, the compiler passes, the noise channels and the program formats, each in the registry's order with its identity and its descriptor. The command line, both MCP servers and the job service serve the built-ins alone.", + "required": [ + "format", + "engine", + "methods", + "passes", + "channels", + "front_ends", + "registered" + ], + "properties": { + "format": { + "const": "wai.quantum.components/1" + }, + "engine": { + "type": "object", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "methods": { + "type": "array", + "items": { + "$ref": "#/$defs/component_entry" + } + }, + "passes": { + "type": "array", + "items": { + "$ref": "#/$defs/component_entry" + } + }, + "channels": { + "type": "array", + "items": { + "$ref": "#/$defs/component_entry" + } + }, + "front_ends": { + "type": "array", + "items": { + "$ref": "#/$defs/component_entry" + } + }, + "registered": { + "type": "integer", + "minimum": 0, + "description": "How many are registered (a host's own); 0 for the built-ins alone." + } + } } } } diff --git a/wai/wai-quantum/src/engine/catalog.rs b/wai/wai-quantum/src/engine/catalog.rs index d15ad7553..cec85febf 100644 --- a/wai/wai-quantum/src/engine/catalog.rs +++ b/wai/wai-quantum/src/engine/catalog.rs @@ -36,6 +36,7 @@ //! the counts and values carry. use super::refusal::{self, tag}; +use super::registry::{Job, Method, Question, Registry}; use crate::qasm::{self, Program}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -149,23 +150,70 @@ pub fn accuracy_classes() -> Value { } /// The methods, for the service description. `job_bytes` is the memory one -/// job may take. +/// job may take. Each method's entry is its own ([`super::registry::Method::describe`]), +/// in the order the built-in registry holds them. pub fn describe(job_bytes: u64) -> Value { - json!([ - { "method": "exact", "accuracy": "bit-exact", "circuits": "every angle a dyadic multiple of 2π (2πm/2^k, k ≤ 32)", "max_qubits": EXACT_MAX_QUBITS, "answers": ["counts", "expectations", "amplitudes", "state_hash"] }, - { "method": "statevector", "accuracy": "exact", "circuits": "any", "max_qubits": statevector_max_qubits(job_bytes), "answers": ["counts", "expectations", "amplitudes"] }, - { "method": "statevector_single", "accuracy": "single", "circuits": "any, gates on at most four qubits", "max_qubits": gpu_max_qubits().map(|m| m.min(single_max_qubits(job_bytes))).unwrap_or(0), "available": gpu_max_qubits().is_ok(), "answers": ["counts", "expectations", "amplitudes"], "options": { "fusion": "the widest fused gate (default 3, at most 3)" } }, - { "method": "stabilizer", "accuracy": "exact; with Pauli noise, counts are noisy samples and expectations estimates", "circuits": "Clifford (rotations by whole quarter turns, controlled phases by whole half turns), optionally with depolarizing, dephasing and readout noise", "max_qubits": STABILIZER_MAX_QUBITS, "answers": ["counts", "expectations"], "options": { "trajectories": "with noise: the shots expectation values are estimated from (default: the shots, at least 10000)" } }, - { "method": "pauli", "accuracy": "bounded", "circuits": "any", "max_qubits": PAULI_MAX_QUBITS, "answers": ["expectations"], "options": { "threshold": "coefficients below it are dropped (default 1e-5); the bound covers what was dropped", "max_weight": "Pauli strings heavier than this are dropped", "ladder": "also report the value at up to three thresholds a decade apart above it, to show it settling (default true)", "max_terms": "the most Pauli terms to hold (default: what the job's memory holds); past it the threshold rises, and the bound covers what drops" } }, - { "method": "mps", "accuracy": "exact when nothing is truncated, otherwise heuristic", "circuits": "any; best on low-entanglement circuits", "max_qubits": MPS_MAX_QUBITS, "answers": ["counts", "expectations", "amplitudes"], "options": { "max_bond": "the largest bond dimension kept (default 64, at most 256)", "cutoff": "singular values whose squared share is below it are dropped (default 1e-12)" } }, - { "method": "stabilizer_rank", "accuracy": "exact; with terms or epsilon, an estimate with its bound", "circuits": "Clifford+T (t, tdg, and p, rz, rx, ry at odd multiples of π/4, cp at odd multiples of π/2, ccz and ccx), each T gate doubling the sum's terms: the state as a sum of stabilizer states", "max_qubits": STABILIZER_RANK_MAX_QUBITS, "answers": ["amplitudes", "expectations"], "options": { "terms": "the sampled mode: this many branches drawn, seeded, the result an estimate with the bound on its distance from the state that holds with the confidence stated", "epsilon": "the sampled mode with as many branches as that bound needs", "confidence": "the probability the sampled bound holds (default 0.95)" } }, - { "method": "tensor_network", "accuracy": "exact", "circuits": "any", "max_qubits": qasm::MAX_QUBITS, "answers": ["amplitudes"], "options": { "path_restarts": "independent contraction-path searches, the cheapest kept (default 8; 1 in a browser tab)" } }, - { "method": "cutrank", "accuracy": "exact; single when only a single-precision table fits", "circuits": "a Clifford circuit, then one layer of one-qubit gates at any angles: each qubit's gates from its first non-Clifford one on act on that qubit alone; the cost is set by the state's cut-rank along an order of the qubits, not by their number", "max_qubits": CUTRANK_MAX_QUBITS, "answers": ["counts", "amplitudes"] }, - { "method": "cutrank_single", "accuracy": "single", "circuits": "as for cutrank; the table on a GPU, its arithmetic in single precision", "max_qubits": CUTRANK_MAX_QUBITS, "max_cut": gpu_cutrank_max_cut().unwrap_or(0), "available": gpu_cutrank_max_cut().is_ok(), "answers": ["counts", "amplitudes"] }, - { "method": "density_matrix", "accuracy": "exact", "circuits": "any, with noise", "max_qubits": density_max_qubits(job_bytes), "answers": ["counts", "expectations"] }, - { "method": "trajectories", "accuracy": "estimate", "circuits": "any, with noise", "max_qubits": TRAJECTORY_MAX_QUBITS.min(statevector_max_qubits(job_bytes)), "answers": ["counts", "expectations"], "options": { "trajectories": "how many (default: the shots, or 1000 for expectations alone); shots that take the same errors share one state" } }, - super::pulse::describe(), - ]) + super::registry::Registry::builtins().describe_methods(job_bytes) +} + +/// A built-in method's words: its accuracy, the circuits and the answers it +/// takes, and its options, which depend on nothing of the host (what is +/// hashed into its identity). +pub(crate) fn method_text(method: &str) -> Value { + match method { + "exact" => json!({ "accuracy": "bit-exact", "circuits": "every angle a dyadic multiple of 2π (2πm/2^k, k ≤ 32)", "answers": ["counts", "expectations", "amplitudes", "state_hash"] }), + "statevector" => json!({ "accuracy": "exact", "circuits": "any", "answers": ["counts", "expectations", "amplitudes"] }), + "statevector_single" => json!({ "accuracy": "single", "circuits": "any, gates on at most four qubits", "answers": ["counts", "expectations", "amplitudes"], "options": { "fusion": "the widest fused gate (default 3, at most 3)" } }), + "stabilizer" => json!({ "accuracy": "exact; with Pauli noise, counts are noisy samples and expectations estimates", "circuits": "Clifford (rotations by whole quarter turns, controlled phases by whole half turns), optionally with depolarizing, dephasing and readout noise", "answers": ["counts", "expectations"], "options": { "trajectories": "with noise: the shots expectation values are estimated from (default: the shots, at least 10000)" } }), + "pauli" => json!({ "accuracy": "bounded", "circuits": "any", "answers": ["expectations"], "options": { "threshold": "coefficients below it are dropped (default 1e-5); the bound covers what was dropped", "max_weight": "Pauli strings heavier than this are dropped", "ladder": "also report the value at up to three thresholds a decade apart above it, to show it settling (default true)", "max_terms": "the most Pauli terms to hold (default: what the job's memory holds); past it the threshold rises, and the bound covers what drops" } }), + "mps" => json!({ "accuracy": "exact when nothing is truncated, otherwise heuristic", "circuits": "any; best on low-entanglement circuits", "answers": ["counts", "expectations", "amplitudes"], "options": { "max_bond": "the largest bond dimension kept (default 64, at most 256)", "cutoff": "singular values whose squared share is below it are dropped (default 1e-12)" } }), + "stabilizer_rank" => json!({ "accuracy": "exact; with terms or epsilon, an estimate with its bound", "circuits": "Clifford+T (t, tdg, and p, rz, rx, ry at odd multiples of π/4, cp at odd multiples of π/2, ccz and ccx), each T gate doubling the sum's terms: the state as a sum of stabilizer states", "answers": ["amplitudes", "expectations"], "options": { "terms": "the sampled mode: this many branches drawn, seeded, the result an estimate with the bound on its distance from the state that holds with the confidence stated", "epsilon": "the sampled mode with as many branches as that bound needs", "confidence": "the probability the sampled bound holds (default 0.95)" } }), + "tensor_network" => json!({ "accuracy": "exact", "circuits": "any", "answers": ["amplitudes"], "options": { "path_restarts": "independent contraction-path searches, the cheapest kept (default 8; 1 in a browser tab)" } }), + "cutrank" => json!({ "accuracy": "exact; single when only a single-precision table fits", "circuits": "a Clifford circuit, then one layer of one-qubit gates at any angles: each qubit's gates from its first non-Clifford one on act on that qubit alone; the cost is set by the state's cut-rank along an order of the qubits, not by their number", "answers": ["counts", "amplitudes"] }), + "cutrank_single" => json!({ "accuracy": "single", "circuits": "as for cutrank; the table on a GPU, its arithmetic in single precision", "answers": ["counts", "amplitudes"] }), + "density_matrix" => json!({ "accuracy": "exact", "circuits": "any, with noise", "answers": ["counts", "expectations"] }), + "trajectories" => json!({ "accuracy": "estimate", "circuits": "any, with noise", "answers": ["counts", "expectations"], "options": { "trajectories": "how many (default: the shots, or 1000 for expectations alone); shots that take the same errors share one state" } }), + _ => json!({}), + } +} + +/// What a built-in method holds on this host in `job_bytes` of memory: its +/// most qubits, and for the methods on a GPU, whether there is one. +fn method_limits(method: &str, job_bytes: u64) -> Value { + match method { + "exact" => json!({ "max_qubits": EXACT_MAX_QUBITS }), + "statevector" => json!({ "max_qubits": statevector_max_qubits(job_bytes) }), + "statevector_single" => json!({ "max_qubits": gpu_max_qubits().map(|m| m.min(single_max_qubits(job_bytes))).unwrap_or(0), "available": gpu_max_qubits().is_ok() }), + "stabilizer" => json!({ "max_qubits": STABILIZER_MAX_QUBITS }), + "pauli" => json!({ "max_qubits": PAULI_MAX_QUBITS }), + "mps" => json!({ "max_qubits": MPS_MAX_QUBITS }), + "stabilizer_rank" => json!({ "max_qubits": STABILIZER_RANK_MAX_QUBITS }), + "tensor_network" => json!({ "max_qubits": qasm::MAX_QUBITS }), + "cutrank" => json!({ "max_qubits": CUTRANK_MAX_QUBITS }), + "cutrank_single" => json!({ "max_qubits": CUTRANK_MAX_QUBITS, "max_cut": gpu_cutrank_max_cut().unwrap_or(0), "available": gpu_cutrank_max_cut().is_ok() }), + "density_matrix" => json!({ "max_qubits": density_max_qubits(job_bytes) }), + "trajectories" => json!({ "max_qubits": TRAJECTORY_MAX_QUBITS.min(statevector_max_qubits(job_bytes)) }), + _ => json!({}), + } +} + +/// A built-in method's entry in the methods list: its name, accuracy and +/// circuits, what it holds here, its answers and its options. +pub(crate) fn describe_one(method: &str, job_bytes: u64) -> Value { + let text = method_text(method); + let mut o = serde_json::Map::new(); + o.insert("method".into(), json!(method)); + for k in ["accuracy", "circuits"] { + o.insert(k.into(), text[k].clone()); + } + if let Some(l) = method_limits(method, job_bytes).as_object() { + o.extend(l.clone()); + } + o.insert("answers".into(), text["answers"].clone()); + if let Some(x) = text.get("options") { + o.insert("options".into(), x.clone()); + } + Value::Object(o) } /// The memory a dense state vector of `n` qubits takes with room to run. @@ -316,33 +364,60 @@ pub(crate) fn read_observable(v: &Value, n: u32) -> Result { observable(v, n) } +/// The noise channels a job names with a value other than zero: each by its +/// place in the registry, with its values. +pub(crate) type Uses = Vec<(usize, Vec)>; + /// The noise channels a job may name. pub const NOISE_CHANNELS: [&str; 5] = ["depolarizing_1q", "depolarizing_2q", "amplitude_damping", "dephasing", "readout"]; -fn noise(v: &Value) -> Result { +/// The noise a job's `noise` object names, read with `reg`'s channels: the +/// built-in channels' values as the noise model the built-in methods take, +/// and every channel named with a value other than zero, each checked +/// ([`super::registry::validate`]), by its place in the registry. +fn noise(reg: &Registry, v: &Value) -> Result<(crate::quantum_noisy::NoiseModel, Uses), String> { // A channel the model does not know is refused: read as no noise, a // misspelt one would run the circuit noiseless. - if let Some(k) = v.as_object().and_then(|o| o.keys().find(|k| !NOISE_CHANNELS.contains(&k.as_str()))) { - return Err(format!("noise {k}: no such channel; the channels are {}", NOISE_CHANNELS.join(", "))); + let names: Vec<&str> = reg.channels().map(|c| c.name()).collect(); + if let Some(k) = v.as_object().and_then(|o| o.keys().find(|k| !names.contains(&k.as_str()))) { + return Err(format!("noise {k}: no such channel; the channels are {}", names.join(", "))); } - let f = |k: &str| -> Result { - let x = v.get(k).and_then(Value::as_f64).unwrap_or(0.0); - if (0.0..=1.0).contains(&x) { Ok(x) } else { Err(format!("noise {k} is a probability, 0 to 1")) } - }; - Ok(crate::quantum_noisy::NoiseModel { - depol1: f("depolarizing_1q")?, - depol2: f("depolarizing_2q")?, - damping: f("amplitude_damping")?, - dephasing: f("dephasing")?, - readout: f("readout")?, - }) + let mut model = crate::quantum_noisy::NoiseModel::default(); + let mut uses = Vec::new(); + for (i, c) in reg.channels().enumerate() { + let values = super::registry::channel_values(c, c.name(), v.get(c.name()).unwrap_or(&Value::Null))?; + if values.iter().all(|&x| x == 0.0) { + continue; + } + super::registry::validate(c, &values)?; + if reg.channel_at(i).1.is_builtin() { + let x = values[0]; + match c.name() { + "depolarizing_1q" => model.depol1 = x, + "depolarizing_2q" => model.depol2 = x, + "amplitude_damping" => model.damping = x, + "dephasing" => model.dephasing = x, + "readout" => model.readout = x, + _ => {} + } + } + uses.push((i, values)); + } + Ok((model, uses)) } /// The noise a job's JSON asks for, read as [`read`] reads it. pub fn noise_of(v: &Value) -> Result, String> { + noise_in(Registry::builtins(), v).map(|(m, _)| m) +} + +/// [`noise_of`] with `reg`'s channels, and each channel named with a value +/// other than zero. +pub(crate) fn noise_in(reg: &Registry, v: &Value) -> Result<(Option, Uses), String> { + let field = |e: String| if refusal::codes_in(&e).is_empty() { tag(refusal::BAD_FIELD, e) } else { e }; match v.get("noise") { - None | Some(Value::Null) => Ok(None), - Some(o @ Value::Object(_)) => noise(o).map(Some).map_err(|e| tag(refusal::BAD_FIELD, e)), + None | Some(Value::Null) => Ok((None, Vec::new())), + Some(o @ Value::Object(_)) => noise(reg, o).map(|(m, u)| (Some(m), u)).map_err(field), Some(_) => Err(tag(refusal::BAD_FIELD, "\"noise\" is an object")), } } @@ -401,6 +476,13 @@ pub fn bind_params(src: &str, params: Option<&Value>) -> Result /// Reads a job's JSON. A refusal ends with its code ([`super::refusal`]). pub fn read(v: &Value) -> Result { + read_in(Registry::builtins(), v).map(|(r, _)| r) +} + +/// [`read`] with `reg`'s noise channels and passes: the request, and each +/// channel its noise names with a value other than zero, by its place in +/// the registry with its values. +pub(crate) fn read_in(reg: &Registry, v: &Value) -> Result<(Request, Uses), String> { // A set runs as a set, item by item; read as one job it would run one // program in place of all of them. if is_set(v) { @@ -444,7 +526,8 @@ pub fn read(v: &Value) -> Result { None | Some(Value::Null) => (program, None, None), Some(c) => { let o = super::compile::Options::read(c).map_err(|e| tag(refusal::COMPILE, format!("compile: {e}")))?; - let out = super::compile::compile(&program, &o).map_err(|e| tag(refusal::COMPILE, format!("compile: {e}")))?; + let extra = super::compile::passes_of(reg, c.get("passes"))?; + let out = super::compile::compile_in(reg, &program, &o, &extra).map_err(|e| tag(refusal::COMPILE, format!("compile: {e}")))?; let summary = super::compile::summary(&out); let passes: Vec = out.certificate["passes"].as_array().into_iter().flatten().map(|r| json!({ "pass": r["pass"], "verdict": r["check"]["verdict"] })).collect(); (out.program, Some(summary), Some(Value::Array(passes))) @@ -481,7 +564,15 @@ pub fn read(v: &Value) -> Result { if shots == 0 && observables.is_empty() && amplitudes.is_empty() { return Err(tag(refusal::ASKS_NOTHING, "the job asks for nothing: give shots, observables or amplitudes")); } - let noise = noise_of(v)?; + let (noise, uses) = noise_in(reg, v)?; + let registered = uses.iter().any(|(i, _)| !reg.channel_at(*i).1.is_builtin()); + // The intent checks and the mitigation model the built-in channels alone. + if registered && v.get("intent").is_some_and(|i| !i.is_null()) { + return Err(tag(refusal::REGISTERED_INTENT, "intent: this job's noise names a registered channel, and the intent checks model the built-in channels alone; run it without the intent, or with the built-in channels")); + } + if registered && v.get("mitigate").is_some_and(|m| !m.is_null()) { + return Err(tag(refusal::MITIGATE, "mitigate: the mitigation models the built-in channels alone, and this job's noise names a registered channel")); + } let seed = v.get("seed").and_then(Value::as_u64).unwrap_or(1); // "mitigate": a method and its choices (super::mitigate), checked here so // a plan refuses what the run could not do. @@ -504,7 +595,7 @@ pub fn read(v: &Value) -> Result { } } }; - Ok(Request { + Ok((Request { program, diagnostics, method: v.get("method").and_then(Value::as_str).unwrap_or("auto").to_string(), @@ -518,7 +609,7 @@ pub fn read(v: &Value) -> Result { compile_passes, repaired, mitigate, - }) + }, uses)) } /// The option `key` in GiB as bytes, within `cap`. @@ -783,16 +874,17 @@ pub fn stabilizer_rank_work(c: &crate::quantum_stabrank::CliffordT, amplitude_te a.saturating_add(e) } -fn try_method(method: &str, r: &Request, dyadic: &Result, clifford: &Result, String>, cap: u64) -> Result { +/// A built-in method's plan for `r` (its registry entry's, +/// [`super::registry::Method::plan`]): whether `method` can take the job, +/// and the plan if so; otherwise why not. A job with noise reaches only the +/// methods that model it (the registry refuses it for the others). +pub(crate) fn try_method(method: &str, r: &Request, dyadic: &Result, clifford: &Result, String>, cap: u64) -> Result { let n = r.program.n_qubits; let gates = r.program.ops.len() as u64; let plan = |memory_bytes: u64, work: u64, why: String| Plan { method: method.into(), why, memory_bytes, work, qubits: n, target: None }; let counts = r.shots > 0; let wants_amplitudes = !r.amplitudes.is_empty(); let wants_expectations = !r.observables.is_empty(); - if r.noise.is_some() && !matches!(method, "density_matrix" | "trajectories" | "stabilizer") { - return Err(format!("{method} does not model noise")); - } if let Some(steps) = &r.program.dynamic { return try_dynamic(method, r, clifford, cap, steps); } @@ -836,7 +928,7 @@ fn try_method(method: &str, r: &Request, dyadic: &Result 0.0) { - return Err("stabilizer models Pauli noise (depolarizing, dephasing, readout), not amplitude damping".into()); + return Err(tag(refusal::CHANNEL_REFUSED, super::registry::STABILIZER_NOT_DAMPING)); } let (nn, measured) = (n as u64, r.program.measured.len() as u64); let words = nn.div_ceil(64); @@ -1033,11 +1125,19 @@ pub fn trajectory_count(r: &Request) -> u64 { /// The plan for a job's JSON, `job_bytes` the memory one job may take. pub fn plan(v: &Value, job_bytes: u64) -> Result { - // A pulse run is read by the pulse reader and integrated by the pulse - // simulator, a device target giving its qubits' parameters - // (engine::pulse). - if super::pulse::names_pulse(v) { - return super::pulse::plan(v, job_bytes); + plan_in(Registry::builtins(), v, job_bytes) +} + +/// The method `v` names, when it reads its job's program itself (a pulse +/// run: the pulse reader reads it, and the pulse simulator integrates it). +pub(crate) fn reads_own<'a>(reg: &'a Registry, v: &Value) -> Option<&'a dyn Method> { + v.get("method").and_then(Value::as_str).and_then(|n| reg.method(n)).filter(|m| m.reads_program()) +} + +/// [`plan`] through `reg`: its methods, channels and passes. +pub(crate) fn plan_in(reg: &Registry, v: &Value, job_bytes: u64) -> Result { + if let Some(m) = reads_own(reg, v) { + return m.plan(&Job::new(reg, v, job_bytes, None, &[])); } // A job that names a device target is compiled, placed and simulated as // the device would run it (engine::targets::job). @@ -1047,67 +1147,83 @@ pub fn plan(v: &Value, job_bytes: u64) -> Result { return Err(tag(refusal::FABRIC, "fabric: a job on a device target runs on the CPU, as the device's simulation; give fabric cpu, or leave it out")); } let p = crate::engine::targets::job::plan(v, job_bytes)?; - return with_intent(v, &read(v)?, p, job_bytes); + return with_intent(reg, v, &read_in(reg, v)?.0, p, job_bytes); } #[cfg(not(feature = "quantum_receipt"))] if v.get("target").is_some_and(|t| !t.is_null()) { return Err("this build has no device targets (the engine's quantum_receipt feature)".into()); } - let r = read(v)?; - let mut p = choose_placed(v, &r, job_bytes)?; + let (r, uses) = read_in(reg, v)?; + let job = Job::new(reg, v, job_bytes, Some(&r), &uses); + let mut p = choose_placed(&job)?; super::fabric::admit(v, &r, &mut p)?; - with_intent(v, &r, p, job_bytes) + with_intent(reg, v, &r, p, job_bytes) } /// [`choose`], except that a job asking for a member of the fabric /// (`accelerator`, a member, or partitions) under `auto` takes the one method /// the fabric's members run, `statevector`. -fn choose_placed(v: &Value, r: &Request, job_bytes: u64) -> Result { +fn choose_placed(job: &Job<'_>) -> Result { use super::fabric::Ask; - let ask = super::fabric::asked(v)?; + let r = job.request()?; + let ask = super::fabric::asked(job.json())?; if r.method != "auto" || matches!(ask, Ask::Auto | Ask::Cpu) { - return choose(r, job_bytes); + return choose(job); } - let (dyadic, clifford) = (r.program.dyadic(), r.program.clifford()); - try_method("statevector", r, &dyadic, &clifford, job_bytes).map_err(|e| tag(refusal::FABRIC, format!("fabric: only the statevector method is placed on the fabric's members, and it cannot take this job: {e}"))) + let sv = job.registry().method("statevector").ok_or("internal: no statevector method")?; + plan_one(sv, job).map_err(|e| tag(refusal::FABRIC, format!("fabric: only the statevector method is placed on the fabric's members, and it cannot take this job: {e}"))) } /// `p` for the job `v`, read as `r`, admitting the memory its intents' check /// takes beside the run, where it fits (super::verdict). -fn with_intent(v: &Value, r: &Request, mut p: Plan, job_bytes: u64) -> Result { +fn with_intent(reg: &Registry, v: &Value, r: &Request, mut p: Plan, job_bytes: u64) -> Result { if let Some(i) = v.get("intent").filter(|i| !i.is_null()) { - let need = super::verdict::intent_memory(v, &super::verdict::intents(i)?, r.program.n_qubits, r.noise.is_some(), job_bytes); + let need = super::verdict::intent_memory_in(reg, v, &super::verdict::intents(i)?, r.program.n_qubits, r.noise.is_some(), job_bytes); p.memory_bytes = p.memory_bytes.max(need); } Ok(p) } -/// The method for `r`: the one it names, or the first `auto` finds. -fn choose(r: &Request, job_bytes: u64) -> Result { - let dyadic = r.program.dyadic(); - let clifford = r.program.clifford(); +/// The method `m`'s plan for `job`: refused for a method that models no +/// noise when the job has a noise model, then the method's own plan, then +/// every channel the job names held to what the method takes (each refusal +/// of a channel `J061`). +pub(crate) fn plan_one(m: &dyn Method, job: &Job<'_>) -> Result { + let r = job.request()?; + if r.noise.is_some() && !m.models_noise() { + return Err(tag(refusal::CHANNEL_REFUSED, format!("{} does not model noise", m.name()))); + } + let p = m.plan(job)?; + if p.method != m.name() { + return Err(tag(refusal::CONTRACT, format!("the method {} planned the job for another method, {}", m.name(), p.method))); + } + for c in job.channels() { + m.takes(c.channel).map_err(|e| tag(refusal::CHANNEL_REFUSED, e))?; + } + Ok(p) +} + +/// The method for the job: the one it names, or the first `auto` finds, in +/// the registry's order for what the job asks ([`Question`]). +fn choose(job: &Job<'_>) -> Result { + let r = job.request()?; + let reg = job.registry(); if r.method != "auto" { - return try_method(&r.method, r, &dyadic, &clifford, job_bytes).map_err(|e| if refusal::codes_in(&e).last().is_some_and(|c| c.starts_with('J')) { e } else { tag(refusal::METHOD_REFUSES, e) }); + let Some(m) = reg.method(&r.method) else { + return Err(tag(refusal::UNKNOWN_METHOD, format!("unknown method {}; GET /v1/methods lists them", r.method))); + }; + return plan_one(m, job).map_err(|e| if refusal::codes_in(&e).last().is_some_and(|c| c.starts_with('J')) { e } else { tag(refusal::METHOD_REFUSES, e) }); } - let order: &[&str] = if r.noise.is_some() { - &["density_matrix", "stabilizer", "trajectories"] - } else if r.shots > 0 { - &["stabilizer", "exact", "statevector", "statevector_single", "cutrank", "mps"] - } else if !r.observables.is_empty() && r.amplitudes.is_empty() { - &["stabilizer", "exact", "statevector", "statevector_single", "stabilizer_rank", "pauli", "mps"] - } else { - &["exact", "statevector", "statevector_single", "cutrank", "stabilizer_rank", "tensor_network", "mps"] - }; let mut refusals = Vec::new(); - for m in order { - match try_method(m, r, &dyadic, &clifford, job_bytes) { + for m in reg.auto_order(Question::of(r)) { + match plan_one(m, job) { Ok(mut p) => { if !refusals.is_empty() { p.why = format!("{}; not {}", p.why, refusals.join("; ")); } return Ok(p); } - Err(e) => refusals.push(format!("{m}: {e}")), + Err(e) => refusals.push(format!("{}: {e}", m.name())), } } Err(tag(refusal::NO_METHOD, format!("no method takes this job — {}", refusals.join("; ")))) diff --git a/wai/wai-quantum/src/engine/compile.rs b/wai/wai-quantum/src/engine/compile.rs index d8187e479..d63c9e1ce 100644 --- a/wai/wai-quantum/src/engine/compile.rs +++ b/wai/wai-quantum/src/engine/compile.rs @@ -57,6 +57,7 @@ //! `epsilon.budget`. Up to [`WHOLE_MAX_QUBITS`] qubits the whole output is //! also measured against the whole program on their dense unitaries. +use super::registry::{Check, Kind as ComponentKind, Pass, PassInput, PassOutput, Registry}; use super::rewrite; use super::synth::{euler, kak, lower, mat, phase, ring, rz}; use crate::qasm::{Kind, Op, Program}; @@ -798,10 +799,143 @@ pub fn hash_of_text(text: &str) -> String { hash_hex(text.as_bytes()) } +/// A built-in pass's run (its registry entry's, +/// [`super::registry::Pass::run`]): the pass named `name` on `input`, with +/// its own check where it acts. +pub(crate) fn builtin_pass(name: &str, input: &PassInput<'_>) -> Result { + let (n, ops, o) = (input.qubits, input.ops, input.options); + let out = match name { + "lower" => pass_lower(ops)?, + "simplify" => pass_simplify(n, ops), + "phase_fold" => pass_phase_fold(n, ops), + "resynth_1q" => pass_resynth_1q(n, ops, o.basis), + "resynth_2q" => pass_resynth_2q(n, ops, o.basis), + "twirl" => pass_twirl(n, ops, o.twirl.unwrap_or(1)), + "dd" => pass_dd(n, ops, o.dd.unwrap_or(Dd::Xx)), + "approximate" => { + let m = rotations(ops); + let left = o.epsilon - input.spent; + let each = if m == 0 { o.epsilon.min(rz::MAX_EPSILON) } else { (left / m as f64).min(rz::MAX_EPSILON) }; + if m > 0 && each < rz::MIN_EPSILON { + return Err(format!("ε = {:e} shared by {m} rotations leaves {each:e} each, below the {:e} a rotation is approximated to; give a larger ε or fewer rotations", o.epsilon, rz::MIN_EPSILON)); + } + let (out, detail) = pass_approximate(ops, each)?; + return Ok(PassOutput { ops: out.ops, check: out.check, distance: out.distance, epsilon: Some((m, each)), rotations: Some(detail) }); + } + other => return Err(format!("no pass {other}")), + }; + Ok(PassOutput { ops: out.ops, check: out.check, distance: out.distance, epsilon: None, rotations: None }) +} + +/// The pass names a compile's `passes` gives, each one `reg` holds: run +/// after the level's own and before twirling and decoupling. Refused: a name +/// no pass here has (`J060`), or `passes` that is not a list of names. +pub(crate) fn passes_of<'a>(reg: &Registry, v: Option<&'a Value>) -> Result, String> { + use super::refusal::{COMPILE, UNKNOWN_COMPONENT, tag}; + let Some(v) = v.filter(|v| !v.is_null()) else { return Ok(Vec::new()) }; + let names = v.as_array().and_then(|a| a.iter().map(Value::as_str).collect::>>()).ok_or_else(|| tag(COMPILE, "compile: \"passes\" is a list of the names of passes to run after the level's"))?; + for name in &names { + if reg.pass(name).is_none() { + let here: Vec<&str> = reg.passes().map(|p| p.name()).collect(); + return Err(tag(UNKNOWN_COMPONENT, format!("compile: no pass {name} here; the passes here are {}", here.join(", ")))); + } + } + Ok(names) +} + +/// A registered pass on `input`, checked by the engine on the whole circuit +/// by the check it declares ([`Check`]): passed over when the circuit holds +/// gates it does not take, and rejected (left out, the program going on as +/// it was) when its gates are not ones it declared it gives or its check does +/// not hold. Refused (`J064`): gates on qubits the program does not have, or +/// that do not read back. +fn registered_pass(pass: &dyn Pass, input: &PassInput<'_>) -> Result { + use super::refusal::{CONTRACT, tag}; + let (n, before, o) = (input.qubits, input.ops, input.options); + let name = pass.name(); + let keep = |check: Value| PassOutput { ops: before.to_vec(), check, distance: 0.0, epsilon: None, rotations: None }; + if !pass.takes().holds(before) { + return Ok(keep(json!({ "how": "none", "verdict": "skipped", "why": format!("the circuit holds gates outside what the pass takes ({})", pass.takes().as_str()) }))); + } + let given = pass.run(input)?; + for g in &given.ops { + if let Some(&q) = g.qubits.iter().find(|&&q| q >= n) { + return Err(tag(CONTRACT, format!("the pass {name} gave a gate on qubit {q} of a {n}-qubit program"))); + } + if (1..g.qubits.len()).any(|i| g.qubits[..i].contains(&g.qubits[i])) { + return Err(tag(CONTRACT, format!("the pass {name} gave a {:?} on one qubit twice", g.kind))); + } + } + let text = rewrite::render(&given.ops, n, &[], 0); + match crate::qasm::parse(&text) { + Ok(back) if back.ops.len() == given.ops.len() => {} + _ => return Err(tag(CONTRACT, format!("the pass {name} gave gates that do not read back as the same circuit"))), + } + let said = given.check.clone(); + if !pass.gives().holds(&given.ops) { + return Ok(keep(json!({ "how": "the gates it gives, against what it declares", "verdict": "rejected", "why": format!("its output holds gates outside what it declares it gives ({})", pass.gives().as_str()), "by": "engine" }))); + } + let (a, b) = (program_of(n, before.to_vec()), program_of(n, given.ops.clone())); + let (mut check, distance) = match pass.check() { + Check::Ladder => { + let v = super::equiv::equivalent_programs(&a, &b); + let how = v["how"].as_str().unwrap_or("none").to_string(); + let verdict = match (v["verdict"].as_str(), how.as_str()) { + (Some("equivalent"), "tableau" | "rewrites") => "proof", + (Some("equivalent"), "exact") => "exact", + _ => "rejected", + }; + let mut c = json!({ "how": format!("the whole circuit against the circuit before, by the equivalence ladder ({how})"), "verdict": verdict, "by": "engine", "ladder": v }); + if verdict == "rejected" { + c["why"] = json!(match v["verdict"].as_str() { + Some("equivalent") => "the ladder found the two equal on random inputs alone, which is evidence, not a check a compile certifies".to_string(), + Some("different") => "the output is not the circuit before".to_string(), + _ => format!("the ladder does not decide it: {}", v["why"].as_str().unwrap_or("")), + }); + } + (c, 0.0) + } + Check::Fidelity { max_distance } => { + let total = (before.len() + given.ops.len()) as f64; + if n > WHOLE_MAX_QUBITS || total * (1u64 << (2 * n)) as f64 > WHOLE_MAX_WORK { + (json!({ "how": "the dense unitaries of the circuit before and after", "verdict": "rejected", "why": format!("{n} qubits and {} gates: wider than the dense check runs ({WHOLE_MAX_QUBITS} qubits)", before.len() + given.ops.len()), "by": "engine" }), 0.0) + } else { + let d = mat::distance(&mat::Dense::of(n, &given.ops), &mat::Dense::of(n, before)); + let left = o.epsilon - input.spent; + let mut c = json!({ "how": "the dense unitaries of the circuit before and after, their operator-norm distance up to a global phase", "by": "engine", "distance": d, "max_distance": max_distance }); + if d > max_distance { + c["verdict"] = json!("rejected"); + c["why"] = json!(format!("the output is {d:e} from the circuit before, more than the pass's bound {max_distance:e}")); + (c, 0.0) + } else if d > left { + c["verdict"] = json!("rejected"); + c["why"] = json!(format!("the output is {d:e} from the circuit before, more than the {left:e} left of the compile's budget")); + (c, 0.0) + } else { + c["verdict"] = json!(if d <= 1e-12 { "exact" } else { "approximate" }); + (c, d) + } + } + } + }; + if !said.is_null() { + check["said"] = said; + } + let applied = check["verdict"] != "rejected"; + Ok(PassOutput { ops: if applied { given.ops } else { before.to_vec() }, check, distance, epsilon: None, rotations: None }) +} + /// `p` compiled under `o`; refused for a program that measures in the /// middle, one larger than [`MAX_GATES`], or a budget the rotations cannot /// share. pub fn compile(p: &Program, o: &Options) -> Result { + compile_in(Registry::builtins(), p, o, &[]) +} + +/// [`compile`] through `reg`, with `extra` (passes `reg` holds) run after +/// the level's own and before twirling and decoupling. A registered pass's +/// record names it (`component`), and its check is the engine's. +pub(crate) fn compile_in(reg: &Registry, p: &Program, o: &Options, extra: &[&str]) -> Result { if p.dynamic.is_some() { return Err("the program measures in the middle, resets after a gate or acts on what it measured; compiling takes programs whose measurements all come at the end".into()); } @@ -815,37 +949,37 @@ pub fn compile(p: &Program, o: &Options) -> Result { let mut epsilon = json!({ "budget": o.epsilon }); let mut rotation_detail = Vec::new(); let mut stages = Vec::new(); - for &name in &pipeline(o) { + let mut names = base_pipeline(o); + names.extend(extra.iter().copied()); + if o.twirl.is_some() { + names.push("twirl"); + } + if o.dd.is_some() { + names.push("dd"); + } + for &name in &names { let before = counts(n, &ops); - let out = match name { - "lower" => pass_lower(&ops)?, - "simplify" => pass_simplify(n, &ops), - "phase_fold" => pass_phase_fold(n, &ops), - "resynth_1q" => pass_resynth_1q(n, &ops, o.basis), - "resynth_2q" => pass_resynth_2q(n, &ops, o.basis), - "twirl" => pass_twirl(n, &ops, o.twirl.unwrap_or(1)), - "dd" => pass_dd(n, &ops, o.dd.unwrap_or(Dd::Xx)), - "approximate" => { - let m = rotations(&ops); - let left = o.epsilon - bound; - let each = if m == 0 { o.epsilon.min(rz::MAX_EPSILON) } else { (left / m as f64).min(rz::MAX_EPSILON) }; - if m > 0 && each < rz::MIN_EPSILON { - return Err(format!("ε = {:e} shared by {m} rotations leaves {each:e} each, below the {:e} a rotation is approximated to; give a larger ε or fewer rotations", o.epsilon, rz::MIN_EPSILON)); - } - epsilon["rotations"] = json!(m); - epsilon["each"] = json!(each); - let (out, detail) = pass_approximate(&ops, each)?; - rotation_detail = detail; - out - } - other => return Err(format!("no pass {other}")), - }; + let pass = reg.pass(name).ok_or_else(|| super::refusal::tag(super::refusal::UNKNOWN_COMPONENT, format!("no pass {name} here")))?; + let id = reg.identity(ComponentKind::Pass, name).ok_or("internal: a pass without an identity")?; + let input = PassInput { qubits: n, ops: &ops, options: o, spent: bound }; + let out = if id.is_builtin() { pass.run(&input)? } else { registered_pass(pass, &input)? }; + if let Some((m, each)) = out.epsilon { + epsilon["rotations"] = json!(m); + epsilon["each"] = json!(each); + } + if let Some(detail) = out.rotations { + rotation_detail = detail; + } let applied = out.check["verdict"] != "rejected" && out.check["verdict"] != "skipped"; if applied { bound += out.distance; ops = out.ops; } - records.push(json!({ "pass": name, "before": before, "after": counts(n, &ops), "check": out.check })); + let mut record = json!({ "pass": name, "before": before, "after": counts(n, &ops), "check": out.check }); + if !id.is_builtin() { + record["component"] = id.to_json(); + } + records.push(record); if o.stages { stages.push((name.to_string(), rewrite::render(&ops, n, &p.measured, p.n_bits))); } @@ -939,10 +1073,18 @@ pub fn certificate_hash(_: &Value) -> Value { /// in, `{"ok": true, "options", "qasm", "certificate"}` out (with `stages`, /// also each pass's circuit), not yet sealed; or the reason it is refused. pub fn call(request: &Value) -> Result { + call_in(Registry::builtins(), request) +} + +/// [`call`] through `reg`, with the request's `passes` (passes `reg` holds) +/// run after the level's own. An answer a registered pass took part in +/// carries `components`, the identity of every component that made it. +pub(crate) fn call_in(reg: &Registry, request: &Value) -> Result { let src = request.get("qasm").and_then(Value::as_str).ok_or("give \"qasm\": the program")?; let p = crate::qasm::parse(src)?; let o = Options::read(request)?; - let c = compile(&p, &o)?; + let extra = passes_of(reg, request.get("passes"))?; + let c = compile_in(reg, &p, &o, &extra)?; let mut out = json!({ "ok": true, "options": o.to_json(), "qasm": c.qasm, "certificate": c.certificate }); if o.stages { out["stages"] = c.stages.iter().map(|(pass, qasm)| json!({ "pass": pass, "qasm": qasm })).collect(); @@ -950,6 +1092,17 @@ pub fn call(request: &Value) -> Result { // The engine that compiled it, and the schema it follows, inside what the // receipt seals. super::schema::stamp_as(&mut out, super::schema::COMPILE_RESULT); + let mut used: Vec<&super::registry::Identity> = reg.identity(ComponentKind::FrontEnd, "qasm").into_iter().collect(); + for r in c.certificate["passes"].as_array().into_iter().flatten() { + if let Some(i) = r["pass"].as_str().and_then(|n| reg.identity(ComponentKind::Pass, n)) + && !used.contains(&i) + { + used.push(i); + } + } + if used.iter().any(|i| !i.is_builtin()) { + out["components"] = used.iter().map(|i| i.to_json()).collect(); + } Ok(out) } diff --git a/wai/wai-quantum/src/engine/fabric_measured.rs b/wai/wai-quantum/src/engine/fabric_measured.rs index 6fd0d0488..0c1e68971 100644 --- a/wai/wai-quantum/src/engine/fabric_measured.rs +++ b/wai/wai-quantum/src/engine/fabric_measured.rs @@ -393,6 +393,9 @@ mod tests { assert_eq!(tail(&[row(18, 10.0, 5.0, 50), row(20, 40.0, 30.0, 55), row(22, 160.0, 170.0, 60)]), None); } + /// A job that names its placement plans only where the fabric is built + /// (a build without it honours `auto` alone). + #[cfg(feature = "quantum_fabric")] #[test] fn the_measured_circuit_reads_and_its_job_plans() { for n in [2u32, 9, 18] { diff --git a/wai/wai-quantum/src/engine/formats/mod.rs b/wai/wai-quantum/src/engine/formats/mod.rs index 9d489c195..c7f54f7cb 100644 --- a/wai/wai-quantum/src/engine/formats/mod.rs +++ b/wai/wai-quantum/src/engine/formats/mod.rs @@ -33,6 +33,7 @@ mod emit; pub use qir::Profile; +use crate::engine::registry::{FrontEnd, Registry}; use crate::qasm::diag::{self, Diagnostic}; use crate::qasm::Program; use serde_json::{Value, json}; @@ -137,25 +138,22 @@ pub struct Exported { /// A program read from `input` in `format`, as OpenQASM 3 that the engine's /// reader reads. pub fn read(format: Format, input: &[u8]) -> Result { - let text = || std::str::from_utf8(input).map_err(|_| Diagnostic::new(diag::FORMAT_UNREAD, None, format!("{} is text, and this is not UTF-8", format.name()))); - let imported = match format { - Format::Qasm => { - let t = text()?; - crate::qasm::read(t)?; - return Ok(Imported { qasm: t.to_string(), notes: Vec::new(), summary: json!({ "format": "qasm" }) }); - } - Format::Qir => { - let r = qir::read(text()?)?; - Imported { qasm: r.qasm, notes: r.notes, summary: r.summary } - } - Format::Jeff => { - let r = jeff::read(input)?; - Imported { qasm: r.qasm, notes: r.notes, summary: r.summary } - } - }; + read_fe(builtin(format), input) +} + +/// The built-in front end of `format`. +fn builtin(format: Format) -> &'static dyn FrontEnd { + Registry::builtins().front_end(format.name()).expect("every format has its built-in front end") +} + +/// A program read by the front end `fe`, as OpenQASM 3 that the engine's +/// reader reads: what the front end gives is read again here, and a +/// program that does not read is refused rather than passed on. +pub(crate) fn read_fe(fe: &dyn FrontEnd, input: &[u8]) -> Result { + let imported = fe.read(input)?; // The text a reader writes reads; if it did not, that is this reader's // fault, and it is refused rather than passed on. - crate::qasm::read(&imported.qasm).map_err(|d| Diagnostic::new(d.code, None, format!("the program read from {} does not read as OpenQASM: {}", format.name(), d.message)))?; + crate::qasm::read(&imported.qasm).map_err(|d| Diagnostic::new(d.code, None, format!("the program read from {} does not read as OpenQASM: {}", fe.name(), d.message)))?; Ok(imported) } @@ -209,28 +207,52 @@ pub struct Converted { /// read into OpenQASM is written back to its own format and read again, and /// the two readings held to each other. pub fn convert(from: Format, to: Format, input: &[u8], profile: Option, check: bool) -> Result { - if profile.is_some() && to != Format::Qir { - return Err(Diagnostic::new(diag::WRITE_CANNOT, None, format!("a profile is QIR's; {} takes none", to.name()))); + let c = convert_fe(Registry::builtins(), builtin(from), builtin(to), input, profile.map(Profile::name), check)?; + Ok(Converted { from, to, output: c.output, qasm: c.qasm, notes: c.notes, read: c.read, written: c.written, check: c.check }) +} + +/// A conversion between two front ends, as [`convert`] gives it. +pub(crate) struct ConvertedFe { + pub output: Vec, + pub qasm: String, + pub notes: Vec, + pub read: Value, + pub written: Value, + pub check: Value, +} + +/// [`convert`] between the front ends `from` and `to` of `reg`. +pub(crate) fn convert_fe(reg: &Registry, from: &dyn FrontEnd, to: &dyn FrontEnd, input: &[u8], profile: Option<&str>, check: bool) -> Result { + if let Some(p) = profile { + let profiles = to.profiles(); + if profiles.is_empty() { + let owners: Vec<&str> = reg.front_ends().filter(|f| !f.profiles().is_empty()).map(|f| f.title()).collect(); + return Err(Diagnostic::new(diag::WRITE_CANNOT, None, format!("a profile is {}'s; {} takes none", owners.join(" or "), to.name()))); + } + if !profiles.iter().any(|x| x == p) { + return Err(Diagnostic::new(diag::WRITE_CANNOT, None, format!("profile: {p} is not a {} profile; the profiles are {}", to.title(), profiles.join(" and ")))); + } } - let imported = read(from, input)?; + let imported = read_fe(from, input)?; let program = crate::qasm::read(&imported.qasm)?.program; let mut notes = imported.notes.clone(); - let (output, written) = if to == Format::Qasm { + let to_qasm = to.name() == Format::Qasm.name(); + let (output, written) = if to_qasm { (imported.qasm.clone().into_bytes(), json!({ "format": "qasm" })) } else { // (The same format as read is written afresh from the program read.) - let e = write(to, &program, profile)?; + let e = to.write(&program, profile)?; notes.extend(e.notes); (e.bytes, e.summary) }; let check = if !check { Value::Null - } else if to != Format::Qasm { - let back = read(to, &output)?; + } else if !to_qasm { + let back = read_fe(to, &output)?; let p2 = crate::qasm::read(&back.qasm)?.program; json!({ "what": format!("the {} written, read back, against the program read", to.name()), "verdict": held(&program, &imported.qasm, &p2, &back.qasm) }) - } else if from != Format::Qasm { - let again = write(from, &program, None).and_then(|e| read(from, &e.bytes)); + } else if from.name() != Format::Qasm.name() { + let again = from.write(&program, None).and_then(|e| read_fe(from, &e.bytes)); match again { Ok(a) => { let p2 = crate::qasm::read(&a.qasm)?.program; @@ -241,7 +263,7 @@ pub fn convert(from: Format, to: Format, input: &[u8], profile: Option, } else { json!({ "what": "OpenQASM to OpenQASM: the text is the program", "verdict": { "verdict": "equivalent", "how": "identity", "why": "the program is passed through as it is" } }) }; - Ok(Converted { from, to, output, qasm: imported.qasm, notes, read: imported.summary, written, check }) + Ok(ConvertedFe { output, qasm: imported.qasm, notes, read: imported.summary, written, check }) } /// The ladder's verdict on two programs. Programs that measure in the middle @@ -392,16 +414,21 @@ pub fn decode_base64(text: &str) -> Result, String> { Ok(out) } -fn format_field(v: &Value, key: &str) -> Result, String> { +fn format_field<'a>(reg: &'a Registry, v: &Value, key: &str) -> Result, String> { match v.get(key) { None | Some(Value::Null) => Ok(None), - Some(Value::String(s)) => Format::parse(s).map(Some).ok_or_else(|| crate::engine::refusal::tag(crate::engine::refusal::BAD_FIELD, format!("{key}: {s} is not a format; the formats are {}", names()))), - Some(_) => Err(crate::engine::refusal::tag(crate::engine::refusal::BAD_FIELD, format!("{key} is a format's name: {}", names()))), + Some(Value::String(s)) => reg.front_end(s).map(Some).ok_or_else(|| crate::engine::refusal::tag(crate::engine::refusal::BAD_FIELD, format!("{key}: {s} is not a format; the formats are {}", names(reg)))), + Some(_) => Err(crate::engine::refusal::tag(crate::engine::refusal::BAD_FIELD, format!("{key} is a format's name: {}", names(reg)))), } } -fn names() -> String { - Format::all().iter().map(|f| f.name()).collect::>().join(", ") +fn names(reg: &Registry) -> String { + reg.front_ends().map(|f| f.name()).collect::>().join(", ") +} + +/// The front end whose format `bytes` look like ([`Format::sniff`]). +fn sniffed(bytes: &[u8]) -> &'static dyn FrontEnd { + builtin(Format::sniff(bytes)) } /// The program a request carries: `program` (text) or `program_base64` @@ -428,16 +455,22 @@ fn notes_json(notes: &[Diagnostic]) -> Value { /// "written", "check"}`. A refusal is the reader's or writer's diagnostic, with its code /// last (`… [Q704]`), or the job layer's for a request that does not read. pub fn convert_json(req: &Value) -> Result { + convert_json_in(Registry::builtins(), req) +} + +/// [`convert_json`] with `reg`'s front ends. A conversion a registered front +/// end took part in carries `components`, the identities of the two. +pub(crate) fn convert_json_in(reg: &Registry, req: &Value) -> Result { use crate::engine::refusal::{BAD_FIELD, tag}; if !req.is_object() { return Err(tag(BAD_FIELD, "a conversion is a JSON object: {\"to\", \"program\" or \"program_base64\", \"from\", \"profile\", \"check\"}")); } let bytes = program_bytes(req)?; - let from = format_field(req, "from")?.unwrap_or_else(|| Format::sniff(&bytes)); - let to = format_field(req, "to")?.ok_or_else(|| tag(BAD_FIELD, format!("\"to\": the format to write, one of {}", names())))?; + let from = format_field(reg, req, "from")?.unwrap_or_else(|| sniffed(&bytes)); + let to = format_field(reg, req, "to")?.ok_or_else(|| tag(BAD_FIELD, format!("\"to\": the format to write, one of {}", names(reg))))?; let profile = match req.get("profile") { None | Some(Value::Null) => None, - Some(Value::String(s)) => Some(Profile::parse(s).ok_or_else(|| tag(BAD_FIELD, format!("profile: {s} is not a QIR profile; the profiles are base and adaptive")))?), + Some(Value::String(s)) => Some(Profile::parse(s).map(Profile::name).or_else(|| to.profiles().into_iter().find(|p| p == s).map(|_| s.as_str())).ok_or_else(|| tag(BAD_FIELD, format!("profile: {s} is not a QIR profile; the profiles are base and adaptive")))?), Some(_) => return Err(tag(BAD_FIELD, "profile is base or adaptive")), }; let check = match req.get("check") { @@ -445,22 +478,26 @@ pub fn convert_json(req: &Value) -> Result { Some(Value::Bool(b)) => *b, Some(_) => return Err(tag(BAD_FIELD, "check is true or false")), }; - let c = convert(from, to, &bytes, profile, check).map_err(String::from)?; + let c = convert_fe(reg, from, to, &bytes, profile, check).map_err(String::from)?; let mut out = json!({ "ok": true, "format": FORMAT, - "from": c.from.name(), - "to": c.to.name(), + "from": from.name(), + "to": to.name(), "qasm": c.qasm, "notes": notes_json(&c.notes), "read": c.read, "written": c.written, "check": c.check, }); - if c.to.is_binary() { + if to.binary() { out["program_base64"] = json!(encode_base64(&c.output)); } else { - out["program"] = json!(String::from_utf8(c.output).expect("a text format writes text")); + out["program"] = json!(String::from_utf8(c.output).map_err(|_| crate::engine::refusal::tag(crate::engine::refusal::CONTRACT, format!("the front end {} is text, and wrote bytes that are not UTF-8", to.name())))?); + } + let used: Vec<&crate::engine::registry::Identity> = [from.name(), to.name()].iter().filter_map(|n| reg.identity(crate::engine::registry::Kind::FrontEnd, n)).collect(); + if used.iter().any(|i| !i.is_builtin()) { + out["components"] = used.iter().map(|i| i.to_json()).collect(); } Ok(out) } @@ -513,6 +550,11 @@ pub fn program_required() -> Value { /// whose `qasm` is what its `source` reads to (read already); one whose /// `qasm` is something else is refused, as is a source that does not read. pub fn expand(req: &Value) -> Result, String> { + expand_in(Registry::builtins(), req) +} + +/// [`expand`] with `reg`'s front ends. +pub(crate) fn expand_in(reg: &Registry, req: &Value) -> Result, String> { use crate::engine::refusal::{BAD_FIELD, NOT_A_JOB, tag}; let Some(src) = req.get("source").filter(|s| !s.is_null()) else { return Ok(None) }; let given = req.get("qasm").filter(|q| !q.is_null()); @@ -520,8 +562,8 @@ pub fn expand(req: &Value) -> Result, String> { return Err(tag(BAD_FIELD, "\"source\" is {\"format\", \"program\" or \"program_base64\"}")); } let bytes = program_bytes(src)?; - let format = format_field(src, "format")?.unwrap_or_else(|| Format::sniff(&bytes)); - let imported = read(format, &bytes).map_err(|d| tag(NOT_A_JOB, format!("the source does not read as {}: {d}", format.name())))?; + let format = format_field(reg, src, "format")?.unwrap_or_else(|| sniffed(&bytes)); + let imported = read_fe(format, &bytes).map_err(|d| tag(NOT_A_JOB, format!("the source does not read as {}: {d}", format.name())))?; // A request read already (one a receipt sealed) holds both, alike. if let Some(q) = given { if q.as_str() == Some(imported.qasm.as_str()) { diff --git a/wai/wai-quantum/src/engine/job.rs b/wai/wai-quantum/src/engine/job.rs index 917d46ae7..0b03eedd3 100644 --- a/wai/wai-quantum/src/engine/job.rs +++ b/wai/wai-quantum/src/engine/job.rs @@ -145,7 +145,7 @@ pub fn answer(request: &Value, outcome: Result) -> Result String { + pinned_digest_in(crate::engine::registry::Registry::builtins(), request) + } + + /// [`pinned_digest`] for the job planned and run through `reg`. + pub(crate) fn pinned_digest_in(reg: &crate::engine::registry::Registry, request: &str) -> String { let req: Value = serde_json::from_str(request).unwrap(); - let (plan, mut result) = plan_and_run(&req, 1 << 30).unwrap(); + let (plan, mut result) = reg.plan_and_run(&req, 1 << 30).unwrap(); // The wall clock, and the engine and schema every result now names // (engine::schema::stamp), which those bytes predate: the rest is what // the pins hold. diff --git a/wai/wai-quantum/src/engine/mod.rs b/wai/wai-quantum/src/engine/mod.rs index 68298fcc1..0338ed620 100644 --- a/wai/wai-quantum/src/engine/mod.rs +++ b/wai/wai-quantum/src/engine/mod.rs @@ -140,6 +140,14 @@ //! calibrations, the device and target snapshot, and the integrator and //! its step; its schedule as a view; and a pulse job checked by running //! it again. +//! - [`registry`]: the extension points. Each simulation method, compiler +//! pass, noise channel and program format is a component behind a trait +//! ([`registry::Method`], [`registry::Pass`], [`registry::Channel`], +//! [`registry::FrontEnd`]), held in one registry the planner, the run, the +//! compile and the conversions go through; every built-in registers through +//! the same trait, and a host registers its own +//! ([`registry::Registry::register`]), its identity sealed in every result +//! it takes part in and marked as registered, never as built in. //! - [`refusal`]: the job layer's refusal codes (`J…`), beside the reader's //! (`Q…`): the request, the method, an intent, a repair and the host. //! - [`energy`]: what the CPU cores spent for one process, as the kernel counts @@ -222,6 +230,7 @@ pub mod provider; pub mod receipt; pub mod pulse; pub mod refusal; +pub mod registry; pub mod repair; #[cfg(feature = "quantum_receipt")] pub mod sets; diff --git a/wai/wai-quantum/src/engine/refusal.rs b/wai/wai-quantum/src/engine/refusal.rs index 38c61fd31..5a09db4e1 100644 --- a/wai/wai-quantum/src/engine/refusal.rs +++ b/wai/wai-quantum/src/engine/refusal.rs @@ -109,6 +109,14 @@ table! { OPTIMIZE = "J052", Refused, "An optimisation, a variational eigensolver or a QAOA run that does not read or cannot be done: an unknown optimiser, settings or steps past their limits, a Hamiltonian or a graph that does not read (the message says which)."; BUILD = "J053", Refused, "An algorithm builder's request that does not read: an unknown algorithm, sizes past its limits, or arguments it does not take (the message says which)."; + // Registered components (engine::registry). + UNKNOWN_COMPONENT = "J060", Refused, "A compiler pass a request names (`compile.passes`) that this host does not hold: the components list names every method, pass, noise channel and front end here, and no host loads a component from a request."; + CHANNEL_REFUSED = "J061", Refused, "A noise channel the method cannot take: a method that models no noise, or a channel the method has no form for (amplitude damping on the stabilizer's Pauli frames, a registered channel on a method that takes only the built-in ones); the message names what does take it."; + CHANNEL_INVALID = "J062", Refused, "A noise channel whose parameters do not read or whose operators fail its validity check: a value outside its range, Kraus operators that are not completely positive and trace preserving (their sum of K†K off the identity), or a Pauli form whose probabilities are negative, do not sum to 1, or are not the channel its operators make (the message says by how much)."; + REGISTER = "J063", Refused, "A component the registry does not take: a name its kind holds already (a built-in's among them), a name or version that is empty or not plain text, a descriptor that is not a JSON object, a channel on other than one or two qubits, or a pass whose check states no bound."; + CONTRACT = "J064", Refused, "A registered component's answer that breaks the engine's contract: a method's result without an accuracy class it declared, or one that writes a member the engine writes (method, qubits, gates, compiled, repaired, engine, schema, components, receipt, verdict); a pass's gates on qubits the program does not have, or that do not read back; a front end's program that does not read. Nothing it gave is returned."; + REGISTERED_INTENT = "J065", Refused, "An intent on a job whose noise names a registered channel: the intent checks model the built-in channels alone, so the check is refused rather than made against another model."; + // An answer's view statements (the answer view layer). ANSWER_PARSE = "J100", Dropped, "A line of an answer that is not a statement (`name = View(...)`), a program (a fenced `qasm` block with its name), a comment or a marker: left out."; ANSWER_VIEW = "J101", Dropped, "A view the answer layer does not have: left out (the views are listed in the layer's specification)."; diff --git a/wai/wai-quantum/src/engine/registry/builtin.rs b/wai/wai-quantum/src/engine/registry/builtin.rs new file mode 100644 index 000000000..a40954a1a --- /dev/null +++ b/wai/wai-quantum/src/engine/registry/builtin.rs @@ -0,0 +1,394 @@ +//! The built-in components, each registered through the trait a host's own +//! implements: the methods (their plans and runs the engine's own, in +//! `catalog` and `run`), the compile's passes (`compile`), the five noise +//! channels and the three formats (`formats`). + +use super::{Channel, Check, Component, FrontEnd, GateSet, Job, Method, Param, Pass, PassInput, PassOutput, Placement, Question}; +use crate::engine::catalog::{self, Plan}; +use crate::engine::formats::{self, Exported, Format, Imported}; +use crate::engine::schema::ENGINE_VERSION; +use crate::gates::C; +use crate::qasm::Program; +use crate::qasm::diag::{self, Diagnostic}; +use serde_json::{Value, json}; + +/// Every built-in, in the order the registry holds them: the methods as the +/// methods list gives them, then the passes, the channels (as a job's +/// `noise` lists them) and the formats. +pub(super) fn components() -> Vec { + let mut v: Vec = METHODS.iter().map(|m| Component::method(*m)).collect(); + v.push(Component::method(PulseMethod)); + v.extend(PASSES.iter().map(|p| Component::pass(*p))); + v.extend(CHANNELS.iter().map(|c| Component::channel(*c))); + v.extend([Component::front_end(Fe(Format::Qasm)), Component::front_end(Fe(Format::Qir)), Component::front_end(Fe(Format::Jeff))]); + v +} + +// --------------------------------------------------------------------------- +// Methods +// --------------------------------------------------------------------------- + +/// A built-in simulation method: its plan is the catalog's, its run the +/// worker's, its words the methods list's. +#[derive(Clone, Copy)] +struct BuiltinMethod { + name: &'static str, + classes: &'static [&'static str], + noise: bool, + /// Its rank for each question: noise, counts, expectations, amplitudes. + ranks: [Option; 4], +} + +const fn m(name: &'static str, classes: &'static [&'static str], noise: bool, ranks: [Option; 4]) -> BuiltinMethod { + BuiltinMethod { name, classes, noise, ranks } +} + +/// The built-in methods, as the methods list gives them, each with where +/// `auto` tries it: with noise, `density_matrix`, `stabilizer`, +/// `trajectories`; for counts, `stabilizer`, `exact`, `statevector`, +/// `statevector_single`, `cutrank`, `mps`; for expectation values alone, +/// `stabilizer`, `exact`, `statevector`, `statevector_single`, +/// `stabilizer_rank`, `pauli`, `mps`; for amplitudes, `exact`, +/// `statevector`, `statevector_single`, `cutrank`, `stabilizer_rank`, +/// `tensor_network`, `mps`. +const METHODS: [BuiltinMethod; 12] = [ + m("exact", &["bit-exact"], false, [None, Some(20), Some(20), Some(10)]), + m("statevector", &["exact"], false, [None, Some(30), Some(30), Some(20)]), + m("statevector_single", &["single"], false, [None, Some(40), Some(40), Some(30)]), + m("stabilizer", &["exact", "estimate"], true, [Some(20), Some(10), Some(10), None]), + m("pauli", &["bounded"], false, [None, None, Some(60), None]), + m("mps", &["exact", "heuristic"], false, [None, Some(60), Some(70), Some(70)]), + m("stabilizer_rank", &["exact", "estimate"], false, [None, None, Some(50), Some(50)]), + m("tensor_network", &["exact"], false, [None, None, None, Some(60)]), + m("cutrank", &["exact", "single"], false, [None, Some(50), None, Some(40)]), + m("cutrank_single", &["single"], false, [None, None, None, None]), + m("density_matrix", &["exact"], true, [Some(10), None, None, None]), + m("trajectories", &["estimate"], true, [Some(30), None, None, None]), +]; + +fn question_index(q: Question) -> usize { + match q { + Question::Noise => 0, + Question::Counts => 1, + Question::Expectations => 2, + Question::Amplitudes => 3, + } +} + +impl Method for BuiltinMethod { + fn name(&self) -> &str { + self.name + } + fn version(&self) -> &str { + ENGINE_VERSION + } + fn classes(&self) -> Vec { + self.classes.iter().map(|c| c.to_string()).collect() + } + fn descriptor(&self) -> Value { + catalog::method_text(self.name) + } + fn describe(&self, job_bytes: u64) -> Value { + catalog::describe_one(self.name, job_bytes) + } + fn models_noise(&self) -> bool { + self.noise + } + fn auto_rank(&self, question: Question) -> Option { + self.ranks[question_index(question)] + } + fn plan(&self, job: &Job<'_>) -> Result { + catalog::try_method(self.name, job.request()?, job.dyadic(), job.clifford(), job.job_bytes()) + } + fn run(&self, job: &Job<'_>, plan: &Plan, out: &mut Value) -> Result<(), String> { + crate::engine::run::run_builtin(self.name, job, plan, out) + } +} + +/// The pulse simulator, as the method `pulse`: it reads its job's pulse +/// program itself (`engine::pulse`). +#[derive(Clone, Copy)] +struct PulseMethod; + +impl Method for PulseMethod { + fn name(&self) -> &str { + crate::engine::pulse::METHOD + } + fn version(&self) -> &str { + ENGINE_VERSION + } + fn classes(&self) -> Vec { + vec!["bounded".into()] + } + fn descriptor(&self) -> Value { + let mut d = crate::engine::pulse::describe(); + if let Some(o) = d.as_object_mut() { + o.remove("method"); + o.remove("max_qubits"); + } + d + } + fn describe(&self, _: u64) -> Value { + crate::engine::pulse::describe() + } + fn reads_program(&self) -> bool { + true + } + fn plan(&self, job: &Job<'_>) -> Result { + crate::engine::pulse::plan(job.json(), job.job_bytes()) + } + fn run(&self, job: &Job<'_>, plan: &Plan, out: &mut Value) -> Result<(), String> { + *out = crate::engine::pulse::run(job.json(), plan)?; + Ok(()) + } +} + +// --------------------------------------------------------------------------- +// Passes +// --------------------------------------------------------------------------- + +/// A built-in compiler pass: its run is the compile's own, and it checks +/// itself where it acts, as its record says. +#[derive(Clone, Copy)] +struct BuiltinPass { + name: &'static str, + takes: GateSet, + gives: GateSet, + check: Check, +} + +const PASSES: [BuiltinPass; 8] = [ + BuiltinPass { name: "lower", takes: GateSet::Any, gives: GateSet::Basis, check: Check::Fidelity { max_distance: 1e-12 } }, + BuiltinPass { name: "simplify", takes: GateSet::Any, gives: GateSet::Any, check: Check::Ladder }, + BuiltinPass { name: "phase_fold", takes: GateSet::Any, gives: GateSet::Any, check: Check::Ladder }, + BuiltinPass { name: "resynth_1q", takes: GateSet::Any, gives: GateSet::Any, check: Check::Fidelity { max_distance: 1e-12 } }, + BuiltinPass { name: "resynth_2q", takes: GateSet::Any, gives: GateSet::Any, check: Check::Fidelity { max_distance: 1e-9 } }, + BuiltinPass { name: "approximate", takes: GateSet::Basis, gives: GateSet::CliffordT, check: Check::Fidelity { max_distance: crate::engine::synth::rz::MAX_EPSILON } }, + BuiltinPass { name: "twirl", takes: GateSet::Any, gives: GateSet::Any, check: Check::Ladder }, + BuiltinPass { name: "dd", takes: GateSet::Any, gives: GateSet::Any, check: Check::Ladder }, +]; + +impl Pass for BuiltinPass { + fn name(&self) -> &str { + self.name + } + fn version(&self) -> &str { + ENGINE_VERSION + } + fn descriptor(&self) -> Value { + let does = crate::engine::compile::PASSES.iter().find(|(n, _)| *n == self.name).map(|(_, d)| *d).unwrap_or(""); + json!({ "does": does }) + } + fn takes(&self) -> GateSet { + self.takes + } + fn gives(&self) -> GateSet { + self.gives + } + fn check(&self) -> Check { + self.check + } + fn run(&self, input: &PassInput<'_>) -> Result { + crate::engine::compile::builtin_pass(self.name, input) + } +} + +// --------------------------------------------------------------------------- +// Noise channels +// --------------------------------------------------------------------------- + +/// A built-in noise channel: one probability, applied by the noisy methods +/// as their noise model holds it. +#[derive(Clone, Copy)] +struct BuiltinChannel { + name: &'static str, + title: &'static str, + does: &'static str, +} + +const CHANNELS: [BuiltinChannel; 5] = [ + BuiltinChannel { name: "depolarizing_1q", title: "one-qubit depolarizing noise", does: "X, Y and Z each with probability p/3 after each gate on other than two qubits, on each qubit it touched" }, + BuiltinChannel { name: "depolarizing_2q", title: "two-qubit depolarizing noise", does: "each of the 15 non-identity two-qubit Paulis with probability p/15 after each two-qubit gate" }, + BuiltinChannel { name: "amplitude_damping", title: "amplitude damping", does: "amplitude damping of strength p after each gate, on each qubit it touched" }, + BuiltinChannel { name: "dephasing", title: "dephasing", does: "Z with probability p after each gate, on each qubit it touched" }, + BuiltinChannel { name: "readout", title: "readout error", does: "each measured bit read flipped with probability p" }, +]; + +/// What `stabilizer` says of amplitude damping, which has no Pauli form. +pub(crate) const STABILIZER_NOT_DAMPING: &str = "stabilizer models Pauli noise (depolarizing, dephasing, readout), not amplitude damping"; + +fn c(re: f64) -> C { + C { re, im: 0.0 } +} + +const PAULIS: [char; 4] = ['I', 'X', 'Y', 'Z']; + +fn pauli_one(p: char) -> [C; 4] { + let (z, o) = (c(0.0), c(1.0)); + match p { + 'X' => [z, o, o, z], + 'Y' => [z, C { re: 0.0, im: -1.0 }, C { re: 0.0, im: 1.0 }, z], + 'Z' => [o, z, z, c(-1.0)], + _ => [o, z, z, o], + } +} + +/// `w · (a ⊗ b)`, `a` on the most significant qubit. +fn kron_scaled(w: f64, a: &[C; 4], b: &[C; 4]) -> Vec { + let mut m = vec![c(0.0); 16]; + for r in 0..4 { + for col in 0..4 { + let (x, y) = (a[(r >> 1) * 2 + (col >> 1)], b[(r & 1) * 2 + (col & 1)]); + m[r * 4 + col] = C { re: w * (x.re * y.re - x.im * y.im), im: w * (x.re * y.im + x.im * y.re) }; + } + } + m +} + +impl BuiltinChannel { + /// Its Pauli form at probability `p`, where it has one. + fn form(&self, p: f64) -> Option> { + Some(match self.name { + "depolarizing_1q" => PAULIS.iter().enumerate().map(|(i, &q)| (if i == 0 { 1.0 - p } else { p / 3.0 }, q.to_string())).collect(), + "depolarizing_2q" => (0..16).map(|k| (if k == 0 { 1.0 - p } else { p / 15.0 }, format!("{}{}", PAULIS[k >> 2], PAULIS[k & 3]))).collect(), + "dephasing" => vec![(1.0 - p, "I".into()), (p, "Z".into())], + "readout" => vec![(1.0 - p, "I".into()), (p, "X".into())], + _ => return None, + }) + } +} + +impl Channel for BuiltinChannel { + fn name(&self) -> &str { + self.name + } + fn version(&self) -> &str { + ENGINE_VERSION + } + fn title(&self) -> &str { + self.title + } + fn descriptor(&self) -> Value { + json!({ "does": self.does }) + } + fn params(&self) -> Vec { + vec![Param::new("p", 0.0, 1.0, "a probability")] + } + fn arity(&self) -> u32 { + if self.name == "depolarizing_2q" { 2 } else { 1 } + } + fn placement(&self) -> Placement { + if self.name == "readout" { Placement::OnReadout } else { Placement::AfterGate } + } + fn kraus(&self, values: &[f64]) -> Vec> { + let p = values.first().copied().unwrap_or(0.0); + if self.name == "amplitude_damping" { + let (z, o) = (c(0.0), c(1.0)); + return vec![vec![o, z, z, c((1.0 - p).sqrt())], vec![z, c(p.sqrt()), z, z]]; + } + let form = self.form(p).unwrap_or_default(); + form.iter() + .filter(|(w, _)| *w > 0.0) + .map(|(w, s)| { + let ps: Vec = s.chars().collect(); + if ps.len() == 2 { + kron_scaled(w.sqrt(), &pauli_one(ps[0]), &pauli_one(ps[1])) + } else { + pauli_one(ps[0]).iter().map(|x| C { re: w.sqrt() * x.re, im: w.sqrt() * x.im }).collect() + } + }) + .collect() + } + fn pauli(&self, values: &[f64]) -> Option> { + self.form(values.first().copied().unwrap_or(0.0)) + } + fn taken_by(&self, method: &str) -> Result { + match method { + "density_matrix" if self.name == "readout" => Ok("exactly: each measured bit's flip applied to the measured distribution".into()), + "density_matrix" => Ok("exactly: its Kraus sum applied to the density matrix as one superoperator with each gate".into()), + "stabilizer" if self.name == "amplitude_damping" => Err(STABILIZER_NOT_DAMPING.into()), + "stabilizer" if self.name == "readout" => Ok("each measured bit flipped in the Pauli frames' samples".into()), + "stabilizer" => Ok("as Pauli frames against a noiseless stabilizer tableau, each frame drawing its Paulis".into()), + "trajectories" if self.name == "amplitude_damping" => Ok("as a jump drawn with the state's own probability, the state renormalised".into()), + "trajectories" if self.name == "readout" => Ok("each measured bit flipped in each trajectory's sample".into()), + "trajectories" => Ok("a Pauli drawn in each trajectory, shots that draw the same sharing a state".into()), + m => Err(format!("{m} does not model noise")), + } + } +} + +// --------------------------------------------------------------------------- +// Front ends +// --------------------------------------------------------------------------- + +/// A built-in format, read and written by `engine::formats`. +#[derive(Clone, Copy)] +struct Fe(Format); + +fn text(format: Format, input: &[u8]) -> Result<&str, Diagnostic> { + std::str::from_utf8(input).map_err(|_| Diagnostic::new(diag::FORMAT_UNREAD, None, format!("{} is text, and this is not UTF-8", format.name()))) +} + +impl FrontEnd for Fe { + fn name(&self) -> &str { + self.0.name() + } + fn version(&self) -> &str { + ENGINE_VERSION + } + fn title(&self) -> &str { + match self.0 { + Format::Qir => "QIR", + Format::Jeff => "jeff", + _ => "OpenQASM 3", + } + } + fn descriptor(&self) -> Value { + match self.0 { + Format::Qir => json!({ "reads": "QIR as LLVM text, under its base and adaptive profiles", "writes": "QIR 2.0 under the base or the adaptive profile" }), + Format::Jeff => json!({ "reads": "jeff, its Cap'n Proto message", "writes": "jeff, its Cap'n Proto message" }), + _ => json!({ "reads": "OpenQASM 3, and the OpenQASM 2 forms in common use", "writes": "OpenQASM 3" }), + } + } + fn binary(&self) -> bool { + self.0.is_binary() + } + fn extensions(&self) -> Vec { + match self.0 { + Format::Qir => vec!["ll".into(), "qir".into()], + Format::Jeff => vec!["jeff".into()], + _ => vec!["qasm".into(), "inc".into()], + } + } + fn profiles(&self) -> Vec { + match self.0 { + Format::Qir => vec!["base".into(), "adaptive".into()], + _ => Vec::new(), + } + } + fn read(&self, input: &[u8]) -> Result { + match self.0 { + Format::Qir => { + let r = formats::qir::read(text(self.0, input)?)?; + Ok(Imported { qasm: r.qasm, notes: r.notes, summary: r.summary }) + } + Format::Jeff => { + let r = formats::jeff::read(input)?; + Ok(Imported { qasm: r.qasm, notes: r.notes, summary: r.summary }) + } + _ => { + let t = text(self.0, input)?; + crate::qasm::read(t)?; + Ok(Imported { qasm: t.to_string(), notes: Vec::new(), summary: json!({ "format": "qasm" }) }) + } + } + } + fn write(&self, program: &Program, profile: Option<&str>) -> Result { + let profile = match profile { + None => None, + Some(p) if self.0 == Format::Qir => Some(formats::Profile::parse(p).ok_or_else(|| Diagnostic::new(diag::WRITE_CANNOT, None, format!("profile: {p} is not a QIR profile; the profiles are base and adaptive")))?), + Some(_) => return Err(Diagnostic::new(diag::WRITE_CANNOT, None, format!("a profile is QIR's; {} takes none", self.0.name()))), + }; + formats::write(self.0, program, profile) + } +} diff --git a/wai/wai-quantum/src/engine/registry/mod.rs b/wai/wai-quantum/src/engine/registry/mod.rs new file mode 100644 index 000000000..6ff1ceb05 --- /dev/null +++ b/wai/wai-quantum/src/engine/registry/mod.rs @@ -0,0 +1,1259 @@ +//! Extension points: the engine's simulation methods, compiler passes, noise +//! channels and program formats, each a component behind a trait, held in +//! one registry that the engine plans, runs, compiles and converts through. +//! +//! - [`Method`]: a simulation method. Its name, the accuracy classes its +//! results carry, what programs and answers it takes (refusing what it does +//! not with a code), the memory and work a job will take on it (its +//! [`Plan`](super::catalog::Plan)), and the run of a plan to a result. +//! - [`Pass`]: a compiler pass. The gates it takes and gives, and how its +//! result is checked: by the equivalence ladder or by its distance from the +//! program (a fidelity bound), never by a hash. +//! - [`Channel`]: a noise channel. Its parameters, its Kraus operators (and +//! its Pauli form, where it is one), checked completely positive and trace +//! preserving at the values a job gives, and how each noisy method takes +//! it. +//! - [`FrontEnd`]: a program format, read to the engine's one program +//! (OpenQASM 3) and written from it. +//! +//! [`Registry::with_builtins`] holds every built-in component, each +//! registered through the same trait a host's own component implements: +//! the twelve methods and the pulse simulator, the compile's passes, the +//! five noise channels and the three formats. [`Registry::register`] adds a +//! host's own. Registration is explicit and stays in the process: nothing is +//! loaded from a file, a path or a request, and the job service runs the +//! built-ins alone. +//! +//! **Identity.** Every component has one: its kind, its name, its version +//! and the BLAKE3 digest of its descriptor (what it declares of itself, in +//! JSON with its keys sorted), and whether it is built in. A built-in's +//! version is the engine's, so a result made by built-ins alone names them +//! already, as it always has: its method, its compile's passes, its noise's +//! channels and the engine's version, sealed by its receipt; the digests of +//! that version are the ones [`Registry::components`] lists. A result that a +//! registered component took part in carries `components`, the identity of +//! every component that made it, each registered one marked `registered` and +//! `certified: false`, and the receipt seals it with the result. A +//! registered component is never presented as built in, and a name a +//! built-in holds is never registered. +//! +//! **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. A registered component runs where the built-ins run +//! (a job's plan and run, a compile, a conversion), and the engine checks +//! what it gives: a method's result must carry an accuracy class it +//! declared and leave the members the engine writes alone; a pass's output +//! is held to the program by the check it declares, on the whole circuit; +//! a channel's operators are checked at every job's values; a front end's +//! program must read. + +mod builtin; +#[cfg(test)] +mod tests; + +pub(crate) use builtin::STABILIZER_NOT_DAMPING; + +use super::catalog::{Plan, Request}; +use super::refusal::{self, tag}; +use crate::gates::C; +use crate::qasm::diag::Diagnostic; +use crate::qasm::{Op, Program}; +use std::cell::OnceCell; + +/// The JSON the traits take and give, so that a component needs no other +/// dependency to write one. +pub use serde_json::{self, Value, json}; + +/// The format of [`Registry::components`]'s listing. +pub const COMPONENTS: &str = "wai.quantum.components/1"; + +/// The accuracy classes a method may declare and its results carry. +pub const ACCURACY_CLASSES: [&str; 6] = ["bit-exact", "exact", "single", "bounded", "estimate", "heuristic"]; + +/// The members of a job's result the engine writes; a registered method's +/// run leaves them as they are. +pub const ENGINE_MEMBERS: [&str; 13] = ["method", "qubits", "gates", "compiled", "repaired", "engine", "schema", "components", "receipt", "verdict", "provenance", "energy", "elapsed_ms"]; + +/// What a component is. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Kind { + Method, + Pass, + Channel, + FrontEnd, +} + +impl Kind { + /// Every kind, in the order listings give them. + pub const ALL: [Kind; 4] = [Kind::Method, Kind::Pass, Kind::Channel, Kind::FrontEnd]; + + pub fn as_str(self) -> &'static str { + match self { + Kind::Method => "method", + Kind::Pass => "pass", + Kind::Channel => "channel", + Kind::FrontEnd => "front_end", + } + } + + /// The kind in words, as a message names it. + pub fn label(self) -> &'static str { + match self { + Kind::FrontEnd => "front end", + k => k.as_str(), + } + } + + /// The listing's member for the kind. + pub fn plural(self) -> &'static str { + match self { + Kind::Method => "methods", + Kind::Pass => "passes", + Kind::Channel => "channels", + Kind::FrontEnd => "front_ends", + } + } +} + +/// Where a component comes from. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Origin { + /// The engine's own, held by its tests and its conformance corpus. + BuiltIn, + /// A host's own, added with [`Registry::register`]: not part of the + /// engine and not certified by it. + Registered, +} + +impl Origin { + pub fn as_str(self) -> &'static str { + match self { + Origin::BuiltIn => "built-in", + Origin::Registered => "registered", + } + } +} + +/// A component's identity: what a result names it by. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Identity { + pub kind: Kind, + pub name: String, + pub version: String, + /// The BLAKE3 of the descriptor's canonical JSON ([`digest_of`]), in hex. + pub digest: String, + pub origin: Origin, +} + +impl Identity { + pub fn is_builtin(&self) -> bool { + self.origin == Origin::BuiltIn + } + + /// `{"kind", "name", "version", "digest", "origin"}`, and for a registered + /// component `"certified": false`. + pub fn to_json(&self) -> Value { + let mut v = json!({ "kind": self.kind.as_str(), "name": self.name, "version": self.version, "digest": self.digest, "origin": self.origin.as_str() }); + if self.origin == Origin::Registered { + v["certified"] = json!(false); + } + v + } +} + +/// What a job asks for, as `auto` reads it: the order it tries methods in +/// depends on it ([`Method::auto_rank`]). +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum Question { + /// A job with a noise model. + Noise, + /// Measurement counts (`shots`). + Counts, + /// Expectation values alone. + Expectations, + /// Amplitudes. + Amplitudes, +} + +impl Question { + pub const ALL: [Question; 4] = [Question::Noise, Question::Counts, Question::Expectations, Question::Amplitudes]; + + /// The question `auto` answers for `r`. + pub fn of(r: &Request) -> Question { + if r.noise.is_some() { + Question::Noise + } else if r.shots > 0 { + Question::Counts + } else if !r.observables.is_empty() && r.amplitudes.is_empty() { + Question::Expectations + } else { + Question::Amplitudes + } + } + + pub fn as_str(self) -> &'static str { + match self { + Question::Noise => "noise", + Question::Counts => "counts", + Question::Expectations => "expectations", + Question::Amplitudes => "amplitudes", + } + } +} + +// --------------------------------------------------------------------------- +// The four traits +// --------------------------------------------------------------------------- + +/// A simulation method. +/// +/// The registry calls [`plan`](Method::plan) to admit a job (refusing what +/// the method does not take, with a code: a refusal with none is given +/// `J011` when the method was named) and to estimate its memory and work, +/// and [`run`](Method::run) to carry a plan out. Before `plan`, a job with +/// noise is refused for a method that models none; after it, every noise +/// channel the job names must be one the method takes ([`Method::takes`]). +pub trait Method: Send + Sync { + /// Its name, as a job's `method` names it. + fn name(&self) -> &str; + /// Its version. + fn version(&self) -> &str; + /// The accuracy classes its results carry (of [`ACCURACY_CLASSES`]). + fn classes(&self) -> Vec; + /// What it declares of itself: the circuits and the answers it takes, its + /// options. Hashed into its identity, so it holds nothing that depends on + /// the host. + fn descriptor(&self) -> Value; + /// Its entry in the methods list, with what it holds on this host in + /// `job_bytes` of memory. + fn describe(&self, job_bytes: u64) -> Value { + let _ = job_bytes; + let mut v = json!({ "method": self.name() }); + if let (Some(o), Some(d)) = (v.as_object_mut(), self.descriptor().as_object()) { + o.extend(d.clone()); + } + v + } + /// Whether it models noise. A job with a noise model is refused for a + /// method that does not (`J061`). + fn models_noise(&self) -> bool { + false + } + /// Whether it reads its job's program itself (a pulse program), rather + /// than as the engine's reader reads it. Only a built-in does. + fn reads_program(&self) -> bool { + false + } + /// Where `auto` tries it for `question`, lower first: the built-ins hold + /// 10, 20, 30 and so on, in the order they are tried. `None`, the + /// default, is never tried by `auto`; the job names it. + fn auto_rank(&self, question: Question) -> Option { + let _ = question; + None + } + /// How it takes the noise channel `channel`, or why it does not (`J061`). + /// By default, as the channel says it is taken by a method of this name + /// ([`Channel::taken_by`]). + fn takes(&self, channel: &dyn Channel) -> Result { + channel.taken_by(self.name()) + } + /// The job admitted, with the memory and work it will take, or why not. + fn plan(&self, job: &Job<'_>) -> Result; + /// The plan carried out: the method writes its answers into `out`, which + /// holds the method, the qubits and the gates (and a compile's or a + /// repair's account) already. It must write `accuracy`, with its + /// `class` one it declared. + fn run(&self, job: &Job<'_>, plan: &Plan, out: &mut Value) -> Result<(), String>; +} + +/// The gates a pass takes or gives. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GateSet { + /// Any gate the reader reads. + Any, + /// The basis the `lower` pass writes: `h x y z s sdg t tdg cx cz swap` + /// and `rz` at any angle. + Basis, + /// Clifford+T: the basis with no rotation off the multiples of `π/4`. + CliffordT, +} + +impl GateSet { + pub fn as_str(self) -> &'static str { + match self { + GateSet::Any => "any", + GateSet::Basis => "basis", + GateSet::CliffordT => "clifford_t", + } + } + + /// Whether every gate of `ops` is in the set. + pub fn holds(self, ops: &[Op]) -> bool { + use super::synth::lower; + match self { + GateSet::Any => true, + GateSet::Basis => ops.iter().all(|o| lower::in_basis(o.kind)), + GateSet::CliffordT => ops.iter().all(|o| lower::in_basis(o.kind) && (o.kind != crate::qasm::Kind::Rz || o.params.first().is_some_and(|&p| lower::eighths(p).is_some()))), + } + } +} + +/// How a pass's result is checked. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum Check { + /// By the equivalence ladder ([`super::equiv`]): a tableau, every basis + /// state, or a chain of checked rewrites. Equality on random inputs alone + /// is evidence, not a check a compile certifies, and is not taken. + Ladder, + /// By the output's distance from the program in the operator norm, up + /// to a global phase, on their dense unitaries: at most `max_distance`, + /// and within the compile's ε budget, the distance counted against it. + Fidelity { max_distance: f64 }, +} + +impl Check { + pub fn to_json(self) -> Value { + match self { + Check::Ladder => json!({ "by": "ladder" }), + Check::Fidelity { max_distance } => json!({ "by": "fidelity", "max_distance": max_distance }), + } + } +} + +/// What a pass is given. +#[non_exhaustive] +pub struct PassInput<'a> { + /// The program's qubits. + pub qubits: u32, + /// The circuit as the passes before left it. + pub ops: &'a [Op], + /// The compile's options. + pub options: &'a super::compile::Options, + /// The distance the passes before have added, of the budget + /// `options.epsilon`. + pub spent: f64, +} + +/// What a pass gives: its gates, and for a built-in, the record of its own +/// check. A registered pass's check is the engine's ([`Pass::check`]), made +/// on what it gives, whatever it says of itself here. +#[non_exhaustive] +pub struct PassOutput { + pub ops: Vec, + /// Its check's record: `{"how", "verdict", …}`, the verdict `proof`, + /// `exact`, `approximate`, `rejected` or `skipped`. + pub check: Value, + /// The distance it adds. + pub distance: f64, + /// For the approximation: the rotations it wrote and each one's share. + pub epsilon: Option<(usize, f64)>, + /// For the approximation: each rotation's word, as the certificate lists + /// them. + pub rotations: Option>, +} + +impl PassOutput { + /// A pass's gates. + pub fn new(ops: Vec) -> PassOutput { + PassOutput { ops, check: Value::Null, distance: 0.0, epsilon: None, rotations: None } + } + + /// The gates with what the pass says of them (`{"how", …}`), which the + /// record keeps beside the engine's check. + pub fn with_note(mut self, note: Value) -> PassOutput { + self.check = note; + self + } +} + +/// A compiler pass. +pub trait Pass: Send + Sync { + /// Its name, as a compile's `passes` names it. + fn name(&self) -> &str; + fn version(&self) -> &str; + /// What it declares of itself (what it does). Hashed into its identity. + fn descriptor(&self) -> Value; + /// The gates it takes: a circuit with others is passed over. + fn takes(&self) -> GateSet { + GateSet::Any + } + /// The gates it gives. + fn gives(&self) -> GateSet { + GateSet::Any + } + /// How its result is checked. + fn check(&self) -> Check; + /// The pass on `input`. + fn run(&self, input: &PassInput<'_>) -> Result; +} + +/// Where a channel acts. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Placement { + /// After each gate: a one-qubit channel on each qubit the gate touched, a + /// two-qubit one on the pair of a two-qubit gate. + AfterGate, + /// On each measured bit, as it is read (the built-in `readout`). + OnReadout, +} + +/// One of a channel's parameters. +#[derive(Clone, Debug, PartialEq)] +pub struct Param { + pub name: String, + pub min: f64, + pub max: f64, + /// What it is, in a few words ("a probability"). + pub meaning: String, +} + +impl Param { + pub fn new(name: impl Into, min: f64, max: f64, meaning: impl Into) -> Param { + Param { name: name.into(), min, max, meaning: meaning.into() } + } + + fn to_json(&self) -> Value { + json!({ "name": self.name, "min": self.min, "max": self.max, "meaning": self.meaning }) + } +} + +/// A noise channel. +/// +/// A job names it under `noise` with its value (`{"name": 0.01}`, for a +/// channel of one parameter) or its values (`{"name": {"p": 0.01, …}}`). +/// At each job's values its Kraus operators are checked completely positive +/// and trace preserving, and its Pauli form, where it gives one, checked a +/// distribution that is the channel its operators make (`J062`). +pub trait Channel: Send + Sync { + /// Its name, as a job's `noise` names it. + fn name(&self) -> &str; + fn version(&self) -> &str; + /// Its name in plain words ("amplitude damping"). + fn title(&self) -> &str; + /// What it declares of itself. Hashed into its identity. + fn descriptor(&self) -> Value; + /// Its parameters, in order: the values [`kraus`](Channel::kraus) and + /// [`pauli`](Channel::pauli) are given. + fn params(&self) -> Vec; + /// The qubits it acts on: 1 or 2. + fn arity(&self) -> u32; + /// Where it acts. A registered channel acts after gates. + fn placement(&self) -> Placement { + Placement::AfterGate + } + /// Its Kraus operators at `values`, each a `2ᵏ × 2ᵏ` row-major matrix on + /// its `k` qubits (rows outputs, columns inputs, the first qubit the most + /// significant bit). + fn kraus(&self, values: &[f64]) -> Vec>; + /// Its Pauli form, where it is one: each Pauli string on its qubits + /// (`"X"`, `"IZ"`) with its probability. + fn pauli(&self, values: &[f64]) -> Option> { + let _ = values; + None + } + /// How the method named `method` takes it, or why it does not. By + /// default `density_matrix` takes it, its Kraus operators applied + /// exactly, and no other method does. + fn taken_by(&self, method: &str) -> Result { + match method { + "density_matrix" => Ok("exactly: its Kraus operators applied to the density matrix after each gate, on the qubits the gate touched".into()), + m => Err(format!("{m} takes the built-in channels alone; {} is a registered channel, taken by density_matrix through its Kraus operators", self.name())), + } + } +} + +/// A program format, read to the engine's one program and written from it. +pub trait FrontEnd: Send + Sync { + /// Its name, as a conversion's `from` and `to` and a job's + /// `source.format` name it. + fn name(&self) -> &str; + fn version(&self) -> &str; + /// Its name in plain words. + fn title(&self) -> &str; + /// What it declares of itself. Hashed into its identity. + fn descriptor(&self) -> Value; + /// Whether its programs are bytes (carried in JSON as base64). + fn binary(&self) -> bool { + false + } + /// The file extensions its programs have, without the dot. + fn extensions(&self) -> Vec { + Vec::new() + } + /// The profiles it writes under, if any (QIR's). + fn profiles(&self) -> Vec { + Vec::new() + } + /// A program read from `input`, as OpenQASM 3 the engine's reader reads + /// (the registry checks that it does). + fn read(&self, input: &[u8]) -> Result; + /// `program` written in this format, under `profile` if one is given. + fn write(&self, program: &Program, profile: Option<&str>) -> Result; +} + +/// A component, as [`Registry::register`] takes it. +pub enum Component { + Method(Box), + Pass(Box), + Channel(Box), + FrontEnd(Box), +} + +impl Component { + pub fn method(m: impl Method + 'static) -> Component { + Component::Method(Box::new(m)) + } + pub fn pass(p: impl Pass + 'static) -> Component { + Component::Pass(Box::new(p)) + } + pub fn channel(c: impl Channel + 'static) -> Component { + Component::Channel(Box::new(c)) + } + pub fn front_end(f: impl FrontEnd + 'static) -> Component { + Component::FrontEnd(Box::new(f)) + } + + pub fn kind(&self) -> Kind { + match self { + Component::Method(_) => Kind::Method, + Component::Pass(_) => Kind::Pass, + Component::Channel(_) => Kind::Channel, + Component::FrontEnd(_) => Kind::FrontEnd, + } + } +} + +// --------------------------------------------------------------------------- +// A job, as a method sees it +// --------------------------------------------------------------------------- + +/// A noise channel a job names, with its values. +pub struct ChannelUse<'a> { + pub channel: &'a dyn Channel, + pub identity: &'a Identity, + pub values: Vec, +} + +/// A job as a method plans and runs it: the request as given, as the +/// engine's reader read it, the memory one job may take, and the noise +/// channels it names. +pub struct Job<'a> { + registry: &'a Registry, + json: &'a Value, + job_bytes: u64, + request: Option<&'a Request>, + channels: Vec>, + dyadic: OnceCell>, + clifford: OnceCell, String>>, +} + +impl<'a> Job<'a> { + pub(crate) fn new(registry: &'a Registry, json: &'a Value, job_bytes: u64, request: Option<&'a Request>, uses: &[(usize, Vec)]) -> Job<'a> { + let channels = uses.iter().map(|(i, v)| ChannelUse { channel: &*registry.channels[*i].component, identity: ®istry.channels[*i].identity, values: v.clone() }).collect(); + Job { registry, json, job_bytes, request, channels, dyadic: OnceCell::new(), clifford: OnceCell::new() } + } + + /// The registry the job runs in. + pub fn registry(&self) -> &'a Registry { + self.registry + } + + /// The job as submitted. + pub fn json(&self) -> &'a Value { + self.json + } + + /// The memory one job may take, bytes. + pub fn job_bytes(&self) -> u64 { + self.job_bytes + } + + /// The job as the engine's reader read it. A method that reads its + /// program itself is given none. + pub fn request(&self) -> Result<&'a Request, String> { + self.request.ok_or_else(|| "internal: this method reads its own program, and the job was not read".to_string()) + } + + /// Every noise channel the job names with a value other than zero, in + /// the registry's order: the built-ins' values are also in the request's + /// `noise`, which the built-in methods read. + pub fn channels(&self) -> &[ChannelUse<'a>] { + &self.channels + } + + /// The registered channels among [`channels`](Job::channels). + pub fn registered_channels(&self) -> impl Iterator> { + self.channels.iter().filter(|c| !c.identity.is_builtin()) + } + + /// The program as the bit-exact simulator's circuit, or why not. + pub fn dyadic(&self) -> &Result { + self.dyadic.get_or_init(|| match self.request { + Some(r) => r.program.dyadic(), + None => Err("no program read".into()), + }) + } + + /// The program as Clifford gates, or why not. + pub fn clifford(&self) -> &Result, String> { + self.clifford.get_or_init(|| match self.request { + Some(r) => r.program.clifford(), + None => Err("no program read".into()), + }) + } +} + +// --------------------------------------------------------------------------- +// The registry +// --------------------------------------------------------------------------- + +struct Entry { + component: Box, + identity: Identity, +} + +/// The components the engine plans, runs, compiles and converts through. +pub struct Registry { + methods: Vec>, + passes: Vec>, + channels: Vec>, + front_ends: Vec>, +} + +/// The built-in registry, made once. +static BUILTINS: std::sync::OnceLock = std::sync::OnceLock::new(); + +fn plain(s: &str) -> bool { + !s.is_empty() && s.len() <= 64 && s.bytes().all(|b| b.is_ascii_alphanumeric() || matches!(b, b'_' | b'-' | b'.' | b'+')) && s.as_bytes()[0].is_ascii_alphanumeric() +} + +impl Registry { + /// Every built-in component, each registered through its trait. + pub fn with_builtins() -> Registry { + let mut r = Registry { methods: Vec::new(), passes: Vec::new(), channels: Vec::new(), front_ends: Vec::new() }; + for c in builtin::components() { + r.add(c, Origin::BuiltIn).expect("a built-in registers"); + } + r + } + + /// The built-in registry, shared: what every public function of the + /// engine that names no registry runs through. + pub fn builtins() -> &'static Registry { + BUILTINS.get_or_init(Registry::with_builtins) + } + + /// Adds a host's own component, after the built-ins and every component + /// registered before it, and gives its identity. Refused (`J063`): a name + /// its kind holds already, a name or version that is empty or not plain + /// text (letters, digits, `_ - . +`), a descriptor that is not an object, + /// a method with no accuracy class of [`ACCURACY_CLASSES`] or one that + /// reads its own program, a channel on other than one or two qubits, with + /// no parameters or acting other than after gates, a pass whose fidelity + /// check states no finite bound, or a front end named as a built-in + /// format's alias. + pub fn register(&mut self, component: Component) -> Result { + self.add(component, Origin::Registered) + } + + fn add(&mut self, component: Component, origin: Origin) -> Result { + let bad = |m: String| tag(refusal::REGISTER, m); + let kind = component.kind(); + let (name, version, descriptor) = match &component { + Component::Method(m) => (m.name().to_string(), m.version().to_string(), m.descriptor()), + Component::Pass(p) => (p.name().to_string(), p.version().to_string(), p.descriptor()), + Component::Channel(c) => (c.name().to_string(), c.version().to_string(), c.descriptor()), + Component::FrontEnd(f) => (f.name().to_string(), f.version().to_string(), f.descriptor()), + }; + if !plain(&name) { + return Err(bad(format!("a {} named {name:?}: a name is 1 to 64 letters, digits, `_ - . +`, starting with a letter or digit", kind.label()))); + } + if !plain(&version) { + return Err(bad(format!("the {} {name}: its version {version:?} is 1 to 64 letters, digits, `_ - . +`", kind.label()))); + } + if !descriptor.is_object() { + return Err(bad(format!("the {} {name}: its descriptor is a JSON object of what it declares", kind.label()))); + } + if self.identity(kind, &name).is_some() { + let whose = if self.identity(kind, &name).is_some_and(Identity::is_builtin) { "a built-in" } else { "a registered" }; + return Err(bad(format!("the {} {name}: {whose} {} holds that name already", kind.label(), kind.label()))); + } + match &component { + Component::Method(m) => { + let classes = m.classes(); + if name == "auto" { + return Err(bad("a method named auto: auto is the engine's choice of a method".into())); + } + if classes.is_empty() || classes.iter().any(|c| !ACCURACY_CLASSES.contains(&c.as_str())) { + return Err(bad(format!("the method {name}: its accuracy classes {classes:?} are one or more of {}", ACCURACY_CLASSES.join(", ")))); + } + if origin == Origin::Registered && m.reads_program() { + return Err(bad(format!("the method {name}: a registered method takes its job as the engine's reader reads it"))); + } + } + Component::Pass(p) => { + if let Check::Fidelity { max_distance } = p.check() + && !(max_distance.is_finite() && max_distance >= 0.0) + { + return Err(bad(format!("the pass {name}: its fidelity check's bound is a finite distance, at least 0, not {max_distance}"))); + } + } + Component::Channel(c) => { + let params = c.params(); + if !(1..=2).contains(&c.arity()) { + return Err(bad(format!("the channel {name}: it acts on 1 or 2 qubits, not {}", c.arity()))); + } + if params.is_empty() || params.iter().any(|p| !plain(&p.name) || !(p.min.is_finite() && p.max.is_finite() && p.min <= p.max)) { + return Err(bad(format!("the channel {name}: it has at least one parameter, each with a plain name and a finite range, its least value first"))); + } + if origin == Origin::Registered && c.placement() != Placement::AfterGate { + return Err(bad(format!("the channel {name}: a registered channel acts after gates"))); + } + } + Component::FrontEnd(_) => { + if origin == Origin::Registered && super::formats::Format::parse(&name).is_some() { + return Err(bad(format!("the front end {name}: the name is a built-in format's"))); + } + } + } + let declared = match &component { + Component::Method(m) => descriptor_of_method(&**m), + Component::Pass(p) => descriptor_of_pass(&**p), + Component::Channel(c) => descriptor_of_channel(&**c), + Component::FrontEnd(f) => descriptor_of_front_end(&**f), + }; + let identity = Identity { kind, digest: digest_of(kind, &name, &version, &declared), name, version, origin }; + match component { + Component::Method(m) => self.methods.push(Entry { component: m, identity: identity.clone() }), + Component::Pass(p) => self.passes.push(Entry { component: p, identity: identity.clone() }), + Component::Channel(c) => self.channels.push(Entry { component: c, identity: identity.clone() }), + Component::FrontEnd(f) => self.front_ends.push(Entry { component: f, identity: identity.clone() }), + } + Ok(identity) + } + + /// The method named `name`. + pub fn method(&self, name: &str) -> Option<&dyn Method> { + self.methods.iter().find(|e| e.identity.name == name).map(|e| &*e.component) + } + + /// The pass named `name`. + pub fn pass(&self, name: &str) -> Option<&dyn Pass> { + self.passes.iter().find(|e| e.identity.name == name).map(|e| &*e.component) + } + + /// The noise channel named `name`. + pub fn channel(&self, name: &str) -> Option<&dyn Channel> { + self.channels.iter().find(|e| e.identity.name == name).map(|e| &*e.component) + } + + /// The front end named `name`, a built-in format by any of its names + /// (`openqasm3`, `ll`). + pub fn front_end(&self, name: &str) -> Option<&dyn FrontEnd> { + let canonical = super::formats::Format::parse(name).map(|f| f.name()); + self.front_ends.iter().find(|e| Some(e.identity.name.as_str()) == canonical || e.identity.name == name).map(|e| &*e.component) + } + + /// The identity of the component of `kind` named `name` (a front end by + /// any of its names). + pub fn identity(&self, kind: Kind, name: &str) -> Option<&Identity> { + match kind { + Kind::Method => self.methods.iter().find(|e| e.identity.name == name).map(|e| &e.identity), + Kind::Pass => self.passes.iter().find(|e| e.identity.name == name).map(|e| &e.identity), + Kind::Channel => self.channels.iter().find(|e| e.identity.name == name).map(|e| &e.identity), + Kind::FrontEnd => { + let canonical = super::formats::Format::parse(name).map(|f| f.name()); + self.front_ends.iter().find(|e| Some(e.identity.name.as_str()) == canonical || e.identity.name == name).map(|e| &e.identity) + } + } + } + + /// Every component's identity: the methods, the passes, the channels and + /// the front ends, each kind in the order its components were registered. + pub fn identities(&self) -> Vec<&Identity> { + let mut v: Vec<&Identity> = self.methods.iter().map(|e| &e.identity).collect(); + v.extend(self.passes.iter().map(|e| &e.identity)); + v.extend(self.channels.iter().map(|e| &e.identity)); + v.extend(self.front_ends.iter().map(|e| &e.identity)); + v + } + + /// The methods, in order. + pub fn methods(&self) -> impl Iterator { + self.methods.iter().map(|e| &*e.component) + } + + /// The passes, in order. + pub fn passes(&self) -> impl Iterator { + self.passes.iter().map(|e| &*e.component) + } + + /// The noise channels, in order. + pub fn channels(&self) -> impl Iterator { + self.channels.iter().map(|e| &*e.component) + } + + /// The front ends, in order. + pub fn front_ends(&self) -> impl Iterator { + self.front_ends.iter().map(|e| &*e.component) + } + + pub(crate) fn channel_at(&self, i: usize) -> (&dyn Channel, &Identity) { + (&*self.channels[i].component, &self.channels[i].identity) + } + + /// The methods `auto` tries for `question`, in order: by rank, the order + /// of registration among equal ranks. + pub fn auto_order(&self, question: Question) -> Vec<&dyn Method> { + let mut ranked: Vec<(u32, usize)> = self.methods.iter().enumerate().filter_map(|(i, e)| e.component.auto_rank(question).map(|r| (r, i))).collect(); + ranked.sort(); + ranked.into_iter().map(|(_, i)| &*self.methods[i].component).collect() + } + + /// Every component with its identity and descriptor, as the command + /// line's `components`, both MCP servers' `list_components` and the + /// service's description give them: + /// `{"format", "engine", "methods", "passes", "channels", "front_ends"}`, + /// each a list in the registry's order of + /// `{"kind", "name", "version", "digest", "origin", "descriptor"}` + /// (`"certified": false` beside a registered one). + pub fn components(&self) -> Value { + let entry = |id: &Identity, descriptor: Value| { + let mut v = id.to_json(); + v["descriptor"] = descriptor; + v + }; + let mut out = json!({ "format": COMPONENTS, "engine": { "name": super::schema::ENGINE, "version": super::schema::ENGINE_VERSION } }); + out["methods"] = self.methods.iter().map(|e| entry(&e.identity, descriptor_of_method(&*e.component))).collect(); + out["passes"] = self.passes.iter().map(|e| entry(&e.identity, descriptor_of_pass(&*e.component))).collect(); + out["channels"] = self.channels.iter().map(|e| entry(&e.identity, descriptor_of_channel(&*e.component))).collect(); + out["front_ends"] = self.front_ends.iter().map(|e| entry(&e.identity, descriptor_of_front_end(&*e.component))).collect(); + let registered = self.identities().iter().filter(|i| !i.is_builtin()).count(); + out["registered"] = json!(registered); + out + } + + /// The methods list: each method's entry on this host, in `job_bytes` of + /// memory ([`Method::describe`]). + pub fn describe_methods(&self, job_bytes: u64) -> Value { + Value::Array(self.methods().map(|m| m.describe(job_bytes)).collect()) + } + + /// The plan for a job's JSON, `job_bytes` the memory one job may take + /// ([`super::catalog::plan`], through this registry). + pub fn plan(&self, job: &Value, job_bytes: u64) -> Result { + super::catalog::plan_in(self, job, job_bytes) + } + + /// A job's JSON read and checked ([`super::catalog::read`], with this + /// registry's channels and passes). + pub fn read(&self, job: &Value) -> Result { + super::catalog::read_in(self, job).map(|(r, _)| r) + } + + /// `plan` carried out for the job `request` ([`super::run::run`], + /// through this registry). + pub fn run(&self, request: &Value, plan: &Plan) -> Result { + super::run::run_in(self, request, plan) + } + + /// [`run`](Registry::run) with the result's `provenance` + /// ([`super::run::run_with_provenance`]). + pub fn run_with_provenance(&self, request: &Value, plan: &Plan, host: &str) -> Result { + super::run::run_with_provenance_in(self, request, plan, host) + } + + /// `request` planned within `job_bytes` and run, as a host runs a job + /// (refusing a whole number a double cannot hold, as the job service + /// does). + pub fn plan_and_run(&self, request: &Value, job_bytes: u64) -> Result<(Plan, Value), String> { + if let Some(at) = inexact_integer(request) { + return Err(tag(refusal::LIMIT, format!("`{at}`: a whole number here must be within ±9007199254740991 (2^53 − 1), so that every JSON reader reads it alike and the receipt binds it"))); + } + let plan = self.plan(request, job_bytes)?; + let result = self.run(request, &plan)?; + Ok((plan, result)) + } + + /// The job body for `request`, in the service's form, its result sealed + /// with `key` ([`super::job::run`], through this registry): a result a + /// registered component took part in carries `components`, and the + /// receipt seals it. + #[cfg(feature = "quantum_receipt")] + pub fn job(&self, request: &Value, job_bytes: u64, key: &ed25519_dalek::SigningKey, energy: Value) -> Result { + let (plan, result) = self.plan_and_run(request, job_bytes)?; + Ok(super::job::seal(request, &plan, result, key, energy)) + } + + /// `program` compiled under `options` ([`super::compile::compile`]), with + /// `passes` (each a pass of this registry) run after the level's own and + /// before twirling and decoupling. + pub fn compile(&self, program: &Program, options: &super::compile::Options, passes: &[&str]) -> Result { + super::compile::compile_in(self, program, options, passes) + } + + /// A compile in the service's form ([`super::compile::call`]), its + /// request's `passes` run through this registry. + pub fn compile_call(&self, request: &Value) -> Result { + super::compile::call_in(self, request) + } + + /// A conversion as JSON ([`super::formats::convert_json`]), its formats + /// this registry's front ends. + pub fn convert_json(&self, request: &Value) -> Result { + super::formats::convert_json_in(self, request) + } + + /// A job whose program is in another format, read to its `qasm` + /// ([`super::formats::expand`]) by this registry's front ends. + pub fn expand(&self, request: &Value) -> Result, String> { + super::formats::expand_in(self, request) + } + + /// A program read from `input` in the format named `format`, checked to + /// read as OpenQASM 3. + pub fn read_program(&self, format: &str, input: &[u8]) -> Result { + let fe = self.front_end(format).ok_or_else(|| Diagnostic::new(crate::qasm::diag::FORMAT_UNREAD, None, format!("{format} is not a format here")))?; + super::formats::read_fe(fe, input) + } + + /// The identities of the components that made `result`, a result of a + /// job `request`: its `components` when it carries them (a registered + /// component took part), and otherwise the built-ins it names: its + /// method, the passes of its compile, the channels of its request's + /// noise and its program's format. + pub fn components_of(&self, request: &Value, result: &Value) -> Vec { + if let Some(a) = result.get("components").and_then(Value::as_array) { + return a.clone(); + } + self.used(request, result).into_iter().map(Identity::to_json).collect() + } + + /// The components a job's run used, in order: its format, the passes its + /// compile ran, the channels its noise names with a value, its method. + pub(crate) fn used(&self, request: &Value, result: &Value) -> Vec<&Identity> { + let mut out: Vec<&Identity> = Vec::new(); + let format = request.pointer("/source/format").and_then(Value::as_str).unwrap_or("qasm"); + if let Some(i) = self.identity(Kind::FrontEnd, format) { + out.push(i); + } + let mut passes: Vec<&str> = Vec::new(); + if let Some(c) = request.get("compile").filter(|c| !c.is_null()) { + if let Ok(o) = super::compile::Options::read(c) { + passes.extend(super::compile::pipeline(&o)); + } + for p in c.get("passes").and_then(Value::as_array).into_iter().flatten().filter_map(Value::as_str) { + passes.push(p); + } + } + for p in passes { + if let Some(i) = self.identity(Kind::Pass, p) + && !out.contains(&i) + { + out.push(i); + } + } + if let Ok(uses) = super::catalog::noise_in(self, request) { + for (i, _) in uses.1 { + out.push(&self.channels[i].identity); + } + } + if let Some(m) = result.get("method").and_then(Value::as_str) + && let Some(i) = self.identity(Kind::Method, m) + { + out.push(i); + } + out + } +} + +/// The path of the first whole number in `v` a double cannot hold, as the +/// receipt's check names it. +#[cfg(feature = "quantum_receipt")] +fn inexact_integer(v: &Value) -> Option { + super::receipt::inexact_number(v) +} + +/// The path of the first whole number in `v` a double cannot hold. +#[cfg(not(feature = "quantum_receipt"))] +fn inexact_integer(v: &Value) -> Option { + fn walk(v: &Value, path: &mut String) -> bool { + match v { + Value::Number(n) => n.as_u64().is_some_and(|x| x > (1u64 << 53) - 1) || n.as_i64().is_some_and(|x| x < -((1i64 << 53) - 1)), + Value::Array(a) => { + for (i, x) in a.iter().enumerate() { + let len = path.len(); + path.push_str(&format!("[{i}]")); + if walk(x, path) { + return true; + } + path.truncate(len); + } + false + } + Value::Object(o) => { + for (k, x) in o { + let len = path.len(); + if !path.is_empty() { + path.push('.'); + } + path.push_str(k); + if walk(x, path) { + return true; + } + path.truncate(len); + } + false + } + _ => false, + } + } + let mut path = String::new(); + walk(v, &mut path).then_some(path) +} + +fn descriptor_of_method(m: &dyn Method) -> Value { + let auto: serde_json::Map = Question::ALL.iter().filter_map(|q| m.auto_rank(*q).map(|r| (q.as_str().to_string(), json!(r)))).collect(); + json!({ "declared": m.descriptor(), "classes": m.classes(), "models_noise": m.models_noise(), "reads_program": m.reads_program(), "auto": auto }) +} + +fn descriptor_of_pass(p: &dyn Pass) -> Value { + json!({ "declared": p.descriptor(), "takes": p.takes().as_str(), "gives": p.gives().as_str(), "check": p.check().to_json() }) +} + +fn descriptor_of_channel(c: &dyn Channel) -> Value { + let placement = match c.placement() { + Placement::AfterGate => "after_gate", + Placement::OnReadout => "on_readout", + }; + json!({ "declared": c.descriptor(), "title": c.title(), "arity": c.arity(), "placement": placement, "params": c.params().iter().map(Param::to_json).collect::>() }) +} + +fn descriptor_of_front_end(f: &dyn FrontEnd) -> Value { + json!({ "declared": f.descriptor(), "title": f.title(), "binary": f.binary(), "extensions": f.extensions(), "profiles": f.profiles() }) +} + +/// The digest of a component's identity: BLAKE3 over the canonical JSON of +/// `{"kind", "name", "version", "descriptor"}` (keys sorted by their UTF-16 +/// code units, no whitespace, strings escaped as RFC 8785 escapes them, +/// whole numbers in decimal and others in their shortest round-trip form), +/// in hex. +pub fn digest_of(kind: Kind, name: &str, version: &str, descriptor: &Value) -> String { + let mut text = String::new(); + canonical(&json!({ "kind": kind.as_str(), "name": name, "version": version, "descriptor": descriptor }), &mut text); + blake3::hash(text.as_bytes()).to_hex().to_string() +} + +fn canonical(v: &Value, o: &mut String) { + match v { + Value::Null => o.push_str("null"), + Value::Bool(b) => o.push_str(if *b { "true" } else { "false" }), + Value::Number(n) => match (n.as_u64(), n.as_i64(), n.as_f64()) { + (Some(u), _, _) => o.push_str(&u.to_string()), + (_, Some(i), _) => o.push_str(&i.to_string()), + (_, _, Some(f)) if f.fract() == 0.0 && f.abs() < 9_007_199_254_740_992.0 => o.push_str(&(f as i64).to_string()), + _ => o.push_str(&n.to_string()), + }, + Value::String(s) => { + o.push('"'); + for c in s.chars() { + match c { + '"' => o.push_str("\\\""), + '\\' => o.push_str("\\\\"), + '\u{8}' => o.push_str("\\b"), + '\t' => o.push_str("\\t"), + '\n' => o.push_str("\\n"), + '\u{c}' => o.push_str("\\f"), + '\r' => o.push_str("\\r"), + c if (c as u32) < 0x20 => o.push_str(&format!("\\u{:04x}", c as u32)), + c => o.push(c), + } + } + o.push('"'); + } + Value::Array(a) => { + o.push('['); + for (i, x) in a.iter().enumerate() { + if i > 0 { + o.push(','); + } + canonical(x, o); + } + o.push(']'); + } + Value::Object(m) => { + let mut keys: Vec<&String> = m.keys().collect(); + keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16())); + o.push('{'); + for (i, k) in keys.into_iter().enumerate() { + if i > 0 { + o.push(','); + } + canonical(&Value::String(k.clone()), o); + o.push(':'); + canonical(&m[k], o); + } + o.push('}'); + } + } +} + +// --------------------------------------------------------------------------- +// Channels: values and the validity check +// --------------------------------------------------------------------------- + +/// A channel's values from a job's `noise` entry `v` for the channel named +/// `name`: a number for a channel of one parameter, or an object of its +/// parameters by name; `null` is no noise. Refused (`J003`): another form, a +/// parameter it does not have or lacks, or a value outside its range. +pub(crate) fn channel_values(c: &dyn Channel, name: &str, v: &Value) -> Result, String> { + let params = c.params(); + let range = |p: &Param, x: f64, at: &str| -> Result { + if x >= p.min && x <= p.max { Ok(x) } else { Err(format!("noise {at} is {}, {} to {}", p.meaning, p.min, p.max)) } + }; + match v { + Value::Null => Ok(vec![0.0; params.len()]), + Value::Number(n) if params.len() == 1 => range(¶ms[0], n.as_f64().unwrap_or(f64::NAN), name).map(|x| vec![x]), + Value::Object(o) => { + if let Some(k) = o.keys().find(|k| !params.iter().any(|p| &p.name == *k)) { + return Err(format!("noise {name}: no parameter {k}; its parameters are {}", params.iter().map(|p| p.name.as_str()).collect::>().join(", "))); + } + params + .iter() + .map(|p| { + let x = o.get(&p.name).and_then(Value::as_f64).ok_or_else(|| format!("noise {name}: give {} ({}, {} to {})", p.name, p.meaning, p.min, p.max))?; + range(p, x, &format!("{name}.{}", p.name)) + }) + .collect() + } + _ if params.len() == 1 => Err(format!("noise {name} is a number: {}, {} to {}", params[0].meaning, params[0].min, params[0].max)), + _ => Err(format!("noise {name} is an object of its parameters: {}", params.iter().map(|p| p.name.as_str()).collect::>().join(", "))), + } +} + +/// The tolerance of the validity check, on each entry of the channel's +/// superoperator and of its sum of `K†K`. +pub const CHANNEL_TOLERANCE: f64 = 1e-9; + +fn cmul(a: C, b: C) -> C { + C { re: a.re * b.re - a.im * b.im, im: a.re * b.im + a.im * b.re } +} + +fn conj(a: C) -> C { + C { re: a.re, im: -a.im } +} + +/// A Pauli string's matrix on its qubits, the first the most significant. +fn pauli_matrix(s: &str) -> Option> { + let one = |c: char| -> Option<[C; 4]> { + let (z, o, i) = (C { re: 0.0, im: 0.0 }, C { re: 1.0, im: 0.0 }, C { re: 0.0, im: 1.0 }); + Some(match c { + 'I' => [o, z, z, o], + 'X' => [z, o, o, z], + 'Y' => [z, C { re: 0.0, im: -1.0 }, i, z], + 'Z' => [o, z, z, C { re: -1.0, im: 0.0 }], + _ => return None, + }) + }; + let mut m = vec![C { re: 1.0, im: 0.0 }]; + let mut d = 1usize; + for c in s.chars() { + let p = one(c)?; + let mut next = vec![C { re: 0.0, im: 0.0 }; 4 * d * d]; + let nd = 2 * d; + for r in 0..d { + for col in 0..d { + for pr in 0..2 { + for pc in 0..2 { + next[(r * 2 + pr) * nd + col * 2 + pc] = cmul(m[r * d + col], p[pr * 2 + pc]); + } + } + } + } + m = next; + d = nd; + } + Some(m) +} + +/// `Σ w K ⊗ K*` of `terms`, a `d² × d²` row-major matrix. +fn superop(d: usize, terms: &[(f64, Vec)]) -> Vec { + let dd = d * d; + let mut s = vec![C { re: 0.0, im: 0.0 }; dd * dd]; + for (w, k) in terms { + for a in 0..d { + for b in 0..d { + for c in 0..d { + for e in 0..d { + let x = cmul(k[a * d + c], conj(k[b * d + e])); + let at = (a * d + b) * dd + c * d + e; + s[at].re += w * x.re; + s[at].im += w * x.im; + } + } + } + } + } + s +} + +/// `c` checked at `values`: its Kraus operators completely positive (as +/// Kraus operators are) and trace preserving, their sum of `K†K` within +/// [`CHANNEL_TOLERANCE`] of the identity; and its Pauli form, where it gives +/// one, a distribution (no probability below zero, their sum 1) on Pauli +/// strings of its arity whose channel is its operators' to the same +/// tolerance. `{"cptp": deviation, "pauli": bool}`, or the refusal (`J062`). +pub fn validate(c: &dyn Channel, values: &[f64]) -> Result { + let name = c.name(); + let bad = |m: String| tag(refusal::CHANNEL_INVALID, format!("noise {name}: {m}")); + let d = 1usize << c.arity(); + let ks = c.kraus(values); + if ks.is_empty() || ks.iter().any(|k| k.len() != d * d || k.iter().any(|x| !x.re.is_finite() || !x.im.is_finite())) { + return Err(bad(format!("its Kraus operators are one or more {d}×{d} matrices of finite entries"))); + } + let mut dev = 0.0f64; + for r in 0..d { + for col in 0..d { + let mut s = C { re: 0.0, im: 0.0 }; + for k in &ks { + for i in 0..d { + let x = cmul(conj(k[i * d + r]), k[i * d + col]); + s.re += x.re; + s.im += x.im; + } + } + let want = if r == col { 1.0 } else { 0.0 }; + dev = dev.max((s.re - want).hypot(s.im)); + } + } + if dev > CHANNEL_TOLERANCE { + return Err(bad(format!("its Kraus operators are not trace preserving: their sum of K†K is {dev:.3e} from the identity (at most {CHANNEL_TOLERANCE:e})"))); + } + let mut pauli = false; + if let Some(form) = c.pauli(values) { + let mut total = 0.0; + let mut terms = Vec::with_capacity(form.len()); + for (p, s) in &form { + if !p.is_finite() || *p < -CHANNEL_TOLERANCE { + return Err(bad(format!("its Pauli form gives {s} the probability {p}, below zero"))); + } + if s.chars().count() != c.arity() as usize { + return Err(bad(format!("its Pauli form names {s:?}, not a Pauli string on its {} qubits", c.arity()))); + } + let m = pauli_matrix(s).ok_or_else(|| bad(format!("its Pauli form names {s:?}, not a string of I, X, Y and Z")))?; + total += p; + terms.push((*p, m)); + } + if (total - 1.0).abs() > CHANNEL_TOLERANCE { + return Err(bad(format!("its Pauli form's probabilities sum to {total}, not 1"))); + } + let from_pauli = superop(d, &terms); + let from_kraus = superop(d, &ks.iter().map(|k| (1.0, k.clone())).collect::>()); + let off = from_pauli.iter().zip(&from_kraus).map(|(a, b)| (a.re - b.re).hypot(a.im - b.im)).fold(0.0f64, f64::max); + if off > CHANNEL_TOLERANCE { + return Err(bad(format!("its Pauli form is not the channel its Kraus operators make: their superoperators differ by {off:.3e}"))); + } + pauli = true; + } + Ok(json!({ "cptp": dev, "pauli": pauli })) +} diff --git a/wai/wai-quantum/src/engine/registry/tests.rs b/wai/wai-quantum/src/engine/registry/tests.rs new file mode 100644 index 000000000..c38edc8e1 --- /dev/null +++ b/wai/wai-quantum/src/engine/registry/tests.rs @@ -0,0 +1,673 @@ +//! The registry: ground truth that every built-in gives the bytes it gave, +//! through the registry and beside a host's own components; and a host's +//! method, pass, channel and front end, registered, run, refused with their +//! codes, and named in what they make. + +use super::*; +use crate::engine::refusal::codes_in; +use crate::qasm::Kind as Gate; +use serde_json::json; + +const CAP: u64 = 1 << 30; + +// --------------------------------------------------------------------------- +// A host's own components, as a test writes them +// --------------------------------------------------------------------------- + +/// A method for circuits of `x`, `cx`, `ccx` and `swap` alone: the state +/// stays one basis state, followed bit by bit. +struct BasisTable; + +fn permutes(p: &Program) -> bool { + p.ops.iter().all(|o| matches!(o.kind, Gate::X | Gate::Cx | Gate::Ccx | Gate::Swap | Gate::Id)) +} + +fn follow(p: &Program) -> Vec { + let mut b = vec![false; p.n_qubits as usize]; + for o in &p.ops { + let q: Vec = o.qubits.iter().map(|&q| q as usize).collect(); + match o.kind { + Gate::X => b[q[0]] = !b[q[0]], + Gate::Cx if b[q[0]] => b[q[1]] = !b[q[1]], + Gate::Ccx if b[q[0]] && b[q[1]] => b[q[2]] = !b[q[2]], + Gate::Swap => b.swap(q[0], q[1]), + _ => {} + } + } + b +} + +impl Method for BasisTable { + fn name(&self) -> &str { + "basis_table" + } + fn version(&self) -> &str { + "1.0.0" + } + fn classes(&self) -> Vec { + vec!["exact".into()] + } + fn descriptor(&self) -> Value { + json!({ "circuits": "x, cx, ccx and swap alone", "answers": ["counts", "amplitudes"] }) + } + fn auto_rank(&self, question: Question) -> Option { + (question == Question::Counts).then_some(5) + } + fn plan(&self, job: &Job<'_>) -> Result { + let r = job.request()?; + if !permutes(&r.program) { + return Err("basis_table takes x, cx, ccx and swap alone".into()); + } + if !r.observables.is_empty() { + return Err("basis_table answers counts and amplitudes".into()); + } + Ok(Plan { method: "basis_table".into(), why: format!("a permutation of basis states on {} qubits, followed bit by bit", r.program.n_qubits), memory_bytes: 1 << 20, work: r.program.ops.len() as u64, qubits: r.program.n_qubits, target: None }) + } + fn run(&self, job: &Job<'_>, _: &Plan, out: &mut Value) -> Result<(), String> { + let r = job.request()?; + let b = follow(&r.program); + out["accuracy"] = json!({ "class": "exact" }); + if r.shots > 0 { + let mut counts = serde_json::Map::new(); + counts.insert(r.program.bitstring(|q| b[q as usize]), json!(r.shots)); + out["counts"] = Value::Object(counts); + } + if !r.amplitudes.is_empty() { + let n = r.program.n_qubits as usize; + let here: String = (0..n).rev().map(|q| if b[q] { '1' } else { '0' }).collect(); + out["amplitudes"] = r.amplitudes.iter().map(|a| json!({ "bits": a, "re": if *a == here { 1.0 } else { 0.0 }, "im": 0.0 })).collect(); + } + Ok(()) + } +} + +/// A method that misbehaves in one of the ways the contract names. +struct Rogue(&'static str); + +impl Method for Rogue { + fn name(&self) -> &str { + self.0 + } + fn version(&self) -> &str { + "0.1" + } + fn classes(&self) -> Vec { + vec!["exact".into()] + } + fn descriptor(&self) -> Value { + json!({}) + } + fn plan(&self, job: &Job<'_>) -> Result { + let method = if self.0 == "rogue_plan" { "exact".to_string() } else { self.0.to_string() }; + Ok(Plan { method, why: "rogue".into(), memory_bytes: 1, work: 1, qubits: job.request()?.program.n_qubits, target: None }) + } + fn run(&self, _: &Job<'_>, _: &Plan, out: &mut Value) -> Result<(), String> { + match self.0 { + "rogue_method" => { + out["accuracy"] = json!({ "class": "exact" }); + out["method"] = json!("exact"); + } + "rogue_class" => out["accuracy"] = json!({ "class": "bit-exact" }), + _ => out["accuracy"] = json!({ "class": "exact" }), + } + Ok(()) + } +} + +/// A pass that drops adjacent pairs of the same self-inverse gate on the +/// same qubits. +struct CancelPairs; + +fn self_inverse(k: Gate) -> bool { + matches!(k, Gate::X | Gate::Y | Gate::Z | Gate::H | Gate::Cx | Gate::Cz | Gate::Swap | Gate::Ccx) +} + +impl Pass for CancelPairs { + fn name(&self) -> &str { + "cancel_pairs" + } + fn version(&self) -> &str { + "1.0.0" + } + fn descriptor(&self) -> Value { + json!({ "does": "adjacent pairs of one self-inverse gate on the same qubits removed" }) + } + fn check(&self) -> Check { + Check::Ladder + } + fn run(&self, input: &PassInput<'_>) -> Result { + let mut out: Vec = Vec::new(); + for o in input.ops { + if self_inverse(o.kind) && out.last().is_some_and(|l| l.kind == o.kind && l.qubits == o.qubits) { + out.pop(); + } else { + out.push(o.clone()); + } + } + Ok(PassOutput::new(out).with_note(json!({ "how": "pairs dropped" }))) + } +} + +/// A pass that is wrong: it drops the last gate. +struct DropLast(Check); + +impl Pass for DropLast { + fn name(&self) -> &str { + if self.0 == Check::Ladder { "drop_last" } else { "drop_last_fidelity" } + } + fn version(&self) -> &str { + "0.0.1" + } + fn descriptor(&self) -> Value { + json!({ "does": "drops the last gate" }) + } + fn check(&self) -> Check { + self.0 + } + fn run(&self, input: &PassInput<'_>) -> Result { + let mut ops = input.ops.to_vec(); + ops.pop(); + Ok(PassOutput::new(ops)) + } +} + +/// A channel: the bit flip, X with probability p. +struct BitFlip; + +fn one(re: f64) -> C { + C { re, im: 0.0 } +} + +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, on each qubit it touched" }) + } + fn params(&self) -> Vec { + vec![Param::new("p", 0.0, 1.0, "a probability")] + } + fn arity(&self) -> u32 { + 1 + } + fn kraus(&self, v: &[f64]) -> Vec> { + let (a, b) = ((1.0 - v[0]).sqrt(), v[0].sqrt()); + vec![vec![one(a), one(0.0), one(0.0), one(a)], vec![one(0.0), one(b), one(b), one(0.0)]] + } + fn pauli(&self, v: &[f64]) -> Option> { + Some(vec![(1.0 - v[0], "I".into()), (v[0], "X".into())]) + } +} + +/// A channel of two parameters: Z with probability `p` and X with +/// probability `q`, whose operators are wrong when `broken`. +struct TwoParam { + broken: bool, +} + +impl Channel for TwoParam { + fn name(&self) -> &str { + if self.broken { "leaky" } else { "zx" } + } + fn version(&self) -> &str { + "1" + } + fn title(&self) -> &str { + "Z and X flips" + } + fn descriptor(&self) -> Value { + json!({}) + } + fn params(&self) -> Vec { + vec![Param::new("p", 0.0, 0.5, "a probability"), Param::new("q", 0.0, 0.5, "a probability")] + } + fn arity(&self) -> u32 { + 1 + } + fn kraus(&self, v: &[f64]) -> Vec> { + let (p, q) = (v[0], v[1]); + let keep = if self.broken { 1.0 } else { (1.0 - p - q).sqrt() }; + vec![vec![one(keep), one(0.0), one(0.0), one(keep)], vec![one(p.sqrt()), one(0.0), one(0.0), one(-p.sqrt())], vec![one(0.0), one(q.sqrt()), one(q.sqrt()), one(0.0)]] + } +} + +/// A front end: a list of gates, one a line (`qubits 2`, `h 0`, `cx 0 1`, +/// `measure`). +struct GateList; + +impl FrontEnd for GateList { + fn name(&self) -> &str { + "gate_list" + } + fn version(&self) -> &str { + "1.0.0" + } + fn title(&self) -> &str { + "a gate list" + } + fn descriptor(&self) -> Value { + json!({ "reads": "qubits N, then a gate and its qubits a line, and measure", "writes": "the same, gates without parameters" }) + } + fn extensions(&self) -> Vec { + vec!["gates".into()] + } + fn read(&self, input: &[u8]) -> Result { + use crate::qasm::diag::FORMAT_UNREAD; + let text = std::str::from_utf8(input).map_err(|_| Diagnostic::new(FORMAT_UNREAD, None, "a gate list is text"))?; + let mut q = String::from("OPENQASM 3.0;\n"); + let mut n = None; + for (i, line) in text.lines().map(str::trim).filter(|l| !l.is_empty()).enumerate() { + let w: Vec<&str> = line.split_whitespace().collect(); + match (i, w.as_slice()) { + (0, ["qubits", k]) => { + let k: u32 = k.parse().map_err(|_| Diagnostic::new(FORMAT_UNREAD, None, "qubits N"))?; + q.push_str(&format!("qubit[{k}] q;\nbit[{k}] c;\n")); + n = Some(k); + } + (0, _) => return Err(Diagnostic::new(FORMAT_UNREAD, None, "a gate list starts with qubits N")), + (_, ["measure"]) => q.push_str("c = measure q;\n"), + (_, [g, qs @ ..]) => q.push_str(&format!("{g} {};\n", qs.iter().map(|x| format!("q[{x}]")).collect::>().join(", "))), + _ => {} + } + } + n.ok_or_else(|| Diagnostic::new(FORMAT_UNREAD, None, "an empty gate list"))?; + Ok(crate::engine::formats::Imported { qasm: q, notes: Vec::new(), summary: json!({ "format": "gate_list" }) }) + } + fn write(&self, p: &Program, _: Option<&str>) -> Result { + let mut s = format!("qubits {}\n", p.n_qubits); + for o in &p.ops { + if !o.params.is_empty() { + return Err(Diagnostic::new(crate::qasm::diag::WRITE_CANNOT, None, "a gate list holds gates without parameters")); + } + s.push_str(&format!("{} {}\n", format!("{:?}", o.kind).to_lowercase(), o.qubits.iter().map(u32::to_string).collect::>().join(" "))); + } + if !p.measured.is_empty() { + s.push_str("measure\n"); + } + Ok(crate::engine::formats::Exported { bytes: s.into_bytes(), notes: Vec::new(), summary: json!({ "format": "gate_list" }) }) + } +} + +/// The built-ins and every component above. +fn host() -> Registry { + let mut r = Registry::with_builtins(); + r.register(Component::method(BasisTable)).unwrap(); + r.register(Component::pass(CancelPairs)).unwrap(); + r.register(Component::channel(BitFlip)).unwrap(); + r.register(Component::channel(TwoParam { broken: false })).unwrap(); + r.register(Component::front_end(GateList)).unwrap(); + r +} + +// --------------------------------------------------------------------------- +// The built-ins +// --------------------------------------------------------------------------- + +#[test] +fn every_built_in_registers_through_its_trait_in_a_fixed_order() { + let r = Registry::with_builtins(); + let names = |k: Kind| r.identities().into_iter().filter(|i| i.kind == k).map(|i| i.name.clone()).collect::>(); + assert_eq!(names(Kind::Method), ["exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "stabilizer_rank", "tensor_network", "cutrank", "cutrank_single", "density_matrix", "trajectories", "pulse"]); + assert_eq!(names(Kind::Pass), crate::engine::compile::PASSES.iter().map(|(n, _)| n.to_string()).collect::>()); + assert_eq!(names(Kind::Channel), crate::engine::catalog::NOISE_CHANNELS); + assert_eq!(names(Kind::FrontEnd), ["qasm", "qir", "jeff"]); + for i in r.identities() { + assert!(i.is_builtin() && i.version == crate::engine::schema::ENGINE_VERSION, "{i:?}"); + assert!(i.digest.len() == 64 && i.digest.bytes().all(|b| b.is_ascii_hexdigit()), "{i:?}"); + assert!(i.to_json().get("certified").is_none(), "a built-in is not marked as registered: {i:?}"); + } + // Made twice, the same bytes; the shared one is the same registry. + assert_eq!(r.components().to_string(), Registry::with_builtins().components().to_string()); + assert_eq!(r.components(), Registry::builtins().components()); + assert_eq!(r.components()["registered"], 0); + // The methods list is the registry's, and the words hashed hold nothing of the host. + assert_eq!(crate::engine::catalog::describe(CAP), r.describe_methods(CAP)); + for m in r.methods() { + let d = m.descriptor(); + assert!(d.get("max_qubits").is_none() && d.get("available").is_none() && d.get("max_cut").is_none() && d.get("method").is_none(), "{}: {d}", m.name()); + let entry = m.describe(CAP); + assert_eq!(entry["method"], m.name()); + for (k, v) in d.as_object().unwrap() { + assert_eq!(&entry[k], v, "{}: {k}", m.name()); + } + } +} + +/// `auto` tries the methods in the order it always has. +#[test] +fn auto_tries_the_built_ins_in_the_order_it_did() { + let r = Registry::with_builtins(); + let order = |q| r.auto_order(q).into_iter().map(|m| m.name().to_string()).collect::>(); + assert_eq!(order(Question::Noise), ["density_matrix", "stabilizer", "trajectories"]); + assert_eq!(order(Question::Counts), ["stabilizer", "exact", "statevector", "statevector_single", "cutrank", "mps"]); + assert_eq!(order(Question::Expectations), ["stabilizer", "exact", "statevector", "statevector_single", "stabilizer_rank", "pauli", "mps"]); + assert_eq!(order(Question::Amplitudes), ["exact", "statevector", "statevector_single", "cutrank", "stabilizer_rank", "tensor_network", "mps"]); + // A host's method joins where its rank puts it, the built-ins kept in their order. + let h = host(); + assert_eq!(h.auto_order(Question::Counts).iter().map(|m| m.name()).collect::>(), ["basis_table", "stabilizer", "exact", "statevector", "statevector_single", "cutrank", "mps"]); +} + +/// Ground truth: the pinned jobs give the bytes they gave, through the +/// built-in registry and through one that holds a host's components beside +/// the built-ins (none of which these jobs name, and no method `auto` tries: +/// one it tries is named in the account of what `auto` passed over). +#[cfg(feature = "quantum_receipt")] +#[test] +fn the_pinned_jobs_give_their_bytes_through_any_registry() { + use crate::engine::job::tests::{PINNED, pinned_digest_in}; + let mut h = Registry::with_builtins(); + h.register(Component::method(Rogue("quiet"))).unwrap(); + h.register(Component::pass(CancelPairs)).unwrap(); + h.register(Component::channel(BitFlip)).unwrap(); + h.register(Component::front_end(GateList)).unwrap(); + for (req, want) in PINNED { + assert_eq!(pinned_digest_in(Registry::builtins(), req), want, "{req}"); + assert_eq!(pinned_digest_in(&h, req), want, "{req}, beside a host's components"); + } +} + +/// The built-in channels pass their own validity check at every strength, +/// and each Pauli form is its operators' channel. +#[test] +fn the_built_in_channels_are_valid_channels() { + let r = Registry::with_builtins(); + for c in r.channels() { + for p in [0.0, 1e-9, 0.01, 0.3, 0.75, 1.0] { + let v = validate(c, &[p]).unwrap_or_else(|e| panic!("{} at {p}: {e}", c.name())); + assert_eq!(v["pauli"], c.name() != "amplitude_damping", "{}", c.name()); + } + } + assert!(validate(&TwoParam { broken: true }, &[0.1, 0.1]).unwrap_err().ends_with("[J062]")); +} + +/// Every built-in pass gives the gates it declares it gives, on compiles +/// that use each at every level. +#[test] +fn the_built_in_passes_give_what_they_declare() { + use crate::engine::compile::{Basis, Dd, Options, pipeline}; + let r = Registry::with_builtins(); + let src = "OPENQASM 3.0;\nqubit[3] q;\nh q;\nrz(0.3) q[0];\ncx q[0], q[1];\nry(1.1) q[1];\ncp(0.7) q[1], q[2];\nccx q[0], q[1], q[2];\nt q[0];\nt q[0];\nswap q[1], q[2];\n"; + let p = crate::qasm::parse(src).unwrap(); + for basis in [Basis::CliffordT, Basis::Standard] { + for level in 0..=crate::engine::compile::MAX_LEVEL { + let o = Options { epsilon: 1e-3, level, basis, twirl: Some(2), dd: Some(Dd::Xy4), ..Default::default() }; + let mut ops = p.ops.clone(); + let mut spent = 0.0; + for name in pipeline(&o) { + let pass = r.pass(name).unwrap(); + assert!(pass.takes().holds(&ops), "{name} is given gates it takes"); + let out = pass.run(&PassInput { qubits: 3, ops: &ops, options: &o, spent }).unwrap(); + if out.check["verdict"] != "rejected" && out.check["verdict"] != "skipped" { + assert!(pass.gives().holds(&out.ops), "{basis:?} level {level}: {name} gives what it declares"); + spent += out.distance; + ops = out.ops; + } + } + // The registry's compile is the compile. + let a = crate::engine::compile::compile(&p, &o).unwrap(); + assert_eq!(r.compile(&p, &o, &[]).unwrap().qasm, a.qasm); + assert_eq!(a.program.ops, ops, "{basis:?} level {level}: the passes run one by one through their trait are the compile"); + } + } +} + +// --------------------------------------------------------------------------- +// A host's components +// --------------------------------------------------------------------------- + +/// A host's method runs where the built-ins run, named or chosen by `auto`, +/// gives the bit-exact method's answer, and the result names every +/// component that made it, the method marked as registered. +#[test] +fn a_registered_method_is_planned_run_and_named_in_its_result() { + let h = host(); + let toffoli = "OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nx q[0];\nx q[1];\nccx q[0], q[1], q[2];\nswap q[0], q[2];\nc = measure q;\n"; + let (plan, out) = h.plan_and_run(&json!({ "qasm": toffoli, "shots": 50 }), CAP).unwrap(); + assert_eq!(plan.method, "basis_table", "auto takes it by its rank: {}", plan.why); + assert_eq!(out["counts"], json!({ "111": 50 }), "{out}"); + let (_, exact) = h.plan_and_run(&json!({ "qasm": toffoli, "shots": 50, "method": "exact" }), CAP).unwrap(); + assert_eq!(exact["counts"], out["counts"]); + assert!(exact.get("components").is_none(), "a built-in's result is as it was: {exact}"); + let c = out["components"].as_array().unwrap(); + assert_eq!(c.iter().map(|x| (x["kind"].as_str().unwrap(), x["name"].as_str().unwrap(), x["origin"].as_str().unwrap())).collect::>(), [("front_end", "qasm", "built-in"), ("method", "basis_table", "registered")]); + assert_eq!(c[1]["certified"], false); + assert_eq!(c[1]["digest"], h.identity(Kind::Method, "basis_table").unwrap().digest); + assert_eq!(h.components_of(&json!({}), &out), c.clone()); + // The same job, the same bytes, less the wall clock. + let again = h.plan_and_run(&json!({ "qasm": toffoli, "shots": 50 }), CAP).unwrap().1; + let strip = |mut v: Value| { + v.as_object_mut().unwrap().remove("elapsed_ms"); + v.to_string() + }; + assert_eq!(strip(again), strip(out)); + // What it does not take, it refuses, and auto passes over it. + let bell = json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\n", "shots": 10, "method": "basis_table" }); + assert_eq!(codes_in(&h.plan(&bell, CAP).unwrap_err()), ["J011"]); + let mut auto = bell.clone(); + auto["method"] = json!("auto"); + let p = h.plan(&auto, CAP).unwrap(); + assert!(p.method == "stabilizer" && p.why.contains("not basis_table: basis_table takes"), "{}", p.why); + // Not in the built-in registry. + assert_eq!(codes_in(&Registry::builtins().plan(&json!({ "qasm": toffoli, "shots": 1, "method": "basis_table" }), CAP).unwrap_err()), ["J010"]); + // A built-in's identity, read from a result that names only built-ins. + let ids = h.components_of(&json!({ "qasm": toffoli }), &exact); + assert_eq!(ids.iter().map(|x| x["name"].as_str().unwrap()).collect::>(), ["qasm", "exact"]); +} + +/// The receipt seals a registered component's identity, which cannot be +/// changed or dropped (it cannot pass for a built-in). +#[cfg(feature = "quantum_receipt")] +#[test] +fn the_receipt_seals_a_registered_component_s_identity() { + let h = host(); + let key = ed25519_dalek::SigningKey::from_bytes(&[5; 32]); + let req = json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nx q[1];\ncx q[1], q[0];\nc = measure q;\n", "shots": 8, "method": "basis_table" }); + let body = h.job(&req, CAP, &key, json!({ "unmetered": "a test" })).unwrap(); + assert!(crate::engine::job::verify(&body).unwrap().0); + assert_eq!(body["result"]["components"][1]["origin"], "registered"); + for (path, v) in [("/result/components/1/origin", json!("built-in")), ("/result/components/1/digest", json!("0".repeat(64))), ("/result/components/1/certified", json!(true))] { + let mut forged = body.clone(); + *forged.pointer_mut(path).unwrap() = v; + assert!(!crate::engine::job::verify(&forged).unwrap().0, "{path}"); + } + let mut dropped = body.clone(); + dropped["result"].as_object_mut().unwrap().remove("components"); + assert!(!crate::engine::job::verify(&dropped).unwrap().0); + // It follows the result's schema. + let errs = crate::engine::schema::Registry::engine().check("job.v1.json", &body); + assert!(errs.is_empty(), "{errs:?}"); +} + +/// Registration is refused with its code, and a built-in's name is never +/// taken. +#[test] +fn registration_refuses_what_it_cannot_hold() { + let mut h = host(); + for (c, needle) in [ + (Component::method(BasisTable), "a registered method holds that name"), + (Component::method(Rogue("exact")), "a built-in method holds that name"), + (Component::method(Rogue("auto")), "auto is the engine's choice"), + (Component::method(Rogue("bad name")), "a name is 1 to 64"), + (Component::pass(DropLast(Check::Fidelity { max_distance: f64::NAN })), "a finite distance"), + (Component::front_end(GateList), "a registered front end holds that name"), + (Component::channel(BitFlip), "a registered channel holds that name"), + ] { + let e = h.register(c).unwrap_err(); + assert!(e.ends_with("[J063]") && e.contains(needle), "{e}"); + } + assert_eq!(h.components()["registered"], 5); + let names: Vec<&str> = h.identities().iter().filter(|i| !i.is_builtin()).map(|i| i.name.as_str()).collect(); + assert_eq!(names, ["basis_table", "cancel_pairs", "bit_flip", "zx", "gate_list"], "in the order registered, by kind"); +} + +/// A registered method that breaks the result's contract is refused, and +/// nothing it gave is returned. +#[test] +fn a_registered_method_s_result_is_held_to_the_contract() { + let mut h = Registry::with_builtins(); + for n in ["rogue_method", "rogue_class", "rogue_plan"] { + h.register(Component::method(Rogue(n))).unwrap(); + } + let job = |m: &str| json!({ "qasm": "OPENQASM 3.0;\nqubit[1] q;\nx q[0];\n", "shots": 1, "method": m }); + for m in ["rogue_method", "rogue_class", "rogue_plan"] { + let e = h.plan_and_run(&job(m), CAP).unwrap_err(); + assert_eq!(codes_in(&e).last().copied(), Some("J064"), "{m}: {e}"); + } +} + +/// A registered channel: its values read and checked, taken by the density +/// matrix through its Kraus operators, refused by the methods that take the +/// built-ins alone, and named in the result. +#[test] +fn a_registered_channel_runs_on_the_density_matrix_and_is_refused_elsewhere() { + let h = host(); + // X then a bit flip: the qubit reads 0 with probability p. + let x = "OPENQASM 3.0;\nqubit q;\nx q;\n"; + let (plan, out) = h.plan_and_run(&json!({ "qasm": x, "observables": ["Z0"], "noise": { "bit_flip": 0.2 } }), CAP).unwrap(); + assert_eq!(plan.method, "density_matrix"); + assert!((out["expectations"][0]["value"].as_f64().unwrap() - (-1.0 + 2.0 * 0.2)).abs() < 1e-12, "{out}"); + let names: Vec<(&str, &str)> = out["components"].as_array().unwrap().iter().map(|c| (c["name"].as_str().unwrap(), c["origin"].as_str().unwrap())).collect(); + assert_eq!(names, [("qasm", "built-in"), ("bit_flip", "registered"), ("density_matrix", "built-in")]); + // A channel of two parameters, given by name, against its closed form, + // beside a built-in channel: ⟨Z⟩ after x then Z and X flips and dephasing. + let (_, two) = h.plan_and_run(&json!({ "qasm": x, "observables": ["Z0"], "noise": { "zx": { "p": 0.1, "q": 0.15 }, "dephasing": 0.05 } }), CAP).unwrap(); + assert!((two["expectations"][0]["value"].as_f64().unwrap() - (-1.0 + 2.0 * 0.15)).abs() < 1e-12, "{two}"); + // The same channel as a built-in, registered under another name: the same numbers. + let mut d = host(); + struct Dephase; + impl Channel for Dephase { + fn name(&self) -> &str { + "my_dephasing" + } + fn version(&self) -> &str { + "1" + } + fn title(&self) -> &str { + "dephasing" + } + fn descriptor(&self) -> Value { + json!({}) + } + fn params(&self) -> Vec { + vec![Param::new("p", 0.0, 1.0, "a probability")] + } + fn arity(&self) -> u32 { + 1 + } + fn kraus(&self, v: &[f64]) -> Vec> { + let (a, b) = ((1.0 - v[0]).sqrt(), v[0].sqrt()); + vec![vec![one(a), one(0.0), one(0.0), one(a)], vec![one(b), one(0.0), one(0.0), one(-b)]] + } + } + d.register(Component::channel(Dephase)).unwrap(); + let ghz = "OPENQASM 3.0;\nqubit[3] q;\nbit[3] c;\nh q[0];\ncx q[0], q[1];\nry(0.4) q[2];\ncx q[1], q[2];\nc = measure q;\n"; + let obs = json!(["X0 X1", "Z1 Z2", "Y0 Y1 X2"]); + let mine = d.plan_and_run(&json!({ "qasm": ghz, "observables": obs, "noise": { "my_dephasing": 0.07 } }), CAP).unwrap().1; + let built = d.plan_and_run(&json!({ "qasm": ghz, "observables": obs, "noise": { "dephasing": 0.07 } }), CAP).unwrap().1; + for k in 0..3 { + let (a, b) = (mine["expectations"][k]["value"].as_f64().unwrap(), built["expectations"][k]["value"].as_f64().unwrap()); + assert!((a - b).abs() < 1e-14, "{k}: {a} against {b}"); + } + // Refused, with J061, by the methods that take the built-in channels alone. + for m in ["stabilizer", "trajectories"] { + let e = h.plan(&json!({ "qasm": x, "shots": 10, "method": m, "noise": { "bit_flip": 0.1 } }), CAP).unwrap_err(); + assert!(e.ends_with("[J061]") && e.contains("taken by density_matrix"), "{m}: {e}"); + } + let e = h.plan(&json!({ "qasm": x, "shots": 10, "method": "exact", "noise": { "bit_flip": 0.1 } }), CAP).unwrap_err(); + assert!(e.ends_with("[J061]") && e.contains("exact does not model noise"), "{e}"); + // Its values read: a value outside its range, a parameter it lacks or + // does not have, a value of another kind (J003); and an intent beside it + // (J065), which the intent checks do not model. + for (noise, code) in [(json!({ "bit_flip": 1.5 }), "J003"), (json!({ "zx": { "p": 0.1 } }), "J003"), (json!({ "zx": { "p": 0.1, "q": 0.1, "r": 0.0 } }), "J003"), (json!({ "bit_flip": "high" }), "J003")] { + let e = h.plan(&json!({ "qasm": x, "shots": 10, "noise": noise }), CAP).unwrap_err(); + assert_eq!(codes_in(&e).last().copied(), Some(code), "{noise}: {e}"); + } + let measured = "OPENQASM 3.0;\nqubit q;\nbit c;\nx q;\nc = measure q;\n"; + let e = h.plan(&json!({ "qasm": measured, "shots": 10, "noise": { "bit_flip": 0.1 }, "intent": { "distribution": { "0": 0.1, "1": 0.9 } } }), CAP).unwrap_err(); + assert!(e.ends_with("[J065]"), "{e}"); + // A channel whose operators are not a channel is refused at the job's values. + let mut l = Registry::with_builtins(); + l.register(Component::channel(TwoParam { broken: true })).unwrap(); + let e = l.plan(&json!({ "qasm": x, "shots": 10, "noise": { "leaky": { "p": 0.1, "q": 0.1 } } }), CAP).unwrap_err(); + assert!(e.ends_with("[J062]") && e.contains("not trace preserving"), "{e}"); + // And it is unknown where it is not registered. + let e = Registry::builtins().plan(&json!({ "qasm": x, "shots": 10, "noise": { "bit_flip": 0.1 } }), CAP).unwrap_err(); + assert!(e.ends_with("[J003]") && e.contains("no such channel"), "{e}"); + // Amplitude damping on the stabilizer's frames is refused with the same code. + let e = Registry::builtins().plan(&json!({ "qasm": "OPENQASM 3.0;\nqubit q;\nh q;\n", "shots": 10, "method": "stabilizer", "noise": { "amplitude_damping": 0.1 } }), CAP).unwrap_err(); + assert!(e.ends_with("[J061]") && e.contains("not amplitude damping"), "{e}"); +} + +/// A registered pass runs where the built-ins run, after the level's own, +/// checked by the engine by the check it declares; a wrong one is rejected +/// and the program goes on as it was. +#[test] +fn a_registered_pass_is_checked_by_the_engine() { + let mut h = host(); + h.register(Component::pass(DropLast(Check::Ladder))).unwrap(); + h.register(Component::pass(DropLast(Check::Fidelity { max_distance: 1e-3 }))).unwrap(); + let src = "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\nx q[1];\nx q[1];\ncx q[0], q[1];\nt q[1];\n"; + let req = |passes: Value| json!({ "qasm": src, "level": 0, "basis": "standard", "passes": passes }); + let good = h.compile_call(&req(json!(["cancel_pairs"]))).unwrap(); + let records = good["certificate"]["passes"].as_array().unwrap(); + let mine = records.iter().find(|r| r["pass"] == "cancel_pairs").unwrap(); + assert_eq!((mine["check"]["verdict"].as_str(), mine["check"]["by"].as_str(), mine["component"]["origin"].as_str()), (Some("exact"), Some("engine"), Some("registered")), "every basis state: {mine}"); + assert_eq!(mine["check"]["said"]["how"], "pairs dropped"); + assert!(records.iter().filter(|r| r["pass"] != "cancel_pairs").all(|r| r.get("component").is_none()), "a built-in's record is as it was"); + assert!(!good["qasm"].as_str().unwrap().contains("x q[1]"), "{}", good["qasm"]); + assert_eq!(good["components"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect::>(), ["qasm", "lower", "cancel_pairs"]); + // Wrong, and caught by the ladder or by its distance: left out. + for name in ["drop_last", "drop_last_fidelity"] { + let bad = h.compile_call(&req(json!([name]))).unwrap(); + let r = bad["certificate"]["passes"].as_array().unwrap().iter().find(|r| r["pass"] == name).unwrap().clone(); + assert_eq!(r["check"]["verdict"], "rejected", "{name}: {r}"); + assert!(bad["qasm"].as_str().unwrap().contains("t q[1]"), "{name}: the program went on as it was"); + } + // The same passes as a job's compile, the result naming them. + let (_, out) = h.plan_and_run(&json!({ "qasm": src, "shots": 20, "compile": { "level": 0, "basis": "standard", "passes": ["cancel_pairs"] } }), CAP).unwrap(); + assert!(out["components"].as_array().unwrap().iter().any(|c| c["name"] == "cancel_pairs" && c["origin"] == "registered"), "{out}"); + // A pass this host does not hold is refused, here and in the built-ins. + let e = h.compile_call(&req(json!(["no_such_pass"]))).unwrap_err(); + assert!(e.ends_with("[J060]") && e.contains("the passes here are"), "{e}"); + let e = Registry::builtins().plan(&json!({ "qasm": src, "shots": 1, "compile": { "passes": ["cancel_pairs"] } }), CAP).unwrap_err(); + assert!(e.ends_with("[J060]"), "{e}"); + // A built-in pass named in `passes` runs through the same path. + let extra = Registry::builtins().compile_call(&json!({ "qasm": src, "level": 0, "basis": "standard", "passes": ["simplify"] })).unwrap(); + assert!(extra.get("components").is_none() && extra["certificate"]["passes"].as_array().unwrap().iter().any(|r| r["pass"] == "simplify")); +} + +/// A registered front end reads and writes through the conversions and a +/// job's source, each checked, and is named in what it made. +#[test] +fn a_registered_front_end_converts_and_feeds_a_job() { + let h = host(); + let list = "qubits 2\nh 0\ncx 0 1\nmeasure\n"; + let to_qir = h.convert_json(&json!({ "from": "gate_list", "to": "qir", "program": list })).unwrap(); + assert_eq!(to_qir["check"]["verdict"]["verdict"], "equivalent", "{to_qir}"); + assert_eq!(to_qir["components"].as_array().unwrap().iter().map(|c| c["origin"].as_str().unwrap()).collect::>(), ["registered", "built-in"]); + let back = h.convert_json(&json!({ "from": "qasm", "to": "gate_list", "program": to_qir["qasm"] })).unwrap(); + assert_eq!(back["program"], list, "{back}"); + assert_eq!(back["check"]["verdict"]["verdict"], "equivalent", "{back}"); + let to_qasm = h.convert_json(&json!({ "from": "gate_list", "to": "qasm", "program": list })).unwrap(); + assert_eq!(to_qasm["check"]["verdict"]["verdict"], "equivalent", "written back as a gate list and read again: {to_qasm}"); + // A job whose source is a gate list. + let job = h.expand(&json!({ "source": { "format": "gate_list", "program": list }, "shots": 100 })).unwrap().unwrap(); + let (_, out) = h.plan_and_run(&job, CAP).unwrap(); + assert!(out["counts"].as_object().unwrap().keys().all(|k| k == "00" || k == "11"), "{out}"); + assert_eq!(out["components"][0]["name"], "gate_list"); + // Refused: a program it cannot write, and the format where it is not registered. + let e = h.convert_json(&json!({ "from": "qasm", "to": "gate_list", "program": "OPENQASM 3.0;\nqubit q;\nrx(0.3) q;\n" })).unwrap_err(); + assert!(e.ends_with("[Q720]"), "{e}"); + let e = Registry::builtins().convert_json(&json!({ "from": "gate_list", "to": "qasm", "program": list })).unwrap_err(); + assert!(e.ends_with("[J003]") && e.contains("the formats are qasm, qir, jeff"), "{e}"); + // A profile is QIR's, as it always said. + let e = h.convert_json(&json!({ "from": "qasm", "to": "gate_list", "profile": "base", "program": "OPENQASM 3.0;\nqubit q;\nh q;\n" })).unwrap_err(); + assert!(e.contains("a profile is QIR's; gate_list takes none"), "{e}"); +} diff --git a/wai/wai-quantum/src/engine/run.rs b/wai/wai-quantum/src/engine/run.rs index 1acbe02af..e450638eb 100644 --- a/wai/wai-quantum/src/engine/run.rs +++ b/wai/wai-quantum/src/engine/run.rs @@ -3,6 +3,7 @@ //! first; an amplitude's bit string names every qubit, qubit `n − 1` first. use crate::engine::catalog::{self, MAX_OUTCOMES, Plan, Request}; +use crate::engine::registry::{Job, Kind, Method, Registry}; use serde_json::{Value, json}; use std::collections::HashMap; @@ -116,16 +117,27 @@ impl Started { /// it always gave. The result names the engine that made it and the schema /// it follows ([`crate::engine::schema::stamp`]). pub fn run(request: &Value, plan: &Plan) -> Result { - run_noting(request, plan, &mut None) + run_in(Registry::builtins(), request, plan) } -fn run_noting(request: &Value, plan: &Plan, passes: &mut Option) -> Result { - let mut out = run_job(request, plan, passes)?; +/// [`run`] through `reg`: its methods, channels, passes and front ends. A +/// result a registered component took part in carries `components`, the +/// identity of every component that made it. +pub(crate) fn run_in(reg: &Registry, request: &Value, plan: &Plan) -> Result { + run_noting(reg, request, plan, &mut None) +} + +fn run_noting(reg: &Registry, request: &Value, plan: &Plan, passes: &mut Option) -> Result { + let mut out = run_job(reg, request, plan, passes)?; // A pulse run carries its own verdict (engine::pulse). - if crate::engine::verdict::asked(request) && !crate::engine::pulse::names_pulse(request) { - out["verdict"] = crate::engine::verdict::of_run(request, &out, plan.memory_bytes)?; + if crate::engine::verdict::asked(request) && catalog::reads_own(reg, request).is_none() { + out["verdict"] = crate::engine::verdict::of_run_in(reg, request, &out, plan.memory_bytes)?; } crate::engine::schema::stamp(&mut out); + let used = reg.used(request, &out); + if used.iter().any(|i| !i.is_builtin()) { + out["components"] = used.iter().map(|i| i.to_json()).collect(); + } Ok(out) } @@ -150,8 +162,13 @@ pub const PROVENANCE: &str = "wai.quantum.provenance/1"; /// identifier and times are the host's, outside the result); /// - `software`: the engine's name and version, and the host's. pub fn run_with_provenance(request: &Value, plan: &Plan, host: &str) -> Result { + run_with_provenance_in(Registry::builtins(), request, plan, host) +} + +/// [`run_with_provenance`] through `reg`. +pub(crate) fn run_with_provenance_in(reg: &Registry, request: &Value, plan: &Plan, host: &str) -> Result { let mut passes = None; - let mut out = run_noting(request, plan, &mut passes)?; + let mut out = run_noting(reg, request, plan, &mut passes)?; out["provenance"] = provenance(request, plan, &out, passes, host); Ok(out) } @@ -207,10 +224,13 @@ pub fn provenance(request: &Value, plan: &Plan, result: &Value, passes: Option) -> Result { - // A pulse run is integrated by the pulse simulator (engine::pulse). - if crate::engine::pulse::names_pulse(request) { - return crate::engine::pulse::run(request, plan); +fn run_job(reg: &Registry, request: &Value, plan: &Plan, passes: &mut Option) -> Result { + // A method that reads its job's program itself: a pulse run is integrated + // by the pulse simulator (engine::pulse). + if let Some(m) = catalog::reads_own(reg, request) { + let mut out = Value::Null; + m.run(&Job::new(reg, request, plan.memory_bytes, None, &[]), plan, &mut out)?; + return Ok(out); } // A job that names a device target runs as the device would run it. #[cfg(feature = "quantum_receipt")] @@ -224,7 +244,7 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result) -> Result Result<(), String> { + use crate::engine::refusal::{CONTRACT, tag}; + let name = m.name(); + if !out.is_object() { + return Err(tag(CONTRACT, format!("the method {name} gave a result that is not an object"))); + } + for k in crate::engine::registry::ENGINE_MEMBERS { + if before.get(k) != out.get(k) { + return Err(tag(CONTRACT, format!("the method {name} wrote `{k}`, which the engine writes"))); + } + } + let class = out.pointer("/accuracy/class").and_then(Value::as_str).unwrap_or(""); + if !m.classes().iter().any(|c| c == class) { + return Err(tag(CONTRACT, format!("the method {name} gave the accuracy class {class:?}; it declared {:?}", m.classes()))); + } + Ok(()) +} + +/// A built-in method's run (its registry entry's, +/// [`crate::engine::registry::Method::run`]): `method` carried out for the +/// job, its answers written into `out`. A program that measures in the +/// middle runs branch by branch. +pub(crate) fn run_builtin(method: &str, job: &Job<'_>, plan: &Plan, out: &mut Value) -> Result<(), String> { + let r = job.request()?; + let request = job.json(); + if let Some(steps) = &r.program.dynamic { + return run_dynamic(r, plan, steps, out); + } + let n = r.program.n_qubits; + match method { "exact" => { let c = r.program.dyadic()?; let sv = c.simulate().map_err(|e| e.to_string())?; @@ -259,10 +328,10 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result> q & 1 == 1)).or_insert(0) += k; } } - merge(&mut out, counts(map)); + merge(out, counts(map)); } if !r.observables.is_empty() { - out["expectations"] = expectation_entries(&r, |s| expect_fixed(&sv.amps, s)); + out["expectations"] = expectation_entries(r, |s| expect_fixed(&sv.amps, s)); } if !r.amplitudes.is_empty() { let scale = (1u64 << 30) as f64; @@ -293,10 +362,10 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result> (n - 1 - q) & 1 == 1)).or_insert(0) += 1; } - merge(&mut out, counts(map)); + merge(out, counts(map)); } if !r.observables.is_empty() { - out["expectations"] = expectation_entries(&r, |s| sv.expectation(&[(1.0, s.to_vec())])); + out["expectations"] = expectation_entries(r, |s| sv.expectation(&[(1.0, s.to_vec())])); } if !r.amplitudes.is_empty() { out["amplitudes"] = Value::Array( @@ -315,7 +384,7 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result) -> Result> (n - 1 - q) & 1 == 1)).or_insert(0) += 1; } - merge(&mut out, counts(map)); + merge(out, counts(map)); } if !r.observables.is_empty() { - out["expectations"] = expectation_entries(&r, |s| sv.expectation(&[(1.0, s.to_vec())])); + out["expectations"] = expectation_entries(r, |s| sv.expectation(&[(1.0, s.to_vec())])); } if !r.amplitudes.is_empty() { out["amplitudes"] = Value::Array( @@ -363,7 +432,7 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result) -> Result> = r.observables.iter().map(|(_, _, s)| s.clone()).collect(); @@ -415,7 +484,7 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result) -> Result { use crate::quantum_mpsim::{Chain, MpsConfig}; - let chi = catalog::mps_bond(&r); + let chi = catalog::mps_bond(r); let cutoff = r.options.get("cutoff").and_then(Value::as_f64).unwrap_or(1e-12).clamp(0.0, 1e-2); let mut chain = Chain::run(n, &r.program.local_matrices(), MpsConfig { max_bond: chi, cutoff }).map_err(|e| e.to_string())?; let rep = chain.report(); @@ -502,7 +571,7 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result) -> Result { use crate::quantum_cutrank_metal::GpuCutRank; - let mut pm = catalog::product_measured(&r, true)?; + let mut pm = catalog::product_measured(r, true)?; let cut = pm.max_cut(); // The batch the plan's memory holds, the first table aside. let one = catalog::cutrank_bytes(&pm, crate::quantum_cutrank::Precision::Single); @@ -546,10 +615,10 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result) -> Result= catalog::cutrank_bytes(&pm, Precision::Double) { Precision::Double } else { Precision::Single }; let cut = pm.max_cut(); out["accuracy"] = json!({ @@ -583,14 +652,14 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result) -> Result], seed: u64| -> Result<(Vec<(f64, f64)>, Option), String> { @@ -689,7 +758,14 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result { use crate::quantum_noisy::DensityMatrix; let noise = r.noise.ok_or("no noise model")?; - let rho = DensityMatrix::evolve(&r.program.matrices(), n, &noise).map_err(|e| format!("{e:?}"))?; + // A registered channel joins the built-ins' after each gate, as its + // Kraus operators; without one, the noise model's own passes. + let registered: Vec<_> = job.registered_channels().collect(); + let rho = if registered.is_empty() { + DensityMatrix::evolve(&r.program.matrices(), n, &noise).map_err(|e| format!("{e:?}"))? + } else { + DensityMatrix::evolve_steps(n, &noisy_steps(&r.program.matrices(), &noise, ®istered)).map_err(|e| format!("{e:?}"))? + }; out["accuracy"] = json!({ "class": "exact", "note": "the noisy state itself, under the model; counts are seeded samples of its distribution" }); if r.shots > 0 { let p = rho.probabilities(noise.readout); @@ -697,16 +773,16 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result> (n - 1 - q) & 1 == 1)).or_insert(0) += 1; } - merge(&mut out, counts(map)); + merge(out, counts(map)); } if !r.observables.is_empty() { - out["expectations"] = expectation_entries(&r, |s| rho.expectation(&[(1.0, s.to_vec())])); + out["expectations"] = expectation_entries(r, |s| rho.expectation(&[(1.0, s.to_vec())])); } } "trajectories" => { use crate::quantum_noisy::branching_trajectories; let noise = r.noise.ok_or("no noise model")?; - let count = catalog::trajectory_count(&r); + let count = catalog::trajectory_count(r); // The plan reserved memory for this many states. let states = ((plan.memory_bytes.saturating_sub(256 << 20)) / (catalog::amplitude_bytes(n) * if n < 16 { 8 } else { 1 }).max(1)).clamp(1, 16) as usize; let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(8); @@ -733,21 +809,13 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option) -> Result> (n - 1 - q) & 1 == 1)).or_insert(0) += k; } - merge(&mut out, counts(map)); + merge(out, counts(map)); out["shots"] = json!(count); } } other => return Err(format!("unknown method {other}")), } - #[cfg(feature = "quantum_noise")] - if let Some(m) = &r.mitigate { - let o = crate::engine::mitigate::Options::read(m, r.seed)?; - let noise = r.noise.ok_or("mitigation needs the job's noise")?; - out["mitigation"] = crate::engine::mitigate::run(&r.program, &noise, &r.observables, &o, out.get("counts")).map_err(|e| crate::engine::refusal::tag(crate::engine::refusal::MITIGATE, e))?; - } - carried(&r, plan, &mut out); - started.stamp(&mut out); - Ok(out) + Ok(()) } /// What the program carries beyond its gates, in the result: its `output`s @@ -832,6 +900,49 @@ fn run_dynamic(r: &catalog::Request, plan: &Plan, steps: &[crate::qasm::Instr], Ok(()) } +/// The steps of `gates` under `noise` and the registered channels +/// `registered`: each gate, then the built-in channels in the order the +/// density matrix applies them ([`crate::quantum_noisy::DensityMatrix::evolve`]), +/// then each registered channel, a one-qubit one on each qubit the gate +/// touched and a two-qubit one on a two-qubit gate's pair. +fn noisy_steps(gates: &[crate::gates::Gate], noise: &crate::quantum_noisy::NoiseModel, registered: &[&crate::engine::registry::ChannelUse<'_>]) -> Vec { + use crate::gates::Gate; + use crate::quantum_noisy::{Step, damping_kraus, pauli_gate, pauli_pair}; + let mut steps = Vec::with_capacity(gates.len() * 4); + for g in gates { + steps.push(Step::Gate(g.clone())); + let k = g.qubits.len(); + if k == 2 && noise.depol2 > 0.0 { + let (a, b) = (g.qubits[0], g.qubits[1]); + steps.push(Step::Channel((0..16u8).map(|j| (if j == 0 { 1.0 - noise.depol2 } else { noise.depol2 / 15.0 }, pauli_pair(a, b, j))).collect())); + } + for &q in &g.qubits { + if k != 2 && noise.depol1 > 0.0 { + steps.push(Step::Channel((0..4u8).map(|j| (if j == 0 { 1.0 - noise.depol1 } else { noise.depol1 / 3.0 }, pauli_gate(q, j))).collect())); + } + if noise.dephasing > 0.0 { + steps.push(Step::Channel(vec![(1.0 - noise.dephasing, pauli_gate(q, 0)), (noise.dephasing, pauli_gate(q, 3))])); + } + if noise.damping > 0.0 { + let [k0, k1] = damping_kraus(q, noise.damping); + steps.push(Step::Channel(vec![(1.0, k0), (1.0, k1)])); + } + } + for u in registered { + let places: Vec> = match u.channel.arity() { + 1 => g.qubits.iter().map(|&q| vec![q]).collect(), + _ if k == 2 => vec![g.qubits.clone()], + _ => Vec::new(), + }; + let ks = u.channel.kraus(&u.values); + for at in places { + steps.push(Step::Channel(ks.iter().map(|m| (1.0, Gate { qubits: at.clone(), matrix: m.clone() })).collect())); + } + } + } + steps +} + pub(crate) fn merge(out: &mut Value, extra: Value) { if let (Some(o), Some(e)) = (out.as_object_mut(), extra.as_object()) { for (k, v) in e { diff --git a/wai/wai-quantum/src/engine/verdict.rs b/wai/wai-quantum/src/engine/verdict.rs index a533f9346..200635c0f 100644 --- a/wai/wai-quantum/src/engine/verdict.rs +++ b/wai/wai-quantum/src/engine/verdict.rs @@ -291,12 +291,17 @@ pub fn fit(intents: &[Value], program: &Program) -> Result<(), String> { /// (`n` qubits) takes, when each fits in `job_bytes`: what a plan admits /// beside the method's own. A sampled intent's run is planned as the job's. pub fn intent_memory(request: &Value, intents: &[Value], n: u32, noisy: bool, job_bytes: u64) -> u64 { + intent_memory_in(super::registry::Registry::builtins(), request, intents, n, noisy, job_bytes) +} + +/// [`intent_memory`], a sampled intent's run planned through `reg`. +pub(crate) fn intent_memory_in(reg: &super::registry::Registry, request: &Value, intents: &[Value], n: u32, noisy: bool, job_bytes: u64) -> u64 { intents .iter() .map(|i| { let need = if i.get("sampled").is_some() { let src = request.get("qasm").and_then(Value::as_str).unwrap_or(""); - catalog::plan(&sampled_request(i, request, src), job_bytes).map(|p| p.memory_bytes).unwrap_or(0) + reg.plan(&sampled_request(i, request, src), job_bytes).map(|p| p.memory_bytes).unwrap_or(0) } else if i.get("distribution").is_some() { if noisy { catalog::statevector_bytes(2 * n) } else if n <= DISTRIBUTION_MAX_QUBITS { catalog::statevector_bytes(n) } else { 0 } } else { @@ -338,9 +343,14 @@ fn sampled_request(intent: &Value, job: &Value, src: &str) -> Value { /// "alpha", "fit"}`, with the counts (up to [`MAX_LISTED`] outcomes). A /// statistical test is evidence, never proof. pub fn check_intent_for(intent: &Value, job: &Value, program: &Program, cap: u64) -> Result { + check_intent_for_in(super::registry::Registry::builtins(), intent, job, program, cap) +} + +/// [`check_intent_for`], a sampled intent's run planned and run through `reg`. +pub(crate) fn check_intent_for_in(reg: &super::registry::Registry, intent: &Value, job: &Value, program: &Program, cap: u64) -> Result { let kind = shape(intent).map_err(bad)?; if kind != "sampled" { - let noise = catalog::noise_of(job)?; + let noise = catalog::noise_in(reg, job)?.0; return check_intent(intent, program, noise.as_ref(), cap); } fit(std::slice::from_ref(intent), program)?; @@ -358,7 +368,7 @@ pub fn check_intent_for(intent: &Value, job: &Value, program: &Program, cap: u64 out["why"] = json!(why); Ok(out) }; - let plan = match catalog::plan(&req, cap) { + let plan = match reg.plan(&req, cap) { Ok(p) => p, Err(e) => return unchecked(out, format!("the run the test samples does not run in this job: {e}")), }; @@ -366,7 +376,7 @@ pub fn check_intent_for(intent: &Value, job: &Value, program: &Program, cap: u64 if plan.memory_bytes > cap { return unchecked(out, format!("the run the test samples needs about {}, more than this job was admitted with ({})", mib(plan.memory_bytes), mib(cap))); } - let result = super::run::run(&req, &plan)?; + let result = reg.run(&req, &plan)?; if result["omitted_shots"].as_u64().unwrap_or(0) > 0 { return unchecked(out, "the run has more distinct outcomes than its counts list, so the test cannot see them all".into()); } @@ -645,6 +655,11 @@ fn base(parsed: Value) -> Value { /// request's intents checked against the program it ran (the repaired one, /// before any compile) within `cap` bytes, the job's admitted memory. pub fn of_run(request: &Value, result: &Value, cap: u64) -> Result { + of_run_in(super::registry::Registry::builtins(), request, result, cap) +} + +/// [`of_run`] for a job run through `reg`. +pub(crate) fn of_run_in(reg: &super::registry::Registry, request: &Value, result: &Value, cap: u64) -> Result { let given = request.get("qasm").and_then(Value::as_str).ok_or_else(|| refusal::tag(refusal::NOT_A_JOB, "a job needs \"qasm\""))?; // The program as it ran: its parameters bound (catalog::bind_params), // then repaired when the job asked (the repair is of the bound text). @@ -665,7 +680,7 @@ pub fn of_run(request: &Value, result: &Value, cap: u64) -> Result, _>>()?.into(); + v["intent"] = intents.iter().map(|i| check_intent_for_in(reg, i, &ran, &reading.program, cap)).collect::, _>>()?.into(); v["critique"] = smells(src); if let Some(r) = result.pointer("/repaired/repairs") { v["repairs"] = r.clone(); diff --git a/wai/wai-quantum/src/quantum_noisy.rs b/wai/wai-quantum/src/quantum_noisy.rs index ca44a8f78..9a2d7ec1b 100644 --- a/wai/wai-quantum/src/quantum_noisy.rs +++ b/wai/wai-quantum/src/quantum_noisy.rs @@ -246,7 +246,7 @@ fn pauli(q: u32, p: u8) -> Gate { } /// The two Kraus operators of amplitude damping. -fn damping_kraus(q: u32, gamma: f64) -> [Gate; 2] { +pub(crate) fn damping_kraus(q: u32, gamma: f64) -> [Gate; 2] { let (z, o) = (complex(0.0, 0.0), complex(1.0, 0.0)); [ Gate { qubits: vec![q], matrix: vec![o, z, z, complex((1.0 - gamma).sqrt(), 0.0)] }, -- 2.54.0 (Apple Git-157) From 022b02f58efe0de51e3b2adb6a8681918f25ee3e Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 31 Oct 2026 02:54:16 -0400 Subject: [PATCH 2/4] wai-quantum-cli, wai-quantum-service: the engine's components listed on the command line, both MCP servers and the service `wai-quantum components` (`--json` with each descriptor), the MCP tool list_components on both servers (its answer tools.v1.json#/$defs/components), and the service's GET /v1 and quantum_service_info list every method, pass, noise channel and format with its version, origin and descriptor digest: the built-ins alone, nothing loaded from a request. The tool lists' tests, the smoke checks and the second schema checker take the new tool; the agent docs are blessed from both servers; the command's summary is translated; the READMEs and the changelog say what the extension points are. Co-Authored-By: Claude Opus 5.5 --- wai/CHANGELOG.md | 52 + wai/wai-quantum-cli/README.md | 3 +- wai/wai-quantum-cli/README.zh-CN.md | 3 +- wai/wai-quantum-cli/docs/llms-full.txt | 43 + wai/wai-quantum-cli/docs/llms.txt | 2 + wai/wai-quantum-cli/docs/local-mcp.json | 728 +++++++++++ wai/wai-quantum-cli/docs/service-mcp.json | 1075 +++++++++++++++++ wai/wai-quantum-cli/locales/zh-CN.json | 1 + .../skills/wai-quantum/reference/tools.md | 2 + wai/wai-quantum-cli/smoke.sh | 6 + wai/wai-quantum-cli/src/main.rs | 27 + wai/wai-quantum-cli/src/mcp.rs | 11 +- wai/wai-quantum-service/README.md | 4 +- .../schemas/service-tools.v1.json | 4 + wai/wai-quantum-service/smoke.sh | 2 +- wai/wai-quantum-service/src/http.rs | 1 + wai/wai-quantum-service/src/mcp.rs | 19 +- 17 files changed, 1976 insertions(+), 7 deletions(-) diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index da8b71de9..efec40ddc 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -363,6 +363,58 @@ wire format may change and such changes are recorded here. implementation, `channel_lineup_verify.py`, from the text alone, agrees on every vector, and a CI step runs both. +### wai-quantum: extension points, registration traits for methods, passes, noise channels and formats + +- **`engine::registry`**: four traits a component implements, and one registry the engine goes through. `Method` + (its name, version, accuracy classes and descriptor, whether it models noise, where `auto` tries it, its `plan` + that admits a job or refuses it and estimates its memory and work, and its `run`); `Pass` (the gates it takes and + gives, and its check: `Check::Ladder`, the equivalence ladder, or `Check::Fidelity`, a distance bound on dense + unitaries; never a hash); `Channel` (its parameters and ranges, its arity, its Kraus operators and its Pauli form + where it is one, and how each noisy method takes it); `FrontEnd` (a format read to OpenQASM 3 and written from the + engine's program). `Registry::with_builtins()` holds every built-in, each registered through the same trait: the + twelve methods and the pulse simulator, the eight compile passes, the five noise channels and the three formats. + `register(Component::…)` adds a host's own in its process; nothing is loaded from a file, a path or a request. +- **The engine through the registry.** `catalog::plan`, `catalog::read`, `run::run`, `compile::compile`, + `compile::call`, `formats::convert`, `formats::convert_json`, `formats::expand` and `verdict::of_run` keep their + signatures and run through the built-in registry; `Registry::plan`, `run`, `plan_and_run`, `job`, `compile`, + `compile_call`, `convert_json` and `expand` run through a host's. `auto` tries methods by rank (the built-ins at + 10, 20, 30, … in the order they always had), and the methods list is each method's own entry. A compile takes + `passes`, the names of passes to run after its level's and before twirling and decoupling; a job's `noise` takes + a registered channel by its value or its parameters (`{"name": {"p": …}}`), which the density matrix applies as + its Kraus operators after each gate. +- **The engine checks what a registered component gives.** A method's result must carry an accuracy class it + declared and leave the members the engine writes as they were; a pass's output is held to its input on the whole + circuit by the check it declares, within its bound and what is left of the ε budget, or left out (`rejected`) and + the program goes on as it was; a channel's Kraus operators are checked completely positive and trace preserving at + every job's values (`registry::validate`), and its Pauli form a distribution that is its operators' channel; a + format's program must read. The built-in channels and passes are held by tests to what they declare. +- **Identity.** A component's identity is its kind, name, version and the BLAKE3 digest of its descriptor + (`registry::digest_of`). A result made by built-ins alone is the result it always was: the eight pinned job + digests hold through the built-in registry and through one that holds a host's components beside them. A result + a registered component took part in carries `components`, every component that made it (its format, its + compile's passes, its noise's channels, its method), each registered one marked `"origin": "registered"` and + `"certified": false`, sealed by its receipt; so does a compile's answer (and its pass's record, `component`) and a + conversion's. A name a built-in holds is never registered. `components` is in `wai.quantum.job-result/1`, + `wai.quantum.compile-result/1` and the conversion's answer (optional), and `wai.quantum.tools/1` gains the + listing's `components` and `component`. +- **Refusal codes, a decade of their own:** `J060` a pass a request names that this host does not hold, `J061` a + noise channel the method cannot take (a method that models no noise, which was `J011`, and amplitude damping on + the stabilizer's frames among them), `J062` a channel whose operators fail the validity check, `J063` a + registration the registry does not take, `J064` a registered component's answer that breaks the contract, `J065` + an intent on a job whose noise names a registered channel. A method a job names that does not exist is `J010` + whatever else the job asks, and a channel's value that is not a number or an object of its parameters is refused + (`J003`), where it was read as no noise. +- **Surfaces:** `wai-quantum components` (with `--json`, each one's descriptor); the MCP tool `list_components` + on both servers; the job service lists its components in `GET /v1` and `quantum_service_info`, the built-ins + alone. +- **The example** `examples/extension_points.rs`: 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 its tests; `docs/extension-points.md` says how to write one, with a + channel registered and run. CI: `wai-quantum-registry.yml`, on both Linux architectures and under + wasm32-wasip2. +- `engine::fabric_measured`'s test of the measured circuit's job runs where the fabric is built (`quantum_fabric`), + so the contract workflow's feature set passes every test it builds. + ### wai-quantum: the state vector resident on the GPU, auto by measurement, and Pauli propagation on the GPU - **Resident** (`quantum_fabric::sv64`, host layout 2): a member runs a whole plan with the state on the device. diff --git a/wai/wai-quantum-cli/README.md b/wai/wai-quantum-cli/README.md index 705c4cd1f..d77c8a4e7 100644 --- a/wai/wai-quantum-cli/README.md +++ b/wai/wai-quantum-cli/README.md @@ -99,7 +99,7 @@ wai-quantum instance ghz-family.qasm --input n=3 | stabilizer_rank | Clifford+T circuits, as a sum of stabilizer states: amplitudes and expectation values, exact, or sampled with a stated bound (`--option terms=N` or `--option epsilon=E`) | | density_matrix, trajectories | noise | -`wai-quantum methods` lists each method's reach in a job's memory here (`--memory-gb`, default 16). +`wai-quantum methods` lists each method's reach in a job's memory here (`--memory-gb`, default 16). `wai-quantum components` lists every component the engine runs through (the methods, the compiler passes, the noise channels and the formats), each with its version and the digest of its descriptor, the identity a result is sealed with (`--json` gives each one's descriptor). The command line runs the built-ins alone; a host that embeds the engine registers its own ([the extension points](../wai-quantum/docs/extension-points.md)). `--fabric` says where the `statevector` method's state runs: `auto` (the default: on a member only where the committed measurements show it faster than this host's CPU for the job's size and fusion width, otherwise on the CPU; a job placed that way carries the rule and the measured row it rests on in its receipt), `cpu`, `accelerator` (the first accelerator member certified bit-exact here that holds the state, the next taking over if it fails during the run), or a member's id or key; given more than once, the state is split by its top qubits into one partition on each member named (a power of two of them; `cpu` is a member, and one may be named twice). Every member placed gives the CPU's answers byte for byte, and the result names where its state ran, with the member's certificate, in its receipt. `wai-quantum fabric` lists the members this host can place on, each with the most qubits it holds (and whether its kernels for gates on more than three qubits at once certified), the devices found and not placed, with why, `auto`'s rule, where it places on this host, and every measured row. A placement this host cannot honour is refused (`J013`), never made elsewhere. A `pauli` job takes `--fabric accelerator` or a member's id or key too: its sum of terms then runs on that member (the CUDA member, certified bit-exact against the CPU for it), with the CPU's answers byte for byte; `auto` keeps it on the CPU. @@ -414,6 +414,7 @@ wai-quantum verify sweep.json # the set's receipt, each item's, the r - `taste_rank` and `taste_next` (your taste, below) - `history_commit`, `history_changes`, `history_blame` and `history_log` (a program's history, below) - `list_methods` +- `list_components` (every method, pass, noise channel and format, each with its identity) - `qasm_diagnostics` (the reader's codes and the job layer's: all of them, or one by `code`) - `list_targets`, `show_target` and `run_on_target` (device targets, above); `run_quantum_circuit` and `plan_quantum_circuit` also take `target` - `providers_list`, `provider_estimate` and `run_on_provider` (your own hardware, above): `run_on_provider` says it can spend your money, and refuses a run that is not free without `confirm_cost: true` diff --git a/wai/wai-quantum-cli/README.zh-CN.md b/wai/wai-quantum-cli/README.zh-CN.md index 73134a7cf..4c63866aa 100644 --- a/wai/wai-quantum-cli/README.zh-CN.md +++ b/wai/wai-quantum-cli/README.zh-CN.md @@ -102,7 +102,7 @@ wai-quantum instance ghz-family.qasm --input n=3 | stabilizer_rank | Clifford+T 线路,写成稳定子态之和:振幅和期望值,精确,或以给定界抽样(`--option terms=N` 或 `--option epsilon=E`) | | density_matrix, trajectories | 噪声 | -`wai-quantum methods` 列出每种方法在本机一个作业的内存(`--memory-gb`,默认 16)内能达到的规模。 +`wai-quantum methods` 列出每种方法在本机一个作业的内存(`--memory-gb`,默认 16)内能达到的规模。`wai-quantum components` 列出引擎所经过的每个组件(方法、编译遍、噪声信道和格式),各附其版本及其描述符的摘要,即结果封存时所带的身份(`--json` 给出每个组件的描述符)。命令行只运行内置组件;嵌入引擎的宿主可以注册自己的组件([扩展点](../wai-quantum/docs/extension-points.md))。 `--fabric` 指定 `statevector` 方法的态在哪里运行:`auto`(默认:只在已提交的测量结果表明某个成员对该作业的规模和融合宽度比本机 CPU 更快时才放到该成员上,否则放在 CPU 上;这样放置的作业在其收据中带有规则及其所依据的测量行)、`cpu`、`accelerator`(本机第一个经认证逐位一致且能容纳该态的加速器成员;若它在运行中失败,由下一个接替),或成员的 id 或键;多次给出时,态按最高位量子比特划分为若干分区,每个所列成员一个分区(数量为 2 的幂;`cpu` 也是成员,同一成员可列两次)。放置到的每个成员都给出与 CPU 逐字节相同的答案,结果在其收据中指明态在哪里运行以及该成员的证书。`wai-quantum fabric` 列出本机可放置的成员(各附其可容纳的最多量子比特数,以及其一次作用于三个以上量子比特的门的内核是否通过认证)、找到但未放置的设备及原因、`auto` 的规则、它在本机的放置范围以及每个测量行。主机无法兑现的放置会被拒绝(`J013`),绝不改放别处。`pauli` 作业同样可以用 `--fabric accelerator` 或成员的 id 或键:其项之和随后在该成员上运行(CUDA 成员,已对照 CPU 认证为逐位一致),答案与 CPU 逐字节相同;`auto` 把它留在 CPU 上。 @@ -410,6 +410,7 @@ wai-quantum verify sweep.json # 程序集的回执、每一项的回 - `taste_rank` 和 `taste_next`(你的品味,见下文) - `history_commit`、`history_changes`、`history_blame` 和 `history_log`(程序的历史,见下文) - `list_methods` +- `list_components`(每个方法、编译遍、噪声信道和格式,各附其身份) - `qasm_diagnostics`(读取器的代码和作业层的代码:全部,或按 `code` 取其中一个) - `list_targets`、`show_target` 和 `run_on_target`(设备目标,见上文);`run_quantum_circuit` 和 `plan_quantum_circuit` 也接受 `target` - `providers_list`、`provider_estimate` 和 `run_on_provider`(你自己的硬件,见上文):`run_on_provider` 会声明它可能花你的钱,对不免费的运行,没有 `confirm_cost: true` 时会拒绝 diff --git a/wai/wai-quantum-cli/docs/llms-full.txt b/wai/wai-quantum-cli/docs/llms-full.txt index dc5710f73..606af7b1e 100644 --- a/wai/wai-quantum-cli/docs/llms-full.txt +++ b/wai/wai-quantum-cli/docs/llms-full.txt @@ -393,6 +393,10 @@ Each simulation method: its accuracy class, the circuits it takes, and the most The members of the fabric a statevector job can be placed on here (a job's `fabric`): the CPU and each accelerator found and certified bit-exact against the CPU in this process, each with its key, its certificate and the most qubits it holds; the devices found and not placed, and why; and the backends this build reaches. +### `list_components`: List the engine's components + +Every component the engine plans, runs, compiles and converts through here: the simulation methods, the compiler passes, the noise channels and the program formats, each with its version, its origin and the BLAKE3 digest of its descriptor, which is the identity a result is sealed with (a result a registered component took part in names each in its `components`). This server runs the built-ins alone. + ### `qasm_diagnostics`: The reader's diagnostic codes Every code the OpenQASM reader gives (each refusal and warning carries one, as `[Q103]` at the end of its message and as `code` in lint's JSON): its outcome (refused, lowered, carried, preserved or warned) and its meaning; and beside them, under job_codes, the codes of what is asked of a program (`[J011]`: the request, the method, an intent, a repair, the host). A code never changes meaning once released. Give `code` for one of either. @@ -775,6 +779,10 @@ Every code the OpenQASM reader gives (each refusal and warning carries one, as ` Parameters: - `code` (string): One code, as Q103 or J011. +### `list_components`: List the engine's components + +Every component the service plans, runs, compiles and converts through: the simulation methods, the compiler passes, the noise channels and the program formats, each with its version, its origin and the BLAKE3 digest of its descriptor, which is the identity a result is sealed with. The service runs the built-ins alone and never loads a component from a request. + ### `list_targets`: List the device targets The device targets the service knows by name: synthetic, made up in the shapes of published formats, each with its qubits, pairs, the gates a program compiles to, the spread of its T1 and readout errors, and its hash. A job may give its own target document instead. @@ -1605,6 +1613,9 @@ Commands: the CPU and each accelerator this build reaches that is found and certified bit-exact against the CPU, each with the most qubits it holds; the devices found and not placed, and why + components Every component the engine plans, runs, compiles and converts through (the simulation + methods, compiler passes, noise channels and program formats), each with its version + and the digest of its descriptor: the identity a result is sealed with diagnostics The diagnostic codes: the OpenQASM reader's (each with its outcome: refused, lowered, carried, preserved or warned) and the job layer's (refused or retry), each with its meaning @@ -3678,6 +3689,38 @@ Options: Print version ``` +### `wai-quantum components` + +```text +Every component the engine plans, runs, compiles and converts through (the simulation methods, +compiler passes, noise channels and program formats), each with its version and the digest of its +descriptor: the identity a result is sealed with + +Usage: wai-quantum components [OPTIONS] + +Options: + --input + A value for a generator's integer `input`, as n=5 (repeat for each input): the program is + read as its family's circuit at those values + + --json + Print the list as JSON, each component with its descriptor + + --lang + The language of the words the command line writes itself, en or zh-CN (by default the one + LANG names); results, codes and the engine's own messages stay in English + + --from + The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default + the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise + + -h, --help + Print help + + -V, --version + Print version +``` + ### `wai-quantum diagnostics` ```text diff --git a/wai/wai-quantum-cli/docs/llms.txt b/wai/wai-quantum-cli/docs/llms.txt index 933973900..fb8e4fc89 100644 --- a/wai/wai-quantum-cli/docs/llms.txt +++ b/wai/wai-quantum-cli/docs/llms.txt @@ -74,6 +74,7 @@ Runs on this machine, signed with the user's key; nothing leaves the machine. - `history_log`: A program's history - `list_methods`: List the methods - `list_fabric`: List the fabric's members +- `list_components`: List the engine's components - `qasm_diagnostics`: The reader's diagnostic codes - `list_targets`: List the device targets - `show_target`: Show a device target @@ -110,6 +111,7 @@ Runs on this machine, signed with the user's key; nothing leaves the machine. - `repair_qasm`: Repair a quantum program on its flagged lines - `quantum_service_info`: What the quantum service can do - `qasm_diagnostics`: The reader's diagnostic codes + - `list_components`: List the engine's components - `list_targets`: List the device targets - `show_target`: Show a device target - `run_on_target`: Run a program as a device would diff --git a/wai/wai-quantum-cli/docs/local-mcp.json b/wai/wai-quantum-cli/docs/local-mcp.json index f0cdc5cea..5ffc77539 100644 --- a/wai/wai-quantum-cli/docs/local-mcp.json +++ b/wai/wai-quantum-cli/docs/local-mcp.json @@ -1383,6 +1383,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -4964,6 +5011,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -14423,6 +14517,53 @@ } ], "description": "The output read back and held to the program read (or, for a program read into OpenQASM, written back to its format and read again), or null when no check was asked." + }, + "components": { + "type": "array", + "description": "The identities of the two formats, when a registered front end took part (engine::registry).", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } } } }, @@ -17814,6 +17955,49 @@ }, "check": { "type": "object" + }, + "component": { + "type": "object", + "description": "Beside a registered pass's record: its identity. Its check is the engine's, on the whole circuit (by: engine).", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } } } } @@ -17896,6 +18080,53 @@ "description": "The signature, 64 bytes in hex." } } + }, + "components": { + "type": "array", + "description": "The identity of every component that made the compile, when a registered pass took part (engine::registry): the program's format and each pass, each registered one marked so.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } } } }, @@ -27829,6 +28060,315 @@ ] } }, + { + "name": "list_components", + "title": "List the engine's components", + "description": "Every component the engine plans, runs, compiles and converts through here: the simulation methods, the compiler passes, the noise channels and the program formats, each with its version, its origin and the BLAKE3 digest of its descriptor, which is the identity a result is sealed with (a result a registered component took part in names each in its `components`). This server runs the built-ins alone.", + "inputSchema": { + "type": "object", + "properties": {} + }, + "outputSchema": { + "$id": "wai-quantum://schemas/list_components/1", + "type": "object", + "description": "Every component the engine plans, runs, compiles and converts through (engine::registry::Registry::components): the simulation methods, the compiler passes, the noise channels and the program formats, each in the registry's order with its identity and its descriptor. The command line, both MCP servers and the job service serve the built-ins alone.", + "anyOf": [ + { + "type": "object", + "description": "Every component the engine plans, runs, compiles and converts through (engine::registry::Registry::components): the simulation methods, the compiler passes, the noise channels and the program formats, each in the registry's order with its identity and its descriptor. The command line, both MCP servers and the job service serve the built-ins alone.", + "required": [ + "format", + "engine", + "methods", + "passes", + "channels", + "front_ends", + "registered" + ], + "properties": { + "format": { + "const": "wai.quantum.components/1" + }, + "engine": { + "type": "object", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "methods": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "passes": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "front_ends": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "registered": { + "type": "integer", + "minimum": 0, + "description": "How many are registered (a host's own); 0 for the built-ins alone." + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, { "name": "qasm_diagnostics", "title": "The reader's diagnostic codes", @@ -30638,6 +31178,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -34219,6 +34806,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -50038,6 +50672,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -53619,6 +54300,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." diff --git a/wai/wai-quantum-cli/docs/service-mcp.json b/wai/wai-quantum-cli/docs/service-mcp.json index f77583118..bde046afe 100644 --- a/wai/wai-quantum-cli/docs/service-mcp.json +++ b/wai/wai-quantum-cli/docs/service-mcp.json @@ -1389,6 +1389,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -4970,6 +5017,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -11105,6 +11199,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -14686,6 +14827,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -21851,6 +22039,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -25432,6 +25667,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -33607,6 +33889,53 @@ } ], "description": "The output read back and held to the program read (or, for a program read into OpenQASM, written back to its format and read again), or null when no check was asked." + }, + "components": { + "type": "array", + "description": "The identities of the two formats, when a registered front end took part (engine::registry).", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } } } }, @@ -37840,6 +38169,49 @@ }, "check": { "type": "object" + }, + "component": { + "type": "object", + "description": "Beside a registered pass's record: its identity. Its check is the engine's, on the whole circuit (by: engine).", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } } } } @@ -37922,6 +38294,53 @@ "description": "The signature, 64 bytes in hex." } } + }, + "components": { + "type": "array", + "description": "The identity of every component that made the compile, when a registered pass took part (engine::registry): the program's format and each pass, each registered one marked so.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } } } }, @@ -40922,6 +41341,256 @@ "receipt_keys": { "type": "string", "description": "Where the service publishes every key it has signed receipts with (the receipt key set, wai.quantum.receipt-keys/1)." + }, + "components": { + "type": "object", + "description": "Every component the service runs through (the built-ins alone), each with its identity: what list_components gives.", + "required": [ + "format", + "engine", + "methods", + "passes", + "channels", + "front_ends", + "registered" + ], + "properties": { + "format": { + "const": "wai.quantum.components/1" + }, + "engine": { + "type": "object", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "methods": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "passes": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "front_ends": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "registered": { + "type": "integer", + "minimum": 0, + "description": "How many are registered (a host's own); 0 for the built-ins alone." + } + } } } }, @@ -41232,6 +41901,318 @@ ] } }, + { + "name": "list_components", + "title": "List the engine's components", + "description": "Every component the service plans, runs, compiles and converts through: the simulation methods, the compiler passes, the noise channels and the program formats, each with its version, its origin and the BLAKE3 digest of its descriptor, which is the identity a result is sealed with. The service runs the built-ins alone and never loads a component from a request.", + "inputSchema": { + "type": "object", + "properties": {} + }, + "annotations": { + "readOnlyHint": true + }, + "outputSchema": { + "$id": "wai-quantum://schemas/list_components/1", + "type": "object", + "description": "Every component the engine plans, runs, compiles and converts through (engine::registry::Registry::components): the simulation methods, the compiler passes, the noise channels and the program formats, each in the registry's order with its identity and its descriptor. The command line, both MCP servers and the job service serve the built-ins alone.", + "anyOf": [ + { + "type": "object", + "description": "Every component the engine plans, runs, compiles and converts through (engine::registry::Registry::components): the simulation methods, the compiler passes, the noise channels and the program formats, each in the registry's order with its identity and its descriptor. The command line, both MCP servers and the job service serve the built-ins alone.", + "required": [ + "format", + "engine", + "methods", + "passes", + "channels", + "front_ends", + "registered" + ], + "properties": { + "format": { + "const": "wai.quantum.components/1" + }, + "engine": { + "type": "object", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "methods": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "passes": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "channels": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "front_ends": { + "type": "array", + "items": { + "type": "object", + "required": [ + "kind", + "name", + "version", + "digest", + "origin", + "descriptor" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + }, + "descriptor": { + "type": "object", + "description": "What the component declares of itself, as hashed into its digest: what it declares (declared) and what the engine reads of it (a method's accuracy classes, whether it models noise, where auto tries it; a pass's gates taken and given and its check; a channel's arity, placement and parameters; a format's title, extensions and profiles).", + "required": [ + "declared" + ] + } + } + } + }, + "registered": { + "type": "integer", + "minimum": 0, + "description": "How many are registered (a host's own); 0 for the built-ins alone." + } + } + }, + { + "type": "object", + "description": "A refusal as data: the message, every code in it (the last is the code), its outcome and meaning as the code's table gives them. A host that refuses for its own reasons gives the error alone.", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": [ + "string", + "null" + ] + }, + "codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "outcome": { + "enum": [ + "refused", + "retry", + "failed", + "lowered", + "carried", + "preserved", + "warned", + "dropped", + "defaulted" + ] + }, + "meaning": { + "type": "string" + } + } + } + ] + } + }, { "name": "list_targets", "title": "List the device targets", @@ -43453,6 +44434,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." @@ -47034,6 +48062,53 @@ "const": "wai.quantum.job-result/1", "description": "The schema the result follows." }, + "components": { + "type": "array", + "description": "The identity of every component that made the result, when a registered one took part (engine::registry): its program's format, the passes its compile ran, the noise channels it names, its method; each registered one marked so. Sealed by the receipt with the rest.", + "items": { + "type": "object", + "description": "One component that made the result (engine::registry): its kind, name and version, the BLAKE3 digest of its descriptor, and whether it is built in. A result carries these only when a registered component took part; one made by built-ins alone names them as it always has (its method, its compile's passes, its noise's channels and the engine's version), the digests of that version listed by the components list.", + "required": [ + "kind", + "name", + "version", + "digest", + "origin" + ], + "properties": { + "kind": { + "enum": [ + "method", + "pass", + "channel", + "front_end" + ] + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "digest": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "BLAKE3 of the component's descriptor in its canonical JSON (engine::registry::digest_of)." + }, + "origin": { + "enum": [ + "built-in", + "registered" + ], + "description": "built-in: the engine's own; registered: a host's own, added in its process, not part of the engine." + }, + "certified": { + "const": false, + "description": "Beside a registered component: the engine does not certify it." + } + } + } + }, "method": { "type": "string", "description": "The method that ran the job (provider for a run on the person's own provider)." diff --git a/wai/wai-quantum-cli/locales/zh-CN.json b/wai/wai-quantum-cli/locales/zh-CN.json index 1f50ff8e6..f5ae42012 100644 --- a/wai/wai-quantum-cli/locales/zh-CN.json +++ b/wai/wai-quantum-cli/locales/zh-CN.json @@ -63,6 +63,7 @@ "key": { "source": "Show the key this user signs receipts with", "text": "显示本用户为回执签名所用的密钥" }, "methods": { "source": "Each method, what it takes, and how far it reaches in a job's memory here", "text": "每种方法、它接受什么,以及在本机作业内存中它能达到的规模" }, "fabric": { "source": "The members of the fabric a statevector job can be placed on here (`run --fabric`): the CPU and each accelerator this build reaches that is found and certified bit-exact against the CPU, each with the most qubits it holds; the devices found and not placed, and why", "text": "本机上 statevector 作业可放置其上的计算结构成员(`run --fabric`):CPU,以及本构建可达、已找到并经认证与 CPU 逐位一致的每个加速器,各附其可容纳的最多量子比特数;以及找到但未放置的设备及其原因" }, + "components": { "source": "Every component the engine plans, runs, compiles and converts through (the simulation methods, compiler passes, noise channels and program formats), each with its version and the digest of its descriptor: the identity a result is sealed with", "text": "引擎规划、运行、编译和转换所经过的每个组件(模拟方法、编译遍、噪声信道和程序格式),各附其版本及其描述符的摘要:结果封存时所带的身份" }, "diagnostics": { "source": "The diagnostic codes: the OpenQASM reader's (each with its outcome: refused, lowered, carried, preserved or warned) and the job layer's (refused or retry), each with its meaning", "text": "诊断代码:OpenQASM 读取器的代码(各附其结果:refused、lowered、carried、preserved 或 warned)和作业层的代码(refused 或 retry),各附其含义" }, "mcp": { "source": "Serve run, plan, lint, critique, the equivalence check, improve, the classical shortcuts, compile, draw, the debugger and verify to an agent over standard input and output (MCP), with resources and prompts; `mcp config` and `mcp install` register it with a client", "text": "通过标准输入输出(MCP)向智能体提供 run、plan、lint、critique、等价检查、improve、经典捷径、compile、draw、调试器和 verify,以及资源和提示词;`mcp config` 和 `mcp install` 把它注册到客户端" }, "fmt": { "source": "Format programs in one layout: a statement to a line, blocks indented two spaces, a space around each operator, comments and blank lines kept; a program whose brackets do not match is left as it is, with its line", "text": "按统一的版式格式化程序:每行一条语句,块缩进两个空格,运算符两侧各留一个空格,保留注释和空行;括号不匹配的程序保持原样,并给出所在的行" }, diff --git a/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md b/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md index e808b30b0..14d4f2905 100644 --- a/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md +++ b/wai/wai-quantum-cli/skills/wai-quantum/reference/tools.md @@ -37,6 +37,7 @@ Generated by `wai-quantum docs skill` from the servers' own lists; `wai-quantum - `history_log`: A program's history - `list_methods`: List the methods - `list_fabric`: List the fabric's members +- `list_components`: List the engine's components - `qasm_diagnostics`: The reader's diagnostic codes - `list_targets`: List the device targets - `show_target`: Show a device target @@ -72,6 +73,7 @@ Generated by `wai-quantum docs skill` from the servers' own lists; `wai-quantum - `repair_qasm`: Repair a quantum program on its flagged lines - `quantum_service_info`: What the quantum service can do - `qasm_diagnostics`: The reader's diagnostic codes +- `list_components`: List the engine's components - `list_targets`: List the device targets - `show_target`: Show a device target - `run_on_target`: Run a program as a device would diff --git a/wai/wai-quantum-cli/smoke.sh b/wai/wai-quantum-cli/smoke.sh index 925ada5fc..49dfa1ec1 100644 --- a/wai/wai-quantum-cli/smoke.sh +++ b/wai/wai-quantum-cli/smoke.sh @@ -44,6 +44,12 @@ has "$tmp/codes" "Q103 refused" python3 -c 'import json,sys; t=json.load(open(sys.argv[1])); assert any(c["code"]=="Q103" and c["outcome"]=="refused" for c in t["codes"]), t' "$tmp/codes.json" echo "lint: ok, with codes" +"$W" components > "$tmp/components" +has "$tmp/components" "density_matrix" +"$W" components --json > "$tmp/components.json" +python3 -c 'import json,sys; c=json.load(open(sys.argv[1])); names=lambda k: [x["name"] for x in c[k]]; assert c["format"]=="wai.quantum.components/1" and c["registered"]==0, c; assert names("front_ends")==["qasm","qir","jeff"] and "exact" in names("methods") and "approximate" in names("passes") and names("channels")[0]=="depolarizing_1q", c; assert all(x["origin"]=="built-in" and len(x["digest"])==64 for k in ("methods","passes","channels","front_ends") for x in c[k]), c' "$tmp/components.json" +echo "components: ok, every built-in with its identity" + "$W" plan "$tmp/ghz.qasm" --json > "$tmp/plan.json" python3 -c 'import json,sys; p=json.load(open(sys.argv[1])); assert p["method"]=="stabilizer" and p["qubits"]==3, p' "$tmp/plan.json" echo "plan: ok, stabilizer" diff --git a/wai/wai-quantum-cli/src/main.rs b/wai/wai-quantum-cli/src/main.rs index bca06b7c6..7e192499d 100644 --- a/wai/wai-quantum-cli/src/main.rs +++ b/wai/wai-quantum-cli/src/main.rs @@ -339,6 +339,12 @@ enum Command { #[arg(long, default_value_t = DEFAULT_MEMORY_GB)] memory_gb: u64, }, + /// Every component the engine plans, runs, compiles and converts through (the simulation methods, compiler passes, noise channels and program formats), each with its version and the digest of its descriptor: the identity a result is sealed with + Components { + /// Print the list as JSON, each component with its descriptor + #[arg(long)] + json: bool, + }, /// The diagnostic codes: the OpenQASM reader's (each with its outcome: refused, lowered, carried, preserved or warned) and the job layer's (refused or retry), each with its meaning Diagnostics { /// Print the table as JSON @@ -607,6 +613,19 @@ fn option_arg(s: &str) -> Result<(String, Value), String> { Ok((k.to_string(), value)) } +/// The components list, one line each: its kind, name, version, where it +/// comes from and its digest. +fn components_text(c: &Value) -> String { + let mut s = format!("engine {} {}\n", c["engine"]["name"].as_str().unwrap_or(""), c["engine"]["version"].as_str().unwrap_or("")); + for (kind, title) in [("methods", "methods"), ("passes", "passes"), ("channels", "noise channels"), ("front_ends", "formats")] { + s.push_str(&format!("\n{title}\n")); + for x in c[kind].as_array().into_iter().flatten() { + s.push_str(&format!(" {:<20} {:<10} {:<10} {}\n", x["name"].as_str().unwrap_or(""), x["version"].as_str().unwrap_or(""), x["origin"].as_str().unwrap_or(""), x["digest"].as_str().unwrap_or(""))); + } + } + s +} + /// The fabric's members as `wai-quantum fabric` prints them. fn fabric_text(f: &Value) -> String { let s = |v: &Value| v.as_str().unwrap_or("").to_string(); @@ -1449,6 +1468,14 @@ fn main() { print!("{}", fabric_text(&f)); } } + Command::Components { json } => { + let c = wai_quantum::engine::registry::Registry::builtins().components(); + if json { + println!("{c}"); + } else { + print!("{}", components_text(&c)); + } + } Command::Diagnostics { json } => { if json { println!("{}", job::diagnostic_codes()); diff --git a/wai/wai-quantum-cli/src/mcp.rs b/wai/wai-quantum-cli/src/mcp.rs index 83f7ca3f1..c965c2649 100644 --- a/wai/wai-quantum-cli/src/mcp.rs +++ b/wai/wai-quantum-cli/src/mcp.rs @@ -73,6 +73,7 @@ fn output_of(tool: &str) -> Option<&'static str> { "draw_circuit" => "tools.v1.json#/$defs/draw", "list_methods" => "tools.v1.json#/$defs/methods", "list_fabric" => "tools.v1.json#/$defs/fabric_members", + "list_components" => "tools.v1.json#/$defs/components", "rewrite_qasm" => "tools.v1.json#/$defs/rewrite", "novelty_qasm" => "tools.v1.json#/$defs/novelty", "discover_evaluate" => "tools.v1.json#/$defs/discover_evaluate", @@ -453,6 +454,12 @@ fn core_tools() -> Value { "description": "The members of the fabric a statevector job can be placed on here (a job's `fabric`): the CPU and each accelerator found and certified bit-exact against the CPU in this process, each with its key, its certificate and the most qubits it holds; the devices found and not placed, and why; and the backends this build reaches.", "inputSchema": { "type": "object", "properties": {} }, }, + { + "name": "list_components", + "title": "List the engine's components", + "description": "Every component the engine plans, runs, compiles and converts through here: the simulation methods, the compiler passes, the noise channels and the program formats, each with its version, its origin and the BLAKE3 digest of its descriptor, which is the identity a result is sealed with (a result a registered component took part in names each in its `components`). This server runs the built-ins alone.", + "inputSchema": { "type": "object", "properties": {} }, + }, { "name": "qasm_diagnostics", "title": "The reader's diagnostic codes", @@ -590,6 +597,7 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli } "list_methods" => Ok(json!({ "methods": catalog::describe(memory_gb << 30) })), "list_fabric" => Ok(wai_quantum::engine::fabric::list(memory_gb << 30)), + "list_components" => Ok(wai_quantum::engine::registry::Registry::builtins().components()), "qasm_diagnostics" => match raw.get("code").and_then(Value::as_str) { None => Ok(job::diagnostic_codes()), Some(c) => job::diagnostic_code(c), @@ -922,7 +930,7 @@ mod tests { let list = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }), 1, &key(), &mut Client::default()).unwrap(); let names: Vec<&str> = list["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); #[allow(unused_mut)] - let mut want = vec!["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "repair_qasm", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "draw_circuit", "debug_step", "debug_branches", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "rewrite_qasm", "converge_cohort", "converge_history", "study_measure", "study_cohort", "taste_rank", "taste_next", "history_commit", "history_changes", "history_blame", "history_log", "list_methods", "list_fabric", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]; + let mut want = vec!["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "repair_qasm", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "draw_circuit", "debug_step", "debug_branches", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "rewrite_qasm", "converge_cohort", "converge_history", "study_measure", "study_cohort", "taste_rank", "taste_next", "history_commit", "history_changes", "history_blame", "history_log", "list_methods", "list_fabric", "list_components", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]; #[cfg(feature = "providers")] want.extend(["providers_list", "provider_estimate", "run_on_provider"]); assert_eq!(names, want); @@ -1450,6 +1458,7 @@ mod tests { call_tool("verify_quantum_result", json!({ "job": job }), false, &mut called, &mut hold); call_tool("list_methods", json!({}), false, &mut called, &mut hold); call_tool("list_fabric", json!({}), false, &mut called, &mut hold); + call_tool("list_components", json!({}), false, &mut called, &mut hold); // A state split over the CPU, as the fabric places it: the answers the // CPU gives, and where it ran in the result; a member that is not here // is refused. diff --git a/wai/wai-quantum-service/README.md b/wai/wai-quantum-service/README.md index 913a04ad5..691ee365c 100644 --- a/wai/wai-quantum-service/README.md +++ b/wai/wai-quantum-service/README.md @@ -206,7 +206,7 @@ The six checks answer at once, in the service's own process. Each counts against ## For agents -`POST /mcp` serves the service to an agent as Model Context Protocol tools: `run_quantum_circuit`, `run_quantum_set` (a set or a sweep), `dry_run_quantum_job` (the plan, running nothing), `get_quantum_job`, `verify_quantum_receipt`, `critique_qasm`, `check_equivalent`, `convert_program`, `improve_qasm`, `classical_shortcuts`, `compile_qasm`, `estimate_resources`, `repair_qasm`, `quantum_service_info`, `qasm_diagnostics`, `list_targets`, `show_target`, `run_on_target`, `quantum_circuit_examples`, `qec_code`, `qec_decode`, `qec_threshold`, `pulse_schedule`, `pulse_check`, `gradient_qasm`, `optimize_qasm`, `vqe_ground_state`, `qaoa_maxcut` and `build_algorithm`. A run it makes asks for its verdict, so the request its receipt seals is the job's own `request`; every answer about a program carries its verdict, every refusal its codes, and every tool a versioned output schema. `verify_quantum_receipt` re-runs an estimate. It also serves the engine's resources and prompts, the same as the command line's own server (`wai-quantum mcp`): +`POST /mcp` serves the service to an agent as Model Context Protocol tools: `run_quantum_circuit`, `run_quantum_set` (a set or a sweep), `dry_run_quantum_job` (the plan, running nothing), `get_quantum_job`, `verify_quantum_receipt`, `critique_qasm`, `check_equivalent`, `convert_program`, `improve_qasm`, `classical_shortcuts`, `compile_qasm`, `estimate_resources`, `repair_qasm`, `quantum_service_info`, `qasm_diagnostics`, `list_components`, `list_targets`, `show_target`, `run_on_target`, `quantum_circuit_examples`, `qec_code`, `qec_decode`, `qec_threshold`, `pulse_schedule`, `pulse_check`, `gradient_qasm`, `optimize_qasm`, `vqe_ground_state`, `qaoa_maxcut` and `build_algorithm`. A run it makes asks for its verdict, so the request its receipt seals is the job's own `request`; every answer about a program carries its verdict, every refusal its codes, and every tool a versioned output schema. `verify_quantum_receipt` re-runs an estimate. It also serves the engine's resources and prompts, the same as the command line's own server (`wai-quantum mcp`): - resources: the language the engine reads (`wai-quantum://docs/language`), its diagnostics, the methods with their accuracy classes (their reach in a job's memory here), and worked examples, each with what it must do; - prompts: writing and verifying a circuit for an intent, explaining a result and its receipt, and repairing a program from its diagnostics. Each names this server's own tools. @@ -262,6 +262,8 @@ Where the service cannot read a count (another system, a zone its user may not r ## Versions +`GET /v1` and `quantum_service_info` list the engine's components (`components`, `wai.quantum.components/1`, which `list_components` also gives): every method, compiler pass, noise channel and format the service runs through, each with its version and the digest of its descriptor. The service runs the built-ins alone and never loads a component from a request ([the extension points](../wai-quantum/docs/extension-points.md)). + `GET /v1` names the service's version and gives `versions` (`wai.quantum.versions/1`): the engine's version, the service's, the schema of each document it writes and the MCP protocol versions it speaks. The MCP server gives the same in `initialize` (`_meta["science.transaction.wai/versions"]`) and in `quantum_service_info`. Every result names the engine that made it and its schema (`engine`, `schema`), inside what its receipt seals. Each MCP tool's `outputSchema` is its answer's schema; this server's own are in [`schemas/service-tools.v1.json`](schemas/service-tools.v1.json), the job's and the engine's in [`wai-quantum/schemas/`](../wai-quantum/schemas). What is stable and for how long: [the policy](https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-quantum/VERSIONING.md); what is deprecated: [the deprecations](https://git.transaction.science/Transaction-Science/open-standards/src/branch/main/wai/wai-quantum/DEPRECATIONS.md). ## Running it diff --git a/wai/wai-quantum-service/schemas/service-tools.v1.json b/wai/wai-quantum-service/schemas/service-tools.v1.json index f629f3cf5..20428c117 100644 --- a/wai/wai-quantum-service/schemas/service-tools.v1.json +++ b/wai/wai-quantum-service/schemas/service-tools.v1.json @@ -181,6 +181,10 @@ "receipt_keys": { "type": "string", "description": "Where the service publishes every key it has signed receipts with (the receipt key set, wai.quantum.receipt-keys/1)." + }, + "components": { + "$ref": "https://git.transaction.science/Transaction-Science/open-standards/raw/branch/main/wai/wai-quantum/schemas/tools.v1.json#/$defs/components", + "description": "Every component the service runs through (the built-ins alone), each with its identity: what list_components gives." } } }, diff --git a/wai/wai-quantum-service/smoke.sh b/wai/wai-quantum-service/smoke.sh index ea9ab5b3c..7c2208019 100644 --- a/wai/wai-quantum-service/smoke.sh +++ b/wai/wai-quantum-service/smoke.sh @@ -248,7 +248,7 @@ mcp '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' > "$tmp/mcp-tools.json" py 'import json,sys i=json.load(open(sys.argv[1]))["result"]; t=json.load(open(sys.argv[2]))["result"]["tools"] assert i["protocolVersion"]=="2025-06-18" and "tools" in i["capabilities"], i -names={x["name"] for x in t}; assert {"run_quantum_circuit","run_quantum_set","dry_run_quantum_job","get_quantum_job","verify_quantum_receipt","quantum_service_info","quantum_circuit_examples","critique_qasm","check_equivalent","improve_qasm","classical_shortcuts","compile_qasm","estimate_resources","qec_code","qec_decode","qec_threshold","gradient_qasm","optimize_qasm","vqe_ground_state","qaoa_maxcut","build_algorithm","pulse_schedule","pulse_check"} <= names, names +names={x["name"] for x in t}; assert {"run_quantum_circuit","run_quantum_set","dry_run_quantum_job","get_quantum_job","verify_quantum_receipt","quantum_service_info","quantum_circuit_examples","critique_qasm","check_equivalent","improve_qasm","classical_shortcuts","compile_qasm","estimate_resources","qec_code","qec_decode","qec_threshold","gradient_qasm","optimize_qasm","vqe_ground_state","qaoa_maxcut","build_algorithm","pulse_schedule","pulse_check","list_components"} <= names, names assert sys.argv[3]=="202", sys.argv[3] print("mcp: ok, initialized,", len(t), "tools")' "$tmp/mcp-init.json" "$tmp/mcp-tools.json" "$code" py 'import json,sys diff --git a/wai/wai-quantum-service/src/http.rs b/wai/wai-quantum-service/src/http.rs index 06d624e8d..bc56b252e 100644 --- a/wai/wai-quantum-service/src/http.rs +++ b/wai/wai-quantum-service/src/http.rs @@ -193,6 +193,7 @@ async fn describe(State(app): State) -> Response { "versions": crate::mcp::versions(), "methods": catalog::describe(c.job_mib << 20), "accuracy_classes": catalog::accuracy_classes(), + "components": wai_quantum::engine::registry::Registry::builtins().components(), "limits": { "body_bytes": MAX_BODY, "job_memory_bytes": c.job_mib << 20, diff --git a/wai/wai-quantum-service/src/mcp.rs b/wai/wai-quantum-service/src/mcp.rs index b5b6d42ed..5f0fb49a9 100644 --- a/wai/wai-quantum-service/src/mcp.rs +++ b/wai/wai-quantum-service/src/mcp.rs @@ -75,6 +75,7 @@ fn output_of(tool: &str) -> Option<&'static str> { "list_targets" => "service-tools.v1.json#/$defs/targets", "show_target" => "tools.v1.json#/$defs/target_summary", "qasm_diagnostics" => "service-tools.v1.json#/$defs/diagnostics", + "list_components" => "tools.v1.json#/$defs/components", "qec_code" | "qec_decode" | "qec_threshold" => "qec.v1.json", "pulse_schedule" | "pulse_check" => "pulse.v1.json", "gradient_qasm" | "optimize_qasm" | "vqe_ground_state" | "qaoa_maxcut" | "build_algorithm" => "algorithms.v1.json", @@ -354,6 +355,13 @@ fn tool_list() -> Value { "inputSchema": { "type": "object", "properties": { "code": { "type": "string", "description": "One code, as Q103 or J011." } } }, "annotations": { "readOnlyHint": true } }, + { + "name": "list_components", + "title": "List the engine's components", + "description": "Every component the service plans, runs, compiles and converts through: the simulation methods, the compiler passes, the noise channels and the program formats, each with its version, its origin and the BLAKE3 digest of its descriptor, which is the identity a result is sealed with. The service runs the built-ins alone and never loads a component from a request.", + "inputSchema": { "type": "object", "properties": {} }, + "annotations": { "readOnlyHint": true } + }, { "name": "list_targets", "title": "List the device targets", @@ -666,6 +674,7 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu "receipt_keys": format!("{base}/.well-known/wai-quantum-receipt-keys"), "api": discovery::index(base, &app.console), "versions": versions(), + "components": wai_quantum::engine::registry::Registry::builtins().components(), }); let lines: Vec = info["methods"] .as_array() @@ -687,6 +696,11 @@ async fn call(app: &App, who: &str, name: &str, args: Value, base: &str) -> Valu Err(_) => tool_result(format!("{c}: no such code; call qasm_diagnostics with no code for them all"), json!({ "error": "no such code" }), true), }, }, + "list_components" => { + let c = wai_quantum::engine::registry::Registry::builtins().components(); + let n = |k: &str| c[k].as_array().map_or(0, Vec::len); + tool_result(format!("{} methods, {} passes, {} noise channels and {} formats, all built in (wai-quantum {}), each with its version and the digest of its descriptor", n("methods"), n("passes"), n("channels"), n("front_ends"), c["engine"]["version"].as_str().unwrap_or("")), c, false) + } "quantum_circuit_examples" => { let ex = discovery::examples(); let lines: Vec = ex.as_array().into_iter().flatten().map(|e| format!("{} — {}: {}", e["name"].as_str().unwrap_or(""), e["title"].as_str().unwrap_or(""), e["shows"].as_str().unwrap_or(""))).collect(); @@ -832,7 +846,7 @@ mod tests { #[test] fn the_tools_are_these() { let names: Vec = tools().as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap().to_string()).collect(); - assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]); + assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_components", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]); let run = &tools()[0]["inputSchema"]["properties"]["compile"]["properties"]; assert!(run.get("epsilon").is_some() && run.get("basis").is_some() && run.get("target").is_none(), "the compile's gate set is its basis; target is a device: {run}"); } @@ -847,7 +861,7 @@ mod tests { } let all = tools(); let names: Vec<&str> = all.as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect(); - assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]); + assert_eq!(names, ["run_quantum_circuit", "run_quantum_set", "dry_run_quantum_job", "get_quantum_job", "verify_quantum_receipt", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "repair_qasm", "quantum_service_info", "qasm_diagnostics", "list_components", "list_targets", "show_target", "run_on_target", "quantum_circuit_examples", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"]); // Each declares its output, versioned; a tool that reads a program requires its verdict. for t in all.as_array().unwrap() { let (name, o) = (t["name"].as_str().unwrap(), &t["outputSchema"]); @@ -995,6 +1009,7 @@ mod tests { ("repair_qasm", json!({ "qasm": "OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n", "proposed": "OPENQASM 3.0;\nqubit[2] q;\nh q[0];\n" }), true), ("estimate_resources", json!({ "counts": { "numQubits": 10, "cczCount": 20, "measurementCount": 10 } }), false), ("estimate_resources", json!({ "qasm": "qubit[1] q;\nh q[0];\n", "budget": 3 }), true), + ("list_components", json!({}), false), ("list_targets", json!({}), false), ("show_target", json!({ "target": "synthetic-heavy-hex-27", "view": true }), false), ("show_target", json!({ "target": "a-device-elsewhere" }), true), -- 2.54.0 (Apple Git-157) From 223ed634bf7d584dc91b8e4dfb298d864d627ea6 Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 31 Oct 2026 03:08:19 -0400 Subject: [PATCH 3/4] 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 --- wai/wai-quantum/docs/extension-points.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/wai/wai-quantum/docs/extension-points.md b/wai/wai-quantum/docs/extension-points.md index f98c829f7..4f07e9718 100644 --- a/wai/wai-quantum/docs/extension-points.md +++ b/wai/wai-quantum/docs/extension-points.md @@ -95,8 +95,8 @@ circuits that permute basis states), a pass (`cancel_pairs`), a channel alone, registered, and run end to end with their tests: ```sh norun=ci:wai-quantum-registry.yml -cargo run --release --features quantum_engine,quantum_receipt --example extension_points -cargo test --release --features quantum_engine,quantum_receipt --example extension_points +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 -- 2.54.0 (Apple Git-157) From 4f901247c7ac3f326e95e5e04f13412aa19a579c Mon Sep 17 00:00:00 2001 From: Transaction Science Date: Sat, 31 Oct 2026 03:14:50 -0400 Subject: [PATCH 4/4] wai-quantum engine::refusal: J011 no longer names noise, which J061 now covers Noise a method cannot take is refused with J061 since the registry, so J011's meaning stops listing it and points there; the docs page's table, which a test holds to the code's, says the same. Co-Authored-By: Claude Opus 5.5 --- wai/wai-quantum/docs/job-refusals.md | 2 +- wai/wai-quantum/src/engine/refusal.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wai/wai-quantum/docs/job-refusals.md b/wai/wai-quantum/docs/job-refusals.md index d7abe0b34..a0c7962dd 100644 --- a/wai/wai-quantum/docs/job-refusals.md +++ b/wai/wai-quantum/docs/job-refusals.md @@ -62,7 +62,7 @@ browser engine. A test holds this page to the code's table. | `J008` | refused | The mitigation the job asked for does not read or cannot be done: no noise model, no observables (no shots, for readout), a program that measures in the middle, more qubits or work than it takes, a device target beside it, or options that do not read (the message says which). | | `J009` | refused | An error-correction request that does not read or cannot be done: an unknown code or decoder, a decoder the code does not admit, a syndrome or an error that does not fit the code, circuit text or a model that does not read, or sizes past the limits (the message says which). | | `J010` | refused | A method that does not exist: the methods list names them. | -| `J011` | refused | The method named cannot take this job: a gate it does not hold, more qubits, memory or work than it takes here, noise it does not model, or an answer it does not give; the message says which, and `auto` picks a method that can. | +| `J011` | refused | The method named cannot take this job: a gate it does not hold, more qubits, memory or work than it takes here, or an answer it does not give (noise it cannot take is `J061`); the message says which, and `auto` picks a method that can. | | `J012` | refused | No method takes this job: each method's reason is listed, so the job can be changed to suit one. | | `J013` | refused | A placement on the fabric (`fabric`) this host cannot honour: no member by that name, no certified accelerator member here, a state or a partition larger than a member holds, a partition count that is not a power of two, or a method or program the fabric's members do not run (the message says which, and what is here). | | `J020` | refused | An intent that does not read: not one of `distribution`, `equivalent` or `prepares`, a distribution whose outcomes are not the program's bits or whose probabilities do not sum to 1, or a reference program that does not read. | diff --git a/wai/wai-quantum/src/engine/refusal.rs b/wai/wai-quantum/src/engine/refusal.rs index 5a09db4e1..70adf6ac6 100644 --- a/wai/wai-quantum/src/engine/refusal.rs +++ b/wai/wai-quantum/src/engine/refusal.rs @@ -85,7 +85,7 @@ table! { // The method. UNKNOWN_METHOD = "J010", Refused, "A method that does not exist: the methods list names them."; - METHOD_REFUSES = "J011", Refused, "The method named cannot take this job: a gate it does not hold, more qubits, memory or work than it takes here, noise it does not model, or an answer it does not give; the message says which, and `auto` picks a method that can."; + METHOD_REFUSES = "J011", Refused, "The method named cannot take this job: a gate it does not hold, more qubits, memory or work than it takes here, or an answer it does not give (noise it cannot take is `J061`); the message says which, and `auto` picks a method that can."; NO_METHOD = "J012", Refused, "No method takes this job: each method's reason is listed, so the job can be changed to suit one."; FABRIC = "J013", Refused, "A placement on the fabric (`fabric`) this host cannot honour: no member by that name, no certified accelerator member here, a state or a partition larger than a member holds, a partition count that is not a power of two, or a method or program the fabric's members do not run (the message says which, and what is here)."; -- 2.54.0 (Apple Git-157)