diff --git a/.github/workflows/wai-conformance.yml b/.github/workflows/wai-conformance.yml index 2cc9cfe29..89f98ba79 100644 --- a/.github/workflows/wai-conformance.yml +++ b/.github/workflows/wai-conformance.yml @@ -62,6 +62,8 @@ on: - 'wai/extensions/http-delivery.md' - 'wai/extensions/moq-streaming-format.md' - 'wai/extensions/jwp-receipts.md' + # The quantum referee requires this extension's Appendix B to state its phase tables. + - 'wai/extensions/quantum-sim.md' - 'wai/wai-web/**' # The audio provenance check below reads the corpora above with this tool. - 'wai/tools/wai_audio_provenance_check.py' @@ -95,6 +97,8 @@ on: - 'wai/extensions/http-delivery.md' - 'wai/extensions/moq-streaming-format.md' - 'wai/extensions/jwp-receipts.md' + # The quantum referee requires this extension's Appendix B to state its phase tables. + - 'wai/extensions/quantum-sim.md' - 'wai/wai-web/**' # The audio provenance check below reads the corpora above with this tool. - 'wai/tools/wai_audio_provenance_check.py' @@ -457,6 +461,94 @@ jobs: v staged-measure wai_stage_measure ../staged-measure-conformance v telemetry wai_telemetry ../telemetry-conformance + # wai-quantum and the quantum corpus under wasm32-wasip2, run by wasmtime. On wasm32 + # usize is 32 bits, so arithmetic that cannot overflow on a 64-bit host can there: a + # section's offset plus its length, a capacity reserved from a count a file claims, a + # shift by a qubit count. The builds are debug builds, so overflow checks are on and + # such an overflow panics instead of wrapping. Three steps: + # 1. tests/bit_identity.rs, which pins one result of each f64 layer, so the wasm32 + # build must give the bits every native leg gives; + # 2. the library's unit tests in the modules that hold the circuit core, its + # container, its JSON reader and its pins (quantum, ijson, receipt_pins, + # quantum_fabric, quantum_source, quantum_channel, quantum_qudit, quantum_receipt), + # each required to have run; + # 3. wai_quantum_vectors verify over all four trees of quantum-conformance/ (both + # capabilities, the reject corpus and the accept corpus), the same check the matrix + # above runs natively. + # One leg: wasm32 is the same machine on every host. Linux, so it does not queue on the + # single macOS runner. + quantum-wasip2: + name: quantum core and corpus under wasm32-wasip2 (wasmtime) + 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" + + - name: tests/bit_identity.rs under wasmtime + working-directory: wai/wai-quantum + env: + CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime + run: cargo test --locked --target wasm32-wasip2 --features full --test bit_identity + + # The filters are module paths, so a module that is renamed or loses its tests would + # leave its filter matching nothing and the run passing; the loop fails the step + # unless every module had a test that passed. + - name: The circuit core's unit tests under wasmtime + shell: bash + working-directory: wai/wai-quantum + env: + CARGO_TARGET_WASM32_WASIP2_RUNNER: wasmtime + run: | + set -euo pipefail + mods="quantum ijson receipt_pins quantum_fabric quantum_source quantum_channel quantum_qudit quantum_receipt" + filters=() + for m in $mods; do filters+=("$m::"); done + cargo test --locked --target wasm32-wasip2 --features full --lib -- "${filters[@]}" 2>&1 \ + | tee "$RUNNER_TEMP/wasip2-lib.log" + for m in $mods; do + grep -q "^test $m::.* \.\.\. ok$" "$RUNNER_TEMP/wasip2-lib.log" \ + || { echo "::error::no test of $m ran"; exit 1; } + done + + - name: wai_quantum_vectors verify under wasmtime + shell: bash + working-directory: wai/wai-rs + run: | + set -euo pipefail + cargo build --locked --target wasm32-wasip2 --no-default-features --features quantum --bin wai_quantum_vectors + wasmtime run --dir ../quantum-conformance::/corpus \ + target/wasm32-wasip2/debug/wai_quantum_vectors.wasm verify /corpus + # The generated v2 vocabulary (wai/vocab/v2.*) is checked as a consumer uses it: # sample manifests, typed and untyped, WAI1 and WAI2, are expanded through the # JSON-LD context exactly as written on the wire and validated against the SHACL @@ -488,7 +580,7 @@ jobs: # verdicts required to equal wai_signer_vectors' line for line), and # int_payload_verify.py (the integer payloads and their refusal fixtures), and # staged_verify.py (the staged-delivery corpus), and http_verify.py (the HTTP delivery - # corpus). + # corpus), and quantum-conformance/tools/referee.py (both quantum-sim capabilities). # Each exits non-zero on any failure or any difference, and the step fails with it. # One platform: what they check is the reading of the text, not the architecture. independent-verifiers: @@ -587,6 +679,19 @@ jobs: working-directory: wai shell: bash run: python3 staged-measure-conformance/sim_verify.py staged-measure-conformance + # The quantum corpus's second implementation (quantum-sim extension), integers only: + # it derives the circuit/1 phase table by its recurrence and the circuit2 table by an + # interval-certified procedure, and requires both literal tables and their digests, + # and the extension's Appendix B to state the same; + # rebuilds every file of both vector trees byte for byte and recomputes every manifest + # entry with its own reader, simulator, sampler and BLAKE3; refuses every reject file + # with its listed class and by the rule its name states; and requires dyadic_ladder + # to see every ±1 change of every P(k) under both tables. Standard library only, run + # isolated (-I), so no module from the working directory, the environment or the + # user's site-packages is imported. + - name: quantum referee.py + shell: bash + run: python3 -I wai/quantum-conformance/tools/referee.py wai/quantum-conformance # chain_differential.py signs receipt chains around jwp-receipts §3 step 3 and §6.1 # (two keys, two tracks, both payloads, replays, forks, gaps, the guard and the # freshness bound), and chain_gap_differential.py genuine chains up to 90 groups diff --git a/.github/workflows/wai-energy-receipts.yml b/.github/workflows/wai-energy-receipts.yml index ed4e474ab..9dec45179 100644 --- a/.github/workflows/wai-energy-receipts.yml +++ b/.github/workflows/wai-energy-receipts.yml @@ -29,7 +29,10 @@ name: WAI energy receipts # 3. wai-quantum-serve: `cargo test --locked --lib`, native: the verdict # logic behind POST /verify, for a receipt in either form, and that an # unmetered receipt's 0 is rendered as the unmetered marker (null), never -# as zero joules. +# as zero joules; and that /run and /receipt build in the gateset the +# request names (circuit2 by default), name its capability, give the +# corpus's GHZ3 hashes in each, and answer 400 for an unknown gateset or +# a circuit with no circuit hash. # # It does NOT: # - check that a newly sealed receipt in a family with an optional label @@ -96,6 +99,9 @@ on: - 'wai/CHANGELOG.md' # tests/video_receipt.rs reads the WIV1 refusal fixtures. - 'wai/int-payload-conformance/**' + # wai_quantum_vectors' tests require the committed quantum corpus to be + # what `gen` writes. + - 'wai/quantum-conformance/**' - '.github/workflows/wai-energy-receipts.yml' pull_request: paths: @@ -107,6 +113,9 @@ on: - 'wai/CHANGELOG.md' # tests/video_receipt.rs reads the WIV1 refusal fixtures. - 'wai/int-payload-conformance/**' + # wai_quantum_vectors' tests require the committed quantum corpus to be + # what `gen` writes. + - 'wai/quantum-conformance/**' - '.github/workflows/wai-energy-receipts.yml' jobs: @@ -170,7 +179,7 @@ jobs: # signer's level beside validity (jwp-receipts §7.1, §7.4). The verdict # logic is a plain function, tested natively; the wasm32-wasip2 build of # the component is not part of this workflow. - - name: wai-quantum-serve — POST /verify accepts either form and reports the signer's level + - name: wai-quantum-serve — POST /verify verdicts, and /run and /receipt in the requested gateset working-directory: wai/wai-quantum-serve shell: bash run: cargo test --locked --lib diff --git a/.github/workflows/wai-web-wasm.yml b/.github/workflows/wai-web-wasm.yml index 3f7c8ee7e..d9a3ae81d 100644 --- a/.github/workflows/wai-web-wasm.yml +++ b/.github/workflows/wai-web-wasm.yml @@ -55,6 +55,8 @@ on: - 'wai/wai-web/**' - 'wai/wai-rs/**' - 'wai/wai-quantum/**' + # quantum::tests and quantum_toolchain::tests read the quantum corpora. + - 'wai/quantum-conformance/**' - 'wai/neural-conformance/**' - 'wai/staged-conformance/**' - 'wai/staged-measure-conformance/**' @@ -67,6 +69,8 @@ on: - 'wai/wai-web/**' - 'wai/wai-rs/**' - 'wai/wai-quantum/**' + # quantum::tests and quantum_toolchain::tests read the quantum corpora. + - 'wai/quantum-conformance/**' - 'wai/neural-conformance/**' - 'wai/staged-conformance/**' - 'wai/staged-measure-conformance/**' @@ -131,7 +135,12 @@ jobs: # staged-delivery binding's pure half replays the staged corpus and reads a layer's # header and cost (stage::tests); the measurement binding's pure half evaluates the # staged-measurement corpus to its goldens and seals a page-recorded session equal - # to the native tool's (measure::tests). The count check turns a test that + # to the native tool's (measure::tests). The quantum sink opens every file of + # both quantum corpora to their manifests' hashes and capability and refuses + # every reject file with its listed class (quantum::tests), and the quantum IDE + # builds its catalog circuits in its gateset to the same manifests, stores no + # parameter on a gate other than P, and shows no hash for an invalid circuit + # (quantum_toolchain::tests). The count check turns a test that # stopped being compiled into a failure instead of a silent "0 passed". - name: wai-web native unit tests shell: bash @@ -139,7 +148,7 @@ jobs: run: | set -euo pipefail cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log" - grep -q "test result: ok. 24 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" + grep -q "test result: ok. 31 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log" - name: wasm32 int_synth decode equals the frozen native corpus shell: bash working-directory: wai/wai-web diff --git a/wai/CHANGELOG.md b/wai/CHANGELOG.md index 346bdce85..303c2dcd3 100644 --- a/wai/CHANGELOG.md +++ b/wai/CHANGELOG.md @@ -6,6 +6,99 @@ wire format may change and such changes are recorded here. ## v0.4.0 — current +### Quantum circuits: `wai.quantum.circuit2`, and `wai.quantum.circuit` stated exactly + +- **A new capability, `wai.quantum.circuit2`** ([quantum-sim](extensions/quantum-sim.md), + SPEC §5). Magic `WQC2`, contract `ext` `wai.quantum.circuit2/1`, numeric + `wai.det.amp64`, gateset `cliffordT+dyadicP.rn`. The container layout, the gate IR and + the arithmetic are those of `wai.quantum.circuit`; the phase table is not. Each + `P(k)` holds the integers nearest to `2^30·cos(2π/2^k)` and `2^30·sin(2π/2^k)`, so its + controlled-phase ladder gives the QFT. The literal table is normative (§3.2, Appendix + B.2, digest `d34cb793…3510`); an exact-integer interval procedure certifies every entry, + informatively. The contract and the measurement are byte-canonical, and the op-log ends + exactly after its last op. Registered in the machine-readable registry and the + vocabularies, with `wai.det.amp64` as an identifier. +- **`wai.quantum.circuit` is frozen, and its table stated for the first time** (§3.1, + Appendix B.1, digest `3fa9805f…8466`). The text gives the exact recurrence — the sine + formula, the truncating halving, the floor square root — and the deviation of every + entry from the ideal phase: from `P(8)` on the imaginary part is too large, by up to + 7032 units of `2^−30`, and `P(19..32)` apply no phase. The claim that this table gives the + QFT is withdrawn. Writers SHOULD NOT emit new circuit/1 content. +- **Both capabilities are specified to the bit** (§2–§4): little-endian integers, section + bounds computed without overflow, a repeated known section refused, the contract and + the measurement read as I-JSON (RFC 7493) by rules stated in full: no repeated member + name, no surrogate or noncharacter, no number of magnitude 2^1024 − 2^970 or more, at + most 127 levels deep, and an integer read exactly; the three refusal + classes (malformed, contract, invalid) in the order a reader checks them, the + complex-product composition, the index convention, the gate matrices, the hash layouts + and the shot sampler. `circuit_hash` covers the canonical contract, so it names the + capability and its table; `statevector_hash` does not. §5 states the error per + capability, and a new §8 why circuit2 is a capability of its own and what an older + reader does with it (it refuses `WQC2` at the magic). +- **Newly non-conforming circuit/1 payloads** (quantum-sim §8, SPEC §5): a contract whose + `ext`, `numeric`, `frac` or `gateset` is missing or carries another value; an + `n_qubits` that is not an unsigned integer of at most 255; a known section kind more + than once; section bounds that overflow; a contract or measurement that repeats a + member name within one object, holds a noncharacter, or holds a number of magnitude + 2^1024 − 2^970 or more that an earlier build's float parser let through (some builds + also refused numbers just below it, which are now accepted). No file a reference writer + wrote is among them. +- **Corpus.** `quantum-conformance/` keeps its four original vectors and manifest entries + byte for byte and adds qft5_x22, dyadic_ladder and qft8_ones, which see the phase table: + under either table, `dyadic_ladder`'s statevector hash changes with every ±1 change of + any `P(1..32)` component. `v2/` holds the same seven circuits as circuit2; `reject/` at + least one payload for each refusal a payload can carry, with its class; and `accept/` + payloads at the edges of the same rules that a reader must accept, with how each reads. + `tools/referee.py`, integers only, derives both tables, requires Appendix B to state + them, reads every JSON section by the same rules, and rebuilds and checks every file. The + Appendix B tables are rendered from the reference constants by + `wai_quantum_vectors appendix`, and a test fails when the text differs. + +### wai-quantum 0.4.0: circuits carry their gateset + +- **`Gateset { V1, V2 }`** names the capability a circuit is reconstructed with, and + `Circuit` carries one in a new field. `Circuit::new` and `Circuit::qft` build V2 + (`wai.quantum.circuit2`); a circuit read from a container takes the container's, and one + derived from another keeps it. `PHASE_V1` and `PHASE_V2` are literal constants: no + recurrence runs at load. `simulate`, `to_wqc`, `circuit_hash`, receipts, the fabric's + lanes and the toolchain follow the circuit's gateset. `from_wqc` reads both magics, + checks a circuit/1 contract's values and a circuit2 contract's bytes, reads the contract + and the measurement as I-JSON with a strict reader of its own that decides every rule + from the text, the number range on the literal's digits (so neither a build feature nor + a float parser's limits changes a verdict), refuses repeated sections and + wrapping bounds, and no longer sizes an allocation from a claimed op count. + `reject_corpus()` and `accept_corpus()` return the payloads a reader must refuse, each + with its class, and must accept, each with its reading; the corpus's `reject/` and + `accept/` trees are written from them. +- **Every circuit built in code changes its `circuit_hash`**, since the contract names the + capability, and its statevector wherever `H`, `T`, `T†`, `P(3)` or `P(k ≥ 5)` acts on a + nonzero amplitude (`P(1)`, `P(2)` and `P(4)` are the same in both tables). Every + circuit/1 artefact reproduces: the four original vectors, the legacy receipt pins, the + lanes32 certificate corpus and the other pins are unchanged, built in `Gateset::V1`, and + new V2 pins sit beside them. +- **wai-rs** registers `wai.quantum.circuit2`; `quantum_envelope` tags an envelope with the + circuit's capability and refuses one whose capability and magic disagree. The + `wai-quantum` CLI takes `--gateset v1|v2` (default v2) and names the capability in its + JSON; with `--gateset v1` and a phase of `k ≥ 8` it prints the circuit/1 deviation on + standard error. `wai_quantum_meter` seals the capability the file names. The browser + sink reads both magics; the IDE has `set_gateset`, and its `add()` sets the parameter of + every gate but `P` to 0. The serve component, `wai-quantum-serve` 0.5.0, takes + `gateset=v1|v2` on `/run`, `/qasm` and `/receipt`. +- **Migration from 0.3** (a breaking release): + - `Circuit::new` is V2. To reproduce or verify a 0.3 artefact, read its `WQC1` file, or + set `c.gateset = Gateset::V1` on a circuit built in code; a receipt's `gateset_of` + says which gateset a circuit's ops match it under. + - `quantum::phase`, `quantum::inv_sqrt2` and `quantum::CAPABILITY` are removed in favour + of `Gateset::phase`, `Gateset::inv_sqrt2` and `Gateset::capability`. + - `to_wqc`, `circuit_hash` and `quantum_toolchain::circuit_id` return a `Result`; they + fail only on a circuit its gateset does not allow, never on one that has simulated. + - `Circuit` and `QuantumError` are `#[non_exhaustive]`; `QuantumError` gains `Contract` + and `kind()`. + - `quantum_fabric::certify_lanes` and `lanes::random_circuit` take a gateset, with a + kernel and generator id per gateset; the circuit/1 ids are unchanged. + - `validate` refuses a gate with more than 255 controls, and in V2 repeated controls and + a parameter on any gate but `P`. `from_wqc` refuses the payloads quantum-sim §8 lists. + ### Integer operators (Draft), and the payload text of two integer codecs - **A new extension, [integer-operators](extensions/integer-operators.md) (Draft).** diff --git a/wai/README.md b/wai/README.md index ebff47b44..b09cdf610 100644 --- a/wai/README.md +++ b/wai/README.md @@ -154,14 +154,19 @@ pure Rust with no host bindings, **one source builds for native and `wasm32-wasip2` and produces byte-identical results** — the same circuit reconstructs to the same statevector hash on a laptop or at the edge. -Only `wai.quantum.circuit` is a registered payload capability, and -`wai.quantum.calibration` and `wai.quantum.job` are registered records; the other -names below are module labels ([`REGISTRY.md`](REGISTRY.md) lists the receipt -labels no revision registers). +`wai.quantum.circuit2` and `wai.quantum.circuit` are the registered payload +capabilities, and `wai.quantum.calibration` and `wai.quantum.job` are registered +records; the other names below are module labels ([`REGISTRY.md`](REGISTRY.md) +lists the receipt labels no revision registers). The two circuit capabilities +share the container and the gates and differ in the phase table: circuit2's holds +the integers nearest to each phase and is the default for new content, and +`wai.quantum.circuit` is frozen so that every file, hash and receipt made with it +reproduces ([quantum-sim](extensions/quantum-sim.md) §8). | Module | What it does | Conformance | |---|---|---| -| `wai.quantum.circuit` | byte-exact statevector simulation (dyadic Clifford+T+P(k)) | statevector-equivalence | +| `wai.quantum.circuit2` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), with the correctly rounded phase table | statevector-equivalence | +| `wai.quantum.circuit` | the same, with the frozen phase table existing content was made with | statevector-equivalence | | `wai.quantum.toolchain` | four backends (statevector / stabilizer / tensor-network MPS / sparse-Pauli), algorithm library, OpenQASM 3.0 in-and-out, **QIR export + ingest**, Bloch + entanglement analysis | statevector-equivalence | | `wai.quantum.compile` | Clifford routing + stabilizer-tableau equivalence proof | signed equivalence certificate | | `wai.quantum.cal.*` | calibration, filter-function robust control, DD noise spectroscopy | reproducible + receipt | @@ -189,8 +194,13 @@ cargo build --release --no-default-features \ ./target/release/wai-quantum qir qft -n 4 | \\ ./target/release/wai-quantum run --qir # and read it back ./target/release/wai-quantum receipt ghz -n 3 | ./target/release/wai-quantum verify +./target/release/wai-quantum run qft -n 5 --gateset v1 # the frozen wai.quantum.circuit ``` +Circuits are `wai.quantum.circuit2` unless `--gateset v1` asks for +`wai.quantum.circuit`, which reproduces the circuit and statevector hashes +earlier releases gave. + ### Same source, at the edge ```bash @@ -222,6 +232,7 @@ wasmtime serve -S cli=y --addr 127.0.0.1:8231 \ GET /health GET /algorithms GET /run?algo=ghz&n=5&top=8 POST /run (body = OpenQASM 3.0) GET /qasm?algo=bell GET /receipt?algo=ghz&n=3[&shots=&seed=] + (/run, /qasm and /receipt also take &gateset=v1|v2) POST /verify (body = receipt JSON; 200 valid / 409 invalid / 400 unparseable) ``` diff --git a/wai/REGISTRY.md b/wai/REGISTRY.md index 0d75ba01d..13509daef 100644 --- a/wai/REGISTRY.md +++ b/wai/REGISTRY.md @@ -49,8 +49,9 @@ either; staged-delivery §4 processes it. Two further tables hold strings that look like capabilities and are not: - **identifiers** name a part of a capability's definition (`wai.det.fixed64`, the - fixed-point floor; `wai.world.sim.*`, the simulation steps of the feed and avatar - capabilities); + fixed-point floor; `wai.det.amp64`, the complex amplitude arithmetic of + `wai.quantum.circuit2`; `wai.world.sim.*`, the simulation steps of the feed and + avatar capabilities); - **unregistered labels** are strings the reference implementation uses as labels that no revision registers, each with the reason. @@ -211,6 +212,7 @@ and a role are fixed once registered. | `wai.prior.set` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Prior artifacts, general tensors and derivation operations | | `wai.quantum.calibration` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.quantum.circuit` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | +| `wai.quantum.circuit2` | payload | registered | binary | standalone | royalty-free | standard-defined | — | §5 Interactive, temporal & computed content | | `wai.quantum.job` | record | registered | — | — | — | — | — | §5 Interactive, temporal & computed content | | `wai.semantic.clip` | payload | registered | image | standalone | royalty-free | behavioral | — | §5 Neural capabilities | | `wai.semantic.perco2` | payload | registered | image | standalone | non-commercial | behavioral | — | §5 Neural capabilities | @@ -250,7 +252,8 @@ and a role are fixed once registered. | identifier (not a capability) | meaning | in | |---|---|---| -| `wai.det.fixed64` | the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in | §5 Interactive, temporal & computed content | +| `wai.det.amp64` | the complex fixed-point arithmetic of wai.quantum.circuit2: re and im each an i64 at scale 2^frac, products exact then rounded half up, sums exact (quantum-sim §2.1, §3.0) | §5 Interactive, temporal & computed content | +| `wai.det.fixed64` | the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in (Q32.32, interactive-worlds §6.2); inside a wai.quantum.circuit contract it names the quantum-sim §3.0 arithmetic at frac 30, products rounded half up | §5 Interactive, temporal & computed content | | `wai.world.sim.feed0` | the identity simulation step of wai.feed.state | §5 Interactive, temporal & computed content | | `wai.world.sim.skeletal0` | the skeletal-pose simulation step of wai.avatar.pose | §5 Interactive, temporal & computed content | diff --git a/wai/SPEC.md b/wai/SPEC.md index a38e0a598..fb69a7615 100644 --- a/wai/SPEC.md +++ b/wai/SPEC.md @@ -522,9 +522,9 @@ fidelity tier, a replicate class and a role. A derivation or record string is no capability and is not dispatchable (§4): step 3 does not take an envelope's `capability` that names one, and step 5 may still take a usable fallback. A row of the tables below that registers a record says what the record attests, not a payload format. Named -sub-identifiers this section uses — `wai.det.fixed64`, `wai.world.sim.feed0`, -`wai.world.sim.skeletal0` — name parts of a capability's definition and are -not capabilities. +sub-identifiers this section uses — `wai.det.fixed64`, `wai.det.amp64`, +`wai.world.sim.feed0`, `wai.world.sim.skeletal0` — name parts of a capability's +definition and are not capabilities. ### Mandatory floor @@ -729,7 +729,27 @@ standard. See the linked extension for each. | `wai.film.linear` | WFLM container (sealed world + camera track + frame schedule) | film-equivalence · [linear-film](extensions/linear-film.md) | | `wai.film.reel` | WREL container (sealed world + a sequence of shots = camera track + schedule each) | reel-equivalence (film-equivalence with cuts) · [linear-film](extensions/linear-film.md) | | `wai.ternary.tt` | WQTT container (balanced-ternary MPS cores — packed trits + integer per-column scales) | a **quantum-inspired** (tensor-network) transform ported from the ternary/MPS fabric with ONE change: the per-column scale is an **integer**, so the whole decode is `i64`/`i128` and the reconstruction is byte-identical on every machine — **reconstruction-equivalence** (a portable recon hash), receiptable. Scope: a **provenance** transform, measured at 13.3 dB SNR for 3.8× with six residual layers (`Lossy` tier; the extension's §5). Encoder-side float SVD only · [ternary-tt](extensions/ternary-tt.md) | -| `wai.quantum.circuit` | WQC container (gate op-log — Clifford+T + dyadic phase) | the wire carries a **circuit** (kilobytes, linear in gate count), the sink CLASSICALLY simulates it in a fixed-point **complex** floor (`Amp` = i64 re/im), and the reconstructed `2^n`-amplitude statevector is byte-identical on every machine — **statevector-equivalence** (a portable statevector hash); measurement is sampled by the pinned `splitmix64` so a shot histogram at a pinned `(seed, shots)` converges too (**shot-histogram-equivalence**). The dyadic phase ladder gives the **QFT** ("quantum FFT"), transported as a circuit and reconstructed byte-exact. No quantum hardware — NISQ noise breaks the determinism the standard rests on; this makes a quantum *computation* transportable + receiptable, with **no** speedup claimed · [quantum-sim](extensions/quantum-sim.md) | +| `wai.quantum.circuit` | WQC container (gate op-log — Clifford+T + dyadic phase) | the wire carries a **circuit** (kilobytes, linear in gate count), the sink CLASSICALLY simulates it in a fixed-point **complex** floor (`Amp` = i64 re/im), and the reconstructed `2^n`-amplitude statevector is byte-identical on every machine — **statevector-equivalence** (a portable statevector hash); measurement is sampled by the pinned `splitmix64` so a shot histogram at a pinned `(seed, shots)` converges too (**shot-histogram-equivalence**). The dyadic phase ladder transports the **QFT** ("quantum FFT") as a circuit, reconstructed byte-exact; the phase table is frozen as existing content was made with it, so `P(k)` deviates from the ideal phase for k ≥ 8 and applies none for k ≥ 19, and new content uses `wai.quantum.circuit2`. No quantum hardware — NISQ noise breaks the determinism the standard rests on; this makes a quantum *computation* transportable + receiptable, with **no** speedup claimed · [quantum-sim](extensions/quantum-sim.md) | +| `wai.quantum.circuit2` | WQC container under magic `WQC2` (the same gate op-log) | the successor of `wai.quantum.circuit`, with the same statevector- and shot-histogram-equivalence: a correctly rounded phase table (each component of every `P(k)` within half a unit of 2^30·e^{2πi/2^k}), so the dyadic phase ladder gives the **QFT**; a byte-canonical contract; an op-log that ends exactly after its last op. A new capability, not a change to the old one, because a registered capability's payload format is fixed (§8): a reader that predates it rejects a `WQC2` payload at the magic · [quantum-sim](extensions/quantum-sim.md) | + +The contract of a `wai.quantum.circuit2` payload names its arithmetic +`wai.det.amp64`: complex amplitudes as `i64` pairs at scale `2^frac`, each +product exact and then rounded half up, each sum exact. Inside a +`wai.quantum.circuit` contract, `wai.det.fixed64` names that same arithmetic at +frac 30. [quantum-sim](extensions/quantum-sim.md) §2.1 defines both. + +*Payloads the quantum-sim reader rules make non-conforming* (their envelopes still +parse). [quantum-sim](extensions/quantum-sim.md) §2 and §2.1 state reader rules that +enforce the registration of `wai.quantum.circuit` as it was made (§8). A sink refuses, +when it decodes it, a `wai.quantum.circuit` payload whose contract does not carry +the registered `ext`, `numeric`, `frac` and `gateset`, a `wai.quantum.circuit2` +contract among them; whose `n_qubits` is not an unsigned integer of at most 255; +that carries a known section kind more than once; that has a section whose +bounds overflow; or whose contract or measurement repeats a member name within one +object, holds a noncharacter, or holds a number of magnitude 2^1024 − 2^970 or more +that an earlier reader's float parser let through. No payload a reference writer +wrote is among them; +[quantum-sim](extensions/quantum-sim.md) §8 lists them with the rules. And the verify half — a signed, energy-accounted lineage for opaque captured content WAI does **not** reconstruct (content-addressing + diff --git a/wai/extensions/quantum-sim.md b/wai/extensions/quantum-sim.md index 0cab4fbe2..245767eb5 100644 --- a/wai/extensions/quantum-sim.md +++ b/wai/extensions/quantum-sim.md @@ -5,20 +5,39 @@ > the sink **classically simulates** it in a fixed-point complex floor, > and the reconstructed statevector hashes identically on every machine. > Conformance is **statevector-equivalence** (and, for measured circuits, -> **shot-histogram-equivalence**). Reference impl: the `quantum` feature of -> `wai-rs` (`crate::quantum`); corpus: `quantum-conformance/`; capability -> `wai.quantum.circuit`. +> **shot-histogram-equivalence**). Two capabilities share the container +> layout, the gate IR and the arithmetic: `wai.quantum.circuit2` (magic +> `WQC2`), for new content, and `wai.quantum.circuit` (magic `WQC1`), frozen +> so that existing content reproduces bit for bit. Reference impl: the +> `quantum` module of the `wai-quantum` crate, which `wai-rs` re-exports as +> `wai::quantum` under its `quantum` feature; corpus: `quantum-conformance/`. Keywords **MUST**, **MUST NOT**, **SHOULD**, **MAY** are RFC 2119/8174. ## 1. Scope and model -A `wai.quantum.circuit` object is a **quantum circuit**: a qubit count and -an ordered list of gates. The sink reconstructs the resulting quantum state -by simulating the circuit from `|0…0⟩` in the `wai.det.fixed64` floor — -every amplitude is a complex fixed-point number (`re, im` as `i64` at scale -`2^FRAC`), every gate an integer matrix–vector update with a pinned rounding -rule. It is lever 3 (instructions-at-the-sink) applied to computation: the -wire carries the *circuit*, the sink reconstructs the *state*. +A quantum-circuit object is a **quantum circuit**: a qubit count and an ordered +list of gates. The sink reconstructs the resulting quantum state by simulating +the circuit from `|0…0⟩` in a fixed-point complex floor — every amplitude is a +complex fixed-point number (`re, im` as `i64` at scale `2^FRAC`), every gate an +integer matrix–vector update with a pinned rounding rule. It is lever 3 +(instructions-at-the-sink) applied to computation: the wire carries the +*circuit*, the sink reconstructs the *state*. + +Two capabilities carry such an object. They differ only in the magic, the +contract (§2.1), the phase table the gates read (§3.1, §3.2) and how strictly a +reader holds the container (§2): + +| capability | magic | contract `ext` | phase table | for | +|---|---|---|---|---| +| `wai.quantum.circuit2` | `WQC2` | `wai.quantum.circuit2/1` | correctly rounded (§3.2) | new content | +| `wai.quantum.circuit` | `WQC1` | `wai.quantum.circuit/1` | frozen (§3.1) | existing content, which reproduces bit for bit | + +Below, **circuit2** and **circuit/1** name the two. Writers SHOULD NOT emit new +circuit/1 content: its phase table deviates from the ideal phases for `k ≥ 8` and +applies none for `k ≥ 19` (§3.1). A sink that reconstructs circuit/1 content +reads it exactly as this text states, so that the content keeps its hashes and +its receipts keep verifying. §8 says why the correction is a capability of its +own. The design rule that governs the whole extension: **WAI's value is determinism** — byte-identical reconstruction, "conformance is a hash." Real @@ -41,38 +60,141 @@ do not belong in this exact-reconstruction extension (§7). Relationship to the core spec: mirrors worlds / score / film. The reconstruct runs in the deterministic floor; `media` takes the informational -value `"quantum"`; `wai.quantum.circuit` is authoritative. Its replicate -class is `StandardDefined` (WAI's own deterministic floor), its fidelity tier -`Lossless` (the simulation analog — the reconstruction is byte-exact). +value `"quantum"`; the envelope's capability names which of the two applies. +Both capabilities have the replicate class `StandardDefined` (WAI's own +deterministic floor) and the fidelity tier `Lossless` (the simulation analog — +the reconstruction is byte-exact). ## 2. Container (`WQC`) ``` -"WQC1" | u16 n_sections | section table (u8 kind, u32 off, u32 len) | sections +magic | u16 n_sections | n_sections × (u8 kind, u32 off, u32 len) | sections ``` -`off` is relative to the start of the sections blob (immediately after the -table). Unknown section kinds MUST be ignored (forward-compatible). +The magic names the capability: `WQC1` is circuit/1 and `WQC2` is circuit2. The +layout is otherwise identical. All integers in the container are little-endian. + +The sections blob starts immediately after the table, at byte +`6 + 9·n_sections`, and `off` is relative to it: a section occupies the bytes +from `blob + off` to `blob + off + len`. A reader computes these bounds without +overflow (in arithmetic wider than 32 bits, or with checked operations) and +refuses a table or a section, of any kind, that runs past the end of the payload. | kind | section | required | |---|---|---| -| `0x01` | contract (canonical JSON) | REQUIRED | -| `0x02` | gate op-log | REQUIRED | -| `0x03` | measurement (canonical JSON) | OPTIONAL | +| `0x01` | contract (canonical JSON, §2.1) | REQUIRED | +| `0x02` | gate op-log (§2.2) | REQUIRED | +| `0x03` | measurement (canonical JSON, §2.3) | OPTIONAL | + +Unknown section kinds MUST be ignored (forward-compatible). A known kind that +appears more than once MUST be refused: two readers could otherwise read +different sections of one payload. The reference writer emits the contract, the +op-log and the measurement, if any, in that order and contiguous from offset 0, +and no other section. + +**JSON in a section.** The contract and the measurement are I-JSON texts +([RFC 7493](https://www.rfc-editor.org/rfc/rfc7493)), and a reader decides each +of these rules from the section's bytes: + +- The section is UTF-8 and holds one JSON value + ([RFC 8259](https://www.rfc-editor.org/rfc/rfc8259)), with only space, tab, + line feed and carriage return around its tokens. A byte-order mark is not + whitespace. +- No object names a member twice. Names are compared after their escapes are + decoded, so `"a"` and `"\u0061"` are the same name. +- No name or string holds a surrogate code point or a noncharacter (U+FDD0 to + U+FDEF, and the last two code points of every plane), written raw or as an + escape. An escaped surrogate pair stands for the one character it encodes. +- No number has a magnitude of 2^1024 − 2^970 or more. These are exactly the + numbers that round to an infinite IEEE 754 double, rounding to nearest with + ties to even. A reader decides the rule exactly, on the literal's digits: its + significant digits and its decimal exponent against the 309 digits of + 2^1024 − 2^970, however many digits or zeros the literal is written with. A + float parser decides it only where it rounds correctly at every length and + every exponent. A number that rounds to zero is in range. +- Arrays and objects nest at most 127 deep, the outermost counted as the first. + +RFC 7493 also asks a sender to keep a number within a double's precision and an +integer within ±(2^53 − 1). A reader here does not hold what it receives to that: +it reads an integer exactly, so `seed` and `shots` (§2.3) span the whole `u64` +range. Where a rule asks for an *unsigned JSON integer*, it means a number +written with no sign, fraction or exponent, whose value is in the range the rule +names: `-0`, `2.0` and `2e0` are not one. + +A section that breaks one of these rules is a contract refusal for the contract +and a malformed one for the measurement. A section that breaks several is +refused with that class whichever of them a reader meets first; readers may name +different rules for it, and only the class is specified. + +A reader refuses a payload that breaks a rule of this section or of §2.1–§2.3. +It checks in this order and refuses with the class of the first rule broken: + +1. The payload is at least 6 bytes and starts with `WQC1` or `WQC2`; else + **malformed**. +2. The section table and every section, of a known kind or not, lie within the + payload; no known kind repeats; the contract and the op-log are present. + Else **malformed**. +3. The contract (§2.1); else **contract**, except that an `n_qubits` that is + not an unsigned integer of at most 255 is **malformed**. +4. The op-log parses (§2.2); else **malformed**. +5. The circuit keeps the rules of its capability (§2.2); else **invalid**. +6. The measurement, if present (§2.3); else **malformed**. + +The corpus's `reject/` tree holds at least one payload for each refusal of this +section and of §2.1–§2.3 that a payload can carry, and `reject.json` names each +one's class. The two limits it cannot carry bind a writer instead: the wire +counts a gate's controls in a `u8` and a circuit's ops in a `u32`. Its `accept/` +tree holds payloads at the edges of the same rules that a reader MUST accept, +and `accept.json` gives how each one reads: its capability, `n_qubits`, op count, +`circuit_hash` and measurement. ### 2.1 Contract -```json -{ "ext": "wai.quantum.circuit/1", "n_qubits": 3, - "numeric": "wai.det.fixed64", "frac": 30, "gateset": "cliffordT+dyadicP" } -``` +The canonical contract bytes, used for writing and for `circuit_hash` (§4.1), +have no whitespace and these members in this order. `N` is `n_qubits` in +decimal, `0` to `255`, with no sign, leading zero, fraction or exponent: -`frac` is the amplitude fixed-point scale (`FRAC`, 30 in v1). `gateset` -names the reconstruction's gate algebra (§3). +- circuit/1: + `{"ext":"wai.quantum.circuit/1","n_qubits":N,"numeric":"wai.det.fixed64","frac":30,"gateset":"cliffordT+dyadicP"}` +- circuit2: + `{"ext":"wai.quantum.circuit2/1","n_qubits":N,"numeric":"wai.det.amp64","frac":30,"gateset":"cliffordT+dyadicP.rn"}` + +A writer writes the canonical contract of its capability. + +`ext` is `/`: `wai.quantum.circuit2/1` is revision +1 of the circuit2 capability, not a version "2.1". `frac` is the amplitude +fixed-point scale (`FRAC`, 30 in both). `gateset` names the reconstruction's gate +algebra (§3); circuit2's string differs from circuit/1's because its constants +differ. `numeric` names the arithmetic: + +- `wai.det.amp64`: complex amplitudes, `re` and `im` each an `i64` at scale + `2^frac`. A product of two components is computed exactly and rounded as + `(a·b + 2^(frac−1)) >> frac`, with an arithmetic shift (round half up); sums + are exact (§3.0). +- `wai.det.fixed64`, inside a circuit/1 contract, means the same: the §3.0 + arithmetic at frac 30, with products rounded half up. Those bytes are inside + every circuit/1 `circuit_hash`, so they stay. It does not mean the Q32.32 + floor, with products rounded toward −∞, that + [interactive-worlds](interactive-worlds.md) §6.2 names `wai.det.fixed64`. + +Reading a circuit/1 contract. The section MUST be an I-JSON object (§2), else +contract. +Its `ext`, `numeric` and `gateset` MUST be the strings of the template above and +its `frac` the JSON integer `30` (`30.0` is not); a member that is missing or +carries another value is a contract refusal, so a circuit2 contract in a `WQC1` +container is refused. Then `n_qubits` MUST be an unsigned JSON integer of at most +255, else malformed. Member order, whitespace and further members are free; +`circuit_hash` covers the canonical bytes, not the received ones (§4.1). + +Reading a circuit2 contract. The section MUST be an I-JSON object (§2), else contract, +whose `n_qubits` is an unsigned JSON integer of at most 255, else malformed. Its +bytes MUST then equal the canonical circuit2 contract for that `n_qubits`, else +contract: no whitespace, no other member order and no further member, and a +circuit/1 contract in a `WQC2` container is refused. ### 2.2 Gate op-log -`n_ops u32`, then per op: +`u32 n_ops`, then per op: ``` u8 opcode | u8 n_ctrl | u8 target | u16 param | n_ctrl × u8 control @@ -80,7 +202,7 @@ u8 opcode | u8 n_ctrl | u8 target | u16 param | n_ctrl × u8 control The op is a **controlled-1-qubit gate**: the base gate `opcode` is applied to `target` iff every qubit in `control` is set. `param` carries the dyadic index -`k` for `P(k)` (0 otherwise). Opcodes: +`k` for `P(k)`. Opcodes: | opcode | gate | | opcode | gate | |---|---|---|---|---| @@ -91,71 +213,246 @@ The op is a **controlled-1-qubit gate**: the base gate `opcode` is applied to | 4 | `H` | | 9 | `P(k) = diag(1, e^{2πi/2^k})` | `CX`/`CZ`/`CP(k)` are these with `n_ctrl = 1`; `CCX` (Toffoli) with -`n_ctrl = 2`; `SWAP` is three `CX`. A target MUST be `< n_qubits`; a control -MUST be `< n_qubits` and `≠ target`; `P(k)` MUST have `1 ≤ k ≤ 32`. +`n_ctrl = 2`; `SWAP` is three `CX`. + +Parsing, in both capabilities: every op lies within the section and its opcode is +at most 9, else malformed. In circuit2 the section MUST end exactly after the +`n_ops`-th op, else malformed; in circuit/1 bytes after it are ignored and are +not part of the circuit. `n_ops` is the payload's claim: every op takes at least +5 bytes, so a section of `len` bytes holds at most `⌊(len − 4)/5⌋` ops, and a +reader reserves no more than that. + +The rules of the circuit, refused as invalid: + +- in both capabilities, a target is `< n_qubits`; a control is `< n_qubits` and + `≠ target`; a gate has at most 255 controls (the wire counts them in a `u8`, + so this binds a writer); `P(k)` has `1 ≤ k ≤ 32`; a circuit has at most + `2^32 − 1` ops; +- in circuit2 also, a gate's controls are pairwise distinct, so there are at + most 254, and `param` is 0 for opcodes 0–8. + +circuit/1 keeps the rules it had: a repeated control is the same condition +twice, and opcodes 0–8 ignore `param`. ### 2.3 Measurement (optional) ```json -{ "seed": 13034510, "shots": 100000, "basis": "computational" } +{"seed":12648430,"shots":100000,"basis":"computational"} ``` -Pins a computational-basis sampling for shot-histogram-equivalence (§4). +Pins a computational-basis sampling for shot-histogram-equivalence (§4.2); the +example is the corpus's bell measurement. +`seed` and `shots` are unsigned 64-bit integers. + +- circuit2: the section MUST be exactly + `{"seed":S,"shots":N,"basis":"computational"}`, with `S` and `N` in decimal + with no sign, leading zero, fraction, exponent or whitespace; else malformed. +- circuit/1: the section MUST be I-JSON (§2), else malformed. A `seed` or `shots` that + is missing or is not an unsigned integer in the `u64` range reads as 0, and + `basis` is not read. The reference writer writes the circuit2 form. ## 3. Reconstruct contract -The state is `2^n` amplitudes, `amps[|0…0⟩] = 1`, all others `0` at start. Each -op applies its base gate's 2×2 fixed-point matrix `M` to the amplitude pair -`(i, i∨2^target)` for every `i` with the target bit clear **and** all control -bits set: +### 3.0 Arithmetic, for both capabilities -``` -a0' = M00·a0 + M01·a1 a1' = M10·a0 + M11·a1 -``` +An implementer needs all of this, with the phase table of §3.1 or §3.2, to +reproduce a statevector. -Complex products use a pinned round-half-up fixed-point multiply -(`(a·b + 2^(FRAC−1)) >> FRAC`, `i128` intermediate). The one irrational the -Clifford+T set needs — `1/√2` — and the dyadic phases `e^{2πi/2^k}` are built -by an **integer half-angle recurrence** seeded at `(cos π/2, sin π/2) = (0, 1)` -(`cos(θ/2)=√((1+cosθ)/2)`, integer `isqrt`), so **no float ever runs**. -`gateset = "cliffordT+dyadicP"` is universal (Clifford+T is dense in -`SU(2^n)`); the dyadic phase ladder gives the **QFT** ("quantum FFT"), which is -transported as a circuit (`H` + controlled-`P(k)` + swaps) and reconstructs -byte-exact. +- **Amplitude.** `(re, im)`, each an `i64` at scale `2^30`; `2^30` is 1. A unit + below means `2^−30`. +- **Product.** `fx(a, b) = (a·b + 2^29) >> 30`, with `a·b` exact (signed 128-bit + arithmetic holds it) and `>>` an arithmetic shift, that is floor division by + `2^30`. +- **Complex product.** `(a·b).re = fx(a.re, b.re) − fx(a.im, b.im)` and + `(a·b).im = fx(a.re, b.im) + fx(a.im, b.re)`. Each product is rounded + separately; the sums are exact. +- **Initial state.** `2^n` amplitudes, `amps[0] = (2^30, 0)` and every other + `(0, 0)`. Qubit `q` is bit `q` of the basis index, qubit 0 the least + significant. +- **A gate** (base, controls `C`, target `t`, param), for every index `i` with + bit `t` clear and every bit of `C` set, with `j = i | 2^t`, sets + + ``` + a_i' = m00·a_i + m01·a_j a_j' = m10·a_i + m11·a_j + ``` + + from the values before the gate, each product a complex product and each sum + exact. Gates apply in op-log order. +- **Matrices.** With `P(k)` the capability's phase table (§3.1 or §3.2), + `h = re P(3)`, `1 = (2^30, 0)` and `i = (0, 2^30)`: + + | opcode | gate | `m00` | `m01` | `m10` | `m11` | + |---|---|---|---|---|---| + | 0 | `I` | `1` | `0` | `0` | `1` | + | 1 | `X` | `0` | `1` | `1` | `0` | + | 2 | `Y` | `0` | `−i` | `i` | `0` | + | 3 | `Z` | `1` | `0` | `0` | `−1` | + | 4 | `H` | `(h, 0)` | `(h, 0)` | `(h, 0)` | `(−h, 0)` | + | 5 | `S` | `1` | `0` | `0` | `i` | + | 6 | `S†` | `1` | `0` | `0` | `−i` | + | 7 | `T` | `1` | `0` | `0` | `P(3)` | + | 8 | `T†` | `1` | `0` | `0` | `conj P(3)` | + | 9 | `P(k)` | `1` | `0` | `0` | `P(k)` | + +- The capability selects the table and nothing else, and only `H`, `T`, `T†` + and `P(k)` read it. +- `fx(±2^30, x) = ±x` and `fx(0, x) = 0` exactly, so a product with a matrix + entry of `0`, `±1` or `±i` is exact. An implementation may skip such products + (a diagonal gate need touch only `a_j`) without changing a bit. The canonical reconstructed quantity is the amplitude vector serialized as `re_le(i64) ‖ im_le(i64)` per amplitude, in index order. +`cliffordT+dyadicP` and `cliffordT+dyadicP.rn` are universal (Clifford+T is +dense in `SU(2^n)`). In circuit2, `P(k)` gives the controlled-phase ladder of +the QFT ("quantum FFT"), transported as a circuit (`H` + controlled-`P(k)` + +swaps) and reconstructed byte-exact; the circuit/1 table does not (§3.1). No float runs on the decode: the phase tables are +integer constants. + +### 3.1 The circuit/1 phase table (frozen) + +`P1(1) = (−2^30, 0)` and `P1(2) = (0, 2^30)`. For `k = 3..32`, with +`c = re P1(k−1)`: + +``` +re P1(k) = isqrt(⌊(2^30 + c) / 2⌋ · 2^30) +im P1(k) = isqrt(⌊(2^30 − c) / 2⌋ · 2^30) +``` + +`isqrt` is the floor square root. Both halves are non-negative, so truncating +division and floor division agree. `H` uses `h = re P1(3) = 759250124`, `T` is +`P1(3)` and `T†` is `conj P1(3)`. + +The table is the one every circuit/1 file, hash and receipt was made with, and it +is kept bit for bit. Its literal values (Appendix B.1) are normative by equality, +and so is its digest, BLAKE3 over `i64le(re) ‖ i64le(im)` for `k = 1..32` +(512 bytes, no domain prefix): +`3fa9805ff96feb819a1cf371fedcd3b5228f93eddf933d0edcf401cbf8c28466`. + +Its floors compound. The stored values deviate from `2^30·e^{2πi/2^k}`, in units: + +- `re P1(k)` is low by up to 1.3 units for every `k ≥ 3`; +- `im P1(k)` is within 2.2 units for `k ≤ 7`, then too large: +9.52 (`k = 8`), + +21.1 (9), +49.0 (10), +90.5 (11), +124.3 (12), +224.4 (13), +112.2 (14), + +1355.6 (15), +677.3 (16), +5283.2 (17, about 10% of the angle) and +7032.1 + (18, about 27%); +- `im P1(19..32) = 0`: these entries apply no phase at all, a deviation of the + whole angle, from −12868 units at `k = 19` to −1.57 at `k = 32`. + +A QFT on `n` qubits uses `P(2..n)`, so in circuit/1 a QFT on 8 or more qubits +applies phases that are off by more than 9 units, and one on 19 or more applies +none for its smallest rotations. + +### 3.2 The circuit2 phase table + +`P2(1) = (−2^30, 0)` and `P2(2) = (0, 2^30)`, both exact. For `k = 3..32`: + +``` +P2(k) = ( ⌊2^30·cos(2π/2^k) + ½⌋ , ⌊2^30·sin(2π/2^k) + ½⌋ ) +``` + +the integers nearest to the ideal values. For `k ≥ 3` every ideal value is +irrational, so none is a half-integer and the nearest integer is unique. The +closest any ideal value comes to a half-integer is 0.00456 unit, at `im P2(22)`, +so any computation of the ideal values accurate to better than that rounds to the +same table. `H` uses `h = re P2(3) = 759250125`, `T` is `P2(3)` and `T†` is +`conj P2(3)`; `re P2(3) = im P2(3)`. + +The literal values (Appendix B.2) are normative by equality, and an +implementation carries them as constants; it need not compute them. Their digest, +computed as for circuit/1, is +`d34cb793715348e7c92db027c0bc3dddb6008f5730470bafaed978ea7fb23510`. + +**The circuit2 table gives the QFT**: every rotation `P(2..32)` of the +controlled-phase ladder is within half a unit per component of its ideal value. +§5 states what that leaves of the error. + +*Derivation (informative).* An exact-integer procedure certifies the table with no +floating point. At scale `2^62` it keeps an enclosure +`c_lo ≤ 2^62·cos(2π/2^k) ≤ c_hi` and `s_lo ≤ 2^62·sin(2π/2^k) ≤ s_hi`, starting +from `(cos, sin)(π/2) = (0, 1)`, which is exact, and halves the angle with +`cos(θ/2) = √((1 + cos θ)/2)` and `sin(θ/2) = sin θ / (2 cos(θ/2))`, which has no +cancellation, rounding every bound outward: + +``` +S = 2^62 +ceil_isqrt(x) = r if r·r = x, else r + 1, where r = isqrt(x) +R(x) = (x + 2^31) >> 32 -- scale 2^62 to 2^30, half up +(c_lo, c_hi, s_lo, s_hi) = (0, 0, S, S) +for k in 3..=32: + c_lo' = isqrt(((S + c_lo)·S) >> 1) + c_hi' = ceil_isqrt(⌈(S + c_hi)·S / 2⌉) + s_lo' = ⌊s_lo·S / (2·c_hi')⌋ + s_hi' = ⌈s_hi·S / (2·c_lo')⌉ + require R(c_lo') = R(c_hi') = re P2(k) + require R(s_lo') = R(s_hi') = im P2(k) + require |(x mod 2^32) − 2^31| > 2^20 for x in {c_lo', c_hi', s_lo', s_hi'} + (c_lo, c_hi, s_lo, s_hi) = (c_lo', c_hi', s_lo', s_hi') +``` + +Every step keeps a true enclosure, and when both ends of an enclosure round to +the same integer, that integer is the correctly rounded value. Every product +fits 125 bits, every enclosure is at most 3 units of `2^−62` wide, and no end +comes within 19,590,902 units of `2^−62` of a tie. The reference implementation +runs this procedure as a test, and the corpus referee runs it independently. + ## 4. Conformance ### 4.1 statevector-equivalence -> Given the same `wai.quantum.circuit`, a conforming sink MUST reconstruct the -> identical fixed-point statevector, hence the identical BLAKE3 — on every -> machine, no tolerance parameter. +> Given the same circuit under the same capability, a conforming sink MUST +> reconstruct the identical fixed-point statevector, hence the identical +> BLAKE3 — on every machine, no tolerance parameter. ``` statevector_hash = BLAKE3("wai:quantum-statevector\x01" - || n_qubits || frac_le || canonical amps) + ‖ u8 n_qubits ‖ u32le frac + ‖ per amplitude, in index order: i64le re ‖ i64le im) circuit_hash = BLAKE3("wai:quantum-circuit\x01" - || contract_bytes || oplog_bytes) + ‖ canonical contract (§2.1) of the capability for n_qubits + ‖ canonical op-log) ``` -`circuit_hash` is the frame-independent identity of the circuit; each sink's -`statevector_hash` is the verifiable result. +The canonical op-log is the §2.2 encoding of the circuit's ops: `u32le n_ops`, +then each op, and nothing after. For circuit2 the canonical contract and op-log +are the bytes received, since the reader requires them. For circuit/1 the +contract is the re-rendered template, however the received one was formatted, +and bytes after the last op are not part of the op-log. + +`circuit_hash` is the identity of the circuit, and its contract names the +capability, so it binds the capability and its phase table: the same ops under +circuit/1 and circuit2 have different circuit hashes. `statevector_hash` hashes +the amplitudes alone and names no capability: the two capabilities give the same +statevector, and so the same hash, for a circuit in which no gate that reads the +table acts on a nonzero amplitude (one of `X` gates only, say). A run is +therefore identified by its `circuit_hash`, and each sink's `statevector_hash` +is the verifiable result. ### 4.2 shot-histogram-equivalence (measured circuits) For a circuit with a §2.3 measurement, a conforming sink MUST sample the -computational-basis outcome with the pinned `splitmix64` PRNG seeded by `seed` -(walking the un-normalized `|amp|²` prefix sums — no divide), producing an -identical per-basis-state count vector, hence: +computational-basis outcome as follows, producing an identical per-basis-state +count vector: + +- The weight of basis state `i` is `w_i = re_i² + im_i²`, exact; `W` is their + sum, and `cum_i` the sum of `w_0..w_i`. The weights are not normalised. +- `splitmix64` with state `s`, starting at `seed`, all arithmetic mod `2^64`: + `s ← s + 0x9E3779B97F4A7C15`; `z ← s`; + `z ← (z ⊕ (z >> 30))·0xBF58476D1CE4E5B9`; + `z ← (z ⊕ (z >> 27))·0x94D049BB133111EB`; the draw is `z ⊕ (z >> 31)`. +- Each of the `shots` shots draws `x`, takes `r = x mod W`, and counts the first + basis state `i` with `cum_i > r`. If `W` is 0, every count is 0. ``` histogram_hash = BLAKE3("wai:quantum-histogram\x01" - || n_qubits || seed_le || shots_le || counts_le) + ‖ u8 n_qubits ‖ u64le seed ‖ u64le shots + ‖ per basis state, in index order: u64le count) ``` +`seed`, `shots` and every count are `u64le`; `n_qubits` is a `u8` prefix. The +histogram depends only on the statevector and the measurement, so it names no +capability either. + ## 5. Fidelity and size (honest) Two honesties the format states rather than implies: @@ -163,22 +460,48 @@ Two honesties the format states rather than implies: 1. **The reconstruction is exact; the model it reconstructs is an approximation.** The statevector is byte-identical across sinks (statevector-equivalence), but it is a *fixed-point* approximation of the - ideal complex-amplitude unitary evolution — each gate carries a bounded - rounding error (`≈ 2^−FRAC` per op, accumulating roughly linearly). This is - the same call as `wai.video.int_motion`: byte-exact reconstruction of a - defined-lossy quantity. The tier is therefore `Lossless` in the *simulation* - sense (like world replay), and the per-gate error bound is the contract, not - a hidden claim. + ideal complex-amplitude unitary evolution. This is the same call as + `wai.video.int_motion`: byte-exact reconstruction of a defined-lossy quantity. + The tier is therefore `Lossless` in the *simulation* sense (like world + replay), and the error bound is part of the contract, not a hidden claim. + Per capability: + + - **circuit2.** Every table constant is within ½ unit of its ideal value, + which §3.2 proves, and every product within ½ unit of the exact product. + One gate applied to a pair of amplitudes whose squared moduli sum to at + most 1 therefore lands within 2 units per component of the ideal gate + applied to the same pair: each component takes at most two rounded + products, ½ unit each, and the constants' error of ½ unit each meets + components whose squares sum to at most 1, which gives `1 + √2/2 ≈ 1.71`. + The worst measured on random normalised pairs is under 1.5 units. Errors + accumulate roughly linearly in the number of gates. Some table entries have + a modulus slightly above 1, by at most 0.3603 unit (`||P2(k)|² − 1|` is at + most 0.73 unit), so a long run of one phase drifts the norm upward, at the + order of the product rounding: `P2(19)` applied `2^18` times to `|1⟩`, a + half turn, ends at modulus 1.0000189 and within `10^−5` rad of the half + turn. The resolution of `2^−30` is a floor no rounding removes: + `P2(26..32)` carry imaginary parts of 101 down to 2, so `P2(32)` is correct + to the nearest unit (ideal 1.5708, stored 2) and 27% off in relative angle. + A finer resolution needs a new capability. + - **circuit/1.** The same arithmetic with the constants of §3.1, which are + off by up to 12868 units, and `P1(19..32)` apply no phase. The bound above + does not hold: one `P1(k)` errs by as much as its constant does. `P1(19)` + applied `2^18` times to `|1⟩` ends at exactly `(1073479680, 0)`: each step + loses a unit and none rotates. + + A QFT on `n` qubits needs `P(2..n)`, so a 26-qubit QFT needs `P(19..26)` and + is circuit2-only. 2. **The state is exponential; the circuit is not.** A statevector is `2^n` amplitudes × 16 bytes — the reconstruction is heavy and grows exponentially. The *transportable* object is the circuit op-log, which is - linear in gate count (the reference corpus's QFT-5 circuit is 261 bytes and - reconstructs a 32-amplitude state; a 26-qubit circuit is still a few kilobytes - and reconstructs ~1 GiB). The win is exactly WAI's thesis — the wire carries - instructions, the sink reconstructs — and it is *why* the receipt matters: - because reconstruction cost is exponential, an energy-metered, byte-exact, - signed record of the computation is a genuinely novel artifact. + linear in gate count (the reference corpus's QFT-5 circuit is 261 bytes as + circuit/1 and 263 as circuit2, and reconstructs a 32-amplitude state; a + 26-qubit QFT is still a few kilobytes and reconstructs ~1 GiB). The win is + exactly WAI's thesis — the wire carries instructions, the sink + reconstructs — and it is *why* the receipt matters: because reconstruction + cost is exponential, an energy-metered, byte-exact, signed record of the + computation is a genuinely novel artifact. This extension is a way to make a quantum computation portable and verifiable. It is not a media codec and it is not a route to quantum advantage. @@ -192,7 +515,7 @@ is trusted the way it honestly can be.** | field | what it is | how it is trusted | |---|---|---| -| `circuit_hash` | identity — the `WQC` circuit's hash | re-checkable | +| `circuit_hash` | identity — the `WQC` circuit's hash, which names its capability | re-checkable | | `statevector_hash` | the byte-exact reconstruction result | **re-checkable** (re-simulate) | | `measurement` | optional `{seed, shots, histogram_hash}` | re-checkable | | `work_amp_updates` | `n_ops · 2^n` — the exponential reconstruction cost | **verified by recomputation** | @@ -209,6 +532,9 @@ over a Merkle root of the leaf hashes (`circuit`, `statevector`, `[histogram]`) the worlds / provenance / film receipt shape reused, `parent_receipt_hash` chaining a derivation exactly as they do. +The receipt format is the same for both capabilities. A receipt names its +capability through `circuit_hash`, whose contract names it (§4.1). + Two verification depths: - `verify()` — Merkle root recomputes, `work == n_ops · 2^n`, signature valid. @@ -220,6 +546,15 @@ Two verification depths: attested-only — re-simulation checks the *result and cost*, never the signer's energy draw.) +`verify_reconstruction` re-simulates the circuit under its own capability: the +circuit as decoded from its container, whose magic names it. A receipt sealed +under one capability therefore does not verify against the same ops under the +other. A receipt sealed before circuit2 existed (by `wai-quantum` 0.3.x) is a +circuit/1 receipt: re-verify it with its `WQC1` file, or with the circuit built +in circuit/1 (`Gateset::V1`). The reference's `gateset_of` reports under which +capability a circuit's ops match a receipt's `circuit_hash`; it explains a failed +reconstruction and decides nothing. + `to_json()` / `from_json()` give the portable, sorted-key JSON an out-of-band verifier reads. `wai_quantum_meter [receipt.json]` fills the energy honestly: on a host that exposes its SoC's power interface to @@ -238,9 +573,11 @@ measured figure the meter writes a measurement report (energy-measurement §6). verified with `Labelled::verify` / `Labelled::verify_reconstruction`. The `wai-quantum verify` CLI and the HTTP handler's `POST /verify` accept a receipt in either form (`quantum_energy::MaybeLabelled`) and restate its class. A receipt -sealed without a label keeps its legacy bytes and its figure is unlabelled. A -representative run on the reference QFT-5 vector: **≈43 µJ measured per -reconstruction, `work_amp_updates = 672`** (21 ops × 2⁵), sealed and self-verified. +sealed without a label keeps its legacy bytes and its figure is unlabelled. The +meter decodes the circuit from its container, so the receipt binds the capability +the file names. A representative run on the reference QFT-5 vector: **≈43 µJ +measured per reconstruction, `work_amp_updates = 672`** (21 ops × 2⁵), sealed and +self-verified. Because a quantum computation carries no speedup here, this is exactly the value the standard adds: an energy-accounted, byte-exact, signed record whose *result* @@ -261,7 +598,55 @@ stochastic. Such a prior is `NeuralFloat`/`Behavioral`: it MUST NOT back `wai.semantic.*` conforms by a CLIP-similarity threshold. Naming the metric now fixes the axis before that capability is ever registered. -## Appendix +## 8. Versions + +**Why circuit2 is a capability of its own.** The circuit/1 table comes from a +recurrence whose floors compound (§3.1). Correcting it under the same string +would change the payload format of a registered capability, which SPEC §8 +forbids: once a capability string is registered, its payload format is fixed +forever, and a later change never widens an existing string. It would also +change the reconstruction, the `circuit_hash` and every receipt of the content +made with it. So the corrected table is a new capability, `wai.quantum.circuit2`, +with a magic of its own, and circuit/1 stays as it was. + +**What a reader that predates circuit2 does.** It requires the magic `WQC1`, so +it refuses a `WQC2` payload at the magic, before it reads anything else. It does +not advertise `wai.quantum.circuit2`, so an envelope that names that capability +is inert to it (SPEC §4). It recomputes `circuit_hash` with the circuit/1 +contract, so a circuit2 receipt does not verify there. Each of these fails +closed: no older reader reconstructs circuit2 content with the circuit/1 table. + +**Payloads this revision makes non-conforming.** This revision states reader +rules for circuit/1 that enforce its registration as it was made (SPEC §8): the +contract's values (§2.1), the range of `n_qubits`, sections within the payload, +and one section of each known kind (§2). Earlier reference readers accepted +payloads that break them, and a reader of this revision refuses: + +- a `WQC1` contract whose `ext`, `numeric`, `frac` or `gateset` is missing or + carries another value, a circuit2 contract among them; +- an `n_qubits` that is not an unsigned integer of at most 255 (an earlier + reader read 258 as 2); +- a known section kind that appears more than once (an earlier reader kept the + last); +- a section whose bounds overflow (an earlier reader could wrap on a 32-bit + target); +- a contract or measurement that repeats a member name within one object (an + earlier reader kept the last), or that holds a noncharacter in a name or a + string; +- a contract or measurement holding a number of magnitude 2^1024 − 2^970 or + more that an earlier reader accepted. Earlier readers left the range to their + float parser, so depending on how they were built they accepted some such + numbers, and refused some numbers below the limit that a reader of this + revision accepts. + +The other rules of §2 refuse what earlier reference readers refused. + +No file written by any reference version is affected: every reference writer +wrote the canonical contract, one section of each kind, and sections within the +payload. The reference implementation adds circuit2 in `wai-quantum` 0.4.0, where +a circuit built in code is circuit2 unless it is built as circuit/1. + +## Appendix A — One floor, one more kind of state statevector-equivalence is replay-equivalence with the world state replaced by a quantum amplitude vector, and the deterministic floor extended from real @@ -271,3 +656,94 @@ cloud reconstruct now makes a quantum state reconstruct — one floor, one more cargo class, one verification. The state is heavy and the model approximate; the *reconstruction* of it is exact and auditable, and that — not a quantum speedup WAI does not claim — is the honest thing the standard adds. + +## Appendix B — The phase tables + +`P(k)` as `(re, im)` at scale `2^30`, for `k = 1..32`, with each table's digest: +BLAKE3 over `i64le(re) ‖ i64le(im)` for `k = 1..32`, 512 bytes, no domain prefix. +The tables and digests below are rendered from the reference implementation's +constants (`PHASE_V1`, `PHASE_V2`) by `wai_quantum_vectors appendix`, and a test +fails when this text differs from what the constants render. B.1 is circuit/1's +table (§3.1) and B.2 is circuit2's (§3.2). + + + +### B.1 `wai.quantum.circuit` + +| k | re P(k) | im P(k) | +|---:|---:|---:| +| 1 | -1073741824 | 0 | +| 2 | 0 | 1073741824 | +| 3 | 759250124 | 759250124 | +| 4 | 992008094 | 410903207 | +| 5 | 1053110175 | 209476638 | +| 6 | 1068571463 | 105245102 | +| 7 | 1072448454 | 52686012 | +| 8 | 1073418432 | 26350953 | +| 9 | 1073660972 | 13176485 | +| 10 | 1073721610 | 6588405 | +| 11 | 1073736770 | 3294284 | +| 12 | 1073740560 | 1647223 | +| 13 | 1073741507 | 823774 | +| 14 | 1073741744 | 411887 | +| 15 | 1073741803 | 207243 | +| 16 | 1073741818 | 103621 | +| 17 | 1073741822 | 56755 | +| 18 | 1073741823 | 32768 | +| 19 | 1073741823 | 0 | +| 20 | 1073741823 | 0 | +| 21 | 1073741823 | 0 | +| 22 | 1073741823 | 0 | +| 23 | 1073741823 | 0 | +| 24 | 1073741823 | 0 | +| 25 | 1073741823 | 0 | +| 26 | 1073741823 | 0 | +| 27 | 1073741823 | 0 | +| 28 | 1073741823 | 0 | +| 29 | 1073741823 | 0 | +| 30 | 1073741823 | 0 | +| 31 | 1073741823 | 0 | +| 32 | 1073741823 | 0 | + +Digest: `3fa9805ff96feb819a1cf371fedcd3b5228f93eddf933d0edcf401cbf8c28466` + +### B.2 `wai.quantum.circuit2` + +| k | re P(k) | im P(k) | +|---:|---:|---:| +| 1 | -1073741824 | 0 | +| 2 | 0 | 1073741824 | +| 3 | 759250125 | 759250125 | +| 4 | 992008094 | 410903207 | +| 5 | 1053110176 | 209476638 | +| 6 | 1068571464 | 105245103 | +| 7 | 1072448455 | 52686014 | +| 8 | 1073418433 | 26350943 | +| 9 | 1073660973 | 13176464 | +| 10 | 1073721611 | 6588356 | +| 11 | 1073736771 | 3294193 | +| 12 | 1073740561 | 1647099 | +| 13 | 1073741508 | 823550 | +| 14 | 1073741745 | 411775 | +| 15 | 1073741804 | 205887 | +| 16 | 1073741819 | 102944 | +| 17 | 1073741823 | 51472 | +| 18 | 1073741824 | 25736 | +| 19 | 1073741824 | 12868 | +| 20 | 1073741824 | 6434 | +| 21 | 1073741824 | 3217 | +| 22 | 1073741824 | 1608 | +| 23 | 1073741824 | 804 | +| 24 | 1073741824 | 402 | +| 25 | 1073741824 | 201 | +| 26 | 1073741824 | 101 | +| 27 | 1073741824 | 50 | +| 28 | 1073741824 | 25 | +| 29 | 1073741824 | 13 | +| 30 | 1073741824 | 6 | +| 31 | 1073741824 | 3 | +| 32 | 1073741824 | 2 | + +Digest: `d34cb793715348e7c92db027c0bc3dddb6008f5730470bafaed978ea7fb23510` + + diff --git a/wai/quantum-conformance/README.md b/wai/quantum-conformance/README.md index 715cd7308..d40d872c6 100644 --- a/wai/quantum-conformance/README.md +++ b/wai/quantum-conformance/README.md @@ -1,12 +1,22 @@ # quantum-conformance -Statevector-equivalence for the `wai.quantum.circuit` extension: given the same -`WQC` circuit, a conforming sink MUST reconstruct the identical fixed-point -statevector — **hash equality, no tolerance**. +Statevector-equivalence for both capabilities of the quantum-sim extension +(`../extensions/quantum-sim.md`): given the same `WQC` circuit, a conforming sink +MUST reconstruct the identical fixed-point statevector — **hash equality, no +tolerance**. **Provenance.** Generated here by `wai_quantum_vectors` from circuits authored in code. No media. -`vectors/*.wqc` plus `expected_hashes.json` are the pinned corpus. They are the +Four trees, each a pinned corpus: + +| tree | capability | what | +|---|---|---| +| `vectors/` + `expected_hashes.json` | `wai.quantum.circuit` (`WQC1`) | bell, ghz3, qft3 and qft5, frozen as they were first written, then qft5_x22, dyadic_ladder and qft8_ones | +| `v2/vectors/` + `v2/expected_hashes.json` | `wai.quantum.circuit2` (`WQC2`) | the same seven circuits; each entry names its `capability` | +| `reject/` + `reject/reject.json` | both | payloads a conforming reader refuses, each with the class it is refused as: `malformed`, `contract` or `invalid` (extension §2) | +| `accept/` + `accept/accept.json` | both | payloads a conforming reader accepts, at the edges of the same rules, each with how it reads: capability, `n_qubits`, op count, circuit hash and measurement (extension §2) | + +An entry with no `capability` is a `wai.quantum.circuit` vector. The trees are the one reference all three surfaces are checked against, rather than being checked against each other. @@ -28,19 +38,61 @@ wasmtime run --dir wai/quantum-conformance::/corpus \ target/wasm32-wasip2/release/wai_quantum_vectors.wasm verify /corpus ``` +CI runs this leg as a debug build, so integer overflow panics instead of wrapping +(`wai-conformance.yml`, job `quantum-wasip2`, which also runs the crate's +`bit_identity` tests and the circuit core's unit tests under `wasmtime`). + ## Browser ```sh -node wai/quantum-conformance/browser_verify.mjs [path/to/quantum-ml/pkg] +node wai/quantum-conformance/browser_verify.mjs [path/to/quantum-ml/pkg] [module file] ``` -Drives the deployed wasm-bindgen bundle in headless Chromium. Note that -`statevector_hash_hex()` is empty until `run()` is called — the circuit is built -eagerly, the state is not. +Drives the deployed wasm-bindgen bundle in headless Chromium: the sink opens every +file of both vector trees to its manifest entry, refuses every reject file with +its listed class and reads every accept file as listed (the two 255-qubit accept +files only to the `simulate` stage: the sink simulates on construction, so they +are checked to be read, not compared), and the IDE builds its catalog circuits in each gateset to the +matching manifest. It is run by hand, before the website deploys a bundle. Note that +the IDE's `statevector_hash_hex()` is empty until `run()` is called — the circuit is +built eagerly, the state is not. + +## Referee + +```sh +python3 -I wai/quantum-conformance/tools/referee.py wai/quantum-conformance +``` + +A second implementation of the extension in Python, integers only, sharing no code +with the Rust reference. It derives both phase tables (circuit/1 by its recurrence, +circuit2 by an interval-certified procedure) and requires their literal values and +digests, and the extension's Appendix B to state the same; rebuilds every vector +file byte for byte and recomputes every manifest entry; refuses every reject file +with its listed class and by the rule its name states; accepts every accept file +and reads it as listed; and requires +`dyadic_ladder` to change its statevector hash under every ±1 change of every +phase-table entry, in both tables. CI runs it on Linux (`wai-conformance.yml`, +job `independent-verifiers`). ## Scope -The corpus covers the **byte-exact dyadic core**, where hash equality is the right -bar. The floating-point layers (`quantum_source`, `quantum_channel`) are held to +The corpus pins the container, the reader's refusals, the fixed-point arithmetic +and both phase tables, where hash equality is the right bar. What it covers, and +where it stops: + +- **Every phase-table entry.** Under either table, a change of ±1 to either + component of any `P(1..32)`, with `H` following `re P(3)`, changes + `dyadic_ladder`'s statevector hash; the referee checks this on every run. A + change of ±1 to `H` alone changes the hash of every vector that applies `H`. +- **The four original vectors see no `P(k)`.** They run the QFT from + `|0…0⟩`, where every controlled phase acts on zero amplitudes, so no change to a + `P(k)` entry moves their hashes except through `H`, which follows `re P(3)`; the + three vectors after them are there for that. +- **One control at most.** No vector applies a gate with more than one control. + Such gates appear in the reject tree, and in the accept tree as circuit/1's + repeated control. +- **One measurement.** The shot sampler is pinned by bell's measurement alone. + +The floating-point layers (`quantum_source`, `quantum_channel`) are held to exact IEEE-754 bits by unit tests instead, since they have no `WQC` circuit to pin — see `entropy_is_bit_identical_across_architectures`. diff --git a/wai/quantum-conformance/accept/accept.json b/wai/quantum-conformance/accept/accept.json new file mode 100644 index 000000000..4d1915f9c --- /dev/null +++ b/wai/quantum-conformance/accept/accept.json @@ -0,0 +1,177 @@ +{ + "v1_members_reordered.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_whitespace.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_further_members.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_escapes_and_characters.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_depth_127.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_numbers_largest_finite.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_numbers_long_literals.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_numbers_toward_zero.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_n_qubits_255.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 255, + "n_ops": 2, + "circuit_hash": "eaa87747d510e6d6cff04212fbcecf37aa22fa6fed8fe716ea3110092e6045e0", + "measure": null + }, + "v1_measure_lax.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 1, + "shots": 10 + } + }, + "v1_measure_missing_shots.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 1, + "shots": 0 + } + }, + "v1_measure_u64_max.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 18446744073709551615, + "shots": 18446744073709551615 + } + }, + "v1_measure_not_u64.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 0, + "shots": 0 + } + }, + "v1_measure_other_types.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 0, + "shots": 0 + } + }, + "v1_measure_not_object.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": { + "seed": 0, + "shots": 0 + } + }, + "v1_oplog_trailing_bytes.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v1_param_on_h.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "2eed24488205bf419b886feed0947cc63a21813f62f3a7a697b5785adcc72b08", + "measure": null + }, + "v1_repeated_control.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "3aaa7132a1cc0f068c8f009f4e8d6137a825f6d22c2e5a32255a4a38c547755b", + "measure": null + }, + "v1_unknown_sections.wqc": { + "capability": "wai.quantum.circuit", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "measure": null + }, + "v2_unknown_sections.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b", + "measure": null + }, + "v2_n_qubits_255.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 255, + "n_ops": 2, + "circuit_hash": "225f13952c14c6375adf7cc85080b1dd34af8f5378e1dac737517e0212d74c32", + "measure": null + }, + "v2_measure_u64_max.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b", + "measure": { + "seed": 18446744073709551615, + "shots": 18446744073709551615 + } + } +} diff --git a/wai/quantum-conformance/accept/v1_depth_127.wqc b/wai/quantum-conformance/accept/v1_depth_127.wqc new file mode 100644 index 000000000..162b46dcd Binary files /dev/null and b/wai/quantum-conformance/accept/v1_depth_127.wqc differ diff --git a/wai/quantum-conformance/accept/v1_escapes_and_characters.wqc b/wai/quantum-conformance/accept/v1_escapes_and_characters.wqc new file mode 100644 index 000000000..2065ea597 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_escapes_and_characters.wqc differ diff --git a/wai/quantum-conformance/accept/v1_further_members.wqc b/wai/quantum-conformance/accept/v1_further_members.wqc new file mode 100644 index 000000000..5f777e29d Binary files /dev/null and b/wai/quantum-conformance/accept/v1_further_members.wqc differ diff --git a/wai/quantum-conformance/accept/v1_measure_lax.wqc b/wai/quantum-conformance/accept/v1_measure_lax.wqc new file mode 100644 index 000000000..bcb8a9245 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_measure_lax.wqc differ diff --git a/wai/quantum-conformance/accept/v1_measure_missing_shots.wqc b/wai/quantum-conformance/accept/v1_measure_missing_shots.wqc new file mode 100644 index 000000000..6952eac5e Binary files /dev/null and b/wai/quantum-conformance/accept/v1_measure_missing_shots.wqc differ diff --git a/wai/quantum-conformance/accept/v1_measure_not_object.wqc b/wai/quantum-conformance/accept/v1_measure_not_object.wqc new file mode 100644 index 000000000..c61643c89 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_measure_not_object.wqc differ diff --git a/wai/quantum-conformance/accept/v1_measure_not_u64.wqc b/wai/quantum-conformance/accept/v1_measure_not_u64.wqc new file mode 100644 index 000000000..95a06dd3e Binary files /dev/null and b/wai/quantum-conformance/accept/v1_measure_not_u64.wqc differ diff --git a/wai/quantum-conformance/accept/v1_measure_other_types.wqc b/wai/quantum-conformance/accept/v1_measure_other_types.wqc new file mode 100644 index 000000000..e18700394 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_measure_other_types.wqc differ diff --git a/wai/quantum-conformance/accept/v1_measure_u64_max.wqc b/wai/quantum-conformance/accept/v1_measure_u64_max.wqc new file mode 100644 index 000000000..18d201750 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_measure_u64_max.wqc differ diff --git a/wai/quantum-conformance/accept/v1_members_reordered.wqc b/wai/quantum-conformance/accept/v1_members_reordered.wqc new file mode 100644 index 000000000..36995bde5 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_members_reordered.wqc differ diff --git a/wai/quantum-conformance/accept/v1_n_qubits_255.wqc b/wai/quantum-conformance/accept/v1_n_qubits_255.wqc new file mode 100644 index 000000000..702f3559b Binary files /dev/null and b/wai/quantum-conformance/accept/v1_n_qubits_255.wqc differ diff --git a/wai/quantum-conformance/accept/v1_numbers_largest_finite.wqc b/wai/quantum-conformance/accept/v1_numbers_largest_finite.wqc new file mode 100644 index 000000000..0eb69a32e Binary files /dev/null and b/wai/quantum-conformance/accept/v1_numbers_largest_finite.wqc differ diff --git a/wai/quantum-conformance/accept/v1_numbers_long_literals.wqc b/wai/quantum-conformance/accept/v1_numbers_long_literals.wqc new file mode 100644 index 000000000..70318dad2 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_numbers_long_literals.wqc differ diff --git a/wai/quantum-conformance/accept/v1_numbers_toward_zero.wqc b/wai/quantum-conformance/accept/v1_numbers_toward_zero.wqc new file mode 100644 index 000000000..08eddd288 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_numbers_toward_zero.wqc differ diff --git a/wai/quantum-conformance/accept/v1_oplog_trailing_bytes.wqc b/wai/quantum-conformance/accept/v1_oplog_trailing_bytes.wqc new file mode 100644 index 000000000..dbb344b69 Binary files /dev/null and b/wai/quantum-conformance/accept/v1_oplog_trailing_bytes.wqc differ diff --git a/wai/quantum-conformance/accept/v1_param_on_h.wqc b/wai/quantum-conformance/accept/v1_param_on_h.wqc new file mode 100644 index 000000000..5d90861ca Binary files /dev/null and b/wai/quantum-conformance/accept/v1_param_on_h.wqc differ diff --git a/wai/quantum-conformance/accept/v1_repeated_control.wqc b/wai/quantum-conformance/accept/v1_repeated_control.wqc new file mode 100644 index 000000000..5db2ea7fe Binary files /dev/null and b/wai/quantum-conformance/accept/v1_repeated_control.wqc differ diff --git a/wai/quantum-conformance/accept/v1_unknown_sections.wqc b/wai/quantum-conformance/accept/v1_unknown_sections.wqc new file mode 100644 index 000000000..81527728a Binary files /dev/null and b/wai/quantum-conformance/accept/v1_unknown_sections.wqc differ diff --git a/wai/quantum-conformance/accept/v1_whitespace.wqc b/wai/quantum-conformance/accept/v1_whitespace.wqc new file mode 100644 index 000000000..a046def8c Binary files /dev/null and b/wai/quantum-conformance/accept/v1_whitespace.wqc differ diff --git a/wai/quantum-conformance/accept/v2_measure_u64_max.wqc b/wai/quantum-conformance/accept/v2_measure_u64_max.wqc new file mode 100644 index 000000000..8f38a87ca Binary files /dev/null and b/wai/quantum-conformance/accept/v2_measure_u64_max.wqc differ diff --git a/wai/quantum-conformance/accept/v2_n_qubits_255.wqc b/wai/quantum-conformance/accept/v2_n_qubits_255.wqc new file mode 100644 index 000000000..d1424da6c Binary files /dev/null and b/wai/quantum-conformance/accept/v2_n_qubits_255.wqc differ diff --git a/wai/quantum-conformance/accept/v2_unknown_sections.wqc b/wai/quantum-conformance/accept/v2_unknown_sections.wqc new file mode 100644 index 000000000..f781fef67 Binary files /dev/null and b/wai/quantum-conformance/accept/v2_unknown_sections.wqc differ diff --git a/wai/quantum-conformance/browser_verify.mjs b/wai/quantum-conformance/browser_verify.mjs index e8db3973c..f2a20f45f 100644 --- a/wai/quantum-conformance/browser_verify.mjs +++ b/wai/quantum-conformance/browser_verify.mjs @@ -1,14 +1,30 @@ -// Browser leg of statevector-equivalence. +// Browser leg of statevector-equivalence, for both capabilities. // // The native and `wasm32-wasip2` legs are checked by `wai_quantum_vectors verify`. -// This checks the third surface the extension claims — a browser — against the -// same pinned corpus, so all three are held to one reference rather than to each -// other. +// This checks the third surface the extension claims, a browser, against the same +// pinned corpus, so all three are held to one reference rather than to each other. +// It is a manual tool: run it on the bundle before the website deploys it. // -// node browser_verify.mjs [path/to/quantum-ml/pkg] +// node browser_verify.mjs [path/to/quantum-ml/pkg] [module file] // -// Needs Playwright with its bundled Chromium. `statevector_hash_hex()` is empty -// until `run()` is called: the circuit is built eagerly, the state is not. +// In headless Chromium, with the wasm-bindgen bundle (module file +// `wai_quantum_ml.js` by default): +// +// - QuantumSink opens every file of `vectors/` (wai.quantum.circuit) and +// `v2/vectors/` (wai.quantum.circuit2) to its manifest entry: the capability (an +// entry of the circuit/1 manifest names none), n_qubits, n_ops, the circuit and +// statevector hashes, and the histogram hash where the entry pins a measurement. +// - QuantumSink refuses every file `reject/reject.json` lists, and its error names +// the listed class in parentheses: ` (): `. +// - QuantumSink reads every file `accept/accept.json` lists as listed: capability, +// n_qubits, n_ops, circuit hash and measurement. A file too wide to simulate +// (n_qubits 255) is read and then refused at the `simulate` stage, never at +// `WQC parse`. +// - QuantumIde.load_algorithm builds bell, ghz and qft in its default gateset to +// `v2/expected_hashes.json`, and after `set_gateset(1)` to the circuit/1 manifest. +// +// Needs Playwright with its bundled Chromium. `statevector_hash_hex()` of the IDE is +// empty until `run()` is called: the circuit is built eagerly, the state is not. import http from "node:http"; import fs from "node:fs"; import path from "node:path"; @@ -18,19 +34,85 @@ import { createRequire } from "node:module"; const HERE = path.dirname(fileURLToPath(import.meta.url)); const PKG = process.argv[2] || "/Users/dcharlot/data-share/vibe-coding/wai-transaction-science-web/public/quantum-ml/pkg"; +const MODULE = process.argv[3] || "wai_quantum_ml.js"; const PLAYWRIGHT_HOST = process.env.PLAYWRIGHT_HOST || "/Users/dcharlot/data-share/vibe-coding/joulesperbit/"; const { chromium } = createRequire(PLAYWRIGHT_HOST)("playwright"); -const EXPECTED = JSON.parse(fs.readFileSync(path.join(HERE, "expected_hashes.json"), "utf8")); -// vector name -> how the catalog builds that circuit -const CASES = [ - ["bell.wqc", "bell", 2, 0], - ["ghz3.wqc", "ghz", 3, 0], - ["qft3.wqc", "qft", 3, 0], - ["qft5.wqc", "qft", 5, 0], +const readJson = (p) => JSON.parse(fs.readFileSync(p, "utf8")); +const bytesOf = (p) => Array.from(fs.readFileSync(p)); + +// The two vector trees. An entry of the circuit/1 manifest names no capability. +const TREES = [ + { prefix: "", dir: HERE, capability: "wai.quantum.circuit" }, + { prefix: "v2/", dir: path.join(HERE, "v2"), capability: null }, ]; +const MANIFEST = Object.fromEntries(TREES.map((t) => [t.prefix, readJson(path.join(t.dir, "expected_hashes.json"))])); + +// Every vector file, with what the sink must report for it. +const SINK = []; +for (const t of TREES) { + for (const [file, e] of Object.entries(MANIFEST[t.prefix])) { + SINK.push({ + label: t.prefix + file, + bytes: bytesOf(path.join(t.dir, "vectors", file)), + want: { + capability: e.capability || t.capability, + n_qubits: e.n_qubits, + n_ops: e.n_ops, + circuit: e.circuit_hash, + statevector: e.statevector_hash, + histogram: e.measure ? e.measure.histogram_hash : null, + }, + }); + } +} + +// Every reject file, with the class it must be refused as. +const REJECT_DIR = path.join(HERE, "reject"); +const REJECT = Object.entries(readJson(path.join(REJECT_DIR, "reject.json"))).map(([file, kind]) => ({ + label: "reject/" + file, + bytes: bytesOf(path.join(REJECT_DIR, file)), + kind, +})); + +// Every accept file, with its reading. seed and shots are u64, beyond what a JSON +// number keeps exactly in JS, so they are compared as decimal strings. +const ACCEPT_DIR = path.join(HERE, "accept"); +const acceptText = fs.readFileSync(path.join(ACCEPT_DIR, "accept.json"), "utf8") + .replace(/"(seed|shots)": (\d+)/g, '"$1": "$2"'); +const ACCEPT = Object.entries(JSON.parse(acceptText)).map(([file, e]) => ({ + label: "accept/" + file, + bytes: bytesOf(path.join(ACCEPT_DIR, file)), + want: { + capability: e.capability, + n_qubits: e.n_qubits, + n_ops: e.n_ops, + circuit: e.circuit_hash, + measure: e.measure ? `${e.measure.seed} ${e.measure.shots}` : null, + }, +})); + +// The IDE's catalog: vector file -> (algorithm id, qubits), in each gateset against +// its manifest. Gateset 2 (circuit2) is the IDE's default; 1 is set explicitly. +const CATALOG = [ + ["bell.wqc", "bell", 2], + ["ghz3.wqc", "ghz", 3], + ["qft3.wqc", "qft", 3], + ["qft5.wqc", "qft", 5], +]; +const IDE = []; +for (const [gateset, prefix, capability] of [[2, "v2/", "wai.quantum.circuit2"], [1, "", "wai.quantum.circuit"]]) { + for (const [file, id, n] of CATALOG) { + const e = MANIFEST[prefix][file]; + IDE.push({ + label: `QuantumIde ${id}(${n}) in gateset ${gateset} = ${prefix}${file}`, + id, n, gateset, + want: { gateset, capability, circuit: e.circuit_hash, statevector: e.statevector_hash }, + }); + } +} const MIME = { ".js": "text/javascript", ".wasm": "application/wasm", ".html": "text/html" }; const PORT = Number(process.env.PORT || 8794); @@ -46,12 +128,17 @@ const server = http.createServer((req, res) => { fs.createReadStream(f).pipe(res); }); +// Compare the fields of `want` with `got`; the names of those that differ. +function differ(want, got) { + return Object.keys(want).filter((k) => got[k] !== want[k]); +} + let browser; process.on("unhandledRejection", (e) => { console.error("UNHANDLED:", e); process.exitCode = 1; }); try { - if (!fs.existsSync(path.join(PKG, "wai_quantum_ml.js"))) { - console.error(`no bundle at ${PKG} — pass the pkg directory as argv[1]`); + if (!fs.existsSync(path.join(PKG, MODULE))) { + console.error(`no ${MODULE} at ${PKG}; pass the pkg directory (and the module file) as arguments`); process.exit(2); } await new Promise((r) => server.listen(PORT, "127.0.0.1", r)); @@ -64,37 +151,126 @@ try { // but networkidle still stalls behind them on some hosts. await page.goto(`http://127.0.0.1:${PORT}/`, { waitUntil: "domcontentloaded", timeout: 30000 }); - const got = await page.evaluate(async ({ cases, port }) => { - const mod = await import(`http://127.0.0.1:${port}/wai_quantum_ml.js`); + const got = await page.evaluate(async ({ sink, reject, accept, ide, port, module }) => { + const mod = await import(`http://127.0.0.1:${port}/${module}`); await mod.default(); - const out = {}; - for (const [name, id, n, param] of cases) { - const ide = new mod.QuantumIde(n); - const ok = ide.load_algorithm(id, n, param); - if (ok) ide.run(); // the state is not computed until this - out[name] = ok - ? { circuit: ide.circuit_hash_hex(), statevector: ide.statevector_hash_hex() } - : { error: "load_algorithm returned false" }; - ide.free(); + const message = (e) => String((e && e.message) || e); + const out = { sink: {}, reject: {}, accept: {}, ide: {} }; + for (const c of sink) { + try { + const s = new mod.QuantumSink(new Uint8Array(c.bytes)); + out.sink[c.label] = { + capability: s.capability(), + n_qubits: s.n_qubits(), + n_ops: s.n_ops(), + circuit: s.circuit_hash_hex(), + statevector: s.statevector_hash_hex(), + histogram: s.histogram_hash_hex() ?? null, + }; + s.free(); + } catch (e) { + out.sink[c.label] = { error: message(e) }; + } + } + for (const c of reject) { + try { + new mod.QuantumSink(new Uint8Array(c.bytes)).free(); + out.reject[c.label] = { accepted: true }; + } catch (e) { + out.reject[c.label] = { error: message(e) }; + } + } + for (const c of accept) { + try { + const s = new mod.QuantumSink(new Uint8Array(c.bytes)); + out.accept[c.label] = { + capability: s.capability(), + n_qubits: s.n_qubits(), + n_ops: s.n_ops(), + circuit: s.circuit_hash_hex(), + measure: s.has_measure() ? `${s.seed()} ${s.shots()}` : null, + }; + s.free(); + } catch (e) { + out.accept[c.label] = { error: message(e) }; + } + } + for (const c of ide) { + const q = new mod.QuantumIde(c.n); + try { + if (c.gateset !== 2 && !q.set_gateset(c.gateset)) { + out.ide[c.label] = { error: `set_gateset(${c.gateset}) returned false` }; + continue; + } + if (!q.load_algorithm(c.id, c.n, 0)) { + out.ide[c.label] = { error: "load_algorithm returned false" }; + continue; + } + q.run(); // the state is not computed until this + out.ide[c.label] = { + gateset: q.gateset(), + capability: q.capability(), + circuit: q.circuit_hash_hex(), + statevector: q.statevector_hash_hex(), + }; + } catch (e) { + out.ide[c.label] = { error: message(e) }; + } finally { + q.free(); + } } return out; - }, { cases: CASES, port: PORT }); + }, { sink: SINK, reject: REJECT, accept: ACCEPT, ide: IDE, port: PORT, module: MODULE }); let fail = 0; - for (const [name] of CASES) { - const e = EXPECTED[name], g = got[name]; - if (g.circuit === e.circuit_hash && g.statevector === e.statevector_hash) { - console.log(`PASS ${name}`); - } else { + const report = (label, problem) => { + if (problem) { fail++; - console.log(`FAIL ${name}`); - console.log(` circuit browser=${g.circuit}\n corpus =${e.circuit_hash}`); - console.log(` statevector browser=${g.statevector}\n corpus =${e.statevector_hash}`); + console.log(`FAIL ${label}: ${problem}`); + } else { + console.log(`PASS ${label}`); } + }; + for (const c of SINK) { + const g = got.sink[c.label]; + if (g.error) { + report(c.label, `refused: ${g.error}`); + continue; + } + const bad = differ(c.want, g); + report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); } - console.log(errs.length ? `console errors: ${errs.length}` : "console errors: 0"); + for (const c of REJECT) { + const g = got.reject[c.label]; + if (g.accepted) { + report(c.label, `accepted; it must be refused as ${c.kind}`); + continue; + } + const m = /^[^()]+ \(([a-z-]+)\): /.exec(g.error); + report(`${c.label} refused as ${c.kind}`, m && m[1] === c.kind ? null : `refused as: ${g.error}`); + } + for (const c of ACCEPT) { + const g = got.accept[c.label]; + if (g.error) { + const wide = c.want.n_qubits === 255 && /^simulate \(/.test(g.error); + report(wide ? `${c.label} read; too wide to simulate` : c.label, wide ? null : `refused: ${g.error}`); + continue; + } + const bad = differ(c.want, g); + report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); + } + for (const c of IDE) { + const g = got.ide[c.label]; + if (g.error) { + report(c.label, g.error); + continue; + } + const bad = differ(c.want, g); + report(c.label, bad.length ? bad.map((k) => `${k} browser=${g[k]} corpus=${c.want[k]}`).join("; ") : null); + } + console.log(errs.length ? `console errors: ${errs.length}\n ${errs.join("\n ")}` : "console errors: 0"); if (fail === 0 && errs.length === 0) { - console.log("all vectors pass: statevector-equivalence holds in the browser"); + console.log("all checks pass: statevector-equivalence holds in the browser for both capabilities, every reject file is refused as listed, and every accept file reads as listed"); } process.exitCode = fail || errs.length ? 1 : 0; } catch (e) { diff --git a/wai/quantum-conformance/expected_hashes.json b/wai/quantum-conformance/expected_hashes.json index 3afca1bbd..df6b4e8e3 100644 --- a/wai/quantum-conformance/expected_hashes.json +++ b/wai/quantum-conformance/expected_hashes.json @@ -27,5 +27,23 @@ "n_ops": 21, "circuit_hash": "f36a9af99a251cae6f42144b2be3c57523f15e1e2ab3492cf15cac709fc048be", "statevector_hash": "c268d77f2ddf84fecb028e80ce989123912870f04e5b9a1e3d885c9ce235c27d" + }, + "qft5_x22.wqc": { + "n_qubits": 5, + "n_ops": 24, + "circuit_hash": "40c847fe9f85b6d79d245b6e65420197e6bbedb9c0b1ab77baaf1cfe077ff4f5", + "statevector_hash": "95a2d2fd41f6a13c19562729a4b553ee12db6adfaa361b5061c16d801354d0cd" + }, + "dyadic_ladder.wqc": { + "n_qubits": 2, + "n_ops": 72, + "circuit_hash": "2fa5b02c952dcaf55ab4cfd2ea5b3fae999ac912a32a4e06dd2b6ee96bb58d4f", + "statevector_hash": "9754af24edd026601297a0f3334eb7171174978841a2938c9b372db942a3851d" + }, + "qft8_ones.wqc": { + "n_qubits": 8, + "n_ops": 56, + "circuit_hash": "5ca94d3a960b8ec65a1eb802699f1ed8e20f00e7fbd9297849f0c60047404a1a", + "statevector_hash": "fd683498744552e9e2e8657a2bdc244e4f03d4ee4dd292c9705ba38b19fb709c" } } diff --git a/wai/quantum-conformance/reject/magic_wqc3.wqc b/wai/quantum-conformance/reject/magic_wqc3.wqc new file mode 100644 index 000000000..05b15a486 Binary files /dev/null and b/wai/quantum-conformance/reject/magic_wqc3.wqc differ diff --git a/wai/quantum-conformance/reject/no_contract.wqc b/wai/quantum-conformance/reject/no_contract.wqc new file mode 100644 index 000000000..9994546a8 Binary files /dev/null and b/wai/quantum-conformance/reject/no_contract.wqc differ diff --git a/wai/quantum-conformance/reject/no_oplog.wqc b/wai/quantum-conformance/reject/no_oplog.wqc new file mode 100644 index 000000000..d7f946b2c Binary files /dev/null and b/wai/quantum-conformance/reject/no_oplog.wqc differ diff --git a/wai/quantum-conformance/reject/reject.json b/wai/quantum-conformance/reject/reject.json new file mode 100644 index 000000000..dd288d20a --- /dev/null +++ b/wai/quantum-conformance/reject/reject.json @@ -0,0 +1,68 @@ +{ + "magic_wqc3.wqc": "malformed", + "v1_ext_other.wqc": "contract", + "v1_numeric_other.wqc": "contract", + "v1_frac_31.wqc": "contract", + "v1_frac_float.wqc": "contract", + "v1_gateset_rn.wqc": "contract", + "v1_contract2_in_wqc1.wqc": "contract", + "v1_fields_missing.wqc": "contract", + "v1_contract_not_object.wqc": "contract", + "v1_n_qubits_256.wqc": "malformed", + "v1_n_qubits_258.wqc": "malformed", + "v1_contract_twice.wqc": "malformed", + "v1_oplog_twice.wqc": "malformed", + "v1_contract_duplicate_member.wqc": "contract", + "v1_contract_lone_surrogate.wqc": "contract", + "v1_contract_too_deep.wqc": "contract", + "v1_n_qubits_minus0.wqc": "malformed", + "v1_contract_lone_low_surrogate.wqc": "contract", + "v1_contract_noncharacter.wqc": "contract", + "v1_contract_raw_noncharacter.wqc": "contract", + "v1_contract_integer_rounds_to_infinity.wqc": "contract", + "v1_contract_number_rounds_to_infinity.wqc": "contract", + "v1_contract_long_literal.wqc": "contract", + "v1_contract_bom.wqc": "contract", + "v1_contract_nan.wqc": "contract", + "v1_contract_control_character.wqc": "contract", + "v1_measure_duplicate_member.wqc": "malformed", + "v1_contract_not_utf8.wqc": "contract", + "v1_contract_number_out_of_range.wqc": "contract", + "v1_n_qubits_fraction.wqc": "malformed", + "v1_n_qubits_missing.wqc": "malformed", + "v1_measure_not_json.wqc": "malformed", + "short_payload.wqc": "malformed", + "table_past_end.wqc": "malformed", + "section_past_end.wqc": "malformed", + "unknown_section_past_end.wqc": "malformed", + "no_contract.wqc": "malformed", + "no_oplog.wqc": "malformed", + "v1_oplog_short.wqc": "malformed", + "v1_op_truncated_controls.wqc": "malformed", + "v1_opcode_10.wqc": "malformed", + "v1_target_out_of_range.wqc": "invalid", + "v1_control_out_of_range.wqc": "invalid", + "v1_control_is_target.wqc": "invalid", + "v1_phase_0.wqc": "invalid", + "v1_phase_33.wqc": "invalid", + "v1_n_ops_u32max.wqc": "malformed", + "v1_offset_wrap.wqc": "malformed", + "v2_contract_whitespace.wqc": "contract", + "v2_contract_key_order.wqc": "contract", + "v2_contract_extra_member.wqc": "contract", + "v2_contract1_in_wqc2.wqc": "contract", + "v2_n_qubits_256.wqc": "malformed", + "v2_contract_not_object.wqc": "contract", + "v2_n_qubits_rounds_to_infinity.wqc": "contract", + "v2_n_qubits_largest_finite.wqc": "malformed", + "v2_phase_0.wqc": "invalid", + "v2_measure_twice.wqc": "malformed", + "v2_measure_seed_signed.wqc": "malformed", + "v2_duplicate_controls.wqc": "invalid", + "v2_param_on_h.wqc": "invalid", + "v2_oplog_trailing_byte.wqc": "malformed", + "v2_opcode_10.wqc": "malformed", + "v2_measure_spaced.wqc": "malformed", + "v2_measure_no_shots.wqc": "malformed", + "v2_measure_basis_x.wqc": "malformed" +} diff --git a/wai/quantum-conformance/reject/section_past_end.wqc b/wai/quantum-conformance/reject/section_past_end.wqc new file mode 100644 index 000000000..cd91fbe7c Binary files /dev/null and b/wai/quantum-conformance/reject/section_past_end.wqc differ diff --git a/wai/quantum-conformance/reject/short_payload.wqc b/wai/quantum-conformance/reject/short_payload.wqc new file mode 100644 index 000000000..9762d59e3 Binary files /dev/null and b/wai/quantum-conformance/reject/short_payload.wqc differ diff --git a/wai/quantum-conformance/reject/table_past_end.wqc b/wai/quantum-conformance/reject/table_past_end.wqc new file mode 100644 index 000000000..24b1ff320 Binary files /dev/null and b/wai/quantum-conformance/reject/table_past_end.wqc differ diff --git a/wai/quantum-conformance/reject/unknown_section_past_end.wqc b/wai/quantum-conformance/reject/unknown_section_past_end.wqc new file mode 100644 index 000000000..2b83adaf3 Binary files /dev/null and b/wai/quantum-conformance/reject/unknown_section_past_end.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract2_in_wqc1.wqc b/wai/quantum-conformance/reject/v1_contract2_in_wqc1.wqc new file mode 100644 index 000000000..f048b802b Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract2_in_wqc1.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_bom.wqc b/wai/quantum-conformance/reject/v1_contract_bom.wqc new file mode 100644 index 000000000..172476497 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_bom.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_control_character.wqc b/wai/quantum-conformance/reject/v1_contract_control_character.wqc new file mode 100644 index 000000000..e79749855 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_control_character.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_duplicate_member.wqc b/wai/quantum-conformance/reject/v1_contract_duplicate_member.wqc new file mode 100644 index 000000000..7571545f8 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_duplicate_member.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_integer_rounds_to_infinity.wqc b/wai/quantum-conformance/reject/v1_contract_integer_rounds_to_infinity.wqc new file mode 100644 index 000000000..73397baa5 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_integer_rounds_to_infinity.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_lone_low_surrogate.wqc b/wai/quantum-conformance/reject/v1_contract_lone_low_surrogate.wqc new file mode 100644 index 000000000..02327527e Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_lone_low_surrogate.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_lone_surrogate.wqc b/wai/quantum-conformance/reject/v1_contract_lone_surrogate.wqc new file mode 100644 index 000000000..e7046e2f5 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_lone_surrogate.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_long_literal.wqc b/wai/quantum-conformance/reject/v1_contract_long_literal.wqc new file mode 100644 index 000000000..7b6cfbd6b Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_long_literal.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_nan.wqc b/wai/quantum-conformance/reject/v1_contract_nan.wqc new file mode 100644 index 000000000..0750f8d7d Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_nan.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_noncharacter.wqc b/wai/quantum-conformance/reject/v1_contract_noncharacter.wqc new file mode 100644 index 000000000..369b6c682 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_noncharacter.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_not_object.wqc b/wai/quantum-conformance/reject/v1_contract_not_object.wqc new file mode 100644 index 000000000..89aa8208e Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_not_object.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_not_utf8.wqc b/wai/quantum-conformance/reject/v1_contract_not_utf8.wqc new file mode 100644 index 000000000..d0ec65225 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_not_utf8.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_number_out_of_range.wqc b/wai/quantum-conformance/reject/v1_contract_number_out_of_range.wqc new file mode 100644 index 000000000..e42d53722 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_number_out_of_range.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_number_rounds_to_infinity.wqc b/wai/quantum-conformance/reject/v1_contract_number_rounds_to_infinity.wqc new file mode 100644 index 000000000..45a9943c8 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_number_rounds_to_infinity.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_raw_noncharacter.wqc b/wai/quantum-conformance/reject/v1_contract_raw_noncharacter.wqc new file mode 100644 index 000000000..5a346ef4e Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_raw_noncharacter.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_too_deep.wqc b/wai/quantum-conformance/reject/v1_contract_too_deep.wqc new file mode 100644 index 000000000..87e1b93b9 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_too_deep.wqc differ diff --git a/wai/quantum-conformance/reject/v1_contract_twice.wqc b/wai/quantum-conformance/reject/v1_contract_twice.wqc new file mode 100644 index 000000000..566c5bf27 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_contract_twice.wqc differ diff --git a/wai/quantum-conformance/reject/v1_control_is_target.wqc b/wai/quantum-conformance/reject/v1_control_is_target.wqc new file mode 100644 index 000000000..537b669d0 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_control_is_target.wqc differ diff --git a/wai/quantum-conformance/reject/v1_control_out_of_range.wqc b/wai/quantum-conformance/reject/v1_control_out_of_range.wqc new file mode 100644 index 000000000..02bd8fefc Binary files /dev/null and b/wai/quantum-conformance/reject/v1_control_out_of_range.wqc differ diff --git a/wai/quantum-conformance/reject/v1_ext_other.wqc b/wai/quantum-conformance/reject/v1_ext_other.wqc new file mode 100644 index 000000000..76c6db5ab Binary files /dev/null and b/wai/quantum-conformance/reject/v1_ext_other.wqc differ diff --git a/wai/quantum-conformance/reject/v1_fields_missing.wqc b/wai/quantum-conformance/reject/v1_fields_missing.wqc new file mode 100644 index 000000000..e35605260 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_fields_missing.wqc differ diff --git a/wai/quantum-conformance/reject/v1_frac_31.wqc b/wai/quantum-conformance/reject/v1_frac_31.wqc new file mode 100644 index 000000000..febe3f827 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_frac_31.wqc differ diff --git a/wai/quantum-conformance/reject/v1_frac_float.wqc b/wai/quantum-conformance/reject/v1_frac_float.wqc new file mode 100644 index 000000000..a471531b4 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_frac_float.wqc differ diff --git a/wai/quantum-conformance/reject/v1_gateset_rn.wqc b/wai/quantum-conformance/reject/v1_gateset_rn.wqc new file mode 100644 index 000000000..f4c6cdcad Binary files /dev/null and b/wai/quantum-conformance/reject/v1_gateset_rn.wqc differ diff --git a/wai/quantum-conformance/reject/v1_measure_duplicate_member.wqc b/wai/quantum-conformance/reject/v1_measure_duplicate_member.wqc new file mode 100644 index 000000000..9ad1664bf Binary files /dev/null and b/wai/quantum-conformance/reject/v1_measure_duplicate_member.wqc differ diff --git a/wai/quantum-conformance/reject/v1_measure_not_json.wqc b/wai/quantum-conformance/reject/v1_measure_not_json.wqc new file mode 100644 index 000000000..24bb3f3bf Binary files /dev/null and b/wai/quantum-conformance/reject/v1_measure_not_json.wqc differ diff --git a/wai/quantum-conformance/reject/v1_n_ops_u32max.wqc b/wai/quantum-conformance/reject/v1_n_ops_u32max.wqc new file mode 100644 index 000000000..7b1ed50bb Binary files /dev/null and b/wai/quantum-conformance/reject/v1_n_ops_u32max.wqc differ diff --git a/wai/quantum-conformance/reject/v1_n_qubits_256.wqc b/wai/quantum-conformance/reject/v1_n_qubits_256.wqc new file mode 100644 index 000000000..2d310260d Binary files /dev/null and b/wai/quantum-conformance/reject/v1_n_qubits_256.wqc differ diff --git a/wai/quantum-conformance/reject/v1_n_qubits_258.wqc b/wai/quantum-conformance/reject/v1_n_qubits_258.wqc new file mode 100644 index 000000000..707e9786f Binary files /dev/null and b/wai/quantum-conformance/reject/v1_n_qubits_258.wqc differ diff --git a/wai/quantum-conformance/reject/v1_n_qubits_fraction.wqc b/wai/quantum-conformance/reject/v1_n_qubits_fraction.wqc new file mode 100644 index 000000000..f6f67cc93 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_n_qubits_fraction.wqc differ diff --git a/wai/quantum-conformance/reject/v1_n_qubits_minus0.wqc b/wai/quantum-conformance/reject/v1_n_qubits_minus0.wqc new file mode 100644 index 000000000..069a5a121 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_n_qubits_minus0.wqc differ diff --git a/wai/quantum-conformance/reject/v1_n_qubits_missing.wqc b/wai/quantum-conformance/reject/v1_n_qubits_missing.wqc new file mode 100644 index 000000000..637f4184a Binary files /dev/null and b/wai/quantum-conformance/reject/v1_n_qubits_missing.wqc differ diff --git a/wai/quantum-conformance/reject/v1_numeric_other.wqc b/wai/quantum-conformance/reject/v1_numeric_other.wqc new file mode 100644 index 000000000..cf449cc5f Binary files /dev/null and b/wai/quantum-conformance/reject/v1_numeric_other.wqc differ diff --git a/wai/quantum-conformance/reject/v1_offset_wrap.wqc b/wai/quantum-conformance/reject/v1_offset_wrap.wqc new file mode 100644 index 000000000..5f7a84056 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_offset_wrap.wqc differ diff --git a/wai/quantum-conformance/reject/v1_op_truncated_controls.wqc b/wai/quantum-conformance/reject/v1_op_truncated_controls.wqc new file mode 100644 index 000000000..9ddf43b9d Binary files /dev/null and b/wai/quantum-conformance/reject/v1_op_truncated_controls.wqc differ diff --git a/wai/quantum-conformance/reject/v1_opcode_10.wqc b/wai/quantum-conformance/reject/v1_opcode_10.wqc new file mode 100644 index 000000000..52f13107d Binary files /dev/null and b/wai/quantum-conformance/reject/v1_opcode_10.wqc differ diff --git a/wai/quantum-conformance/reject/v1_oplog_short.wqc b/wai/quantum-conformance/reject/v1_oplog_short.wqc new file mode 100644 index 000000000..4ae862bec Binary files /dev/null and b/wai/quantum-conformance/reject/v1_oplog_short.wqc differ diff --git a/wai/quantum-conformance/reject/v1_oplog_twice.wqc b/wai/quantum-conformance/reject/v1_oplog_twice.wqc new file mode 100644 index 000000000..b45fa740e Binary files /dev/null and b/wai/quantum-conformance/reject/v1_oplog_twice.wqc differ diff --git a/wai/quantum-conformance/reject/v1_phase_0.wqc b/wai/quantum-conformance/reject/v1_phase_0.wqc new file mode 100644 index 000000000..c97e9f9a8 Binary files /dev/null and b/wai/quantum-conformance/reject/v1_phase_0.wqc differ diff --git a/wai/quantum-conformance/reject/v1_phase_33.wqc b/wai/quantum-conformance/reject/v1_phase_33.wqc new file mode 100644 index 000000000..c270690eb Binary files /dev/null and b/wai/quantum-conformance/reject/v1_phase_33.wqc differ diff --git a/wai/quantum-conformance/reject/v1_target_out_of_range.wqc b/wai/quantum-conformance/reject/v1_target_out_of_range.wqc new file mode 100644 index 000000000..ddb09d24e Binary files /dev/null and b/wai/quantum-conformance/reject/v1_target_out_of_range.wqc differ diff --git a/wai/quantum-conformance/reject/v2_contract1_in_wqc2.wqc b/wai/quantum-conformance/reject/v2_contract1_in_wqc2.wqc new file mode 100644 index 000000000..56f49e202 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_contract1_in_wqc2.wqc differ diff --git a/wai/quantum-conformance/reject/v2_contract_extra_member.wqc b/wai/quantum-conformance/reject/v2_contract_extra_member.wqc new file mode 100644 index 000000000..b757e91c1 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_contract_extra_member.wqc differ diff --git a/wai/quantum-conformance/reject/v2_contract_key_order.wqc b/wai/quantum-conformance/reject/v2_contract_key_order.wqc new file mode 100644 index 000000000..6b546226c Binary files /dev/null and b/wai/quantum-conformance/reject/v2_contract_key_order.wqc differ diff --git a/wai/quantum-conformance/reject/v2_contract_not_object.wqc b/wai/quantum-conformance/reject/v2_contract_not_object.wqc new file mode 100644 index 000000000..8e0fafe82 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_contract_not_object.wqc differ diff --git a/wai/quantum-conformance/reject/v2_contract_whitespace.wqc b/wai/quantum-conformance/reject/v2_contract_whitespace.wqc new file mode 100644 index 000000000..8acd67730 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_contract_whitespace.wqc differ diff --git a/wai/quantum-conformance/reject/v2_duplicate_controls.wqc b/wai/quantum-conformance/reject/v2_duplicate_controls.wqc new file mode 100644 index 000000000..4df7ce6a8 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_duplicate_controls.wqc differ diff --git a/wai/quantum-conformance/reject/v2_measure_basis_x.wqc b/wai/quantum-conformance/reject/v2_measure_basis_x.wqc new file mode 100644 index 000000000..7739f2d1e Binary files /dev/null and b/wai/quantum-conformance/reject/v2_measure_basis_x.wqc differ diff --git a/wai/quantum-conformance/reject/v2_measure_no_shots.wqc b/wai/quantum-conformance/reject/v2_measure_no_shots.wqc new file mode 100644 index 000000000..5eaa1e7db Binary files /dev/null and b/wai/quantum-conformance/reject/v2_measure_no_shots.wqc differ diff --git a/wai/quantum-conformance/reject/v2_measure_seed_signed.wqc b/wai/quantum-conformance/reject/v2_measure_seed_signed.wqc new file mode 100644 index 000000000..3efe7370e Binary files /dev/null and b/wai/quantum-conformance/reject/v2_measure_seed_signed.wqc differ diff --git a/wai/quantum-conformance/reject/v2_measure_spaced.wqc b/wai/quantum-conformance/reject/v2_measure_spaced.wqc new file mode 100644 index 000000000..ec520bd4c Binary files /dev/null and b/wai/quantum-conformance/reject/v2_measure_spaced.wqc differ diff --git a/wai/quantum-conformance/reject/v2_measure_twice.wqc b/wai/quantum-conformance/reject/v2_measure_twice.wqc new file mode 100644 index 000000000..7517c37d8 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_measure_twice.wqc differ diff --git a/wai/quantum-conformance/reject/v2_n_qubits_256.wqc b/wai/quantum-conformance/reject/v2_n_qubits_256.wqc new file mode 100644 index 000000000..d14a5b329 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_n_qubits_256.wqc differ diff --git a/wai/quantum-conformance/reject/v2_n_qubits_largest_finite.wqc b/wai/quantum-conformance/reject/v2_n_qubits_largest_finite.wqc new file mode 100644 index 000000000..d3d3d2bb9 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_n_qubits_largest_finite.wqc differ diff --git a/wai/quantum-conformance/reject/v2_n_qubits_rounds_to_infinity.wqc b/wai/quantum-conformance/reject/v2_n_qubits_rounds_to_infinity.wqc new file mode 100644 index 000000000..cc96fed54 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_n_qubits_rounds_to_infinity.wqc differ diff --git a/wai/quantum-conformance/reject/v2_opcode_10.wqc b/wai/quantum-conformance/reject/v2_opcode_10.wqc new file mode 100644 index 000000000..465f1427a Binary files /dev/null and b/wai/quantum-conformance/reject/v2_opcode_10.wqc differ diff --git a/wai/quantum-conformance/reject/v2_oplog_trailing_byte.wqc b/wai/quantum-conformance/reject/v2_oplog_trailing_byte.wqc new file mode 100644 index 000000000..3604c7117 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_oplog_trailing_byte.wqc differ diff --git a/wai/quantum-conformance/reject/v2_param_on_h.wqc b/wai/quantum-conformance/reject/v2_param_on_h.wqc new file mode 100644 index 000000000..92cbde089 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_param_on_h.wqc differ diff --git a/wai/quantum-conformance/reject/v2_phase_0.wqc b/wai/quantum-conformance/reject/v2_phase_0.wqc new file mode 100644 index 000000000..c0696e789 Binary files /dev/null and b/wai/quantum-conformance/reject/v2_phase_0.wqc differ diff --git a/wai/quantum-conformance/tools/referee.py b/wai/quantum-conformance/tools/referee.py new file mode 100644 index 000000000..8a621c079 --- /dev/null +++ b/wai/quantum-conformance/tools/referee.py @@ -0,0 +1,1037 @@ +#!/usr/bin/env python3 +"""Referee for quantum-conformance/: both capabilities of the quantum-sim extension +(extensions/quantum-sim.md), `wai.quantum.circuit` (WQC1) and `wai.quantum.circuit2` +(WQC2), recomputed with integers only. + +Written from the extension's rules. It shares no code with wai-rs or wai-quantum, +which generate the corpus: the phase tables, the container writer and reader, the +fixed-point simulator, the shot sampler and BLAKE3 are all written here. Standard +library only, and no floating point anywhere. + +It checks, in this order, and reports every failure rather than stopping at one: + +1. BLAKE3 against the published test vectors. +2. The phase tables. circuit/1's is derived by its recurrence (§3.1) and circuit2's + by an interval-certified procedure in exact integers (§3.2): each step keeps a + true enclosure of 2^62·cos(2π/2^k) and 2^62·sin(2π/2^k), and both ends of every + enclosure round to the same integer far from a tie, so that integer is the + nearest one. Each derived table must equal its literal table (restated below) and + its BLAKE3 digest, and the extension's Appendix B must state those same tables and + digests. +3. The vector trees: `./` (circuit/1) and `v2/` (circuit2). Every file is rebuilt byte + for byte from the circuit definitions below and read back by the reader here, and + every manifest entry must equal the entry computed here: n_qubits, n_ops, the + circuit, statevector and histogram hashes, the measurement, and in `v2/` the + capability. The four original circuit/1 vectors must also keep their frozen hashes. +4. The reject tree: `reject.json` must list exactly the cases below, the directory + must hold exactly the listed files, and the reader here must refuse each one with + its listed class and by the rule its name states. +5. The accept tree: `accept.json` must list exactly the cases below, the directory + must hold exactly the listed files, and the reader here must accept each one and + read it as listed: its capability, n_qubits, op count, circuit hash and + measurement. +6. Mutation completeness: under each table, a change of ±1 to either component of any + P(1..32), with H following re P(3), must change dyadic_ladder's statevector hash. + +It was written from the same reading of the text as the Rust reference, so a misreading +the two share would pass both. + + python3 -I referee.py [quantum-conformance dir [quantum-sim.md]] + +The extension defaults to ../extensions/quantum-sim.md beside the corpus directory. +Exit status 0 only when every check passes. +""" +import json +import os +import struct +import sys +from math import isqrt + +# ---------------------------------------------------------------- BLAKE3 (hash mode) + +_IV = [0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19] +_PERM = [2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8] +_CHUNK_START, _CHUNK_END, _PARENT, _ROOT = 1, 2, 4, 8 +_M32 = 0xFFFFFFFF +_CHUNK = 1024 +_BLOCK = 64 + + +def _rotr(x, n): + return ((x >> n) | (x << (32 - n))) & _M32 + + +def _g(s, a, b, c, d, x, y): + s[a] = (s[a] + s[b] + x) & _M32 + s[d] = _rotr(s[d] ^ s[a], 16) + s[c] = (s[c] + s[d]) & _M32 + s[b] = _rotr(s[b] ^ s[c], 12) + s[a] = (s[a] + s[b] + y) & _M32 + s[d] = _rotr(s[d] ^ s[a], 8) + s[c] = (s[c] + s[d]) & _M32 + s[b] = _rotr(s[b] ^ s[c], 7) + + +def _compress(cv, block, counter, block_len, flags): + m = list(struct.unpack("<16I", block)) + s = list(cv) + _IV[:4] + [counter & _M32, (counter >> 32) & _M32, block_len, flags] + for r in range(7): + _g(s, 0, 4, 8, 12, m[0], m[1]) + _g(s, 1, 5, 9, 13, m[2], m[3]) + _g(s, 2, 6, 10, 14, m[4], m[5]) + _g(s, 3, 7, 11, 15, m[6], m[7]) + _g(s, 0, 5, 10, 15, m[8], m[9]) + _g(s, 1, 6, 11, 12, m[10], m[11]) + _g(s, 2, 7, 8, 13, m[12], m[13]) + _g(s, 3, 4, 9, 14, m[14], m[15]) + if r < 6: + m = [m[i] for i in _PERM] + return [s[i] ^ s[i + 8] for i in range(8)] + [s[i + 8] ^ cv[i] for i in range(8)] + + +def _chunk_output(chunk, counter): + """The last block of a chunk, ready to compress: (cv, block, counter, len, flags).""" + cv = _IV + n_blocks = max(1, (len(chunk) + _BLOCK - 1) // _BLOCK) + for b in range(n_blocks): + block = chunk[b * _BLOCK:(b + 1) * _BLOCK] + flags = _CHUNK_START if b == 0 else 0 + if b == n_blocks - 1: + return (cv, block.ljust(_BLOCK, b"\0"), counter, len(block), flags | _CHUNK_END) + cv = _compress(cv, block, counter, _BLOCK, flags)[:8] + + +def _cv(output): + cv, block, counter, block_len, flags = output + return _compress(cv, block, counter, block_len, flags)[:8] + + +def _parent(left_cv, right_cv): + return (_IV, struct.pack("<8I", *left_cv) + struct.pack("<8I", *right_cv), 0, _BLOCK, _PARENT) + + +def blake3(data): + """The 32-byte BLAKE3 hash of `data`.""" + chunks = [data[i:i + _CHUNK] for i in range(0, len(data), _CHUNK)] or [b""] + stack = [] + for i, chunk in enumerate(chunks[:-1]): + cv = _cv(_chunk_output(chunk, i)) + total = i + 1 + while total & 1 == 0: + cv = _cv(_parent(stack.pop(), cv)) + total >>= 1 + stack.append(cv) + node = _chunk_output(chunks[-1], len(chunks) - 1) + while stack: + node = _parent(stack.pop(), _cv(node)) + cv, block, counter, block_len, flags = node + return struct.pack("<8I", *_compress(cv, block, counter, block_len, flags | _ROOT)[:8]) + + +# The published BLAKE3 test vectors (input byte i is i mod 251): input length, hash. +BLAKE3_VECTORS = [ + (0, "af1349b9f5f9a1a6a0404dea36dcc9499bcb25c9adc112b7cc9a93cae41f3262"), + (1, "2d3adedff11b61f14c886e35afa036736dcd87a74d27b5c1510225d0f592e213"), + (63, "e9bc37a594daad83be9470df7f7b3798297c3d834ce80ba85d6e207627b7db7b"), + (64, "4eed7141ea4a5cd4b788606bd23f46e212af9cacebacdc7d1f4c6dc7f2511b98"), + (65, "de1e5fa0be70df6d2be8fffd0e99ceaa8eb6e8c93a63f2d8d1c30ecb6b263dee"), + (1023, "10108970eeda3eb932baac1428c7a2163b0e924c9a9e25b35bba72b28f70bd11"), + (1024, "42214739f095a406f3fc83deb889744ac00df831c10daa55189b5d121c855af7"), + (1025, "d00278ae47eb27b34faecf67b4fe263f82d5412916c1ffd97c8cb7fb814b8444"), + (2048, "e776b6028c7cd22a4d0ba182a8bf62205d2ef576467e838ed6f2529b85fba24a"), + (2049, "5f4d72f40d7a5f82b15ca2b2e44b1de3c2ef86c426c95c1af0b6879522563030"), + (3072, "b98cb0ff3623be03326b373de6b9095218513e64f1ee2edd2525c7ad1e5cffd2"), + (3073, "7124b49501012f81cc7f11ca069ec9226cecb8a2c850cfe644e327d22d3e1cd3"), + (4096, "015094013f57a5277b59d8475c0501042c0b642e531b0a1c8f58d2163229e969"), + (4097, "9b4052b38f1c5fc8b1f9ff7ac7b27cd242487b3d890d15c96a1c25b8aa0fb995"), + (8193, "bab6c09cb8ce8cf459261398d2e7aef35700bf488116ceb94a36d0f5f1b7bc3b"), + (16384, "f875d6646de28985646f34ee13be9a576fd515f76b5b0a26bb324735041ddde4"), + (31744, "62b6960e1a44bcc1eb1a611a8d6235b6b4b78f32e7abc4fb4c6cdcce94895c47"), +] + +# ---------------------------------------------------------------- the phase tables + +FRAC = 30 +ONE = 1 << FRAC +HALF = 1 << (FRAC - 1) +DYADIC_MAX = 32 + +# Appendix B.1: circuit/1, (re, im) for k = 1..32. P(19..32) apply no phase at all. +PHASE_V1 = [None] + [ + (-1073741824, 0), (0, 1073741824), (759250124, 759250124), (992008094, 410903207), + (1053110175, 209476638), (1068571463, 105245102), (1072448454, 52686012), + (1073418432, 26350953), (1073660972, 13176485), (1073721610, 6588405), + (1073736770, 3294284), (1073740560, 1647223), (1073741507, 823774), + (1073741744, 411887), (1073741803, 207243), (1073741818, 103621), + (1073741822, 56755), (1073741823, 32768), +] + [(1073741823, 0)] * 14 +PHASE_V1_DIGEST = "3fa9805ff96feb819a1cf371fedcd3b5228f93eddf933d0edcf401cbf8c28466" + +# Appendix B.2: circuit2, the integers nearest 2^30·cos(2π/2^k) and 2^30·sin(2π/2^k). +PHASE_V2 = [None] + [ + (-1073741824, 0), (0, 1073741824), (759250125, 759250125), (992008094, 410903207), + (1053110176, 209476638), (1068571464, 105245103), (1072448455, 52686014), + (1073418433, 26350943), (1073660973, 13176464), (1073721611, 6588356), + (1073736771, 3294193), (1073740561, 1647099), (1073741508, 823550), + (1073741745, 411775), (1073741804, 205887), (1073741819, 102944), + (1073741823, 51472), +] + [(ONE, im) for im in (25736, 12868, 6434, 3217, 1608, 804, 402, 201, 101, 50, 25, 13, 6, 3, 2)] +PHASE_V2_DIGEST = "d34cb793715348e7c92db027c0bc3dddb6008f5730470bafaed978ea7fb23510" + + +def derive_v1(): + """circuit/1's table by its recurrence: with c = re P(k-1), + re P(k) = isqrt(floor((2^30 + c) / 2) · 2^30), im P(k) = isqrt(floor((2^30 - c) / 2) · 2^30).""" + t = [None, (-ONE, 0), (0, ONE)] + for k in range(3, DYADIC_MAX + 1): + c = t[k - 1][0] + t.append((isqrt(((ONE + c) // 2) << FRAC), isqrt(((ONE - c) // 2) << FRAC))) + return t + + +def derive_v2(): + """circuit2's table by enclosures at scale 2^62, and the reasons it is not certified. + + cos(θ/2) = √((1 + cos θ)/2) and sin(θ/2) = sin θ / (2 cos(θ/2)), each bound rounded + outward, starting from (cos, sin)(π/2) = (0, 1), which is exact.""" + s = 1 << 62 + + def ceil_isqrt(x): + r = isqrt(x) + return r if r * r == x else r + 1 + + def ceil_div(a, b): + return -((-a) // b) + + def nearest(x): + # From 2^-62 to 2^-30, half up. + return (x + (1 << 31)) >> 32 + + problems = [] + c_lo, c_hi, s_lo, s_hi = 0, 0, s, s + t = [None, (-ONE, 0), (0, ONE)] + for k in range(3, DYADIC_MAX + 1): + c_lo2 = isqrt(((s + c_lo) * s) >> 1) + c_hi2 = ceil_isqrt(ceil_div((s + c_hi) * s, 2)) + s_lo2 = (s_lo * s) // (2 * c_hi2) + s_hi2 = ceil_div(s_hi * s, 2 * c_lo2) + if nearest(c_lo2) != nearest(c_hi2) or nearest(s_lo2) != nearest(s_hi2): + problems.append("k=%d: the enclosure's ends round apart" % k) + for x in (c_lo2, c_hi2, s_lo2, s_hi2): + if abs((x % (1 << 32)) - (1 << 31)) <= (1 << 20): + problems.append("k=%d: an end lies within 2^20 of a tie" % k) + t.append((nearest(c_lo2), nearest(s_lo2))) + c_lo, c_hi, s_lo, s_hi = c_lo2, c_hi2, s_lo2, s_hi2 + return t, problems + + +def table_digest(t): + """BLAKE3 over i64le(re) ‖ i64le(im) for k = 1..32, no domain prefix.""" + return blake3(b"".join(struct.pack(" 18: # a section holds at most 2^32 digits, so the sign alone decides + return not exponent.startswith("-") + e = -int(e or "0") if exponent.startswith("-") else int(e or "0") + k = len(whole) - (len(digits) - len(digits.lstrip("0"))) + e + if k != len(RANGE_LIMIT): + return k > len(RANGE_LIMIT) + n = max(len(significant), len(RANGE_LIMIT)) + return significant.ljust(n, "0") >= RANGE_LIMIT.ljust(n, "0") + + +def strict_json(raw): + """`raw` read as I-JSON (§2), or NotIJson naming the rule it breaks. An integer reads + as a Python int, so `-0` and any number with a fraction or exponent read as + ("number", literal): never an unsigned integer, and never a float.""" + + def number(text): + if out_of_range(text): + raise NotIJson("number range") + return ("number", text) + + def integer(text): + if out_of_range(text): + raise NotIJson("number range") + return ("number", text) if text == "-0" else int(text) + + def no_constant(name): + raise NotIJson("syntax") + + def unique(pairs): + names = [k for k, _ in pairs] + if len(set(names)) != len(names): + raise NotIJson("duplicate member") + return dict(pairs) + + def text(s): + for ch in s: + o = ord(ch) + if 0xD800 <= o <= 0xDFFF: + raise NotIJson("surrogate") + if 0xFDD0 <= o <= 0xFDEF or o & 0xFFFE == 0xFFFE: + raise NotIJson("noncharacter") + + def walk(v, depth): + if isinstance(v, (dict, list)): + if depth + 1 > MAX_DEPTH: + raise NotIJson("depth") + items = v.items() if isinstance(v, dict) else enumerate(v) + for k, x in items: + if isinstance(k, str): + text(k) + walk(x, depth + 1) + elif isinstance(v, str): + text(v) + + try: + decoded = raw.decode("utf-8") + except UnicodeDecodeError: + raise NotIJson("utf-8") + try: + v = json.loads(decoded, parse_constant=no_constant, parse_float=number, + parse_int=integer, object_pairs_hook=unique) + except RecursionError: + raise NotIJson("depth") + except ValueError: # json.JSONDecodeError among them + raise NotIJson("syntax") + walk(v, 0) + return v + + +def unsigned(v, below): + """Whether `v` is a JSON unsigned integer less than `below` (not a float, not a boolean).""" + return type(v) is int and 0 <= v < below + + +def read_contract(gs, raw): + """The contract's n_qubits, under the capability the magic names.""" + try: + cv = strict_json(raw) + except NotIJson as e: + raise Refused("contract", "not JSON: " + e.rule) + if not isinstance(cv, dict): + raise Refused("contract", "not an object") + + def n_qubits(): + n = cv.get("n_qubits") + if not unsigned(n, 256): + raise Refused("malformed", "n_qubits") + return n + + if gs is V1: + # The four values, in any formatting, with any other members. + for key, value in (("ext", gs.ext), ("numeric", gs.numeric)): + if cv.get(key) != value: + raise Refused("contract", key) + if not (unsigned(cv.get("frac"), 1 << 64) and cv["frac"] == FRAC): + raise Refused("contract", "frac") + if cv.get("gateset") != gs.name: + raise Refused("contract", "gateset") + return n_qubits() + n = n_qubits() + if raw != gs.contract(n): + raise Refused("contract", "canonical contract") + return n + + +def read_oplog(raw): + """The ops and the bytes they took; bytes after the last op are the caller's to judge.""" + if len(raw) < 4: + raise Refused("malformed", "op-log header") + n_ops = struct.unpack_from(" P: + raise Refused("malformed", "opcode") + param = struct.unpack_from("= n_qubits: + raise Refused("invalid", "target") + if len(controls) > 255: + raise Refused("invalid", "control count") + for c in controls: + if c >= n_qubits: + raise Refused("invalid", "control") + if c == target: + raise Refused("invalid", "control is target") + if gs is V2 and len(set(controls)) != len(controls): + raise Refused("invalid", "repeated control") + if op == P: + if not 1 <= param <= DYADIC_MAX: + raise Refused("invalid", "phase index") + elif gs is V2 and param != 0: + raise Refused("invalid", "param") + + +def read_measure(gs, raw): + """circuit/1: JSON, a missing or non-u64 seed or shots reads as 0. circuit2: u64 seed + and shots, and exactly the canonical bytes.""" + try: + mv = strict_json(raw) + except NotIJson as e: + raise Refused("malformed", "measure JSON: " + e.rule) + if gs is V1: + def field(key): + v = mv.get(key) if isinstance(mv, dict) else None + return v if unsigned(v, 1 << 64) else 0 + return (field("seed"), field("shots")) + for key in ("seed", "shots"): + if not (isinstance(mv, dict) and unsigned(mv.get(key), 1 << 64)): + raise Refused("malformed", "measure " + key) + m = (mv["seed"], mv["shots"]) + if raw != measure_bytes(*m): + raise Refused("malformed", "canonical measure") + return m + + +def read_wqc(data): + """(gateset, n_qubits, ops, measure or None), or Refused.""" + if len(data) < 6 or data[:4] not in BY_MAGIC: + raise Refused("malformed", "magic") + gs = BY_MAGIC[data[:4]] + n_sections = struct.unpack_from(" len(data): + raise Refused("malformed", "section table") + sections = {} + for i in range(n_sections): + p = 6 + 9 * i + kind = data[p] + offset, length = struct.unpack_from(" len(data): + raise Refused("malformed", "section bounds") + if kind not in (SECT_CONTRACT, SECT_OPLOG, SECT_MEASURE): + continue # unknown kinds are ignored + if kind in sections: + raise Refused("malformed", "duplicate section") + sections[kind] = data[start:end] + if SECT_CONTRACT not in sections: + raise Refused("malformed", "missing contract") + if SECT_OPLOG not in sections: + raise Refused("malformed", "missing op-log") + n_qubits = read_contract(gs, sections[SECT_CONTRACT]) + ops, used = read_oplog(sections[SECT_OPLOG]) + if gs is V2 and used != len(sections[SECT_OPLOG]): + raise Refused("malformed", "trailing bytes") + check_valid(gs, n_qubits, ops) + measure = read_measure(gs, sections[SECT_MEASURE]) if SECT_MEASURE in sections else None + return gs, n_qubits, ops, measure + + +# ---------------------------------------------------------------- simulator (§3.0) + + +def fx(a, b): + """(a·b + 2^29) >> 30, exact, with an arithmetic shift.""" + return (a * b + HALF) >> FRAC + + +def cmul(a, b): + return (fx(a[0], b[0]) - fx(a[1], b[1]), fx(a[0], b[1]) + fx(a[1], b[0])) + + +def cadd(a, b): + return (a[0] + b[0], a[1] + b[1]) + + +def matrix(op, param, table, h): + z, one, i, minus_i = (0, 0), (ONE, 0), (0, ONE), (0, -ONE) + p3 = table[3] + return { + I: ((one, z), (z, one)), + X: ((z, one), (one, z)), + Y: ((z, minus_i), (i, z)), + Z: ((one, z), (z, (-ONE, 0))), + H: (((h, 0), (h, 0)), ((h, 0), (-h, 0))), + S: ((one, z), (z, i)), + SDG: ((one, z), (z, minus_i)), + T: ((one, z), (z, p3)), + TDG: ((one, z), (z, (p3[0], -p3[1]))), + P: ((one, z), (z, table[param] if op == P else z)), + }[op] + + +def simulate(n_qubits, ops, table): + """The statevector from |0…0⟩. H is re P(3) of the same table.""" + h = table[3][0] + dim = 1 << n_qubits + amps = [(0, 0)] * dim + amps[0] = (ONE, 0) + for op, controls, target, param in ops: + (m00, m01), (m10, m11) = matrix(op, param, table, h) + tbit = 1 << target + cmask = 0 + for c in controls: + cmask |= 1 << c + for i in range(dim): + if i & tbit == 0 and i & cmask == cmask: + j = i | tbit + a, b = amps[i], amps[j] + amps[i] = cadd(cmul(m00, a), cmul(m01, b)) + amps[j] = cadd(cmul(m10, a), cmul(m11, b)) + return amps + + +# ---------------------------------------------------------------- the hashes (§4) + + +def circuit_hash(gs, n_qubits, ops): + return blake3(b"wai:quantum-circuit\x01" + gs.contract(n_qubits) + oplog_bytes(ops)).hex() + + +def statevector_hash(n_qubits, amps): + out = bytearray(b"wai:quantum-statevector\x01") + bytes([n_qubits]) + struct.pack("> 30)) * 0xBF58476D1CE4E5B9) & 0xFFFFFFFFFFFFFFFF + z = ((z ^ (z >> 27)) * 0x94D049BB133111EB) & 0xFFFFFFFFFFFFFFFF + return state, z ^ (z >> 31) + + +def histogram_hash(n_qubits, amps, seed, shots): + """Each shot draws r = splitmix64 mod W, W the sum of the weights re² + im², and + counts the first basis state whose cumulative weight exceeds r.""" + cumulative, total = [], 0 + for re, im in amps: + total += re * re + im * im + cumulative.append(total) + counts = [0] * len(amps) + state = seed + for _ in range(shots): + state, r = splitmix64(state) + r %= total + lo, hi = 0, len(cumulative) + while lo < hi: + mid = (lo + hi) // 2 + if cumulative[mid] > r: + hi = mid + else: + lo = mid + 1 + counts[min(lo, len(counts) - 1)] += 1 + out = bytearray(b"wai:quantum-histogram\x01") + bytes([n_qubits]) + struct.pack(" j; then the + reversal swaps, each as CX(a→b), CX(b→a), CX(a→b).""" + ops = [] + for j in range(n): + ops.append(gate(H, j)) + for l in range(j + 1, n): + ops.append(gate(P, j, (l,), l - j + 1)) + for j in range(n // 2): + a, b = j, n - 1 - j + ops += [gate(X, b, (a,)), gate(X, a, (b,)), gate(X, b, (a,))] + return ops + + +def dyadic_ladder_ops(): + ops = [gate(X, 0)] + [gate(op, 1) for op in (Y, Z, S, SDG, T, TDG, I)] + chain = [gate(P, 1, (0,), k) for k in range(1, DYADIC_MAX + 1)] + return ops + chain + chain + + +# name: (n_qubits, ops, measure (seed, shots) or None). Both trees hold all of them. +VECTORS = { + "bell": (2, [gate(H, 0), gate(X, 1, (0,))], (12648430, 100000)), + "ghz3": (3, [gate(H, 0), gate(X, 1, (0,)), gate(X, 2, (1,))], None), + "qft3": (3, qft_ops(3), None), + "qft5": (5, qft_ops(5), None), + "qft5_x22": (5, [gate(X, 1), gate(X, 2), gate(X, 4)] + qft_ops(5), None), + "dyadic_ladder": (2, dyadic_ladder_ops(), None), + "qft8_ones": (8, [gate(X, q) for q in range(8)] + qft_ops(8), None), +} + +# The four original circuit/1 vectors are frozen: circuit, statevector, histogram hash. +FROZEN_V1 = { + "bell": ("894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "bd5df69662f4e61878d17baee1f3cf5705c3726d6740d1a4bf7be5133c099efb", + "0893f48ef32ca9f2865f6617537f0a4a5334f67e3c944bba845beb2f377acdb3"), + "ghz3": ("34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c", + "08c5e6fb50b91a00d28795506cc5d99ce1e9a3a8b4c41e4bfd05f5d829af9542", None), + "qft3": ("7e8414be4410557a05b52bcc1084e1c5f93b6caa0f28fa45f4d2ab661efd2a60", + "0fe1e5b8e2ac8e9afbfa926b3877967ebfead92efb7ed8e1ff6b117668926d6e", None), + "qft5": ("f36a9af99a251cae6f42144b2be3c57523f15e1e2ab3492cf15cac709fc048be", + "c268d77f2ddf84fecb028e80ce989123912870f04e5b9a1e3d885c9ce235c27d", None), +} + +# The reject corpus: file stem → (class, the rule that refuses it here). +REJECTS = { + "magic_wqc3": ("malformed", "magic"), + "v1_ext_other": ("contract", "ext"), + "v1_numeric_other": ("contract", "numeric"), + "v1_frac_31": ("contract", "frac"), + "v1_frac_float": ("contract", "frac"), + "v1_gateset_rn": ("contract", "gateset"), + "v1_contract2_in_wqc1": ("contract", "ext"), + "v1_fields_missing": ("contract", "ext"), + "v1_contract_not_object": ("contract", "not an object"), + "v1_n_qubits_256": ("malformed", "n_qubits"), + "v1_n_qubits_258": ("malformed", "n_qubits"), + "v1_contract_twice": ("malformed", "duplicate section"), + "v1_oplog_twice": ("malformed", "duplicate section"), + "v1_contract_duplicate_member": ("contract", "not JSON: duplicate member"), + "v1_contract_lone_surrogate": ("contract", "not JSON: surrogate"), + "v1_contract_too_deep": ("contract", "not JSON: depth"), + "v1_n_qubits_minus0": ("malformed", "n_qubits"), + "v1_contract_lone_low_surrogate": ("contract", "not JSON: surrogate"), + "v1_contract_noncharacter": ("contract", "not JSON: noncharacter"), + "v1_contract_raw_noncharacter": ("contract", "not JSON: noncharacter"), + "v1_contract_integer_rounds_to_infinity": ("contract", "not JSON: number range"), + "v1_contract_number_rounds_to_infinity": ("contract", "not JSON: number range"), + "v1_contract_long_literal": ("contract", "not JSON: number range"), + "v1_contract_bom": ("contract", "not JSON: syntax"), + "v1_contract_nan": ("contract", "not JSON: syntax"), + "v1_contract_control_character": ("contract", "not JSON: syntax"), + "v1_measure_duplicate_member": ("malformed", "measure JSON: duplicate member"), + "v1_contract_not_utf8": ("contract", "not JSON: utf-8"), + "v1_contract_number_out_of_range": ("contract", "not JSON: number range"), + "v1_n_qubits_fraction": ("malformed", "n_qubits"), + "v1_n_qubits_missing": ("malformed", "n_qubits"), + "v1_measure_not_json": ("malformed", "measure JSON: syntax"), + "short_payload": ("malformed", "magic"), + "table_past_end": ("malformed", "section table"), + "section_past_end": ("malformed", "section bounds"), + "unknown_section_past_end": ("malformed", "section bounds"), + "no_contract": ("malformed", "missing contract"), + "no_oplog": ("malformed", "missing op-log"), + "v1_oplog_short": ("malformed", "op-log header"), + "v1_op_truncated_controls": ("malformed", "op controls"), + "v1_opcode_10": ("malformed", "opcode"), + "v1_target_out_of_range": ("invalid", "target"), + "v1_control_out_of_range": ("invalid", "control"), + "v1_control_is_target": ("invalid", "control is target"), + "v1_phase_0": ("invalid", "phase index"), + "v1_phase_33": ("invalid", "phase index"), + "v1_n_ops_u32max": ("malformed", "op header"), + "v1_offset_wrap": ("malformed", "section bounds"), + "v2_contract_whitespace": ("contract", "canonical contract"), + "v2_contract_key_order": ("contract", "canonical contract"), + "v2_contract_extra_member": ("contract", "canonical contract"), + "v2_contract1_in_wqc2": ("contract", "canonical contract"), + "v2_n_qubits_256": ("malformed", "n_qubits"), + "v2_contract_not_object": ("contract", "not an object"), + "v2_n_qubits_rounds_to_infinity": ("contract", "not JSON: number range"), + "v2_n_qubits_largest_finite": ("malformed", "n_qubits"), + "v2_phase_0": ("invalid", "phase index"), + "v2_measure_twice": ("malformed", "duplicate section"), + "v2_measure_seed_signed": ("malformed", "measure seed"), + "v2_duplicate_controls": ("invalid", "repeated control"), + "v2_param_on_h": ("invalid", "param"), + "v2_oplog_trailing_byte": ("malformed", "trailing bytes"), + "v2_opcode_10": ("malformed", "opcode"), + "v2_measure_spaced": ("malformed", "canonical measure"), + "v2_measure_no_shots": ("malformed", "measure shots"), + "v2_measure_basis_x": ("malformed", "canonical measure"), +} + +# ---------------------------------------------------------------- the checks + + +class Report: + def __init__(self): + self.failures = 0 + + def check(self, label, problem): + """`problem` is None for a pass, else why it failed.""" + if problem is None: + print("PASS", label) + else: + self.failures += 1 + print("FAIL %s: %s" % (label, problem)) + + +def check_blake3(rep): + bad = [n for n, want in BLAKE3_VECTORS if blake3(bytes(i % 251 for i in range(n))).hex() != want] + rep.check("BLAKE3 against the published test vectors", "lengths %s differ" % bad if bad else None) + + +def check_tables(rep): + v1 = derive_v1() + rep.check("circuit/1 table: the recurrence gives Appendix B.1", + None if v1 == PHASE_V1 else "k = %s differ" % [k for k in range(1, 33) if v1[k] != PHASE_V1[k]]) + d1 = table_digest(PHASE_V1) + rep.check("circuit/1 table digest", None if d1 == PHASE_V1_DIGEST else "got " + d1) + v2, problems = derive_v2() + rep.check("circuit2 table: every enclosure certifies its nearest integer", "; ".join(problems) or None) + rep.check("circuit2 table: the certified integers are Appendix B.2", + None if v2 == PHASE_V2 else "k = %s differ" % [k for k in range(1, 33) if v2[k] != PHASE_V2[k]]) + d2 = table_digest(PHASE_V2) + rep.check("circuit2 table digest", None if d2 == PHASE_V2_DIGEST else "got " + d2) + + +APPENDIX_BEGIN = "" +APPENDIX_END = "" + + +def read_appendix(path): + """Appendix B of the extension as {subsection: (capability, table, digest)}, or the + reason it cannot be read. A subsection is a `### B.n `capability`` heading, rows + `| k | re | im |`, and a `Digest: `hex`` line.""" + try: + with open(path, encoding="utf-8") as f: + text = f.read() + except OSError as e: + return None, str(e) + begin, end = text.find(APPENDIX_BEGIN), text.find(APPENDIX_END) + if begin < 0 or end < begin: + return None, "no generated Appendix B" + out, current = {}, None + for line in text[begin + len(APPENDIX_BEGIN):end].splitlines(): + line = line.strip() + if line.startswith("### B."): + sub, _, cap = line[4:].partition(" ") + current = sub + out[sub] = (cap.strip("`"), [None], None) + elif current and line.startswith("| ") and line[2:3].isdigit(): + try: + k, re_, im = (int(c.strip()) for c in line.strip("|").split("|")) + except ValueError: + return None, "%s: unreadable row %r" % (current, line) + table = out[current][1] + if k != len(table): + return None, "%s: row k = %d out of order" % (current, k) + table.append((re_, im)) + elif current and line.startswith("Digest: `"): + out[current] = (out[current][0], out[current][1], line[len("Digest: `"):].rstrip("`")) + return out, None + + +def check_appendix(rep, path): + """The extension's Appendix B states the literal tables and digests above.""" + appendix, why = read_appendix(path) + if appendix is None: + rep.check("extension Appendix B (%s)" % path, why) + return + for sub, gs, digest in (("B.1", V1, PHASE_V1_DIGEST), ("B.2", V2, PHASE_V2_DIGEST)): + got = appendix.get(sub) + if got is None: + rep.check("extension Appendix %s" % sub, "missing") + continue + cap, table, stated = got + problems = [] + if cap != gs.capability: + problems.append("names %s" % cap) + if table != gs.table: + problems.append("k = %s differ" % [k for k in range(1, DYADIC_MAX + 1) + if k >= len(table) or table[k] != gs.table[k]]) + if stated != digest: + problems.append("digest %s" % stated) + rep.check("extension Appendix %s is the %s table and digest" % (sub, gs.label), "; ".join(problems) or None) + extra = sorted(set(appendix) - {"B.1", "B.2"}) + rep.check("extension Appendix B has no other table", "has %s" % extra if extra else None) + + +def read_object(path): + """A JSON object from `path`, or the reason it is not one.""" + try: + with open(path, "rb") as f: + v = strict_json(f.read()) + except OSError as e: + return None, str(e) + if not isinstance(v, dict): + return None, "not a JSON object" + return v, None + + +def listed_and_present(listed, directory): + """None when `directory` holds exactly the `.wqc` files `listed` names.""" + try: + present = {f for f in os.listdir(directory) if f.endswith(".wqc")} + except OSError as e: + return str(e) + listed = set(listed) + if listed == present: + return None + return "listed but missing %s, present but unlisted %s" % (sorted(listed - present), sorted(present - listed)) + + +def expected_entry(gs, name, label_capability): + """The manifest entry for vector `name` in `gs`, computed here, and the hashes.""" + n, ops, measure = VECTORS[name] + amps = simulate(n, ops, gs.table) + entry = {"n_qubits": n, "n_ops": len(ops), "circuit_hash": circuit_hash(gs, n, ops), + "statevector_hash": statevector_hash(n, amps)} + if label_capability: + entry["capability"] = gs.capability + if measure is not None: + entry["measure"] = {"seed": measure[0], "shots": measure[1], + "histogram_hash": histogram_hash(n, amps, *measure)} + return entry + + +def check_tree(rep, root, sub, gs, label_capability): + tree = os.path.join(root, sub) if sub else root + prefix = sub + "/" if sub else "" + manifest, why = read_object(os.path.join(tree, "expected_hashes.json")) + if manifest is None: + rep.check(prefix + "expected_hashes.json", why) + return + names = ["%s.wqc" % v for v in VECTORS] + rep.check(prefix + "expected_hashes.json lists every vector and no other", + None if set(manifest) == set(names) else "lists %s" % sorted(manifest)) + rep.check(prefix + "vectors/ holds exactly the listed files", listed_and_present(names, os.path.join(tree, "vectors"))) + for name, (n, ops, measure) in VECTORS.items(): + label = "%s%s.wqc (%s)" % (prefix, name, gs.label) + try: + with open(os.path.join(tree, "vectors", name + ".wqc"), "rb") as f: + data = f.read() + except OSError as e: + rep.check(label, str(e)) + continue + problems = [] + if data != write_wqc(gs, n, ops, measure): + problems.append("the file is not the bytes rebuilt here") + try: + if read_wqc(data) != (gs, n, ops, measure): + problems.append("the file reads back as another circuit") + except Refused as r: + problems.append("refused (%s)" % r) + want = expected_entry(gs, name, label_capability) + got = manifest.get(name + ".wqc") + if got != want: + differ = sorted(k for k in set(want) | set(got or {}) if (got or {}).get(k) != want.get(k)) + problems.append("manifest members %s differ from the values computed here" % differ) + if gs is V1 and name in FROZEN_V1: + frozen = FROZEN_V1[name] + computed = (want["circuit_hash"], want["statevector_hash"], + want["measure"]["histogram_hash"] if "measure" in want else None) + if computed != frozen: + problems.append("a frozen circuit/1 hash changed") + rep.check(label, "; ".join(problems) or None) + + +def check_rejects(rep, root): + tree = os.path.join(root, "reject") + listing, why = read_object(os.path.join(tree, "reject.json")) + if listing is None: + rep.check("reject/reject.json", why) + return + want = {"%s.wqc" % stem: kind for stem, (kind, _) in REJECTS.items()} + rep.check("reject/reject.json lists every reject case with its class", + None if listing == want else "differs in %s" % sorted( + f for f in set(listing) | set(want) if listing.get(f) != want.get(f))) + rep.check("reject/ holds exactly the listed files", listed_and_present(listing, tree)) + for stem, (kind, rule) in REJECTS.items(): + label = "reject/%s.wqc" % stem + try: + with open(os.path.join(tree, stem + ".wqc"), "rb") as f: + data = f.read() + except OSError as e: + rep.check(label, str(e)) + continue + try: + read_wqc(data) + rep.check(label, "accepted; it must be refused as %s (%s)" % (kind, rule)) + except Refused as r: + rep.check(label + " refused as %s (%s)" % (kind, rule), + None if (r.kind, r.rule) == (kind, rule) else "refused as %s (%s)" % (r.kind, r.rule)) + + +# The accept corpus: the file stems `accept.json` must list. Each file's reading is +# recomputed here and compared with the listing. +ACCEPTS = [ + "v1_members_reordered", "v1_whitespace", "v1_further_members", "v1_escapes_and_characters", + "v1_depth_127", "v1_numbers_largest_finite", "v1_numbers_long_literals", "v1_numbers_toward_zero", + "v1_n_qubits_255", + "v1_measure_lax", "v1_measure_missing_shots", "v1_measure_u64_max", "v1_measure_not_u64", + "v1_measure_other_types", "v1_measure_not_object", "v1_oplog_trailing_bytes", "v1_param_on_h", + "v1_repeated_control", "v1_unknown_sections", "v2_unknown_sections", "v2_n_qubits_255", + "v2_measure_u64_max", +] + + +def check_accepts(rep, root): + tree = os.path.join(root, "accept") + listing, why = read_object(os.path.join(tree, "accept.json")) + if listing is None: + rep.check("accept/accept.json", why) + return + want = sorted("%s.wqc" % stem for stem in ACCEPTS) + rep.check("accept/accept.json lists every accept case", + None if sorted(listing) == want else "differs in %s" % sorted(set(listing) ^ set(want))) + rep.check("accept/ holds exactly the listed files", listed_and_present(listing, tree)) + for file in want: + label = "accept/" + file + try: + with open(os.path.join(tree, file), "rb") as f: + data = f.read() + gs, n_qubits, ops, measure = read_wqc(data) + except OSError as e: + rep.check(label, str(e)) + continue + except Refused as r: + rep.check(label, "refused as %s (%s); it must be accepted" % (r.kind, r.rule)) + continue + got = { + "capability": gs.capability, + "n_qubits": n_qubits, + "n_ops": len(ops), + "circuit_hash": circuit_hash(gs, n_qubits, ops), + "measure": None if measure is None else {"seed": measure[0], "shots": measure[1]}, + } + rep.check(label + " accepted and read as listed", + None if got == listing.get(file) else "read as %s, listed as %s" % (got, listing.get(file))) + + +def check_mutations(rep): + n, ops, _ = VECTORS["dyadic_ladder"] + for gs in (V1, V2): + base = statevector_hash(n, simulate(n, ops, gs.table)) + missed = [] + for k in range(1, DYADIC_MAX + 1): + for component in (0, 1): + for delta in (-1, 1): + table = list(gs.table) + entry = list(table[k]) + entry[component] += delta + table[k] = tuple(entry) + if statevector_hash(n, simulate(n, ops, table)) == base: + missed.append("P(%d).%s%+d" % (k, "re" if component == 0 else "im", delta)) + rep.check("dyadic_ladder sees every ±1 change of P(1..32) in the %s table" % gs.label, + "missed %s" % missed if missed else None) + + +def main(): + root = sys.argv[1] if len(sys.argv) > 1 else os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") + extension = sys.argv[2] if len(sys.argv) > 2 else os.path.join(root, "..", "extensions", "quantum-sim.md") + rep = Report() + check_blake3(rep) + check_tables(rep) + check_appendix(rep, extension) + check_tree(rep, root, "", V1, False) + check_tree(rep, root, "v2", V2, True) + check_rejects(rep, root) + check_accepts(rep, root) + check_mutations(rep) + if rep.failures: + print("%d failed — NOT conformant" % rep.failures) + sys.exit(1) + print("referee: every check passes") + + +if __name__ == "__main__": + main() diff --git a/wai/quantum-conformance/v2/expected_hashes.json b/wai/quantum-conformance/v2/expected_hashes.json new file mode 100644 index 000000000..ddcf98c29 --- /dev/null +++ b/wai/quantum-conformance/v2/expected_hashes.json @@ -0,0 +1,56 @@ +{ + "bell.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 2, + "n_ops": 2, + "circuit_hash": "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b", + "statevector_hash": "db68a50b08a8c2eabc89a881db9fcb130fff89dc36182d4c7f90da5bf4720b15", + "measure": { + "seed": 12648430, + "shots": 100000, + "histogram_hash": "0893f48ef32ca9f2865f6617537f0a4a5334f67e3c944bba845beb2f377acdb3" + } + }, + "ghz3.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 3, + "n_ops": 3, + "circuit_hash": "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f", + "statevector_hash": "51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8" + }, + "qft3.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 3, + "n_ops": 9, + "circuit_hash": "c23a1b1e31b87705999cbc54866f4c9e3e4c4d2b274d6260672121abb5ca89b8", + "statevector_hash": "70fe25ec22008f86076328b2c0908ede778e6d8227e563e8c7dcd89f7e844bf2" + }, + "qft5.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 5, + "n_ops": 21, + "circuit_hash": "8559c83769ac8c0cb3aabc64672233aa569cd4ce92aa6545f1449982b9aa8b8c", + "statevector_hash": "50b01fab0bfee966e553a947aea6aa09cebf8f21ba201cd548d703048d66f2ba" + }, + "qft5_x22.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 5, + "n_ops": 24, + "circuit_hash": "c126604030b16f4f1e88e6683894f9316262f49b3419be06ab74c7dbd2441651", + "statevector_hash": "5def2fca1d29e9e2b9ca2020cd2fa2dc056d07ce2e21b6e101b6116a03efd61c" + }, + "dyadic_ladder.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 2, + "n_ops": 72, + "circuit_hash": "8a6dcf0d41ecefd1a9761fef49b385d7dffe7543a9382efda02bfe4f7a0896fe", + "statevector_hash": "28bdb7265c10d798012ac78b604327700acd8cdac0e1f88ed4045f8b0b78bc7c" + }, + "qft8_ones.wqc": { + "capability": "wai.quantum.circuit2", + "n_qubits": 8, + "n_ops": 56, + "circuit_hash": "286f044750b19937f6e7bf6f534053aa74a21777516c773dbd7c254df7301b34", + "statevector_hash": "e059d43a574780d819a872f7904bf96918ba5880e1c7c32b38159306acd559fe" + } +} diff --git a/wai/quantum-conformance/v2/vectors/bell.wqc b/wai/quantum-conformance/v2/vectors/bell.wqc new file mode 100644 index 000000000..83badc0cd Binary files /dev/null and b/wai/quantum-conformance/v2/vectors/bell.wqc differ diff --git a/wai/quantum-conformance/v2/vectors/dyadic_ladder.wqc b/wai/quantum-conformance/v2/vectors/dyadic_ladder.wqc new file mode 100644 index 000000000..484851926 Binary files /dev/null and b/wai/quantum-conformance/v2/vectors/dyadic_ladder.wqc differ diff --git a/wai/quantum-conformance/v2/vectors/ghz3.wqc b/wai/quantum-conformance/v2/vectors/ghz3.wqc new file mode 100644 index 000000000..7c7c1853b Binary files /dev/null and b/wai/quantum-conformance/v2/vectors/ghz3.wqc differ diff --git a/wai/quantum-conformance/v2/vectors/qft3.wqc b/wai/quantum-conformance/v2/vectors/qft3.wqc new file mode 100644 index 000000000..1fc23702a Binary files /dev/null and b/wai/quantum-conformance/v2/vectors/qft3.wqc differ diff --git a/wai/quantum-conformance/v2/vectors/qft5.wqc b/wai/quantum-conformance/v2/vectors/qft5.wqc new file mode 100644 index 000000000..101916660 Binary files /dev/null and b/wai/quantum-conformance/v2/vectors/qft5.wqc differ diff --git a/wai/quantum-conformance/v2/vectors/qft5_x22.wqc b/wai/quantum-conformance/v2/vectors/qft5_x22.wqc new file mode 100644 index 000000000..c60de2281 Binary files /dev/null and b/wai/quantum-conformance/v2/vectors/qft5_x22.wqc differ diff --git a/wai/quantum-conformance/v2/vectors/qft8_ones.wqc b/wai/quantum-conformance/v2/vectors/qft8_ones.wqc new file mode 100644 index 000000000..fad622ddd Binary files /dev/null and b/wai/quantum-conformance/v2/vectors/qft8_ones.wqc differ diff --git a/wai/quantum-conformance/vectors/dyadic_ladder.wqc b/wai/quantum-conformance/vectors/dyadic_ladder.wqc new file mode 100644 index 000000000..2a1f25534 Binary files /dev/null and b/wai/quantum-conformance/vectors/dyadic_ladder.wqc differ diff --git a/wai/quantum-conformance/vectors/qft5_x22.wqc b/wai/quantum-conformance/vectors/qft5_x22.wqc new file mode 100644 index 000000000..ca8b8a584 Binary files /dev/null and b/wai/quantum-conformance/vectors/qft5_x22.wqc differ diff --git a/wai/quantum-conformance/vectors/qft8_ones.wqc b/wai/quantum-conformance/vectors/qft8_ones.wqc new file mode 100644 index 000000000..163d10ef9 Binary files /dev/null and b/wai/quantum-conformance/vectors/qft8_ones.wqc differ diff --git a/wai/registry/capabilities.json b/wai/registry/capabilities.json index f8d98874c..ca312240b 100644 --- a/wai/registry/capabilities.json +++ b/wai/registry/capabilities.json @@ -923,6 +923,20 @@ "edition": null, "registered": "2026-07-25" }, + { + "capability": "wai.quantum.circuit2", + "kind": "payload", + "status": "registered", + "media": "binary", + "role": "standalone", + "tier": "lossless", + "licence": "royalty-free", + "replicate": "standard-defined", + "sets": [], + "spec_section": "§5 Interactive, temporal & computed content", + "edition": null, + "registered": "2026-10-09" + }, { "capability": "wai.quantum.job", "kind": "record", @@ -1437,10 +1451,15 @@ } ], "identifiers": [ + { + "identifier": "wai.det.amp64", + "spec_section": "§5 Interactive, temporal & computed content", + "meaning": "the complex fixed-point arithmetic of wai.quantum.circuit2: re and im each an i64 at scale 2^frac, products exact then rounded half up, sums exact (quantum-sim §2.1, §3.0)" + }, { "identifier": "wai.det.fixed64", "spec_section": "§5 Interactive, temporal & computed content", - "meaning": "the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in" + "meaning": "the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in (Q32.32, interactive-worlds §6.2); inside a wai.quantum.circuit contract it names the quantum-sim §3.0 arithmetic at frac 30, products rounded half up" }, { "identifier": "wai.world.sim.feed0", diff --git a/wai/vocab/v2.shapes.ttl b/wai/vocab/v2.shapes.ttl index dd4bfb0e9..ff62ab10f 100644 --- a/wai/vocab/v2.shapes.ttl +++ b/wai/vocab/v2.shapes.ttl @@ -70,6 +70,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.prior.nnc wai:wai.prior.set wai:wai.quantum.circuit + wai:wai.quantum.circuit2 wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -150,6 +151,7 @@ wai:ManifestShape a sh:NodeShape ; wai:wai.prior.nnc wai:wai.prior.set wai:wai.quantum.circuit + wai:wai.quantum.circuit2 wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -244,6 +246,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.prior.nnc wai:wai.prior.set wai:wai.quantum.circuit + wai:wai.quantum.circuit2 wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d @@ -324,6 +327,7 @@ wai:ComponentShape a sh:NodeShape ; wai:wai.prior.nnc wai:wai.prior.set wai:wai.quantum.circuit + wai:wai.quantum.circuit2 wai:wai.semantic.clip wai:wai.semantic.perco2 wai:wai.splat.4d diff --git a/wai/vocab/v2.ttl b/wai/vocab/v2.ttl index 9747b7ac6..77495b0fc 100644 --- a/wai/vocab/v2.ttl +++ b/wai/vocab/v2.ttl @@ -847,6 +847,18 @@ wai:wai.quantum.circuit a wai:Capability ; wai:specSection "§5 Interactive, temporal & computed content" ; wai:registeredOn "2026-07-25"^^xsd:date . +wai:wai.quantum.circuit2 a wai:Capability ; + rdfs:label "wai.quantum.circuit2" ; + wai:kind wai:kind-payload ; + wai:status wai:status-registered ; + wai:media wai:media-binary ; + wai:role wai:role-standalone ; + wai:tier wai:tier-lossless ; + wai:licence wai:licence-royalty-free ; + wai:replicateClass wai:replicate-standard-defined ; + wai:specSection "§5 Interactive, temporal & computed content" ; + wai:registeredOn "2026-10-09"^^xsd:date . + wai:wai.quantum.job a wai:Capability ; rdfs:label "wai.quantum.job" ; wai:kind wai:kind-record ; diff --git a/wai/wai-pack/Cargo.lock b/wai/wai-pack/Cargo.lock index b5be75c42..11894f19c 100644 --- a/wai/wai-pack/Cargo.lock +++ b/wai/wai-pack/Cargo.lock @@ -682,7 +682,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum-serve/Cargo.lock b/wai/wai-quantum-serve/Cargo.lock index 9b768f3ce..b196c468c 100644 --- a/wai/wai-quantum-serve/Cargo.lock +++ b/wai/wai-quantum-serve/Cargo.lock @@ -589,7 +589,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", @@ -598,7 +598,7 @@ dependencies = [ [[package]] name = "wai-quantum-serve" -version = "0.4.0" +version = "0.5.0" dependencies = [ "ed25519-dalek", "wai", diff --git a/wai/wai-quantum-serve/Cargo.toml b/wai/wai-quantum-serve/Cargo.toml index f30447a9c..578953629 100644 --- a/wai/wai-quantum-serve/Cargo.toml +++ b/wai/wai-quantum-serve/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wai-quantum-serve" -version = "0.4.0" +version = "0.5.0" edition = "2021" license = "Apache-2.0" description = "The WAI quantum stack as a WASI-HTTP component — deterministic quantum computation as a serverless function. Runs on any wasi:http/proxy host (wasmtime serve, and other component-model runtimes); returns the same statevector hashes and the same signed receipt bytes as a native run." diff --git a/wai/wai-quantum-serve/src/lib.rs b/wai/wai-quantum-serve/src/lib.rs index e9b463b28..b32824155 100644 --- a/wai/wai-quantum-serve/src/lib.rs +++ b/wai/wai-quantum-serve/src/lib.rs @@ -18,6 +18,15 @@ //! POST /verify body = receipt JSON → verdict //! ``` //! +//! `/run` (both forms), `/qasm` and `/receipt` take `gateset=v2` (the default, +//! `wai.quantum.circuit2`) or `gateset=v1` (the frozen `wai.quantum.circuit`), +//! and build the circuit in it; any other value is a 400. The circuit hash, the +//! statevector and the receipt follow the gateset. `/run` names the capability +//! in its JSON as `"capability"`, and `/run` and `/receipt` name it in a +//! `wai-capability` response header. The `/receipt` body stays the receipt JSON +//! alone, so it is the same bytes a native run seals. A circuit whose hash +//! cannot be computed is a 400. +//! //! `POST /verify` accepts a receipt in either form: as sealed, or with its //! joule figure's acquisition class inside the signature //! (`quantum_energy::Labelled`), and restates the class in its verdict. It @@ -27,7 +36,7 @@ //! `unconfirmed`, with the reason it stopped at. use ed25519_dalek::SigningKey; -use wai::quantum::{Circuit, Measure}; +use wai::quantum::{Circuit, Gateset, Measure}; use wai::quantum_energy::MaybeLabelled; use wai::quantum_receipt::QuantumReceipt; use wai::quantum_toolchain as qt; @@ -116,9 +125,15 @@ fn esc(s: &str) -> String { o } -fn send(out: ResponseOutparam, status: u16, ctype: &str, body: &[u8]) { +/// The response header that names the capability a circuit was built in. +const CAPABILITY_HEADER: &str = "wai-capability"; + +fn send(out: ResponseOutparam, status: u16, ctype: &str, capability: Option<&str>, body: &[u8]) { let hdrs = Fields::new(); let _ = hdrs.append("content-type", &ctype.as_bytes().to_vec()); + if let Some(c) = capability { + let _ = hdrs.append(CAPABILITY_HEADER, &c.as_bytes().to_vec()); + } let resp = OutgoingResponse::new(hdrs); let _ = resp.set_status_code(status); let Ok(rbody) = resp.body() else { return }; @@ -136,11 +151,34 @@ fn send(out: ResponseOutparam, status: u16, ctype: &str, body: &[u8]) { } fn json(out: ResponseOutparam, status: u16, body: String) { - send(out, status, "application/json", body.as_bytes()); + send(out, status, "application/json", None, body.as_bytes()); +} + +/// The JSON body of an error response. +fn err_body(msg: &str) -> String { + format!("{{\"error\":\"{}\"}}", esc(msg)) } fn err(out: ResponseOutparam, status: u16, msg: &str) { - json(out, status, format!("{{\"error\":\"{}\"}}", esc(msg))); + json(out, status, err_body(msg)); +} + +/// An answer computed apart from the WASI types, so the native tests can check +/// it: the status, the JSON body and, for a circuit that was built, the +/// capability it was built in. +struct Reply { + status: u16, + body: String, + capability: Option<&'static str>, +} + +impl Reply { + fn error(status: u16, msg: &str) -> Reply { + Reply { status, body: err_body(msg), capability: None } + } + fn send(self, out: ResponseOutparam) { + send(out, self.status, "application/json", self.capability, self.body.as_bytes()); + } } fn read_body(body: IncomingBody) -> Vec { @@ -211,16 +249,36 @@ fn qnum(pq: &str, key: &str, dflt: T) -> T { query(pq, key).and_then(|v| v.parse().ok()).unwrap_or(dflt) } +/// The gateset `?gateset=` names: `v2` (`wai.quantum.circuit2`, the default) +/// or `v1` (the frozen `wai.quantum.circuit`). Any other value is an error. +fn gateset(pq: &str) -> Result { + match query(pq, "gateset").as_deref() { + None | Some("v2") => Ok(Gateset::V2), + Some("v1") => Ok(Gateset::V1), + Some(other) => Err(format!("unknown gateset '{other}' (v1, v2)")), + } +} + +/// Move a built circuit into the gateset the request names. The builders make +/// circuit2 circuits with distinct controls and no parameter on a gate other +/// than `P`, which are valid circuit/1 circuits too. +fn in_gateset(mut c: Circuit, pq: &str) -> Result { + c.gateset = gateset(pq)?; + Ok(c) +} + fn build(pq: &str) -> Result { let algo = query(pq, "algo").ok_or("missing ?algo= (see /algorithms)")?; let n: u8 = qnum(pq, "n", 3); let p: u32 = qnum(pq, "p", 0); - qt::build_algorithm(&algo, n, p).ok_or(format!("unknown algorithm '{algo}'")) + let c = qt::build_algorithm(&algo, n, p).ok_or(format!("unknown algorithm '{algo}'"))?; + in_gateset(c, pq) } /// The simulation result as JSON — the same fields the CLI prints. fn run_json(c: &Circuit, top: usize) -> Result { c.validate().map_err(|e| format!("invalid circuit: {e:?}"))?; + let id = qt::circuit_id(c).map_err(|e| format!("circuit id: {e}"))?; let sv = c.simulate().map_err(|e| format!("simulate: {e:?}"))?; let (backend, why) = qt::recommend_backend(c); let n = c.n_qubits as usize; @@ -229,9 +287,10 @@ fn run_json(c: &Circuit, top: usize) -> Result { .map(|(k, p)| format!("{{\"state\":\"{k:0n$b}\",\"p\":{p}}}", n = n)) .collect(); Ok(format!( - "{{\"circuit_id\":\"{}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend:?}\",\ + "{{\"circuit_id\":\"{}\",\"capability\":\"{}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend:?}\",\ \"backend_reason\":\"{}\",\"work_amp_updates\":{},\"statevector_hash\":\"{}\",\"top\":[{}]}}", - hx(&qt::circuit_id(c)), + hx(&id), + c.gateset.capability(), c.n_qubits, c.ops.len(), esc(why), @@ -241,6 +300,38 @@ fn run_json(c: &Circuit, top: usize) -> Result { )) } +/// `/run`: 200 and the run JSON for the circuit the request built, or 400. +fn run_reply(built: Result, top: usize) -> Reply { + match built.and_then(|c| run_json(&c, top).map(|body| (c.gateset, body))) { + Ok((gs, body)) => Reply { status: 200, body, capability: Some(gs.capability()) }, + Err(e) => Reply::error(400, &e), + } +} + +/// `/receipt`: 200 and the sealed receipt JSON, or 400 for a request that +/// names no circuit or an unknown gateset, or a circuit the seal refuses. +fn receipt_reply(pq: &str) -> Reply { + let c = match build(pq) { + Ok(c) => c, + Err(e) => return Reply::error(400, &e), + }; + let shots: u64 = qnum(pq, "shots", 0); + let seed: u64 = qnum(pq, "seed", 0); + seal_reply(&c, (shots > 0).then_some(Measure { seed, shots })) +} + +/// Seal `c`: 200 and the receipt JSON, or 400 when the seal refuses the +/// circuit. Every refusal is the circuit's own: one its gateset does not allow, +/// so it has no circuit hash, or one too large to simulate. +fn seal_reply(c: &Circuit, measure: Option) -> Reply { + // Deterministic demo key so the sealed bytes match a native run. + let key = SigningKey::from_bytes(&[42u8; 32]); + match QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", c, measure, 0, None) { + Ok(r) => Reply { status: 200, body: r.to_json(), capability: Some(c.gateset.capability()) }, + Err(e) => Reply::error(400, &format!("seal: {e}")), + } +} + impl wasi::exports::http::incoming_handler::Guest for Component { fn handle(req: IncomingRequest, out: ResponseOutparam) { let method = req.method(); @@ -254,6 +345,7 @@ impl wasi::exports::http::incoming_handler::Guest for Component { 200, "{\"service\":\"wai-quantum\",\"kind\":\"wasi:http/proxy component\",\ \"deterministic\":true,\"note\":\"statevector hashes and receipt bytes are identical to a native run\",\ + \"gateset\":{\"default\":\"v2\",\"v2\":\"wai.quantum.circuit2\",\"v1\":\"wai.quantum.circuit\"},\ \"endpoints\":[\"/health\",\"/algorithms\",\"/run?algo=ghz&n=5\",\"/qasm?algo=bell\",\"/receipt?algo=ghz&n=3\",\"POST /run (OpenQASM)\",\"POST /verify (receipt JSON)\"]}" .to_string(), ), @@ -275,13 +367,7 @@ impl wasi::exports::http::incoming_handler::Guest for Component { json(out, 200, format!("{{\"algorithms\":[{}]}}", items.join(","))); } - (false, "/run") => match build(&pq) { - Err(e) => err(out, 400, &e), - Ok(c) => match run_json(&c, qnum(&pq, "top", 8usize)) { - Ok(body) => json(out, 200, body), - Err(e) => err(out, 400, &e), - }, - }, + (false, "/run") => run_reply(build(&pq), qnum(&pq, "top", 8usize)).send(out), (true, "/run") => { let top = qnum(&pq, "top", 8usize); @@ -289,35 +375,16 @@ impl wasi::exports::http::incoming_handler::Guest for Component { return err(out, 400, "no body"); }; let src = String::from_utf8_lossy(&read_body(body)).into_owned(); - match qt::from_qasm(&src) { - Err(e) => err(out, 400, &format!("QASM parse: {e}")), - Ok(c) => match run_json(&c, top) { - Ok(b) => json(out, 200, b), - Err(e) => err(out, 400, &e), - }, - } + let built = qt::from_qasm(&src).map_err(|e| format!("QASM parse: {e}")); + run_reply(built.and_then(|c| in_gateset(c, &pq)), top).send(out) } (false, "/qasm") => match build(&pq) { Err(e) => err(out, 400, &e), - Ok(c) => send(out, 200, "text/plain; charset=utf-8", qt::to_qasm(&c).as_bytes()), + Ok(c) => send(out, 200, "text/plain; charset=utf-8", None, qt::to_qasm(&c).as_bytes()), }, - (false, "/receipt") => match build(&pq) { - Err(e) => err(out, 400, &e), - Ok(c) => { - let shots: u64 = qnum(&pq, "shots", 0); - let seed: u64 = qnum(&pq, "seed", 0); - let measure = (shots > 0).then_some(Measure { seed, shots }); - // Deterministic demo key so the sealed bytes match a native run. - let key = SigningKey::from_bytes(&[42u8; 32]); - match QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, measure, 0, None) - { - Ok(r) => json(out, 200, r.to_json()), - Err(e) => err(out, 500, &format!("seal: {e:?}")), - } - } - }, + (false, "/receipt") => receipt_reply(&pq).send(out), (true, "/verify") => { let Ok(body) = req.consume() else { @@ -336,6 +403,7 @@ impl wasi::exports::http::incoming_handler::Guest for Component { #[cfg(test)] mod tests { use super::*; + use wai::quantum::{circuit_hash, BaseGate, Gate}; use wai::quantum_energy::{CounterFiltering, EnergyClass, Labelled, Uncertainty}; fn ghz3() -> Circuit { @@ -344,6 +412,107 @@ mod tests { c } + /// GHZ3's circuit and statevector hashes in each capability, as the + /// conformance corpora state them (`v2/expected_hashes.json` and + /// `expected_hashes.json`). + const GHZ3_V2: (&str, &str) = ( + "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f", + "51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8", + ); + const GHZ3_V1: (&str, &str) = ( + "34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c", + "08c5e6fb50b91a00d28795506cc5d99ce1e9a3a8b4c41e4bfd05f5d829af9542", + ); + + #[test] + fn the_gateset_parameter_defaults_to_circuit2_and_names_no_other() { + assert_eq!(gateset("/run?algo=bell"), Ok(Gateset::V2)); + assert_eq!(gateset("/run?algo=bell&gateset=v2"), Ok(Gateset::V2)); + assert_eq!(gateset("/run?gateset=v1&algo=bell"), Ok(Gateset::V1)); + for bad in ["v3", "V1", "", "circuit2", "wai.quantum.circuit"] { + let e = gateset(&format!("/run?algo=bell&gateset={bad}")).unwrap_err(); + assert!(e.contains("unknown gateset"), "{bad}: {e}"); + } + // The OpenQASM body of `POST /run` takes the query's gateset too. + let c = qt::from_qasm("OPENQASM 3.0;\nqubit[3] q;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\n").unwrap(); + assert_eq!(in_gateset(c.clone(), "/run?gateset=v1").unwrap().gateset, Gateset::V1); + assert_eq!(in_gateset(c.clone(), "/run").unwrap().gateset, Gateset::V2); + assert!(in_gateset(c, "/run?gateset=v0").is_err()); + } + + /// `/run` builds in the gateset the request names and says which, in its + /// JSON and its header; its hashes are the corpus's for that capability. + #[test] + fn run_names_its_capability_and_reproduces_the_corpus() { + for (q, capability, (circuit, statevector)) in [ + ("/run?algo=ghz&n=3", "wai.quantum.circuit2", GHZ3_V2), + ("/run?algo=ghz&n=3&gateset=v2", "wai.quantum.circuit2", GHZ3_V2), + ("/run?algo=ghz&n=3&gateset=v1", "wai.quantum.circuit", GHZ3_V1), + ] { + let r = run_reply(build(q), 8); + assert_eq!((r.status, r.capability), (200, Some(capability)), "{q}: {}", r.body); + assert!( + r.body.starts_with(&format!("{{\"circuit_id\":\"{circuit}\",\"capability\":\"{capability}\",\"n_qubits\":3,")), + "{q}: {}", + r.body + ); + assert!(r.body.contains(&format!("\"statevector_hash\":\"{statevector}\"")), "{q}: {}", r.body); + } + let r = run_reply(build("/run?algo=ghz&n=3&gateset=v9"), 8); + assert_eq!((r.status, r.capability), (400, None)); + assert_eq!(r.body, "{\"error\":\"unknown gateset 'v9' (v1, v2)\"}"); + } + + /// A circuit with no circuit hash is a 400 from `/run` and from the seal, + /// never a 500 or a panic. A repeated control is such a circuit in + /// circuit2, and an ordinary one in circuit/1. + #[test] + fn a_circuit_with_no_hash_is_a_bad_request() { + let mut c = Circuit::new(3); + c.h(0); + c.ops.push(Gate { base: BaseGate::X, controls: vec![0, 0], target: 1, param: 0 }); + assert!(circuit_hash(&c).is_err()); + let r = run_reply(Ok(c.clone()), 8); + assert_eq!((r.status, r.capability), (400, None), "{}", r.body); + assert!(r.body.contains("invalid circuit"), "{}", r.body); + let r = seal_reply(&c, None); + assert_eq!((r.status, r.capability), (400, None), "{}", r.body); + assert!(r.body.starts_with("{\"error\":\"seal: invalid circuit: "), "{}", r.body); + + c.gateset = Gateset::V1; + assert!(circuit_hash(&c).is_ok()); + assert_eq!(run_reply(Ok(c.clone()), 8).status, 200); + assert_eq!(seal_reply(&c, None).status, 200); + } + + /// `/receipt` seals in the gateset the request names. Its body is the + /// receipt JSON alone, the bytes a native seal of the same circuit gives; + /// the capability rides in the header. + #[test] + fn receipt_seals_in_the_requested_gateset() { + for (q, gs, circuit) in [ + ("/receipt?algo=ghz&n=3&shots=64&seed=7", Gateset::V2, GHZ3_V2.0), + ("/receipt?algo=ghz&n=3&shots=64&seed=7&gateset=v1", Gateset::V1, GHZ3_V1.0), + ] { + let r = receipt_reply(q); + assert_eq!((r.status, r.capability), (200, Some(gs.capability())), "{q}: {}", r.body); + let mut c = ghz3(); + c.gateset = gs; + let key = SigningKey::from_bytes(&[42u8; 32]); + let native = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, Some(Measure { seed: 7, shots: 64 }), 0, None).unwrap(); + assert_eq!(r.body, native.to_json(), "{q}"); + assert!(!r.body.contains("capability"), "{q}"); + let parsed = QuantumReceipt::from_json(&r.body).unwrap(); + assert!(parsed.verify()); + assert_eq!(hx(&parsed.circuit_hash), circuit, "{q}"); + assert!(parsed.verify_reconstruction(&c)); + } + let r = receipt_reply("/receipt?algo=ghz&n=3&gateset=2"); + assert_eq!((r.status, r.capability), (400, None)); + assert!(r.body.contains("unknown gateset"), "{}", r.body); + assert_eq!(receipt_reply("/receipt?algo=nope").status, 400); + } + #[test] fn verify_accepts_both_forms_and_restates_the_class() { let key = SigningKey::from_bytes(&[0x71u8; 32]); diff --git a/wai/wai-quantum/Cargo.lock b/wai/wai-quantum/Cargo.lock index 24e5f81a5..51a8fe778 100644 --- a/wai/wai-quantum/Cargo.lock +++ b/wai/wai-quantum/Cargo.lock @@ -415,7 +415,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-quantum/Cargo.toml b/wai/wai-quantum/Cargo.toml index 2831bd971..9561e8e23 100644 --- a/wai/wai-quantum/Cargo.toml +++ b/wai/wai-quantum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" edition = "2024" rust-version = "1.97" license = "Apache-2.0" diff --git a/wai/wai-quantum/README.md b/wai/wai-quantum/README.md index a10b06a3a..dba47d4f0 100644 --- a/wai/wai-quantum/README.md +++ b/wai/wai-quantum/README.md @@ -10,7 +10,7 @@ the edge, producing **byte-identical results and receipts on all three**. ```toml [dependencies] -wai-quantum = "0.3" +wai-quantum = "0.4" ``` ```rust @@ -54,7 +54,7 @@ wai-quantum qutrit # d = 3, not just qubits `channel` prints the Holevo bound before and after, and it can only fall: noise never creates information. -| `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)) | +| `quantum` | byte-exact statevector simulation (dyadic Clifford+T+P(k)), as `wai.quantum.circuit2` by default or as the frozen `wai.quantum.circuit` | | `quantum_toolchain` | algorithm library, backend recommendation, **OpenQASM 2/3 ingest of what the mainstream toolchains actually export** — both register and both measurement spellings, `rx`/`ry`/`rz`/`sx`/`u`/`u2`/`u3`/`crz`, and exact rational-multiple-of-pi angles (non-dyadic angles are refused, never rounded) — Bloch / entanglement / purity analysis | | `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector | | `quantum_mps` | tensor-network (matrix-product-state) backend | @@ -439,6 +439,16 @@ that four of these layers once gave different bits natively and as a WASI component. `tests/bit_identity.rs` pins one result from each, so any platform whose arithmetic differs fails there. +A circuit carries the `Gateset` its phases come from, and its `circuit_hash` +names it, so the same gates in the two gatesets are different circuits. +`Circuit::new` and `Circuit::qft` build `wai.quantum.circuit2` (`Gateset::V2`, +container magic `WQC2`), whose table holds the integers nearest to each +`e^{2πi/2^k}`. `wai.quantum.circuit` (`Gateset::V1`, `WQC1`) is kept bit for bit +so that every file, hash and receipt made with it reproduces; its table deviates +from the ideal phase for `P(k ≥ 8)` and applies none for `k ≥ 19`. A circuit +read from a container takes the container's gateset, and one derived from +another keeps it. + A receipt separates the two halves of a cost honestly: **work** is portable-exact (`n_ops · 2^n` amplitude updates, recomputed by the verifier, so a sink cannot inflate it) and **energy** is measured-attested (only the signer can @@ -456,6 +466,23 @@ its calibration in either form. A label's declared uncertainty must be at least the `0.5/√3` µJ (≈ 0.289 µJ) that rounding the figure to whole microjoules adds: `joules_micro × relative_ppm` of at least 288 676. +## Upgrading from 0.3 + +- `Circuit::new` builds `Gateset::V2`. To reproduce or verify an artefact made + with 0.3, read its `WQC1` file, or set `c.gateset = Gateset::V1` on a circuit + built in code. +- `quantum::phase`, `quantum::inv_sqrt2` and `quantum::CAPABILITY` are removed; + `Gateset::phase`, `Gateset::inv_sqrt2` and `Gateset::capability` replace them. +- `to_wqc`, `circuit_hash` and `quantum_toolchain::circuit_id` return a `Result`. + They fail only on a circuit its gateset does not allow, so never on one that + has simulated. +- `Circuit` and `QuantumError` are `#[non_exhaustive]`, and `QuantumError` has a + `Contract` variant and a `kind()`. +- `quantum_fabric::certify_lanes` and `lanes::random_circuit` take a gateset. +- `from_wqc` refuses the circuit/1 payloads extensions/quantum-sim §8 lists; + `validate` refuses a gate with more than 255 controls, and a receipt's + `from_json` a qubit or op count its fields cannot hold. + ## Also available - A CLI (`wai-quantum` in the [`wai`](https://crates.io/crates/wai) crate) — run diff --git a/wai/wai-quantum/examples/bell.rs b/wai/wai-quantum/examples/bell.rs index 0045ac07f..538bb24a8 100644 --- a/wai/wai-quantum/examples/bell.rs +++ b/wai/wai-quantum/examples/bell.rs @@ -3,16 +3,26 @@ //! //! cargo run --example bell -use wai_quantum::quantum::Circuit; +use wai_quantum::quantum::{circuit_hash, Circuit}; use wai_quantum::quantum_toolchain as qt; fn main() { + // A new circuit is in the circuit2 gateset, whose phase table is correctly + // rounded; `Circuit::with_gateset(2, Gateset::V1)` reproduces circuit/1. let mut c = Circuit::new(2); c.h(0).cx(0, 1); // a Bell pair let sv = c.simulate().expect("simulate"); + // The circuit's identity covers its capability; a circuit that simulates + // always has one. + let id = circuit_hash(&c).expect("a valid circuit has a hash"); - println!("circuit {} qubits, {} gates", c.n_qubits, c.ops.len()); + println!("circuit {} qubits, {} gates, {}", c.n_qubits, c.ops.len(), c.gateset.capability()); + print!("circuit-id "); + for b in id { + print!("{b:02x}"); + } + println!(); let (backend, why) = qt::recommend_backend(&c); println!("backend {backend:?} — {why}"); diff --git a/wai/wai-quantum/examples/receipt.rs b/wai/wai-quantum/examples/receipt.rs index 68914bbad..68749b0aa 100644 --- a/wai/wai-quantum/examples/receipt.rs +++ b/wai/wai-quantum/examples/receipt.rs @@ -4,7 +4,7 @@ //! cargo run --example receipt use wai_quantum::ed25519_dalek::SigningKey; -use wai_quantum::quantum::Measure; +use wai_quantum::quantum::{Gateset, Measure}; use wai_quantum::quantum_receipt::QuantumReceipt; use wai_quantum::quantum_toolchain as qt; @@ -22,8 +22,18 @@ fn main() { ) .expect("seal"); + println!("capability {}", circuit.gateset.capability()); println!("verifies {}", receipt.verify()); println!("re-simulates to claim {}", receipt.verify_reconstruction(&circuit)); + + // The receipt binds the circuit's gateset: the same ops in circuit/1 do not + // reconstruct it, and the receipt says which gateset they need. + let mut v1 = circuit.clone(); + v1.gateset = Gateset::V1; + println!("same ops, circuit/1 {}", receipt.verify_reconstruction(&v1)); + if let Some(note) = receipt.gateset_note(&v1) { + println!(" {note}"); + } println!("work {} amplitude updates", receipt.work_amp_updates); println!("energy {} uJ (0 = unmetered)", receipt.joules_micro); diff --git a/wai/wai-quantum/src/ijson.rs b/wai/wai-quantum/src/ijson.rs new file mode 100644 index 000000000..49e6c8230 --- /dev/null +++ b/wai/wai-quantum/src/ijson.rs @@ -0,0 +1,524 @@ +//! The JSON a WQC container's contract and measurement sections are read as +//! (extensions/quantum-sim §2): I-JSON (RFC 7493), with every rule decided here +//! from the section's text. +//! +//! A general JSON parser leaves some of these rules to its build: whether a +//! number near the largest double is in range, for one, can depend on which +//! float parser was compiled in, and a float parser may stop reading a long +//! exponent. A conformance verdict must not, so this reader keeps each number's +//! literal text and decides each rule itself, the range rule on the digits. + +use std::collections::BTreeSet; +use std::fmt; + +/// The deepest nesting of arrays and objects a section may hold, the outermost +/// counted as the first. +pub(crate) const MAX_DEPTH: usize = 127; + +/// 2^1024 − 2^970 in decimal: the least magnitude that rounds to an infinite +/// double, rounding to nearest with ties to even. A number of this magnitude or +/// more is out of range (extensions/quantum-sim §2). +pub(crate) const RANGE_LIMIT: &str = "179769313486231580793728971405303415079934132710037826936173778980444968292764750946649017977587207096330286416692887910946555547851940402630657488671505820681908902000708383676273854845817711531764475730270069855571366959622842914819860834936475292719074168444365510704342711559699508093042880177904174497792"; + +/// A JSON value as the sections need it. A number keeps its literal text: the +/// sections read only unsigned integers, and the range rule is decided on the +/// text. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Json { + Null, + Bool(bool), + Number(String), + String(String), + Array(Vec), + Object(Vec<(String, Json)>), +} + +impl Json { + /// The member `name` of an object; `None` for a missing member or a value + /// that is not an object. + pub(crate) fn get(&self, name: &str) -> Option<&Json> { + match self { + Json::Object(members) => members.iter().find(|(k, _)| k == name).map(|(_, v)| v), + _ => None, + } + } + + pub(crate) fn as_str(&self) -> Option<&str> { + match self { + Json::String(s) => Some(s), + _ => None, + } + } + + /// The value of an unsigned JSON integer that fits a `u64`: a number written + /// with no sign, fraction or exponent. `-0`, `2.0` and `2e0` are not one. + pub(crate) fn as_u64(&self) -> Option { + match self { + Json::Number(t) if t.bytes().all(|b| b.is_ascii_digit()) => t.parse().ok(), + _ => None, + } + } +} + +impl fmt::Display for Json { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Json::Null => f.write_str("null"), + Json::Bool(b) => write!(f, "{b}"), + Json::Number(t) => f.write_str(t), + Json::String(s) => write!(f, "{s:?}"), + Json::Array(items) => { + f.write_str("[")?; + for (i, v) in items.iter().enumerate() { + if i > 0 { + f.write_str(",")?; + } + write!(f, "{v}")?; + } + f.write_str("]") + } + Json::Object(members) => { + f.write_str("{")?; + for (i, (k, v)) in members.iter().enumerate() { + if i > 0 { + f.write_str(",")?; + } + write!(f, "{k:?}:{v}")?; + } + f.write_str("}") + } + } + } +} + +/// Why a section is not I-JSON: the rule it breaks, and the byte offset where +/// the reader found it. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) struct NotIJson { + /// One of `utf-8`, `syntax`, `depth`, `duplicate member`, `surrogate`, + /// `noncharacter` and `number range`. + pub(crate) rule: &'static str, + pub(crate) at: usize, +} + +impl fmt::Display for NotIJson { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{} at byte {}", self.rule, self.at) + } +} + +/// `raw` read as one I-JSON value, with whitespace allowed around it. +pub(crate) fn parse(raw: &[u8]) -> Result { + let text = std::str::from_utf8(raw).map_err(|e| NotIJson { rule: "utf-8", at: e.valid_up_to() })?; + let mut p = Parser { s: text.as_bytes(), text, i: 0 }; + p.ws(); + let v = p.value(0)?; + p.ws(); + if p.i != p.s.len() { + return Err(p.err("syntax")); + } + Ok(v) +} + +/// Whether `c` is a Unicode noncharacter: U+FDD0 to U+FDEF, and the last two +/// code points of every plane. +pub(crate) fn is_noncharacter(c: char) -> bool { + let c = u32::from(c); + (0xFDD0..=0xFDEF).contains(&c) || c & 0xFFFE == 0xFFFE +} + +/// Whether a JSON number's magnitude is [`RANGE_LIMIT`] or more, decided on +/// its digits. Written as `0.D × 10^k`, with `D` its significant digits, the +/// number is out of range when `k` exceeds the limit's 309 digits, and when `k` +/// equals it and `D` is at least the limit's digits. +fn out_of_range(literal: &str) -> bool { + let unsigned = literal.strip_prefix('-').unwrap_or(literal); + let (mantissa, exponent) = unsigned.split_once(['e', 'E']).unwrap_or((unsigned, "")); + let (whole, frac) = mantissa.split_once('.').unwrap_or((mantissa, "")); + let digits: Vec = whole.bytes().chain(frac.bytes()).collect(); + let lead = digits.iter().take_while(|&&d| d == b'0').count(); + let Some(last) = digits.iter().rposition(|&d| d != b'0') else { + return false; // zero + }; + let significant = &digits[lead..=last]; + let (negative, e) = match exponent.as_bytes().first() { + Some(b'-') => (true, &exponent[1..]), + Some(b'+') => (false, &exponent[1..]), + _ => (false, exponent), + }; + let e = e.trim_start_matches('0'); + // A section holds at most u32::MAX digits, so an exponent past 10^18 decides + // the rule by its sign alone. + if e.len() > 18 { + return !negative; + } + let e: i64 = if e.is_empty() { 0 } else { e.parse().expect("at most 18 decimal digits") }; + let k = whole.len() as i64 - lead as i64 + if negative { -e } else { e }; + let limit = RANGE_LIMIT.as_bytes(); + if k != limit.len() as i64 { + return k > limit.len() as i64; + } + for i in 0..significant.len().max(limit.len()) { + let (a, b) = (significant.get(i).copied().unwrap_or(b'0'), limit.get(i).copied().unwrap_or(b'0')); + if a != b { + return a > b; + } + } + true +} + +struct Parser<'a> { + s: &'a [u8], + text: &'a str, + i: usize, +} + +impl Parser<'_> { + fn err(&self, rule: &'static str) -> NotIJson { + NotIJson { rule, at: self.i } + } + + fn peek(&self) -> Option { + self.s.get(self.i).copied() + } + + fn ws(&mut self) { + while matches!(self.peek(), Some(b' ' | b'\t' | b'\n' | b'\r')) { + self.i += 1; + } + } + + fn eat(&mut self, b: u8) -> Result<(), NotIJson> { + if self.peek() == Some(b) { + self.i += 1; + Ok(()) + } else { + Err(self.err("syntax")) + } + } + + /// A value inside `depth` arrays and objects. + fn value(&mut self, depth: usize) -> Result { + match self.peek() { + Some(b'{') => self.object(depth + 1), + Some(b'[') => self.array(depth + 1), + Some(b'"') => self.string().map(Json::String), + Some(b'-' | b'0'..=b'9') => self.number(), + Some(b't') => self.literal("true", Json::Bool(true)), + Some(b'f') => self.literal("false", Json::Bool(false)), + Some(b'n') => self.literal("null", Json::Null), + _ => Err(self.err("syntax")), + } + } + + fn literal(&mut self, word: &str, v: Json) -> Result { + if self.s[self.i..].starts_with(word.as_bytes()) { + self.i += word.len(); + Ok(v) + } else { + Err(self.err("syntax")) + } + } + + fn object(&mut self, depth: usize) -> Result { + if depth > MAX_DEPTH { + return Err(self.err("depth")); + } + self.i += 1; + let (mut members, mut seen) = (Vec::new(), BTreeSet::new()); + self.ws(); + if self.peek() == Some(b'}') { + self.i += 1; + return Ok(Json::Object(members)); + } + loop { + self.ws(); + let at = self.i; + if self.peek() != Some(b'"') { + return Err(self.err("syntax")); + } + let name = self.string()?; + if !seen.insert(name.clone()) { + return Err(NotIJson { rule: "duplicate member", at }); + } + self.ws(); + self.eat(b':')?; + self.ws(); + let v = self.value(depth)?; + members.push((name, v)); + self.ws(); + match self.peek() { + Some(b',') => self.i += 1, + Some(b'}') => { + self.i += 1; + return Ok(Json::Object(members)); + } + _ => return Err(self.err("syntax")), + } + } + } + + fn array(&mut self, depth: usize) -> Result { + if depth > MAX_DEPTH { + return Err(self.err("depth")); + } + self.i += 1; + let mut items = Vec::new(); + self.ws(); + if self.peek() == Some(b']') { + self.i += 1; + return Ok(Json::Array(items)); + } + loop { + self.ws(); + items.push(self.value(depth)?); + self.ws(); + match self.peek() { + Some(b',') => self.i += 1, + Some(b']') => { + self.i += 1; + return Ok(Json::Array(items)); + } + _ => return Err(self.err("syntax")), + } + } + } + + /// Four hex digits after `\u`. + fn hex4(&mut self) -> Result { + let digits = self.s.get(self.i..self.i + 4).ok_or_else(|| self.err("syntax"))?; + let mut v = 0; + for &d in digits { + v = v * 16 + char::from(d).to_digit(16).ok_or_else(|| self.err("syntax"))?; + } + self.i += 4; + Ok(v) + } + + /// A string, unescaped. No surrogate and no noncharacter, raw or escaped; + /// an escaped surrogate pair is the character it encodes. + fn string(&mut self) -> Result { + self.i += 1; + let mut out = String::new(); + loop { + let at = self.i; + let c = match self.peek() { + None => return Err(self.err("syntax")), + Some(b'"') => { + self.i += 1; + return Ok(out); + } + Some(0..=0x1F) => return Err(self.err("syntax")), + Some(b'\\') => { + self.i += 1; + let e = self.peek().ok_or_else(|| self.err("syntax"))?; + self.i += 1; + match e { + b'"' => '"', + b'\\' => '\\', + b'/' => '/', + b'b' => '\u{8}', + b'f' => '\u{c}', + b'n' => '\n', + b'r' => '\r', + b't' => '\t', + b'u' => { + let hi = self.hex4()?; + let code = match hi { + 0xD800..=0xDBFF => { + if !self.s[self.i..].starts_with(b"\\u") { + return Err(NotIJson { rule: "surrogate", at }); + } + self.i += 2; + let lo = self.hex4()?; + if !(0xDC00..=0xDFFF).contains(&lo) { + return Err(NotIJson { rule: "surrogate", at }); + } + 0x10000 + ((hi - 0xD800) << 10) + (lo - 0xDC00) + } + 0xDC00..=0xDFFF => return Err(NotIJson { rule: "surrogate", at }), + _ => hi, + }; + char::from_u32(code).ok_or(NotIJson { rule: "surrogate", at })? + } + _ => return Err(NotIJson { rule: "syntax", at }), + } + } + Some(_) => { + let c = self.text[self.i..].chars().next().ok_or_else(|| self.err("syntax"))?; + self.i += c.len_utf8(); + c + } + }; + if is_noncharacter(c) { + return Err(NotIJson { rule: "noncharacter", at }); + } + out.push(c); + } + } + + /// `-? (0 | [1-9][0-9]*) (. [0-9]+)? ([eE] [+-]? [0-9]+)?`, in range. + fn number(&mut self) -> Result { + let start = self.i; + let digits = |p: &mut Self| { + let from = p.i; + while matches!(p.peek(), Some(b'0'..=b'9')) { + p.i += 1; + } + p.i - from + }; + if self.peek() == Some(b'-') { + self.i += 1; + } + match self.peek() { + Some(b'0') => self.i += 1, + Some(b'1'..=b'9') => { + digits(self); + } + _ => return Err(self.err("syntax")), + } + if self.peek() == Some(b'.') { + self.i += 1; + if digits(self) == 0 { + return Err(self.err("syntax")); + } + } + if matches!(self.peek(), Some(b'e' | b'E')) { + self.i += 1; + if matches!(self.peek(), Some(b'+' | b'-')) { + self.i += 1; + } + if digits(self) == 0 { + return Err(self.err("syntax")); + } + } + let literal = &self.text[start..self.i]; + if out_of_range(literal) { + return Err(NotIJson { rule: "number range", at: start }); + } + Ok(Json::Number(literal.to_string())) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 2^e in decimal digits, most significant first. + fn pow2(e: u32) -> Vec { + let mut d = vec![1u8]; // least significant first + for _ in 0..e { + let mut carry = 0; + for x in d.iter_mut() { + let v = *x * 2 + carry; + (*x, carry) = (v % 10, v / 10); + } + if carry > 0 { + d.push(carry); + } + } + d.reverse(); + d + } + + /// `a − b` for decimal digit strings with `a ≥ b`, most significant first. + fn sub(a: &[u8], b: &[u8]) -> String { + let (mut out, mut borrow) = (Vec::new(), 0i8); + for i in 0..a.len() { + let x = a[a.len() - 1 - i] as i8 - borrow - b.len().checked_sub(1 + i).map_or(0, |j| b[j] as i8); + (borrow, out) = (i8::from(x < 0), [vec![(x + if x < 0 { 10 } else { 0 }) as u8], out].concat()); + } + let s: String = out.iter().map(|d| char::from(b'0' + d)).collect(); + s.trim_start_matches('0').to_string() + } + + /// The limit is 2^1024 − 2^970, derived here in decimal, and the standard + /// library's parse, where it is exact, puts the limit in the same place. + #[test] + fn the_range_limit_is_2_1024_minus_2_970() { + assert_eq!(sub(&pow2(1024), &pow2(970)), RANGE_LIMIT); + let below = sub(RANGE_LIMIT.as_bytes().iter().map(|b| b - b'0').collect::>().as_slice(), &[1]); + assert!(RANGE_LIMIT.parse::().unwrap().is_infinite()); + assert_eq!(below.parse::().unwrap(), f64::MAX); + for t in ["1e308", "1.7976931348623157e308", "1.7976931348623158e308", "1.7976931348623159e308", "2e308", "9e307", "1e-400", RANGE_LIMIT, &below] { + assert_eq!(out_of_range(t), t.parse::().unwrap().is_infinite(), "{t}"); + } + } + + fn rule(text: &str) -> Option<&'static str> { + parse(text.as_bytes()).err().map(|e| e.rule) + } + + /// The range rule falls exactly at 2^1024 − 2^970, written as an integer or + /// with a fraction or exponent, and a number that underflows is in range. + #[test] + fn number_range_is_exact() { + assert_eq!(RANGE_LIMIT.len(), 309); + let below = format!("{}1", &RANGE_LIMIT[..308]); // RANGE_LIMIT − 1, its last digit 2 + assert_eq!(below.as_bytes()[308], b'1'); + assert_eq!(rule(RANGE_LIMIT), Some("number range")); + assert_eq!(rule(&format!("-{RANGE_LIMIT}")), Some("number range")); + assert_eq!(rule(&below), None); + assert_eq!(rule(&format!("{}.{}e308", &RANGE_LIMIT[..1], &RANGE_LIMIT[1..])), Some("number range")); + assert_eq!(rule(&format!("{}.{}e308", &below[..1], &below[1..])), None); + assert_eq!(rule("1.7976931348623158e308"), None); + assert_eq!(rule("1.7976931348623159e308"), Some("number range")); + assert_eq!(rule("179769313486231570814527423731704356798070567525844996598917476803157260780028538760589558632766878171540458953514382464234321326889464182768467546703537516986049910576551282076245490090389328944075868508455133942304583236903222948165808559332123348274797826204144723168738177180919299881250404026184124858368"), None); + assert_eq!(rule("1e400"), Some("number range")); + assert_eq!(rule("1e99999999999999999999999"), Some("number range")); + assert_eq!(rule("1e-400"), None); + assert_eq!(rule("0e99999999999999999999999"), None); + assert_eq!(rule("-0"), None); + // Long literals whose exponent offsets a run of zeros, past where a + // float parser may stop reading the exponent; and long exponents. + let zeros = "0".repeat(655_360); + assert_eq!(rule(&format!("1{zeros}e-655360")), None); + assert_eq!(rule(&format!("0.{}1e655669", &zeros[1..])), Some("number range")); + assert_eq!(rule(&format!("0.{}1e655668", &zeros[1..])), None); + assert_eq!(rule(&format!("1e{}1", "0".repeat(4300))), None); + assert_eq!(rule(&format!("1e{}", "9".repeat(30))), Some("number range")); + assert_eq!(rule(&format!("1e-{}", "9".repeat(30))), None); + assert_eq!(rule(&format!("0e{}", "9".repeat(30))), None); + } + + /// Only a number with no sign, fraction or exponent is an unsigned integer, + /// and only one that fits a `u64` reads as one. + #[test] + fn unsigned_integers() { + let n = |t: &str| parse(t.as_bytes()).unwrap().as_u64(); + assert_eq!(n("0"), Some(0)); + assert_eq!(n("18446744073709551615"), Some(u64::MAX)); + assert_eq!(n("18446744073709551616"), None); + for t in ["-0", "-1", "2.0", "2e0", "2E0"] { + assert_eq!(n(t), None, "{t}"); + } + } + + /// Each rule, refused by name; and what the rules allow. + #[test] + fn rules() { + let deep = |n: usize| format!("{}{}", "[".repeat(n), "]".repeat(n)); + assert_eq!(rule(&deep(MAX_DEPTH)), None); + assert_eq!(rule(&deep(MAX_DEPTH + 1)), Some("depth")); + assert_eq!(rule(&format!("{{\"a\":{}}}", deep(MAX_DEPTH - 1))), None); + assert_eq!(rule(&format!("{{\"a\":{}}}", deep(MAX_DEPTH))), Some("depth")); + assert_eq!(rule(r#"{"a":1,"a":1}"#), Some("duplicate member")); + assert_eq!(rule(r#"{"a":1,"\u0061":2}"#), Some("duplicate member")); + assert_eq!(rule(r#"[{"a":1},{"a":1}]"#), None); + assert_eq!(rule(r#"{"a":{"b":1,"b":2}}"#), Some("duplicate member")); + assert_eq!(rule(r#""\ud800""#), Some("surrogate")); + assert_eq!(rule(r#""\udc00""#), Some("surrogate")); + assert_eq!(rule(r#""\ud800\u0041""#), Some("surrogate")); + assert_eq!(rule(r#""\ud83d\ude00""#), None); + assert_eq!(rule(r#""\ufdd0""#), Some("noncharacter")); + assert_eq!(rule(r#""\ud83f\udffe""#), Some("noncharacter")); + assert_eq!(rule("\"\u{ffff}\""), Some("noncharacter")); + assert_eq!(rule(r#"{"\uffff":1}"#), Some("noncharacter")); + assert_eq!(rule("\"\u{fffd}\u{fdcf}\u{fdf0}\""), None); + assert_eq!(parse(b"\"\xff\"").unwrap_err(), NotIJson { rule: "utf-8", at: 1 }); + for bad in ["", "\u{feff}{}", "{} x", "NaN", "Infinity", "01", "1.", ".5", "+1", "1e", "[1,]", "{\"a\"}", "\"\x01\"", "\"\\x\"", "tru", "'a'"] { + assert_eq!(rule(bad), Some("syntax"), "{bad:?}"); + } + assert_eq!(parse(b" \t\n\r{ \"a\" : [ 1 , -2.5e-3 , true , false , null ] }\r\n").unwrap().get("a").map(Json::to_string).as_deref(), Some("[1,-2.5e-3,true,false,null]")); + } +} diff --git a/wai/wai-quantum/src/lib.rs b/wai/wai-quantum/src/lib.rs index 2a9e5fe81..52f1fc996 100644 --- a/wai/wai-quantum/src/lib.rs +++ b/wai/wai-quantum/src/lib.rs @@ -51,6 +51,10 @@ mod repro; pub use ed25519_dalek; +// The strict JSON reader for a container's contract and measurement sections. +#[cfg(feature = "quantum")] +mod ijson; + #[cfg(feature = "quantum")] pub mod quantum; diff --git a/wai/wai-quantum/src/quantum.rs b/wai/wai-quantum/src/quantum.rs index 3afc0e06c..12b3836fd 100644 --- a/wai/wai-quantum/src/quantum.rs +++ b/wai/wai-quantum/src/quantum.rs @@ -1,5 +1,6 @@ //! Deterministic quantum-circuit **simulation** as transportable, receiptable -//! content — `wai.quantum.circuit` (extensions/quantum-sim). +//! content — `wai.quantum.circuit2` and the frozen `wai.quantum.circuit` +//! (extensions/quantum-sim). //! //! The framing first: WAI's core property is *determinism* — byte-identical //! reconstruction on every machine, "conformance is a hash". Real quantum @@ -10,12 +11,12 @@ //! compact **circuit** (a gate op-log), the sink CLASSICALLY simulates it, and the //! reconstructed statevector hashes identically everywhere. //! -//! The reconstruction runs in a fixed-point complex floor (the `wai.det.fixed64` -//! discipline applied to amplitudes): every amplitude is an [`Amp`] — real and -//! imaginary parts as `i64` at scale `2^FRAC` — and every gate is an integer -//! fixed-point matrix–vector update with a pinned rounding rule and a *stated -//! error bound* versus the ideal unitary (the same contract as -//! the WAI integer transform floor). No float anywhere on the decode, so: +//! The reconstruction runs in a fixed-point complex floor (`wai.det.amp64`, the +//! arithmetic a circuit/1 contract calls `wai.det.fixed64`): every amplitude is +//! an [`Amp`] — real and imaginary parts as `i64` at scale `2^FRAC` — and every +//! gate is an integer fixed-point matrix–vector update with a pinned rounding +//! rule and a *stated error bound* versus the ideal unitary (the same contract +//! as the WAI integer transform floor). No float anywhere on the decode, so: //! //! - **statevector-equivalence** — the whole amplitude vector is byte-identical on //! every machine (a portable statevector hash), and @@ -32,10 +33,20 @@ //! //! Gate set: a *controlled-1-qubit-gate* IR over Clifford+T + dyadic phase //! (`I,X,Y,Z,H,S,S†,T,T†,P(k)` with any number of controls). Clifford+T is dense -//! in `SU(2^n)`, so this is universal; the dyadic phase `P(k) = e^{2πi/2^k}` gives -//! the controlled-phase ladder the **QFT** ("quantum FFT") needs, transported as a -//! circuit and reconstructed byte-exact. Pure i64, no float, no `ort`; compiles to -//! wasm32. +//! in `SU(2^n)`, so this is universal. The dyadic phase `P(k) = e^{2πi/2^k}` is +//! read from the circuit's [`Gateset`] table: +//! +//! - [`Gateset::V2`] (`wai.quantum.circuit2`, magic `WQC2`) holds the integers +//! nearest to each phase, so its controlled-phase ladder gives the **QFT** +//! ("quantum FFT"), transported as a circuit and reconstructed byte-exact. +//! Circuits built in code are V2 unless built otherwise. +//! - [`Gateset::V1`] (`wai.quantum.circuit`, magic `WQC1`) is frozen so that +//! every circuit/1 file, hash and receipt reproduces. Its `P(k)` deviates from +//! the ideal phase for `k ≥ 8` and applies none for `k ≥ 19`. +//! +//! A circuit decoded from a container takes the container's gateset, and its +//! [`circuit_hash`] names the capability. Pure i64, no float, no `ort`; +//! compiles to wasm32. //! //! Out of scope (and honestly so): variational-quantum-circuit / "quantum AI" //! priors whose measurement is stochastic — those are at best a float/behavioral @@ -43,7 +54,7 @@ //! this exact-reconstruction extension. [`StateVector::fidelity_fx`] is provided as the //! quantum-information-theoretic conformance metric such a future path would use. -use std::sync::OnceLock; +use crate::ijson::{self, Json, RANGE_LIMIT}; /// Fractional bits of the fixed-point amplitude floor. Amplitudes live in /// `[-1, 1]`, so `2^FRAC` (≈ `1.07e9` at 30) is the unit and the i128 products in @@ -52,8 +63,8 @@ pub const FRAC: u32 = 30; /// The fixed-point unit `1.0`. pub const ONE: i64 = 1 << FRAC; const ROUND: i128 = 1 << (FRAC - 1); -/// Largest `k` for which `P(k) = e^{2πi/2^k}` is tabulated — the QFT reaches this -/// many qubits. The table is built by an integer half-angle recurrence (no float). +/// Largest `k` for which `P(k) = e^{2πi/2^k}` is tabulated: [`PHASE_V1`] and +/// [`PHASE_V2`] each hold `P(1..=DYADIC_MAX)`. pub const DYADIC_MAX: usize = 32; /// Domain separators for the conformance hashes (the WAI discipline). @@ -61,9 +72,6 @@ const DOMAIN_STATEVECTOR: &[u8] = b"wai:quantum-statevector\x01"; const DOMAIN_CIRCUIT: &[u8] = b"wai:quantum-circuit\x01"; const DOMAIN_HISTOGRAM: &[u8] = b"wai:quantum-histogram\x01"; -/// The capability string this module reconstructs. -pub const CAPABILITY: &str = "wai.quantum.circuit"; - // --------------------------------------------------------------------------- // Fixed-point complex amplitude // --------------------------------------------------------------------------- @@ -131,41 +139,236 @@ impl Amp { } // --------------------------------------------------------------------------- -// Pinned dyadic phase table — e^{2πi / 2^k}, built by integer half-angle -// recurrence from (cos π/2, sin π/2) = (0, 1). No float ever runs. +// Dyadic phase tables — P(k) = e^{2πi / 2^k} at scale 2^FRAC, one per gateset. +// Literal constants: no float and no recurrence runs on the decode path. // --------------------------------------------------------------------------- -fn dyadic_table() -> &'static [Amp] { - static TABLE: OnceLock> = OnceLock::new(); - TABLE.get_or_init(|| { - let mut t = vec![Amp::ZERO; DYADIC_MAX + 1]; - // k = 1: e^{iπ} = -1 + 0i - t[1] = Amp { re: -ONE, im: 0 }; - // k = 2: e^{iπ/2} = 0 + 1i - t[2] = Amp { re: 0, im: ONE }; - // k ≥ 3: half the angle each step. For θ ∈ (0, π/2], both cos and sin are - // non-negative, so the positive integer sqrt is the right branch: - // cos(θ/2) = sqrt((1+cosθ)/2), sin(θ/2) = sqrt((1−cosθ)/2) - for k in 3..=DYADIC_MAX { - let c = t[k - 1].re; - t[k] = Amp { - re: sqrt_fx((ONE + c) / 2), - im: sqrt_fx((ONE - c) / 2), - }; +/// The `wai.quantum.circuit` phase table, frozen bit for bit. +/// +/// Index `k` holds `P(k) = e^{2πi/2^k}` as `(re, im)` at scale `2^FRAC`, for +/// `1 ≤ k ≤ DYADIC_MAX`; index 0 is unused and holds [`Amp::ZERO`]. +/// +/// These are the values of the integer half-angle recurrence every circuit/1 +/// artefact was made with: `P(1) = (−2^30, 0)`, `P(2) = (0, 2^30)`, and for +/// `k ≥ 3`, with `c = P(k−1).re`, +/// `re = isqrt(⌊(2^30 + c)/2⌋·2^30)` and `im = isqrt(⌊(2^30 − c)/2⌋·2^30)`, +/// where `isqrt` is the floor square root. The floors compound: from `P(8)` on +/// the imaginary part is too large, by up to 7032 units at `P(18)`, and +/// `P(19..=32)` apply no phase at all. The table is kept exactly as it is so that +/// every circuit/1 file, hash and receipt reproduces. [`PHASE_V2`] is the +/// correctly rounded table. The extension states this table in its §3.1 and +/// Appendix B.1, rendered from this constant. +pub const PHASE_V1: [Amp; 33] = [ + Amp::ZERO, + Amp { re: -1073741824, im: 0 }, // k = 1 + Amp { re: 0, im: 1073741824 }, // k = 2 + Amp { re: 759250124, im: 759250124 }, // k = 3 + Amp { re: 992008094, im: 410903207 }, // k = 4 + Amp { re: 1053110175, im: 209476638 }, // k = 5 + Amp { re: 1068571463, im: 105245102 }, // k = 6 + Amp { re: 1072448454, im: 52686012 }, // k = 7 + Amp { re: 1073418432, im: 26350953 }, // k = 8 + Amp { re: 1073660972, im: 13176485 }, // k = 9 + Amp { re: 1073721610, im: 6588405 }, // k = 10 + Amp { re: 1073736770, im: 3294284 }, // k = 11 + Amp { re: 1073740560, im: 1647223 }, // k = 12 + Amp { re: 1073741507, im: 823774 }, // k = 13 + Amp { re: 1073741744, im: 411887 }, // k = 14 + Amp { re: 1073741803, im: 207243 }, // k = 15 + Amp { re: 1073741818, im: 103621 }, // k = 16 + Amp { re: 1073741822, im: 56755 }, // k = 17 + Amp { re: 1073741823, im: 32768 }, // k = 18 + Amp { re: 1073741823, im: 0 }, // k = 19 + Amp { re: 1073741823, im: 0 }, // k = 20 + Amp { re: 1073741823, im: 0 }, // k = 21 + Amp { re: 1073741823, im: 0 }, // k = 22 + Amp { re: 1073741823, im: 0 }, // k = 23 + Amp { re: 1073741823, im: 0 }, // k = 24 + Amp { re: 1073741823, im: 0 }, // k = 25 + Amp { re: 1073741823, im: 0 }, // k = 26 + Amp { re: 1073741823, im: 0 }, // k = 27 + Amp { re: 1073741823, im: 0 }, // k = 28 + Amp { re: 1073741823, im: 0 }, // k = 29 + Amp { re: 1073741823, im: 0 }, // k = 30 + Amp { re: 1073741823, im: 0 }, // k = 31 + Amp { re: 1073741823, im: 0 }, // k = 32 +]; + +/// The `wai.quantum.circuit2` phase table: each entry is the integer nearest to +/// `2^30·cos(2π/2^k)` and to `2^30·sin(2π/2^k)`. +/// +/// Index `k` holds `P(k)` as `(re, im)` at scale `2^FRAC`, for +/// `1 ≤ k ≤ DYADIC_MAX`; index 0 is unused and holds [`Amp::ZERO`]. `P(1)` and +/// `P(2)` are exact. For `k ≥ 3` every true value is irrational, so none is a +/// half-integer and the nearest integer is unique; the closest any comes to a +/// tie is 0.00456 unit, at the imaginary part of `P(22)`. Every component is +/// within ½ unit of the ideal, which the crate's tests prove with integer +/// interval arithmetic. The literal values are normative: the extension +/// states them in its §3.2 and Appendix B.2, rendered from this constant. +/// +/// `re P(3) == im P(3)`, so `T` keeps `re == im`. The resolution of `2^-30` is a +/// floor no rounding removes: `P(26..=32)` carry imaginary parts of 101 down to +/// 2, so the smallest angles are correct to the nearest unit and no further. +pub const PHASE_V2: [Amp; 33] = [ + Amp::ZERO, + Amp { re: -1073741824, im: 0 }, // k = 1 + Amp { re: 0, im: 1073741824 }, // k = 2 + Amp { re: 759250125, im: 759250125 }, // k = 3 + Amp { re: 992008094, im: 410903207 }, // k = 4 + Amp { re: 1053110176, im: 209476638 }, // k = 5 + Amp { re: 1068571464, im: 105245103 }, // k = 6 + Amp { re: 1072448455, im: 52686014 }, // k = 7 + Amp { re: 1073418433, im: 26350943 }, // k = 8 + Amp { re: 1073660973, im: 13176464 }, // k = 9 + Amp { re: 1073721611, im: 6588356 }, // k = 10 + Amp { re: 1073736771, im: 3294193 }, // k = 11 + Amp { re: 1073740561, im: 1647099 }, // k = 12 + Amp { re: 1073741508, im: 823550 }, // k = 13 + Amp { re: 1073741745, im: 411775 }, // k = 14 + Amp { re: 1073741804, im: 205887 }, // k = 15 + Amp { re: 1073741819, im: 102944 }, // k = 16 + Amp { re: 1073741823, im: 51472 }, // k = 17 + Amp { re: 1073741824, im: 25736 }, // k = 18 + Amp { re: 1073741824, im: 12868 }, // k = 19 + Amp { re: 1073741824, im: 6434 }, // k = 20 + Amp { re: 1073741824, im: 3217 }, // k = 21 + Amp { re: 1073741824, im: 1608 }, // k = 22 + Amp { re: 1073741824, im: 804 }, // k = 23 + Amp { re: 1073741824, im: 402 }, // k = 24 + Amp { re: 1073741824, im: 201 }, // k = 25 + Amp { re: 1073741824, im: 101 }, // k = 26 + Amp { re: 1073741824, im: 50 }, // k = 27 + Amp { re: 1073741824, im: 25 }, // k = 28 + Amp { re: 1073741824, im: 13 }, // k = 29 + Amp { re: 1073741824, im: 6 }, // k = 30 + Amp { re: 1073741824, im: 3 }, // k = 31 + Amp { re: 1073741824, im: 2 }, // k = 32 +]; + +/// Which phase table a circuit is reconstructed with, and so which capability +/// carries it. +/// +/// Both gatesets share the arithmetic, the gate matrices and the container +/// layout. They differ only in the phase table, which `H`, `T`, `T†` and `P(k)` +/// read (`H` uses `re P(3)` as `1/√2`). +/// +/// - [`Gateset::V1`] is `wai.quantum.circuit` (magic `WQC1`), frozen so that +/// existing content reproduces bit for bit. +/// - [`Gateset::V2`] is `wai.quantum.circuit2` (magic `WQC2`), with the correctly +/// rounded table. It is the default. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum Gateset { + /// `wai.quantum.circuit`, with the frozen [`PHASE_V1`] table. + V1, + /// `wai.quantum.circuit2`, with the correctly rounded [`PHASE_V2`] table. + #[default] + V2, +} + +impl Gateset { + /// Every gateset, oldest first. + pub const ALL: [Gateset; 2] = [Gateset::V1, Gateset::V2]; + + /// The capability string: `wai.quantum.circuit` or `wai.quantum.circuit2`. + pub const fn capability(self) -> &'static str { + match self { + Gateset::V1 => "wai.quantum.circuit", + Gateset::V2 => "wai.quantum.circuit2", } - t - }) -} + } -/// `1/√2` in the fixed-point floor — derived from the dyadic table (`cos π/4`), so -/// there is a *single* source for the one irrational the Clifford+T set needs. -pub fn inv_sqrt2() -> i64 { - dyadic_table()[3].re -} + /// The container magic: `WQC1` or `WQC2`. + pub const fn magic(self) -> [u8; 4] { + match self { + Gateset::V1 => *b"WQC1", + Gateset::V2 => *b"WQC2", + } + } -/// `e^{2πi / 2^k}` in the fixed-point floor, for `1 ≤ k ≤ DYADIC_MAX`. -pub fn phase(k: usize) -> Amp { - dyadic_table()[k] + /// The contract's `ext`: `/`, so + /// `wai.quantum.circuit2/1` is revision 1 of the circuit2 capability. + pub const fn ext(self) -> &'static str { + match self { + Gateset::V1 => "wai.quantum.circuit/1", + Gateset::V2 => "wai.quantum.circuit2/1", + } + } + + /// The contract's `numeric` identifier. + pub const fn numeric(self) -> &'static str { + match self { + Gateset::V1 => "wai.det.fixed64", + Gateset::V2 => "wai.det.amp64", + } + } + + /// The contract's `gateset` string, which names the reconstruction's gate + /// algebra and so differs wherever its constants differ. + pub const fn name(self) -> &'static str { + match self { + Gateset::V1 => "cliffordT+dyadicP", + Gateset::V2 => "cliffordT+dyadicP.rn", + } + } + + /// The phase table: [`PHASE_V1`] or [`PHASE_V2`]. + pub const fn table(self) -> &'static [Amp; 33] { + match self { + Gateset::V1 => &PHASE_V1, + Gateset::V2 => &PHASE_V2, + } + } + + /// `P(k) = e^{2πi/2^k}` from this gateset's table. `validate` guarantees + /// `1 ≤ k ≤ DYADIC_MAX` for every `P` in a circuit. + #[inline] + pub fn phase(self, k: usize) -> Amp { + self.table()[k] + } + + /// `1/√2` in this gateset: `re P(3)`, the single source of the one irrational + /// the Clifford+T set needs. 759250124 in V1, 759250125 in V2. + #[inline] + pub fn inv_sqrt2(self) -> i64 { + self.table()[3].re + } + + /// The canonical contract bytes for an `n_qubits` circuit: no whitespace, + /// members in a fixed order, rendered directly rather than through a JSON + /// serializer so that no serializer setting can change them. + pub fn contract_bytes(self, n_qubits: u8) -> Vec { + format!( + "{{\"ext\":\"{}\",\"n_qubits\":{},\"numeric\":\"{}\",\"frac\":{},\"gateset\":\"{}\"}}", + self.ext(), + n_qubits, + self.numeric(), + FRAC, + self.name(), + ) + .into_bytes() + } + + /// BLAKE3 of the table: `i64le(re) ‖ i64le(im)` for `k = 1..=DYADIC_MAX`, + /// 512 bytes, with no domain prefix. + pub fn table_digest(self) -> [u8; 32] { + let mut h = blake3::Hasher::new(); + for a in &self.table()[1..] { + h.update(&a.re.to_le_bytes()); + h.update(&a.im.to_le_bytes()); + } + *h.finalize().as_bytes() + } + + /// The gateset a container magic names, or `None` for any other bytes. + pub fn from_magic(m: &[u8]) -> Option { + Gateset::ALL.into_iter().find(|g| m == g.magic().as_slice()) + } + + /// The gateset a capability string names, or `None` for any other string. + pub fn from_capability(s: &str) -> Option { + Gateset::ALL.into_iter().find(|g| s == g.capability()) + } } // --------------------------------------------------------------------------- @@ -219,14 +422,16 @@ impl BaseGate { }) } - /// The 2×2 fixed-point matrix `[[m00,m01],[m10,m11]]` for this gate. - pub(crate) fn matrix(self, param: u16) -> [[Amp; 2]; 2] { + /// The 2×2 fixed-point matrix `[[m00,m01],[m10,m11]]` for this gate in the + /// gateset `gs`, whose table `H` (`re P(3)` as `1/√2`), `T`, `T†` and `P` + /// read. `param` must be in `1..=DYADIC_MAX` for `P`, as `validate` ensures. + pub(crate) fn matrix(self, param: u16, gs: Gateset) -> [[Amp; 2]; 2] { let z = Amp::ZERO; let one = Amp::ONE; let i = Amp { re: 0, im: ONE }; let neg_i = Amp { re: 0, im: -ONE }; - let s = Amp { re: inv_sqrt2(), im: 0 }; - let neg_s = Amp { re: -inv_sqrt2(), im: 0 }; + let s = Amp { re: gs.inv_sqrt2(), im: 0 }; + let neg_s = Amp { re: -gs.inv_sqrt2(), im: 0 }; let neg_one = Amp { re: -ONE, im: 0 }; match self { BaseGate::I => [[one, z], [z, one]], @@ -236,9 +441,9 @@ impl BaseGate { BaseGate::H => [[s, s], [s, neg_s]], BaseGate::S => [[one, z], [z, i]], BaseGate::Sdg => [[one, z], [z, neg_i]], - BaseGate::T => [[one, z], [z, phase(3)]], - BaseGate::Tdg => [[one, z], [z, phase(3).conj()]], - BaseGate::P => [[one, z], [z, phase(param as usize)]], + BaseGate::T => [[one, z], [z, gs.phase(3)]], + BaseGate::Tdg => [[one, z], [z, gs.phase(3).conj()]], + BaseGate::P => [[one, z], [z, gs.phase(param as usize)]], } } } @@ -258,31 +463,67 @@ pub struct Gate { // Circuit // --------------------------------------------------------------------------- -/// A quantum circuit: `n_qubits` and an ordered op-log. This is the transportable -/// object — compact, linear in gate count. +/// A quantum circuit: `n_qubits`, an ordered op-log, and the [`Gateset`] it is +/// reconstructed with. This is the transportable object — compact, linear in +/// gate count. +/// +/// The gateset is part of the circuit: two circuits with the same ops and +/// different gatesets are different circuits, and compare unequal. +/// [`Circuit::new`] gives [`Gateset::V2`]; a circuit decoded from a container +/// takes the container's; a circuit derived from another keeps the other's +/// ([`Circuit::empty_like`]). The fields stay public, so a circuit/1 build is +/// `let mut c = Circuit::qft(n); c.gateset = Gateset::V1;`. #[derive(Clone, Debug, PartialEq, Eq)] +#[non_exhaustive] pub struct Circuit { pub n_qubits: u8, pub ops: Vec, + pub gateset: Gateset, } /// Errors from parsing a `WQC` container or building an out-of-range circuit. #[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] pub enum QuantumError { - /// Bad magic, truncated section, or malformed op-log. + /// A container whose structure is broken: a bad magic, a section out of + /// bounds, repeated or missing, an `n_qubits` that is not an integer in + /// `0..=255`, a malformed op-log, or a measurement that is not I-JSON. In + /// `WQC2`, also bytes after the last op or a measurement that is not the + /// canonical bytes. Malformed(String), - /// A qubit index ≥ `n_qubits`, a control equal to the target, or a `P(k)` with - /// `k` outside `1..=DYADIC_MAX`. + /// A circuit its gateset does not allow: a qubit index ≥ `n_qubits`, a + /// control equal to the target, more than 255 controls, more than + /// `u32::MAX` ops, a `P(k)` with `k` outside `1..=DYADIC_MAX`, or, in + /// [`Gateset::V2`], a repeated control or a parameter on a gate other than + /// `P`. Also a circuit too large for a container's `u32` section sizes. Invalid(String), + /// A container's contract section does not declare the capability its + /// magic names: it is not a JSON object carrying that capability's values, + /// or, in `WQC2`, it is not the canonical bytes. + Contract(String), /// `n_qubits` exceeds what this build will materialize (`2^n` amplitudes). TooManyQubits(u8), } +impl QuantumError { + /// The error's class, as the conformance corpus names it: `"malformed"`, + /// `"invalid"`, `"contract"` or `"too-many-qubits"`. + pub fn kind(&self) -> &'static str { + match self { + QuantumError::Malformed(_) => "malformed", + QuantumError::Invalid(_) => "invalid", + QuantumError::Contract(_) => "contract", + QuantumError::TooManyQubits(_) => "too-many-qubits", + } + } +} + impl std::fmt::Display for QuantumError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { QuantumError::Malformed(e) => write!(f, "malformed WQC: {e}"), QuantumError::Invalid(e) => write!(f, "invalid circuit: {e}"), + QuantumError::Contract(e) => write!(f, "contract mismatch: {e}"), QuantumError::TooManyQubits(n) => write!(f, "too many qubits: {n}"), } } @@ -295,8 +536,20 @@ impl std::error::Error for QuantumError {} pub const MAX_QUBITS: u8 = 26; impl Circuit { + /// An empty circuit on `n_qubits` in [`Gateset::V2`], `wai.quantum.circuit2`. pub fn new(n_qubits: u8) -> Self { - Circuit { n_qubits, ops: Vec::new() } + Circuit::with_gateset(n_qubits, Gateset::V2) + } + + /// An empty circuit on `n_qubits` in the gateset `gateset`. + pub fn with_gateset(n_qubits: u8, gateset: Gateset) -> Self { + Circuit { n_qubits, ops: Vec::new(), gateset } + } + + /// An empty circuit with this one's qubit count and gateset, for building a + /// circuit derived from it without resetting its gateset. + pub fn empty_like(&self) -> Self { + Circuit::with_gateset(self.n_qubits, self.gateset) } fn push(&mut self, base: BaseGate, controls: Vec, target: u8, param: u16) -> &mut Self { @@ -328,7 +581,8 @@ impl Circuit { /// The **Quantum Fourier Transform** over all `n` qubits — the "quantum FFT", /// transported as a circuit (Hadamard + controlled-phase ladder + bit-reversal - /// swaps) and reconstructed byte-exact. Uses the pinned dyadic phases. + /// swaps) and reconstructed byte-exact. Built with [`Circuit::new`], so in + /// [`Gateset::V2`], whose table gives every phase the ladder needs. pub fn qft(n: u8) -> Circuit { let mut c = Circuit::new(n); for j in 0..n { @@ -345,13 +599,32 @@ impl Circuit { c } - /// Validate qubit indices, control/target distinctness, and `P(k)` range. + /// Check the circuit against its gateset's rules. + /// + /// In both gatesets every target and control is a qubit of the circuit, no + /// control equals its target, a gate has at most 255 controls (the op-log + /// counts them in a `u8`), and `P(k)` has `1 ≤ k ≤ DYADIC_MAX`. The circuit + /// has at most `u32::MAX` ops, since the op-log counts them in a `u32`. + /// [`Gateset::V2`] also requires a gate's controls to be pairwise distinct, + /// so at most 254, and the parameter of every gate other than `P` to be 0. pub fn validate(&self) -> Result<(), QuantumError> { let n = self.n_qubits; + let strict = match self.gateset { + Gateset::V1 => false, + Gateset::V2 => true, + }; + if u32::try_from(self.ops.len()).is_err() { + return Err(QuantumError::Invalid(format!("{} ops, more than u32::MAX", self.ops.len()))); + } for g in &self.ops { if g.target >= n { return Err(QuantumError::Invalid(format!("target {} ≥ n_qubits {}", g.target, n))); } + if g.controls.len() > usize::from(u8::MAX) { + return Err(QuantumError::Invalid(format!("{} controls, more than 255", g.controls.len()))); + } + // One bit per qubit index, for the distinct-controls rule. + let mut seen = [0u64; 4]; for &c in &g.controls { if c >= n { return Err(QuantumError::Invalid(format!("control {c} ≥ n_qubits {n}"))); @@ -359,19 +632,29 @@ impl Circuit { if c == g.target { return Err(QuantumError::Invalid(format!("control {c} equals target"))); } + if strict { + let (word, bit) = (usize::from(c >> 6), 1u64 << (c & 63)); + if seen[word] & bit != 0 { + return Err(QuantumError::Invalid(format!("control {c} repeated"))); + } + seen[word] |= bit; + } } if g.base == BaseGate::P { let k = g.param as usize; if k < 1 || k > DYADIC_MAX { return Err(QuantumError::Invalid(format!("P(k) with k={k} out of 1..={DYADIC_MAX}"))); } + } else if strict && g.param != 0 { + return Err(QuantumError::Invalid(format!("{:?} with param {}; only P takes one", g.base, g.param))); } } Ok(()) } - /// Classically simulate the circuit from `|0…0⟩`, returning the fixed-point - /// state vector. Pure i64: byte-identical on every machine. + /// Classically simulate the circuit from `|0…0⟩` with its gateset's table, + /// returning the fixed-point state vector. Pure i64: byte-identical on every + /// machine. pub fn simulate(&self) -> Result { self.simulate_from(0) } @@ -386,7 +669,7 @@ impl Circuit { let mut amps = vec![Amp::ZERO; dim]; amps[start % dim] = Amp::ONE; for g in &self.ops { - apply(&mut amps, self.n_qubits, g); + apply(&mut amps, self.n_qubits, g, self.gateset); } Ok(StateVector { n_qubits: self.n_qubits, amps }) } @@ -420,11 +703,15 @@ impl Circuit { /// The loop shape is deliberately left alone. Blocking it, and indexing the pairs /// by bit-insertion, were both tried and both came out slower than the plain scan: /// the arithmetic is what costs here, not the iteration. -fn apply(amps: &mut [Amp], n: u8, g: &Gate) { +/// +/// The shortcuts read only the matrix, so they hold for either gateset's table: +/// circuit2's `P(k ≥ 18)` has `re == ONE` and `im ≠ 0`, which is the diagonal +/// path, and only an exact `(−ONE, 0)` takes the negation. +fn apply(amps: &mut [Amp], n: u8, g: &Gate, gs: Gateset) { if matches!(g.base, BaseGate::I) { return; } - let m = g.base.matrix(g.param); + let m = g.base.matrix(g.param, gs); let tbit = 1usize << g.target; let ctrl_mask: usize = g.controls.iter().fold(0usize, |acc, &c| acc | (1usize << c)); let dim = 1usize << n; @@ -640,7 +927,7 @@ impl StateVector { } // --------------------------------------------------------------------------- -// WQC container (magic "WQC1"): the on-wire circuit object +// WQC container (magic "WQC1" or "WQC2"): the on-wire circuit object // --------------------------------------------------------------------------- /// A pinned measurement request carried in a `WQC` (section `0x03`), so a file can @@ -655,25 +942,22 @@ const SECT_CONTRACT: u8 = 0x01; const SECT_OPLOG: u8 = 0x02; const SECT_MEASURE: u8 = 0x03; -fn contract_json(n_qubits: u8) -> Vec { - // canonical, fixed field order (serde_json preserve_order is on for the crate) - let v = serde_json::json!({ - "ext": "wai.quantum.circuit/1", - "n_qubits": n_qubits, - "numeric": "wai.det.fixed64", - "frac": FRAC, - "gateset": "cliffordT+dyadicP", - }); - serde_json::to_vec(&v).expect("contract json") +/// The canonical measurement bytes, `{"seed":S,"shots":N,"basis":"computational"}`, +/// rendered directly, as the contract is. +fn measure_bytes(m: Measure) -> Vec { + format!("{{\"seed\":{},\"shots\":{},\"basis\":\"computational\"}}", m.seed, m.shots).into_bytes() } +/// The op-log of a circuit that passed `validate`: `u32le n_ops`, then per op +/// `u8 opcode | u8 n_ctrl | u8 target | u16le param | n_ctrl × u8`. `validate` +/// bounds both counts, so neither conversion can fail. fn oplog_bytes(c: &Circuit) -> Vec { - // u32 n_ops || per op: u8 opcode | u8 n_ctrl | u8 target | u16 param | ctrls... + let n_ops = u32::try_from(c.ops.len()).expect("validate bounds the op count by u32::MAX"); let mut out = Vec::new(); - out.extend_from_slice(&(c.ops.len() as u32).to_le_bytes()); + out.extend_from_slice(&n_ops.to_le_bytes()); for g in &c.ops { out.push(g.base.opcode()); - out.push(g.controls.len() as u8); + out.push(u8::try_from(g.controls.len()).expect("validate bounds a gate's controls by 255")); out.push(g.target); out.extend_from_slice(&g.param.to_le_bytes()); out.extend_from_slice(&g.controls); @@ -681,130 +965,644 @@ fn oplog_bytes(c: &Circuit) -> Vec { out } -fn parse_oplog(n_qubits: u8, b: &[u8]) -> Result, QuantumError> { +/// How many ops to reserve for an op-log of `len` bytes that claims `n_ops`. +/// The count is the file's claim, so it never sizes an allocation alone: every +/// op takes at least 5 bytes, so the section holds at most `(len − 4) / 5`. +fn oplog_capacity(n_ops: u32, len: usize) -> usize { + usize::try_from(n_ops).unwrap_or(usize::MAX).min(len.saturating_sub(4) / 5) +} + +/// Read an op-log: the ops, and how many bytes they took. Every op is +/// bounds-checked, and an opcode above 9 is malformed. Bytes after the last op +/// are the caller's to judge. +fn parse_oplog(b: &[u8]) -> Result<(Vec, usize), QuantumError> { if b.len() < 4 { return Err(QuantumError::Malformed("oplog header".into())); } - let n_ops = u32::from_le_bytes([b[0], b[1], b[2], b[3]]) as usize; + let n_ops = u32::from_le_bytes([b[0], b[1], b[2], b[3]]); + let mut ops = Vec::with_capacity(oplog_capacity(n_ops, b.len())); let mut pos = 4; - let mut ops = Vec::with_capacity(n_ops); for _ in 0..n_ops { - if pos + 5 > b.len() { + if b.len() - pos < 5 { return Err(QuantumError::Malformed("op header".into())); } let base = BaseGate::from_opcode(b[pos]) .ok_or_else(|| QuantumError::Malformed(format!("opcode {}", b[pos])))?; - let n_ctrl = b[pos + 1] as usize; + let n_ctrl = usize::from(b[pos + 1]); let target = b[pos + 2]; let param = u16::from_le_bytes([b[pos + 3], b[pos + 4]]); pos += 5; - if pos + n_ctrl > b.len() { + if b.len() - pos < n_ctrl { return Err(QuantumError::Malformed("op controls".into())); } let controls = b[pos..pos + n_ctrl].to_vec(); pos += n_ctrl; ops.push(Gate { base, controls, target, param }); } - let _ = n_qubits; - Ok(ops) + Ok((ops, pos)) } -/// Serialize a circuit (and optional measurement) to a `WQC1` container. -pub fn to_wqc(c: &Circuit, measure: Option) -> Vec { - let contract = contract_json(c.n_qubits); - let oplog = oplog_bytes(c); - let measure_bytes = measure.map(|m| { - let v = serde_json::json!({ "seed": m.seed, "shots": m.shots, "basis": "computational" }); - serde_json::to_vec(&v).expect("measure json") - }); +/// The byte range of a section, `blob_start + off` for `len` bytes, or `None` +/// if it runs past `total` or its arithmetic overflows. Checked, so that it +/// cannot wrap where `usize` is 32 bits. +fn section_range(blob_start: usize, off: u32, len: u32, total: usize) -> Option> { + let start = blob_start.checked_add(usize::try_from(off).ok()?)?; + let end = start.checked_add(usize::try_from(len).ok()?)?; + (end <= total).then_some(start..end) +} - let mut sections: Vec<(u8, Vec)> = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)]; - if let Some(mb) = measure_bytes { - sections.push((SECT_MEASURE, mb)); +/// How a member of a contract or measurement object reads in an error: its +/// JSON, cut at 64 characters, since it comes from an untrusted file. +fn shown(v: Option<&Json>) -> String { + let Some(v) = v else { + return "missing".to_string(); + }; + let s = v.to_string(); + match s.char_indices().nth(64) { + Some((cut, _)) => format!("{}…", &s[..cut]), + None => s, + } +} + +/// Read a contract section under the gateset its container's magic names, and +/// return its `n_qubits`. +/// +/// The section must be an I-JSON object (extensions/quantum-sim §2, read by +/// [`ijson::parse`]), or it is a contract mismatch, and `n_qubits` must be an +/// unsigned JSON integer of at most 255, or the container is malformed. circuit/1 requires `ext`, `numeric`, `frac` (the integer 30) +/// and `gateset` to carry its values, and leaves member order, whitespace and +/// other members free. circuit2 requires the canonical bytes exactly. +fn read_contract(gateset: Gateset, raw: &[u8]) -> Result { + let obj = ijson::parse(raw).map_err(|e| QuantumError::Contract(format!("the contract is not I-JSON: {e}")))?; + if !matches!(obj, Json::Object(_)) { + return Err(QuantumError::Contract(format!("the contract is not a JSON object: {}", shown(Some(&obj))))); + } + let n_qubits = || { + obj.get("n_qubits") + .and_then(Json::as_u64) + .and_then(|n| u8::try_from(n).ok()) + .ok_or_else(|| { + QuantumError::Malformed(format!("n_qubits {} is not an integer in 0..=255", shown(obj.get("n_qubits")))) + }) + }; + match gateset { + Gateset::V1 => { + let text = |key: &str, value: &str| { + if obj.get(key).and_then(Json::as_str) == Some(value) { + Ok(()) + } else { + Err(QuantumError::Contract(format!("{key} is {}, not {value:?}", shown(obj.get(key))))) + } + }; + text("ext", gateset.ext())?; + text("numeric", gateset.numeric())?; + if obj.get("frac").and_then(Json::as_u64) != Some(u64::from(FRAC)) { + return Err(QuantumError::Contract(format!("frac is {}, not {FRAC}", shown(obj.get("frac"))))); + } + text("gateset", gateset.name())?; + n_qubits() + } + Gateset::V2 => { + let n = n_qubits()?; + if raw != gateset.contract_bytes(n).as_slice() { + return Err(QuantumError::Contract(format!( + "not the canonical {} contract for {n} qubits", + gateset.capability() + ))); + } + Ok(n) + } + } +} + +/// Read a measurement section. It must be I-JSON ([`ijson::parse`]). circuit/1 then +/// reads it as it always has: a missing or non-integer `seed` or `shots` reads +/// as 0. +/// circuit2 requires a `u64` `seed` and `shots` and the canonical bytes for +/// them, which fixes the basis as `computational`. +fn read_measure(gateset: Gateset, raw: &[u8]) -> Result { + let mv = ijson::parse(raw).map_err(|e| QuantumError::Malformed(format!("the measurement is not I-JSON: {e}")))?; + let field = |key: &str| mv.get(key).and_then(Json::as_u64); + match gateset { + Gateset::V1 => Ok(Measure { seed: field("seed").unwrap_or(0), shots: field("shots").unwrap_or(0) }), + Gateset::V2 => { + let u64_field = |key: &str| { + field(key).ok_or_else(|| QuantumError::Malformed(format!("measure {key} is {}, not a u64", shown(mv.get(key))))) + }; + let m = Measure { seed: u64_field("seed")?, shots: u64_field("shots")? }; + if raw != measure_bytes(m).as_slice() { + return Err(QuantumError::Malformed("not the canonical measure bytes".into())); + } + Ok(m) + } + } +} + +/// Serialize a circuit, and optionally a measurement, to the container its +/// gateset names: `WQC1` for [`Gateset::V1`], `WQC2` for [`Gateset::V2`]. +/// +/// The layout is the same for both: the magic, `u16le` section count, a table +/// of `(u8 kind, u32le offset, u32le len)` with offsets relative to the end of +/// the table, then the contract (`0x01`), the op-log (`0x02`) and the +/// measurement (`0x03`). A circuit/1 circuit is written byte for byte as every +/// earlier release wrote it. +/// +/// Fails, with the error `validate` gives, on a circuit its gateset does not +/// allow. A circuit that has simulated cannot fail here, unless its op-log runs +/// past what a `u32` section length can address. +pub fn to_wqc(c: &Circuit, measure: Option) -> Result, QuantumError> { + c.validate()?; + let mut sections = vec![(SECT_CONTRACT, c.gateset.contract_bytes(c.n_qubits)), (SECT_OPLOG, oplog_bytes(c))]; + if let Some(m) = measure { + sections.push((SECT_MEASURE, measure_bytes(m))); } let mut out = Vec::new(); - out.extend_from_slice(b"WQC1"); + out.extend_from_slice(&c.gateset.magic()); + // At most three sections. out.extend_from_slice(&(sections.len() as u16).to_le_bytes()); - // section table: u8 kind, u32 off (relative to start of the sections blob), u32 len - let mut off: u32 = 0; + let mut off: u64 = 0; for (kind, data) in §ions { + let (Ok(off32), Ok(len32)) = (u32::try_from(off), u32::try_from(data.len())) else { + return Err(QuantumError::Invalid(format!( + "section {kind:#04x}, {} bytes at offset {off}, does not fit the container's u32 offsets", + data.len() + ))); + }; out.push(*kind); - out.extend_from_slice(&off.to_le_bytes()); - out.extend_from_slice(&(data.len() as u32).to_le_bytes()); - off += data.len() as u32; + out.extend_from_slice(&off32.to_le_bytes()); + out.extend_from_slice(&len32.to_le_bytes()); + off += u64::from(len32); } for (_, data) in §ions { out.extend_from_slice(data); } - out + Ok(out) } -/// Parse a `WQC1` container back into a circuit and optional measurement. +/// Parse a `WQC1` or `WQC2` container into a circuit, in the gateset its magic +/// names, and its optional measurement. +/// +/// The checks run in this order, and the first to fail is the error: +/// +/// 1. The magic is `WQC1` or `WQC2`, else malformed. +/// 2. Every section lies inside the file, with arithmetic that cannot wrap; no +/// known section (`0x01`, `0x02`, `0x03`) appears twice; the contract and +/// op-log are present. Unknown sections are ignored. Else malformed. +/// 3. The contract declares the capability the magic names. In `WQC1` it must +/// be an I-JSON object carrying circuit/1's `ext`, `numeric`, `frac` and +/// `gateset`, in any formatting; in `WQC2` it must be the canonical bytes. +/// Else a contract mismatch, except that an `n_qubits` that is not an +/// unsigned JSON integer in `0..=255` is malformed. +/// 4. The op-log parses within its section: every op lies inside it and has +/// an opcode of at most 9. In `WQC2` it must end exactly after its last op; +/// in `WQC1` any bytes after it are ignored. Else malformed. +/// 5. The circuit passes `validate` under its gateset, else invalid. +/// 6. The measurement, if present, is I-JSON, and in `WQC2` it is the +/// canonical bytes. Else malformed. +/// +/// I-JSON is extensions/quantum-sim §2's reading of a section: UTF-8, no +/// member name twice in one object, no surrogate or noncharacter, no number of +/// magnitude 2^1024 − 2^970 or more, and at most 127 nested arrays and +/// objects, each decided from the section's text. [`reject_corpus`] holds a +/// payload for each refusal, and [`accept_corpus`] payloads at the edges of +/// the rules that must be read. pub fn from_wqc(bytes: &[u8]) -> Result<(Circuit, Option), QuantumError> { - if bytes.len() < 6 || &bytes[0..4] != b"WQC1" { + if bytes.len() < 6 { return Err(QuantumError::Malformed("magic".into())); } - let n_sections = u16::from_le_bytes([bytes[4], bytes[5]]) as usize; + let gateset = Gateset::from_magic(&bytes[0..4]).ok_or_else(|| QuantumError::Malformed("magic".into()))?; + let strict = match gateset { + Gateset::V1 => false, + Gateset::V2 => true, + }; + + let n_sections = usize::from(u16::from_le_bytes([bytes[4], bytes[5]])); let table_start = 6; - let table_len = n_sections * 9; - if bytes.len() < table_start + table_len { - return Err(QuantumError::Malformed("section table".into())); - } - let blob_start = table_start + table_len; + let blob_start = n_sections + .checked_mul(9) + .and_then(|len| len.checked_add(table_start)) + .filter(|&end| end <= bytes.len()) + .ok_or_else(|| QuantumError::Malformed("section table".into()))?; + let u32_at = |p: usize| u32::from_le_bytes([bytes[p], bytes[p + 1], bytes[p + 2], bytes[p + 3]]); let mut contract: Option<&[u8]> = None; let mut oplog: Option<&[u8]> = None; let mut measure_raw: Option<&[u8]> = None; for s in 0..n_sections { + // Inside the table, which ends at blob_start ≤ bytes.len(). let p = table_start + s * 9; let kind = bytes[p]; - let off = u32::from_le_bytes([bytes[p + 1], bytes[p + 2], bytes[p + 3], bytes[p + 4]]) as usize; - let len = u32::from_le_bytes([bytes[p + 5], bytes[p + 6], bytes[p + 7], bytes[p + 8]]) as usize; - let start = blob_start + off; - if start + len > bytes.len() { - return Err(QuantumError::Malformed("section bounds".into())); - } - let data = &bytes[start..start + len]; - match kind { - SECT_CONTRACT => contract = Some(data), - SECT_OPLOG => oplog = Some(data), - SECT_MEASURE => measure_raw = Some(data), - _ => {} // unknown section: ignore (forward-compatible) + let range = section_range(blob_start, u32_at(p + 1), u32_at(p + 5), bytes.len()) + .ok_or_else(|| QuantumError::Malformed(format!("section {kind:#04x} bounds")))?; + let slot = match kind { + SECT_CONTRACT => &mut contract, + SECT_OPLOG => &mut oplog, + SECT_MEASURE => &mut measure_raw, + _ => continue, // unknown section: ignore (forward-compatible) + }; + if slot.replace(&bytes[range]).is_some() { + return Err(QuantumError::Malformed(format!("duplicate section {kind:#04x}"))); } } let contract = contract.ok_or_else(|| QuantumError::Malformed("missing contract".into()))?; let oplog = oplog.ok_or_else(|| QuantumError::Malformed("missing oplog".into()))?; - let cv: serde_json::Value = - serde_json::from_slice(contract).map_err(|e| QuantumError::Malformed(format!("contract json: {e}")))?; - let n_qubits = cv["n_qubits"].as_u64().ok_or_else(|| QuantumError::Malformed("n_qubits".into()))? as u8; + let n_qubits = read_contract(gateset, contract)?; - let ops = parse_oplog(n_qubits, oplog)?; - let circuit = Circuit { n_qubits, ops }; + let (ops, used) = parse_oplog(oplog)?; + if strict && used != oplog.len() { + return Err(QuantumError::Malformed(format!("{} bytes after the last op", oplog.len() - used))); + } + + let circuit = Circuit { n_qubits, ops, gateset }; circuit.validate()?; - let measure = match measure_raw { - Some(mb) => { - let mv: serde_json::Value = - serde_json::from_slice(mb).map_err(|e| QuantumError::Malformed(format!("measure json: {e}")))?; - Some(Measure { - seed: mv["seed"].as_u64().unwrap_or(0), - shots: mv["shots"].as_u64().unwrap_or(0), - }) - } - None => None, - }; + let measure = measure_raw.map(|raw| read_measure(gateset, raw)).transpose()?; Ok((circuit, measure)) } -/// **circuit-equivalence** — the portable BLAKE3 identity of the circuit itself -/// (contract + op-log). The receipt binds this alongside the statevector hash. -pub fn circuit_hash(c: &Circuit) -> [u8; 32] { +/// **circuit-equivalence** — the portable BLAKE3 identity of a circuit: +/// `BLAKE3("wai:quantum-circuit\x01" ‖ contract ‖ op-log)`, over the canonical +/// contract bytes of its gateset and its op-log. The contract names the +/// capability, so the hash binds the gateset and its table: the same ops in +/// circuit/1 and circuit2 are different circuits. The receipt binds this +/// alongside the statevector hash. +/// +/// Fails, as [`to_wqc`] does, on a circuit its gateset does not allow; a +/// circuit that has simulated cannot fail. +pub fn circuit_hash(c: &Circuit) -> Result<[u8; 32], QuantumError> { + c.validate()?; let mut h = blake3::Hasher::new(); h.update(DOMAIN_CIRCUIT); - h.update(&contract_json(c.n_qubits)); + h.update(&c.gateset.contract_bytes(c.n_qubits)); h.update(&oplog_bytes(c)); - *h.finalize().as_bytes() + Ok(*h.finalize().as_bytes()) +} + +// ---- The reject corpus --------------------------------------------------------- + +/// A container from raw section-table entries `(kind, offset, len)` and a blob. +fn wqc_table(magic: &[u8; 4], table: &[(u8, u32, u32)], blob: &[u8]) -> Vec { + let mut out = magic.to_vec(); + out.extend_from_slice(&u16::try_from(table.len()).expect("a short table").to_le_bytes()); + for &(kind, off, len) in table { + out.push(kind); + out.extend_from_slice(&off.to_le_bytes()); + out.extend_from_slice(&len.to_le_bytes()); + } + out.extend_from_slice(blob); + out +} + +/// A container holding `sections` in order, each table entry pointing at its +/// own bytes. +fn wqc_raw(magic: &[u8; 4], sections: &[(u8, &[u8])]) -> Vec { + let (mut table, mut blob) = (Vec::new(), Vec::new()); + for &(kind, data) in sections { + let at = |n: usize| u32::try_from(n).expect("a small section"); + table.push((kind, at(blob.len()), at(data.len()))); + blob.extend_from_slice(data); + } + wqc_table(magic, &table, &blob) +} + +/// One op-log entry, as the wire has it. +fn op(opcode: u8, controls: &[u8], target: u8, param: u16) -> Vec { + let mut out = vec![opcode, u8::try_from(controls.len()).expect("a few controls"), target]; + out.extend_from_slice(¶m.to_le_bytes()); + out.extend_from_slice(controls); + out +} + +/// An op-log holding `ops`. +fn oplog_of(ops: &[Vec]) -> Vec { + let mut out = u32::try_from(ops.len()).expect("a few ops").to_le_bytes().to_vec(); + for o in ops { + out.extend_from_slice(o); + } + out +} + + +/// `contract`, an object, with `members` added at its end. +fn with_members(contract: &[u8], members: &[u8]) -> Vec { + let (body, close) = contract.split_at(contract.len() - 1); + assert_eq!(close, b"}", "a contract is an object"); + [body, b",", members, b"}"].concat() +} + +/// `bytes` with the first `from` replaced by `to`; `from` must be there. +fn swap(bytes: &[u8], from: &str, to: &str) -> Vec { + let s = std::str::from_utf8(bytes).expect("a contract is text"); + assert!(s.contains(from), "{from:?} is not in {s}"); + s.replacen(from, to, 1).into_bytes() +} + +/// The reject corpus of extensions/quantum-sim §2, as `(name, payload, class)`: +/// payloads a reader must refuse, each with the class [`QuantumError::kind`] +/// gives it. Each breaks one rule and keeps every rule checked before it, so +/// the check order of [`from_wqc`] decides the class; most are a two-qubit Bell +/// container with that one thing broken. The conformance corpus's `reject/` +/// tree is written from this list. +pub fn reject_corpus() -> Vec<(&'static str, Vec, &'static str)> { + let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); + let canon1 = Gateset::V1.contract_bytes(2); + let canon2 = Gateset::V2.contract_bytes(2); + let v1 = |contract: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &bell)]); + let v1_oplog = |oplog: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, oplog)]); + let v2 = |contract: &[u8], oplog: &[u8], measure: Option<&[u8]>| { + let mut sections = vec![(SECT_CONTRACT, contract), (SECT_OPLOG, oplog)]; + sections.extend(measure.map(|m| (SECT_MEASURE, m))); + wqc_raw(b"WQC2", §ions) + }; + let v2_contract = |contract: &[u8]| v2(contract, &bell, None); + let v2_oplog = |oplog: &[u8]| v2(&canon2, oplog, None); + let v2_measure = |measure: &[u8]| v2(&canon2, &bell, Some(measure)); + let mut magic_wqc3 = v2_contract(&canon2); + magic_wqc3[0..4].copy_from_slice(b"WQC3"); + // An op-log of 9 bytes that claims u32::MAX ops. + let n_ops_u32max = [u32::MAX.to_le_bytes().as_slice(), &op(4, &[], 0, 0)].concat(); + // An op-log section whose offset plus length wraps a u32. + let wrap = wqc_table( + b"WQC1", + &[(SECT_CONTRACT, 0, u32::try_from(canon1.len()).expect("a short contract")), (SECT_OPLOG, 0xFFFF_FFF0, 0x20)], + &[canon1.as_slice(), &bell].concat(), + ); + // The Bell container cut off inside its section table: two entries claimed, one present. + let mut table_past_end = v1(&canon1); + table_past_end.truncate(6 + 9); + let at = |n: usize| u32::try_from(n).expect("a short section"); + let (c1, b1) = (at(canon1.len()), at(bell.len())); + let both = [canon1.as_slice(), &bell].concat(); + // A contract whose last member's string holds the byte 0xFF. + let not_utf8 = [&canon1[..canon1.len() - 1], b",\"note\":\"\xff\"}"].concat(); + // An op that names two controls where the section holds one. + let truncated_controls = [1u32.to_le_bytes().as_slice(), &[1, 2, 1, 0, 0, 0]].concat(); + let measure = b"{\"seed\":1,\"shots\":10,\"basis\":\"computational\"}"; + // A circuit/1 contract nesting 127 arrays inside its object: 128 levels, + // one past what §2 allows. + let deep = [&canon1[..canon1.len() - 1], b",\"x\":", &b"[".repeat(127), &b"]".repeat(127), b"}"].concat(); + let reordered2 = format!( + r#"{{"n_qubits":2,"ext":"{}","numeric":"{}","frac":30,"gateset":"{}"}}"#, + Gateset::V2.ext(), + Gateset::V2.numeric(), + Gateset::V2.name() + ); + vec![ + ("magic_wqc3", magic_wqc3, "malformed"), + ("v1_ext_other", v1(&swap(&canon1, "circuit/1", "circuit/2")), "contract"), + ("v1_numeric_other", v1(&swap(&canon1, "wai.det.fixed64", "wai.det.amp64")), "contract"), + ("v1_frac_31", v1(&swap(&canon1, "\"frac\":30", "\"frac\":31")), "contract"), + ("v1_frac_float", v1(&swap(&canon1, "\"frac\":30", "\"frac\":30.0")), "contract"), + ("v1_gateset_rn", v1(&swap(&canon1, "dyadicP\"", "dyadicP.rn\"")), "contract"), + ("v1_contract2_in_wqc1", v1(&canon2), "contract"), + ("v1_fields_missing", v1(br#"{"n_qubits":3}"#), "contract"), + ("v1_contract_not_object", v1(&[b"[".as_slice(), &canon1, b"]"].concat()), "contract"), + ("v1_n_qubits_256", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), + ("v1_n_qubits_258", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":258")), "malformed"), + ( + "v1_contract_twice", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_CONTRACT, &Gateset::V1.contract_bytes(3))]), + "malformed", + ), + ( + "v1_oplog_twice", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_OPLOG, &oplog_of(&[op(1, &[], 0, 0)]))]), + "malformed", + ), + // §2.1: the contract is I-JSON. + ("v1_contract_duplicate_member", v1(&swap(&canon1, "\"frac\":30", "\"frac\":30,\"frac\":30")), "contract"), + ("v1_contract_lone_surrogate", v1(&swap(&canon1, "}", ",\"note\":\"\\ud800\"}")), "contract"), + ("v1_contract_too_deep", v1(&deep), "contract"), + ("v1_n_qubits_minus0", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":-0")), "malformed"), + ("v1_contract_lone_low_surrogate", v1(&with_members(&canon1, br#""note":"\udc00""#)), "contract"), + ("v1_contract_noncharacter", v1(&with_members(&canon1, br#""note":"\ufdd0""#)), "contract"), + ("v1_contract_raw_noncharacter", v1(&with_members(&canon1, "\"note\":\"\u{ffff}\"".as_bytes())), "contract"), + ( + "v1_contract_integer_rounds_to_infinity", + v1(&with_members(&canon1, format!("\"x\":{RANGE_LIMIT}").as_bytes())), + "contract", + ), + ("v1_contract_number_rounds_to_infinity", v1(&with_members(&canon1, br#""x":1.7976931348623159e308"#)), "contract"), + // 10^309, written with a run of zeros its exponent offsets. + ( + "v1_contract_long_literal", + v1(&with_members(&canon1, format!("\"x\":0.{}1e655669", "0".repeat(655_359)).as_bytes())), + "contract", + ), + ("v1_contract_bom", v1(&[b"\xef\xbb\xbf".as_slice(), &canon1].concat()), "contract"), + ("v1_contract_nan", v1(&with_members(&canon1, br#""x":NaN"#)), "contract"), + ("v1_contract_control_character", v1(&with_members(&canon1, b"\"note\":\"a\x01b\"")), "contract"), + // §2.3: so is the measurement. + ( + "v1_measure_duplicate_member", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_MEASURE, br#"{"seed":1,"seed":2,"shots":10}"#)]), + "malformed", + ), + ("v1_contract_not_utf8", v1(¬_utf8), "contract"), + ("v1_contract_number_out_of_range", v1(&swap(&canon1, "}", ",\"x\":1e400}")), "contract"), + ("v1_n_qubits_fraction", v1(&swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":2.0")), "malformed"), + ("v1_n_qubits_missing", v1(&swap(&canon1, "\"n_qubits\":2,", "")), "malformed"), + ( + "v1_measure_not_json", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_MEASURE, br#"{"seed":1,"#)]), + "malformed", + ), + // §2: the payload, its table and its sections. + ("short_payload", b"WQC2\x00".to_vec(), "malformed"), + ("table_past_end", table_past_end, "malformed"), + ("section_past_end", wqc_table(b"WQC1", &[(SECT_CONTRACT, 0, c1), (SECT_OPLOG, c1, b1 + 1)], &both), "malformed"), + ( + "unknown_section_past_end", + wqc_table(b"WQC1", &[(SECT_CONTRACT, 0, c1), (SECT_OPLOG, c1, b1), (0x7F, c1 + b1, 1)], &both), + "malformed", + ), + ("no_contract", wqc_raw(b"WQC1", &[(SECT_OPLOG, &bell)]), "malformed"), + ("no_oplog", wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1)]), "malformed"), + // §2.2: the op-log parses, and the circuit keeps its rules. + ("v1_oplog_short", v1_oplog(&[0, 0, 0]), "malformed"), + ("v1_op_truncated_controls", v1_oplog(&truncated_controls), "malformed"), + ("v1_opcode_10", v1_oplog(&oplog_of(&[op(10, &[], 0, 0)])), "malformed"), + ("v1_target_out_of_range", v1_oplog(&oplog_of(&[op(4, &[], 2, 0)])), "invalid"), + ("v1_control_out_of_range", v1_oplog(&oplog_of(&[op(1, &[2], 1, 0)])), "invalid"), + ("v1_control_is_target", v1_oplog(&oplog_of(&[op(1, &[1], 1, 0)])), "invalid"), + ("v1_phase_0", v1_oplog(&oplog_of(&[op(9, &[], 0, 0)])), "invalid"), + ("v1_phase_33", v1_oplog(&oplog_of(&[op(9, &[], 0, 33)])), "invalid"), + ("v1_n_ops_u32max", v1_oplog(&n_ops_u32max), "malformed"), + ("v1_offset_wrap", wrap, "malformed"), + ("v2_contract_whitespace", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\": 2")), "contract"), + ("v2_contract_key_order", v2_contract(reordered2.as_bytes()), "contract"), + ("v2_contract_extra_member", v2_contract(&swap(&canon2, "}", ",\"note\":\"x\"}")), "contract"), + ("v2_contract1_in_wqc2", v2_contract(&canon1), "contract"), + ("v2_n_qubits_256", v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":256")), "malformed"), + ("v2_contract_not_object", v2_contract(&[b"[".as_slice(), &canon2, b"]"].concat()), "contract"), + // The I-JSON rules come before n_qubits: out of range is a contract + // refusal, in range but not an integer a malformed one. + ( + "v2_n_qubits_rounds_to_infinity", + v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":1.7976931348623159e308")), + "contract", + ), + ( + "v2_n_qubits_largest_finite", + v2_contract(&swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":1.7976931348623158e308")), + "malformed", + ), + ("v2_phase_0", v2_oplog(&oplog_of(&[op(9, &[], 0, 0)])), "invalid"), + ( + "v2_measure_twice", + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &canon2), (SECT_OPLOG, &bell), (SECT_MEASURE, measure), (SECT_MEASURE, measure)]), + "malformed", + ), + ("v2_measure_seed_signed", v2_measure(br#"{"seed":-1,"shots":10,"basis":"computational"}"#), "malformed"), + ( + "v2_duplicate_controls", + v2(&Gateset::V2.contract_bytes(3), &oplog_of(&[op(1, &[0, 0], 2, 0)]), None), + "invalid", + ), + ("v2_param_on_h", v2_oplog(&oplog_of(&[op(4, &[], 0, 7), op(1, &[0], 1, 0)])), "invalid"), + ("v2_oplog_trailing_byte", v2_oplog(&[bell.as_slice(), &[0]].concat()), "malformed"), + ("v2_opcode_10", v2_oplog(&oplog_of(&[op(10, &[], 0, 0)])), "malformed"), + ("v2_measure_spaced", v2_measure(br#"{"seed": 1, "shots": 10, "basis": "computational"}"#), "malformed"), + ("v2_measure_no_shots", v2_measure(br#"{"seed":1,"basis":"computational"}"#), "malformed"), + ("v2_measure_basis_x", v2_measure(br#"{"seed":1,"shots":10,"basis":"x"}"#), "malformed"), + ] +} + +/// The accept corpus of extensions/quantum-sim §2, as `(name, payload, circuit, +/// measurement)`: payloads a reader must accept, each with the circuit and the +/// measurement it reads as. They sit at the edges the reject corpus does not +/// reach from the other side: circuit/1's free formatting and further members, +/// every I-JSON rule at its limit, circuit/1's lax measurement, op-log bytes +/// after the last op, unknown sections, and the widest `n_qubits` and `u64` +/// values. The conformance corpus's `accept/` tree is written from this list. +pub fn accept_corpus() -> Vec<(&'static str, Vec, Circuit, Option)> { + let bell_ops = |gateset| Circuit { + n_qubits: 2, + ops: vec![ + Gate { base: BaseGate::H, controls: vec![], target: 0, param: 0 }, + Gate { base: BaseGate::X, controls: vec![0], target: 1, param: 0 }, + ], + gateset, + }; + let (bell1, bell2) = (bell_ops(Gateset::V1), bell_ops(Gateset::V2)); + let wide = |c: &Circuit| Circuit { n_qubits: 255, ..c.clone() }; + let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); + let canon1 = Gateset::V1.contract_bytes(2); + let canon2 = Gateset::V2.contract_bytes(2); + let v1 = |contract: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, contract), (SECT_OPLOG, &bell)]); + let v1_measure = |measure: &[u8]| wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_MEASURE, measure)]); + let m = |seed, shots| Some(Measure { seed, shots }); + let reordered = format!( + r#"{{"gateset":"{}","frac":30,"numeric":"{}","n_qubits":2,"ext":"{}"}}"#, + Gateset::V1.name(), + Gateset::V1.numeric(), + Gateset::V1.ext() + ); + let spaced = String::from_utf8(canon1.clone()).expect("a contract is text").replace(':', " : ").replace(',', " ,\n\t"); + let spaced = format!(" \r\n{spaced}\r\n "); + let depth = format!(r#""x":{}{}"#, "[".repeat(126), "]".repeat(126)); + let largest_integer = format!("{}1", &RANGE_LIMIT[..RANGE_LIMIT.len() - 1]); + let unknown = |magic: &[u8; 4], contract: &[u8]| { + wqc_raw(magic, &[(0x7F, b"later"), (SECT_CONTRACT, contract), (SECT_OPLOG, &bell), (0x04, b"")]) + }; + let mut param_on_h = bell1.clone(); + param_on_h.ops[0].param = 7; + let mut repeated_control = bell1.clone(); + repeated_control.ops[1].controls = vec![0, 0]; + vec![ + ("v1_members_reordered", v1(reordered.as_bytes()), bell1.clone(), None), + ("v1_whitespace", v1(spaced.as_bytes()), bell1.clone(), None), + ( + "v1_further_members", + v1(&with_members(&canon1, br#""note":"x","list":[0,-1,2.5,-3e-2,1E+2,true,false,null,{},[]],"obj":{"a":{"b":[]}},"again":{"note":1}"#)), + bell1.clone(), + None, + ), + ( + "v1_escapes_and_characters", + v1(&with_members(&canon1, "\"note\":\"\\\"\\\\\\/\\b\\f\\n\\r\\t\\u00e9\\ud83d\\ude00\\udbff\\udffd\u{e9}\u{fdcf}\u{fdf0}\u{fffd}\u{10fffd}\"".as_bytes())), + bell1.clone(), + None, + ), + ("v1_depth_127", v1(&with_members(&canon1, depth.as_bytes())), bell1.clone(), None), + ( + "v1_numbers_largest_finite", + v1(&with_members(&canon1, format!(r#""x":1.7976931348623158e308,"y":-1.7976931348623157e308,"z":{largest_integer},"w":-{largest_integer}"#).as_bytes())), + bell1.clone(), + None, + ), + // Exactly 1 and exactly 10, written long: a run of zeros the exponent + // offsets, and an exponent with leading zeros. + ( + "v1_numbers_long_literals", + v1(&with_members(&canon1, format!("\"x\":1{}e-655360,\"y\":1e{}1", "0".repeat(655_360), "0".repeat(4300)).as_bytes())), + bell1.clone(), + None, + ), + ( + "v1_numbers_toward_zero", + v1(&with_members(&canon1, br#""x":1e-400,"y":-0,"z":0e99999999999999999999,"w":-0.0E-0"#)), + bell1.clone(), + None, + ), + ("v1_n_qubits_255", v1(&Gateset::V1.contract_bytes(255)), wide(&bell1), None), + ("v1_measure_lax", v1_measure(br#" { "basis" : "x" , "shots" : 10 , "seed" : 1 } "#), bell1.clone(), m(1, 10)), + ("v1_measure_missing_shots", v1_measure(br#"{"seed":1,"basis":"computational"}"#), bell1.clone(), m(1, 0)), + ( + "v1_measure_u64_max", + v1_measure(br#"{"seed":18446744073709551615,"shots":18446744073709551615}"#), + bell1.clone(), + m(u64::MAX, u64::MAX), + ), + ( + "v1_measure_not_u64", + v1_measure(br#"{"seed":18446744073709551616,"shots":-0,"x":2.5}"#), + bell1.clone(), + m(0, 0), + ), + ("v1_measure_other_types", v1_measure(br#"{"seed":"1","shots":1e1,"basis":null}"#), bell1.clone(), m(0, 0)), + ("v1_measure_not_object", v1_measure(b"[1,2]"), bell1.clone(), m(0, 0)), + ( + "v1_oplog_trailing_bytes", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &[bell.as_slice(), &[0, 1, 2]].concat())]), + bell1.clone(), + None, + ), + ( + "v1_param_on_h", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &oplog_of(&[op(4, &[], 0, 7), op(1, &[0], 1, 0)]))]), + param_on_h, + None, + ), + ( + "v1_repeated_control", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &oplog_of(&[op(4, &[], 0, 0), op(1, &[0, 0], 1, 0)]))]), + repeated_control, + None, + ), + ("v1_unknown_sections", unknown(b"WQC1", &canon1), bell1.clone(), None), + ("v2_unknown_sections", unknown(b"WQC2", &canon2), bell2.clone(), None), + ( + "v2_n_qubits_255", + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &Gateset::V2.contract_bytes(255)), (SECT_OPLOG, &bell)]), + wide(&bell2), + None, + ), + ( + "v2_measure_u64_max", + wqc_raw( + b"WQC2", + &[ + (SECT_CONTRACT, &canon2), + (SECT_OPLOG, &bell), + (SECT_MEASURE, br#"{"seed":18446744073709551615,"shots":18446744073709551615,"basis":"computational"}"#), + ], + ), + bell2, + m(u64::MAX, u64::MAX), + ), + ] } #[cfg(test)] @@ -814,8 +1612,8 @@ mod tests { /// are justified by an argument (`fxmul(ONE,x) == x`, `fxmul(0,x) == 0`), and /// an argument about fixed-point rounding is exactly the kind that is /// convincing and wrong, so it is checked against the code it replaced. - fn apply_reference(amps: &mut [Amp], n: u8, g: &Gate) { - let m = g.base.matrix(g.param); + fn apply_reference(amps: &mut [Amp], n: u8, g: &Gate, gs: Gateset) { + let m = g.base.matrix(g.param, gs); let tbit = 1usize << g.target; let ctrl_mask: usize = g.controls.iter().fold(0usize, |acc, &c| acc | (1usize << c)); let dim = 1usize << n; @@ -868,14 +1666,14 @@ mod tests { let mut a = vec![Amp::ZERO; dim]; a[0] = Amp::ONE; let t0 = Instant::now(); - for g in &ops { apply_reference(&mut a, n, g); } + for g in &ops { apply_reference(&mut a, n, g, Gateset::V2); } best_ref = best_ref.min(t0.elapsed().as_nanos()); std::hint::black_box(&a); let mut b = vec![Amp::ZERO; dim]; b[0] = Amp::ONE; let t1 = Instant::now(); - for g in &ops { apply(&mut b, n, g); } + for g in &ops { apply(&mut b, n, g, Gateset::V2); } best_opt = best_opt.min(t1.elapsed().as_nanos()); std::hint::black_box(&b); } @@ -887,7 +1685,9 @@ mod tests { /// Every amplitude, not just the hash: random circuits over the whole gate set /// must come out of the optimised path bit-for-bit as they came out of the old /// one. A faster simulator that moved one low bit would have broken every - /// receipt this crate has ever signed. + /// receipt this crate has ever signed. Both tables, and every `P(k)`: in + /// circuit2, `P(k ≥ 18)` has `re == ONE` with `im ≠ 0`, which must take the + /// diagonal path and nothing else. #[test] fn optimised_apply_is_bit_identical_to_the_general_path() { let mut st = 0x1234_5678_9ABC_DEF0u64; @@ -895,10 +1695,10 @@ mod tests { st = st.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407); ((st >> 33) as usize) % m.max(1) }; - for _ in 0..300 { + for (case, gs) in Gateset::ALL.into_iter().flat_map(|gs| std::iter::repeat_n(gs, 300)).enumerate() { let n = 3 + rnd(3) as u8; let dim = 1usize << n; - let mut c = Circuit::new(n); + let mut c = Circuit::with_gateset(n, gs); for _ in 0..24 { let t = rnd(n as usize) as u8; let mut other = rnd(n as usize) as u8; @@ -912,10 +1712,10 @@ mod tests { 3 => { c.h(t); } 4 => { c.s(t); } 5 => { c.t(t); } - 6 => { c.p(1 + rnd(5) as u16, t); } + 6 => { c.p(1 + rnd(DYADIC_MAX) as u16, t); } 7 => { c.cx(other, t); } 8 => { c.cz(other, t); } - 9 => { c.cp(1 + rnd(4) as u16, other, t); } + 9 => { c.cp(1 + rnd(DYADIC_MAX) as u16, other, t); } 10 => c.ops.push(Gate { base: BaseGate::Sdg, controls: vec![], target: t, param: 0 }), 11 => c.ops.push(Gate { base: BaseGate::Tdg, controls: vec![], target: t, param: 0 }), _ => c.ops.push(Gate { base: BaseGate::I, controls: vec![], target: t, param: 0 }), @@ -931,10 +1731,10 @@ mod tests { let mut want = vec![Amp::ZERO; dim]; want[0] = Amp::ONE; for g in &c.ops { - apply_reference(&mut want, n, g); + apply_reference(&mut want, n, g, gs); } let got = c.simulate().unwrap(); - assert_eq!(got.amps, want, "optimised apply diverged on {} gates, n={n}", c.ops.len()); + assert_eq!(got.amps, want, "case {case}: optimised apply diverged on {} gates, n={n}, {gs:?}", c.ops.len()); } } @@ -947,19 +1747,233 @@ mod tests { (a - b).abs() <= tol } - #[test] - fn dyadic_table_is_unit_modulus() { - // cos^2 + sin^2 ≈ 1 for every tabulated phase, and P(1)=Z, P(2)=S, P(3)=T. - for k in 1..=DYADIC_MAX { - let a = phase(k); - let n2 = ((a.norm2()) >> FRAC) as i64; // at 2^FRAC - assert!(approx(n2, ONE, ONE / 1000), "|e^(2πi/2^{k})|^2 = {n2} not ≈ ONE"); + fn hx(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() + } + + /// The conformance corpus's `dyadic_ladder`: X(0); then Y, Z, S, S†, T, T† + /// and I on qubit 1; then CP(k; control 0, target 1) for k = 1..=32, twice. + /// Every table entry multiplies a nonzero amplitude, so a change of one unit + /// in any entry moves the statevector hash. + fn dyadic_ladder(gs: Gateset) -> Circuit { + let mut c = Circuit::with_gateset(2, gs); + c.x(0).y(1).z(1).s(1); + c.push(BaseGate::Sdg, vec![], 1, 0); + c.t(1); + c.push(BaseGate::Tdg, vec![], 1, 0); + c.push(BaseGate::I, vec![], 1, 0); + for _ in 0..2 { + for k in 1..=DYADIC_MAX as u16 { + c.cp(k, 0, 1); + } } - assert_eq!(phase(1), Amp { re: -ONE, im: 0 }); // e^{iπ} - assert_eq!(phase(2), Amp { re: 0, im: ONE }); // e^{iπ/2} - // T = e^{iπ/4}: re == im == 1/√2 - assert_eq!(phase(3).re, inv_sqrt2()); - assert_eq!(phase(3).im, inv_sqrt2()); + c + } + + /// circuit/1's table is the recurrence its artefacts were made with, and + /// stays that way. The defect is pinned on purpose, so that nobody repairs + /// circuit/1 in place: a repaired table would stop every circuit/1 file and + /// receipt from reproducing. The repair is circuit2. + #[test] + fn phase_table_v1_is_frozen() { + // The recurrence: truncating halving, floor square root. Both halves are + // non-negative, so truncating division is floor division. + let mut t = [Amp::ZERO; DYADIC_MAX + 1]; + t[1] = Amp { re: -ONE, im: 0 }; + t[2] = Amp { re: 0, im: ONE }; + for k in 3..=DYADIC_MAX { + let c = t[k - 1].re; + t[k] = Amp { re: sqrt_fx((ONE + c) / 2), im: sqrt_fx((ONE - c) / 2) }; + } + assert_eq!(t, PHASE_V1, "PHASE_V1 is not the circuit/1 recurrence"); + assert_eq!( + hx(&Gateset::V1.table_digest()), + "3fa9805ff96feb819a1cf371fedcd3b5228f93eddf933d0edcf401cbf8c28466" + ); + // From k = 19 on, circuit/1 applies no phase at all. + for k in 19..=DYADIC_MAX { + assert_eq!(PHASE_V1[k], Amp { re: ONE - 1, im: 0 }, "PHASE_V1[{k}]"); + } + assert_eq!(Gateset::V1.inv_sqrt2(), 759_250_124); + // End to end, so the unit suite alone catches circuit/1 drift: the ladder + // reproduces its circuit/1 corpus hash. + let sv = dyadic_ladder(Gateset::V1).simulate().unwrap(); + assert_eq!( + hx(&sv.statevector_hash()), + "9754af24edd026601297a0f3334eb7171174978841a2938c9b372db942a3851d" + ); + } + + /// Every circuit2 entry is the nearest integer, proved with integers alone. + /// + /// Enclose `2^62·cos(2π/2^k)` and `2^62·sin(2π/2^k)` between integer bounds, + /// starting exactly at `k = 2`, and halve the angle with rounding chosen to + /// keep each bound on its side: `cos(θ/2) = √((1 + cos θ)/2)` and + /// `sin(θ/2) = sin θ / (2 cos(θ/2))`, a quotient, so nothing cancels. If both + /// ends of an enclosure round to the same integer at `2^-30`, the true value + /// does too, and that integer is within ½ unit of it. Every endpoint is also + /// required to sit more than `2^20` ulps from a tie. + #[test] + fn phase_table_v2_is_certified_nearest() { + const S: u128 = 1 << 62; + let mul = |a: u128, b: u128| a.checked_mul(b).expect("the enclosure overflowed u128"); + let ceil_isqrt = |x: u128| { + let r = x.isqrt(); + if r * r == x { r } else { r + 1 } + }; + // Round half up from 2^-62 to 2^-30. + let r = |x: u128| ((x + (1 << 31)) >> 32) as i64; + let far_from_tie = |x: u128| ((x & 0xFFFF_FFFF) as i128 - (1i128 << 31)).abs() > 1 << 20; + + // (cos, sin)(π/2) = (0, 1), exactly. + let (mut c_lo, mut c_hi, mut s_lo, mut s_hi) = (0u128, 0u128, S, S); + for k in 3..=DYADIC_MAX { + let c_lo2 = (mul(S + c_lo, S) >> 1).isqrt(); + let c_hi2 = ceil_isqrt(mul(S + c_hi, S).div_ceil(2)); + let s_lo2 = mul(s_lo, S) / (2 * c_hi2); + let s_hi2 = mul(s_hi, S).div_ceil(2 * c_lo2); + assert!(c_lo2 <= c_hi2 && s_lo2 <= s_hi2, "P({k}): an empty enclosure"); + let want = PHASE_V2[k]; + assert_eq!((r(c_lo2), r(c_hi2)), (want.re, want.re), "re P({k}) is not certified"); + assert_eq!((r(s_lo2), r(s_hi2)), (want.im, want.im), "im P({k}) is not certified"); + for x in [c_lo2, c_hi2, s_lo2, s_hi2] { + assert!(far_from_tie(x), "P({k}): an endpoint lies within 2^20 ulps of a tie"); + } + (c_lo, c_hi, s_lo, s_hi) = (c_lo2, c_hi2, s_lo2, s_hi2); + } + assert_eq!(PHASE_V2[0], Amp::ZERO); + assert_eq!(PHASE_V2[1], Amp { re: -ONE, im: 0 }); + assert_eq!(PHASE_V2[2], Amp { re: 0, im: ONE }); + assert_eq!( + hx(&Gateset::V2.table_digest()), + "d34cb793715348e7c92db027c0bc3dddb6008f5730470bafaed978ea7fb23510" + ); + assert_eq!(Gateset::V2.inv_sqrt2(), 759_250_125); + } + + /// Every circuit2 component is within one unit of the ideal, against an + /// independent floating-point reference, and the same check fails on + /// circuit/1, so it can tell a good table from a bad one. + /// + /// The reference is the crate's own `sin_cos`, which gives the same bits on + /// every IEEE platform and is within an ulp; it is compiled under these + /// features, all of which `full` enables. + #[cfg(any( + feature = "quantum_spd", + feature = "quantum_pauli", + feature = "quantum_mps", + feature = "quantum_vml", + feature = "quantum_phasor", + feature = "quantum_kernel", + feature = "quantum_tn", + feature = "quantum_sv", + feature = "quantum_tdvp", + feature = "quantum_nqs" + ))] + #[test] + fn every_v2_phase_is_within_one_unit_of_ideal() { + // ½ unit, plus slack for the reference's own error (about 1e-7 unit). + const BOUND: f64 = 0.5 + 1e-6; + // The largest deviation of each entry from 2^30·(cos, sin)(2π/2^k). + let deviation = |t: &[Amp; 33]| -> Vec<(usize, f64)> { + (1..=DYADIC_MAX) + .map(|k| { + let (s, c) = crate::repro::sin_cos(std::f64::consts::TAU / (1u64 << k) as f64); + let one = ONE as f64; + let dre = (t[k].re as f64 - c * one).abs(); + let dim = (t[k].im as f64 - s * one).abs(); + (k, dre.max(dim)) + }) + .collect() + }; + let mut worst = 0.0f64; + for (k, d) in deviation(&PHASE_V2) { + assert!(d <= BOUND, "PHASE_V2[{k}] is {d} units from the ideal"); + worst = worst.max(d); + } + assert!(worst < 1.0); + // Negative control: circuit/1 fails the same check from k = 8 on (its + // P(8) imaginary part is 9.52 units high). + let v1_bad: Vec = + deviation(&PHASE_V1).into_iter().filter(|&(_, d)| d > BOUND).map(|(k, _)| k).collect(); + assert!(v1_bad.contains(&8), "the check passed circuit/1's P(8): {v1_bad:?}"); + assert!((8..=DYADIC_MAX).all(|k| v1_bad.contains(&k)), "circuit/1 failures: {v1_bad:?}"); + } + + /// The identities each table must keep: `P(1) = −1` and `P(2) = i` exactly, + /// `T = e^{iπ/4}` with `re == im == 1/√2`, and `|P(k)|² = 1` to within one + /// unit in circuit2 and three in circuit/1. + #[test] + fn table_identities_per_gateset() { + for gs in Gateset::ALL { + assert_eq!(gs.phase(1), Amp { re: -ONE, im: 0 }, "{gs:?} P(1)"); + assert_eq!(gs.phase(2), Amp { re: 0, im: ONE }, "{gs:?} P(2)"); + assert_eq!(gs.phase(3).re, gs.phase(3).im, "{gs:?} T"); + assert_eq!(gs.phase(3).re, gs.inv_sqrt2(), "{gs:?} 1/√2"); + assert_eq!(gs.table()[0], Amp::ZERO, "{gs:?} index 0"); + let units: i128 = match gs { + Gateset::V1 => 3, + Gateset::V2 => 1, + }; + for k in 1..=DYADIC_MAX { + // |P|² − 1 at scale 2^60, exactly. + let d = gs.phase(k).norm2() - (ONE as i128) * (ONE as i128); + assert!( + d.abs() <= units << FRAC, + "{gs:?}: |P({k})|² − 1 = {} units", + d as f64 / ONE as f64 + ); + } + } + } + + /// The identifiers each gateset names map back to it, and its contract bytes + /// are the canonical template. circuit/1's must equal the serializer render + /// every existing circuit hash was taken over (`serde_contract_v1`). + #[test] + fn gateset_identifiers_are_consistent() { + assert_eq!(Gateset::default(), Gateset::V2); + for gs in Gateset::ALL { + assert_eq!(Gateset::from_magic(&gs.magic()), Some(gs)); + assert_eq!(Gateset::from_capability(gs.capability()), Some(gs)); + assert_eq!(gs.ext(), format!("{}/1", gs.capability())); + } + assert_eq!(Gateset::from_magic(b"WQC3"), None); + assert_eq!(Gateset::from_magic(b"WQC"), None); + assert_eq!(Gateset::from_magic(b"WQC1\0"), None); + assert_eq!(Gateset::from_capability("wai.quantum.circuit/1"), None); + assert_eq!(Gateset::from_capability("wai.quantum.circuit3"), None); + assert_eq!( + Gateset::V1.contract_bytes(5), + br#"{"ext":"wai.quantum.circuit/1","n_qubits":5,"numeric":"wai.det.fixed64","frac":30,"gateset":"cliffordT+dyadicP"}"# + ); + assert_eq!( + Gateset::V2.contract_bytes(5), + br#"{"ext":"wai.quantum.circuit2/1","n_qubits":5,"numeric":"wai.det.amp64","frac":30,"gateset":"cliffordT+dyadicP.rn"}"# + ); + assert_eq!((Gateset::V1.contract_bytes(5).len(), Gateset::V2.contract_bytes(5).len()), (112, 114)); + for n in 0..=u8::MAX { + assert_eq!(Gateset::V1.contract_bytes(n), serde_contract_v1(n), "n_qubits {n}"); + } + } + + /// circuit/1's contract as 0.3.x rendered it, through the serializer with + /// member order preserved. Every circuit/1 hash and file was taken over + /// these bytes. + fn serde_contract_v1(n_qubits: u8) -> Vec { + serde_json::to_vec(&serde_json::json!({ + "ext": "wai.quantum.circuit/1", + "n_qubits": n_qubits, + "numeric": "wai.det.fixed64", + "frac": FRAC, + "gateset": "cliffordT+dyadicP", + })) + .unwrap() + } + + /// The measurement section as 0.3.x rendered it, through the serializer. + fn serde_measure(m: Measure) -> Vec { + serde_json::to_vec(&serde_json::json!({ "seed": m.seed, "shots": m.shots, "basis": "computational" })).unwrap() } #[test] @@ -978,7 +1992,7 @@ mod tests { let mut c = Circuit::new(2); c.h(0).cx(0, 1); let sv = c.simulate().unwrap(); - let s = inv_sqrt2(); + let s = c.gateset.inv_sqrt2(); assert!(approx(sv.amps[0b00].re, s, TOL)); assert_eq!(sv.amps[0b01], Amp::ZERO); assert_eq!(sv.amps[0b10], Amp::ZERO); @@ -993,7 +2007,7 @@ mod tests { let mut c = Circuit::new(3); c.h(0).cx(0, 1).cx(1, 2); let sv = c.simulate().unwrap(); - let s = inv_sqrt2(); + let s = c.gateset.inv_sqrt2(); assert!(approx(sv.amps[0b000].re, s, TOL)); assert!(approx(sv.amps[0b111].re, s, TOL)); for k in 1..7 { @@ -1040,18 +2054,318 @@ mod tests { assert_eq!(counts[0b00] + counts[0b11], 10_000); } + /// The four circuits of the circuit/1 corpus, built in `gs`: bell, with + /// the corpus measurement, then ghz3, qft3 and qft5. + fn corpus_circuits(gs: Gateset) -> [(&'static str, Circuit, Option); 4] { + let mut bell = Circuit::with_gateset(2, gs); + bell.h(0).cx(0, 1); + let mut ghz3 = Circuit::with_gateset(3, gs); + ghz3.h(0).cx(0, 1).cx(1, 2); + let qft = |n| { + let mut c = Circuit::qft(n); + c.gateset = gs; + c + }; + [ + ("bell", bell, Some(Measure { seed: 12_648_430, shots: 100_000 })), + ("ghz3", ghz3, None), + ("qft3", qft(3), None), + ("qft5", qft(5), None), + ] + } + + /// A circuit/1 container is written byte for byte as 0.3.x wrote it: the + /// four corpus files reproduce, and so do their manifest circuit hashes. + /// The measurement, now rendered directly, matches the serializer's render. + #[test] + fn circuit1_files_are_written_as_before() { + // (file, bytes, BLAKE3 of the file, manifest circuit_hash) + let want = [ + ("bell", 216, "97b294a85afe628060a51f12e7f9c2104ebc0b7d04ef36ff15b154ed1c54728e", + "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e"), + ("ghz3", 157, "8f2d924416a19bc96407f36669ca3f55bcd6a57b3256bd16a669dc96bd3fb490", + "34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c"), + ("qft3", 191, "e1a7e49e2804130d802d8e927f63d6633b966c8e6b7cd3eb7278e01fd1f884c5", + "7e8414be4410557a05b52bcc1084e1c5f93b6caa0f28fa45f4d2ab661efd2a60"), + ("qft5", 261, "92b4e1497f52109922a0f2ea7ec8de2b1d9e92f155dafbf6e5f69eca4427878c", + "f36a9af99a251cae6f42144b2be3c57523f15e1e2ab3492cf15cac709fc048be"), + ]; + for ((name, c, m), (file, len, digest, hash)) in corpus_circuits(Gateset::V1).into_iter().zip(want) { + assert_eq!(name, file); + let bytes = to_wqc(&c, m).unwrap(); + assert_eq!(bytes.len(), len, "{name}.wqc"); + assert_eq!(hx(blake3::hash(&bytes).as_bytes()), digest, "{name}.wqc"); + assert_eq!(hx(&circuit_hash(&c).unwrap()), hash, "{name}"); + } + let values = [0, 1, 42, 1024, 100_000, 12_648_430, u64::from(u32::MAX), u64::MAX]; + for seed in values { + for shots in values { + let m = Measure { seed, shots }; + assert_eq!(measure_bytes(m), serde_measure(m), "{m:?}"); + } + } + } + + /// Every hash of both conformance corpora, reproduced from code, so the + /// unit suite alone catches a drift in either gateset. The circuit/1 rows + /// for bell, ghz3, qft3 and qft5 are the manifest of the files 0.3.x + /// wrote, nine hashes in all; the other rows were computed by an + /// integer-only implementation that shares no code with this crate. None + /// of these values may be edited to make this test pass. + #[test] + fn the_conformance_corpora_are_pinned() { + // (file, bytes, n_ops, circuit_hash, statevector_hash) + type Row = (&'static str, usize, usize, &'static str, &'static str); + let v1: [Row; 7] = [ + ("bell", 216, 2, "894d9a9582b19f27b8d08d06938357237fc00f7f30bf4bfc2be0c04fa5e47a4e", + "bd5df69662f4e61878d17baee1f3cf5705c3726d6740d1a4bf7be5133c099efb"), + ("ghz3", 157, 3, "34e44f7d68037cbb3aee677f56b894dc48df6d408aa068a16d3b18076e5e7c5c", + "08c5e6fb50b91a00d28795506cc5d99ce1e9a3a8b4c41e4bfd05f5d829af9542"), + ("qft3", 191, 9, "7e8414be4410557a05b52bcc1084e1c5f93b6caa0f28fa45f4d2ab661efd2a60", + "0fe1e5b8e2ac8e9afbfa926b3877967ebfead92efb7ed8e1ff6b117668926d6e"), + ("qft5", 261, 21, "f36a9af99a251cae6f42144b2be3c57523f15e1e2ab3492cf15cac709fc048be", + "c268d77f2ddf84fecb028e80ce989123912870f04e5b9a1e3d885c9ce235c27d"), + ("qft5_x22", 276, 24, "40c847fe9f85b6d79d245b6e65420197e6bbedb9c0b1ab77baaf1cfe077ff4f5", + "95a2d2fd41f6a13c19562729a4b553ee12db6adfaa361b5061c16d801354d0cd"), + ("dyadic_ladder", 564, 72, "2fa5b02c952dcaf55ab4cfd2ea5b3fae999ac912a32a4e06dd2b6ee96bb58d4f", + "9754af24edd026601297a0f3334eb7171174978841a2938c9b372db942a3851d"), + ("qft8_ones", 460, 56, "5ca94d3a960b8ec65a1eb802699f1ed8e20f00e7fbd9297849f0c60047404a1a", + "fd683498744552e9e2e8657a2bdc244e4f03d4ee4dd292c9705ba38b19fb709c"), + ]; + let v2: [Row; 7] = [ + ("bell", 218, 2, "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b", + "db68a50b08a8c2eabc89a881db9fcb130fff89dc36182d4c7f90da5bf4720b15"), + ("ghz3", 159, 3, "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f", + "51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8"), + ("qft3", 193, 9, "c23a1b1e31b87705999cbc54866f4c9e3e4c4d2b274d6260672121abb5ca89b8", + "70fe25ec22008f86076328b2c0908ede778e6d8227e563e8c7dcd89f7e844bf2"), + ("qft5", 263, 21, "8559c83769ac8c0cb3aabc64672233aa569cd4ce92aa6545f1449982b9aa8b8c", + "50b01fab0bfee966e553a947aea6aa09cebf8f21ba201cd548d703048d66f2ba"), + ("qft5_x22", 278, 24, "c126604030b16f4f1e88e6683894f9316262f49b3419be06ab74c7dbd2441651", + "5def2fca1d29e9e2b9ca2020cd2fa2dc056d07ce2e21b6e101b6116a03efd61c"), + ("dyadic_ladder", 566, 72, "8a6dcf0d41ecefd1a9761fef49b385d7dffe7543a9382efda02bfe4f7a0896fe", + "28bdb7265c10d798012ac78b604327700acd8cdac0e1f88ed4045f8b0b78bc7c"), + ("qft8_ones", 462, 56, "286f044750b19937f6e7bf6f534053aa74a21777516c773dbd7c254df7301b34", + "e059d43a574780d819a872f7904bf96918ba5880e1c7c32b38159306acd559fe"), + ]; + for (gs, rows) in [(Gateset::V1, v1), (Gateset::V2, v2)] { + // The four originals, then the three table-sensitive vectors. + let x_then_qft = |n: u8, xs: &[u8]| { + let mut c = Circuit::with_gateset(n, gs); + for &q in xs { + c.x(q); + } + c.ops.extend(Circuit::qft(n).ops); + c + }; + let mut vectors: Vec<(&str, Circuit, Option)> = corpus_circuits(gs).into(); + vectors.push(("qft5_x22", x_then_qft(5, &[1, 2, 4]), None)); + vectors.push(("dyadic_ladder", dyadic_ladder(gs), None)); + vectors.push(("qft8_ones", x_then_qft(8, &[0, 1, 2, 3, 4, 5, 6, 7]), None)); + for ((name, c, m), (file, len, n_ops, circuit, statevector)) in vectors.into_iter().zip(rows) { + assert_eq!(name, file); + assert_eq!(c.gateset, gs, "{name}"); + assert_eq!(to_wqc(&c, m).unwrap().len(), len, "{gs:?} {name}.wqc"); + assert_eq!(c.ops.len(), n_ops, "{gs:?} {name}"); + assert_eq!(hx(&circuit_hash(&c).unwrap()), circuit, "{gs:?} {name}"); + let sv = c.simulate().unwrap(); + assert_eq!(hx(&sv.statevector_hash()), statevector, "{gs:?} {name}"); + // Bell is the measured vector. Its sampled outcomes do not + // move between the tables, so neither does its histogram. + if let Some(Measure { seed, shots }) = m { + assert_eq!(name, "bell"); + assert_eq!( + hx(&sv.histogram_hash(seed, shots)), + "0893f48ef32ca9f2865f6617537f0a4a5334f67e3c944bba845beb2f377acdb3", + "{gs:?} {name}" + ); + } + } + } + } + + /// Both containers round-trip: the magic names the gateset, and the + /// decoded circuit equals the written one, gateset included, with the same + /// hash and state. circuit2's corpus circuits have the plan's sizes and + /// circuit hashes. circuit/1 keeps reading what it always read: its + /// contract in any member order, whitespace or with extra members, hashed + /// as the canonical one; bytes after the op-log; a measurement that is + /// spaced or lacks `shots`. Unknown sections are ignored in both. #[test] fn wqc_round_trips() { - let c = Circuit::qft(5); - let m = Some(Measure { seed: 42, shots: 1024 }); - let bytes = to_wqc(&c, m); - assert_eq!(&bytes[0..4], b"WQC1"); - let (c2, m2) = from_wqc(&bytes).unwrap(); - assert_eq!(c, c2, "circuit must round-trip through WQC"); - assert_eq!(m, m2); - // and the reconstructed circuit hashes + simulates identically - assert_eq!(circuit_hash(&c), circuit_hash(&c2)); - assert_eq!(c.simulate().unwrap().amps, c2.simulate().unwrap().amps); + for gs in Gateset::ALL { + for (name, c, m) in corpus_circuits(gs) { + for m in [None, m.or(Some(Measure { seed: 42, shots: 1024 }))] { + let bytes = to_wqc(&c, m).unwrap(); + assert_eq!(bytes[0..4], gs.magic(), "{gs:?} {name}"); + let (c2, m2) = from_wqc(&bytes).unwrap(); + assert_eq!((&c2, m2), (&c, m), "{gs:?} {name}"); + assert_eq!(circuit_hash(&c2).unwrap(), circuit_hash(&c).unwrap(), "{gs:?} {name}"); + assert_eq!(c2.simulate().unwrap(), c.simulate().unwrap(), "{gs:?} {name}"); + } + } + } + // circuit2's four: (file, bytes, circuit_hash). QFT-5 is 263 bytes + // here and 261 in circuit/1. + let want = [ + ("bell", 218, "6f6199943059fc4d90554d7c33f1ac69723603f5128962a6e3cac9c765484d2b"), + ("ghz3", 159, "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f"), + ("qft3", 193, "c23a1b1e31b87705999cbc54866f4c9e3e4c4d2b274d6260672121abb5ca89b8"), + ("qft5", 263, "8559c83769ac8c0cb3aabc64672233aa569cd4ce92aa6545f1449982b9aa8b8c"), + ]; + for ((name, c, m), (file, len, hash)) in corpus_circuits(Gateset::V2).into_iter().zip(want) { + assert_eq!(name, file); + assert_eq!(to_wqc(&c, m).unwrap().len(), len, "circuit2 {name}.wqc"); + assert_eq!(hx(&circuit_hash(&c).unwrap()), hash, "circuit2 {name}"); + } + + // The accept corpus reads as it says, and its Bell circuit is the + // vectors' bell. + let [(_, bell, _), ..] = corpus_circuits(Gateset::V1); + let accepted = accept_corpus(); + assert_eq!(accepted[0].2, bell); + let names: std::collections::BTreeSet<_> = accepted.iter().map(|c| c.0).collect(); + assert_eq!(names.len(), accepted.len(), "case names repeat"); + for (name, file, circuit, measure) in accepted { + let (back, m) = from_wqc(&file).unwrap_or_else(|e| panic!("{name}: {e}")); + assert_eq!((&back, m), (&circuit, measure), "{name}"); + } + } + + /// Every case of the reject corpus fails with its class; so do the + /// structural edge cases; the rules apply in the order the spec gives. + /// More than 255 controls is refused everywhere a circuit is checked, and + /// the widest circuit2 gate round-trips. An op-log that claims `u32::MAX` + /// ops reserves for the ops its bytes can hold, not for the claim, and no + /// section range can wrap. + #[test] + fn container_rejects() { + let bell = oplog_of(&[op(4, &[], 0, 0), op(1, &[0], 1, 0)]); + let (canon1, canon2) = (Gateset::V1.contract_bytes(2), Gateset::V2.contract_bytes(2)); + let measure = measure_bytes(Measure { seed: 1, shots: 10 }); + // The bases the cases are built from are accepted. + for file in [ + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell)]), + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &canon2), (SECT_OPLOG, &bell)]), + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &canon2), (SECT_OPLOG, &bell), (SECT_MEASURE, &measure)]), + ] { + from_wqc(&file).unwrap(); + } + + let cases = reject_corpus(); + let names: std::collections::BTreeSet<_> = cases.iter().map(|c| c.0).collect(); + assert_eq!(names.len(), cases.len(), "case names repeat"); + let kinds: std::collections::BTreeSet<_> = cases.iter().map(|c| c.2).collect(); + assert_eq!(kinds, std::collections::BTreeSet::from(["contract", "invalid", "malformed"])); + for (name, file, kind) in &cases { + match from_wqc(file) { + Ok(c) => panic!("{name} was accepted: {c:?}"), + Err(e) => assert_eq!(e.kind(), *kind, "{name}: {e}"), + } + } + // The I-JSON cases are refused by the rule their names give. + for (name, rule) in [ + ("v1_contract_duplicate_member", "duplicate member"), + ("v1_contract_lone_surrogate", "surrogate"), + ("v1_contract_lone_low_surrogate", "surrogate"), + ("v1_contract_too_deep", "depth"), + ("v1_contract_not_utf8", "utf-8"), + ("v1_contract_number_out_of_range", "number range"), + ("v1_contract_integer_rounds_to_infinity", "number range"), + ("v1_contract_number_rounds_to_infinity", "number range"), + ("v1_contract_noncharacter", "noncharacter"), + ("v1_contract_raw_noncharacter", "noncharacter"), + ("v1_contract_bom", "syntax"), + ("v1_contract_nan", "syntax"), + ("v1_contract_control_character", "syntax"), + ("v2_n_qubits_rounds_to_infinity", "number range"), + ("v1_contract_long_literal", "number range"), + ("v1_measure_duplicate_member", "duplicate member"), + ("v1_measure_not_json", "syntax"), + ] { + let (_, file, _) = cases.iter().find(|c| c.0 == name).unwrap_or_else(|| panic!("no case {name}")); + let e = from_wqc(file).unwrap_err().to_string(); + assert!(e.contains(&format!("I-JSON: {rule} at byte")), "{name}: {e}"); + } + + // Structure, beyond the corpus. + let structural: [(&str, Vec, &str); 9] = [ + ("short", b"WQC1\x00".to_vec(), "malformed"), + ("table_cut", wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell)])[..20].to_vec(), "malformed"), + ("no_contract", wqc_raw(b"WQC2", &[(SECT_OPLOG, &bell)]), "malformed"), + ("no_oplog", wqc_raw(b"WQC2", &[(SECT_CONTRACT, &canon2)]), "malformed"), + ("past_end", wqc_table(b"WQC1", &[(SECT_CONTRACT, 0, 112), (SECT_OPLOG, 112, 16)], &[canon1.as_slice(), &bell].concat()), "malformed"), + ("contract_not_json", wqc_raw(b"WQC1", &[(SECT_CONTRACT, b"{\"ext\":"), (SECT_OPLOG, &bell)]), "contract"), + ( + "v2_n_qubits_256", + wqc_raw(b"WQC2", &[(SECT_CONTRACT, &swap(&canon2, "\"n_qubits\":2", "\"n_qubits\":256")), (SECT_OPLOG, &bell)]), + "malformed", + ), + ( + "v1_n_qubits_negative", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &swap(&canon1, "\"n_qubits\":2", "\"n_qubits\":-2")), (SECT_OPLOG, &bell)]), + "malformed", + ), + ( + "v1_measure_not_json", + wqc_raw(b"WQC1", &[(SECT_CONTRACT, &canon1), (SECT_OPLOG, &bell), (SECT_MEASURE, b"seed=1")]), + "malformed", + ), + ]; + for (name, file, kind) in &structural { + assert_eq!(from_wqc(file).map(|_| ()).unwrap_err().kind(), *kind, "{name}"); + } + // An error quotes at most 64 characters of what the file holds. + let long = swap(&canon1, "wai.quantum.circuit/1", &"é".repeat(10_000)); + let e = from_wqc(&wqc_raw(b"WQC1", &[(SECT_CONTRACT, &long), (SECT_OPLOG, &bell)])).unwrap_err(); + assert_eq!(e.kind(), "contract"); + assert!(e.to_string().chars().count() < 160, "{e}"); + + // The order: a repeated section before the contract, the contract + // before the op-log, the op-log before validation, validation before + // the measurement. + let first = |sections: &[(u8, &[u8])]| from_wqc(&wqc_raw(b"WQC2", sections)).unwrap_err().kind(); + let canon3 = Gateset::V2.contract_bytes(3); + let spaced = swap(&canon3, ":3", ": 3"); + let repeats = oplog_of(&[op(1, &[0, 0], 2, 0)]); + assert_eq!(first(&[(SECT_CONTRACT, &spaced), (SECT_OPLOG, &repeats), (SECT_OPLOG, &repeats)]), "malformed"); + assert_eq!(first(&[(SECT_CONTRACT, &spaced), (SECT_OPLOG, &[0, 0])]), "contract"); + assert_eq!(first(&[(SECT_CONTRACT, &canon3), (SECT_OPLOG, &[repeats.as_slice(), &[0]].concat())]), "malformed"); + assert_eq!(first(&[(SECT_CONTRACT, &canon3), (SECT_OPLOG, &repeats), (SECT_MEASURE, b"[]")]), "invalid"); + + // More than 255 controls: refused by validate, to_wqc and circuit_hash + // alike, in both gatesets. + for gs in Gateset::ALL { + let mut c = Circuit::with_gateset(2, gs); + c.ops.push(Gate { base: BaseGate::X, controls: vec![0; 256], target: 1, param: 0 }); + let e = c.validate().unwrap_err(); + assert_eq!(e.kind(), "invalid", "{gs:?}"); + assert_eq!(to_wqc(&c, None).unwrap_err(), e, "{gs:?}"); + assert_eq!(circuit_hash(&c).unwrap_err(), e, "{gs:?}"); + } + // 254 distinct controls on 255 qubits is the widest circuit2 gate. + let mut wide = Circuit::with_gateset(255, Gateset::V2); + wide.ops.push(Gate { base: BaseGate::X, controls: (0..254).collect(), target: 254, param: 0 }); + let bytes = to_wqc(&wide, None).unwrap(); + let (back, _) = from_wqc(&bytes).unwrap(); + assert_eq!(back, wide); + assert_eq!(circuit_hash(&back).unwrap(), circuit_hash(&wide).unwrap()); + + // A 9-byte op-log claiming u32::MAX ops: the reservation is what 9 + // bytes can hold, one op, and the decode is malformed at the second. + assert_eq!(oplog_capacity(u32::MAX, 9), 1); + assert_eq!(oplog_capacity(u32::MAX, 3), 0); + assert_eq!(oplog_capacity(3, 1 << 20), 3); + let n_ops_u32max = [u32::MAX.to_le_bytes().as_slice(), &op(4, &[], 0, 0)].concat(); + assert_eq!(parse_oplog(&n_ops_u32max).unwrap_err(), QuantumError::Malformed("op header".into())); + + // Section ranges are checked, so an offset that would wrap a 32-bit + // usize is out of bounds there, as it is here. + assert_eq!(section_range(24, 0, 112, 136), Some(24..136)); + assert_eq!(section_range(24, 0, 113, 136), None); + assert_eq!(section_range(usize::MAX - 8, 0xFFFF_FFF0, 0x20, usize::MAX), None); + assert_eq!(section_range(usize::MAX - 0x10, 0x8, 0x20, usize::MAX), None); + assert_eq!(section_range(usize::MAX - 0x40, 0x10, 0x20, usize::MAX), Some(usize::MAX - 0x30..usize::MAX - 0x10)); } @@ -1068,5 +2382,275 @@ mod tests { let mut c = Circuit::new(1); c.push(BaseGate::P, vec![], 0, 999); // P(k) out of dyadic range assert!(matches!(c.validate(), Err(QuantumError::Invalid(_)))); + + // The op-log counts controls in a u8: 256 is too many in either + // gateset, and circuit/1, which allows repeats, takes 255. + for gs in Gateset::ALL { + let mut c = Circuit::with_gateset(2, gs); + c.push(BaseGate::X, vec![0; 256], 1, 0); + assert!(matches!(c.validate(), Err(QuantumError::Invalid(_))), "{gs:?}"); + } + let mut c = Circuit::with_gateset(2, Gateset::V1); + c.push(BaseGate::X, vec![0; 255], 1, 0); + assert_eq!(c.validate(), Ok(())); + } + + /// A circuit carries its gateset, and a circuit compares unequal to the same + /// ops in the other one. + #[test] + fn circuits_carry_their_gateset() { + assert_eq!(Circuit::new(3).gateset, Gateset::V2); + assert_eq!(Circuit::qft(3).gateset, Gateset::V2); + for gs in Gateset::ALL { + let mut c = Circuit::with_gateset(3, gs); + assert_eq!((c.n_qubits, c.gateset), (3, gs)); + c.h(0).cx(0, 1); + let e = c.empty_like(); + assert_eq!((e.n_qubits, e.gateset, e.ops.len()), (3, gs, 0)); + } + let v2 = Circuit::qft(3); + let mut v1 = v2.clone(); + v1.gateset = Gateset::V1; + assert_ne!(v1, v2); + assert_ne!(v1.simulate().unwrap(), v2.simulate().unwrap()); + } + + #[test] + fn error_kinds_name_their_class() { + let cases = [ + (QuantumError::Malformed("m".into()), "malformed", "malformed WQC: m"), + (QuantumError::Invalid("i".into()), "invalid", "invalid circuit: i"), + (QuantumError::Contract("c".into()), "contract", "contract mismatch: c"), + (QuantumError::TooManyQubits(27), "too-many-qubits", "too many qubits: 27"), + ]; + for (e, kind, shown) in cases { + assert_eq!((e.kind(), e.to_string().as_str()), (kind, shown)); + } + } + + /// `P(k)` on `|1⟩` lands on the table entry itself, bit for bit, in both + /// gatesets, plain and controlled: `fxmul` by `ONE` is exact. + #[test] + fn phase_is_copied_exactly() { + for gs in Gateset::ALL { + for k in 1..=DYADIC_MAX as u16 { + let mut c = Circuit::with_gateset(1, gs); + c.x(0).p(k, 0); + assert_eq!(c.simulate().unwrap().amps, [Amp::ZERO, gs.phase(k.into())], "{gs:?} P({k})"); + let mut c = Circuit::with_gateset(2, gs); + c.x(0).x(1).cp(k, 0, 1); + let amps = c.simulate().unwrap().amps; + assert_eq!(amps[0b11], gs.phase(k.into()), "{gs:?} CP({k})"); + assert!(amps[..0b11].iter().all(|a| *a == Amp::ZERO), "{gs:?} CP({k})"); + } + } + } + + /// A small phase applied many times. `P(19)` applied 2^18 times to `|1⟩` is + /// a rotation by π. circuit2 gets there. circuit/1's `P(19)` is + /// `(2^30 − 1, 0)`: it never rotates, and each application loses one unit. + #[test] + fn repeated_small_phase() { + let reps = 1usize << 18; + let run = |gs| { + let mut c = Circuit::with_gateset(1, gs); + c.x(0); + c.ops.extend(std::iter::repeat_n(Gate { base: BaseGate::P, controls: vec![], target: 0, param: 19 }, reps)); + let amps = c.simulate().unwrap().amps; + assert_eq!(amps[0], Amp::ZERO); + amps[1] + }; + assert_eq!(run(Gateset::V1), Amp { re: 1_073_479_680, im: 0 }); + assert_eq!(run(Gateset::V1).re, ONE - reps as i64); + let v2 = run(Gateset::V2); + let angle = (v2.im as f64).atan2(v2.re as f64); + assert!((angle.abs() - std::f64::consts::PI).abs() < 1e-4, "circuit2 turned {angle} rad, not π"); + // The norm drifts as slowly as the rounding allows (1.0000189 here). + let norm = (v2.norm2() as f64).sqrt() / ONE as f64; + assert!((norm - 1.0).abs() < 1e-4, "|amp| = {norm}"); + } + + /// circuit2 is strict where circuit/1 is not: a repeated control and a + /// parameter on a gate that takes none are circuit/1 circuits, and are not + /// circuit2 ones. + #[test] + fn v2_rejects_what_v1_tolerates() { + let use_gate = |gs, n, g: Gate| { + let mut c = Circuit::with_gateset(n, gs); + c.ops.push(g); + c + }; + let mut lax = vec![Gate { base: BaseGate::X, controls: vec![0, 0], target: 1, param: 0 }]; + for base in [BaseGate::I, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg] { + lax.push(Gate { base, controls: vec![], target: 0, param: 1 }); + } + lax.push(Gate { base: BaseGate::H, controls: vec![], target: 0, param: u16::MAX }); + for g in lax { + let v1 = use_gate(Gateset::V1, 2, g.clone()); + assert_eq!(v1.validate(), Ok(()), "{g:?}"); + assert!(v1.simulate().is_ok(), "{g:?}"); + let v2 = use_gate(Gateset::V2, 2, g.clone()); + let e = v2.validate().unwrap_err(); + assert_eq!(e.kind(), "invalid", "{g:?}"); + assert_eq!(v2.simulate().unwrap_err(), e, "{g:?}"); + } + // The parameter rule spares P, whose parameter is its k. + let mut p = Circuit::new(1); + p.p(5, 0); + assert_eq!(p.validate(), Ok(())); + // Distinct controls: 254 on 255 qubits is the most circuit2 can hold. + let all: Vec = (0..254).collect(); + assert_eq!(use_gate(Gateset::V2, 255, Gate { base: BaseGate::X, controls: all.clone(), target: 254, param: 0 }).validate(), Ok(())); + let mut repeat = all.clone(); + repeat.push(253); + let g = Gate { base: BaseGate::X, controls: repeat, target: 254, param: 0 }; + assert_eq!(use_gate(Gateset::V1, 255, g.clone()).validate(), Ok(())); + assert!(matches!(use_gate(Gateset::V2, 255, g).validate(), Err(QuantumError::Invalid(_)))); + } + + /// Accuracy against the ideal, by floating-point references whose phases + /// come from the crate's own `sin_cos`, compiled under these features, all + /// of which `full` enables. + #[cfg(any( + feature = "quantum_spd", + feature = "quantum_pauli", + feature = "quantum_mps", + feature = "quantum_vml", + feature = "quantum_phasor", + feature = "quantum_kernel", + feature = "quantum_tn", + feature = "quantum_sv", + feature = "quantum_tdvp", + feature = "quantum_nqs" + ))] + mod against_ideal { + use super::*; + + type C = (f64, f64); + + fn cmul(a: C, b: C) -> C { + (a.0 * b.0 - a.1 * b.1, a.0 * b.1 + a.1 * b.0) + } + + fn cadd(a: C, b: C) -> C { + (a.0 + b.0, a.1 + b.1) + } + + /// `e^{2πi/2^k}`. + fn root(k: u16) -> C { + let (s, c) = crate::repro::sin_cos(std::f64::consts::TAU / (1u64 << k) as f64); + (c, s) + } + + /// The ideal matrix of a base gate. + fn matrix(base: BaseGate, param: u16) -> [[C; 2]; 2] { + let (z, one, i, neg_i) = ((0.0, 0.0), (1.0, 0.0), (0.0, 1.0), (0.0, -1.0)); + let h = std::f64::consts::FRAC_1_SQRT_2; + let t = root(3); + match base { + BaseGate::I => [[one, z], [z, one]], + BaseGate::X => [[z, one], [one, z]], + BaseGate::Y => [[z, neg_i], [i, z]], + BaseGate::Z => [[one, z], [z, (-1.0, 0.0)]], + BaseGate::H => [[(h, 0.0), (h, 0.0)], [(h, 0.0), (-h, 0.0)]], + BaseGate::S => [[one, z], [z, i]], + BaseGate::Sdg => [[one, z], [z, neg_i]], + BaseGate::T => [[one, z], [z, t]], + BaseGate::Tdg => [[one, z], [z, (t.0, -t.1)]], + BaseGate::P => [[one, z], [z, root(param)]], + } + } + + /// The circuit's ideal state from `|0…0⟩`, gate by gate. + fn simulate(c: &Circuit) -> Vec { + let dim = 1usize << c.n_qubits; + let mut a = vec![(0.0, 0.0); dim]; + a[0] = (1.0, 0.0); + for g in &c.ops { + let m = matrix(g.base, g.param); + let tbit = 1usize << g.target; + let mask = g.controls.iter().fold(0usize, |acc, &q| acc | (1usize << q)); + for i in 0..dim { + if i & tbit == 0 && i & mask == mask { + let j = i | tbit; + let (a0, a1) = (a[i], a[j]); + a[i] = cadd(cmul(m[0][0], a0), cmul(m[0][1], a1)); + a[j] = cadd(cmul(m[1][0], a0), cmul(m[1][1], a1)); + } + } + } + a + } + + /// QFT-14 of `|1…1⟩`, whose ladder multiplies nonzero amplitudes by every + /// `P(2..=14)`. circuit2 stays within 3 units of the ideal amplitude + /// (2.0 measured); circuit/1 is off by more than 8 (14.4 measured). + #[test] + fn qft14_accuracy() { + let n = 14u8; + let mut c = Circuit::new(n); + for q in 0..n { + c.x(q); + } + c.ops.extend(Circuit::qft(n).ops); + let want = simulate(&c); + let error = |gs| { + let mut c = c.clone(); + c.gateset = gs; + let one = ONE as f64; + c.simulate() + .unwrap() + .amps + .iter() + .zip(&want) + .map(|(a, w)| { + let (dre, dim) = (a.re as f64 - w.0 * one, a.im as f64 - w.1 * one); + (dre * dre + dim * dim).sqrt() + }) + .fold(0.0f64, f64::max) + }; + let (v2, v1) = (error(Gateset::V2), error(Gateset::V1)); + assert!(v2 <= 3.0, "circuit2 QFT-14 is {v2} units from the ideal"); + assert!(v1 > 8.0, "circuit/1 QFT-14 is only {v1} units from the ideal"); + } + + /// One circuit2 gate on a normalised pair moves each component at most + /// 2 units from the ideal (1.43 measured): ½ unit per table entry and ½ + /// per rounded product. `H`, `T`, `T†` and every `P(k)`, 400 seeded pairs + /// each, through the simulator's own `apply`. circuit/1 fails the same + /// check. + #[test] + fn one_v2_gate_is_within_two_units() { + let mut gates = vec![(BaseGate::H, 0u16), (BaseGate::T, 0), (BaseGate::Tdg, 0)]; + gates.extend((1..=DYADIC_MAX as u16).map(|k| (BaseGate::P, k))); + let worst = |gs| { + let mut s = 0x5EED_0000_0000_0008u64; + let mut uniform = || (splitmix64(&mut s) >> 11) as f64 / (1u64 << 53) as f64 * 2.0 - 1.0; + let one = ONE as f64; + let mut worst = 0.0f64; + for &(base, param) in &gates { + let m = matrix(base, param); + for _ in 0..400 { + let z = [uniform(), uniform(), uniform(), uniform()]; + let norm = z.iter().map(|x| x * x).sum::().sqrt(); + let fixed = |x: f64| (x / norm * one) as i64; + let a = [Amp { re: fixed(z[0]), im: fixed(z[1]) }, Amp { re: fixed(z[2]), im: fixed(z[3]) }]; + let mut got = a; + apply(&mut got, 1, &Gate { base, controls: vec![], target: 0, param }, gs); + let af = a.map(|x| (x.re as f64 / one, x.im as f64 / one)); + for r in 0..2 { + let w = cadd(cmul(m[r][0], af[0]), cmul(m[r][1], af[1])); + worst = worst.max((got[r].re as f64 - w.0 * one).abs()).max((got[r].im as f64 - w.1 * one).abs()); + } + } + } + worst + }; + assert_eq!(gates.len() * 400, 14_000); + let v2 = worst(Gateset::V2); + assert!(v2 <= 2.0, "a circuit2 gate is {v2} units from the ideal"); + let v1 = worst(Gateset::V1); + assert!(v1 > 2.0, "the check passed circuit/1: {v1}"); + } } } diff --git a/wai/wai-quantum/src/quantum_channel.rs b/wai/wai-quantum/src/quantum_channel.rs index 2e5e76912..41a0d58f3 100644 --- a/wai/wai-quantum/src/quantum_channel.rs +++ b/wai/wai-quantum/src/quantum_channel.rs @@ -212,7 +212,7 @@ pub fn tomography(rho: &DensityMatrix) -> DensityMatrix { #[cfg(test)] mod tests { use super::*; - use crate::quantum::Circuit; + use crate::quantum::{Circuit, Gateset}; use crate::quantum_source::{holevo_bound, von_neumann_entropy}; fn pure(c: &mut Circuit) -> DensityMatrix { @@ -227,7 +227,10 @@ mod tests { pure(&mut c) } fn plus() -> DensityMatrix { - let mut c = Circuit::new(1); + plus_in(Gateset::V2) + } + fn plus_in(gs: Gateset) -> DensityMatrix { + let mut c = Circuit::with_gateset(1, gs); c.h(0); pure(&mut c) } @@ -366,7 +369,8 @@ mod tests { /// on `aarch64-apple-darwin` and `wasm32-wasip2` under wasmtime. #[test] fn a_non_trivial_spectrum_is_bit_identical_across_architectures() { - let out = Channel::depolarizing(ONE / 2).apply(&plus()).unwrap(); + // circuit/1's |+⟩, whose bits these are. + let out = Channel::depolarizing(ONE / 2).apply(&plus_in(Gateset::V1)).unwrap(); let s = von_neumann_entropy(&out); assert_eq!( s.to_bits(), @@ -385,6 +389,19 @@ mod tests { ); } + /// The same spectrum from circuit2's |+⟩. Its eigenvalues are 3/4 and + /// 1/4 up to fixed-point rounding, so the entropy lands next to the ideal + /// H2(1/4) = 0.8112781244591328. Confirmed identical on + /// `aarch64-apple-darwin` and `wasm32-wasip2` under wasmtime. + #[test] + fn the_circuit2_spectrum_is_pinned() { + let out = Channel::depolarizing(ONE / 2).apply(&plus_in(Gateset::V2)).unwrap(); + let s = von_neumann_entropy(&out); + assert_eq!(s.to_bits(), 0x3fe9_f5fd_8a90_63e5, "entropy drifted from the pinned bits: {s:.17}"); + assert_eq!(s.to_bits(), 0.811278124459133f64.to_bits()); + assert!((s - 0.8112781244591328).abs() < 1e-12, "{s:.17} is not next to H2(1/4)"); + } + /// A channel that does nothing costs nothing — the boundary case of the same law. #[test] fn a_perfect_wire_loses_nothing() { diff --git a/wai/wai-quantum/src/quantum_comm.rs b/wai/wai-quantum/src/quantum_comm.rs index b37fcacda..f7cf46e30 100644 --- a/wai/wai-quantum/src/quantum_comm.rs +++ b/wai/wai-quantum/src/quantum_comm.rs @@ -12,7 +12,11 @@ //! seed the whole run is reproducible, so a reported error rate is a fact about a //! specific run that anyone can reproduce, not a number from a private die. -use crate::quantum::{fxmul, inv_sqrt2, Amp, Circuit, StateVector, FRAC, ONE}; +use crate::quantum::{fxmul, Amp, Circuit, Gateset, StateVector, FRAC, ONE}; + +/// The gateset every state here is prepared and measured in, named once so that +/// the X-basis measurement's `1/√2` is the one the preparation's `H` used. +const GS: Gateset = Gateset::V2; fn splitmix64(s: &mut u64) -> u64 { *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15); @@ -27,7 +31,7 @@ fn coin(s: &mut u64) -> bool { /// Prepare `bit` in the Z basis (`false`) or the X basis (`true`). fn prepare(bit: bool, x_basis: bool) -> StateVector { - let mut c = Circuit::new(1); + let mut c = Circuit::with_gateset(1, GS); if bit { c.x(0); } @@ -45,7 +49,7 @@ fn measure(sv: &StateVector, x_basis: bool, rng: &mut u64) -> bool { // X basis is measuring in Z after a Hadamard. let (a0, a1) = (sv.amps[0], sv.amps[1]); let p0 = if x_basis { - let inv = inv_sqrt2(); + let inv = GS.inv_sqrt2(); let h0 = Amp { re: fxmul(inv, a0.re + a1.re), im: fxmul(inv, a0.im + a1.im) }; (h0.norm2() >> FRAC) as i64 } else { @@ -114,7 +118,7 @@ pub fn bb84(rounds: u32, seed: u64, eavesdropper: bool) -> Bb84 { /// produces an entangled pair, and the fidelity against a true clone collapses. pub fn clone_fidelity(x_basis_input: bool) -> f64 { // attempt: |psi> (x) |0> --CNOT--> hoping for |psi> (x) |psi> - let mut attempt = Circuit::new(2); + let mut attempt = Circuit::with_gateset(2, GS); if x_basis_input { attempt.h(0); } @@ -122,7 +126,7 @@ pub fn clone_fidelity(x_basis_input: bool) -> f64 { let got = attempt.simulate().expect("two qubits"); // the state a real cloner would have produced - let mut ideal = Circuit::new(2); + let mut ideal = Circuit::with_gateset(2, GS); if x_basis_input { ideal.h(0); ideal.h(1); diff --git a/wai/wai-quantum/src/quantum_fabric.rs b/wai/wai-quantum/src/quantum_fabric.rs index 7efe5ce83..65cbcf42b 100644 --- a/wai/wai-quantum/src/quantum_fabric.rs +++ b/wai/wai-quantum/src/quantum_fabric.rs @@ -28,14 +28,20 @@ //! //! Numerics decide how much parity a fabric can earn. Most accelerators carry //! no 64-bit float, and some no 64-bit integer. The gate kernel of the -//! fixed-point core ([`KERNEL_CIRCUIT_APPLY`]) needs neither: for the -//! amplitudes and gate entries of a valid circuit, each product fits in 64 -//! bits and each result in 32. So a fabric with only 32-bit integer lanes can -//! reproduce it bit for bit. [`lanes`] is that kernel written with only the -//! operations such a fabric has, and its certificate against the native -//! simulator is bit-exact. The floating-point modules can earn bit-exactness -//! only on a fabric with 64-bit floats and the same order of operations; -//! elsewhere their parity is a measured bound. +//! fixed-point core needs neither: for the amplitudes and gate entries of a +//! valid circuit, each product fits in 64 bits and each result in 32. So a +//! fabric with only 32-bit integer lanes can reproduce it bit for bit. +//! [`lanes`] is that kernel written with only the operations such a fabric +//! has, and its certificate against the native simulator is bit-exact. The +//! floating-point modules can earn bit-exactness only on a fabric with 64-bit +//! floats and the same order of operations; elsewhere their parity is a +//! measured bound. +//! +//! The gate kernel is named once per capability, because the capabilities' +//! phase tables differ: [`KERNEL_CIRCUIT_APPLY`] for `wai.quantum.circuit` +//! ([`Gateset::V1`]) and [`KERNEL_CIRCUIT2_APPLY`] for `wai.quantum.circuit2` +//! ([`Gateset::V2`]). A certificate names its kernel, so it holds for one +//! table only. //! //! # Checked //! @@ -46,7 +52,9 @@ //! for every input including the most negative 32-bit value. //! - **The lane simulator is the native one.** Random circuits of every gate //! the core supports, dyadic phases to `P(32)` and up to three controls, on -//! up to ten qubits: every final amplitude bit-identical. The lane kernel +//! up to ten qubits, under each gateset: every final amplitude +//! bit-identical. The corpus digest of each gateset's certificate is pinned, +//! so a certificate issued under either one can be re-earned. The lane kernel //! runs the general 2×2 path for every gate, where the native one takes //! shortcuts; the shortcuts are exact by construction, and the certificate //! confirms it. The lane simulator refuses every circuit the native one @@ -63,7 +71,8 @@ //! implementation and this member,** earned on a corpus unless it is the CPU //! reference itself; it prefers fewer joules per operation, a known figure //! over none, and is deterministic on ties. A bound does not stand in for a -//! fidelity. +//! fidelity, and a certificate earned under one gateset does not serve the +//! other gateset's kernel. //! - **Receipts verify, refuse tampering, refuse an uncertified kernel, and //! refuse a joule figure without its acquisition class.** Moving bytes //! between the named fields changes what is signed. @@ -86,7 +95,7 @@ //! - **Only the gate kernel is in lanes.** Sampling, probability weights and //! fidelities in the core use 128-bit sums and are not ported. -use crate::quantum::{Circuit, FRAC, MAX_QUBITS, ONE, QuantumError}; +use crate::quantum::{Circuit, FRAC, Gateset, MAX_QUBITS, ONE, QuantumError}; use crate::quantum_energy::{EnergyClass, check_label}; use crate::quantum_ops::content_hash; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; @@ -95,15 +104,41 @@ use serde_json::{Map, Value, json}; pub use crate::quantum_ops::CAP_QUANTUM_FABRIC; /// The kernel that applies one controlled one-qubit gate to the fixed-point -/// state vector, the step every circuit of the core is made of. +/// state vector, the step every circuit of the core is made of, with the +/// `wai.quantum.circuit` table ([`Gateset::V1`]). pub const KERNEL_CIRCUIT_APPLY: &str = "wai.quantum.circuit/apply"; +/// The same kernel with the `wai.quantum.circuit2` table ([`Gateset::V2`]). +pub const KERNEL_CIRCUIT2_APPLY: &str = "wai.quantum.circuit2/apply"; + /// The implementation name of the crate's own kernel. pub const REFERENCE: &str = "reference"; -/// The corpus [`certify_lanes`] draws, by name and version. +/// The corpus [`certify_lanes`] draws for [`Gateset::V1`], by name and version. pub const GENERATOR_RANDOM_CIRCUITS: &str = "wai.quantum.circuit/random-v1"; +/// The corpus [`certify_lanes`] draws for [`Gateset::V2`]: the same circuits as +/// [`GENERATOR_RANDOM_CIRCUITS`], in the circuit2 gateset. +pub const GENERATOR_RANDOM_CIRCUITS2: &str = "wai.quantum.circuit2/random-v1"; + +/// The gate kernel's name for circuits in `gs`: [`KERNEL_CIRCUIT_APPLY`] or +/// [`KERNEL_CIRCUIT2_APPLY`]. +pub const fn apply_kernel(gs: Gateset) -> &'static str { + match gs { + Gateset::V1 => KERNEL_CIRCUIT_APPLY, + Gateset::V2 => KERNEL_CIRCUIT2_APPLY, + } +} + +/// The name of the random corpus [`certify_lanes`] draws in `gs`: +/// [`GENERATOR_RANDOM_CIRCUITS`] or [`GENERATOR_RANDOM_CIRCUITS2`]. +pub const fn random_generator(gs: Gateset) -> &'static str { + match gs { + Gateset::V1 => GENERATOR_RANDOM_CIRCUITS, + Gateset::V2 => GENERATOR_RANDOM_CIRCUITS2, + } +} + const DOMAIN_FABRIC: &[u8] = b"wai:quantum-fabric\x01"; const DOMAIN_FABRIC_ID: &[u8] = b"wai:quantum-fabric-id\x01"; const DOMAIN_CERT: &[u8] = b"wai:quantum-fabric-certificate\x01"; @@ -347,16 +382,18 @@ fn vector_units() -> (Vec<&'static str>, &'static str) { } /// The member this process runs on: the CPU, offering the reference -/// implementation of [`KERNEL_CIRCUIT_APPLY`]. A port such as the 32-bit -/// lanes joins it once [`certify_lanes`] has certified it. +/// implementation of the gate kernel for each gateset, [`KERNEL_CIRCUIT_APPLY`] +/// and [`KERNEL_CIRCUIT2_APPLY`]. A port such as the 32-bit lanes joins it +/// once [`certify_lanes`] has certified it. pub fn detect_local() -> Member { let arch = std::env::consts::ARCH; let (units, how) = vector_units(); let threads = if cfg!(target_arch = "wasm32") { 1 } else { std::thread::available_parallelism().map_or(1, |n| n.get()) }; let units = if units.is_empty() { "none found".to_string() } else { units.join(", ") }; let id = format!("local/cpu-{arch}"); + let reference = |kernel: &str| KernelEntry { kernel: kernel.into(), implementation: REFERENCE.into(), certificate: Certificate::reference(kernel, &id), energy: None }; Member { - kernels: vec![KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: REFERENCE.into(), certificate: Certificate::reference(KERNEL_CIRCUIT_APPLY, &id), energy: None }], + kernels: Gateset::ALL.into_iter().map(|gs| reference(apply_kernel(gs))).collect(), id, class: FabricClass::Cpu, presence: Presence::Detected, @@ -560,20 +597,22 @@ pub fn certify(kernel: &str, implementation: &str, member: &str, generator: &str } } -/// Certify the 32-bit-lane port of [`KERNEL_CIRCUIT_APPLY`], as -/// implementation `"lanes32"` on `member`, against the native simulator on -/// `circuits` random circuits drawn from `seed`. -pub fn certify_lanes(member: &str, circuits: usize, seed: u64) -> Certificate { +/// Certify the 32-bit-lane port of the gate kernel for `gs` +/// ([`apply_kernel`]), as implementation `"lanes32"` on `member`, against the +/// native simulator on `circuits` random circuits in `gs` drawn from `seed` +/// ([`random_generator`]). The two gatesets draw the same ops from the same +/// seed; their certificates differ in kernel, generator and corpus. +pub fn certify_lanes(gs: Gateset, member: &str, circuits: usize, seed: u64) -> Certificate { let mut s = seed; let cases = (0..circuits).map(move |_| { - let c = lanes::random_circuit(&mut s); + let c = lanes::random_circuit(&mut s, gs); let native = c.simulate_from(0).expect("random circuits are valid"); let lane = lanes::simulate(&c).expect("random circuits are valid"); let reference = Output::Fixed { values: native.amps.iter().flat_map(|a| [a.re, a.im]).collect(), frac: FRAC }; let port = Output::Fixed { values: lane.iter().flat_map(|a| [a[0] as i64, a[1] as i64]).collect(), frac: FRAC }; (lanes::circuit_bytes(&c), reference, port) }); - certify(KERNEL_CIRCUIT_APPLY, "lanes32", member, &format!("{GENERATOR_RANDOM_CIRCUITS} seed={seed} cases={circuits}"), cases) + certify(apply_kernel(gs), "lanes32", member, &format!("{} seed={seed} cases={circuits}", random_generator(gs)), cases) } /// Certify a sampling implementation of `kernel` on `member`: each case is an @@ -793,7 +832,7 @@ impl FabricReceipt { /// values. pub mod lanes { use super::{Circuit, MAX_QUBITS, QuantumError}; - use crate::quantum::{BaseGate, Gate, ONE}; + use crate::quantum::{BaseGate, Gate, Gateset, ONE}; /// The 64-bit two's-complement product of two 32-bit values, as /// `(high, low)` words, built from 16-bit halves so that no partial @@ -844,9 +883,10 @@ pub mod lanes { [a[0].wrapping_add(b[0]), a[1].wrapping_add(b[1])] } - /// The gate's matrix in lanes. Only for a validated gate. - fn matrix(g: &Gate) -> [[Lane; 2]; 2] { - let m = g.base.matrix(g.param); + /// The gate's matrix in lanes, from the gateset's table. Only for a + /// validated gate. + fn matrix(g: &Gate, gs: Gateset) -> [[Lane; 2]; 2] { + let m = g.base.matrix(g.param, gs); let l = |a: crate::quantum::Amp| [a.re as i32, a.im as i32]; [[l(m[0][0]), l(m[0][1])], [l(m[1][0]), l(m[1][1])]] } @@ -877,7 +917,7 @@ pub mod lanes { amps[0] = [ONE as i32, 0]; for g in &c.ops { let mask = g.controls.iter().fold(0usize, |acc, &q| acc | (1usize << q)); - apply(&mut amps, g.target, mask, matrix(g)); + apply(&mut amps, g.target, mask, matrix(g, c.gateset)); } Ok(amps) } @@ -890,11 +930,13 @@ pub mod lanes { z ^ (z >> 31) } - /// A random circuit of the core's gates: two to ten qubits, up to three - /// controls, dyadic phases up to `P(32)`. - pub fn random_circuit(s: &mut u64) -> Circuit { + /// A random circuit of the core's gates in `gs`: two to ten qubits, up to + /// three distinct controls, dyadic phases up to `P(32)` and no parameter + /// on any other gate, so it is valid in either gateset. The ops drawn from + /// a seed are the same in both. + pub fn random_circuit(s: &mut u64, gs: Gateset) -> Circuit { let n = 2 + (next(s) % 9) as u8; - let mut c = Circuit::new(n); + let mut c = Circuit::with_gateset(n, gs); let bases = [BaseGate::I, BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::H, BaseGate::S, BaseGate::Sdg, BaseGate::T, BaseGate::Tdg, BaseGate::P]; for _ in 0..(10 + next(s) % 50) { let base = bases[(next(s) % bases.len() as u64) as usize]; @@ -933,7 +975,7 @@ const _: () = assert!(ONE <= i32::MAX as i64); #[cfg(test)] mod tests { use super::*; - use crate::quantum::{BaseGate, Gate, fxmul}; + use crate::quantum::{BaseGate, Gate, Gateset, fxmul}; use crate::quantum_energy::{CounterFiltering, Uncertainty}; fn next(s: &mut u64) -> u64 { @@ -980,18 +1022,72 @@ mod tests { #[test] fn the_lane_simulator_is_the_native_one() { - let cert = certify_lanes("local/test", 300, 7); - assert_eq!(cert.parity, Parity::BitExact, "{cert:?}"); - assert_eq!(cert.cases, 300); - assert_eq!(cert.generator, format!("{GENERATOR_RANDOM_CIRCUITS} seed=7 cases=300")); - // The same corpus, the same certificate; another seed, another corpus. - assert_eq!(certify_lanes("local/test", 300, 7).hash(), cert.hash()); - assert_ne!(certify_lanes("local/test", 300, 8).corpus, cert.corpus); - // The corpus is not trivial: some final states spread over many - // amplitudes, with phases. - let mut s = 7u64; - let spread = (0..300).filter(|_| lanes::simulate(&lanes::random_circuit(&mut s)).unwrap().iter().filter(|a| a[0] != 0 || a[1] != 0).count() >= 4).count(); - assert!(spread >= 100, "{spread} of 300 circuits spread over four amplitudes or more"); + for gs in Gateset::ALL { + let cert = certify_lanes(gs, "local/test", 300, 7); + assert_eq!(cert.parity, Parity::BitExact, "{cert:?}"); + assert_eq!(cert.cases, 300); + assert_eq!(cert.generator, format!("{} seed=7 cases=300", random_generator(gs))); + // The same corpus, the same certificate; another seed, another corpus. + assert_eq!(certify_lanes(gs, "local/test", 300, 7).hash(), cert.hash()); + assert_ne!(certify_lanes(gs, "local/test", 300, 8).corpus, cert.corpus); + // The corpus is not trivial: some final states spread over many + // amplitudes, with phases. + let mut s = 7u64; + let spread = (0..300).filter(|_| lanes::simulate(&lanes::random_circuit(&mut s, gs)).unwrap().iter().filter(|a| a[0] != 0 || a[1] != 0).count() >= 4).count(); + assert!(spread >= 100, "{gs:?}: {spread} of 300 circuits spread over four amplitudes or more"); + } + } + + fn hex(b: &[u8]) -> String { + b.iter().map(|x| format!("{x:02x}")).collect() + } + + /// The lanes32 corpus of each gateset, seed 7, 200 cases. The circuit/1 + /// digest is the one 0.3.41 certified, so its certificates can still be + /// re-earned; the circuit2 digest is computed with the correctly rounded + /// table. The digests must never be edited to make this test pass. + #[test] + fn the_lanes32_corpus_of_each_gateset_is_pinned() { + let v1 = certify_lanes(Gateset::V1, "local/test", 200, 7); + assert_eq!(v1.parity, Parity::BitExact, "{v1:?}"); + assert_eq!(hex(&v1.corpus), "d18454c639acfd9c7b66450f4ded1aac4ab46cc10a7bedbcf4958a96e204fb36"); + let v2 = certify_lanes(Gateset::V2, "local/test", 200, 7); + assert_eq!(v2.parity, Parity::BitExact, "{v2:?}"); + assert_eq!(hex(&v2.corpus), "3dbde276c1095bf827faec71d5980e050f8a55a5ba7d97cd3bbdaec3f43a13b2"); + } + + #[test] + fn a_v2_certificate_is_its_own() { + let v1 = certify_lanes(Gateset::V1, "local/test", 40, 7); + let v2 = certify_lanes(Gateset::V2, "local/test", 40, 7); + assert_eq!((v1.kernel.as_str(), v2.kernel.as_str()), (KERNEL_CIRCUIT_APPLY, KERNEL_CIRCUIT2_APPLY)); + assert_eq!(v1.generator, format!("{GENERATOR_RANDOM_CIRCUITS} seed=7 cases=40")); + assert_eq!(v2.generator, format!("{GENERATOR_RANDOM_CIRCUITS2} seed=7 cases=40")); + assert_eq!(v2.parity, Parity::BitExact, "{v2:?}"); + // The two gatesets draw the same ops from a seed; only the gateset, + // and so the outputs, differ. + let (mut a, mut b) = (7u64, 7u64); + for _ in 0..40 { + let (x, y) = (lanes::random_circuit(&mut a, Gateset::V1), lanes::random_circuit(&mut b, Gateset::V2)); + assert_eq!((x.n_qubits, &x.ops), (y.n_qubits, &y.ops)); + assert_eq!((x.gateset, y.gateset), (Gateset::V1, Gateset::V2)); + y.validate().unwrap(); + } + assert_ne!(v1.corpus, v2.corpus); + assert_ne!(v1.hash(), v2.hash()); + // Each serves its own kernel only, even filed under the other's. + let mut member = detect_local(); + member.id = "local/test".into(); + let filed = |kernel: &str, cert: &Certificate| KernelEntry { kernel: kernel.into(), implementation: "lanes32".into(), certificate: cert.clone(), energy: Some((1, EnergyClass::ModelBased)) }; + for (gs, cert) in [(Gateset::V1, &v1), (Gateset::V2, &v2)] { + let other = Gateset::ALL.into_iter().find(|&o| o != gs).unwrap(); + let mut m = member.clone(); + m.kernels = vec![filed(apply_kernel(gs), cert)]; + assert_eq!(place(std::slice::from_ref(&m), apply_kernel(gs), &Parity::BitExact).unwrap().1.implementation, "lanes32"); + m.kernels = vec![filed(apply_kernel(other), cert)]; + assert!(place(std::slice::from_ref(&m), apply_kernel(other), &Parity::BitExact).is_none(), "a {gs:?} certificate served {}", apply_kernel(other)); + assert!(place(std::slice::from_ref(&m), apply_kernel(gs), &Parity::BitExact).is_none()); + } } #[test] @@ -1004,10 +1100,12 @@ mod tests { (2, vec![gate(BaseGate::X, vec![0], 0, 0)]), (2, vec![gate(BaseGate::X, vec![5], 1, 0)]), ]; - for (n, ops) in bad { - let c = Circuit { n_qubits: n, ops }; - let native = c.simulate_from(0).unwrap_err(); - assert_eq!(lanes::simulate(&c).unwrap_err(), native); + for gateset in Gateset::ALL { + for (n, ops) in bad.clone() { + let c = Circuit { n_qubits: n, ops, gateset }; + let native = c.simulate_from(0).unwrap_err(); + assert_eq!(lanes::simulate(&c).unwrap_err(), native); + } } assert!(matches!(lanes::simulate(&Circuit::new(MAX_QUBITS + 1)), Err(QuantumError::TooManyQubits(_)))); } @@ -1037,8 +1135,8 @@ mod tests { assert_eq!(compare(&Output::Float(vec![f64::INFINITY]), &Output::Float(vec![f64::INFINITY])), Parity::Uncertified); // Circuits that differ encode differently, however many controls. let x = |controls: Vec| Gate { base: BaseGate::X, controls, target: 1, param: 0 }; - let a = Circuit { n_qubits: 2, ops: vec![x(vec![0; 1280])] }; - let mut b = Circuit { n_qubits: 2, ops: vec![x(vec![])] }; + let a = Circuit { n_qubits: 2, ops: vec![x(vec![0; 1280])], gateset: Gateset::V2 }; + let mut b = Circuit { n_qubits: 2, ops: vec![x(vec![])], gateset: Gateset::V2 }; b.ops.extend((0..256).map(|_| Gate { base: BaseGate::I, controls: vec![], target: 0, param: 0 })); assert_ne!(lanes::circuit_bytes(&a), lanes::circuit_bytes(&b)); // Outputs split differently between reference and member hash apart. @@ -1140,7 +1238,7 @@ mod tests { let mut local = detect_local(); assert_eq!(local.presence, Presence::Detected); let id = local.id.clone(); - local.kernels.push(KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: "lanes32".into(), certificate: certify_lanes(&id, 5, 1), energy: None }); + local.kernels.push(KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: "lanes32".into(), certificate: certify_lanes(Gateset::V1, &id, 5, 1), energy: None }); assert_eq!(place(std::slice::from_ref(&local), KERNEL_CIRCUIT_APPLY, &Parity::BitExact).unwrap().1.implementation, REFERENCE); } @@ -1172,7 +1270,7 @@ mod tests { fn receipts_verify_and_refuse() { let signer = SigningKey::from_bytes(&[7u8; 32]); let mut member = detect_local(); - let cert = certify_lanes(&member.id, 20, 3); + let cert = certify_lanes(Gateset::V1, &member.id, 20, 3); member.kernels.push(KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: "lanes32".into(), certificate: cert.clone(), energy: None }); let entry = member.kernels.last().unwrap().clone(); let counter = EnergyClass::OnChipCounter { uncertainty: Uncertainty { relative_ppm: 50_000, window_us: 1_000 }, filtering: CounterFiltering::Off }; @@ -1180,8 +1278,9 @@ mod tests { let r = FabricReceipt::seal(&signer, "fabric-test", [9; 32], &member, &entry, joules, class); assert!(r.verify()); assert!(r.certificate_matches(&cert)); - assert!(!r.certificate_matches(&certify_lanes(&member.id, 20, 4))); - assert!(!r.certificate_matches(&certify_lanes("local/other", 20, 3))); + assert!(!r.certificate_matches(&certify_lanes(Gateset::V1, &member.id, 20, 4))); + assert!(!r.certificate_matches(&certify_lanes(Gateset::V1, "local/other", 20, 3))); + assert!(!r.certificate_matches(&certify_lanes(Gateset::V2, &member.id, 20, 3))); let back = FabricReceipt::from_json(&r.to_json()).unwrap(); assert_eq!(back, r); assert!(back.verify()); @@ -1232,7 +1331,15 @@ mod tests { let local = detect_local(); assert_eq!(local.class, FabricClass::Cpu); assert!(local.detail.contains("asked of the CPU") || local.detail.contains("compiled for"), "{}", local.detail); - assert_eq!(local.kernels.len(), 1); - assert_eq!(local.kernels[0].certificate.member, local.id); + // The reference kernel of each gateset, each placeable on its own. + let kernels: Vec<&str> = local.kernels.iter().map(|k| k.kernel.as_str()).collect(); + assert_eq!(kernels, [KERNEL_CIRCUIT_APPLY, KERNEL_CIRCUIT2_APPLY]); + for k in &local.kernels { + assert_eq!((k.certificate.member.as_str(), k.certificate.kernel.as_str(), k.implementation.as_str()), (local.id.as_str(), k.kernel.as_str(), REFERENCE)); + } + for gs in Gateset::ALL { + let (_, k) = place(std::slice::from_ref(&local), apply_kernel(gs), &Parity::BitExact).unwrap(); + assert_eq!((k.kernel.as_str(), k.implementation.as_str()), (apply_kernel(gs), REFERENCE)); + } } } diff --git a/wai/wai-quantum/src/quantum_qir.rs b/wai/wai-quantum/src/quantum_qir.rs index 9833b8e94..0e9951838 100644 --- a/wai/wai-quantum/src/quantum_qir.rs +++ b/wai/wai-quantum/src/quantum_qir.rs @@ -34,6 +34,17 @@ //! global phase, which is physically unobservable — every measurement //! distribution is identical. //! +//! # Gatesets +//! +//! The export writes the ideal angle `2π/2ᵏ` for `P(k)`, whatever the circuit's +//! gateset, and `T` as the `t` intrinsic. That is the circuit2 table's meaning +//! to within its rounding. A circuit/1 circuit ([`Gateset::V1`](crate::quantum::Gateset::V1)) exports the +//! same ideal angles, not its own numerics: circuit/1's `P(k)` deviates from +//! `2π/2ᵏ` by up to 12868 units of `2⁻³⁰` for `k ≥ 8`, and applies no phase at +//! all for `k ≥ 19`, so a module exported from such a circuit is not what the +//! circuit simulates here. Ingest ([`from_qir`]) builds circuits in +//! [`Gateset::V2`](crate::quantum::Gateset::V2). +//! //! # How this is checked //! //! - The exact rewrites (`CY`, `CCX`, `CCZ`) are verified against the simulator @@ -198,6 +209,10 @@ fn declaration(name: &str) -> String { /// Emit `c` as a QIR module. `measure_all` appends an `mz` + result record for /// every qubit (what a base-profile consumer expects to find). +/// +/// Angles are the ideal ones in every gateset: a [`Gateset::V1`](crate::quantum::Gateset::V1) circuit +/// exports angles its own table does not simulate for `P(k ≥ 8)` (see the +/// module's notes on gatesets). pub fn to_qir(c: &Circuit, measure_all: bool) -> String { let mut e = Emitter::new(); for g in &c.ops { @@ -338,6 +353,7 @@ fn arg_double(line: &str) -> Option { /// is calls to the standard `__quantum__qis__*` intrinsics with constant qubit /// operands. Measurement calls (`mz`, `result_record_output`) are recognised and /// skipped — a `Circuit` is unitary, the measurement is the caller's business. +/// The circuit is built in [`Gateset::V2`](crate::quantum::Gateset::V2). /// /// # Errors /// diff --git a/wai/wai-quantum/src/quantum_qudit.rs b/wai/wai-quantum/src/quantum_qudit.rs index 4d4c66bb5..61bf95b4e 100644 --- a/wai/wai-quantum/src/quantum_qudit.rs +++ b/wai/wai-quantum/src/quantum_qudit.rs @@ -22,7 +22,7 @@ //! whose accuracy is a promise rather than a proof. Adding a dimension means //! adding its exact roots, and [`omega`] refuses the rest rather than guessing. -use crate::quantum::{sqrt_fx, Amp, FRAC, ONE}; +use crate::quantum::{sqrt_fx, Amp, Gateset, FRAC, ONE}; /// `e^{2πik/d}`, exactly, for the supported dimensions. /// @@ -45,10 +45,12 @@ pub fn omega(d: u8, k: u32) -> Option { } } -/// `1/√d`, the Fourier normaliser, exactly. +/// `1/√d`, the Fourier normaliser, exactly. At `d = 2` it is circuit2's +/// `1/√2`, the nearest integer, so that the qudit Fourier gate is that +/// gateset's Hadamard; `d = 3` is the floor square root. fn inv_sqrt_d(d: u8) -> Option { match d { - 2 => Some(sqrt_fx(ONE / 2)), + 2 => Some(Gateset::V2.inv_sqrt2()), 3 => Some(sqrt_fx(ONE / 3)), _ => None, } @@ -234,7 +236,7 @@ mod tests { q.fourier(0); q.csum(0, 1); - let mut c = Circuit::new(2); + let mut c = Circuit::with_gateset(2, Gateset::V2); c.h(0).cx(0, 1); let sv = c.simulate().unwrap(); diff --git a/wai/wai-quantum/src/quantum_receipt.rs b/wai/wai-quantum/src/quantum_receipt.rs index 3efbafe74..e7832dd92 100644 --- a/wai/wai-quantum/src/quantum_receipt.rs +++ b/wai/wai-quantum/src/quantum_receipt.rs @@ -1,5 +1,6 @@ //! Quantum-circuit receipt — the energy-accounted seal for a -//! `wai.quantum.circuit` reconstruction (extensions/quantum-sim § Receipt). +//! `wai.quantum.circuit` or `wai.quantum.circuit2` reconstruction +//! (extensions/quantum-sim § Receipt). //! //! This is the artifact WAI's determinism uniquely enables here: a signed record //! that cleanly **separates the two halves** of a classical quantum-simulation's @@ -25,6 +26,13 @@ //! is unlabelled, with exactly the legacy bytes, and //! `quantum_energy::Labelled::seal` labels and re-signs it (`quantum_energy`). //! +//! The receipt's format is the same for both capabilities. It names its +//! capability through `circuit_hash`, which covers the circuit's contract and +//! so its [`Gateset`]: a circuit/1 receipt re-simulates only against the +//! circuit/1 circuit, never against its circuit2 twin with the same ops. +//! [`QuantumReceipt::gateset_of`] says which gateset a receipt was sealed +//! under, as a diagnosis when a reconstruction does not match. +//! //! It is a JWP profile like every other receipt in this crate — the //! worlds/provenance Merkle (`crate::merkle::merkle_root`) + Ed25519, //! reused, not reinvented — and chains across a derivation via @@ -32,7 +40,7 @@ //! the receipt's value is exactly this: it makes the *cost* of the reconstruction //! auditable, and the *result* reproducible. -use crate::quantum::{circuit_hash, Circuit, Measure, QuantumError}; +use crate::quantum::{circuit_hash, Circuit, Gateset, Measure, QuantumError}; use crate::merkle::merkle_root; use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; @@ -85,10 +93,12 @@ pub struct MeasureRecord { pub histogram_hash: [u8; 32], } -/// A signed, energy-accounted seal over one `wai.quantum.circuit` reconstruction. +/// A signed, energy-accounted seal over one `wai.quantum.circuit` or +/// `wai.quantum.circuit2` reconstruction. #[derive(Clone, Debug, PartialEq)] pub struct QuantumReceipt { - /// Identity: the WQC circuit hash (`crate::quantum::circuit_hash`). + /// Identity: the WQC circuit hash (`crate::quantum::circuit_hash`), which + /// binds the circuit's capability and phase table. pub circuit_hash: [u8; 32], /// The reconstruction result: the `2^n`-amplitude statevector's portable hash. pub statevector_hash: [u8; 32], @@ -165,6 +175,10 @@ impl QuantumReceipt { /// `statevector_hash` is always the true reconstruction, never a caller /// claim. `joules_micro` is the sink's measured energy (`0` if unmetered); /// pass `measure` to also vouch for a shot histogram. + /// + /// The circuit simulates under its own gateset, and the receipt binds that + /// gateset's contract through `circuit_hash`. Fails as + /// [`Circuit::simulate`] does. pub fn seal( signer: &SigningKey, signer_id: impl Into, @@ -180,8 +194,9 @@ impl QuantumReceipt { shots: m.shots, histogram_hash: sv.histogram_hash(m.seed, m.shots), }); + // `simulate` validated the circuit, which bounds its ops by `u32::MAX`. let mut r = QuantumReceipt { - circuit_hash: circuit_hash(circuit), + circuit_hash: circuit_hash(circuit)?, statevector_hash, n_qubits: circuit.n_qubits, n_ops: circuit.ops.len() as u32, @@ -233,16 +248,50 @@ impl QuantumReceipt { /// claimed statevector is not what the circuit actually reconstructs to. /// (The measured `joules_micro` remains attested-only — re-simulation checks /// the *result and cost*, never the signer's energy draw.) + /// + /// The circuit is re-simulated under its own gateset, as decoded from its + /// container. A receipt sealed under one gateset does not verify against + /// the same ops in another; [`Self::gateset_of`] tells which one it was. pub fn verify_reconstruction(&self, circuit: &Circuit) -> bool { self.reconstruction_matches(circuit) && self.verify() } + /// The gateset under which `circuit`'s ops hash to this receipt's + /// `circuit_hash`, whatever gateset `circuit` itself carries; `None` when + /// no gateset does. + /// + /// A migration aid, not a check: it matches the circuit hash alone and + /// re-simulates nothing, and [`Self::verify_reconstruction`] stays strict. + /// Use it to explain a failed reconstruction, as [`Self::gateset_note`] + /// does. + pub fn gateset_of(&self, circuit: &Circuit) -> Option { + let mut twin = circuit.clone(); + Gateset::ALL.into_iter().find(|&gs| { + twin.gateset = gs; + circuit_hash(&twin).ok() == Some(self.circuit_hash) + }) + } + + /// When `circuit`'s ops match this receipt under a gateset other than + /// `circuit`'s own, the text that says so, for example `"this is a + /// circuit/1 receipt; rebuild with Gateset::V1"`. `None` when the gateset + /// is not the reason the reconstruction fails. + pub fn gateset_note(&self, circuit: &Circuit) -> Option { + let gs = self.gateset_of(circuit).filter(|&gs| gs != circuit.gateset)?; + let (name, variant) = match gs { + Gateset::V1 => ("circuit/1", "V1"), + Gateset::V2 => ("circuit2", "V2"), + }; + Some(format!("this is a {name} receipt; rebuild with Gateset::{variant}")) + } + /// The re-simulation half of [`Self::verify_reconstruction`], shared with /// the labelled form (`crate::quantum_energy::Labelled`). pub(crate) fn reconstruction_matches(&self, circuit: &Circuit) -> bool { - if circuit_hash(circuit) != self.circuit_hash { + if circuit_hash(circuit).ok() != Some(self.circuit_hash) { return false; } + // A circuit with a hash is valid, so its ops fit a `u32`. if circuit.n_qubits != self.n_qubits || circuit.ops.len() as u32 != self.n_ops { return false; } @@ -307,6 +356,8 @@ impl QuantumReceipt { } /// Parse a receipt from its canonical JSON (for a verifying sink). + /// `n_qubits` must fit a `u8` and `n_ops` a `u32`; a value that does not + /// is refused, never truncated. pub fn from_json(s: &str) -> Option { let v: serde_json::Value = serde_json::from_str(s).ok()?; let o = v.as_object()?; @@ -326,8 +377,8 @@ impl QuantumReceipt { Some(QuantumReceipt { circuit_hash: from_hex32(o.get("circuit_hash")?.as_str()?)?, statevector_hash: from_hex32(o.get("statevector_hash")?.as_str()?)?, - n_qubits: u("n_qubits")? as u8, - n_ops: u("n_ops")? as u32, + n_qubits: u8::try_from(u("n_qubits")?).ok()?, + n_ops: u32::try_from(u("n_ops")?).ok()?, measurement, merkle_root: from_hex32(o.get("root_hash")?.as_str()?)?, work_amp_updates: u("work_amp_updates")?, @@ -423,6 +474,105 @@ mod tests { assert!(child.verify()); assert_eq!(child.parent_receipt_hash, Some(root.receipt_hash())); } + + /// GHZ-3 in `gs`. Its `H` reads the gateset's table, so the two gatesets + /// give different statevectors as well as different circuit hashes. + fn ghz3(gs: Gateset) -> Circuit { + let mut c = Circuit::with_gateset(3, gs); + c.h(0).cx(0, 1).cx(1, 2); + c + } + + #[test] + fn a_receipt_reconstructs_only_under_its_own_gateset() { + let m = Measure { seed: 7, shots: 1024 }; + for gs in Gateset::ALL { + let r = QuantumReceipt::seal(&key(7), "m", &ghz3(gs), Some(m), 0, None).unwrap(); + assert!(r.verify_reconstruction(&ghz3(gs))); + assert_eq!(r.gateset_of(&ghz3(gs)), Some(gs)); + assert_eq!(r.gateset_note(&ghz3(gs)), None); + for other in Gateset::ALL.into_iter().filter(|&o| o != gs) { + // The twin has the same ops and fails cleanly: no panic, a + // false, and a diagnosis naming the receipt's gateset. + let twin = ghz3(other); + assert!(!r.verify_reconstruction(&twin), "{gs:?} receipt against its {other:?} twin"); + assert_eq!(r.gateset_of(&twin), Some(gs)); + assert!(r.gateset_note(&twin).is_some()); + } + } + // A circuit/1 receipt against its circuit2 twin, by name. + let v1 = QuantumReceipt::seal(&key(7), "m", &ghz3(Gateset::V1), None, 0, None).unwrap(); + let v2_twin = ghz3(Gateset::V2); + assert!(!v1.verify_reconstruction(&v2_twin)); + assert_eq!(v1.gateset_of(&v2_twin), Some(Gateset::V1)); + assert_eq!(v1.gateset_note(&v2_twin).as_deref(), Some("this is a circuit/1 receipt; rebuild with Gateset::V1")); + let v2 = QuantumReceipt::seal(&key(7), "m", &v2_twin, None, 0, None).unwrap(); + assert_eq!(v2.gateset_note(&ghz3(Gateset::V1)).as_deref(), Some("this is a circuit2 receipt; rebuild with Gateset::V2")); + // The gateset is bound even where it changes no amplitude: X and CX + // read no table, so the twins reconstruct to one statevector, and the + // receipt still holds only its own. + let clifford = |gs| { + let mut c = Circuit::with_gateset(2, gs); + c.x(0).cx(0, 1); + c + }; + assert_eq!(clifford(Gateset::V1).simulate().unwrap(), clifford(Gateset::V2).simulate().unwrap()); + let r = QuantumReceipt::seal(&key(7), "m", &clifford(Gateset::V1), None, 0, None).unwrap(); + assert!(r.verify_reconstruction(&clifford(Gateset::V1))); + assert!(!r.verify_reconstruction(&clifford(Gateset::V2))); + // Ops that match under no gateset get no diagnosis. + let mut unrelated = Circuit::with_gateset(3, Gateset::V1); + unrelated.h(1); + assert_eq!(v1.gateset_of(&unrelated), None); + assert_eq!(v1.gateset_note(&unrelated), None); + // Ops only circuit/1 allows still get their diagnosis: repeated + // controls hash in circuit/1, and circuit2 refuses them. + let mut repeated = Circuit::with_gateset(3, Gateset::V1); + repeated.ops.push(crate::quantum::Gate { base: crate::quantum::BaseGate::X, controls: vec![0, 0], target: 1, param: 0 }); + let r = QuantumReceipt::seal(&key(7), "m", &repeated, None, 0, None).unwrap(); + let mut repeated_v2 = repeated.clone(); + repeated_v2.gateset = Gateset::V2; + assert!(!r.verify_reconstruction(&repeated_v2)); + assert_eq!(r.gateset_of(&repeated_v2), Some(Gateset::V1)); + } + + /// A receipt that names one gateset's circuit and claims the other's + /// statevector: well signed and internally consistent, and reconstructed + /// by neither circuit. + #[test] + fn a_cross_gateset_forgery_is_rejected() { + let forger = key(9); + let (seed, shots) = (7, 1024); + for (named, simulated) in [(Gateset::V1, Gateset::V2), (Gateset::V2, Gateset::V1)] { + let mut r = QuantumReceipt::seal(&forger, "m", &ghz3(named), Some(Measure { seed, shots }), 0, None).unwrap(); + let sv = ghz3(simulated).simulate().unwrap(); + assert_ne!(sv.statevector_hash(), r.statevector_hash); + r.statevector_hash = sv.statevector_hash(); + r.measurement = Some(MeasureRecord { seed, shots, histogram_hash: sv.histogram_hash(seed, shots) }); + r.merkle_root = merkle_root(&r.leaves()); + r.sig = forger.sign(&r.signing_payload()).to_bytes(); + assert!(r.verify(), "the forgery is well signed"); + for gs in Gateset::ALL { + assert!(!r.verify_reconstruction(&ghz3(gs)), "named {named:?}, simulated {simulated:?}, checked {gs:?}"); + } + } + } + + #[test] + fn json_refuses_counts_that_do_not_fit() { + let r = QuantumReceipt::seal(&key(5), "m", &ghz3(Gateset::V2), None, 0, None).unwrap(); + let j = r.to_json(); + assert!(j.contains("\"n_qubits\":3,") && j.contains("\"n_ops\":3,"), "{j}"); + // 258 is 2 modulo 256, and 2^32 + 3 is 3 modulo 2^32: neither may be + // read as the smaller number. + assert!(QuantumReceipt::from_json(&j.replace("\"n_qubits\":3,", "\"n_qubits\":258,")).is_none()); + assert!(QuantumReceipt::from_json(&j.replace("\"n_qubits\":3,", "\"n_qubits\":256,")).is_none()); + assert!(QuantumReceipt::from_json(&j.replace("\"n_ops\":3,", "\"n_ops\":4294967299,")).is_none()); + // The largest values that fit still parse. + let widest = j.replace("\"n_qubits\":3,", "\"n_qubits\":255,").replace("\"n_ops\":3,", "\"n_ops\":4294967295,"); + let back = QuantumReceipt::from_json(&widest).expect("fits"); + assert_eq!((back.n_qubits, back.n_ops), (255, u32::MAX)); + } } crate::quantum_energy::labellable!(QuantumReceipt, DOMAIN_RECEIPT_ID); diff --git a/wai/wai-quantum/src/quantum_source.rs b/wai/wai-quantum/src/quantum_source.rs index 46a94efa5..c2ce13746 100644 --- a/wai/wai-quantum/src/quantum_source.rs +++ b/wai/wai-quantum/src/quantum_source.rs @@ -263,7 +263,8 @@ mod tests { /// in its last places between platforms, and this test would catch it. #[test] fn entropy_is_bit_identical_across_architectures() { - let mut c = Circuit::new(2); + // circuit/1's Bell pair, whose bits these are. + let mut c = Circuit::with_gateset(2, crate::quantum::Gateset::V1); c.h(0).cx(0, 1); let half = DensityMatrix::from_pure(&c.simulate().unwrap()).partial_trace(&[0]); let s = von_neumann_entropy(&half); @@ -274,6 +275,20 @@ mod tests { ); } + /// circuit2's Bell pair. Its correctly rounded 1/√2 makes the reduced + /// state exactly half the identity at this scale, so the entropy is + /// exactly one bit. Confirmed identical on `aarch64-apple-darwin` and + /// `wasm32-wasip2` under wasmtime. + #[test] + fn entropy_of_the_circuit2_bell_pair_is_exactly_one_bit() { + let mut c = Circuit::with_gateset(2, crate::quantum::Gateset::V2); + c.h(0).cx(0, 1); + let half = DensityMatrix::from_pure(&c.simulate().unwrap()).partial_trace(&[0]); + let s = von_neumann_entropy(&half); + assert_eq!(s.to_bits(), 0x3ff0_0000_0000_0000, "entropy drifted from the pinned bits: {s:.17}"); + assert_eq!(s.to_bits(), 1.0f64.to_bits()); + } + #[test] fn entropy_is_reproducible() { let mut c = Circuit::new(3); diff --git a/wai/wai-quantum/src/quantum_spd.rs b/wai/wai-quantum/src/quantum_spd.rs index 695034b60..6c6e19729 100644 --- a/wai/wai-quantum/src/quantum_spd.rs +++ b/wai/wai-quantum/src/quantum_spd.rs @@ -831,16 +831,16 @@ pub fn from_dyadic(c: &crate::quantum::Circuit) -> Result } (B::X, _) => { out.push(Op::H(t)); - controlled_phase(&mut out, &ctrls, t, 0.5 * tau); + controlled_phase(&mut out, &ctrls, t, 0.5 * tau)?; out.push(Op::H(t)); } (B::Y, _) => { out.push(Op::Sdg(t)).push(Op::H(t)); - controlled_phase(&mut out, &ctrls, t, 0.5 * tau); + controlled_phase(&mut out, &ctrls, t, 0.5 * tau)?; out.push(Op::H(t)).push(Op::S(t)); } (base, _) => match phase(base) { - Some(turns) => controlled_phase(&mut out, &ctrls, t, turns * tau), + Some(turns) => controlled_phase(&mut out, &ctrls, t, turns * tau)?, None => return Err(SpdError::Unsupported(format!("{base:?} on {} controls", ctrls.len()))), }, } @@ -852,16 +852,24 @@ pub fn from_dyadic(c: &crate::quantum::Circuit) -> Result /// With `m` qubits, `|1…1⟩⟨1…1| = 2⁻ᵐ Σ_S (−1)^{|S|} Z_S`, so the gate is /// `Π_S exp(iφ(−1)^{|S|} Z_S / 2ᵐ)` over nonempty subsets `S` (the empty one is /// a global phase): a rotation `exp(−iθZ_S/2)` with `θ = −2φ(−1)^{|S|}/2ᵐ`. +/// +/// The subsets are counted in a `u32`, so `m` must be below 32; a gate on more +/// qubits (a circuit2 gate may carry up to 254 controls) is refused as +/// unsupported rather than shifted out of range. #[cfg(feature = "quantum")] -fn controlled_phase(out: &mut RotCircuit, controls: &[u32], target: u32, phi: f64) { +fn controlled_phase(out: &mut RotCircuit, controls: &[u32], target: u32, phi: f64) -> Result<(), SpdError> { let mut qs = controls.to_vec(); qs.push(target); - let m = qs.len() as u32; - for mask in 1u32..(1 << m) { + let subsets = u32::try_from(qs.len()).ok().and_then(|m| 1u32.checked_shl(m).map(|s| (m, s))); + let Some((m, subsets)) = subsets else { + return Err(SpdError::Unsupported(format!("a phase on {} qubits; at most 31", qs.len()))); + }; + for mask in 1u32..subsets { let axis: PauliString = (0..m).filter(|i| mask >> i & 1 == 1).map(|i| (qs[i as usize], Pauli::Z)).collect(); let sign = if axis.len() % 2 == 0 { 1.0 } else { -1.0 }; - out.push(Op::Rot { axis, theta: -2.0 * phi * sign / f64::from(1u32 << m) }); + out.push(Op::Rot { axis, theta: -2.0 * phi * sign / f64::from(subsets) }); } + Ok(()) } #[cfg(test)] @@ -1324,6 +1332,32 @@ mod tests { // ---- the dyadic adapter ------------------------------------------------------ + /// A controlled phase counts its `2^m` subsets in a `u32`. Forty controls + /// would shift out of range; the gate is refused instead. + #[cfg(feature = "quantum")] + #[test] + fn a_phase_on_forty_controls_does_not_overflow() { + use crate::quantum::{BaseGate, Circuit, Gate}; + let mut out = RotCircuit::new(41); + let controls: Vec = (0..40).collect(); + assert!(matches!(controlled_phase(&mut out, &controls, 40, 1.0), Err(SpdError::Unsupported(_)))); + assert!(out.ops.is_empty(), "nothing is emitted before the refusal"); + // At the edge: 31 controls and the target make 32 qubits, refused. + assert!(matches!(controlled_phase(&mut out, &(0..31).collect::>(), 31, 1.0), Err(SpdError::Unsupported(_)))); + // Through the adapter, for every multi-control gate it expands. + for base in [BaseGate::X, BaseGate::Y, BaseGate::Z, BaseGate::T, BaseGate::P] { + let mut c = Circuit::new(41); + let param = if base == BaseGate::P { 5 } else { 0 }; + c.ops.push(Gate { base, controls: (0..40).collect(), target: 40, param }); + c.validate().unwrap(); + assert!(matches!(from_dyadic(&c), Err(SpdError::Unsupported(_))), "{base:?}"); + } + // A small gate still expands: two controls, 2^3 − 1 rotations. + let mut out = RotCircuit::new(3); + controlled_phase(&mut out, &[0, 1], 2, 1.0).unwrap(); + assert_eq!(out.ops.len(), 7); + } + #[cfg(feature = "quantum")] #[test] fn every_dyadic_algorithm_agrees_with_the_byte_exact_core() { diff --git a/wai/wai-quantum/src/quantum_toolchain.rs b/wai/wai-quantum/src/quantum_toolchain.rs index c9ffd399c..f597de04e 100644 --- a/wai/wai-quantum/src/quantum_toolchain.rs +++ b/wai/wai-quantum/src/quantum_toolchain.rs @@ -25,8 +25,13 @@ //! representable, which is *why* the result is byte-identical on every machine. //! Continuous rotations (`RX(θ)` for irrational θ) are out of the exact set by //! construction, not omission. +//! +//! Gatesets: the algorithms and the OpenQASM importer build circuits from +//! nothing, so in [`crate::quantum::Gateset::V2`], `wai.quantum.circuit2`. A +//! circuit derived from another (a noisy trajectory, a routed circuit) keeps +//! the other's gateset. -use crate::quantum::{circuit_hash, Amp, BaseGate, Circuit, Gate, StateVector, FRAC}; +use crate::quantum::{circuit_hash, Amp, BaseGate, Circuit, Gate, QuantumError, StateVector, FRAC}; // =========================================================================== // Algorithm library @@ -386,7 +391,8 @@ pub fn top_outcomes(sv: &StateVector, top: usize) -> Vec<(usize, f64)> { /// `⟨Z_q⟩` under a per-gate **depolarizing** channel, by averaging over /// `trajectories` random error insertions: after each gate, each qubit it touched /// suffers a uniformly random `X`/`Y`/`Z` with probability `depol_permille`/1000. -/// Each trajectory reuses the byte-exact dense simulator; deterministic in `seed`. +/// Each trajectory reuses the byte-exact dense simulator, in the circuit's own +/// gateset; deterministic in `seed`. /// `⟨Z_q⟩` decays toward 0 as noise or depth grows — decoherence, made measurable. pub fn noisy_expect_z( circuit: &Circuit, @@ -399,7 +405,7 @@ pub fn noisy_expect_z( let mut acc = 0.0; let traj = trajectories.max(1); for _ in 0..traj { - let mut noisy = Circuit::new(circuit.n_qubits); + let mut noisy = circuit.empty_like(); for g in &circuit.ops { noisy.ops.push(g.clone()); for &qb in g.controls.iter().chain(std::iter::once(&g.target)) { @@ -484,11 +490,12 @@ pub struct Routed { /// Route `src` onto a line topology `0-1-…-(n-1)`, inserting SWAPs (as 3·CX) so /// every one-control gate acts on adjacent physical qubits. Gates with ≥2 controls /// are placed on the mapped qubits directly (still correct, not made local). +/// The routed circuit keeps `src`'s gateset. pub fn route_line(src: &Circuit) -> Routed { let n = src.n_qubits; let mut phys: Vec = (0..n).collect(); let mut logq: Vec = (0..n).collect(); - let mut out = Circuit::new(n); + let mut out = src.empty_like(); let mut swaps = 0u32; let (mut s2q, mut nonlocal) = (0u32, 0u32); let do_swap = |out: &mut Circuit, phys: &mut [u8], logq: &mut [u8], a: u8, b: u8| { @@ -585,6 +592,11 @@ pub fn routing_equivalent(src: &Circuit, routed: &Routed) -> bool { /// Emit OpenQASM 3.0 for a circuit (the dyadic gate set maps to standard gates; /// `P(k)` becomes `p(2*pi/2^k)`). +/// +/// The angles are the ideal ones, whatever the circuit's gateset. A +/// [`crate::quantum::Gateset::V1`] circuit exports angles its own table does +/// not simulate: circuit/1's `P(k)` deviates from `2π/2^k` for `k ≥ 8` and +/// applies no phase for `k ≥ 19`. pub fn to_qasm(c: &Circuit) -> String { let mut s = String::from("OPENQASM 3.0;\ninclude \"stdgates.inc\";\n"); s.push_str(&format!("qubit[{}] q;\n", c.n_qubits)); @@ -637,6 +649,13 @@ pub fn to_qasm(c: &Circuit) -> String { /// `rz`, `rx`, `ry`, `sx` and `u*` are exact up to a global phase, which no /// measurement can observe; `crz` is exact outright, because its leftover factor /// lands on the control where it *would* be observable. +/// +/// # Gateset +/// +/// The circuit is built in [`crate::quantum::Gateset::V2`]. A gate that names +/// one qubit twice (`ccx q[0],q[0],q[1]`, `cx q[1],q[1]`) is refused as an +/// invalid circuit when it is read, never built, so the circuit stays valid +/// if its gateset is then changed. pub fn from_qasm(src: &str) -> Result { let mut n: Option = None; // first pass: qubit count @@ -675,36 +694,40 @@ pub fn from_qasm(src: &str) -> Result { _ => {} } let qs = parse_qubits(&line); - let push = |c: &mut Circuit, base: BaseGate, controls: Vec, target: u8, param: u16| { + let push = |c: &mut Circuit, base: BaseGate, controls: Vec, target: u8, param: u16| -> Result<(), String> { + distinct_qubits(&controls, target)?; c.ops.push(Gate { base, controls, target, param }); + Ok(()) }; match head { "id" | "i" => {} - "x" => require1(&qs).map(|t| push(&mut c, BaseGate::X, vec![], t, 0))?, - "y" => require1(&qs).map(|t| push(&mut c, BaseGate::Y, vec![], t, 0))?, - "z" => require1(&qs).map(|t| push(&mut c, BaseGate::Z, vec![], t, 0))?, - "h" => require1(&qs).map(|t| push(&mut c, BaseGate::H, vec![], t, 0))?, - "s" => require1(&qs).map(|t| push(&mut c, BaseGate::S, vec![], t, 0))?, - "sdg" => require1(&qs).map(|t| push(&mut c, BaseGate::Sdg, vec![], t, 0))?, - "t" => require1(&qs).map(|t| push(&mut c, BaseGate::T, vec![], t, 0))?, - "tdg" => require1(&qs).map(|t| push(&mut c, BaseGate::Tdg, vec![], t, 0))?, - "cx" | "cnot" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::X, vec![a], t, 0))?, - "cy" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::Y, vec![a], t, 0))?, - "cz" => require2(&qs).map(|(a, t)| push(&mut c, BaseGate::Z, vec![a], t, 0))?, - "swap" => require2(&qs).map(|(a, b)| { + "x" => require1(&qs).and_then(|t| push(&mut c, BaseGate::X, vec![], t, 0))?, + "y" => require1(&qs).and_then(|t| push(&mut c, BaseGate::Y, vec![], t, 0))?, + "z" => require1(&qs).and_then(|t| push(&mut c, BaseGate::Z, vec![], t, 0))?, + "h" => require1(&qs).and_then(|t| push(&mut c, BaseGate::H, vec![], t, 0))?, + "s" => require1(&qs).and_then(|t| push(&mut c, BaseGate::S, vec![], t, 0))?, + "sdg" => require1(&qs).and_then(|t| push(&mut c, BaseGate::Sdg, vec![], t, 0))?, + "t" => require1(&qs).and_then(|t| push(&mut c, BaseGate::T, vec![], t, 0))?, + "tdg" => require1(&qs).and_then(|t| push(&mut c, BaseGate::Tdg, vec![], t, 0))?, + "cx" | "cnot" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::X, vec![a], t, 0))?, + "cy" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::Y, vec![a], t, 0))?, + "cz" => require2(&qs).and_then(|(a, t)| push(&mut c, BaseGate::Z, vec![a], t, 0))?, + "swap" => require2(&qs).and_then(|(a, b)| { + distinct_qubits(&[a], b)?; c.swap(a, b); + Ok(()) })?, - "ccx" | "toffoli" => require3(&qs).map(|(a, b, t)| push(&mut c, BaseGate::X, vec![a, b], t, 0))?, - "ccz" => require3(&qs).map(|(a, b, t)| push(&mut c, BaseGate::Z, vec![a, b], t, 0))?, + "ccx" | "toffoli" => require3(&qs).and_then(|(a, b, t)| push(&mut c, BaseGate::X, vec![a, b], t, 0))?, + "ccz" => require3(&qs).and_then(|(a, b, t)| push(&mut c, BaseGate::Z, vec![a, b], t, 0))?, "p" | "u1" | "phase" => { let t = parse_angle(&arg_n(&line, 0)?)?; let q = require1(&qs)?; - push_turn(&mut c, &[], q, t); + push_turn(&mut c, &[], q, t)?; } "cp" | "cu1" | "cphase" => { let t = parse_angle(&arg_n(&line, 0)?)?; let (a, q) = require2(&qs)?; - push_turn(&mut c, &[a], q, t); + push_turn(&mut c, &[a], q, t)?; } // Rotations. These are exact for dyadic angles but carry an // unobservable global phase relative to the textbook `r*` matrices: @@ -713,17 +736,17 @@ pub fn from_qasm(src: &str) -> Result { "rz" => { let t = parse_angle(&arg_n(&line, 0)?)?; let q = require1(&qs)?; - push_turn(&mut c, &[], q, t); + push_turn(&mut c, &[], q, t)?; } "rx" => { let t = parse_angle(&arg_n(&line, 0)?)?; let q = require1(&qs)?; - push_rx(&mut c, q, t); + push_rx(&mut c, q, t)?; } "ry" => { let t = parse_angle(&arg_n(&line, 0)?)?; let q = require1(&qs)?; - push_ry(&mut c, q, t); + push_ry(&mut c, q, t)?; } // `sx` is `rx(π/2)` — a quarter turn, so `P(2)` between two `H`s. "sx" | "sxdg" => { @@ -733,7 +756,7 @@ pub fn from_qasm(src: &str) -> Result { } else { Turn { m: 3, k: 2 } }; - push_rx(&mut c, q, t); + push_rx(&mut c, q, t)?; } // `u(θ,φ,λ) = rz(φ)·ry(θ)·rz(λ)`; the rightmost applies first. "u" | "u3" | "u2" => { @@ -748,9 +771,9 @@ pub fn from_qasm(src: &str) -> Result { ) }; let q = require1(&qs)?; - push_turn(&mut c, &[], q, lam); - push_ry(&mut c, q, theta); - push_turn(&mut c, &[], q, phi); + push_turn(&mut c, &[], q, lam)?; + push_ry(&mut c, q, theta)?; + push_turn(&mut c, &[], q, phi)?; } // Controlled-rz is exact with no phase caveat: `CP(θ)` preceded by // `P(−θ/2)` on the control reproduces `diag(1,1,e^{−iθ/2},e^{iθ/2})`. @@ -762,8 +785,9 @@ pub fn from_qasm(src: &str) -> Result { return Err("crz angle is finer than the dyadic floor".into()); } let (a, q) = require2(&qs)?; - push_turn(&mut c, &[], a, to_turn(-p, den * 2)); - push_turn(&mut c, &[a], q, to_turn(p, den)); + distinct_qubits(&[a], q)?; + push_turn(&mut c, &[], a, to_turn(-p, den * 2))?; + push_turn(&mut c, &[a], q, to_turn(p, den))?; } other => { return Err(format!( @@ -777,6 +801,20 @@ pub fn from_qasm(src: &str) -> Result { Ok(c) } +/// Refuse a gate that names one qubit twice, a control repeated or equal to +/// the target, as an invalid circuit in every gateset. +fn distinct_qubits(controls: &[u8], target: u8) -> Result<(), String> { + for (i, &q) in controls.iter().enumerate() { + if q == target { + return Err(QuantumError::Invalid(format!("control {q} equals target")).to_string()); + } + if controls[..i].contains(&q) { + return Err(QuantumError::Invalid(format!("control {q} repeated")).to_string()); + } + } + Ok(()) +} + fn strip(line: &str) -> String { let no_comment = line.split("//").next().unwrap_or(""); no_comment.trim().trim_end_matches(';').trim().to_string() @@ -1003,8 +1041,11 @@ fn arg_n(line: &str, i: usize) -> Result { .ok_or_else(|| format!("expected at least {} argument(s)", i + 1)) } -/// Emit an exact dyadic phase as `P` gates, one per set bit of `m`. -fn push_turn(c: &mut Circuit, controls: &[u8], target: u8, t: Turn) { +/// Emit an exact dyadic phase as `P` gates, one per set bit of `m`. A gate +/// naming one qubit twice is refused, even when the phase is a whole turn and +/// emits nothing. +fn push_turn(c: &mut Circuit, controls: &[u8], target: u8, t: Turn) -> Result<(), String> { + distinct_qubits(controls, target)?; for i in 0..=t.k { if (t.m >> i) & 1 == 1 { let k = t.k - i; @@ -1019,25 +1060,30 @@ fn push_turn(c: &mut Circuit, controls: &[u8], target: u8, t: Turn) { } } } + Ok(()) } /// `rx(θ) = H · rz(θ) · H`, exactly, in the dyadic basis. -fn push_rx(c: &mut Circuit, target: u8, t: Turn) { +fn push_rx(c: &mut Circuit, target: u8, t: Turn) -> Result<(), String> { c.h(target); - push_turn(c, &[], target, t); + push_turn(c, &[], target, t)?; c.h(target); + Ok(()) } /// `ry(θ) = S · rx(θ) · S†`. Circuit order applies the rightmost factor first. -fn push_ry(c: &mut Circuit, target: u8, t: Turn) { +fn push_ry(c: &mut Circuit, target: u8, t: Turn) -> Result<(), String> { let one = |b: BaseGate| Gate { base: b, controls: vec![], target, param: 0 }; c.ops.push(one(BaseGate::Sdg)); - push_rx(c, target, t); + push_rx(c, target, t)?; c.ops.push(one(BaseGate::S)); + Ok(()) } -/// The content hash of a circuit — the identity a run receipt binds. -pub fn circuit_id(c: &Circuit) -> [u8; 32] { +/// The content hash of a circuit — the identity a run receipt binds. It is +/// [`circuit_hash`], so it covers the circuit's gateset, and fails as that +/// does on a circuit its gateset does not allow. +pub fn circuit_id(c: &Circuit) -> Result<[u8; 32], QuantumError> { circuit_hash(c) } @@ -1356,4 +1402,53 @@ mod tests { assert!(is_clifford(&rc), "random Clifford must be Clifford"); assert_eq!(random_clifford(6, 60, 7), random_clifford(6, 60, 7), "deterministic in seed"); } + + /// A circuit derived from another keeps its gateset: a noisy trajectory + /// and a routed circuit run under the source's table, not the default. + #[test] + fn derived_circuits_keep_their_gateset() { + use crate::quantum::Gateset; + // H·T·H reads ⟨Z⟩ through the table, so the two gatesets give + // different expectations. + let htz = |gs| { + let mut c = Circuit::with_gateset(1, gs); + c.h(0).t(0).h(0); + c + }; + let exact = |gs| expect_z(&htz(gs).simulate().unwrap(), 0); + assert_ne!(exact(Gateset::V1).to_bits(), exact(Gateset::V2).to_bits()); + for gs in Gateset::ALL { + // No noise: each trajectory is the circuit itself. + assert_eq!(noisy_expect_z(&htz(gs), 0, 0, 3, 1).to_bits(), exact(gs).to_bits(), "{gs:?}"); + let mut src = Circuit::qft(5); + src.gateset = gs; + let r = route_line(&src); + assert_eq!(r.circuit.gateset, gs); + assert!(routing_equivalent(&src, &r)); + } + } + + /// A QASM gate naming one qubit twice is refused as an invalid circuit, + /// never built, whatever gateset the circuit is later given. + #[test] + fn qasm_gates_that_repeat_a_qubit_are_invalid() { + for (gate, want) in [ + ("ccx q[0],q[0],q[1];", "control 0 repeated"), + ("ccz q[2],q[2],q[0];", "control 2 repeated"), + ("cx q[1],q[1];", "control 1 equals target"), + ("cz q[0],q[0];", "control 0 equals target"), + ("ccx q[0],q[1],q[1];", "control 1 equals target"), + ("swap q[1],q[1];", "control 1 equals target"), + ("cp(pi/2) q[0],q[0];", "control 0 equals target"), + // A whole turn emits no gate, and is still a gate naming q[0] twice. + ("cp(0) q[0],q[0];", "control 0 equals target"), + ("crz(pi/2) q[2],q[2];", "control 2 equals target"), + ] { + let e = from_qasm(&format!("qreg q[3];\n{gate}")).expect_err(gate); + assert_eq!(e, format!("invalid circuit: {want}"), "{gate}"); + } + let ok = from_qasm("qreg q[3];\nccx q[0],q[1],q[2];\ncp(pi/2) q[0],q[2];").unwrap(); + assert_eq!(ok.gateset, crate::quantum::Gateset::V2); + assert_eq!(ok.ops.len(), 2); + } } diff --git a/wai/wai-quantum/src/receipt_pins.rs b/wai/wai-quantum/src/receipt_pins.rs index 9a7c7917e..1cd55ebd7 100644 --- a/wai/wai-quantum/src/receipt_pins.rs +++ b/wai/wai-quantum/src/receipt_pins.rs @@ -34,9 +34,10 @@ fn json_hash(j: &str) -> String { #[cfg(feature = "quantum_receipt")] #[test] fn quantum_circuit_legacy_bytes() { - use crate::quantum::{Circuit, Measure}; + use crate::quantum::{Circuit, Gateset, Measure}; use crate::quantum_receipt::QuantumReceipt; - let mut c = Circuit::new(3); + // Sealed by circuit/1, as every receipt before circuit2 was. + let mut c = Circuit::with_gateset(3, Gateset::V1); c.h(0).cx(0, 1).cx(1, 2); let r = QuantumReceipt::seal(&key(1), "did:key:pin", &c, Some(Measure { seed: 7, shots: 1024 }), 117_000, Some([0x21; 32])) .unwrap(); @@ -45,6 +46,38 @@ fn quantum_circuit_legacy_bytes() { assert_eq!(json_hash(&r.to_json()), "bc19d790962cecf2efea9d275f1b9494eb5a44bc1948e1f1bbaf2134f222b37d"); } +/// The same receipt sealed by circuit2. The three leaves are checked first. +/// The circuit and statevector leaves are the GHZ3 entries of the circuit2 +/// conformance corpus, and all three were computed by an integer-only +/// implementation that shares no code with this crate. The receipt and JSON +/// hashes were frozen only after the leaves matched, and must never be edited +/// to make this test pass. +#[cfg(feature = "quantum_receipt")] +#[test] +fn quantum_circuit_v2_bytes() { + use crate::quantum::{Circuit, Gateset, Measure}; + use crate::quantum_receipt::QuantumReceipt; + let mut c = Circuit::with_gateset(3, Gateset::V2); + c.h(0).cx(0, 1).cx(1, 2); + let r = QuantumReceipt::seal(&key(1), "did:key:pin", &c, Some(Measure { seed: 7, shots: 1024 }), 117_000, Some([0x21; 32])) + .unwrap(); + assert!(r.verify()); + assert!(r.verify_reconstruction(&c)); + assert_eq!(hx(&r.circuit_hash), "c5911b898a899c999ce01730bb825c84678569493ac513760e15dcbccd84227f"); + assert_eq!(hx(&r.statevector_hash), "51850f28817200e9e03980b60f34879c7bd0147a07b0c1746528b801cae2e2f8"); + let m = r.measurement.expect("the receipt vouches for a histogram"); + assert_eq!((m.seed, m.shots), (7, 1024)); + assert_eq!(hx(&m.histogram_hash), "8e119ee752f82a5035015472e751cf3a773ffe0d486b6541a14899a37264a8eb"); + assert_eq!(hx(&r.receipt_hash()), "102b06b6734a31bdc12eb33eae6694c20c048bb613f49800b066c59addf6134c"); + assert_eq!(json_hash(&r.to_json()), "8525056275ac518a959e8aadf7d76d7346bc317173eea67d9c617c0da1f51d59"); + // The circuit/1 twin of the same ops is a different circuit, so the + // receipt names circuit2 and refuses the twin. + let mut v1 = Circuit::with_gateset(3, Gateset::V1); + v1.h(0).cx(0, 1).cx(1, 2); + assert!(!r.verify_reconstruction(&v1)); + assert_eq!(r.gateset_of(&v1), Some(Gateset::V2)); +} + #[cfg(feature = "quantum_ops")] mod ops { use super::*; diff --git a/wai/wai-quantum/tests/bit_identity.rs b/wai/wai-quantum/tests/bit_identity.rs index 910f9723d..a3ffd2579 100644 --- a/wai/wai-quantum/tests/bit_identity.rs +++ b/wai/wai-quantum/tests/bit_identity.rs @@ -87,8 +87,8 @@ fn phasor_kernel() { assert_eq!(digest(&v), 0x58ba_1666_5ef6_fd9e); } -fn brickwork() -> quantum::Circuit { - let mut c = quantum::Circuit::new(8); +fn brickwork(gs: quantum::Gateset) -> quantum::Circuit { + let mut c = quantum::Circuit::with_gateset(8, gs); for l in 0..4u8 { for q in 0..8 { c.h(q); @@ -103,14 +103,24 @@ fn brickwork() -> quantum::Circuit { #[test] fn mps_truncation() { - let c = brickwork(); + let c = brickwork(quantum::Gateset::V1); let (mps, _) = quantum_mps::run(&c, 4).unwrap(); assert_eq!(digest(&[quantum_mps::fidelity(&mps, &c.simulate().unwrap())]), 0xa807_44ca_a97f_bff8); } +/// The same truncation against the circuit2 reference statevector. The MPS +/// itself is an ideal-angle simulation and does not read the phase table, so +/// only the reference it is compared with changes. +#[test] +fn mps_truncation_v2() { + let c = brickwork(quantum::Gateset::V2); + let (mps, _) = quantum_mps::run(&c, 4).unwrap(); + assert_eq!(digest(&[quantum_mps::fidelity(&mps, &c.simulate().unwrap())]), 0xe408_42e9_2c89_20ed); +} + #[test] fn sparse_pauli_propagation() { - let (e, _) = quantum_pauli::expect_z(&brickwork(), 3, 1e-6).unwrap(); + let (e, _) = quantum_pauli::expect_z(&brickwork(quantum::Gateset::V1), 3, 1e-6).unwrap(); assert_eq!(digest(&[e]), 0x8cc6_5829_1fb1_8909); } diff --git a/wai/wai-rs/Cargo.lock b/wai/wai-rs/Cargo.lock index b4b666cda..f43953910 100644 --- a/wai/wai-rs/Cargo.lock +++ b/wai/wai-rs/Cargo.lock @@ -3900,7 +3900,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-rs/Cargo.toml b/wai/wai-rs/Cargo.toml index 46ec1caa4..56d8aaba8 100644 --- a/wai/wai-rs/Cargo.toml +++ b/wai/wai-rs/Cargo.toml @@ -93,7 +93,7 @@ curve25519-dalek = { version = "4", optional = true } # ---- quantum (optional, behind the `quantum*` features) ---- # The quantum stack now lives in its own dependency-light crate so it can be used # without the media/codec chain. `wai` re-exports it unchanged. -wai-quantum = { version = "0.3.0", path = "../wai-quantum", optional = true, default-features = false } +wai-quantum = { version = "0.4.0", path = "../wai-quantum", optional = true, default-features = false } [features] default = ["codecs"] @@ -234,7 +234,8 @@ world_model = ["world", "provenance"] # (one envelope = movie + sound). No codec/ML deps (the render is the sink's). film = ["world", "score"] # Deterministic quantum-circuit SIMULATION (extensions/quantum-sim, -# `wai.quantum.circuit`): the wire carries a gate op-log (WQC container), the sink +# `wai.quantum.circuit2` and the frozen `wai.quantum.circuit`): the wire carries a +# gate op-log (WQC container), the sink # classically simulates it in a fixed-point complex floor, the statevector is # byte-identical on every machine (statevector-equivalence). Pure i64, no float — # blake3 for the conformance/receipt hashes, same discipline as the other floor diff --git a/wai/wai-rs/src/bin/wai_quantum.rs b/wai/wai-rs/src/bin/wai_quantum.rs index bdec6038e..a2073ede9 100644 --- a/wai/wai-rs/src/bin/wai_quantum.rs +++ b/wai/wai-rs/src/bin/wai_quantum.rs @@ -19,6 +19,12 @@ //! wai-quantum verify # read a receipt JSON on stdin //! ``` //! +//! A circuit is built in `wai.quantum.circuit2`, whose phase table is correctly +//! rounded. `--gateset v1` builds it in the frozen `wai.quantum.circuit` instead, +//! to reproduce or re-verify content made with it. Every command that builds a +//! circuit from an algorithm, OpenQASM or QIR (`run`, `receipt`, `qasm`, `qir`, +//! `entropy`) takes it, and so does `fabric`. +//! //! Beyond running circuits, the same binary answers what a state *carries* and //! what transport does to it: //! @@ -68,7 +74,7 @@ use wai_quantum::ed25519_dalek::SigningKey; use std::io::Read; -use wai::quantum::{Circuit, Measure, ONE}; +use wai::quantum::{BaseGate, Circuit, Gateset, Measure, ONE}; use wai::quantum_channel::{tomography, Channel}; use wai::quantum_comm::{bb84, clone_fidelity}; use wai::quantum_info::{chsh, DensityMatrix}; @@ -138,15 +144,16 @@ fn usage() -> ! { "wai-quantum — deterministic quantum computation (native + cloud WASM)\n\n\ USAGE:\n \ wai-quantum list\n \ - wai-quantum run [-n QUBITS] [-p PARAM] [--top K] [--json]\n \ - wai-quantum run --qasm [--top K] [--json] (OpenQASM on stdin)\n \ - wai-quantum run --qir [--top K] [--json] (QIR on stdin)\n \ - wai-quantum qasm [-n QUBITS] [-p PARAM]\n \ - wai-quantum qir [-n QUBITS] [-p PARAM] [--no-measure]\n \ - wai-quantum receipt [-n QUBITS] [-p PARAM] [--shots N] [--seed S]\n \ - wai-quantum verify (receipt JSON on stdin)\n\n\ + wai-quantum run [-n QUBITS] [-p PARAM] [--top K] [--json] [--gateset v1|v2]\n \ + wai-quantum run --qasm [--top K] [--json] [--gateset v1|v2] (OpenQASM on stdin)\n \ + wai-quantum run --qir [--top K] [--json] [--gateset v1|v2] (QIR on stdin)\n \ + wai-quantum qasm [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \ + wai-quantum qir [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \ + wai-quantum receipt [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2]\n \ + wai-quantum verify (receipt JSON on stdin)\n \ + --gateset v2 (the default) is wai.quantum.circuit2; v1 is the frozen wai.quantum.circuit.\n\n\ INFORMATION AND TRANSPORT:\n \ - wai-quantum entropy [-n QUBITS] [--keep 0,1] [--json]\n \ + wai-quantum entropy [-n QUBITS] [--keep 0,1] [--json] [--gateset v1|v2]\n \ wai-quantum channel [--prob P] [--state zero|one|plus] [--json]\n \ wai-quantum bb84 [--rounds N] [--seed S] [--eavesdrop] [--json]\n \ wai-quantum chsh [--json]\n \ @@ -181,7 +188,7 @@ fn usage() -> ! { [--odd]] [--alpha A] [--steps N] [--samples S] [--lr R]\n \ [--init W] [--guide ε] [--exact] [--seed S] [--json]\n\n\ THE FABRIC (members, parity against the CPU reference, placement):\n \ - wai-quantum fabric [--circuits N] [--seed S] [--json]\n\n\ + wai-quantum fabric [--circuits N] [--seed S] [--gateset v1|v2] [--json]\n\n\ OPTIMISATION (simulated bifurcation, annealing, exact):\n \ wai-quantum qio (--gset FILE | --sk N [--instance S]) [--method dsb|bsb|hdsb|hbsb|sa|exact]\n \ [--steps N] [--trials T] [--dt D] [--c1 C] [--gamma G] [--beta0 B --beta1 B] [--descend]\n \ @@ -231,21 +238,43 @@ fn num(args: &[String], long: &str, short: &str, dflt: T) } } +/// What `--gateset v1` prints on standard error, never on standard output or in +/// JSON, for a circuit whose phases circuit/1 does not reproduce. +const V1_NOTE: &str = + "circuit/1 table: P(k≥8) deviates by up to 12868 units of 2^-30, P(k≥19) applies no phase; use --gateset v2"; + +/// The gateset `--gateset` names: `v2` (`wai.quantum.circuit2`, the default) or +/// `v1` (the frozen `wai.quantum.circuit`). +fn gateset_from(args: &[String]) -> Gateset { + match opt(args, "--gateset", "").as_deref() { + None | Some("v2") => Gateset::V2, + Some("v1") => Gateset::V1, + Some(other) => die(&format!("unknown gateset '{other}' (v1, v2)")), + } +} + /// Build the circuit a command operates on: either a named algorithm or OpenQASM -/// read from stdin (`--qasm`). +/// read from stdin (`--qasm`), in the gateset `--gateset` names. A circuit/1 +/// circuit with a `P(k ≥ 8)` gets the note on standard error. fn circuit_from(args: &[String], algo: Option<&String>) -> Circuit { - if flag(args, "--qasm") { - return qt::from_qasm(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}"))); + let gs = gateset_from(args); + let mut c = if flag(args, "--qasm") { + qt::from_qasm(&read_stdin()).unwrap_or_else(|e| die(&format!("QASM parse: {e}"))) + } else if flag(args, "--qir") { + wai::quantum_qir::from_qir(&read_stdin()).unwrap_or_else(|e| die(&format!("QIR parse: {e}"))) + } else { + let id = algo.unwrap_or_else(|| usage()); + let n: u8 = num(args, "--qubits", "-n", 3); + let p: u32 = num(args, "--param", "-p", 0); + qt::build_algorithm(id, n, p) + .unwrap_or_else(|| die(&format!("unknown algorithm '{id}' (try `wai-quantum list`)"))) + }; + // Every circuit2 circuit is a valid circuit/1 circuit, so the switch is safe. + c.gateset = gs; + if gs == Gateset::V1 && c.ops.iter().any(|g| g.base == BaseGate::P && g.param >= 8) { + eprintln!("{V1_NOTE}"); } - if flag(args, "--qir") { - return wai::quantum_qir::from_qir(&read_stdin()) - .unwrap_or_else(|e| die(&format!("QIR parse: {e}"))); - } - let id = algo.unwrap_or_else(|| usage()); - let n: u8 = num(args, "--qubits", "-n", 3); - let p: u32 = num(args, "--param", "-p", 0); - qt::build_algorithm(id, n, p) - .unwrap_or_else(|| die(&format!("unknown algorithm '{id}' (try `wai-quantum list`)"))) + c } fn describe(c: &Circuit) -> (String, String, u64, String) { @@ -254,7 +283,7 @@ fn describe(c: &Circuit) -> (String, String, u64, String) { format!("{backend:?}"), why.to_string(), qt::statevector_work(c), - hx(&qt::circuit_id(c)), + hx(&qt::circuit_id(c).unwrap_or_else(|e| die(&format!("circuit id: {e}")))), ) } @@ -451,14 +480,16 @@ fn main() { .map(|(k, p)| format!("{{\"state\":\"{k:0n$b}\",\"p\":{p}}}", n = n)) .collect(); println!( - "{{\"circuit_id\":\"{cid}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend}\",\ + "{{\"circuit_id\":\"{cid}\",\"capability\":\"{}\",\"n_qubits\":{},\"n_gates\":{},\"backend\":\"{backend}\",\ \"work_amp_updates\":{work},\"statevector_hash\":\"{hash}\",\"top\":[{}]}}", + c.gateset.capability(), c.n_qubits, c.ops.len(), items.join(",") ); } else { println!("circuit-id {cid}"); + println!("capability {}", c.gateset.capability()); println!("qubits {}", c.n_qubits); println!("gates {}", c.ops.len()); println!("backend {backend} ({why})"); @@ -1305,17 +1336,20 @@ fn main() { // The fabric this process stands on: its detected members, and the // fixed-point core's 32-bit-lane port certified against the native - // simulator on random circuits, then placed. The member lines and the - // certificate's member describe the host, so they differ between - // platforms; the parity and the corpus digest do not. + // simulator on random circuits of the gateset `--gateset` names, then + // placed. The member lines and the certificate's member describe the + // host, so they differ between platforms; the parity and the corpus + // digest do not. "fabric" => { use wai::quantum_fabric::*; let circuits: usize = num(args, "--circuits", "", 200); let seed: u64 = num(args, "--seed", "", 7); + let gs = gateset_from(args); + let kernel = apply_kernel(gs); let mut member = detect_local(); - let cert = certify_lanes(&member.id, circuits, seed); - member.kernels.push(KernelEntry { kernel: KERNEL_CIRCUIT_APPLY.into(), implementation: "lanes32".into(), certificate: cert, energy: None }); - let placed = place(std::slice::from_ref(&member), KERNEL_CIRCUIT_APPLY, &Parity::BitExact); + let cert = certify_lanes(gs, &member.id, circuits, seed); + member.kernels.push(KernelEntry { kernel: kernel.into(), implementation: "lanes32".into(), certificate: cert, energy: None }); + let placed = place(std::slice::from_ref(&member), kernel, &Parity::BitExact); let parity = |p: &Parity| match p { Parity::BitExact => "bit-exact".to_string(), Parity::Within { max_ulp, max_abs } => format!("within {max_ulp} ulp ({max_abs:.3e})"), @@ -1330,7 +1364,8 @@ fn main() { .map(|k| format!("{{\"implementation\":{:?},\"kernel\":{:?},\"parity\":{:?},\"generator\":{:?},\"cases\":{},\"corpus\":\"{}\"}}", k.implementation, k.kernel, parity(&k.certificate.parity), k.certificate.generator, k.certificate.cases, hex(&k.certificate.corpus))) .collect(); println!( - "{{\"member\":{:?},\"class\":\"{}\",\"presence\":\"{}\",\"detail\":{:?},\"kernels\":[{}],\"placed\":{}}}", + "{{\"capability\":\"{}\",\"member\":{:?},\"class\":\"{}\",\"presence\":\"{}\",\"detail\":{:?},\"kernels\":[{}],\"placed\":{}}}", + gs.capability(), member.id, member.class.label(), member.presence.label(), @@ -1341,8 +1376,8 @@ fn main() { } else { println!("member {} ({}, {})", member.id, member.class.label(), member.presence.label()); println!("detail {}", member.detail); - println!("kernel {KERNEL_CIRCUIT_APPLY}"); - for k in &member.kernels { + println!("kernel {kernel}"); + for k in member.kernels.iter().filter(|k| k.kernel == kernel) { if k.implementation == REFERENCE { println!(" {:<11}{} (the reference)", k.implementation, parity(&k.certificate.parity)); } else { diff --git a/wai/wai-rs/src/bin/wai_quantum_meter.rs b/wai/wai-rs/src/bin/wai_quantum_meter.rs index a9898193a..f2b7e693e 100644 --- a/wai/wai-rs/src/bin/wai_quantum_meter.rs +++ b/wai/wai-rs/src/bin/wai_quantum_meter.rs @@ -1,5 +1,8 @@ //! `wai_quantum_meter` — the native sink-metering path for a -//! `wai.quantum.circuit` reconstruction's energy (extensions/quantum-sim § Receipt). +//! `wai.quantum.circuit2` or `wai.quantum.circuit` reconstruction's energy +//! (extensions/quantum-sim § Receipt). The circuit is read from its `WQC2` or +//! `WQC1` container and keeps that container's gateset, so the receipt's +//! circuit hash binds the capability the file was written under. //! //! Measures the **real CPU energy** a classical sink spends reconstructing a WQC //! circuit's statevector, with no elevated privileges. On arm64 macOS the SoC @@ -62,8 +65,9 @@ fn main() { let work = work_amp_updates(n, n_ops); eprintln!( - "metering {path} ({n} qubits, {n_ops} ops, work {work} amp-updates), \ - no sudo — IOReport via macmon" + "metering {path} ({}, {n} qubits, {n_ops} ops, work {work} amp-updates), \ + no sudo — IOReport via macmon", + circuit.gateset.capability() ); // The §3 protocol (energy_burst): an idle bracket, then reconstructions on diff --git a/wai/wai-rs/src/bin/wai_quantum_vectors.rs b/wai/wai-rs/src/bin/wai_quantum_vectors.rs index e665bdbe4..9a62f6cc4 100644 --- a/wai/wai-rs/src/bin/wai_quantum_vectors.rs +++ b/wai/wai-rs/src/bin/wai_quantum_vectors.rs @@ -5,18 +5,61 @@ //! hash and statevector hash (and, where a measurement is pinned, a shot-histogram //! hash — shot-histogram-equivalence). //! -//! wai_quantum_vectors gen | verify +//! The corpus has four trees: +//! +//! - `/`: `wai.quantum.circuit` (`WQC1`), `vectors/*.wqc` and +//! `expected_hashes.json`. The four original vectors are frozen; three +//! table-sensitive vectors follow them. An entry with no `capability` is a +//! `wai.quantum.circuit` vector, as every entry of the original manifest is. +//! - `/v2/`: `wai.quantum.circuit2` (`WQC2`), the same layout and the same +//! seven circuits, each entry naming its `capability`. +//! - `/reject/`: containers a conforming reader MUST refuse, and +//! `reject.json`, which maps each file to the class it is refused as +//! (`malformed`, `invalid` or `contract`). +//! - `/accept/`: containers a conforming reader MUST accept, at the edges +//! of the reader's rules, and `accept.json`, which gives each one's reading: +//! its capability, `n_qubits`, op count, `circuit_hash` and measurement. +//! +//! `verify` checks all four: each vector decodes to the capability its entry +//! names and reproduces its hashes, each reject file fails with its class, and +//! each accept file reads as listed. +//! +//! `appendix` writes the phase tables of the extension's Appendix B, rendered +//! from the reference constants, into the generated part of the file it is +//! given (`extensions/quantum-sim.md`), so the tables are never typed by hand. +//! +//! wai_quantum_vectors gen | verify | appendix +use std::collections::BTreeSet; use std::path::Path; -use wai::quantum::{circuit_hash, from_wqc, to_wqc, Circuit, Measure}; +use wai::quantum::{accept_corpus, circuit_hash, from_wqc, reject_corpus, to_wqc, BaseGate, Circuit, Gate, Gateset, Measure, DYADIC_MAX}; + +/// The circuit2 tree, under the corpus directory. +const V2_DIR: &str = "v2"; +/// The reject tree, under the corpus directory. +const REJECT_DIR: &str = "reject"; +/// The reject tree's listing: file → class. +const REJECT_LISTING: &str = "reject.json"; +/// The accept tree, under the corpus directory. +const ACCEPT_DIR: &str = "accept"; +/// The accept tree's listing: file → reading. +const ACCEPT_LISTING: &str = "accept.json"; fn main() { let a: Vec = std::env::args().collect(); match (a.get(1).map(String::as_str), a.get(2)) { (Some("gen"), Some(d)) => generate(Path::new(d)), - (Some("verify"), Some(d)) => verify(Path::new(d)), + (Some("verify"), Some(d)) => { + let fails = verify(Path::new(d)); + if fails > 0 { + eprintln!("{fails} failed — NOT conformant"); + std::process::exit(1); + } + println!("all vectors pass: statevector-equivalence holds, every reject file is refused as listed, and every accept file reads as listed"); + } + (Some("appendix"), Some(f)) => write_appendix(Path::new(f)), _ => { - eprintln!("usage: wai_quantum_vectors "); + eprintln!("usage: wai_quantum_vectors | wai_quantum_vectors appendix "); std::process::exit(2); } } @@ -26,81 +69,486 @@ fn hex(b: &[u8]) -> String { b.iter().map(|x| format!("{x:02x}")).collect() } -/// The canonical vectors: a Bell pair, a 3-qubit GHZ, and QFTs of 3 and 5 qubits -/// (the "quantum FFT" — Hadamard + controlled-phase ladder + swaps). The Bell pair +/// The canonical vectors, in the gateset `gs`. +/// +/// The first four: a Bell pair, a 3-qubit GHZ, and QFTs of 3 and 5 qubits (the +/// "quantum FFT" — Hadamard + controlled-phase ladder + swaps). The Bell pair /// also pins a measurement so the corpus exercises shot-histogram-equivalence. -fn vectors() -> Vec<(&'static str, Circuit, Option)> { - let mut bell = Circuit::new(2); +/// Every controlled phase of a QFT from |0…0⟩ acts while its control is |0⟩, so +/// these four see `H` but no `P(k)`. +/// +/// The three that follow see the phase table. `qft5_x22` and `qft8_ones` run +/// the QFT from a basis state with set bits. `dyadic_ladder` sets qubit 0, +/// applies Y, Z, S, S†, T, T† and I to qubit 1, and then every `P(1..=32)`, +/// twice, to qubit 1 under qubit 0 as control, so a change of ±1 to either +/// component of any table entry changes its statevector hash. +fn vectors(gs: Gateset) -> Vec<(&'static str, Circuit, Option)> { + let mut bell = Circuit::with_gateset(2, gs); bell.h(0).cx(0, 1); - let mut ghz = Circuit::new(3); + let mut ghz = Circuit::with_gateset(3, gs); ghz.h(0).cx(0, 1).cx(1, 2); + let qft = |n: u8| { + let mut c = Circuit::qft(n); + c.gateset = gs; + c + }; + let x_then_qft = |n: u8, xs: &[u8]| { + let mut c = Circuit::with_gateset(n, gs); + for &q in xs { + c.x(q); + } + c.ops.extend(Circuit::qft(n).ops); + c + }; + + let mut ladder = Circuit::with_gateset(2, gs); + ladder.x(0).y(1).z(1).s(1); + let one = |base| Gate { base, controls: vec![], target: 1, param: 0 }; + ladder.ops.push(one(BaseGate::Sdg)); + ladder.t(1); + ladder.ops.push(one(BaseGate::Tdg)); + ladder.ops.push(one(BaseGate::I)); + for _ in 0..2 { + for k in 1..=32 { + ladder.cp(k, 0, 1); + } + } + vec![ ("bell", bell, Some(Measure { seed: 0xC0FFEE, shots: 100_000 })), ("ghz3", ghz, None), - ("qft3", Circuit::qft(3), None), - ("qft5", Circuit::qft(5), None), + ("qft3", qft(3), None), + ("qft5", qft(5), None), + ("qft5_x22", x_then_qft(5, &[1, 2, 4]), None), + ("dyadic_ladder", ladder, None), + ("qft8_ones", x_then_qft(8, &[0, 1, 2, 3, 4, 5, 6, 7]), None), ] } -fn generate(dir: &Path) { - let v = dir.join("vectors"); - std::fs::create_dir_all(&v).expect("mkdir"); +/// One vector tree: `vectors/.wqc` for each vector and its +/// `expected_hashes.json`, as (path under `prefix`, bytes). With +/// `name_capability`, each entry names its capability first. +fn tree_files(prefix: &str, gs: Gateset, name_capability: bool) -> Vec<(String, Vec)> { + let mut files = Vec::new(); let mut manifest = serde_json::Map::new(); - for (name, circuit, measure) in vectors() { - let bytes = to_wqc(&circuit, measure); - std::fs::write(v.join(format!("{name}.wqc")), &bytes).expect("write wqc"); + for (name, circuit, measure) in vectors(gs) { + let bytes = to_wqc(&circuit, measure).expect("a corpus circuit is valid"); + files.push((format!("{prefix}vectors/{name}.wqc"), bytes)); let sv = circuit.simulate().expect("simulate"); - let mut entry = serde_json::json!({ - "n_qubits": circuit.n_qubits, - "n_ops": circuit.ops.len(), - "circuit_hash": hex(&circuit_hash(&circuit)), - "statevector_hash": hex(&sv.statevector_hash()), - }); - if let Some(m) = measure { - entry["measure"] = serde_json::json!({ - "seed": m.seed, - "shots": m.shots, - "histogram_hash": hex(&sv.histogram_hash(m.seed, m.shots)), - }); + let mut entry = serde_json::Map::new(); + if name_capability { + entry.insert("capability".into(), gs.capability().into()); } - manifest.insert(format!("{name}.wqc"), entry); + entry.insert("n_qubits".into(), circuit.n_qubits.into()); + entry.insert("n_ops".into(), circuit.ops.len().into()); + entry.insert("circuit_hash".into(), hex(&circuit_hash(&circuit).expect("a corpus circuit is valid")).into()); + entry.insert("statevector_hash".into(), hex(&sv.statevector_hash()).into()); + if let Some(m) = measure { + entry.insert( + "measure".into(), + serde_json::json!({ + "seed": m.seed, + "shots": m.shots, + "histogram_hash": hex(&sv.histogram_hash(m.seed, m.shots)), + }), + ); + } + manifest.insert(format!("{name}.wqc"), entry.into()); + } + let text = serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n"; + files.push((format!("{prefix}expected_hashes.json"), text.into_bytes())); + files +} + +/// The reject tree: `.wqc` for each case and the listing, as (path under +/// `prefix`, bytes). Each case is checked here to fail with its class, so `gen` +/// never writes a reject file the reader accepts. +fn reject_files(prefix: &str) -> Vec<(String, Vec)> { + let mut files = Vec::new(); + let mut listing = serde_json::Map::new(); + for (name, bytes, kind) in reject_corpus() { + match from_wqc(&bytes) { + Ok(_) => panic!("reject case {name} is accepted"), + Err(e) => assert_eq!(e.kind(), kind, "reject case {name}: {e}"), + } + listing.insert(format!("{name}.wqc"), kind.into()); + files.push((format!("{prefix}{name}.wqc"), bytes)); + } + let text = serde_json::to_string_pretty(&serde_json::Value::Object(listing)).unwrap() + "\n"; + files.push((format!("{prefix}{REJECT_LISTING}"), text.into_bytes())); + files +} + +/// How a container reads, as an accept listing states it. +fn reading(c: &Circuit, m: Option) -> serde_json::Value { + serde_json::json!({ + "capability": c.gateset.capability(), + "n_qubits": c.n_qubits, + "n_ops": c.ops.len(), + "circuit_hash": hex(&circuit_hash(c).expect("an accepted circuit hashes")), + "measure": m.map(|m| serde_json::json!({ "seed": m.seed, "shots": m.shots })), + }) +} + +/// The accept tree: `.wqc` for each case and the listing of its reading, +/// as (path under `prefix`, bytes). The listing is the reading the case +/// states, and each case is checked here to read so, so `gen` never writes an +/// accept file the reader refuses or misreads. +fn accept_files(prefix: &str) -> Vec<(String, Vec)> { + let mut files = Vec::new(); + let mut listing = serde_json::Map::new(); + for (name, bytes, circuit, measure) in accept_corpus() { + match from_wqc(&bytes) { + Ok(got) => assert_eq!(got, (circuit.clone(), measure), "accept case {name}"), + Err(e) => panic!("accept case {name} is refused: {e}"), + } + listing.insert(format!("{name}.wqc"), reading(&circuit, measure)); + files.push((format!("{prefix}{name}.wqc"), bytes)); + } + let text = serde_json::to_string_pretty(&serde_json::Value::Object(listing)).unwrap() + "\n"; + files.push((format!("{prefix}{ACCEPT_LISTING}"), text.into_bytes())); + files +} + +/// Every file `gen` writes, as (path under the corpus directory, bytes). +fn corpus_files() -> Vec<(String, Vec)> { + let mut files = tree_files("", Gateset::V1, false); + files.extend(tree_files(&format!("{V2_DIR}/"), Gateset::V2, true)); + files.extend(reject_files(&format!("{REJECT_DIR}/"))); + files.extend(accept_files(&format!("{ACCEPT_DIR}/"))); + files +} + +fn generate(dir: &Path) { + for (rel, bytes) in corpus_files() { + let path = dir.join(&rel); + std::fs::create_dir_all(path.parent().expect("a file has a directory")).expect("mkdir"); + std::fs::write(&path, &bytes).expect("write"); } - std::fs::write( - dir.join("expected_hashes.json"), - serde_json::to_string_pretty(&serde_json::Value::Object(manifest)).unwrap() + "\n", - ) - .expect("manifest"); println!("generated quantum vectors into {}", dir.display()); } -fn verify(dir: &Path) { - let m: serde_json::Value = - serde_json::from_slice(&std::fs::read(dir.join("expected_hashes.json")).expect("manifest")).expect("parse"); - let v = dir.join("vectors"); - let mut fails = 0; - for (file, spec) in m.as_object().expect("obj") { - let (circuit, measure) = from_wqc(&std::fs::read(v.join(file)).expect("wqc")).expect("parse"); - let sv = circuit.simulate().expect("simulate"); - let circ_ok = hex(&circuit_hash(&circuit)) == spec["circuit_hash"].as_str().unwrap(); - let sv_ok = hex(&sv.statevector_hash()) == spec["statevector_hash"].as_str().unwrap(); - let hist_ok = match (measure, spec.get("measure")) { - (Some(mm), Some(ms)) => { - hex(&sv.histogram_hash(mm.seed, mm.shots)) == ms["histogram_hash"].as_str().unwrap() - } - (None, None) => true, - _ => false, - }; - let ok = circ_ok && sv_ok && hist_ok; - println!("{} {file}", if ok { "PASS" } else { "FAIL" }); - if !ok { - fails += 1; +// ---- Appendix B of the extension --------------------------------------------------- + +/// The markers around the generated part of the extension's Appendix B. +const APPENDIX_BEGIN: &str = ""; +const APPENDIX_END: &str = ""; + +/// The phase tables of Appendix B, rendered from the reference constants: for +/// each gateset, oldest first, a subsection `B.` named by its capability, +/// `P(1..=DYADIC_MAX)` as `(re, im)`, and the table's digest. +fn appendix_tables() -> String { + let mut o = String::from("\n"); + for (i, gs) in Gateset::ALL.into_iter().enumerate() { + o.push_str(&format!("### B.{} `{}`\n\n", i + 1, gs.capability())); + o.push_str("| k | re P(k) | im P(k) |\n|---:|---:|---:|\n"); + for k in 1..=DYADIC_MAX { + let p = gs.phase(k); + o.push_str(&format!("| {k} | {} | {} |\n", p.re, p.im)); + } + o.push_str(&format!("\nDigest: `{}`\n\n", hex(&gs.table_digest()))); + } + o +} + +/// `text` with the generated part of Appendix B replaced by +/// [`appendix_tables`], or `None` when it lacks the markers. +fn splice_appendix(text: &str) -> Option { + let b = text.find(APPENDIX_BEGIN)? + APPENDIX_BEGIN.len(); + let e = text[b..].find(APPENDIX_END)? + b; + Some(format!("{}\n{}{}", &text[..b], appendix_tables(), &text[e..])) +} + +fn write_appendix(path: &Path) { + let text = std::fs::read_to_string(path).unwrap_or_else(|e| { + eprintln!("{}: {e}", path.display()); + std::process::exit(1); + }); + let Some(new) = splice_appendix(&text) else { + eprintln!("{}: no generated Appendix B markers ({APPENDIX_BEGIN} … {APPENDIX_END})", path.display()); + std::process::exit(1); + }; + if new == text { + println!("{}: Appendix B is current", path.display()); + } else { + std::fs::write(path, new).expect("write"); + println!("{}: Appendix B rewritten from the phase tables", path.display()); + } +} + +// ---- Verification ---------------------------------------------------------------- + +/// Verify all four trees under `dir`; the number of failures. +fn verify(dir: &Path) -> usize { + verify_tree(dir, "") + + verify_tree(&dir.join(V2_DIR), &format!("{V2_DIR}/")) + + verify_rejects(&dir.join(REJECT_DIR), &format!("{REJECT_DIR}/")) + + verify_accepts(&dir.join(ACCEPT_DIR), &format!("{ACCEPT_DIR}/")) +} + +/// The `.wqc` file names in `dir`. +fn wqc_names(dir: &Path) -> Result, String> { + let entries = std::fs::read_dir(dir).map_err(|e| format!("{}: {e}", dir.display()))?; + let mut names = BTreeSet::new(); + for e in entries { + let name = e.map_err(|e| e.to_string())?.file_name().to_string_lossy().into_owned(); + if name.ends_with(".wqc") { + names.insert(name); } } - if fails > 0 { - eprintln!("{fails} failed — NOT conformant"); - std::process::exit(1); + Ok(names) +} + +/// A JSON object read from `path`. +fn read_object(path: &Path) -> Result, String> { + let bytes = std::fs::read(path).map_err(|e| format!("{}: {e}", path.display()))?; + match serde_json::from_slice(&bytes).map_err(|e| format!("{}: {e}", path.display()))? { + serde_json::Value::Object(o) => Ok(o), + _ => Err(format!("{}: not a JSON object", path.display())), } - println!("all vectors pass: statevector-equivalence holds"); +} + +/// Report one check: `PASS` or `FAIL` with the reason; 1 for a failure. +fn report(label: &str, r: Result) -> usize { + match r { + Ok(note) => { + println!("PASS {label}{note}"); + 0 + } + Err(why) => { + println!("FAIL {label}: {why}"); + 1 + } + } +} + +/// Each listed file and only those are present. +fn same_files(listed: BTreeSet, present: Result, String>) -> Result { + let present = present?; + let missing: Vec<_> = listed.difference(&present).collect(); + let unlisted: Vec<_> = present.difference(&listed).collect(); + if missing.is_empty() && unlisted.is_empty() { + Ok(String::new()) + } else { + Err(format!("listed but missing {missing:?}, present but unlisted {unlisted:?}")) + } +} + +/// A vector tree: `expected_hashes.json` and `vectors/`. +fn verify_tree(dir: &Path, label: &str) -> usize { + let m = match read_object(&dir.join("expected_hashes.json")) { + Ok(m) => m, + Err(why) => return report(&format!("{label}expected_hashes.json"), Err(why)), + }; + let v = dir.join("vectors"); + let mut fails = report(&format!("{label}vectors/ holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(&v))); + for (file, spec) in &m { + fails += report(&format!("{label}{file}"), check_vector(&v.join(file), spec)); + } + fails +} + +/// One vector against its manifest entry. +fn check_vector(path: &Path, spec: &serde_json::Value) -> Result { + let field = |k: &str| spec.get(k).ok_or(format!("the entry has no {k}")); + let text = |k: &str| field(k).and_then(|v| v.as_str().ok_or(format!("{k} is not a string"))); + let number = |v: &serde_json::Value, k: &str| v.get(k).and_then(|x| x.as_u64()).ok_or(format!("{k} is not an unsigned integer")); + // The original manifest names no capability: its entries are circuit/1. + let capability = match spec.get("capability") { + None => Gateset::V1.capability(), + Some(c) => c.as_str().ok_or("capability is not a string")?, + }; + let gs = Gateset::from_capability(capability).ok_or(format!("unknown capability {capability:?}"))?; + let bytes = std::fs::read(path).map_err(|e| e.to_string())?; + let (circuit, measure) = from_wqc(&bytes).map_err(|e| e.to_string())?; + if circuit.gateset != gs { + return Err(format!("the file is {}, the entry names {capability}", circuit.gateset.capability())); + } + if u64::from(circuit.n_qubits) != number(spec, "n_qubits")? || circuit.ops.len() as u64 != number(spec, "n_ops")? { + return Err("n_qubits or n_ops differ from the entry".into()); + } + let sv = circuit.simulate().map_err(|e| e.to_string())?; + let mut bad = Vec::new(); + if hex(&circuit_hash(&circuit).map_err(|e| e.to_string())?) != text("circuit_hash")? { + bad.push("circuit_hash"); + } + if hex(&sv.statevector_hash()) != text("statevector_hash")? { + bad.push("statevector_hash"); + } + match (measure, spec.get("measure")) { + (Some(mm), Some(ms)) => { + let hist = ms.get("histogram_hash").and_then(|h| h.as_str()).ok_or("histogram_hash is not a string")?; + if mm.seed != number(ms, "seed")? || mm.shots != number(ms, "shots")? { + bad.push("measure seed or shots"); + } + if hex(&sv.histogram_hash(mm.seed, mm.shots)) != hist { + bad.push("histogram_hash"); + } + } + (None, None) => {} + _ => bad.push("measure (present in only one of the file and the entry)"), + } + if bad.is_empty() { Ok(String::new()) } else { Err(format!("{} differ", bad.join(", "))) } +} + +/// The reject tree: every listed file fails with its listed class. +fn verify_rejects(dir: &Path, label: &str) -> usize { + let m = match read_object(&dir.join(REJECT_LISTING)) { + Ok(m) => m, + Err(why) => return report(&format!("{label}{REJECT_LISTING}"), Err(why)), + }; + let mut fails = report(&format!("{label} holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(dir))); + for (file, kind) in &m { + let r = (|| -> Result { + let kind = kind.as_str().ok_or("the class is not a string")?; + let bytes = std::fs::read(dir.join(file)).map_err(|e| e.to_string())?; + match from_wqc(&bytes) { + Ok(_) => Err(format!("accepted; it must be refused as {kind}")), + Err(e) if e.kind() == kind => Ok(format!(" (refused: {kind})")), + Err(e) => Err(format!("refused as {}, not {kind}: {e}", e.kind())), + } + })(); + fails += report(&format!("{label}{file}"), r); + } + fails +} + +/// The accept tree: every listed file is accepted and reads as listed. +fn verify_accepts(dir: &Path, label: &str) -> usize { + let m = match read_object(&dir.join(ACCEPT_LISTING)) { + Ok(m) => m, + Err(why) => return report(&format!("{label}{ACCEPT_LISTING}"), Err(why)), + }; + let mut fails = report(&format!("{label} holds the listed files"), same_files(m.keys().cloned().collect(), wqc_names(dir))); + for (file, want) in &m { + let r = (|| -> Result { + let bytes = std::fs::read(dir.join(file)).map_err(|e| e.to_string())?; + let (c, measure) = from_wqc(&bytes).map_err(|e| format!("refused as {}: {e}", e.kind()))?; + let got = reading(&c, measure); + if &got == want { Ok(" (accepted, read as listed)".into()) } else { Err(format!("read as {got}, the listing says {want}")) } + })(); + fails += report(&format!("{label}{file}"), r); + } + fails +} + +#[cfg(test)] +mod tests { + use super::*; + use std::path::PathBuf; + + fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance") + } + + /// A scratch copy of what `gen` writes, removed when dropped. + struct Scratch(PathBuf); + impl Scratch { + fn new(name: &str) -> Scratch { + let dir = std::env::temp_dir().join(format!("wai_quantum_vectors-{name}-{}", std::process::id())); + let _ = std::fs::remove_dir_all(&dir); + generate(&dir); + Scratch(dir) + } + } + impl Drop for Scratch { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(&self.0); + } + } + + /// The committed corpus is what `gen` writes today, file for file and byte + /// for byte, and it verifies. + #[test] + fn the_committed_corpus_is_what_gen_writes() { + let dir = corpus(); + let files = corpus_files(); + // Two trees of seven vectors and a manifest, and the reject and accept + // trees: a file per case and the listing. + assert_eq!(files.len(), 2 * 8 + reject_corpus().len() + 1 + accept_corpus().len() + 1); + for (rel, want) in files { + let mut got = std::fs::read(dir.join(&rel)).unwrap_or_else(|e| panic!("{rel}: {e}")); + if rel.ends_with(".json") { + // A checkout may convert line endings of text files. + got = String::from_utf8(got).unwrap().replace("\r\n", "\n").into_bytes(); + } + assert!(got == want, "{rel} differs from what gen writes"); + } + assert_eq!(verify(&dir), 0); + } + + /// The extension's Appendix B is what the reference constants render, and + /// each table's digest, which §3.1 and §3.2 also state, appears there and + /// in the appendix and nowhere else. + #[test] + fn the_extension_appendix_is_the_tables() { + let path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../extensions/quantum-sim.md"); + // A checkout may convert line endings of text files. + let text = std::fs::read_to_string(&path).unwrap().replace("\r\n", "\n"); + let want = splice_appendix(&text).expect("the extension has the generated markers"); + assert!( + text == want, + "Appendix B of extensions/quantum-sim.md differs from the phase tables: \ + run `wai_quantum_vectors appendix ../extensions/quantum-sim.md` from wai/wai-rs" + ); + for gs in Gateset::ALL { + let digest = hex(&gs.table_digest()); + assert_eq!(text.matches(digest.as_str()).count(), 2, "{} digest {digest}", gs.capability()); + } + let tables = appendix_tables(); + assert!(tables.contains("### B.1 `wai.quantum.circuit`\n")); + assert!(tables.contains("### B.2 `wai.quantum.circuit2`\n")); + assert!(tables.contains("| 3 | 759250124 | 759250124 |\n")); + assert!(tables.contains("| 22 | 1073741824 | 1608 |\n")); + let rows = tables.lines().filter(|l| l.starts_with("| ") && l.as_bytes()[2].is_ascii_digit()).count(); + assert_eq!(rows, Gateset::ALL.len() * DYADIC_MAX); + } + + /// `verify` fails a corpus whose v2 entry names the other capability, whose + /// reject listing names an accepted file or the wrong class, whose accept + /// listing gives another reading or names a refused file, or that holds a + /// file its listing does not. + #[test] + fn verify_fails_what_it_should() { + let s = Scratch::new("verify"); + assert_eq!(verify(&s.0), 0); + let edit = |rel: &str, from: &str, to: &str| { + let p = s.0.join(rel); + let text = std::fs::read_to_string(&p).unwrap(); + assert!(text.contains(from), "{from}"); + std::fs::write(&p, text.replacen(from, to, 1)).unwrap(); + }; + // A circuit2 file under a circuit/1 entry. + edit("v2/expected_hashes.json", "\"wai.quantum.circuit2\"", "\"wai.quantum.circuit\""); + assert_eq!(verify(&s.0), 1); + edit("v2/expected_hashes.json", "\"wai.quantum.circuit\"", "\"wai.quantum.circuit2\""); + // A refused file listed under another class. + edit("reject/reject.json", "\"v2_param_on_h.wqc\": \"invalid\"", "\"v2_param_on_h.wqc\": \"malformed\""); + assert_eq!(verify(&s.0), 1); + edit("reject/reject.json", "\"v2_param_on_h.wqc\": \"malformed\"", "\"v2_param_on_h.wqc\": \"invalid\""); + // An accept entry with another reading. + edit("accept/accept.json", "\"seed\": 1,", "\"seed\": 2,"); + assert_eq!(verify(&s.0), 1); + edit("accept/accept.json", "\"seed\": 2,", "\"seed\": 1,"); + // A refused file in the accept tree. + std::fs::copy(s.0.join("reject/v2_param_on_h.wqc"), s.0.join("accept/v1_whitespace.wqc")).unwrap(); + assert_eq!(verify(&s.0), 1); + std::fs::copy(s.0.join("vectors/bell.wqc"), s.0.join("accept/v1_whitespace.wqc")).unwrap(); + assert_eq!(verify(&s.0), 1, "bell has a measurement; v1_whitespace has none"); + std::fs::remove_file(s.0.join("accept/v1_whitespace.wqc")).unwrap(); + generate(&s.0); + assert_eq!(verify(&s.0), 0); + // An accepted file in the reject tree. + std::fs::copy(s.0.join("v2/vectors/bell.wqc"), s.0.join("reject/v2_param_on_h.wqc")).unwrap(); + assert_eq!(verify(&s.0), 1); + // A file no listing names, in the reject tree and in a vector tree. + std::fs::write(s.0.join("reject/stray.wqc"), b"WQC2").unwrap(); + std::fs::copy(s.0.join("vectors/bell.wqc"), s.0.join("v2/vectors/stray.wqc")).unwrap(); + assert_eq!(verify(&s.0), 3); + // A missing tree. + std::fs::remove_dir_all(s.0.join("v2")).unwrap(); + assert_eq!(verify(&s.0), 3); + } } diff --git a/wai/wai-rs/src/codecs/glossary_check.rs b/wai/wai-rs/src/codecs/glossary_check.rs index b630e2627..22bac592a 100644 --- a/wai/wai-rs/src/codecs/glossary_check.rs +++ b/wai/wai-rs/src/codecs/glossary_check.rs @@ -93,6 +93,7 @@ const GLOSSARY_NO_ENTRY: &[(&str, &str)] = &[ (CAP_NEURAL_INT_MLICPP, WAI_NATIVE), (CAP_NEURAL_INT_SYNTH, WAI_NATIVE), (CAP_QUANTUM_CIRCUIT, WAI_NATIVE), + (CAP_QUANTUM_CIRCUIT2, WAI_NATIVE), (CAP_SPLAT_4D, WAI_NATIVE), (CAP_SPLAT_INT_CODEC, WAI_NATIVE), (CAP_TERNARY_TT, WAI_NATIVE), diff --git a/wai/wai-rs/src/codecs/mod.rs b/wai/wai-rs/src/codecs/mod.rs index e1c0f0c8f..a9318580e 100644 --- a/wai/wai-rs/src/codecs/mod.rs +++ b/wai/wai-rs/src/codecs/mod.rs @@ -289,8 +289,22 @@ pub const CAP_AVATAR_POSE: &str = "wai.avatar.pose"; // absent: NISQ noise + stochastic measurement break the determinism the standard // rests on. Engine behind the `quantum` feature (crate::quantum). NOT a media // codec — a way to make a quantum computation a transportable, receiptable object. +// Frozen (magic WQC1), so that existing content reproduces bit for bit: its phase +// table deviates from e^{2πi/2^k} for k ≥ 8 and applies no phase for k ≥ 19. New +// content uses wai.quantum.circuit2. pub const CAP_QUANTUM_CIRCUIT: &str = "wai.quantum.circuit"; +// The successor of wai.quantum.circuit (extensions/quantum-sim): the same WQC +// layout under magic WQC2, with a correctly rounded phase table (each component +// of every P(k) within half a unit of 2^30·e^{2πi/2^k}, so the controlled-phase +// ladder gives the QFT), a byte-canonical contract and a strict op-log. A new +// capability rather than a change to the old one, because a registered +// capability's payload format is fixed (SPEC §8): a reader that predates it +// rejects a WQC2 payload at the magic, and an envelope under this string is inert +// to it. Same equivalence, tier and replicate class as wai.quantum.circuit. Engine +// behind the `quantum` feature. +pub const CAP_QUANTUM_CIRCUIT2: &str = "wai.quantum.circuit2"; + // Balanced-ternary matrix-product-state transform (extensions/ternary-tt): a // quantum-INSPIRED (tensor-network) LOSSY compressor whose whole decode is // integer (per-column integer scale, i64/i128), so the reconstruction is @@ -580,7 +594,7 @@ pub fn known_capabilities() -> &'static [&'static str] { CAP_MESH_GLB, CAP_SPLAT_SPZ, CAP_AUDIO_SCENE, CAP_AUDIO_SCORE, CAP_HAPTIC_SIGNAL, CAP_FEED_STATE, CAP_AVATAR_POSE, CAP_SPLAT_4D, CAP_FILM_LINEAR, CAP_FILM_REEL, // deterministic quantum-circuit simulation (byte-exact statevector) - CAP_QUANTUM_CIRCUIT, + CAP_QUANTUM_CIRCUIT, CAP_QUANTUM_CIRCUIT2, // quantum-inspired ternary-MPS transform (byte-exact recon, lossy) CAP_TERNARY_TT, // neural-network coding: the prior artifact form + the general-tensor candidate @@ -793,12 +807,13 @@ pub fn capability_tier(cap: &str) -> Option { // wai.generate.spectral is generation-equivalence: a deterministic integer // generator reproduces the GENERATED image byte-for-byte from its pin — the // generative analog of lossless (byte-exact, just of created not captured content). - // wai.quantum.circuit is statevector-equivalence: the reconstructed - // fixed-point statevector is byte-identical on every machine — the - // simulation analog of lossless, like world replay (it IS a fixed-point - // approximation of the ideal unitary evolution, but its reconstruction is - // exact and portable; the honest note lives in the extension's §Fidelity). -CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_MESH_GLB | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_FILM_LINEAR | CAP_FILM_REEL | CAP_GENERATE_SPECTRAL | CAP_QUANTUM_CIRCUIT => Lossless, + // wai.quantum.circuit and wai.quantum.circuit2 are statevector-equivalence: + // the reconstructed fixed-point statevector is byte-identical on every + // machine — the simulation analog of lossless, like world replay (it IS a + // fixed-point approximation of the ideal unitary evolution, but its + // reconstruction is exact and portable; the honest note lives in the + // extension's §Fidelity). +CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_MESH_GLB | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_FILM_LINEAR | CAP_FILM_REEL | CAP_GENERATE_SPECTRAL | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 => Lossless, // Splat delivery is byte-exact and the temporal reconstruction is // frame-state-exact; the splat representation itself approximates a // scene, so the honest tier matches the captured medium, not the @@ -937,7 +952,7 @@ pub fn capability_license(cap: &str) -> Option { | CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_SPLAT_4D | CAP_FILM_LINEAR | CAP_FILM_REEL - | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT | CAP_PRIOR_SET + | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_TERNARY_TT | CAP_PRIOR_SET | CAP_IMAGE_INT_REFINE | CAP_VIDEO_INT_REFINE | CAP_AUDIO_INT_REFINE | CAP_SPLAT_INT_REFINE // Open asset formats: glTF (Khronos), SPZ (open source). | CAP_MESH_GLB | CAP_SPLAT_SPZ @@ -1277,7 +1292,7 @@ pub fn capability_replicate_class(cap: &str) -> Option { | CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_AUDIO_SCENE | CAP_AUDIO_SCORE | CAP_HAPTIC_SIGNAL | CAP_FEED_STATE | CAP_AVATAR_POSE | CAP_SPLAT_4D | CAP_FILM_LINEAR | CAP_FILM_REEL - | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT + | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 | CAP_TERNARY_TT // A parameter-set listing: SPEC §3.1 fixes its bytes exactly, and they are // its own output. | CAP_PRIOR_SET @@ -1749,6 +1764,7 @@ mod tests { // Quantum-circuit simulation is statevector-equivalence — the simulation // analog of lossless (byte-exact reconstruction of the fixed-point state). assert_eq!(capability_tier(CAP_QUANTUM_CIRCUIT), Some(FidelityTier::Lossless)); + assert_eq!(capability_tier(CAP_QUANTUM_CIRCUIT2), Some(FidelityTier::Lossless)); // The semantic path is perceptual, not byte-exact. assert_eq!(capability_tier(CAP_SEMANTIC_CLIP), Some(FidelityTier::Perceptual)); assert_eq!(capability_tier(CAP_SEMANTIC_PERCO2), Some(FidelityTier::Perceptual)); @@ -1768,6 +1784,7 @@ mod tests { assert!(is_royalty_free(CAP_VIDEO_AV1)); assert!(is_royalty_free(CAP_FILM_REEL)); assert!(is_royalty_free(CAP_QUANTUM_CIRCUIT)); // WAI-native, integer floor + assert!(is_royalty_free(CAP_QUANTUM_CIRCUIT2)); // Verified June 2026 against primary sources: assert_eq!(capability_license(CAP_AUDIO_IAMF), Some(RoyaltyFree)); // AOMedia PL 1.0 assert_eq!(capability_license(CAP_IMAGE_JP2), Some(RoyaltyFree)); // ITU-T 2.1 Part 1 @@ -2092,7 +2109,8 @@ mod tests { // are standard-defined (reconstruction specified exactly, not by a model // WAI runs). for &c in &[CAP_IMAGE_PNG, CAP_AUDIO_FLAC, CAP_TEXT_ZSTD, - CAP_WORLD_REPLAY, CAP_AUDIO_SCORE, CAP_FILM_REEL, CAP_QUANTUM_CIRCUIT] { + CAP_WORLD_REPLAY, CAP_AUDIO_SCORE, CAP_FILM_REEL, CAP_QUANTUM_CIRCUIT, + CAP_QUANTUM_CIRCUIT2] { assert_eq!(capability_replicate_class(c), Some(StandardDefined), "{c}"); assert!(!is_neural_integer_exact(c), "{c} is not a neural decode"); } @@ -2330,6 +2348,7 @@ mod tests { (CAP_FILM_LINEAR, "extensions/linear-film.md: film-equivalence"), (CAP_FILM_REEL, "extensions/linear-film.md: reel-equivalence"), (CAP_QUANTUM_CIRCUIT, "extensions/quantum-sim.md: statevector-equivalence"), + (CAP_QUANTUM_CIRCUIT2, "extensions/quantum-sim.md: statevector-equivalence"), (CAP_TERNARY_TT, "extensions/ternary-tt.md: reconstruction-equivalence"), (CAP_PRIOR_SET, "SPEC.md §3.1: a listing's bytes are fixed exactly and are their own output"), (CAP_IMAGE_INT_REFINE, "extensions/staged-delivery.md §6.1: integer predictor, residual and clamp; staged-conformance"), diff --git a/wai/wai-rs/src/codecs/registry.rs b/wai/wai-rs/src/codecs/registry.rs index 781ae1b06..31dc32e2e 100644 --- a/wai/wai-rs/src/codecs/registry.rs +++ b/wai/wai-rs/src/codecs/registry.rs @@ -282,6 +282,7 @@ const ROWS: &[(&str, &str, Option<&str>, &str)] = &[ (CAP_FILM_REEL, S_INTERACTIVE, None, "2026-06-17"), (CAP_TERNARY_TT, S_INTERACTIVE, None, "2026-07-30"), (CAP_QUANTUM_CIRCUIT, S_INTERACTIVE, None, "2026-07-25"), + (CAP_QUANTUM_CIRCUIT2, S_INTERACTIVE, None, "2026-10-09"), (CAP_ASSET_PROVENANCE, S_INTERACTIVE, None, "2026-06-13"), (CAP_QUANTUM_CALIBRATION, S_INTERACTIVE, None, "2026-08-01"), (CAP_QUANTUM_JOB, S_INTERACTIVE, None, "2026-08-01"), @@ -344,10 +345,15 @@ pub struct Identifier { /// Every [`Identifier`] SPEC uses. pub const IDENTIFIERS: &[Identifier] = &[ + Identifier { + identifier: "wai.det.amp64", + spec_section: S_INTERACTIVE, + meaning: "the complex fixed-point arithmetic of wai.quantum.circuit2: re and im each an i64 at scale 2^frac, products exact then rounded half up, sums exact (quantum-sim §2.1, §3.0)", + }, Identifier { identifier: "wai.det.fixed64", spec_section: S_INTERACTIVE, - meaning: "the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in", + meaning: "the fixed-point arithmetic floor the deterministic *-equivalence criteria are computed in (Q32.32, interactive-worlds §6.2); inside a wai.quantum.circuit contract it names the quantum-sim §3.0 arithmetic at frac 30, products rounded half up", }, Identifier { identifier: "wai.world.sim.feed0", @@ -685,7 +691,8 @@ pub fn capability_media(cap: &str) -> Option { | CAP_FILM_LINEAR | CAP_FILM_REEL => Video, CAP_WORLD_SCENE | CAP_WORLD_REPLAY | CAP_WORLD_LIVE | CAP_WORLD_MODEL | CAP_FEED_STATE | CAP_AVATAR_POSE => Interactive, - CAP_PRIOR_NNC | CAP_PRIOR_SET | CAP_TENSOR_NNC | CAP_QUANTUM_CIRCUIT | CAP_TERNARY_TT => Binary, + CAP_PRIOR_NNC | CAP_PRIOR_SET | CAP_TENSOR_NNC | CAP_QUANTUM_CIRCUIT | CAP_QUANTUM_CIRCUIT2 + | CAP_TERNARY_TT => Binary, c if c.starts_with("wai.image.") => Image, c if c.starts_with("wai.audio.") => Audio, c if c.starts_with("wai.video.") => Video, diff --git a/wai/wai-rs/src/lib.rs b/wai/wai-rs/src/lib.rs index 58d42b4ba..190b51941 100644 --- a/wai/wai-rs/src/lib.rs +++ b/wai/wai-rs/src/lib.rs @@ -426,12 +426,13 @@ pub mod world_model; pub mod film; // Deterministic quantum-circuit SIMULATION as transportable content -// (extensions/quantum-sim, `wai.quantum.circuit`): the wire carries a compact -// gate op-log (WQC container), the sink classically simulates it in a fixed-point -// complex floor (Amp = i64 re/im at 2^FRAC), and the reconstructed statevector is -// byte-identical on every machine (statevector-equivalence). Clifford+T + dyadic -// phase, so the controlled-phase ladder of the QFT ("quantum FFT") reconstructs -// byte-exact. Pure i64, no float/ONNX; compiles to wasm32. Quantum HARDWARE is +// (extensions/quantum-sim, `wai.quantum.circuit2` and the frozen +// `wai.quantum.circuit`): the wire carries a compact gate op-log (WQC container), +// the sink classically simulates it in a fixed-point complex floor (Amp = i64 +// re/im at 2^FRAC), and the reconstructed statevector is byte-identical on every +// machine (statevector-equivalence). Clifford+T + dyadic phase; circuit2's +// correctly rounded phase table gives the controlled-phase ladder of the QFT +// ("quantum FFT"). Pure i64, no float/ONNX; compiles to wasm32. Quantum HARDWARE is // deliberately absent — NISQ noise breaks the determinism the standard rests on. #[cfg(feature = "quantum")] pub use wai_quantum::quantum; diff --git a/wai/wai-rs/src/quantum_envelope.rs b/wai/wai-rs/src/quantum_envelope.rs index 5b09aae60..03b0c4a39 100644 --- a/wai/wai-rs/src/quantum_envelope.rs +++ b/wai/wai-rs/src/quantum_envelope.rs @@ -1,35 +1,115 @@ -//! Wrapping a quantum circuit in a WAI envelope. +//! Wrapping a quantum circuit in a WAI envelope, and opening one. //! //! The computation itself lives in the dependency-light [`wai_quantum`] crate; //! this is the container half — the part that belongs to WAI, not to quantum. +//! +//! The envelope's capability is the circuit's: `wai.quantum.circuit2` for a +//! [`Gateset::V2`] circuit, whose payload is a `WQC2` container, and +//! `wai.quantum.circuit` for a [`Gateset::V1`] one, whose payload is `WQC1`. -use wai_quantum::quantum::{to_wqc, Circuit, Measure, CAPABILITY}; +use wai_quantum::quantum::{from_wqc, to_wqc, Circuit, Gateset, Measure, QuantumError}; -/// Wrap a circuit in a v1.0 WAI envelope under `wai.quantum.circuit`. The payload -/// is the `WQC1` container; `intent=replicate` is sound because the reconstruction -/// is byte-exact (statevector-equivalence). -pub fn wrap_envelope(c: &Circuit, measure: Option) -> crate::container::Wai { +/// Wrap a circuit in a v1.0 WAI envelope under its gateset's capability +/// ([`Gateset::capability`]). The payload is the circuit's `WQC` container; +/// `intent=replicate` is sound because the reconstruction is byte-exact +/// (statevector-equivalence). +/// +/// Fails as [`to_wqc`] does, for a circuit its gateset does not allow, and with +/// [`QuantumError::TooManyQubits`] for a circuit of 64 qubits or more, whose +/// dimension the manifest's `target.dim` cannot hold. +pub fn wrap_envelope(c: &Circuit, measure: Option) -> Result { use crate::container::{Conditioning, Manifest, ModelRequirement}; - let payload = to_wqc(c, measure); - let manifest = Manifest::new("quantum", "replicate", ModelRequirement::new(CAPABILITY), Conditioning::new("wqc")) - .with_target(serde_json::json!({ "n_qubits": c.n_qubits, "dim": 1u64 << c.n_qubits })); - crate::container::Wai::new(manifest, payload) + let payload = to_wqc(c, measure)?; + let dim = 1u64.checked_shl(u32::from(c.n_qubits)).ok_or(QuantumError::TooManyQubits(c.n_qubits))?; + let manifest = Manifest::new("quantum", "replicate", ModelRequirement::new(c.gateset.capability()), Conditioning::new("wqc")) + .with_target(serde_json::json!({ "n_qubits": c.n_qubits, "dim": dim })); + Ok(crate::container::Wai::new(manifest, payload)) } +/// Open a quantum envelope: decode its `WQC` payload, then require the +/// envelope's capability to be the one the container's magic names. +/// +/// A payload that does not decode fails as [`from_wqc`] does. A decoded circuit +/// whose gateset is not the one the capability names — a `WQC2` payload under +/// `wai.quantum.circuit`, a `WQC1` payload under `wai.quantum.circuit2`, or any +/// payload under another capability — is [`QuantumError::Contract`]. +pub fn open_envelope(w: &crate::container::Wai) -> Result<(Circuit, Option), QuantumError> { + let (c, m) = from_wqc(&w.payload)?; + let cap = w.capability(); + if Gateset::from_capability(cap) != Some(c.gateset) { + // The capability is the file's; quote at most 64 characters of it. + let quoted: String = cap.chars().take(64).collect(); + return Err(QuantumError::Contract(format!( + "the envelope's capability {quoted:?} does not name the payload's container, {} ({})", + String::from_utf8_lossy(&c.gateset.magic()), + c.gateset.capability() + ))); + } + Ok((c, m)) +} #[cfg(test)] mod tests { use super::*; - use wai_quantum::quantum::from_wqc; - #[test] - fn envelope_wraps_under_capability() { - let mut c = Circuit::new(2); + use crate::container::Wai; + + fn bell(gs: Gateset) -> Circuit { + let mut c = Circuit::with_gateset(2, gs); c.h(0).cx(0, 1); - let wai = wrap_envelope(&c, None); - assert_eq!(wai.capability(), CAPABILITY); - let bytes = wai.to_bytes().unwrap(); - let back = crate::container::Wai::from_bytes(&bytes).unwrap(); - let (c2, _) = from_wqc(&back.payload).unwrap(); - assert_eq!(c, c2); + c + } + + /// The envelope's bytes, read back. + fn reread(w: &Wai) -> Wai { + Wai::from_bytes(&w.to_bytes().unwrap()).unwrap() + } + + /// The capability follows the circuit's gateset, and the circuit — gateset + /// included — and its measurement survive the envelope. + #[test] + fn envelope_wraps_under_the_gatesets_capability() { + for (gs, cap, magic) in [ + (Gateset::V2, crate::codecs::CAP_QUANTUM_CIRCUIT2, b"WQC2"), + (Gateset::V1, crate::codecs::CAP_QUANTUM_CIRCUIT, b"WQC1"), + ] { + // The registry's strings are copies of the crate's. + assert_eq!(gs.capability(), cap); + let c = bell(gs); + let m = Some(Measure { seed: 7, shots: 1024 }); + let wai = wrap_envelope(&c, m).unwrap(); + assert_eq!(wai.capability(), cap); + assert_eq!(&wai.payload[0..4], magic); + assert_eq!(wai.manifest.target, serde_json::json!({ "n_qubits": 2, "dim": 4 })); + let back = reread(&wai); + assert_eq!(open_envelope(&back).unwrap(), (c.clone(), m)); + let (c2, _) = from_wqc(&back.payload).unwrap(); + assert_eq!(c2, c); + } + assert_eq!(wrap_envelope(&Circuit::new(1), None).unwrap().capability(), "wai.quantum.circuit2"); + } + + /// An envelope whose capability is not the one its payload's magic names is + /// refused, whichever way round, and so is a quantum payload under any + /// other capability. A payload that is not a container fails as the + /// container reader does, and a circuit its gateset forbids is never wrapped. + #[test] + fn a_capability_that_does_not_name_the_payload_is_refused() { + for (gs, other) in [(Gateset::V2, "wai.quantum.circuit"), (Gateset::V1, "wai.quantum.circuit2"), (Gateset::V2, "wai.image.png")] { + let mut wai = wrap_envelope(&bell(gs), None).unwrap(); + wai.manifest.model_requirement.capability = other.to_string(); + let e = open_envelope(&reread(&wai)).unwrap_err(); + assert_eq!(e.kind(), "contract", "{gs:?} under {other}: {e}"); + } + let mut wai = wrap_envelope(&bell(Gateset::V2), None).unwrap(); + wai.payload = b"not a circuit".to_vec(); + assert_eq!(open_envelope(&wai).unwrap_err().kind(), "malformed"); + // A repeated control: allowed in circuit/1, refused by circuit2. + let mut c = Circuit::with_gateset(3, Gateset::V2); + c.ccx(0, 0, 2); + assert_eq!(wrap_envelope(&c, None).err().expect("refused").kind(), "invalid"); + c.gateset = Gateset::V1; + assert_eq!(open_envelope(&wrap_envelope(&c, None).unwrap()).unwrap().0, c); + // A circuit whose dimension the manifest cannot hold. + assert_eq!(wrap_envelope(&Circuit::new(64), None).err(), Some(QuantumError::TooManyQubits(64))); } } diff --git a/wai/wai-web/Cargo.lock b/wai/wai-web/Cargo.lock index 9a9e1e3ef..cf84498df 100644 --- a/wai/wai-web/Cargo.lock +++ b/wai/wai-web/Cargo.lock @@ -755,7 +755,7 @@ dependencies = [ [[package]] name = "wai-quantum" -version = "0.3.41" +version = "0.4.0" dependencies = [ "blake3", "ed25519-dalek", diff --git a/wai/wai-web/src/lib.rs b/wai/wai-web/src/lib.rs index 48711a466..af884ba2f 100644 --- a/wai/wai-web/src/lib.rs +++ b/wai/wai-web/src/lib.rs @@ -65,8 +65,9 @@ pub mod int_world; // sink-supplied rasterizer presents the cloud (splat-equivalence). pub mod int_splat; -// Deterministic quantum-circuit SIMULATION (wai.quantum.circuit) — the SAME -// `wai::quantum` engine native sinks run, compiled to wasm32. A WQC gate op-log +// Deterministic quantum-circuit SIMULATION (wai.quantum.circuit2, and the frozen +// wai.quantum.circuit) — the SAME `wai::quantum` engine native sinks run, compiled +// to wasm32. A WQC gate op-log, in the phase table its magic names, // is classically simulated in the fixed-point complex floor; the reconstructed // statevector hashes byte-identically to the native reference // (statevector-equivalence). Pure i64, no float, no ML runtime, no quantum diff --git a/wai/wai-web/src/quantum.rs b/wai/wai-web/src/quantum.rs index 30258c1b2..beef65ed6 100644 --- a/wai/wai-web/src/quantum.rs +++ b/wai/wai-web/src/quantum.rs @@ -1,4 +1,5 @@ -//! Deterministic quantum-circuit SIMULATION sink (`wai.quantum.circuit`). +//! Deterministic quantum-circuit SIMULATION sink (`wai.quantum.circuit2`, and +//! the frozen `wai.quantum.circuit`). //! //! The SAME `wai::quantum` engine native sinks run, compiled to wasm32 — no //! second implementation. A `WQC` gate op-log is parsed and classically @@ -9,8 +10,13 @@ //! equivalence). Pure i64, no float, no ML runtime, and no quantum hardware — //! the point is a quantum computation made a transportable, receiptable object, //! with no speedup claimed. +//! +//! The container's magic names its capability: a `WQC2` file is simulated with +//! the circuit2 phase table and a `WQC1` file with the frozen circuit/1 table, +//! and [`QuantumSink::capability`] says which one the file was. A file the +//! reference reader refuses is refused here with the same error class. -use wai::quantum::{from_wqc, StateVector}; +use wai::quantum::{circuit_hash, from_wqc, Gateset, QuantumError, StateVector}; use wasm_bindgen::prelude::*; fn hex(b: &[u8]) -> String { @@ -19,38 +25,73 @@ fn hex(b: &[u8]) -> String { #[wasm_bindgen] pub struct QuantumSink { + gateset: Gateset, n_qubits: u8, n_ops: u32, + circuit_hash: [u8; 32], sv: StateVector, seed: u64, shots: u64, has_measure: bool, } -#[wasm_bindgen] +/// The text of an error the sink throws: ` (): `, where +/// the class is [`QuantumError::kind`] (`malformed`, `invalid`, `contract` or +/// `too-many-qubits`), the word the reject corpus's `reject.json` lists. +fn error_text(stage: &str, e: &QuantumError) -> String { + format!("{stage} ({}): {e}", e.kind()) +} + impl QuantumSink { - /// Parse a `WQC` container and classically simulate it — pure i64, no float. - /// Throws on a malformed container or an over-large circuit. - #[wasm_bindgen(constructor)] - pub fn new(wqc: &[u8]) -> Result { - let (circuit, measure) = - from_wqc(wqc).map_err(|e| JsError::new(&format!("WQC parse: {e:?}")))?; - let sv = circuit - .simulate() - .map_err(|e| JsError::new(&format!("simulate: {e:?}")))?; + /// Parse and simulate, with the reference reader's own error. The + /// `#[wasm_bindgen]` constructor only converts that error for JS. + fn open(wqc: &[u8]) -> Result { + let (circuit, measure) = from_wqc(wqc).map_err(|e| ("WQC parse", e))?; + // `from_wqc` validated the circuit, so its hash cannot fail. + let circuit_hash = circuit_hash(&circuit).map_err(|e| ("circuit hash", e))?; + let sv = circuit.simulate().map_err(|e| ("simulate", e))?; let (seed, shots, has_measure) = match measure { Some(m) => (m.seed, m.shots, true), None => (0, 0, false), }; Ok(QuantumSink { + gateset: circuit.gateset, n_qubits: circuit.n_qubits, + // A valid circuit has at most `u32::MAX` ops. n_ops: circuit.ops.len() as u32, + circuit_hash, sv, seed, shots, has_measure, }) } +} + +#[wasm_bindgen] +impl QuantumSink { + /// Parse a `WQC1` or `WQC2` container and classically simulate it — pure + /// i64, no float. Throws on a container the reference reader refuses or an + /// over-large circuit; the message is ` (): `, with + /// the class `reject.json` names (`malformed`, `invalid`, `contract`, + /// `too-many-qubits`). + #[wasm_bindgen(constructor)] + pub fn new(wqc: &[u8]) -> Result { + QuantumSink::open(wqc).map_err(|(stage, e)| JsError::new(&error_text(stage, &e))) + } + + /// The capability the container's magic names: `wai.quantum.circuit2` + /// (`WQC2`) or `wai.quantum.circuit` (`WQC1`). + pub fn capability(&self) -> String { + self.gateset.capability().to_string() + } + + /// The circuit hash: BLAKE3 over the capability's canonical contract and the + /// op-log, so it binds the capability and its phase table. It is the value + /// a corpus manifest and a run receipt carry. + pub fn circuit_hash_hex(&self) -> String { + hex(&self.circuit_hash) + } pub fn n_qubits(&self) -> u8 { self.n_qubits @@ -110,3 +151,59 @@ impl QuantumSink { .collect() } } + +#[cfg(test)] +mod tests { + use super::QuantumSink; + use std::path::{Path, PathBuf}; + + fn corpus() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance") + } + + fn manifest(dir: &Path) -> serde_json::Map { + let text = std::fs::read_to_string(dir.join("expected_hashes.json")).unwrap(); + serde_json::from_str::(&text).unwrap().as_object().unwrap().clone() + } + + /// Every file of both corpora opens in the sink with the manifest's + /// capability, circuit hash, statevector hash and, where it pins a + /// measurement, histogram hash. A circuit/1 manifest entry names no + /// capability. + #[test] + fn the_sink_reproduces_both_corpora() { + for (dir, default_capability) in [(corpus(), "wai.quantum.circuit"), (corpus().join("v2"), "")] { + let entries = manifest(&dir); + assert!(!entries.is_empty(), "{}", dir.display()); + for (name, e) in &entries { + let bytes = std::fs::read(dir.join("vectors").join(name)).unwrap(); + let Ok(sink) = QuantumSink::open(&bytes) else { panic!("{name} is refused") }; + let capability = e.get("capability").and_then(|c| c.as_str()).unwrap_or(default_capability); + assert_eq!(sink.capability(), capability, "{name}"); + assert_eq!(sink.n_qubits() as u64, e["n_qubits"].as_u64().unwrap(), "{name}"); + assert_eq!(sink.n_ops() as u64, e["n_ops"].as_u64().unwrap(), "{name}"); + assert_eq!(sink.circuit_hash_hex(), e["circuit_hash"].as_str().unwrap(), "{name}"); + assert_eq!(sink.statevector_hash_hex(), e["statevector_hash"].as_str().unwrap(), "{name}"); + let histogram = e.get("measure").map(|m| m["histogram_hash"].as_str().unwrap().to_string()); + assert_eq!(sink.histogram_hash_hex(), histogram, "{name}"); + } + } + } + + /// Every file of the reject corpus is refused with the class `reject.json` + /// lists, and the thrown text names that class in parentheses. + #[test] + fn the_sink_refuses_the_reject_corpus_by_class() { + let dir = corpus().join("reject"); + let text = std::fs::read_to_string(dir.join("reject.json")).unwrap(); + let cases: serde_json::Map = serde_json::from_str(&text).unwrap(); + assert!(!cases.is_empty()); + for (name, kind) in &cases { + let bytes = std::fs::read(dir.join(name)).unwrap(); + let Err((stage, e)) = QuantumSink::open(&bytes) else { panic!("{name} is accepted") }; + assert_eq!(e.kind(), kind.as_str().unwrap(), "{name}: {e}"); + let shown = super::error_text(stage, &e); + assert!(shown.starts_with(&format!("WQC parse ({}): ", e.kind())), "{name}: {shown}"); + } + } +} diff --git a/wai/wai-web/src/quantum_toolchain.rs b/wai/wai-web/src/quantum_toolchain.rs index c39dacb35..8756a19b5 100644 --- a/wai/wai-web/src/quantum_toolchain.rs +++ b/wai/wai-web/src/quantum_toolchain.rs @@ -3,9 +3,14 @@ //! native sink runs, compiled to wasm32 — a full quantum development environment //! that runs entirely in the page, and whose every run is reproducible to the bit //! and sealed into a signed receipt. +//! +//! The IDE builds its circuits in one gateset at a time: `wai.quantum.circuit2` +//! by default, or the frozen `wai.quantum.circuit` after `set_gateset(1)`, to +//! reproduce circuit/1 content. The circuit hash, the statevector and the +//! receipt all follow that gateset. use ed25519_dalek::SigningKey; -use wai::quantum::{circuit_hash, BaseGate, Circuit, Gate, Measure, StateVector, FRAC}; +use wai::quantum::{circuit_hash, BaseGate, Circuit, Gate, Gateset, Measure, StateVector, FRAC}; use wai::quantum_mps; use wai::quantum_pauli; use wai::quantum_receipt::QuantumReceipt; @@ -51,17 +56,86 @@ fn name_of(b: BaseGate) -> &'static str { } } +/// What the IDE prints on the browser console, never in a returned value, when +/// a circuit/1 circuit holds a phase that circuit/1 does not reproduce. It is the +/// command-line tool's note, with the IDE's own switch as the remedy. +const V1_NOTE: &str = + "circuit/1 table: P(k≥8) deviates by up to 12868 units of 2^-30, P(k≥19) applies no phase; use set_gateset(2)"; + +/// The note for `c`, if it is a circuit/1 circuit with a `P(k ≥ 8)`: the same +/// condition the command-line tool notes. +fn v1_note(c: &Circuit) -> Option<&'static str> { + (c.gateset == Gateset::V1 && c.ops.iter().any(|g| g.base == BaseGate::P && g.param >= 8)).then_some(V1_NOTE) +} + +#[cfg(target_arch = "wasm32")] +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_namespace = console, js_name = warn)] + fn console_warn(s: &str); +} + +/// Off wasm32 (the native unit tests) the console is standard error. +#[cfg(not(target_arch = "wasm32"))] +fn console_warn(s: &str) { + eprintln!("{s}"); +} + +/// The gateset `set_gateset` names: 1 is circuit/1, 2 is circuit2. +fn gateset_numbered(g: u8) -> Option { + match g { + 1 => Some(Gateset::V1), + 2 => Some(Gateset::V2), + _ => None, + } +} + #[wasm_bindgen] pub struct QuantumIde { + /// The circuit being edited. Its gateset is the IDE's: every circuit the IDE + /// builds is built in it. circuit: Circuit, sv: Option, + /// Whether the console note has been printed for the circuit as it stands. + noted: bool, } #[wasm_bindgen] impl QuantumIde { + /// An empty circuit on `n_qubits` (1 to 12), in circuit2. #[wasm_bindgen(constructor)] pub fn new(n_qubits: u8) -> QuantumIde { - QuantumIde { circuit: Circuit::new(n_qubits.clamp(1, 12)), sv: None } + QuantumIde { circuit: Circuit::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false } + } + + // ---- gateset ---- + /// Switch the IDE to gateset `g`: 2 for `wai.quantum.circuit2` (the + /// default), 1 for the frozen `wai.quantum.circuit`. The current circuit + /// keeps its gates and moves to that gateset, and every later build uses it. + /// Any other `g` changes nothing and returns `false`. + pub fn set_gateset(&mut self, g: u8) -> bool { + let Some(gs) = gateset_numbered(g) else { return false }; + self.circuit.gateset = gs; + self.changed(); + true + } + /// The IDE's gateset: 2 for circuit2, 1 for circuit/1. + pub fn gateset(&self) -> u8 { + match self.circuit.gateset { + Gateset::V1 => 1, + Gateset::V2 => 2, + // `set_gateset` names no other gateset. + _ => 0, + } + } + /// The IDE's capability string: `wai.quantum.circuit2` or `wai.quantum.circuit`. + pub fn capability(&self) -> String { + self.circuit.gateset.capability().to_string() + } + /// The console note for the current circuit, or `""`: the text `run` and + /// `run_to` print on the console for a circuit/1 circuit with a `P(k ≥ 8)`. + pub fn note(&self) -> String { + v1_note(&self.circuit).unwrap_or_default().to_string() } // ---- editing ---- @@ -71,45 +145,49 @@ impl QuantumIde { pub fn n_gates(&self) -> u32 { self.circuit.ops.len() as u32 } + /// Replace the circuit with an empty one on `n` qubits (1 to 12), in the + /// IDE's gateset. pub fn set_qubits(&mut self, n: u8) { - self.circuit = Circuit::new(n.clamp(1, 12)); - self.sv = None; + self.circuit = Circuit::with_gateset(n.clamp(1, 12), self.circuit.gateset); + self.changed(); } /// Append a gate. `controls` are the control qubits (empty for a 1-qubit gate); - /// `param` is the `P(k)` dyadic index (ignored otherwise). + /// `param` is the `P(k)` dyadic index, and is stored as 0 for every other + /// gate, which carries no parameter. Returns whether the circuit is still + /// valid in its gateset; the gate is appended either way. pub fn add(&mut self, name: &str, controls: Vec, target: u8, param: u16) -> bool { let Some(base) = base_of(name) else { return false }; + let param = if base == BaseGate::P { param } else { 0 }; self.circuit.ops.push(Gate { base, controls, target, param }); - self.sv = None; + self.changed(); self.circuit.validate().is_ok() } pub fn pop(&mut self) { self.circuit.ops.pop(); - self.sv = None; + self.changed(); } pub fn clear(&mut self) { self.circuit.ops.clear(); - self.sv = None; + self.changed(); } - /// Replace the circuit with a library algorithm. + /// Replace the circuit with a library algorithm, built in the IDE's gateset. pub fn load_algorithm(&mut self, id: &str, n: u8, param: u32) -> bool { if let Some(c) = tc::build_algorithm(id, n.clamp(1, 12), param) { - self.circuit = c; - self.sv = None; + self.replace(c); true } else { false } } - /// Replace the circuit from an OpenQASM source. Returns an error string on - /// failure (unsupported gate, non-dyadic phase, bad register). + /// Replace the circuit from an OpenQASM source, built in the IDE's gateset. + /// Returns an error string on failure (unsupported gate, non-dyadic phase, + /// bad register, a gate that names one qubit twice). pub fn load_qasm(&mut self, src: &str) -> Result<(), String> { let c = tc::from_qasm(src)?; if c.n_qubits > 12 { return Err(format!("{} qubits exceeds the in-browser limit (12)", c.n_qubits)); } - self.circuit = c; - self.sv = None; + self.replace(c); Ok(()) } /// The op-log as JSON, for the editor to render. @@ -136,6 +214,7 @@ impl QuantumIde { pub fn run(&mut self) -> Result<(), String> { let sv = self.circuit.simulate().map_err(|e| e.to_string())?; self.sv = Some(sv); + self.print_note(); Ok(()) } /// Debugger step: simulate only the first `k` gates and set the inspected state @@ -144,10 +223,11 @@ impl QuantumIde { /// entanglement build up. pub fn run_to(&mut self, k: u32) -> Result<(), String> { let k = (k as usize).min(self.circuit.ops.len()); - let mut sub = Circuit::new(self.circuit.n_qubits); + let mut sub = self.circuit.empty_like(); sub.ops = self.circuit.ops[..k].to_vec(); let sv = sub.simulate().map_err(|e| e.to_string())?; self.sv = Some(sv); + self.print_note(); Ok(()) } fn state(&self) -> Option<&StateVector> { @@ -217,8 +297,10 @@ impl QuantumIde { pub fn work(&self) -> f64 { tc::statevector_work(&self.circuit) as f64 } + /// The circuit hash, which binds the IDE's capability and its phase table, + /// or `""` for a circuit its gateset does not allow. pub fn circuit_hash_hex(&self) -> String { - hex(&circuit_hash(&self.circuit)) + circuit_hash(&self.circuit).map(|h| hex(&h)).unwrap_or_default() } pub fn statevector_hash_hex(&self) -> String { self.state().map(|sv| hex(&sv.statevector_hash())).unwrap_or_default() @@ -338,6 +420,31 @@ impl QuantumIde { } impl QuantumIde { + /// The circuit changed: its state and its note are stale. + fn changed(&mut self) { + self.sv = None; + self.noted = false; + } + + /// Install a circuit built elsewhere, moved into the IDE's gateset. The + /// builders make circuit2 circuits with distinct controls and no parameter + /// on a gate other than `P`, which are valid circuit/1 circuits too. + fn replace(&mut self, mut c: Circuit) { + c.gateset = self.circuit.gateset; + self.circuit = c; + self.changed(); + } + + /// Print the console note once for the circuit as it stands. + fn print_note(&mut self) { + if !self.noted { + if let Some(note) = v1_note(&self.circuit) { + console_warn(note); + } + self.noted = true; + } + } + fn seal(&self, seed: u64, shots: u64) -> Option { QuantumReceipt::seal( &signer(), @@ -360,3 +467,137 @@ pub fn ide_catalog() -> String { .collect(); format!("[{}]", items.join(",")) } + +#[cfg(test)] +mod tests { + use super::QuantumIde; + use std::path::Path; + + fn manifest(rel: &str) -> serde_json::Map { + let p = Path::new(env!("CARGO_MANIFEST_DIR")).join("../quantum-conformance").join(rel); + let text = std::fs::read_to_string(p).unwrap(); + serde_json::from_str::(&text).unwrap().as_object().unwrap().clone() + } + + /// The catalog circuits the browser check drives, by corpus file name. + const CASES: [(&str, &str, u8); 4] = [("bell.wqc", "bell", 2), ("ghz3.wqc", "ghz", 3), ("qft3.wqc", "qft", 3), ("qft5.wqc", "qft", 5)]; + + /// The IDE builds in circuit2 by default and reproduces the v2 manifest; + /// after `set_gateset(1)` it reproduces the circuit/1 manifest. A gateset + /// it does not know changes nothing. + #[test] + fn catalog_circuits_follow_the_ide_gateset() { + for (g, rel, capability) in [(2u8, "v2/expected_hashes.json", "wai.quantum.circuit2"), (1, "expected_hashes.json", "wai.quantum.circuit")] { + let m = manifest(rel); + for (name, id, n) in CASES { + let mut ide = QuantumIde::new(n); + if g == 1 { + assert!(ide.set_gateset(1)); + } + assert!(ide.load_algorithm(id, n, 0), "{name}"); + assert_eq!((ide.gateset(), ide.capability().as_str()), (g, capability), "{name}"); + ide.run().unwrap(); + assert_eq!(ide.circuit_hash_hex(), m[name]["circuit_hash"].as_str().unwrap(), "{name} in {g}"); + assert_eq!(ide.statevector_hash_hex(), m[name]["statevector_hash"].as_str().unwrap(), "{name} in {g}"); + } + } + let mut ide = QuantumIde::new(2); + assert_eq!(ide.gateset(), 2); + for g in [0, 3, 255] { + assert!(!ide.set_gateset(g)); + assert_eq!(ide.gateset(), 2); + } + } + + /// Switching gateset moves the current circuit, and every later build + /// (`set_qubits`, `load_algorithm`, `load_qasm`) and the debugger's prefix + /// circuit stay in it. + #[test] + fn builds_and_prefixes_keep_the_ide_gateset() { + let mut ide = QuantumIde::new(5); + assert!(ide.load_algorithm("qft", 5, 0)); + let v2 = ide.circuit_hash_hex(); + assert!(ide.set_gateset(1)); + let v1 = ide.circuit_hash_hex(); + assert_ne!(v1, v2); + ide.set_qubits(3); + assert_eq!(ide.gateset(), 1); + ide.load_qasm("OPENQASM 3.0;\nqubit[2] q;\nh q[0];\ncx q[0], q[1];\n").unwrap(); + assert_eq!(ide.capability(), "wai.quantum.circuit"); + assert!(ide.load_algorithm("qft", 5, 0)); + assert_eq!(ide.circuit_hash_hex(), v1); + + // The debugger's state after the last gate is the run's state, in circuit/1. + ide.run().unwrap(); + let full = ide.statevector_hash_hex(); + ide.run_to(ide.n_gates()).unwrap(); + assert_eq!(ide.statevector_hash_hex(), full); + // A prefix differs from the same prefix in circuit2. + ide.run_to(3).unwrap(); + let prefix_v1 = ide.statevector_hash_hex(); + assert!(ide.set_gateset(2)); + ide.run_to(3).unwrap(); + assert_ne!(ide.statevector_hash_hex(), prefix_v1); + } + + /// A parameter given to a gate other than `P` is stored as 0, so the gate + /// is valid in circuit2; a `P(k)` keeps its index and is checked. + #[test] + fn add_keeps_a_parameter_only_on_p() { + let mut ide = QuantumIde::new(2); + assert!(ide.add("h", vec![], 0, 5)); + assert!(ide.add("x", vec![0], 1, 9)); + assert!(ide.gates_json().contains("{\"name\":\"h\",\"controls\":[],\"target\":0,\"param\":0}"), "{}", ide.gates_json()); + assert!(ide.add("p", vec![0], 1, 32)); + assert!(ide.gates_json().ends_with("\"param\":32}]"), "{}", ide.gates_json()); + assert!(!ide.add("p", vec![], 0, 0)); + ide.pop(); + assert!(!ide.add("p", vec![], 0, 33)); + ide.pop(); + assert!(ide.run().is_ok()); + } + + /// An invalid circuit has no circuit hash: the IDE shows `""` instead of + /// failing, and shows the hash again once the circuit is valid. + #[test] + fn an_invalid_circuit_has_an_empty_hash() { + let mut ide = QuantumIde::new(3); + ide.add("h", vec![], 0, 0); + let valid = ide.circuit_hash_hex(); + assert_eq!(valid.len(), 64); + // A repeated control is invalid in circuit2 and allowed in circuit/1. + assert!(!ide.add("x", vec![0, 0], 1, 0)); + assert_eq!(ide.circuit_hash_hex(), ""); + assert!(ide.run().is_err()); + assert!(ide.set_gateset(1)); + assert_eq!(ide.circuit_hash_hex().len(), 64); + assert!(ide.set_gateset(2)); + // A target out of range is invalid in both. + ide.pop(); + assert!(!ide.add("x", vec![], 7, 0)); + for g in [1, 2] { + assert!(ide.set_gateset(g)); + assert_eq!(ide.circuit_hash_hex(), ""); + } + ide.pop(); + assert_eq!(ide.circuit_hash_hex(), valid); + } + + /// The note is the command-line tool's condition: a circuit/1 circuit with + /// a `P(k ≥ 8)`. A circuit2 circuit, or a circuit/1 circuit whose phases + /// stop at `P(7)`, gets none. + #[test] + fn the_note_names_the_circuit1_phases() { + let mut ide = QuantumIde::new(2); + ide.add("p", vec![0], 1, 7); + assert_eq!(ide.note(), ""); + assert!(ide.set_gateset(1)); + assert_eq!(ide.note(), ""); + ide.add("p", vec![0], 1, 8); + assert_eq!(ide.note(), super::V1_NOTE); + assert!(super::V1_NOTE.starts_with("circuit/1 table: P(k≥8) deviates by up to 12868 units of 2^-30, P(k≥19) applies no phase; use ")); + ide.run().unwrap(); + assert!(ide.set_gateset(2)); + assert_eq!(ide.note(), ""); + } +}