wai-quantum: the job engine in a browser tab, one job function for every host #361

Closed
dcharlot wants to merge 9 commits from wai/quantum-engine-browser into main
45 changed files with 4501 additions and 772 deletions

151
.github/workflows/wai-quantum-cli.yml vendored Normal file
View file

@ -0,0 +1,151 @@
name: wai-quantum command line
# The wai-quantum command line (wai/wai-quantum-cli): its tests and clippy with
# warnings as errors, with and without `submit`; its smoke checks (smoke.sh: lint,
# plan, run, verify and a forged result refused, key, draw, MCP over stdio, the
# research tools); and the WASI component, whose output under wasmtime equals the
# native build's byte for byte. On macOS, also with the GPU state vector.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: [main]
paths:
- 'wai/wai-quantum/**'
- 'wai/wai-quantum-cli/**'
- 'rust-toolchain.toml'
- '.github/workflows/wai-quantum-cli.yml'
pull_request:
paths:
- 'wai/wai-quantum/**'
- 'wai/wai-quantum-cli/**'
- 'rust-toolchain.toml'
- '.github/workflows/wai-quantum-cli.yml'
jobs:
relevant:
name: does this change touch it
runs-on: linux-arm64
outputs:
run: ${{ steps.gate.outputs.run }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
- id: gate
run: |
set -euo pipefail
r=$(python3 scripts/pr-touches-paths.py .github/workflows/wai-quantum-cli.yml \
"${{ github.event.pull_request.base.sha }}" \
"${{ github.event.pull_request.head.sha }}")
echo "this change touches the wai-quantum command line: $r"
echo "run=$r" >> "$GITHUB_OUTPUT"
cli-linux:
name: command line · linux
needs: relevant
if: needs.relevant.outputs.run == 'true'
runs-on: linux
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
- name: Install Rust 1.98.0 with clippy and 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 --component clippy
rustup default 1.98.0
rustup target add --toolchain 1.98.0 wasm32-wasip2
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Tests and clippy, with and without submit
shell: bash
working-directory: wai/wai-quantum-cli
run: |
set -euo pipefail
cargo test --locked --release 2>&1 | tee "$RUNNER_TEMP/test.log"
grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; }
cargo clippy --locked --release --all-targets -- -D warnings
cargo clippy --locked --release --all-targets --no-default-features -- -D warnings
- name: The smoke checks
shell: bash
working-directory: wai/wai-quantum-cli
run: |
set -euo pipefail
cargo build --locked --release
bash smoke.sh ./target/release/wai-quantum
# 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"
# The same source as a WASI component: a circuit's state hash, a run's
# result and a receipt signed with the published key are the native
# build's, byte for byte.
- name: The WASI component gives the native build's bytes
shell: bash
working-directory: wai/wai-quantum-cli
run: |
set -euo pipefail
cargo build --locked --release --target wasm32-wasip2 --no-default-features
w=target/wasm32-wasip2/release/wai-quantum.wasm
n=target/release/wai-quantum
printf 'OPENQASM 3.0; qubit[3] q; h q[0]; cx q[0], q[1]; t q[2]; cx q[1], q[2];' > "$RUNNER_TEMP/p.qasm"
for args in "lab run ghz -n 5" "lab receipt ghz -n 3 --demo-key"; do
$n $args > "$RUNNER_TEMP/native.txt" 2> /dev/null
wasmtime run "$w" $args > "$RUNNER_TEMP/wasi.txt" 2> /dev/null
cmp "$RUNNER_TEMP/native.txt" "$RUNNER_TEMP/wasi.txt" || { echo "::error::$args differs under wasmtime"; diff "$RUNNER_TEMP/native.txt" "$RUNNER_TEMP/wasi.txt" || true; exit 1; }
done
$n run --demo-key --json --shots 100 < "$RUNNER_TEMP/p.qasm" 2> /dev/null | python3 -c 'import json,sys; j=json.load(sys.stdin); j["result"].pop("elapsed_ms"); j["result"]["receipt"].pop("digest"); j["result"]["receipt"].pop("signature"); print(json.dumps(j, sort_keys=True))' > "$RUNNER_TEMP/native.json"
wasmtime run "$w" run --demo-key --json --shots 100 < "$RUNNER_TEMP/p.qasm" 2> /dev/null | python3 -c 'import json,sys; j=json.load(sys.stdin); j["result"].pop("elapsed_ms"); j["result"]["receipt"].pop("digest"); j["result"]["receipt"].pop("signature"); print(json.dumps(j, sort_keys=True))' > "$RUNNER_TEMP/wasi.json"
cmp "$RUNNER_TEMP/native.json" "$RUNNER_TEMP/wasi.json" || { echo "::error::a run's result differs under wasmtime"; exit 1; }
echo "the WASI component's output equals the native build's"
cli-macos:
name: command line · macos
needs: relevant
if: needs.relevant.outputs.run == 'true'
runs-on: macos
timeout-minutes: 60
steps:
- uses: actions/checkout@v5
# launchd's PATH omits cargo on the macOS host runner.
- name: Put cargo on PATH
shell: bash
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Tests and clippy, with the GPU state vector
shell: bash
working-directory: wai/wai-quantum-cli
run: |
set -euo pipefail
nice -n 10 cargo test --locked --release --features metal 2>&1 | tee "$RUNNER_TEMP/test.log"
grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/test.log" || { echo "::error::no test ran"; exit 1; }
nice -n 10 cargo clippy --locked --release --all-targets --features metal -- -D warnings
- name: The smoke checks, and a run on the GPU
shell: bash
working-directory: wai/wai-quantum-cli
run: |
set -euo pipefail
nice -n 10 cargo build --locked --release --features metal
bash smoke.sh ./target/release/wai-quantum
printf 'OPENQASM 3.0; qubit[12] q; bit[12] c; h q; rx(0.3) q[0]; cx q[0], q[1]; c = measure q;' > "$RUNNER_TEMP/gpu.qasm"
./target/release/wai-quantum run "$RUNNER_TEMP/gpu.qasm" --method statevector_single --shots 200 --demo-key --json > "$RUNNER_TEMP/gpu.json" 2> /dev/null
python3 -c 'import json,sys; j=json.load(open(sys.argv[1])); r=j["result"]; assert r["accuracy"]["class"]=="single" and sum(r["counts"].values())==200, r' "$RUNNER_TEMP/gpu.json"
echo "statevector_single ran on $(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["result"]["accuracy"]["device"])' "$RUNNER_TEMP/gpu.json")"

View file

@ -149,6 +149,28 @@ jobs:
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \
cargo test --locked --target wasm32-unknown-unknown 2>&1 | tee "$RUNNER_TEMP/wai-web-wasm.log"
grep -q "test result: ok. 4 passed; 0 failed" "$RUNNER_TEMP/wai-web-wasm.log"
# The job service's engine in a page (src/quantum_engine.rs, feature
# quantum_engine, which only the IDE's bundle carries): a job planned, run and
# signed in wasm32 under Node, its receipt checked there and a changed result
# refused, and every method a tab holds run with no clock and no threads. The
# count is the four tests above and its two.
- name: The job engine in a page (wasm32 tests under Node)
shell: bash
working-directory: wai/wai-web
run: |
set -euo pipefail
CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER=wasm-bindgen-test-runner \
cargo test --locked --target wasm32-unknown-unknown --features quantum_engine 2>&1 | tee "$RUNNER_TEMP/wai-web-engine-wasm.log"
grep -q "test result: ok. 6 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine-wasm.log"
# Its native half: the service's form and receipt, any angle on the state
# vector, what does not run saying why, and plan, lint, draw and methods as JSON.
- name: The job engine in a page (native unit tests)
shell: bash
working-directory: wai/wai-web
run: |
set -euo pipefail
cargo test --locked --lib --features quantum_engine quantum_engine:: 2>&1 | tee "$RUNNER_TEMP/wai-web-engine.log"
grep -q "test result: ok. 4 passed; 0 failed" "$RUNNER_TEMP/wai-web-engine.log"
# The crate's native unit tests, run on the host: the pure-Rust halves of the
# bindings, which wasm_tests does not run. They decode the committed integer
# payloads to their sidecars' SHA-256 (int_payload::tests) and check that the
@ -161,8 +183,10 @@ jobs:
# 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 OTT demo seals a figure the page supplies as
# parameter on a gate other than P, and shows no hash for an invalid circuit;
# its import reads a program as the job service does and refuses one that
# measures and then acts, its replies stay valid JSON whatever an error says,
# and it signs with the page's own key once given one (quantum_toolchain::tests). The OTT demo seals a figure the page supplies as
# an estimate, never a reading (video_ott::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
@ -171,7 +195,7 @@ jobs:
run: |
set -euo pipefail
cargo test --locked --lib 2>&1 | tee "$RUNNER_TEMP/wai-web-native.log"
grep -q "test result: ok. 32 passed; 0 failed" "$RUNNER_TEMP/wai-web-native.log"
grep -q "test result: ok. 35 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

View file

@ -6,6 +6,63 @@ wire format may change and such changes are recorded here.
## v0.4.0 — current
### wai-quantum 0.6.11 and wai-quantum-cli 0.1.0: one job engine for the service, the command line and a browser tab
Additive, apart from two refusals and an admission noted below. Every pinned digest is unchanged.
- **`wai_quantum::engine`** (feature `quantum_engine`) is the job service's engine, moved into the library.
- `catalog` plans a job onto the strongest method it admits, and says why.
- `run` carries the plan out.
- `dynamic` runs programs that measure in the middle.
- `receipt` is the service's receipt form: a result sealed over the request and the result in their RFC 8785 canonical text.
- `job` takes a job end to end and checks it.
- `draw` writes a program's circuit as text.
The service imports all of these. A job now gives the same result and the same receipt form whether it runs on the service, on the command line or in a browser tab.
- **The engine builds for `wasm32-unknown-unknown`.** There a result carries no `elapsed_ms`, since a tab has no clock std can read. The multi-seed cut-rank order search runs its seeds one after another and finds the same orders.
- **A noise channel the model does not know is refused** (`catalog::NOISE_CHANNELS`). It was read as no noise, so a misspelt channel ran the circuit noiseless.
- **The job's memory margin is counted once for a program that measures in the middle on the state vector,** as it is for trajectories. It was counted once per state, and the service refused a qubit it could hold.
- **`quantum_receipt::did_key`** gives an Ed25519 key's `did:key`, held to the W3C method's example.
- **`wai-quantum-cli` 0.1.0** is a new crate whose binary is `wai-quantum`. Its commands:
- `run`: planned and run as the service runs it, signed with the user's key;
- `plan`, `lint` and `draw`;
- `submit` to the service;
- `verify`, for any receipt this tool, the service or `lab` signs;
- `key`, `methods` and `completions`;
- `mcp`, which serves these tools to an agent over stdio;
- `lab`, which holds the earlier command's research tools.
It depends on the library alone. The `wai-quantum` bin and the `cli_keys` feature leave wai-rs.
- **wai-web, feature `quantum_engine`:** `QuantumEngine` (run, plan, methods and signer), `engine_lint`, `engine_draw` and `engine_verify`, for a page. Tested in wasm32 under Node on every method a tab holds.
### wai-quantum 0.6.10: one OpenQASM reader for the service, the command line and the browser IDE
Additive, apart from the old line reader's refusals below. Every pinned digest is unchanged.
- **`wai_quantum::qasm`** (feature `quantum_qasm`) is the job service's OpenQASM reader,
moved into the library unchanged. The service, `wai-quantum run --qasm` and the browser
IDE's import now read a program the same way. Before, the IDE read two registers as
one, skipped a gate on a whole register, and read past what it did not know.
- **`qasm::dyadic_circuit`** gives the exact state vector's circuit for a program whose
measurements all come at the end, and refuses one that measures and then acts, resets
after a gate or uses `if`, with the reason.
- **`quantum_toolchain::from_qasm`**, the older line reader, refuses what it cannot take
instead of dropping the line: more than one register, `reset`, gate definitions,
`if`/`while`/`for`, a gate after a measurement, two statements on one line. Each
refusal names `wai_quantum::qasm`.
- **The IDE binding** (`wai-web`) builds its JSON with `serde_json`, so a quote or a
control character in a program or an error no longer breaks a reply.
`set_signing_key`, `public_key_hex`, `signer_did` and `key_is_demo` let a page sign
receipts with a key of its own; without one it signs with the published demo key and
says so.
- **The command line** signs with `--key FILE`, `$WAI_QUANTUM_KEY`, or this user's key at
`$XDG_CONFIG_HOME/wai/quantum-key` (or `~/.config/wai/quantum-key`). That key is made
from the operating system's random source on first use, with mode 0600.
`--demo-key` keeps the published key, with a note that its receipts show a result is
unaltered, not who signed it.
- **A receipt from the command line or the IDE names its signer by its key's `did:key`**
(`wai::signer_id::did_key`), where it used to carry a label naming no key.
### wai-quantum 0.6.9: cut-rank amplitudes with the circuit's own global phase
Additive. Every pinned digest is unchanged.

View file

@ -207,39 +207,44 @@ reproduces ([quantum-sim](extensions/quantum-sim.md) §8).
### Quickstart
```bash
# native
cargo build --release --no-default-features \
--features "quantum_toolchain,quantum_receipt" --bin wai-quantum
The command line is [`wai-quantum-cli`](wai-quantum-cli/README.md): OpenQASM
programs run here as the quantum job service runs them, each result naming its
method and accuracy and signed with this user's key.
./target/release/wai-quantum list # algorithm catalog
./target/release/wai-quantum run ghz -n 5 # simulate + hash + outcomes
./target/release/wai-quantum run grover -n 4 -p 9 # amplitude amplification
./target/release/wai-quantum qasm bell # emit OpenQASM 3.0
./target/release/wai-quantum qir bell # emit QIR (LLVM IR)
./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
```bash
cd wai-quantum-cli && cargo build --release # or: cargo install wai-quantum-cli
W=./target/release/wai-quantum
$W run bell.qasm --shots 1000 -o "Z0 Z1" # planned as the service plans it
$W run bell.qasm --json > job.json && $W verify job.json
$W plan prog.qasm; $W lint prog.qasm; $W draw prog.qasm
$W submit prog.qasm # on the job service
$W lab list # the algorithm catalogue
$W lab run ghz -n 5 # simulate + hash + outcomes
$W lab qasm bell # emit OpenQASM 3.0
$W lab qir qft -n 4 | $W lab run --qir # QIR out, and back in
$W lab receipt ghz -n 3 | $W verify
$W lab run qft -n 5 --gateset v1 # the frozen wai.quantum.circuit
```
Circuits are `wai.quantum.circuit2` unless `--gateset v1` asks for
Circuits built by `lab` 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
cargo build --release --target wasm32-wasip2 --no-default-features \
--features "quantum_toolchain,quantum_receipt" --bin wai-quantum
cd wai-quantum-cli && cargo build --release --target wasm32-wasip2 --no-default-features
wasmtime run target/wasm32-wasip2/release/wai-quantum.wasm run ghz -n 5
wasmtime run target/wasm32-wasip2/release/wai-quantum.wasm lab run ghz -n 5
wasmtime run target/wasm32-wasip2/release/wai-quantum.wasm lab receipt ghz -n 3 --demo-key
```
Both print the identical `state-hash`. A receipt sealed in cloud WASM verifies
natively and vice-versa, and the receipt bytes themselves are identical — the
signature travels because the computation is reproducible, not because the two
runtimes agreed to trust each other.
natively and vice-versa, and with the same key (here the published demo key)
the receipt bytes themselves are identical: the signature travels because the
computation is reproducible, not because the two runtimes agreed to trust each
other.
### As a serverless function

View file

@ -1017,7 +1017,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -599,7 +599,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -713,7 +713,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",

1348
wai/wai-quantum-cli/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,33 @@
[package]
name = "wai-quantum-cli"
version = "0.1.0"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
description = "The wai-quantum command line: OpenQASM programs run on this machine as the quantum job service runs them (the strongest method each admits, its accuracy named, the result signed with the user's key), sent to the service, linted, drawn and verified; an MCP server for agents over stdio; and the research tools (error correction, DMRG, neural quantum states, resource estimates)."
repository = "https://git.transaction.science/Transaction-Science/open-standards"
homepage = "https://wai.transaction.science/quantum-run"
keywords = ["quantum", "openqasm", "simulation", "cli", "mcp"]
categories = ["command-line-utilities", "science", "simulation"]
readme = "README.md"
[[bin]]
name = "wai-quantum"
path = "src/main.rs"
[features]
default = ["service"]
# `submit`: send a job to the quantum job service over HTTPS.
service = ["dep:ureq"]
# The single-precision state vector on the GPU (`--method statevector_single`), on macOS.
metal = ["wai-quantum/quantum_sv_metal"]
[dependencies]
clap = { version = "4.6", features = ["derive", "env", "wrap_help"] }
clap_complete = "4.6"
getrandom = "0.4"
# float_roundtrip: a number read back reads as the double it was, so a
# result's receipt still verifies after any JSON writer has carried it.
serde_json = { version = "1", features = ["float_roundtrip"] }
ureq = { version = "3", optional = true, features = ["json"] }
wai-quantum = { version = "0.6.11", path = "../wai-quantum", default-features = false, features = ["full"] }

View file

@ -0,0 +1,129 @@
# wai-quantum
The command line of the WAI quantum stack. It runs OpenQASM programs on this machine, planned and run by the same engine as the quantum job service ([`wai_quantum::engine`](../wai-quantum/src/engine/mod.rs)), so a job gives the same result here as there. It also sends programs to the service, checks and draws them, verifies receipts, and serves its tools to agents over MCP.
```sh
cargo install wai-quantum-cli # installs the `wai-quantum` binary
```
## Running a program
```sh
wai-quantum run bell.qasm --shots 1000
wai-quantum run prog.qasm -o "Z0 Z1" -o "0.5 X2" -a 000 -a 111
wai-quantum run prog.qasm --noise depolarizing_2q=0.01 --noise readout=0.02
wai-quantum run prog.qasm --method mps --option max_bond=128
cat prog.qasm | wai-quantum run --json > job.json
```
A program is OpenQASM 3, or the OpenQASM 2 forms in common use. The reader accepts:
- registers, and a gate applied to a whole register;
- the standard gates;
- `ctrl` and `negctrl` modifiers;
- gate definitions;
- measurement and `reset` anywhere, and `if` on measured bits.
It refuses loops, `inv` and `pow`, giving the line of each.
`--method auto` (the default) picks the strongest method the program admits:
| Method | Takes |
|---|---|
| exact | dyadic angles; the state vector, the same bits on every machine |
| statevector | any angle |
| stabilizer | Clifford circuits, to tens of thousands of qubits |
| mps | matrix product states |
| pauli | Pauli dynamics, for expectation values |
| tensor_network | amplitudes |
| cutrank | a Clifford circuit with a layer of rotations |
| density_matrix, trajectories | noise |
`wai-quantum methods` lists each method's reach in a job's memory here (`--memory-gb`, default 16).
Each result names:
- its method, and why that method was chosen;
- its accuracy class;
- the counts, expectation values or amplitudes asked for;
- its receipt.
## Receipts
Every result is signed over the request and the result in their canonical JSON. The service signs with its key; this tool signs with this user's key:
- `--key FILE`, or `$WAI_QUANTUM_KEY`, names a file holding the key;
- otherwise the key is `$XDG_CONFIG_HOME/wai/quantum-key`, or `~/.config/wai/quantum-key`. It is made from the system's random source on first use and is readable by its owner only.
`wai-quantum key` prints the key's `did:key`. `--demo-key` signs with the published demo key. Its receipts show only that a result is unaltered, not who signed it.
```sh
wai-quantum run prog.qasm --json > job.json
wai-quantum verify job.json # VALID, and the signer's did:key
wai-quantum submit prog.qasm --json > svc.json
wai-quantum verify svc.json # the same check on the service's receipt
```
A receipt shows that this key signed this request and this result, and anyone can rerun the request to compare. It does not say who holds the key.
## Checking and drawing
```sh
wai-quantum lint prog.qasm # ok, with its shape, or the line of the first problem
wai-quantum plan prog.qasm # the method and why, the memory and work, without running
wai-quantum draw prog.qasm --width 80
```
```text
q0: ─────H─────●──M0──────
│
q1: ───────────⊕──●───M1──
│
q2: ──Rz(π/4)─────⊕───M2──
```
## The service
`wai-quantum submit` sends a job to `https://wai.transaction.science/quantum-jobs`, or to `--service URL` (also read from `$WAI_QUANTUM_SERVICE`). It waits for a long job to finish, then prints the result just as `run` does.
## For agents
`wai-quantum mcp` is a Model Context Protocol server on standard input and output. Its tools are:
- `run_quantum_circuit`
- `plan_quantum_circuit`
- `lint_qasm`
- `draw_circuit`
- `verify_quantum_result`
- `list_methods`
Runs happen on this machine and are signed with this user's key; nothing is sent anywhere. In an MCP client's configuration:
```json
{ "mcpServers": { "wai-quantum": { "command": "wai-quantum", "args": ["mcp"] } } }
```
## The research tools
`wai-quantum lab …` holds the research tools:
- algorithms from the catalogue, OpenQASM and QIR export, and receipts over them;
- error correction at circuit level;
- qLDPC memory;
- fault-tolerant resource estimates and architectures;
- DMRG, TDVP and molecules;
- neural quantum states;
- Gaussian boson sampling;
- optimisation;
- the compute fabric;
- information and transport.
`wai-quantum lab help` lists them. `wai-quantum verify` also checks the receipts they sign.
## Builds
- **Default:** `submit`, over HTTPS.
- **`--features metal`:** the single-precision state vector on the GPU (`--method statevector_single`), on macOS.
- **`--no-default-features --target wasm32-wasip2`:** a WASI component without `submit`. It gives the same output under any WASI runtime.
`wai-quantum completions zsh` (or bash, fish, elvish, PowerShell) prints shell completion.

View file

@ -0,0 +1,111 @@
#!/usr/bin/env bash
# smoke.sh: the command line as a reader uses it, every check exact.
#
# bash smoke.sh path/to/wai-quantum [SERVICE_URL]
#
# With SERVICE_URL, `submit` sends one job to the service there and its
# receipt is checked here. Each command's output is written to a file before
# it is read, so no check depends on a pipe closing early.
set -euo pipefail
W="$1"
SVC="${2:-}"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
export XDG_CONFIG_HOME="$tmp/config"
unset WAI_QUANTUM_KEY
fail() { echo "smoke: $*" >&2; exit 1; }
has() { grep -q -- "$2" "$1" || { echo "--- $1:" >&2; cat "$1" >&2; fail "expected: $2"; }; }
cat > "$tmp/ghz.qasm" <<'EOF'
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
bit[3] c;
h q[0];
cx q[0], q[1];
cx q[1], q[2];
c = measure q;
EOF
"$W" lint "$tmp/ghz.qasm" > "$tmp/lint"
has "$tmp/lint" "ok — 3 qubits, 3 gates, 3 bits measured"
printf 'OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n' > "$tmp/bad.qasm"
if "$W" lint "$tmp/bad.qasm" > "$tmp/lint-bad"; then fail "lint passed a program with an unknown gate"; fi
has "$tmp/lint-bad" "bad.qasm:3: unknown gate foo"
echo "lint: ok"
"$W" plan "$tmp/ghz.qasm" --json > "$tmp/plan.json"
python3 -c 'import json,sys; p=json.load(open(sys.argv[1])); assert p["method"]=="stabilizer" and p["qubits"]==3, p' "$tmp/plan.json"
echo "plan: ok, stabilizer"
"$W" run "$tmp/ghz.qasm" --shots 1000 -o "Z0 Z2" --json > "$tmp/job.json" 2> "$tmp/run.err"
has "$tmp/run.err" "made a signing key"
python3 - "$tmp/job.json" <<'EOF'
import json, sys
j = json.load(open(sys.argv[1]))
r = j["result"]
assert j["status"] == "done" and j["method"] == "stabilizer", j
assert set(r["counts"]) <= {"000", "111"} and sum(r["counts"].values()) == 1000, r["counts"]
assert r["expectations"][0]["value"] == 1.0, r["expectations"]
assert r["receipt"]["alg"] == "ed25519" and len(r["receipt"]["key"]) == 64, r["receipt"]
assert j["request"] == {"qasm": j["request"]["qasm"], "shots": 1000, "observables": ["Z0 Z2"]}, j["request"]
EOF
"$W" verify "$tmp/job.json" > "$tmp/verify"
has "$tmp/verify" "^signature VALID"
python3 -c 'import json,sys; j=json.load(open(sys.argv[1])); j["result"]["counts"]={"000":1000}; json.dump(j,open(sys.argv[2],"w"))' "$tmp/job.json" "$tmp/forged.json"
if "$W" verify "$tmp/forged.json" > "$tmp/verify-forged"; then fail "a changed result verified"; fi
has "$tmp/verify-forged" "^signature INVALID"
# The same user, the same key; the signer is the key's did:key.
"$W" key > "$tmp/key" 2> /dev/null
has "$tmp/key" "^did:key:z6Mk"
has "$tmp/verify" "^signer $(cat "$tmp/key")"
echo "run, verify, key: ok, signed by $(cat "$tmp/key")"
"$W" run "$tmp/ghz.qasm" > "$tmp/run-text"
has "$tmp/run-text" "^method stabilizer"
has "$tmp/run-text" "^receipt verifies"
if "$W" run "$tmp/ghz.qasm" --shot 5 > /dev/null 2> "$tmp/typo"; then fail "an unknown flag was accepted"; fi
has "$tmp/typo" "unexpected argument '--shot'"
if "$W" run "$tmp/ghz.qasm" --noise depolarizing=0.1 > /dev/null 2> "$tmp/noise"; then fail "an unknown noise channel was accepted"; fi
has "$tmp/noise" "no such channel"
echo "refusals: ok"
"$W" draw "$tmp/ghz.qasm" > "$tmp/draw"
has "$tmp/draw" "^q0: .*H.*●"
has "$tmp/draw" "^q2: .*⊕.*M2"
echo "draw: ok"
printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18"}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
'{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"run_quantum_circuit","arguments":{"qasm":"OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];","observables":["Z0 Z1"]}}}' \
> "$tmp/mcp.in"
"$W" mcp < "$tmp/mcp.in" > "$tmp/mcp.out" 2> /dev/null
python3 - "$tmp/mcp.out" <<'EOF'
import json, sys
lines = [json.loads(l) for l in open(sys.argv[1])]
assert [m["id"] for m in lines] == [1, 2, 3], lines
assert lines[0]["result"]["serverInfo"]["name"] == "wai-quantum"
assert "run_quantum_circuit" in [t["name"] for t in lines[1]["result"]["tools"]]
job = lines[2]["result"]["structuredContent"]
assert job["result"]["expectations"][0]["value"] == 1.0, job
EOF
echo "mcp: ok"
"$W" lab list > "$tmp/lab-list"
has "$tmp/lab-list" "^ghz "
"$W" lab receipt ghz -n 3 > "$tmp/lab-receipt" 2> /dev/null
"$W" verify "$tmp/lab-receipt" > "$tmp/lab-verify"
has "$tmp/lab-verify" "^signature VALID"
has "$tmp/lab-verify" "^signer $(cat "$tmp/key")"
echo "lab: ok"
if [ -n "$SVC" ]; then
"$W" submit "$tmp/ghz.qasm" --shots 500 --service "$SVC" --json > "$tmp/svc.json"
"$W" verify "$tmp/svc.json" > "$tmp/svc-verify"
has "$tmp/svc-verify" "^signature VALID"
echo "submit: ok, the service's receipt verifies here"
fi
echo "smoke: every check held"

View file

@ -0,0 +1,135 @@
//! The key a receipt is signed with.
//!
//! - `--demo-key`: the published demo key, for receipts that repeat byte for
//! byte everywhere. Anyone can sign with it, so it proves nothing about who
//! signed.
//! - `--key FILE`, or `$WAI_QUANTUM_KEY` naming a file: that key.
//! - Otherwise this user's key, `$XDG_CONFIG_HOME/wai/quantum-key` (or
//! `~/.config/wai/quantum-key`), made from the operating system's random
//! source on first use and readable by its owner only.
//!
//! A key file holds the 32-byte Ed25519 seed as 64 hex digits.
use std::path::{Path, PathBuf};
use wai_quantum::ed25519_dalek::SigningKey;
/// The published demo key's seed.
pub const DEMO_SEED: [u8; 32] = [42; 32];
/// The key to sign with, and where it came from.
pub struct Key {
pub key: SigningKey,
/// The file it was read from or made in; `None` for the demo key.
pub path: Option<PathBuf>,
/// Whether this call made the file.
pub made: bool,
}
/// This user's key file: `$XDG_CONFIG_HOME/wai/quantum-key`, or
/// `~/.config/wai/quantum-key`.
pub fn user_path() -> Option<PathBuf> {
let base = std::env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| Path::new(&h).join(".config")))?;
Some(base.join("wai").join("quantum-key"))
}
fn parse(text: &str) -> Option<[u8; 32]> {
let t = text.trim();
if t.len() != 64 {
return None;
}
let mut out = [0u8; 32];
for (i, b) in out.iter_mut().enumerate() {
*b = u8::from_str_radix(t.get(2 * i..2 * i + 2)?, 16).ok()?;
}
Some(out)
}
/// The key `demo` and `given` (`--demo-key`, `--key FILE`) name: a named file
/// must exist; this user's own is made when there is none.
pub fn load(given: Option<&Path>, demo: bool) -> Result<Key, String> {
if demo {
return Ok(Key { key: SigningKey::from_bytes(&DEMO_SEED), path: None, made: false });
}
let named = given.map(Path::to_path_buf).or_else(|| std::env::var_os("WAI_QUANTUM_KEY").map(PathBuf::from));
let path = match &named {
Some(p) => p.clone(),
None => user_path().ok_or("no HOME to keep a key in: pass --key FILE")?,
};
match std::fs::read_to_string(&path) {
Ok(text) => {
let seed = parse(&text).ok_or_else(|| format!("{}: a key file holds 64 hex digits", path.display()))?;
Ok(Key { key: SigningKey::from_bytes(&seed), path: Some(path), made: false })
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound && named.is_none() => {
let mut seed = [0u8; 32];
getrandom::fill(&mut seed).map_err(|e| format!("no random source for a new key: {e}"))?;
if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| format!("{}: {e}", dir.display()))?;
}
let text: String = seed.iter().map(|b| format!("{b:02x}")).collect();
write_private(&path, &format!("{text}\n")).map_err(|e| format!("{}: {e}", path.display()))?;
Ok(Key { key: SigningKey::from_bytes(&seed), path: Some(path), made: true })
}
Err(e) => Err(format!("{}: {e}", path.display())),
}
}
fn write_private(path: &Path, text: &str) -> std::io::Result<()> {
use std::io::Write as _;
let mut f = std::fs::OpenOptions::new();
f.write(true).create_new(true);
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
f.mode(0o600);
}
f.open(path)?.write_all(text.as_bytes())
}
/// What `--demo-key` and a new key say on standard error.
pub fn notes(k: &Key) {
match &k.path {
None => eprintln!("note: signed with the published demo key; the receipt shows the result is unaltered, not who signed it"),
Some(p) if k.made => eprintln!("note: made a signing key at {}", p.display()),
Some(_) => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_key_is_made_once_private_then_read_and_a_named_file_must_exist() {
let dir = std::env::temp_dir().join(format!("wai-quantum-cli-key-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let path = dir.join("k");
// A named file that is not there is refused, never made.
assert!(load(Some(&path), false).is_err());
assert!(!path.exists());
// The demo key is the published one.
let demo = load(None, true).unwrap();
assert_eq!(demo.key.to_bytes(), DEMO_SEED);
assert!(demo.path.is_none());
// A written key reads back; a malformed one is refused.
std::fs::create_dir_all(&dir).unwrap();
let seed: String = (0..32).map(|i| format!("{:02x}", i * 7)).collect();
std::fs::write(&path, format!("{seed}\n")).unwrap();
let k = load(Some(&path), false).unwrap();
assert_eq!(k.key.to_bytes()[1], 7);
assert!(!k.made);
std::fs::write(&path, "not a key").unwrap();
assert!(load(Some(&path), false).is_err());
// A made key is the owner's alone.
let fresh = dir.join("fresh");
write_private(&fresh, "x").unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
assert_eq!(std::fs::metadata(&fresh).unwrap().permissions().mode() & 0o777, 0o600);
}
let _ = std::fs::remove_dir_all(&dir);
}
}

View file

@ -1,4 +1,5 @@
//! `wai-quantum` — the deterministic quantum stack as a command-line tool.
//! `wai-quantum lab`: the research tools of the command line, the deterministic
//! quantum stack's every capability.
//!
//! Every quantum capability in this crate is pure Rust with no host bindings, so
//! this binary targets **native and cloud WASM from one source**:
@ -11,12 +12,12 @@
//! and corpus digest.
//!
//! ```text
//! wai-quantum list
//! wai-quantum run ghz --qubits 5 [--top 8] [--json]
//! wai-quantum run --qasm # read OpenQASM on stdin
//! wai-quantum qasm bell # emit OpenQASM
//! wai-quantum receipt ghz -n 3 [--shots 4096 --seed 7]
//! wai-quantum verify # read a receipt JSON on stdin
//! wai-quantum lab list
//! wai-quantum lab run ghz --qubits 5 [--top 8] [--json]
//! wai-quantum lab run --qasm # read OpenQASM on stdin
//! wai-quantum lab qasm bell # emit OpenQASM
//! wai-quantum lab receipt ghz -n 3 [--shots 4096 --seed 7]
//! wai-quantum lab verify # read a receipt JSON on stdin
//! ```
//!
//! A circuit is built in `wai.quantum.circuit2`, whose phase table is correctly
@ -29,20 +30,20 @@
//! what transport does to it:
//!
//! ```text
//! wai-quantum entropy bell --keep 0 # half a Bell pair: 1 bit, and no state vector
//! wai-quantum channel depolarizing --prob 0.5
//! wai-quantum bb84 --eavesdrop # interception shows up in the error rate
//! wai-quantum chsh # 2√2, exactly, not sampled
//! wai-quantum qutrit # d = 3, not just qubits
//! wai-quantum lab entropy bell --keep 0 # half a Bell pair: 1 bit, and no state vector
//! wai-quantum lab channel depolarizing --prob 0.5
//! wai-quantum lab bb84 --eavesdrop # interception shows up in the error rate
//! wai-quantum lab chsh # 2√2, exactly, not sampled
//! wai-quantum lab qutrit # d = 3, not just qubits
//! ```
//!
//! At utility scale, sparse Pauli dynamics computes observables of circuits far
//! past any state vector, with arbitrary rotation angles:
//!
//! ```text
//! wai-quantum utility --theta 0.2 --steps 20 # the 127-qubit heavy-hex experiment
//! wai-quantum utility --method bptn --chi 32 # the same, by tensor networks
//! wai-quantum utility --receipt | wai-quantum verify
//! wai-quantum lab utility --theta 0.2 --steps 20 # the 127-qubit heavy-hex experiment
//! wai-quantum lab utility --method bptn --chi 32 # the same, by tensor networks
//! wai-quantum lab utility --receipt | wai-quantum verify
//! ```
//!
//! And for fault-tolerant machines that do not exist yet, what a program would
@ -50,10 +51,10 @@
//! signed estimate:
//!
//! ```text
//! wai-quantum estimate factoring # 2048-bit factoring, all six qubits
//! wai-quantum estimate chemistry --qubit ns-e4 --frontier
//! wai-quantum estimate --qubits 100 --ccz 1e6 --budget 0.01
//! wai-quantum estimate factoring --qubit ns-e4 --receipt | wai-quantum verify
//! wai-quantum lab estimate factoring # 2048-bit factoring, all six qubits
//! wai-quantum lab estimate chemistry --qubit ns-e4 --frontier
//! wai-quantum lab estimate --qubits 100 --ccz 1e6 --budget 0.01
//! wai-quantum lab estimate factoring --qubit ns-e4 --receipt | wai-quantum verify
//! ```
//!
//! Error correction itself, at circuit level: sample a noisy Clifford circuit,
@ -61,9 +62,9 @@
//! model that estimates otherwise take on trust:
//!
//! ```text
//! wai-quantum qec -d 5 --p 0.003 --shots 100000
//! wai-quantum qec --file circuit.txt
//! wai-quantum qec --fit # then: estimate --prefactor A --threshold P
//! wai-quantum lab qec -d 5 --p 0.003 --shots 100000
//! wai-quantum lab qec --file circuit.txt
//! wai-quantum lab qec --fit # then: estimate --prefactor A --threshold P
//! ```
//!
//! The `channel` command prints the Holevo bound before and after, which can only
@ -74,18 +75,19 @@
use wai_quantum::ed25519_dalek::SigningKey;
use std::io::Read;
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};
use wai::quantum_qudit::QuditState;
use wai::quantum_energy::{EnergyClass, MaybeLabelled};
use wai::quantum_receipt::QuantumReceipt;
use wai::quantum_resource::{self as qr, EstimateConfig, LogicalCounts, PhysicalQubit, QecModel};
use wai::quantum_resource_receipt::{Program, ResourceJob, ResourceReceipt};
use wai::quantum_spd_receipt::{SpdJob, SpdReceipt};
use wai::quantum_source::{holevo_bound, schumacher_limit, von_neumann_entropy};
use wai::quantum_toolchain as qt;
use wai_quantum::quantum::{BaseGate, Circuit, Gateset, Measure, ONE};
use wai_quantum::quantum_channel::{tomography, Channel};
use wai_quantum::quantum_comm::{bb84, clone_fidelity};
use wai_quantum::quantum_info::{chsh, DensityMatrix};
use wai_quantum::quantum_qudit::QuditState;
use wai_quantum::quantum_energy::{EnergyClass, MaybeLabelled};
use wai_quantum::quantum_receipt::QuantumReceipt;
use wai_quantum::quantum_receipt::did_key;
use wai_quantum::quantum_resource::{self as qr, EstimateConfig, LogicalCounts, PhysicalQubit, QecModel};
use wai_quantum::quantum_resource_receipt::{Program, ResourceJob, ResourceReceipt};
use wai_quantum::quantum_spd_receipt::{SpdJob, SpdReceipt};
use wai_quantum::quantum_source::{holevo_bound, schumacher_limit, von_neumann_entropy};
use wai_quantum::quantum_toolchain as qt;
/// The three single-qubit states the channel command sends: the ground state, the
/// excited one, and the superposition whose coherence noise actually destroys.
@ -141,75 +143,75 @@ fn die(msg: &str) -> ! {
fn usage() -> ! {
eprintln!(
"wai-quantum — deterministic quantum computation (native + cloud WASM)\n\n\
"wai-quantum lab — the research tools (run `wai-quantum --help` for run, plan, lint, draw, verify and the rest)\n\n\
USAGE:\n \
wai-quantum list\n \
wai-quantum run <algorithm> [-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 <algorithm> [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \
wai-quantum qir <algorithm> [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \
wai-quantum receipt <algorithm> [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2]\n \
wai-quantum verify (receipt JSON on stdin)\n \
wai-quantum lab list\n \
wai-quantum lab run <algorithm> [-n QUBITS] [-p PARAM] [--top K] [--json] [--gateset v1|v2]\n \
wai-quantum lab run --qasm [--top K] [--json] [--gateset v1|v2] (OpenQASM on stdin)\n \
wai-quantum lab run --qir [--top K] [--json] [--gateset v1|v2] (QIR on stdin)\n \
wai-quantum lab qasm <algorithm> [-n QUBITS] [-p PARAM] [--gateset v1|v2]\n \
wai-quantum lab qir <algorithm> [-n QUBITS] [-p PARAM] [--no-measure] [--gateset v1|v2]\n \
wai-quantum lab receipt <algorithm> [-n QUBITS] [-p PARAM] [--shots N] [--seed S] [--gateset v1|v2] [--key FILE | --demo-key]\n \
wai-quantum lab 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 <algorithm> [-n QUBITS] [--keep 0,1] [--json] [--gateset v1|v2]\n \
wai-quantum channel <name> [--prob P] [--state zero|one|plus] [--json]\n \
wai-quantum bb84 [--rounds N] [--seed S] [--eavesdrop] [--json]\n \
wai-quantum chsh [--json]\n \
wai-quantum qutrit [-n QUTRITS] [--json]\n\n\
wai-quantum lab entropy <algorithm> [-n QUBITS] [--keep 0,1] [--json] [--gateset v1|v2]\n \
wai-quantum lab channel <name> [--prob P] [--state zero|one|plus] [--json]\n \
wai-quantum lab bb84 [--rounds N] [--seed S] [--eavesdrop] [--json]\n \
wai-quantum lab chsh [--json]\n \
wai-quantum lab qutrit [-n QUTRITS] [--json]\n\n\
UTILITY SCALE:\n \
wai-quantum utility [--theta T] [--steps N] [--threshold D] [-q QUBIT] [--json] [--receipt]\n \
wai-quantum utility --method bptn [--chi X] [--theta T] [--steps N] [-q QUBIT] [--json]\n\n\
wai-quantum lab utility [--theta T] [--steps N] [--threshold D] [-q QUBIT] [--json] [--receipt]\n \
wai-quantum lab utility --method bptn [--chi X] [--theta T] [--steps N] [-q QUBIT] [--json]\n\n\
FAULT-TOLERANT RESOURCES:\n \
wai-quantum estimate [factoring|chemistry] [--qubit NAME|all] [--code surface|surface-meas|floquet]\n \
wai-quantum lab estimate [factoring|chemistry] [--qubit NAME|all] [--code surface|surface-meas|floquet]\n \
[--budget E] [--slowdown N] [--frontier] [--json] [--receipt]\n \
wai-quantum estimate --qubits N [--ccz N] [--t N] [--rotations N] [--depth N] [--measurements N] ...\n \
wai-quantum lab estimate --qubits N [--ccz N] [--t N] [--rotations N] [--depth N] [--measurements N] ...\n \
Qubits: us-e3, us-e4, ns-e3, ns-e4, meas-e4, meas-e6. A measured code: --prefactor A --threshold P.\n\n\
ERROR CORRECTION AT CIRCUIT LEVEL:\n \
wai-quantum qec [-d D] [--rounds R] [--p P] [--shots N] [--seed S] [--decoder exact|uf] [--json]\n \
wai-quantum qec --file CIRCUIT [--shots N] (the ecosystem's circuit text)\n \
wai-quantum qec [--circuit | --dem] [-d D | --file CIRCUIT]\n \
wai-quantum qec --fit [--shots N] (measure a and p* of the code model)\n\n\
wai-quantum lab qec [-d D] [--rounds R] [--p P] [--shots N] [--seed S] [--decoder exact|uf] [--json]\n \
wai-quantum lab qec --file CIRCUIT [--shots N] (the ecosystem's circuit text)\n \
wai-quantum lab qec [--circuit | --dem] [-d D | --file CIRCUIT]\n \
wai-quantum lab qec --fit [--shots N] (measure a and p* of the code model)\n\n\
GAUSSIAN BOSON SAMPLING:\n \
wai-quantum gbs --squeezing r1,r2,… [--eta E] [--seed S | --unitary U.json] [--json]\n \
wai-quantum gbs … --sample K (exact click patterns)\n \
wai-quantum gbs … --check SAMPLES.txt (observed patterns vs exact marginals)\n\n\
wai-quantum lab gbs --squeezing r1,r2,… [--eta E] [--seed S | --unitary U.json] [--json]\n \
wai-quantum lab gbs … --sample K (exact click patterns)\n \
wai-quantum lab gbs … --check SAMPLES.txt (observed patterns vs exact marginals)\n\n\
SPIN CHAINS (DMRG ground states, exact referees):\n \
wai-quantum dmrg [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--bond D] [--sweeps S]\n \
wai-quantum lab dmrg [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--bond D] [--sweeps S]\n \
[--cutoff C] [--exact] [--json]\n \
wai-quantum tdvp [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--start plus|neel|up]\n \
wai-quantum lab tdvp [--chain ising|heisenberg] [-n N] [--j J] [--jz JZ] [--h H] [--start plus|neel|up]\n \
[--bond D] [--dt DT] [--t T] [--every K] [--cutoff C] [--json] (time evolution by TDVP)\n\n\
MOLECULES (DMRG from FCIDUMP integrals, full CI as referee):\n \
wai-quantum chem --fcidump FILE [--bond D] [--sweeps S] [--penalty P] [--no-fci] [--json]\n\n\
wai-quantum lab chem --fcidump FILE [--bond D] [--sweeps S] [--penalty P] [--no-fci] [--json]\n\n\
NEURAL QUANTUM STATES (RBM, variational Monte Carlo, stochastic reconfiguration):\n \
wai-quantum nqs [--model heisenberg|j1j2|ising] [--lx L] [--ly L] [--j2 J] [--h H] [--periodic]\n \
wai-quantum lab nqs [--model heisenberg|j1j2|ising] [--lx L] [--ly L] [--j2 J] [--h H] [--periodic]\n \
[--complex] [--project [--kpi] [--point-group a1|a2|b1|b2 | --reflections a1|a2|b1|b2]\n \
[--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] [--gateset v1|v2] [--json]\n\n\
wai-quantum lab 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 \
wai-quantum lab 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 \
[--target V] [--seed S] [--threads T] [--spins] [--json]\n\n\
QLDPC MEMORY (circuit level, BP-OSD):\n \
wai-quantum ldpc [--code 72|90|108|144|288] [--p P] [--cycles N] [--shots S] [--basis z|x|both]\n \
wai-quantum lab ldpc [--code 72|90|108|144|288] [--p P] [--cycles N] [--shots S] [--basis z|x|both]\n \
[--max-iter I] [--osd-order K] [--threads T] [--seed S] [--json]\n \
wai-quantum ldpc --circuit [--code …] [--p P] [--cycles N] [--basis z|x] (the circuit text)\n\n\
wai-quantum lab ldpc --circuit [--code …] [--p P] [--cycles N] [--basis z|x] (the circuit text)\n\n\
ARCHITECTURE:\n \
wai-quantum arch factoring [--bits 1024|1536|2048|3072|4096|6144|8192] [--json]\n \
wai-quantum arch storage [--target T] [--fits refit|uniform|si1000] [--json]\n \
wai-quantum arch cultivate [--p 5e-4|1e-3|2e-3] [--target T] [--json]\n\n\
wai-quantum lab arch factoring [--bits 1024|1536|2048|3072|4096|6144|8192] [--json]\n \
wai-quantum lab arch storage [--target T] [--fits refit|uniform|si1000] [--json]\n \
wai-quantum lab arch cultivate [--p 5e-4|1e-3|2e-3] [--target T] [--json]\n\n\
NOISY CIRCUITS (trajectories, exact density matrix, cross-entropy):\n \
wai-quantum noisy --file CIRCUIT.jsonl [-n N] [--depol1 P] [--depol2 P] [--damping G] [--dephasing P]\n \
wai-quantum lab noisy --file CIRCUIT.jsonl [-n N] [--depol1 P] [--depol2 P] [--damping G] [--dephasing P]\n \
[--readout P] [--trajectories T] [--seed S] [--expect \"Z0 Z1; X3\"] [--xeb] [--exact] [--json]\n\n\
STATE VECTOR (any gate, any angle):\n \
wai-quantum sv --file CIRCUIT.jsonl [-n N] [--fuse W] [--top K | --sample S [--seed X] | --expect \"Z0 Z1; X3\"] [--json]\n\n\
wai-quantum lab sv --file CIRCUIT.jsonl [-n N] [--fuse W] [--top K | --sample S [--seed X] | --expect \"Z0 Z1; X3\"] [--json]\n\n\
TENSOR NETWORKS:\n \
wai-quantum tn --file CIRCUIT.jsonl [-n N] [--bits B] [--open q,q] [--trials T] [--seed S] [--cap-log2 L] [--whole-gates] [--plan-only] [--json]\n\n\
wai-quantum lab tn --file CIRCUIT.jsonl [-n N] [--bits B] [--open q,q] [--trials T] [--seed S] [--cap-log2 L] [--whole-gates] [--plan-only] [--json]\n\n\
Channels: identity, bitflip, dephasing, depolarizing, damping.\n\
Run `wai-quantum list` for the algorithm catalog."
Run `wai-quantum lab list` for the algorithm catalog."
);
std::process::exit(2);
}
@ -222,6 +224,15 @@ fn read_stdin() -> String {
s
}
/// The key a receipt is signed with ([`crate::keys`]), from `--key FILE` or
/// `--demo-key`.
fn signing_key(args: &[String]) -> SigningKey {
let given = opt(args, "--key", "").map(std::path::PathBuf::from);
let k = crate::keys::load(given.as_deref(), flag(args, "--demo-key")).unwrap_or_else(|e| die(&e));
crate::keys::notes(&k);
k.key
}
/// Pull `--flag VALUE` out of the argument list.
fn opt(args: &[String], long: &str, short: &str) -> Option<String> {
args.iter()
@ -259,9 +270,9 @@ fn gateset_from(args: &[String]) -> Gateset {
fn circuit_from(args: &[String], algo: Option<&String>) -> Circuit {
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}")))
wai_quantum::qasm::dyadic_circuit(&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}")))
wai_quantum::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);
@ -324,8 +335,8 @@ fn human_time(ns: f64) -> String {
/// A circuit file: JSON lines, one gate per line, `{"q":[a,b],"m":[re,im, …]}`
/// (rows are outputs, the first qubit most significant). Returns the gates
/// and the register size they need.
fn read_gate_file(path: &str) -> (Vec<wai::gates::Gate>, u32) {
use wai::gates::{complex, Gate};
fn read_gate_file(path: &str) -> (Vec<wai_quantum::gates::Gate>, u32) {
use wai_quantum::gates::{complex, Gate};
let text = std::fs::read_to_string(path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let mut gates = Vec::new();
let mut n: u32 = 0;
@ -435,8 +446,76 @@ fn estimate_json(e: &qr::Estimate) -> String {
)
}
fn main() {
let argv: Vec<String> = std::env::args().collect();
/// Check a receipt of any kind this tool or the library signs: `src` is its
/// JSON. Prints what it binds, and exits 1 when it does not verify.
pub fn verify_receipt(src: &str) {
// Either form: a receipt as sealed, or one whose joule figure
// carries its acquisition class inside the signature
// (`quantum_energy::Labelled`, as `wai_quantum_meter` emits).
if let Some(any) = MaybeLabelled::<ResourceReceipt>::from_json(src) {
let ok = any.verify();
let r = any.receipt();
let e = &r.estimate;
println!("signature {}", if ok { "VALID" } else { "INVALID" });
println!("kind fault-tolerant resource estimate");
println!("job-hash {}", hx(&r.job_hash));
println!("qubit {} ({}, d = {})", e.qubit, e.code, e.distance);
println!("physical {} qubits", e.physical_qubits);
println!("runtime {}", human_time(e.runtime_ns));
println!("factories {}", e.factories);
println!("joules {} uJ", r.joules_micro);
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
println!("signer {}", r.signer_id);
if !ok {
std::process::exit(1);
}
return;
}
if let Some(any) = MaybeLabelled::<SpdReceipt>::from_json(src) {
let ok = any.verify();
let r = any.receipt();
println!("signature {}", if ok { "VALID" } else { "INVALID" });
println!("kind sparse Pauli dynamics");
println!("circuit-hash {}", hx(&r.circuit_hash));
println!("query-hash {}", hx(&r.query_hash));
println!("value {}", r.result.value);
println!("bound {}", r.result.truncation_bound);
println!("work {} term-updates (peak {} terms)", r.result.term_updates, r.result.peak_terms);
println!("joules {} uJ", r.joules_micro);
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
println!("signer {}", r.signer_id);
if !ok {
std::process::exit(1);
}
return;
}
match MaybeLabelled::<QuantumReceipt>::from_json(src) {
None => {
println!("INVALID (could not parse receipt JSON, or its energy class is malformed)");
std::process::exit(1);
}
Some(any) => {
let ok = any.verify();
let r = any.receipt();
println!("signature {}", if ok { "VALID" } else { "INVALID" });
println!("circuit-hash {}", hx(&r.circuit_hash));
println!("state-hash {}", hx(&r.statevector_hash));
println!("qubits {}", r.n_qubits);
println!("gates {}", r.n_ops);
println!("work {} amplitude-updates", r.work_amp_updates);
println!("joules {} uJ", r.joules_micro);
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
println!("signer {}", r.signer_id);
if !ok {
std::process::exit(1);
}
}
}
}
/// The research tools, with `args` the words after `wai-quantum lab`.
pub fn main(args: Vec<String>) {
let argv: Vec<String> = std::iter::once("wai-quantum lab".to_string()).chain(args).collect();
if argv.len() < 2 {
usage();
}
@ -446,7 +525,7 @@ fn main() {
match cmd {
"list" => {
println!("{:<14} {:<26} {}", "ID", "NAME", "DESCRIPTION");
println!("{:<14} {:<26} DESCRIPTION", "ID", "NAME");
for (id, name, desc) in qt::catalog() {
println!("{id:<14} {name:<26} {desc}");
}
@ -459,7 +538,7 @@ fn main() {
"qir" => {
let c = circuit_from(args, algo);
print!("{}", wai::quantum_qir::to_qir(&c, !flag(args, "--no-measure")));
print!("{}", wai_quantum::quantum_qir::to_qir(&c, !flag(args, "--no-measure")));
}
"run" => {
@ -509,79 +588,13 @@ fn main() {
let shots: u64 = num(args, "--shots", "", 0);
let seed: u64 = num(args, "--seed", "", 0);
let measure = (shots > 0).then_some(Measure { seed, shots });
// Deterministic demo key: the receipt is reproducible byte-for-byte, so
// native and cloud-WASM runs sign identically. Supply real keys in prod.
let key = SigningKey::from_bytes(&[42u8; 32]);
let r = QuantumReceipt::seal(&key, "did:key:wai-quantum-cli", &c, measure, 0, None)
let key = signing_key(args);
let r = QuantumReceipt::seal(&key, did_key(&key.verifying_key()), &c, measure, 0, None)
.unwrap_or_else(|e| die(&format!("seal: {e:?}")));
println!("{}", r.to_json());
}
"verify" => {
// Either form: a receipt as sealed, or one whose joule figure
// carries its acquisition class inside the signature
// (`quantum_energy::Labelled`, as `wai_quantum_meter` emits).
let src = read_stdin();
if let Some(any) = MaybeLabelled::<ResourceReceipt>::from_json(&src) {
let ok = any.verify();
let r = any.receipt();
let e = &r.estimate;
println!("signature {}", if ok { "VALID" } else { "INVALID" });
println!("kind fault-tolerant resource estimate");
println!("job-hash {}", hx(&r.job_hash));
println!("qubit {} ({}, d = {})", e.qubit, e.code, e.distance);
println!("physical {} qubits", e.physical_qubits);
println!("runtime {}", human_time(e.runtime_ns));
println!("factories {}", e.factories);
println!("joules {} uJ", r.joules_micro);
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
println!("signer {}", r.signer_id);
if !ok {
std::process::exit(1);
}
return;
}
if let Some(any) = MaybeLabelled::<SpdReceipt>::from_json(&src) {
let ok = any.verify();
let r = any.receipt();
println!("signature {}", if ok { "VALID" } else { "INVALID" });
println!("kind sparse Pauli dynamics");
println!("circuit-hash {}", hx(&r.circuit_hash));
println!("query-hash {}", hx(&r.query_hash));
println!("value {}", r.result.value);
println!("bound {}", r.result.truncation_bound);
println!("work {} term-updates (peak {} terms)", r.result.term_updates, r.result.peak_terms);
println!("joules {} uJ", r.joules_micro);
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
println!("signer {}", r.signer_id);
if !ok {
std::process::exit(1);
}
return;
}
match MaybeLabelled::<QuantumReceipt>::from_json(&src) {
None => {
println!("INVALID (could not parse receipt JSON, or its energy class is malformed)");
std::process::exit(1);
}
Some(any) => {
let ok = any.verify();
let r = any.receipt();
println!("signature {}", if ok { "VALID" } else { "INVALID" });
println!("circuit-hash {}", hx(&r.circuit_hash));
println!("state-hash {}", hx(&r.statevector_hash));
println!("qubits {}", r.n_qubits);
println!("gates {}", r.n_ops);
println!("work {} amplitude-updates", r.work_amp_updates);
println!("joules {} uJ", r.joules_micro);
println!("energy class {}", energy_class_line(any.energy_class(), r.joules_micro));
println!("signer {}", r.signer_id);
if !ok {
std::process::exit(1);
}
}
}
}
"verify" => verify_receipt(&read_stdin()),
// How much quantum information a state actually carries. A state vector
// cannot answer this for a subsystem — half a Bell pair has no state
@ -592,7 +605,7 @@ fn main() {
// qubit and RZZ(−π/2) on every edge, and the bound on how far
// truncation can have moved it. `--receipt` seals the run instead.
"utility" => {
use wai::quantum_spd::{heavy_hex_127, kicked_ising, Pauli, SpdConfig};
use wai_quantum::quantum_spd::{heavy_hex_127, kicked_ising, Pauli, SpdConfig};
let theta: f64 = num(args, "--theta", "", std::f64::consts::FRAC_PI_4);
let steps: u32 = num(args, "--steps", "", 20);
let threshold: f64 = num(args, "--threshold", "", 1e-3);
@ -606,7 +619,7 @@ fn main() {
}
let chi: usize = num(args, "--chi", "", 16);
let edges = heavy_hex_127();
let mut tns = wai::quantum_bptn::Tns::new(127, &edges, chi).unwrap_or_else(|e| die(&e.to_string()));
let mut tns = wai_quantum::quantum_bptn::Tns::new(127, &edges, chi).unwrap_or_else(|e| die(&e.to_string()));
tns.run(&kicked_ising(127, &edges, theta, steps)).unwrap_or_else(|e| die(&e.to_string()));
let value = tns.expect1(qubit, Pauli::Z).unwrap_or_else(|e| die(&e.to_string()));
if flag(args, "--json") {
@ -633,10 +646,8 @@ fn main() {
config: SpdConfig { threshold, max_weight: None },
};
if flag(args, "--receipt") {
// The same deterministic demo key as `receipt`: the run is
// bit-identical everywhere, so is its receipt.
let key = SigningKey::from_bytes(&[42u8; 32]);
let r = SpdReceipt::seal(&key, "did:key:wai-quantum-cli", &job, None, |run| {
let key = signing_key(args);
let r = SpdReceipt::seal(&key, did_key(&key.verifying_key()), &job, None, |run| {
run();
0
})
@ -698,10 +709,8 @@ fn main() {
die("--receipt seals one estimate: pick a --qubit");
}
let job = ResourceJob { program: Program::Logical(counts), qubit: qubits[0].clone(), code, config: cfg };
// The same deterministic demo key as `receipt`: the estimate is
// bit-identical everywhere, so is its receipt.
let key = SigningKey::from_bytes(&[42u8; 32]);
let r = ResourceReceipt::seal(&key, "did:key:wai-quantum-cli", &job, None, |run| {
let key = signing_key(args);
let r = ResourceReceipt::seal(&key, did_key(&key.verifying_key()), &job, None, |run| {
run();
0
})
@ -731,7 +740,7 @@ fn main() {
println!("[{}]", rows.join(","));
return;
}
println!("{:>8} {:>3} {:>9} {:>14} {}", "slowdown", "d", "factories", "physical", "runtime");
println!("{:>8} {:>3} {:>9} {:>14} runtime", "slowdown", "d", "factories", "physical");
for e in &points {
println!(
"{:>8} {:>3} {:>9} {:>14} {}",
@ -759,8 +768,8 @@ fn main() {
let isa = qr::layout(&counts, budget);
println!("program {} tiles, {} logical steps, {} T states (budget {budget})", isa.tiles, isa.min_steps, isa.t_states);
println!(
"{:<12} {:<22} {:>3} {:>9} {:>8} {:>14} {}",
"qubit", "code", "d", "factories", "share", "physical", "runtime"
"{:<12} {:<22} {:>3} {:>9} {:>8} {:>14} runtime",
"qubit", "code", "d", "factories", "share", "physical"
);
for (q, e) in &estimates {
match e {
@ -782,8 +791,8 @@ fn main() {
// Error correction at circuit level: sample a noisy Clifford circuit,
// derive its detector error model, decode it exactly.
"qec" => {
use wai::quantum_frame::{error_model, parse, sample, surface_code_memory};
use wai::quantum_match::{fit_code_model, memory_experiment, MatchingGraph};
use wai_quantum::quantum_frame::{error_model, parse, sample, surface_code_memory};
use wai_quantum::quantum_match::{fit_code_model, memory_experiment, MatchingGraph};
let shots: usize = num(args, "--shots", "", 100_000);
let seed: u64 = num(args, "--seed", "", 1);
let json = flag(args, "--json");
@ -840,7 +849,7 @@ fn main() {
let decoder = opt(args, "--decoder", "").unwrap_or_else(|| "exact".into());
let failures = match decoder.as_str() {
"exact" => g.failures(&s),
"uf" => wai::quantum_uf::UnionFind::new(&g).failures(&s),
"uf" => wai_quantum::quantum_uf::UnionFind::new(&g).failures(&s),
_ => die("--decoder: exact or uf"),
}
.unwrap_or_else(|e| die(&format!("decode: {e:?}")));
@ -858,7 +867,7 @@ fn main() {
println!("model {} error mechanisms", model.mechanisms.len());
println!("failures {failures} of {shots} shots ({rate:.3e} per shot, {decoder} decoder)");
if opt(args, "--file", "").is_none() {
let per_round = wai::quantum_match::MemoryPoint { distance: d, rounds, p, shots: shots as u64, failures }.per_round();
let per_round = wai_quantum::quantum_match::MemoryPoint { distance: d, rounds, p, shots: shots as u64, failures }.per_round();
println!("per round {per_round:.3e} (d = {d}, {rounds} rounds, p = {p})");
}
}
@ -870,7 +879,7 @@ fn main() {
// {"q":[0,1],"m":[re,im, …]} (rows are outputs, first qubit most
// significant).
"tn" => {
use wai::quantum_tn::{contract_plan, find_path, Families, Network, PathOptions};
use wai_quantum::quantum_tn::{contract_plan, find_path, Families, Network, PathOptions};
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
let (gates, n) = read_gate_file(&path);
let n: u32 = num(args, "--qubits", "-n", n);
@ -950,7 +959,7 @@ fn main() {
// Gaussian boson sampling, exactly: squeezed light through an
// interferometer with loss, and the click statistics it must show.
"gbs" => {
use wai::quantum_gbs::{random_unitary, GaussianState, Mat, C};
use wai_quantum::quantum_gbs::{random_unitary, GaussianState, Mat, C};
let r: Vec<f64> = opt(args, "--squeezing", "")
.unwrap_or_else(|| die("--squeezing r1,r2,… is required (one value per mode)"))
.split(',')
@ -1039,7 +1048,7 @@ fn main() {
// where one is known (the Ising chain at any length, any chain by
// exact diagonalisation up to 22 sites).
"dmrg" => {
use wai::quantum_dmrg::*;
use wai_quantum::quantum_dmrg::*;
let n: usize = num(args, "--sites", "-n", 40);
if n < 2 {
die("-n: at least 2 sites");
@ -1088,8 +1097,8 @@ fn main() {
// Ising chain started with every σˣ = +1 is held at each report to
// its free-fermion solution.
"tdvp" => {
use wai::quantum_dmrg::{Chain, Mps, Op, Scalar, C};
use wai::quantum_tdvp::*;
use wai_quantum::quantum_dmrg::{Chain, Mps, Op, Scalar, C};
use wai_quantum::quantum_tdvp::*;
let n: usize = num(args, "--sites", "-n", 40);
if n < 2 {
die("-n: at least 2 sites");
@ -1171,7 +1180,7 @@ fn main() {
// A molecule's ground state by DMRG from its integrals, against
// Hartree–Fock and full configuration interaction where it fits.
"chem" => {
use wai::quantum_chem::*;
use wai_quantum::quantum_chem::*;
let path = opt(args, "--fcidump", "").unwrap_or_else(|| die("--fcidump FILE is required"));
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let fd = read_fcidump(&text).unwrap_or_else(|e| die(&format!("{path}: {e}")));
@ -1210,7 +1219,7 @@ fn main() {
// A neural quantum state for a spin model on a chain or an open
// square lattice, against exact diagonalisation where it fits.
"nqs" => {
use wai::quantum_nqs::*;
use wai_quantum::quantum_nqs::*;
let lx: usize = num(args, "--lx", "", 10);
let ly: usize = num(args, "--ly", "", 1);
let n = lx * ly;
@ -1350,7 +1359,7 @@ fn main() {
// host, so they differ between platforms; the parity and the corpus
// digest do not.
"fabric" => {
use wai::quantum_fabric::*;
use wai_quantum::quantum_fabric::*;
let circuits: usize = num(args, "--circuits", "", 200);
let seed: u64 = num(args, "--seed", "", 7);
let gs = gateset_from(args);
@ -1405,7 +1414,7 @@ fn main() {
// glass, by simulated bifurcation, annealing, or exact enumeration.
// Max-Cut reports cuts; a spin glass reports energies.
"qio" => {
use wai::quantum_qio::*;
use wai_quantum::quantum_qio::*;
let (problem, graph) = if let Some(path) = opt(args, "--gset", "") {
let text = std::fs::read_to_string(&path).unwrap_or_else(|e| die(&format!("{path}: {e}")));
let g = read_gset(&text).unwrap_or_else(|e| die(&e.to_string()));
@ -1479,8 +1488,8 @@ fn main() {
// qLDPC memory: a bivariate-bicycle code's depth-8 syndrome cycle,
// sampled with circuit-level noise and decoded by BP-OSD.
"ldpc" => {
use wai::quantum_frame::Basis;
use wai::quantum_ldpc::{memory_circuit, memory_experiment, BbCode, BpOsdConfig};
use wai_quantum::quantum_frame::Basis;
use wai_quantum::quantum_ldpc::{memory_circuit, memory_experiment, BbCode, BpOsdConfig};
let code = match opt(args, "--code", "").as_deref().unwrap_or("72") {
"72" => BbCode::bb72(),
"90" => BbCode::bb90(),
@ -1540,7 +1549,7 @@ fn main() {
// states, CCZ factories and the clock, priced; and the published
// factoring machine re-run region by region.
"arch" => {
use wai::quantum_arch::*;
use wai_quantum::quantum_arch::*;
let json = flag(args, "--json");
match algo.map(String::as_str) {
Some("factoring") | None => {
@ -1618,7 +1627,7 @@ fn main() {
// exact density matrix (up to 12 qubits) and cross-entropy fidelity
// against the ideal circuit and the digital error model.
"noisy" => {
use wai::quantum_noisy::*;
use wai_quantum::quantum_noisy::*;
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
let (gates, n) = read_gate_file(&path);
let n: u32 = num(args, "--qubits", "-n", n);
@ -1661,7 +1670,7 @@ fn main() {
None
};
let xeb = if flag(args, "--xeb") {
let mut ideal = wai::quantum_sv::StateVector::zero(n).unwrap_or_else(|e| die(&format!("{e:?}")));
let mut ideal = wai_quantum::quantum_sv::StateVector::zero(n).unwrap_or_else(|e| die(&format!("{e:?}")));
ideal.run(&gates, 2).unwrap_or_else(|e| die(&format!("{e:?}")));
let sampled = linear_xeb(&ideal, &run.samples);
let exact_x = exact.as_ref().map(|rho| exact_xeb(&ideal, rho, noise.readout));
@ -1710,7 +1719,7 @@ fn main() {
// (fused to --fuse qubits), then show the likeliest outcomes, draw
// samples, or evaluate Pauli observables.
"sv" => {
use wai::quantum_sv::StateVector;
use wai_quantum::quantum_sv::StateVector;
let path = opt(args, "--file", "").unwrap_or_else(|| die("--file CIRCUIT.jsonl is required"));
let (gates, n) = read_gate_file(&path);
let n: u32 = num(args, "--qubits", "-n", n);

View file

@ -0,0 +1,519 @@
//! `wai-quantum`: OpenQASM programs run on this machine as the quantum job
//! service runs them, or sent to the service; every result names its method
//! and its accuracy and carries a signed receipt.
//!
//! ```text
//! wai-quantum run bell.qasm --shots 1000 # here, planned as the service plans it
//! wai-quantum run prog.qasm -o "Z0 Z1" -a 00 --json
//! wai-quantum plan prog.qasm # the method and why, without running
//! wai-quantum lint prog.qasm # each problem with its line
//! wai-quantum draw prog.qasm # the circuit as text
//! wai-quantum submit prog.qasm # on the job service
//! wai-quantum verify result.json # a result's receipt, or a lab receipt
//! wai-quantum key # the key this user signs with
//! wai-quantum methods # each method and its reach here
//! wai-quantum mcp # these tools for an agent, over stdio
//! wai-quantum lab … # the research tools
//! wai-quantum completions zsh # shell completion
//! ```
//!
//! `run`, `plan` and `mcp` use the job service's own engine
//! ([`wai_quantum::engine`]), so a job gives here the result it gives there.
//! `run --json` prints the job as the service returns it: the request, the
//! plan, and the result with its receipt, which `verify` checks.
mod keys;
mod lab;
mod mcp;
use clap::{Args, CommandFactory, Parser, Subcommand};
use serde_json::{Value, json};
use std::path::{Path, PathBuf};
use wai_quantum::engine::{catalog, draw, job};
#[derive(Parser)]
#[command(
name = "wai-quantum",
version,
about = "OpenQASM programs run here as the quantum job service runs them, or sent to it; every result names its method and accuracy and is signed",
propagate_version = true
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Run a program here, planned as the job service plans it, and sign the result
Run {
#[command(flatten)]
job: Job,
#[command(flatten)]
out: Output,
#[command(flatten)]
key: KeyArgs,
},
/// Show the method a program would run on and why, without running it
Plan {
#[command(flatten)]
job: Job,
/// Print the plan as JSON
#[arg(long)]
json: bool,
},
/// Check a program and print each problem with its line
Lint {
/// The program; `-` or nothing reads standard input
file: Option<PathBuf>,
/// Print the outcome as JSON
#[arg(long)]
json: bool,
},
/// Draw a program's circuit as text
Draw {
/// The program; `-` or nothing reads standard input
file: Option<PathBuf>,
/// Wrap at this many columns
#[arg(long, default_value_t = 100)]
width: usize,
},
/// Send a program to the job service and print its result
#[cfg(feature = "service")]
Submit {
#[command(flatten)]
job: Job,
#[command(flatten)]
out: Output,
/// The service's address
#[arg(long, env = "WAI_QUANTUM_SERVICE", default_value = "https://wai.transaction.science/quantum-jobs")]
service: String,
},
/// Check a receipt: a result from `run --json`, `submit --json` or the service, or a `lab` receipt
Verify {
/// The JSON; `-` or nothing reads standard input
file: Option<PathBuf>,
},
/// Show the key this user signs receipts with
Key {
#[command(flatten)]
key: KeyArgs,
},
/// Each method, what it takes, and how far it reaches in a job's memory here
Methods {
/// Print the table as JSON
#[arg(long)]
json: bool,
/// The memory a job may take, GiB
#[arg(long, default_value_t = DEFAULT_MEMORY_GB)]
memory_gb: u64,
},
/// Serve run, plan, lint, draw and verify to an agent over standard input and output (MCP)
Mcp {
/// The memory a job may take, GiB
#[arg(long, default_value_t = DEFAULT_MEMORY_GB)]
memory_gb: u64,
#[command(flatten)]
key: KeyArgs,
},
/// The research tools: error correction, DMRG, neural states, resource estimates and more (`wai-quantum lab help`)
Lab {
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
args: Vec<String>,
},
/// Print shell completion for bash, zsh, fish, elvish or PowerShell
Completions { shell: clap_complete::Shell },
}
/// The memory a job may take by default, GiB.
const DEFAULT_MEMORY_GB: u64 = 16;
/// A job: a program and what is asked of it, as the service reads it.
#[derive(Args, Clone)]
struct Job {
/// The OpenQASM 3 (or 2) program; `-` or nothing reads standard input
file: Option<PathBuf>,
/// Shots to sample (1024 when nothing else is asked)
#[arg(long)]
shots: Option<u64>,
/// The seed shots are sampled with (1 if not given)
#[arg(long)]
seed: Option<u64>,
/// A method by name, or auto (the default) for the strongest the program admits; `wai-quantum methods` lists them
#[arg(long)]
method: Option<String>,
/// A Pauli observable to take the expectation of, as "Z0 Z1" or "0.5 X2" (repeatable)
#[arg(short = 'o', long = "observable")]
observables: Vec<String>,
/// A basis state's amplitude, as a bit string with the highest qubit first (repeatable)
#[arg(short = 'a', long = "amplitude")]
amplitudes: Vec<String>,
/// A noise channel and its probability, as depolarizing_1q=0.01 (repeatable)
#[arg(long, value_parser = noise_arg)]
noise: Vec<(String, f64)>,
/// A method option, as max_bond=128 (repeatable)
#[arg(long = "option", value_parser = option_arg)]
options: Vec<(String, Value)>,
/// The memory a job may take here, GiB
#[arg(long, default_value_t = DEFAULT_MEMORY_GB)]
memory_gb: u64,
}
#[derive(Args, Clone)]
struct Output {
/// Print the job as the service returns it: request, plan, result and receipt
#[arg(long)]
json: bool,
/// The most outcomes to list
#[arg(long, default_value_t = 16)]
top: usize,
}
#[derive(Args, Clone)]
struct KeyArgs {
/// Sign with the key in this file (64 hex digits)
#[arg(long, value_name = "FILE")]
key: Option<PathBuf>,
/// Sign with the published demo key: the receipt then shows a result is unaltered, not who signed it
#[arg(long, conflicts_with = "key")]
demo_key: bool,
}
fn noise_arg(s: &str) -> Result<(String, f64), String> {
let (k, v) = s.split_once('=').ok_or("give a channel and its probability, as depolarizing_1q=0.01")?;
if !catalog::NOISE_CHANNELS.contains(&k) {
return Err(format!("{k}: no such channel; the channels are {}", catalog::NOISE_CHANNELS.join(", ")));
}
let p: f64 = v.parse().map_err(|_| format!("{v}: a probability, 0 to 1"))?;
Ok((k.to_string(), p))
}
fn option_arg(s: &str) -> Result<(String, Value), String> {
let (k, v) = s.split_once('=').ok_or("give an option and its value, as max_bond=128")?;
// A number or true/false reads as JSON; anything else is a string.
let value = serde_json::from_str::<Value>(v).ok().filter(|x| x.is_number() || x.is_boolean()).unwrap_or_else(|| json!(v));
Ok((k.to_string(), value))
}
fn die(msg: &str) -> ! {
eprintln!("wai-quantum: {msg}");
std::process::exit(1)
}
/// The text of `file`, or standard input for `-` or none.
fn read_input(file: Option<&Path>) -> Result<String, String> {
match file {
Some(p) if p != Path::new("-") => std::fs::read_to_string(p).map_err(|e| format!("{}: {e}", p.display())),
_ => {
use std::io::Read as _;
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).map_err(|e| format!("standard input: {e}"))?;
Ok(s)
}
}
}
/// The job's request, with only the fields given, as the service receives it.
fn request(job: &Job, qasm: String) -> Value {
let mut r = json!({ "qasm": qasm });
if let Some(s) = job.shots {
r["shots"] = json!(s);
}
if let Some(s) = job.seed {
r["seed"] = json!(s);
}
if let Some(m) = &job.method {
r["method"] = json!(m);
}
if !job.observables.is_empty() {
r["observables"] = json!(job.observables);
}
if !job.amplitudes.is_empty() {
r["amplitudes"] = json!(job.amplitudes);
}
if !job.noise.is_empty() {
r["noise"] = Value::Object(job.noise.iter().map(|(k, v)| (k.clone(), json!(v))).collect());
}
if !job.options.is_empty() {
r["options"] = Value::Object(job.options.iter().cloned().collect());
}
r
}
/// What a result says of its energy: the command line does not meter a run.
const UNMETERED: &str = "the command line does not meter a run's energy";
/// The job body `run` gives, as the service gives it: the request, the plan,
/// and the result sealed with `key` ([`job::run`]).
pub fn run_here(req: &Value, memory_gb: u64, key: &wai_quantum::ed25519_dalek::SigningKey) -> Result<Value, String> {
job::run(req, memory_gb << 30, key, json!({ "unmetered": UNMETERED }))
}
/// A job body as a reader takes it in.
fn print_job(body: &Value, top: usize) {
let r = &body["result"];
let s = |v: &Value| v.as_str().map(str::to_string).unwrap_or_else(|| v.to_string());
println!("method {} — {}", s(&body["method"]), s(&body["why"]));
if let Some(acc) = r.get("accuracy") {
let mut extra: Vec<String> = acc.as_object().map(|o| o.iter().filter(|(k, _)| *k != "class" && *k != "note").map(|(k, v)| format!("{k} {}", s(v))).collect()).unwrap_or_default();
if let Some(n) = acc.get("note").and_then(Value::as_str) {
extra.push(n.to_string());
}
println!("accuracy {}{}", s(&acc["class"]), if extra.is_empty() { String::new() } else { format!(" ({})", extra.join("; ")) });
}
println!("circuit {} qubits, {} gates", r["qubits"], r["gates"]);
if let Some(w) = r.get("warning").and_then(Value::as_str) {
println!("warning {w}");
}
if let Some(p) = r.get("global_phase").and_then(Value::as_str) {
println!("phase {p}");
}
if let Some(counts) = r.get("counts").and_then(Value::as_object) {
let total: u64 = counts.values().filter_map(Value::as_u64).sum::<u64>() + r["omitted_shots"].as_u64().unwrap_or(0);
println!("counts {total} shots, {} outcomes", r["distinct_outcomes"].as_u64().unwrap_or(counts.len() as u64));
let mut v: Vec<(&String, u64)> = counts.iter().map(|(k, c)| (k, c.as_u64().unwrap_or(0))).collect();
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(b.0)));
for (k, c) in v.iter().take(top) {
println!(" {k} {c:>8} {:.4}", *c as f64 / total.max(1) as f64);
}
if v.len() > top {
println!(" … {} more (--top N)", v.len() - top);
}
}
for e in r.get("expectations").and_then(Value::as_array).into_iter().flatten() {
let bound = e.get("bound").map(|b| format!(" ± {}", s(b))).unwrap_or_default();
println!("⟨{}⟩ {}{bound}", s(&e["observable"]), s(&e["value"]));
}
for a in r.get("amplitudes").and_then(Value::as_array).into_iter().flatten() {
let (re, im) = (a["re"].as_f64().unwrap_or(f64::NAN), a["im"].as_f64().unwrap_or(f64::NAN));
println!("|{}⟩ {re:+.12} {}{:.12}i", s(&a["bits"]), if im < 0.0 { '-' } else { '+' }, im.abs());
}
if let Some(d) = r.get("dynamic") {
println!("dynamic {d}");
}
if let Some(ms) = r.get("elapsed_ms") {
println!("time {ms} ms");
}
if let Some(key) = r.pointer("/receipt/key").and_then(Value::as_str) {
let ok = job::verify(body).map(|(ok, _)| ok).unwrap_or(false);
println!("receipt {}, signed by {}", if ok { "verifies" } else { "DOES NOT VERIFY" }, job::did_of(key).unwrap_or_else(|| key.to_string()));
}
}
fn load_key(k: &KeyArgs) -> keys::Key {
let key = keys::load(k.key.as_deref(), k.demo_key).unwrap_or_else(|e| die(&e));
keys::notes(&key);
key
}
#[cfg(feature = "service")]
fn submit(req: &Value, service: &str) -> Result<Value, String> {
use std::time::Duration;
let base = service.trim_end_matches('/');
let agent: ureq::Agent = ureq::Agent::config_builder().http_status_as_error(false).timeout_global(Some(Duration::from_secs(120))).build().into();
let ua = concat!("wai-quantum/", env!("CARGO_PKG_VERSION"));
let read = |mut resp: ureq::http::Response<ureq::Body>| -> Result<(u16, Value), String> {
let code = resp.status().as_u16();
let v: Value = resp.body_mut().read_json().map_err(|e| format!("the service's reply did not read: {e}"))?;
Ok((code, v))
};
let (mut code, mut body) = read(agent.post(format!("{base}/v1/run")).header("user-agent", ua).send_json(req).map_err(|e| format!("{base}: {e}"))?)?;
// A job still running after the service's wait is fetched until it ends.
while code == 202 {
let id = body["id"].as_str().ok_or("the service answered 202 with no job id")?.to_string();
eprintln!("note: job {id} is {}; waiting", body["status"].as_str().unwrap_or("running"));
std::thread::sleep(Duration::from_secs(3));
(code, body) = read(agent.get(format!("{base}/v1/jobs/{id}")).header("user-agent", ua).call().map_err(|e| format!("{base}: {e}"))?)?;
if body["status"] != "queued" && body["status"] != "running" {
break;
}
code = 202;
}
if code >= 400 {
return Err(body["error"].as_str().map(str::to_string).unwrap_or_else(|| format!("HTTP {code}: {body}")));
}
if body["status"] != "done" {
return Err(format!("the job {}: {}", body["status"].as_str().unwrap_or("failed"), body["error"].as_str().unwrap_or("")));
}
Ok(body)
}
fn main() {
let cli = Cli::parse();
match cli.command {
Command::Run { job, out, key } => {
let qasm = read_input(job.file.as_deref()).unwrap_or_else(|e| die(&e));
let req = request(&job, qasm);
let k = load_key(&key);
let body = run_here(&req, job.memory_gb, &k.key).unwrap_or_else(|e| die(&e));
if out.json {
println!("{body}");
} else {
print_job(&body, out.top);
}
}
Command::Plan { job, json } => {
let qasm = read_input(job.file.as_deref()).unwrap_or_else(|e| die(&e));
let p = catalog::plan(&request(&job, qasm), job.memory_gb << 30).unwrap_or_else(|e| die(&e));
if json {
println!("{}", serde_json::to_value(&p).unwrap_or(Value::Null));
} else {
println!("method {}", p.method);
println!("why {}", p.why);
println!("qubits {}", p.qubits);
println!("memory {:.1} MiB at most", p.memory_bytes as f64 / (1u64 << 20) as f64);
println!("work {}", p.work);
}
}
Command::Lint { file, json } => {
let name = file.as_deref().filter(|p| *p != Path::new("-")).map(|p| p.display().to_string()).unwrap_or_else(|| "<stdin>".into());
let src = read_input(file.as_deref()).unwrap_or_else(|e| die(&e));
let v = job::lint(&src);
if json {
println!("{v}");
} else if v["ok"] == true {
let dynamic = v["dynamic_steps"].as_u64().map(|s| format!(", dynamic: {s} steps")).unwrap_or_default();
println!("{name}: ok — {} qubits, {} gates, {} bits measured{dynamic}", v["qubits"], v["gates"], v["measured"]);
} else {
println!("{name}:{}", v["error"].as_str().unwrap_or("").trim_start_matches("line "));
}
if v["ok"] != true {
std::process::exit(1);
}
}
Command::Draw { file, width } => {
let src = read_input(file.as_deref()).unwrap_or_else(|e| die(&e));
let p = wai_quantum::qasm::parse(&src).unwrap_or_else(|e| die(&e));
print!("{}", draw::draw(&p, width));
}
#[cfg(feature = "service")]
Command::Submit { job, out, service } => {
let qasm = read_input(job.file.as_deref()).unwrap_or_else(|e| die(&e));
let req = request(&job, qasm);
let body = submit(&req, &service).unwrap_or_else(|e| die(&e));
if out.json {
println!("{body}");
} else {
print_job(&body, out.top);
}
}
Command::Verify { file } => {
let src = read_input(file.as_deref()).unwrap_or_else(|e| die(&e));
let v: Value = serde_json::from_str(&src).unwrap_or_else(|e| die(&format!("not JSON: {e}")));
if v.get("request").is_some() && v.get("result").is_some() {
let (ok, key) = job::verify(&v).unwrap_or_else(|e| die(&e));
println!("signature {}", if ok { "VALID" } else { "INVALID" });
println!("binds the request and the result as given");
if let Some(k) = key.as_str() {
println!("signer {}", job::did_of(k).unwrap_or_else(|| k.to_string()));
}
if !ok {
std::process::exit(1);
}
} else {
lab::verify_receipt(&src);
}
}
Command::Key { key } => {
let k = load_key(&key);
println!("{}", wai_quantum::quantum_receipt::did_key(&k.key.verifying_key()));
match &k.path {
Some(p) => eprintln!("key file {}", p.display()),
None => eprintln!("the published demo key"),
}
}
Command::Methods { json, memory_gb } => {
let m = catalog::describe(memory_gb << 30);
if json {
println!("{m}");
} else {
for x in m.as_array().into_iter().flatten() {
let reach = if x.get("available") == Some(&Value::Bool(false)) { "not in this build".to_string() } else { format!("up to {} qubits", x["max_qubits"]) };
println!("{:<20} {reach:<20} {}", x["method"].as_str().unwrap_or(""), x["accuracy"].as_str().unwrap_or(""));
println!("{:<20} {}", "", x["circuits"].as_str().unwrap_or(""));
}
}
}
Command::Mcp { memory_gb, key } => {
let k = load_key(&key);
mcp::serve(memory_gb, &k.key);
}
Command::Lab { args } => lab::main(args),
Command::Completions { shell } => {
clap_complete::generate(shell, &mut Cli::command(), "wai-quantum", &mut std::io::stdout());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn job(qasm: &str) -> (Job, String) {
(
Job { file: None, shots: None, seed: None, method: None, observables: vec![], amplitudes: vec![], noise: vec![], options: vec![], memory_gb: 1 },
qasm.to_string(),
)
}
#[test]
fn a_run_here_is_the_services_job_and_its_receipt_verifies() {
let (mut j, q) = job("OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];");
j.shots = Some(1000);
j.observables = vec!["Z0 Z1".into()];
let req = request(&j, q);
// Only what was given is in the request.
assert_eq!(req.as_object().unwrap().keys().collect::<Vec<_>>(), ["qasm", "shots", "observables"]);
let key = wai_quantum::ed25519_dalek::SigningKey::from_bytes(&[3; 32]);
let body = run_here(&req, 1, &key).unwrap();
assert_eq!(body["method"], "stabilizer");
assert_eq!(body["result"]["expectations"][0]["value"], 1.0);
let counts = body["result"]["counts"].as_object().unwrap();
assert!(counts.keys().all(|k| k == "00" || k == "11"), "{counts:?}");
assert!(job::verify(&body).unwrap().0);
// The result changed, or the request, and the receipt no longer holds.
let mut forged = body.clone();
forged["result"]["expectations"][0]["value"] = json!(0.5);
assert!(!job::verify(&forged).unwrap().0);
let mut forged = body.clone();
forged["request"]["shots"] = json!(999);
assert!(!job::verify(&forged).unwrap().0);
// A JSON writer's round trip leaves it verifying.
let again: Value = serde_json::from_str(&body.to_string()).unwrap();
assert!(job::verify(&again).unwrap().0);
}
#[test]
fn noise_and_options_are_read_and_a_misspelt_channel_is_refused() {
assert_eq!(noise_arg("depolarizing_1q=0.01").unwrap(), ("depolarizing_1q".into(), 0.01));
assert!(noise_arg("depolarizing=0.01").unwrap_err().contains("no such channel"));
assert!(noise_arg("readout").is_err());
assert_eq!(option_arg("max_bond=128").unwrap().1, json!(128));
assert_eq!(option_arg("x=true").unwrap().1, json!(true));
assert_eq!(option_arg("kind=fast").unwrap().1, json!("fast"));
}
#[test]
fn the_command_line_parses_as_documented() {
Cli::command().debug_assert();
let c = Cli::try_parse_from(["wai-quantum", "run", "p.qasm", "--shots", "10", "-o", "Z0", "-o", "X1 X2", "--noise", "readout=0.02", "--option", "max_bond=8", "--json"]).unwrap();
match c.command {
Command::Run { job, out, .. } => {
assert_eq!(job.shots, Some(10));
assert_eq!(job.observables, ["Z0", "X1 X2"]);
assert_eq!(job.noise, [("readout".to_string(), 0.02)]);
assert!(out.json);
}
_ => panic!("not run"),
}
// An unknown flag is an error, not ignored.
assert!(Cli::try_parse_from(["wai-quantum", "run", "p.qasm", "--shot", "10"]).is_err());
// The research tools take their own words.
match Cli::try_parse_from(["wai-quantum", "lab", "qec", "-d", "5", "--p", "0.003"]).unwrap().command {
Command::Lab { args } => assert_eq!(args, ["qec", "-d", "5", "--p", "0.003"]),
_ => panic!("not lab"),
}
}
}

View file

@ -0,0 +1,196 @@
//! `wai-quantum mcp`: the command line's tools for an agent, as a Model Context
//! Protocol server on standard input and output (one JSON-RPC message per
//! line). Runs happen here, planned as the job service plans them, and are
//! signed with this user's key; nothing leaves the machine.
use serde_json::{Value, json};
use std::io::{BufRead, Write};
use wai_quantum::ed25519_dalek::SigningKey;
use wai_quantum::engine::{catalog, draw, job};
const PROTOCOL: &str = "2025-06-18";
fn job_schema() -> Value {
json!({
"type": "object",
"required": ["qasm"],
"properties": {
"qasm": { "type": "string", "description": "The program, OpenQASM 3 or 2: registers and broadcast over them, the standard gates, ctrl/negctrl modifiers, gate definitions, measurement and reset anywhere, and if on measured bits. Loops, inv and pow are refused with their line." },
"shots": { "type": "integer", "minimum": 0, "description": "Shots to sample; 1024 when nothing else is asked." },
"seed": { "type": "integer", "minimum": 0, "description": "The sampling seed (default 1)." },
"method": { "type": "string", "description": "auto (the default: the strongest method the program admits), or one of list_methods' names." },
"observables": { "type": "array", "items": { "type": "string" }, "description": "Pauli observables, as \"Z0 Z1\" or \"0.5 X2\"." },
"amplitudes": { "type": "array", "items": { "type": "string" }, "description": "Basis states whose amplitudes to return, as bit strings with the highest qubit first." },
"noise": { "type": "object", "description": "Channel probabilities", "properties": catalog::NOISE_CHANNELS.iter().map(|k| (k.to_string(), json!({ "type": "number", "minimum": 0, "maximum": 1 }))).collect::<serde_json::Map<_, _>>(), "additionalProperties": false },
"options": { "type": "object", "description": "Method options: max_bond, fusion, trajectories, threshold, memory_gb." }
}
})
}
fn qasm_schema(extra: Value) -> Value {
let mut s = json!({ "type": "object", "required": ["qasm"], "properties": { "qasm": { "type": "string", "description": "The program, OpenQASM 3 or 2." } } });
if let (Some(p), Some(e)) = (s["properties"].as_object_mut(), extra.as_object()) {
p.extend(e.clone());
}
s
}
fn tools() -> Value {
json!([
{
"name": "run_quantum_circuit",
"title": "Run a quantum program here",
"description": "Run an OpenQASM program on this machine, planned as the quantum job service plans it: measurement counts, Pauli expectation values and/or amplitudes, the method chosen and why, its accuracy class, and a receipt signed with this user's key. Every number in the answer was computed; quote them, do not estimate.",
"inputSchema": job_schema(),
},
{
"name": "plan_quantum_circuit",
"title": "Plan a quantum program",
"description": "The method a program would run on and why, the memory and work it would take, without running it.",
"inputSchema": job_schema(),
},
{
"name": "lint_qasm",
"title": "Check an OpenQASM program",
"description": "Parse an OpenQASM program as the engine reads it: its qubits, gates and measured bits, or the first problem with its line.",
"inputSchema": qasm_schema(json!({})),
},
{
"name": "draw_circuit",
"title": "Draw a circuit",
"description": "A program's circuit as text: one wire per qubit, controls as ●, targets as ⊕, measurements as M with their bit.",
"inputSchema": qasm_schema(json!({ "width": { "type": "integer", "minimum": 20, "description": "Wrap at this many columns (default 100)." } })),
},
{
"name": "verify_quantum_result",
"title": "Check a result's receipt",
"description": "Whether a job's receipt seals its request and its result, as run_quantum_circuit or the job service returns them, and which key signed it.",
"inputSchema": { "type": "object", "required": ["job"], "properties": { "job": { "type": "object", "description": "The job: {\"request\": ..., \"result\": ...}, the result carrying its receipt." } } },
},
{
"name": "list_methods",
"title": "List the methods",
"description": "Each simulation method: its accuracy class, the circuits it takes, and the most qubits it holds in a job's memory here.",
"inputSchema": { "type": "object", "properties": {} },
}
])
}
/// A tool's answer: its JSON, or an error the agent can read and act on.
fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey) -> Result<Value, String> {
let qasm = || args.get("qasm").and_then(Value::as_str).ok_or("give \"qasm\": the program");
match name {
"run_quantum_circuit" => {
qasm()?;
crate::run_here(args, memory_gb, key)
}
"plan_quantum_circuit" => {
qasm()?;
catalog::plan(args, memory_gb << 30).map(|p| serde_json::to_value(p).unwrap_or(Value::Null))
}
"lint_qasm" => Ok(job::lint(qasm()?)),
"draw_circuit" => {
let p = wai_quantum::qasm::parse(qasm()?)?;
let width = args.get("width").and_then(Value::as_u64).unwrap_or(100).max(20) as usize;
Ok(json!({ "text": draw::draw(&p, width) }))
}
"verify_quantum_result" => {
let given = args.get("job").ok_or("give \"job\": {\"request\": ..., \"result\": ...}")?;
let (valid, key) = job::verify(given)?;
Ok(json!({ "valid": valid, "key": key }))
}
"list_methods" => Ok(json!({ "methods": catalog::describe(memory_gb << 30) })),
_ => Err(format!("no tool {name}")),
}
}
/// The reply to one message, or `None` for a notification.
fn handle(msg: &Value, memory_gb: u64, key: &SigningKey) -> Option<Value> {
let id = msg.get("id")?.clone();
let method = msg.get("method").and_then(Value::as_str).unwrap_or("");
let params = msg.get("params").cloned().unwrap_or(json!({}));
let ok = |result: Value| json!({ "jsonrpc": "2.0", "id": id, "result": result });
let err = |code: i64, message: String| json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } });
Some(match method {
"initialize" => ok(json!({
"protocolVersion": params.get("protocolVersion").and_then(Value::as_str).unwrap_or(PROTOCOL),
"capabilities": { "tools": {} },
"serverInfo": { "name": "wai-quantum", "title": "wai-quantum", "version": env!("CARGO_PKG_VERSION") },
"instructions": "Quantum programs in OpenQASM, run on this machine as the quantum job service runs them. Plan or lint before a large run. Every result names its method and accuracy; quote its numbers rather than estimating them."
})),
"ping" => ok(json!({})),
"tools/list" => ok(json!({ "tools": tools() })),
"tools/call" => {
let name = params.get("name").and_then(Value::as_str).unwrap_or("");
let args = params.get("arguments").cloned().unwrap_or(json!({}));
match call(name, &args, memory_gb, key) {
Ok(v) => ok(json!({ "content": [{ "type": "text", "text": v.to_string() }], "structuredContent": v, "isError": false })),
Err(e) => ok(json!({ "content": [{ "type": "text", "text": e }], "isError": true })),
}
}
_ => err(-32601, format!("no method {method}")),
})
}
/// Serve until standard input closes.
pub fn serve(memory_gb: u64, key: &SigningKey) {
let stdin = std::io::stdin();
let mut out = std::io::stdout().lock();
for line in stdin.lock().lines() {
let Ok(line) = line else { break };
if line.trim().is_empty() {
continue;
}
let reply = match serde_json::from_str::<Value>(&line) {
Ok(msg) => handle(&msg, memory_gb, key),
Err(e) => Some(json!({ "jsonrpc": "2.0", "id": null, "error": { "code": -32700, "message": format!("not JSON: {e}") } })),
};
if let Some(r) = reply
&& writeln!(out, "{r}").and_then(|_| out.flush()).is_err()
{
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn key() -> SigningKey {
SigningKey::from_bytes(&[5; 32])
}
#[test]
fn an_agent_initializes_lists_the_tools_and_runs_a_circuit() {
let init = handle(&json!({ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": PROTOCOL } }), 1, &key()).unwrap();
assert_eq!(init["result"]["serverInfo"]["name"], "wai-quantum");
assert!(handle(&json!({ "jsonrpc": "2.0", "method": "notifications/initialized" }), 1, &key()).is_none());
let list = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }), 1, &key()).unwrap();
let names: Vec<&str> = list["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect();
assert_eq!(names, ["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "draw_circuit", "verify_quantum_result", "list_methods"]);
let run = handle(
&json!({ "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "run_quantum_circuit", "arguments": { "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "observables": ["Z0 Z1"] } } }),
1,
&key(),
)
.unwrap();
let job = &run["result"]["structuredContent"];
assert_eq!(run["result"]["isError"], false, "{run}");
assert_eq!(job["result"]["expectations"][0]["value"], 1.0);
// The job it returns checks with its own tool.
let check = handle(&json!({ "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "verify_quantum_result", "arguments": { "job": job } } }), 1, &key()).unwrap();
assert_eq!(check["result"]["structuredContent"]["valid"], true, "{check}");
}
#[test]
fn a_bad_program_is_an_error_the_agent_can_read() {
let r = handle(&json!({ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "run_quantum_circuit", "arguments": { "qasm": "qubit[1] q; foo q[0];" } } }), 1, &key()).unwrap();
assert_eq!(r["result"]["isError"], true);
assert!(r["result"]["content"][0]["text"].as_str().unwrap().contains("foo"), "{r}");
let lint = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": { "name": "lint_qasm", "arguments": { "qasm": "qubit[1] q;\nfoo q[0];" } } }), 1, &key()).unwrap();
assert_eq!(lint["result"]["structuredContent"]["line"], 2, "{lint}");
let none = handle(&json!({ "jsonrpc": "2.0", "id": 3, "method": "nonsense" }), 1, &key()).unwrap();
assert_eq!(none["error"]["code"], -32601);
}
}

View file

@ -422,7 +422,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -599,7 +599,7 @@ dependencies = [
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",

View file

@ -857,13 +857,14 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
"objc2",
"objc2-foundation",
"objc2-metal",
"serde",
"serde_json",
]
@ -872,7 +873,6 @@ name = "wai-quantum-service"
version = "0.1.0"
dependencies = [
"axum",
"blake3",
"getrandom 0.4.3",
"libc",
"serde",

View file

@ -15,9 +15,8 @@ serde = { version = "1", features = ["derive"] }
# float_roundtrip: a number a client sends back reads as the double it was
# (the default reader rounds twice), so its receipt still verifies.
serde_json = { version = "1", features = ["float_roundtrip"] }
blake3 = "1"
getrandom = "0.4"
wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum", "quantum_toolchain", "quantum_receipt", "quantum_sv", "quantum_tn", "quantum_tableau", "quantum_cutrank", "quantum_mpsim", "quantum_spd", "quantum_noisy", "quantum_sv_metal"] }
wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt", "quantum_sv_metal"] }
# proc_pid_rusage and waitid, for each job's energy (src/energy.rs).
[target.'cfg(target_os = "macos")'.dependencies]

View file

@ -199,6 +199,32 @@ pub fn index(base: &str, console: &str) -> Value {
#[cfg(test)]
mod tests {
use super::*;
use wai_quantum::engine::{catalog::plan, run::run};
fn go(job: Value) -> Value {
let p = plan(&job, 8 << 30).unwrap();
run(&job, &p).unwrap()
}
/// The two worked examples that measure in the middle do what their
/// `shows` says.
#[test]
fn the_measuring_examples_show_what_they_say() {
let ex = examples();
let job = |name: &str| ex.as_array().unwrap().iter().find(|e| e["name"] == name).unwrap()["job"].clone();
let t = go(job("teleport"));
assert_eq!(t["method"], "statevector", "{t}");
assert!((t["expectations"][0]["value"].as_f64().unwrap() - 0.7f64.cos()).abs() < 1e-12, "{t}");
assert!((t["expectations"][1]["value"].as_f64().unwrap() - 0.7f64.sin()).abs() < 1e-12, "{t}");
assert_eq!(t["dynamic"]["branches_weighed"], 4, "{t}");
let b = go(job("bit-flip-code"));
assert_eq!(b["method"], "stabilizer", "{b}");
assert_eq!(b["expectations"][0]["value"], 1.0, "{b}");
let counts = b["counts"].as_object().unwrap();
assert!(counts.keys().all(|k| k.ends_with("11")), "the syndrome names the middle qubit on every shot: {b}");
assert_eq!(counts.values().map(|v| v.as_u64().unwrap()).sum::<u64>(), 1000);
}
/// Every example plans, with the method its description promises.
#[test]

View file

@ -13,16 +13,13 @@
//! wai-quantum-service run FILE one job from a JSON file, here, printed
//! ```
mod catalog;
mod discovery;
mod dynamic;
mod energy;
mod http;
mod jobs;
mod mcp;
mod qasm;
use wai_quantum::engine::{catalog, run};
mod receipt;
mod run;
use std::io::Read;
use std::net::{IpAddr, SocketAddr};

View file

@ -1,269 +1,18 @@
//! Signed receipts: every finished job's result is sealed with the service's
//! Ed25519 key, binding the request as submitted and the result as returned,
//! so a client can show anyone what was run and what came back.
//!
//! What is signed is the JSON Canonicalization Scheme (RFC 8785) text of each:
//! keys sorted, no whitespace, and every number written as ECMAScript writes
//! the double it reads. A result therefore verifies after any JSON reader and
//! writer has carried it: a browser's `JSON.parse` and `JSON.stringify` turn
//! `1.0` into `1`, and other readers reorder keys.
//! The service's signing key. The receipt form and its check are the
//! engine's ([`wai_quantum::engine::receipt`]): every finished job's result is
//! sealed with this key.
use serde_json::{Value, json};
pub use wai_quantum::engine::receipt::{inexact_number, verify};
use serde_json::Value;
use std::path::{Path, PathBuf};
use wai_quantum::ed25519_dalek::{Signer as _, SigningKey, Verifier as _, VerifyingKey};
/// The first receipts signed serde_json's own text, which only a reader that
/// keeps each number's spelling and each object's key order can reproduce.
/// They still verify; new ones are canonical.
const DOMAIN_V1: &[u8] = b"wai:quantum-service-receipt\x01";
const DOMAIN: &[u8] = b"wai:quantum-service-receipt\x02";
/// The receipt's name for its canonical form.
const CANON: &str = "rfc8785";
use wai_quantum::ed25519_dalek::SigningKey;
use wai_quantum::engine::receipt;
/// The service's signing key.
pub struct Signer {
key: SigningKey,
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn unhex(s: &str) -> Option<Vec<u8>> {
s.len().is_multiple_of(2).then_some(())?;
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
}
/// A length-prefixed byte string, so that no field reads as part of another.
fn put(o: &mut Vec<u8>, bytes: &[u8]) {
o.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
o.extend_from_slice(bytes);
}
/// The digest a receipt signs: BLAKE3(domain ‖ put(request) ‖ put(result)),
/// each in its RFC 8785 canonical text.
pub fn digest(request: &Value, result: &Value) -> [u8; 32] {
let mut o = DOMAIN.to_vec();
put(&mut o, &canonical(request));
put(&mut o, &canonical(result));
*blake3::hash(&o).as_bytes()
}
/// The digest the first receipts signed (see [`DOMAIN_V1`]).
fn digest_v1(request: &Value, result: &Value) -> [u8; 32] {
let mut o = DOMAIN_V1.to_vec();
put(&mut o, request.to_string().as_bytes());
put(&mut o, result.to_string().as_bytes());
*blake3::hash(&o).as_bytes()
}
/// The RFC 8785 canonical text of `v`.
pub fn canonical(v: &Value) -> Vec<u8> {
let mut o = Vec::new();
write(v, &mut o);
o
}
fn write(v: &Value, o: &mut Vec<u8>) {
match v {
Value::Null => o.extend_from_slice(b"null"),
Value::Bool(b) => o.extend_from_slice(if *b { b"true" } else { b"false" }),
// Every number as the double a JSON reader in ECMAScript holds.
Value::Number(n) => o.extend_from_slice(es_number(n.as_f64().unwrap_or(f64::NAN)).as_bytes()),
Value::String(s) => write_str(s, o),
Value::Array(a) => {
o.push(b'[');
for (i, x) in a.iter().enumerate() {
if i > 0 {
o.push(b',');
}
write(x, o);
}
o.push(b']');
}
Value::Object(m) => {
// Keys in the order of their UTF-16 code units.
let mut keys: Vec<&String> = m.keys().collect();
keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
o.push(b'{');
for (i, k) in keys.into_iter().enumerate() {
if i > 0 {
o.push(b',');
}
write_str(k, o);
o.push(b':');
write(&m[k], o);
}
o.push(b'}');
}
}
}
/// A string as RFC 8785 writes it: `"` and `\\` escaped, the five short
/// control escapes, every other control character as `\u00xx`, and the rest
/// as it is.
fn write_str(s: &str, o: &mut Vec<u8>) {
o.push(b'"');
for c in s.chars() {
match c {
'"' => o.extend_from_slice(b"\\\""),
'\\' => o.extend_from_slice(b"\\\\"),
'\u{8}' => o.extend_from_slice(b"\\b"),
'\t' => o.extend_from_slice(b"\\t"),
'\n' => o.extend_from_slice(b"\\n"),
'\u{c}' => o.extend_from_slice(b"\\f"),
'\r' => o.extend_from_slice(b"\\r"),
c if (c as u32) < 0x20 => o.extend_from_slice(format!("\\u{:04x}", c as u32).as_bytes()),
c => {
let mut b = [0u8; 4];
o.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
}
}
}
o.push(b'"');
}
/// A double as ECMAScript's Number::toString writes it (ECMA-262, 6.1.6.1.20):
/// the shortest digits that read back as `x`, laid out by where the decimal
/// point falls. JSON holds no infinity or NaN; they write as `null`.
pub fn es_number(x: f64) -> String {
if !x.is_finite() {
return "null".into();
}
if x == 0.0 {
return "0".into();
}
let (s, n) = shortest(x.abs());
let digits: String = s.iter().map(|d| char::from(b'0' + d)).collect();
let k = s.len() as i32;
let mut out = String::new();
if x < 0.0 {
out.push('-');
}
if k <= n && n <= 21 {
out.push_str(&digits);
out.extend(std::iter::repeat_n('0', (n - k) as usize));
} else if 0 < n && n <= 21 {
out.push_str(&digits[..n as usize]);
out.push('.');
out.push_str(&digits[n as usize..]);
} else if -6 < n && n <= 0 {
out.push_str("0.");
out.extend(std::iter::repeat_n('0', (-n) as usize));
out.push_str(&digits);
} else {
out.push_str(&digits[..1]);
if k > 1 {
out.push('.');
out.push_str(&digits[1..]);
}
out.push('e');
out.push(if n - 1 < 0 { '-' } else { '+' });
out.push_str(&(n - 1).abs().to_string());
}
out
}
/// The digits s and the exponent n, x = 0.s × 10^n, that ECMAScript chooses:
/// the fewest that read back as `x` (> 0), and of those the nearest to `x`,
/// a tie going to the even.
fn shortest(x: f64) -> (Vec<u8>, i32) {
let e = format!("{x:e}");
let (mant, exp) = e.split_once('e').unwrap_or((&e, "0"));
let mut s: Vec<u8> = mant.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
while s.len() > 1 && s.last() == Some(&0) {
s.pop();
}
let n = exp.parse::<i32>().unwrap_or(0) + 1;
// Rust's digits are the fewest, but a tie between two equally near it
// rounds up. Only when another of the same length also reads back as x
// (near 17 digits) is there a choice: then take x's exact expansion (a
// double's has at most 767 significant digits) rounded to that length.
if ![false, true].into_iter().any(|up| step(&s, up).is_some_and(|t| reads_as(&t, n, x))) {
return (s, n);
}
let k = s.len();
let exact = format!("{x:.800e}");
let (em, ee) = exact.split_once('e').unwrap_or((&exact, "0"));
let all: Vec<u8> = em.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
let mut cn = ee.parse::<i32>().unwrap_or(0) + 1;
let mut c = all[..k].to_vec();
let rest = &all[k..];
let up = match rest.first().copied().unwrap_or(0) {
d if d > 5 => true,
5 => rest[1..].iter().any(|&d| d != 0) || c[k - 1] % 2 == 1,
_ => false,
};
if up {
match step(&c, true) {
Some(t) => c = t,
None => {
// 99…9 rounds to 10…0.
c = std::iter::once(1).chain(std::iter::repeat_n(0, k - 1)).collect();
cn += 1;
}
}
}
if reads_as(&c, cn, x) { (c, cn) } else { (s, n) }
}
/// The k-digit decimal one unit above or below `s` in its last digit, if it
/// still has k digits.
fn step(s: &[u8], up: bool) -> Option<Vec<u8>> {
let mut t = s.to_vec();
for i in (0..t.len()).rev() {
match (up, t[i]) {
(true, 9) => t[i] = 0,
(false, 0) => t[i] = 9,
(true, d) | (false, d) => {
t[i] = if up { d + 1 } else { d - 1 };
return (t[0] != 0).then_some(t);
}
}
}
None
}
/// Whether 0.s × 10^n reads as `x` (std's reader is correctly rounded).
fn reads_as(s: &[u8], n: i32, x: f64) -> bool {
let digits: String = s.iter().map(|d| char::from(b'0' + d)).collect();
format!("0.{digits}e{n}").parse::<f64>().is_ok_and(|y| y == x)
}
/// The first number in `v` that a double cannot hold exactly (an integer past
/// ±2^53), by its path. Such a number would be read differently by readers that
/// hold every number as a double, so the receipt could not bind it.
pub fn inexact_number(v: &Value) -> Option<String> {
const SAFE: u64 = (1 << 53) - 1;
fn walk(v: &Value, path: &mut String) -> bool {
match v {
Value::Number(n) => n.as_u64().is_some_and(|u| u > SAFE) || n.as_i64().is_some_and(|i| i.unsigned_abs() > SAFE),
Value::Array(a) => a.iter().enumerate().any(|(i, x)| {
let at = path.len();
path.push_str(&format!("[{i}]"));
let hit = walk(x, path);
if !hit {
path.truncate(at);
}
hit
}),
Value::Object(m) => m.iter().any(|(k, x)| {
let at = path.len();
path.push('.');
path.push_str(k);
let hit = walk(x, path);
if !hit {
path.truncate(at);
}
hit
}),
_ => false,
}
}
let mut path = String::new();
walk(v, &mut path).then(|| path.trim_start_matches('.').to_string())
}
impl Signer {
/// The key at `path`, created (32 random bytes, readable by the owner
/// only) when there is none.
@ -281,40 +30,17 @@ impl Signer {
Ok(Signer { key: SigningKey::from_bytes(&seed) })
}
/// A key for tests.
#[cfg(test)]
pub fn from_seed(seed: [u8; 32]) -> Signer {
Signer { key: SigningKey::from_bytes(&seed) }
}
/// The public key, in hex.
pub fn public_key(&self) -> String {
hex(self.key.verifying_key().as_bytes())
self.key.verifying_key().as_bytes().iter().map(|b| format!("{b:02x}")).collect()
}
/// The receipt for `result` of `request`.
pub fn seal(&self, request: &Value, result: &Value) -> Value {
let d = digest(request, result);
let sig = self.key.sign(&d);
json!({ "alg": "ed25519", "canon": CANON, "key": self.public_key(), "digest": hex(&d), "signature": hex(&sig.to_bytes()) })
receipt::seal(&self.key, request, result)
}
}
/// Whether `receipt` is a valid seal of `result` of `request` under the key
/// it names.
pub fn verify(request: &Value, result: &Value, receipt: &Value) -> bool {
let field = |k: &str| receipt.get(k).and_then(Value::as_str).and_then(unhex);
let (Some(key), Some(sig)) = (field("key"), field("signature")) else { return false };
let (Ok(key), Ok(sig)) = (<[u8; 32]>::try_from(key.as_slice()), <[u8; 64]>::try_from(sig.as_slice())) else { return false };
let Ok(vk) = VerifyingKey::from_bytes(&key) else { return false };
let d = match receipt.get("canon").and_then(Value::as_str) {
Some(CANON) => digest(request, result),
None => digest_v1(request, result),
Some(_) => return false,
};
field("digest").is_some_and(|g| g == d) && vk.verify(&d, &wai_quantum::ed25519_dalek::Signature::from_bytes(&sig)).is_ok()
}
#[cfg(unix)]
fn write_private(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
use std::io::Write;
@ -341,100 +67,6 @@ pub fn default_path() -> PathBuf {
mod tests {
use super::*;
#[test]
fn a_receipt_verifies_and_any_change_breaks_it() {
let s = Signer::from_seed([7; 32]);
let req = json!({"qasm": "OPENQASM 3.0;", "shots": 10});
let res = json!({"method": "statevector", "counts": {"0": 10}});
let r = s.seal(&req, &res);
assert!(verify(&req, &res, &r));
assert!(!verify(&req, &json!({"method": "statevector", "counts": {"0": 9}}), &r));
assert!(!verify(&json!({"qasm": "OPENQASM 3.0;", "shots": 11}), &res, &r));
let mut forged = r.clone();
forged["key"] = json!(Signer::from_seed([8; 32]).public_key());
assert!(!verify(&req, &res, &forged));
}
#[test]
fn numbers_are_written_as_ecmascript_writes_them() {
// Bit patterns and ECMAScript's Number::toString of each, written by a
// JavaScript engine: RFC 8785's appendix B, the edges of each layout,
// and patterns spread over every exponent.
let mut n = 0;
for line in include_str!("../testdata/es-number-tostring.txt").lines() {
let (bits, want) = line.split_once(' ').unwrap();
let x = f64::from_bits(u64::from_str_radix(bits, 16).unwrap());
assert_eq!(es_number(x), want, "{bits}");
// And the text reads back as the same double, as it must for a
// result to verify after a round trip (serde_json's
// float_roundtrip: its default reader rounds twice).
if x != 0.0 {
assert_eq!(serde_json::from_str::<f64>(want).unwrap().to_bits(), x.to_bits(), "{bits} {want}");
}
n += 1;
}
assert!(n > 3000);
}
/// The same against a larger set, in the same format, written by a
/// JavaScript engine: `WAI_ES_VECTORS=path cargo test -- --ignored`.
#[test]
#[ignore]
fn numbers_match_a_javascript_engine_on_a_large_set() {
let path = std::env::var("WAI_ES_VECTORS").expect("WAI_ES_VECTORS: a file of `hexbits text` lines");
let text = std::fs::read_to_string(path).unwrap();
let mut n = 0u64;
for line in text.lines() {
let (bits, want) = line.split_once(' ').unwrap();
assert_eq!(es_number(f64::from_bits(u64::from_str_radix(bits, 16).unwrap())), want, "{bits}");
n += 1;
}
eprintln!("{n} numbers agree");
}
#[test]
fn the_canonical_text_sorts_keys_and_writes_numbers_and_strings_one_way() {
let v = json!({ "b": 1.0, "a": [true, null, "x\n\u{1}\"\\é"], "\u{e000}": 0.000001, "\u{1f600}": -0.0, "c": 1e21 });
// U+1F600 is a surrogate pair, so it sorts before U+E000.
assert_eq!(String::from_utf8(canonical(&v)).unwrap(), "{\"a\":[true,null,\"x\\n\\u0001\\\"\\\\é\"],\"b\":1,\"c\":1e+21,\"\u{1f600}\":0,\"\u{e000}\":0.000001}");
}
#[test]
fn a_receipt_verifies_after_a_round_trip_through_another_json_writer() {
let s = Signer::from_seed([7; 32]);
let req = json!({ "qasm": "qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 1000, "observables": ["Z0 Z1"] });
let res = json!({ "method": "stabilizer", "expectations": [{ "observable": "Z0 Z1", "coeff": 1.0, "value": 1.0 }], "threshold": 0.00001, "elapsed_ms": 0 });
let r = s.seal(&req, &res);
// What a browser sends back: integral doubles without a fraction, keys
// in another order, the threshold in plain decimals.
let req2: Value = serde_json::from_str(r#"{"observables":["Z0 Z1"],"shots":1000,"qasm":"qubit[2] q; h q[0]; cx q[0], q[1];"}"#).unwrap();
let res2: Value = serde_json::from_str(r#"{"elapsed_ms":0,"threshold":0.00001,"method":"stabilizer","expectations":[{"value":1,"coeff":1,"observable":"Z0 Z1"}]}"#).unwrap();
assert!(verify(&req2, &res2, &r));
let res3: Value = serde_json::from_str(r#"{"elapsed_ms":0,"threshold":0.00001,"method":"stabilizer","expectations":[{"value":0.999,"coeff":1,"observable":"Z0 Z1"}]}"#).unwrap();
assert!(!verify(&req2, &res3, &r));
}
#[test]
fn the_first_receipts_still_verify_and_an_unknown_form_does_not() {
let s = Signer::from_seed([7; 32]);
let req = json!({ "qasm": "OPENQASM 3.0;", "shots": 10 });
let res = json!({ "method": "statevector", "counts": { "0": 10 } });
let d = digest_v1(&req, &res);
let old = json!({ "alg": "ed25519", "key": s.public_key(), "digest": hex(&d), "signature": hex(&s.key.sign(&d).to_bytes()) });
assert!(verify(&req, &res, &old));
let mut other = s.seal(&req, &res);
other["canon"] = json!("something-else");
assert!(!verify(&req, &res, &other));
}
#[test]
fn integers_a_double_cannot_hold_are_found_by_path() {
assert_eq!(inexact_number(&json!({ "seed": 9007199254740991u64, "shots": 10 })), None);
assert_eq!(inexact_number(&json!({ "seed": 9007199254740992u64 })).as_deref(), Some("seed"));
assert_eq!(inexact_number(&json!({ "options": { "x": [1, -9007199254740993i64] } })).as_deref(), Some("options.x[1]"));
assert_eq!(inexact_number(&json!({ "noise": { "readout": 1e300 } })), None);
}
#[test]
fn a_key_is_created_once_private_and_then_reused() {
let dir = std::env::temp_dir().join(format!("wai-qsvc-key-{}", std::process::id()));

View file

@ -1079,7 +1079,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"block2",
@ -1090,6 +1090,7 @@ dependencies = [
"objc2-foundation",
"objc2-metal",
"opencl3",
"serde",
"serde_json",
"wgpu",
]

View file

@ -1,6 +1,6 @@
[package]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
edition = "2024"
rust-version = "1.97"
license = "Apache-2.0"
@ -26,6 +26,8 @@ all-features = true
blake3 = "1"
# Canonical receipt JSON (sorted keys, byte-stable round-trip).
serde_json = { version = "1", features = ["preserve_order"] }
# The engine's request and plan types (feature quantum_engine).
serde = { version = "1", features = ["derive"], optional = true }
# Receipt signatures. Optional — only the sealing features need it.
ed25519-dalek = { version = "2", optional = true }
@ -80,7 +82,15 @@ quantum_tableau = []
# Exact amplitudes, probabilities and samples of a Clifford circuit measured in a
# product basis, at a cost set by its cut-rank (src/quantum_cutrank.rs).
quantum_cutrank = ["quantum_tableau", "quantum_sv"]
# OpenQASM 3 (and the OpenQASM 2 forms in common use) read to one program and
# lowered to each method's input (src/qasm.rs).
quantum_qasm = ["quantum", "quantum_sv", "quantum_spd", "quantum_tableau"]
quantum_mpsim = ["quantum_sv", "quantum_mps"]
# The job service's engine: a request planned onto a method, run, and answered
# as the service's JSON, for every host that runs jobs (src/engine/). Numbers
# are read correctly rounded, so a result a client sends back reads as the
# double it was.
quantum_engine = ["quantum_qasm", "quantum_tn", "quantum_cutrank", "quantum_mpsim", "quantum_noisy", "dep:serde", "serde_json/float_roundtrip"]
quantum_pauli = ["quantum"]
# Sparse Pauli dynamics at utility scale: arbitrary-angle Pauli rotations on up
# to 1024 qubits, reproducible bit for bit, with a rigorous truncation bound.
@ -206,7 +216,7 @@ quantum_phasor_meter = ["_signing"]
# Everything.
full = [
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
"quantum", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_tableau", "quantum_cutrank", "quantum_qasm", "quantum_engine", "quantum_mpsim", "quantum_pauli", "quantum_spd", "quantum_spd_receipt", "quantum_bptn", "quantum_resource", "quantum_resource_receipt", "quantum_frame", "quantum_tn", "quantum_gbs", "quantum_sv", "quantum_arch", "quantum_ldpc", "quantum_qio", "quantum_uf", "quantum_noisy", "quantum_dmrg", "quantum_tdvp", "quantum_chem", "quantum_nqs", "quantum_qir", "quantum_info", "quantum_qudit", "quantum_comm", "quantum_source", "quantum_channel",
"quantum_receipt", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec",
"quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_fabric",
"quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_qdata",

View file

@ -67,6 +67,8 @@ never creates information.
| `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); **device-ready OpenQASM 2/3** (`device::device_qasm`): every gate lowered exactly to its dialect's standard library, multi-controlled gates included, every qubit measured into `c[i]`, and the service's counts read back onto the crate's basis; **native programs** (`native::compile`): a processor's own basis (`rz`/`sx`/`x`/`cz`; `prx`/`cz`; or `gpi`/`gpi2`/`zz` with angles in turns, written exactly; the Z rotations carried in the phases of the last two) on its own qubits, routed over its coupling map, and checked against the circuit by simulation before it is sent — Bloch / entanglement / purity analysis |
| `quantum_qasm` | **the OpenQASM reader the job service runs**, for every surface that takes a program: OpenQASM 3 and the OpenQASM 2 forms in common use, every register, a gate on a whole register, gate definitions with parameters, `ctrl`/`negctrl` modifiers, angle expressions, comments, and measurement in every spelling. A program that measures and then acts, resets after a gate or uses `if` is kept as dynamic steps; loops, `inv`, `pow` and anything else not listed are refused with their line. Lowered to each method's input exactly up to a global phase. `qasm::dyadic_circuit` gives the exact state vector's circuit (dyadic angles) for a program whose measurements all come at the end, and refuses any other with the reason |
| `quantum_engine` | **the quantum job service's engine, for every host**: a job (an OpenQASM program and what is asked of it: shots, Pauli expectation values, amplitudes, noise) planned onto the strongest method it admits, with the reason and the memory and work it will take; run; answered in the service's JSON with its accuracy class; and sealed over the request and the result in their canonical JSON (`engine::job`, `engine::receipt`), so a result from the service, the command line or a browser tab checks the same way. Also a program's circuit drawn as text (`engine::draw`). It builds for `wasm32-unknown-unknown`, where it runs on one thread and claims no time |
| `quantum_stabilizer` | stabilizer (CHP) tableau — `O(n²)`, scales far past statevector |
| `quantum_mps` | tensor-network (matrix-product-state) backend |
| `quantum_pauli` | sparse-Pauli / Heisenberg observable propagation over the dyadic gate set (up to 64 qubits) |

View file

@ -36,14 +36,14 @@ pub const MAX_AMPLITUDES: usize = 64;
pub const MAX_OUTCOMES: usize = 65_536;
pub const EXACT_MAX_QUBITS: u32 = 26;
pub const STABILIZER_MAX_QUBITS: u32 = wai_quantum::quantum_tableau::MAX_QUBITS;
pub const STABILIZER_MAX_QUBITS: u32 = crate::quantum_tableau::MAX_QUBITS;
pub const PAULI_MAX_QUBITS: u32 = 1024;
pub const DENSITY_MAX_QUBITS: u32 = wai_quantum::quantum_noisy::MAX_EVOLVE_QUBITS;
pub const DENSITY_MAX_QUBITS: u32 = crate::quantum_noisy::MAX_EVOLVE_QUBITS;
/// The most a density-matrix job may do: its gates times the `4ⁿ` entries
/// of `ρ`, a few minutes of passes.
pub const DENSITY_MAX_WORK: u64 = 1_000_000_000_000;
pub const TRAJECTORY_MAX_QUBITS: u32 = 32;
pub const MPS_MAX_QUBITS: u32 = wai_quantum::quantum_mpsim::MAX_QUBITS;
pub const MPS_MAX_QUBITS: u32 = crate::quantum_mpsim::MAX_QUBITS;
/// The bond dimension an `mps` job gets unless it asks, and the most it may.
pub const MPS_DEFAULT_BOND: usize = 64;
pub const MPS_MAX_BOND: usize = 256;
@ -131,11 +131,25 @@ pub fn density_max_qubits(job_bytes: u64) -> u32 {
}
/// The most qubits the GPU's largest buffer holds, asked of the device once.
/// A build without the GPU state vector (feature `quantum_sv_metal`) has none.
pub fn gpu_max_qubits() -> Result<u32, String> {
static MAX: std::sync::OnceLock<Result<u32, String>> = std::sync::OnceLock::new();
MAX.get_or_init(|| wai_quantum::quantum_sv_metal::GpuState::max_qubits().map_err(|e| e.to_string())).clone()
#[cfg(feature = "quantum_sv_metal")]
{
static MAX: std::sync::OnceLock<Result<u32, String>> = std::sync::OnceLock::new();
MAX.get_or_init(|| crate::quantum_sv_metal::GpuState::max_qubits().map_err(|e| e.to_string())).clone()
}
#[cfg(not(feature = "quantum_sv_metal"))]
{
Err("this build has no GPU state vector".into())
}
}
/// The widest gate the GPU state vector applies.
#[cfg(feature = "quantum_sv_metal")]
const SINGLE_MAX_WIDTH: usize = crate::quantum_sv_metal::MAX_WIDTH;
#[cfg(not(feature = "quantum_sv_metal"))]
const SINGLE_MAX_WIDTH: usize = 4;
/// The most qubits the single-precision state vector holds in `job_bytes`.
pub fn single_max_qubits(job_bytes: u64) -> u32 {
(3..=40u32).take_while(|&n| (8u64 << n) + 256 * MIB <= job_bytes).last().unwrap_or(0)
@ -143,7 +157,7 @@ pub fn single_max_qubits(job_bytes: u64) -> u32 {
/// The most qubits the dense state vector holds in `job_bytes`.
pub fn statevector_max_qubits(job_bytes: u64) -> u32 {
(1..=wai_quantum::quantum_sv::MAX_QUBITS).rev().find(|&n| statevector_bytes(n) <= job_bytes).unwrap_or(0)
(1..=crate::quantum_sv::MAX_QUBITS).rev().find(|&n| statevector_bytes(n) <= job_bytes).unwrap_or(0)
}
/// An observable as asked: its text, its coefficient, and its Pauli factors
@ -158,7 +172,7 @@ pub struct Request {
pub seed: u64,
pub observables: Vec<Observable>,
pub amplitudes: Vec<String>,
pub noise: Option<wai_quantum::quantum_noisy::NoiseModel>,
pub noise: Option<crate::quantum_noisy::NoiseModel>,
pub options: Value,
}
@ -192,12 +206,20 @@ fn observable(v: &Value, n: u32) -> Result<Observable, String> {
Ok((text, coeff, string))
}
fn noise(v: &Value) -> Result<wai_quantum::quantum_noisy::NoiseModel, String> {
/// The noise channels a job may name.
pub const NOISE_CHANNELS: [&str; 5] = ["depolarizing_1q", "depolarizing_2q", "amplitude_damping", "dephasing", "readout"];
fn noise(v: &Value) -> Result<crate::quantum_noisy::NoiseModel, String> {
// A channel the model does not know is refused: read as no noise, a
// misspelt one would run the circuit noiseless.
if let Some(k) = v.as_object().and_then(|o| o.keys().find(|k| !NOISE_CHANNELS.contains(&k.as_str()))) {
return Err(format!("noise {k}: no such channel; the channels are {}", NOISE_CHANNELS.join(", ")));
}
let f = |k: &str| -> Result<f64, String> {
let x = v.get(k).and_then(Value::as_f64).unwrap_or(0.0);
if (0.0..=1.0).contains(&x) { Ok(x) } else { Err(format!("noise {k} is a probability, 0 to 1")) }
};
Ok(wai_quantum::quantum_noisy::NoiseModel {
Ok(crate::quantum_noisy::NoiseModel {
depol1: f("depolarizing_1q")?,
depol2: f("depolarizing_2q")?,
damping: f("amplitude_damping")?,
@ -284,9 +306,9 @@ pub fn mps_work(r: &Request, chi: u64) -> u64 {
/// memory and work; the run (`thorough`) makes several longer ones and keeps
/// the cheapest order, which is never costlier than the planner's. Both are
/// deterministic.
pub fn product_measured(r: &Request, thorough: bool) -> Result<wai_quantum::quantum_cutrank::ProductMeasured, String> {
use wai_quantum::quantum_cutrank::ProductMeasured;
use wai_quantum::quantum_tableau::Tableau;
pub fn product_measured(r: &Request, thorough: bool) -> Result<crate::quantum_cutrank::ProductMeasured, String> {
use crate::quantum_cutrank::ProductMeasured;
use crate::quantum_tableau::Tableau;
let n = r.program.n_qubits;
if n > CUTRANK_MAX_QUBITS {
return Err(format!("cutrank holds at most {CUTRANK_MAX_QUBITS} qubits"));
@ -312,15 +334,21 @@ pub fn product_measured(r: &Request, thorough: bool) -> Result<wai_quantum::quan
// candidates, and none wider than it at its widest cut is taken, so the
// job stays inside the memory the planner admitted it with.
let m = moves(CUTRANK_RUN_OPS, 300_000);
let found: Vec<Vec<usize>> = std::thread::scope(|sc| {
let handles: Vec<_> = (1..=CUTRANK_RUN_SEEDS)
.map(|seed| {
let pm = &pm;
sc.spawn(move || pm.anneal_order(m, seed))
})
.collect();
handles.into_iter().map(|h| h.join().expect("a search")).collect()
});
// One thread per seed, where there are threads; a browser tab has one, so
// there the seeds run in turn and find the same orders.
let found: Vec<Vec<usize>> = if cfg!(target_arch = "wasm32") {
(1..=CUTRANK_RUN_SEEDS).map(|seed| pm.anneal_order(m, seed)).collect()
} else {
std::thread::scope(|sc| {
let handles: Vec<_> = (1..=CUTRANK_RUN_SEEDS)
.map(|seed| {
let pm = &pm;
sc.spawn(move || pm.anneal_order(m, seed))
})
.collect();
handles.into_iter().map(|h| h.join().expect("a search")).collect()
})
};
let mut best = quick;
for o in &found {
let cand = pm.clone().with_order(o).map_err(|e| e.to_string())?;
@ -344,9 +372,9 @@ pub const CUTRANK_PHASE_OPS: u64 = 50_000_000_000;
/// identity in place of the closing gates. The lowering of the gates to
/// Clifford ones drops a phase `e^{iΘ}` ([`qasm::Program::product_measured`]).
/// The factor is `e^{iΘ} a / b`, of modulus 1.
pub fn cutrank_phase(r: &Request, pm: &wai_quantum::quantum_cutrank::ProductMeasured, cut: usize, precision: wai_quantum::quantum_cutrank::Precision) -> Result<Option<wai_quantum::gates::C>, String> {
use wai_quantum::gates::complex;
use wai_quantum::quantum_tableau::{Clifford, PhasedTableau};
pub fn cutrank_phase(r: &Request, pm: &crate::quantum_cutrank::ProductMeasured, cut: usize, precision: crate::quantum_cutrank::Precision) -> Result<Option<crate::gates::C>, String> {
use crate::gates::complex;
use crate::quantum_tableau::{Clifford, PhasedTableau};
let n = r.program.n_qubits as u64;
let (bulk, _, theta) = r.program.product_measured()?;
let hadamards = bulk.iter().map(|g| match g {
@ -367,7 +395,7 @@ pub fn cutrank_phase(r: &Request, pm: &wai_quantum::quantum_cutrank::ProductMeas
let ratio = complex((are * b.re + aim * b.im) / bb, (aim * b.re - are * b.im) / bb);
let k = complex(theta.re * ratio.re - theta.im * ratio.im, theta.re * ratio.im + theta.im * ratio.re);
let off = (k.re.hypot(k.im) - 1.0).abs();
let tolerance = if precision == wai_quantum::quantum_cutrank::Precision::Double { 1e-9 } else { 1e-5 };
let tolerance = if precision == crate::quantum_cutrank::Precision::Double { 1e-9 } else { 1e-5 };
if off > tolerance {
return Err(format!("internal: the tracked and summed amplitudes differ in modulus by {off:e}"));
}
@ -376,7 +404,7 @@ pub fn cutrank_phase(r: &Request, pm: &wai_quantum::quantum_cutrank::ProductMeas
/// The memory a `cutrank` job takes: its largest table with a margin, the
/// graph form, and room to run.
pub fn cutrank_bytes(pm: &wai_quantum::quantum_cutrank::ProductMeasured, precision: wai_quantum::quantum_cutrank::Precision) -> u64 {
pub fn cutrank_bytes(pm: &crate::quantum_cutrank::ProductMeasured, precision: crate::quantum_cutrank::Precision) -> u64 {
let n = pm.qubits() as u64;
let table = pm.table_bytes(precision);
table + table / 16 + n * n.div_ceil(64) * 8 * 4 + 256 * MIB
@ -384,13 +412,13 @@ pub fn cutrank_bytes(pm: &wai_quantum::quantum_cutrank::ProductMeasured, precisi
/// Whether `method` can take the job, and the plan if so; otherwise why not.
/// A program that measures in the middle, resets after a qubit's first gate,
/// or acts on measured bits ([`crate::dynamic`]): the dense state vector and
/// or acts on measured bits ([`crate::engine::dynamic`]): the dense state vector and
/// the stabilizer tableau run it, splitting its shots at each measurement and
/// keeping a copy of the state for each branch that runs on.
fn try_dynamic(method: &str, r: &Request, clifford: &Result<Vec<wai_quantum::quantum_tableau::Clifford>, String>, cap: u64, steps: &[crate::qasm::Instr]) -> Result<Plan, String> {
fn try_dynamic(method: &str, r: &Request, clifford: &Result<Vec<crate::quantum_tableau::Clifford>, String>, cap: u64, steps: &[crate::qasm::Instr]) -> Result<Plan, String> {
let n = r.program.n_qubits;
let gates = r.program.ops.len() as u64;
let measured = crate::dynamic::measurements(steps);
let measured = crate::engine::dynamic::measurements(steps);
if r.noise.is_some() {
return Err("noise in a program that measures in the middle, resets or acts on measured bits is not simulated yet".into());
}
@ -400,12 +428,16 @@ fn try_dynamic(method: &str, r: &Request, clifford: &Result<Vec<wai_quantum::qua
let plan = |memory_bytes: u64, work: u64, why: String| Plan { method: method.into(), why, memory_bytes, work, qubits: n };
match method {
"statevector" => {
let one = statevector_bytes(n);
if n > wai_quantum::quantum_sv::MAX_QUBITS || 2 * one > cap {
return Err(format!("statevector runs a program that measures in the middle on at most {} qubits: a branch holds a second state", statevector_max_qubits(cap / 2)));
// At least two states (a branch's copy beside the state it left),
// at most sixteen, and the job's margin once, as for trajectories.
let fits = |m: u32| 2 * amplitude_bytes(m) + 256 * MIB <= cap;
if n > crate::quantum_sv::MAX_QUBITS || !fits(n) {
let most = (1..=crate::quantum_sv::MAX_QUBITS).rev().find(|&m| fits(m)).unwrap_or(0);
return Err(format!("statevector runs a program that measures in the middle on at most {most} qubits: a branch holds a second state"));
}
let states = (cap / one).clamp(2, 16);
Ok(plan(states * one + r.shots * 24, (gates << n).saturating_mul(r.shots.clamp(1, 64)), format!("dense double-precision state vector of {n} qubits, its shots split at each of {measured} mid-circuit measurements and resets")))
let one = amplitude_bytes(n);
let states = (cap.saturating_sub(256 * MIB) / one).clamp(2, 16);
Ok(plan(states * one + 256 * MIB + r.shots * 24, (gates << n).saturating_mul(r.shots.clamp(1, 64)), format!("dense double-precision state vector of {n} qubits, its shots split at each of {measured} mid-circuit measurements and resets")))
}
"stabilizer" => {
if let Err(e) = clifford {
@ -430,7 +462,7 @@ fn try_dynamic(method: &str, r: &Request, clifford: &Result<Vec<wai_quantum::qua
}
}
fn try_method(method: &str, r: &Request, dyadic: &Result<wai_quantum::quantum::Circuit, String>, clifford: &Result<Vec<wai_quantum::quantum_tableau::Clifford>, String>, cap: u64) -> Result<Plan, String> {
fn try_method(method: &str, r: &Request, dyadic: &Result<crate::quantum::Circuit, String>, clifford: &Result<Vec<crate::quantum_tableau::Clifford>, String>, cap: u64) -> Result<Plan, String> {
let n = r.program.n_qubits;
let gates = r.program.ops.len() as u64;
let plan = |memory_bytes: u64, work: u64, why: String| Plan { method: method.into(), why, memory_bytes, work, qubits: n };
@ -456,7 +488,7 @@ fn try_method(method: &str, r: &Request, dyadic: &Result<wai_quantum::quantum::C
}
"statevector" => {
let bytes = statevector_bytes(n);
if bytes > cap || n > wai_quantum::quantum_sv::MAX_QUBITS {
if bytes > cap || n > crate::quantum_sv::MAX_QUBITS {
return Err(format!("statevector holds at most {} qubits in a job's memory", statevector_max_qubits(cap)));
}
Ok(plan(bytes + r.shots * 24, gates << n, format!("dense double-precision state vector of {n} qubits")))
@ -467,7 +499,7 @@ fn try_method(method: &str, r: &Request, dyadic: &Result<wai_quantum::quantum::C
if n > max || bytes > cap {
return Err(format!("statevector_single holds at most {} qubits on this GPU in a job's memory", max.min(single_max_qubits(cap))));
}
if r.program.ops.iter().any(|o| o.qubits.len() > wai_quantum::quantum_sv_metal::MAX_WIDTH) {
if r.program.ops.iter().any(|o| o.qubits.len() > SINGLE_MAX_WIDTH) {
return Err("statevector_single takes gates on at most four qubits".into());
}
Ok(plan(bytes + r.shots * 24, gates << n, format!("dense single-precision state vector of {n} qubits on the GPU")))
@ -535,7 +567,7 @@ fn try_method(method: &str, r: &Request, dyadic: &Result<wai_quantum::quantum::C
Ok(plan(memory, work, format!("matrix product state of {n} qubits at bond dimension {chi}")))
}
"cutrank" => {
use wai_quantum::quantum_cutrank::Precision;
use crate::quantum_cutrank::Precision;
if wants_expectations {
return Err("cutrank answers counts and amplitudes".into());
}
@ -706,6 +738,18 @@ mod tests {
assert!(plan(&job(rot, json!({ "method": "nonsense" })), CAP).unwrap_err().contains("unknown method"));
}
/// A noise channel the model does not know is refused, never read as no
/// noise.
#[test]
fn an_unknown_noise_channel_is_refused() {
let bell = "qubit[2] q; h q[0]; cx q[0], q[1];";
let e = plan(&job(bell, json!({ "noise": { "depolarizing": 0.1 } })), CAP).unwrap_err();
assert!(e.contains("noise depolarizing: no such channel") && e.contains("depolarizing_1q"), "{e}");
for k in NOISE_CHANNELS {
assert!(plan(&job(bell, json!({ "noise": { k: 0.01 } })), CAP).is_ok(), "{k}");
}
}
/// A program that measures in the middle goes to the tableau when it is
/// Clifford and to the state vector otherwise; every other method, noise
/// and amplitudes are refused with what would run it.
@ -718,7 +762,7 @@ mod tests {
assert!(p.why.contains("split at each of 2 mid-circuit measurements"), "{}", p.why);
let p = plan(&job(rotated, json!({ "observables": ["Z1"] })), CAP).unwrap();
assert_eq!(p.method, "statevector");
assert!(p.memory_bytes >= 2 * statevector_bytes(2), "room for a branch's copy");
assert!(p.memory_bytes >= 2 * amplitude_bytes(2) + 256 * MIB, "room for a branch's copy");
for (extra, needle) in [
(json!({ "method": "mps" }), "statevector and stabilizer do"),
(json!({ "method": "exact" }), "statevector and stabilizer do"),

View file

@ -0,0 +1,321 @@
//! A program's circuit as text: one wire per qubit, gates packed into the
//! earliest column their qubits are free, a control as `●`, a CX target as
//! `⊕`, a swap as `×`, a measurement into bit `k` as `Mk`, a reset as `|0⟩`.
//! A gate under a condition carries `?n`, and the legend under the drawing
//! says what condition `n` is. Wide circuits wrap at `width` columns.
use std::f64::consts::PI;
use crate::qasm::{Cond, Instr, Kind, Op, Program};
/// One thing drawn in a column: its label on each qubit it touches, and
/// whether a vertical line joins them.
struct Item {
cells: Vec<(u32, String)>,
joined: bool,
}
/// `x` as a multiple of π where it is one with a small denominator, else in
/// decimals.
pub fn angle(x: f64) -> String {
let r = x / PI;
for d in [1i64, 2, 3, 4, 6, 8, 12, 16] {
let n = r * d as f64;
if (n - n.round()).abs() < 1e-9 {
let n = n.round() as i64;
let g = gcd(n.abs(), d);
let (n, d) = (n / g, d / g);
return match (n, d) {
(0, _) => "0".into(),
(1, 1) => "π".into(),
(-1, 1) => "-π".into(),
(n, 1) => format!("{n}π"),
(1, d) => format!("π/{d}"),
(-1, d) => format!("-π/{d}"),
(n, d) => format!("{n}π/{d}"),
};
}
}
let t = format!("{x:.4}");
t.trim_end_matches('0').trim_end_matches('.').to_string()
}
fn gcd(a: i64, b: i64) -> i64 {
if b == 0 { a.max(1) } else { gcd(b, a % b) }
}
fn params(p: &[f64]) -> String {
p.iter().map(|&x| angle(x)).collect::<Vec<_>>().join(",")
}
/// The cells of one gate.
fn gate(op: &Op) -> Item {
use Kind::*;
let q = &op.qubits;
let p = params(&op.params);
let one = |l: &str| Item { cells: vec![(q[0], l.to_string())], joined: false };
let ctl = |rest: Vec<(u32, String)>| {
let mut cells = vec![(q[0], "●".to_string())];
cells.extend(rest);
Item { cells, joined: true }
};
match op.kind {
Id => one("I"),
X => one("X"),
Y => one("Y"),
Z => one("Z"),
H => one("H"),
S => one("S"),
Sdg => one("S†"),
T => one("T"),
Tdg => one("T†"),
Sx => one("√X"),
Sxdg => one("√X†"),
Rx => one(&format!("Rx({p})")),
Ry => one(&format!("Ry({p})")),
Rz => one(&format!("Rz({p})")),
P => one(&format!("P({p})")),
U => one(&format!("U({p})")),
Cx => ctl(vec![(q[1], "⊕".into())]),
Cy => ctl(vec![(q[1], "Y".into())]),
Cz => ctl(vec![(q[1], "●".into())]),
Ch => ctl(vec![(q[1], "H".into())]),
Cp => ctl(vec![(q[1], format!("P({p})"))]),
Crx => ctl(vec![(q[1], format!("Rx({p})"))]),
Cry => ctl(vec![(q[1], format!("Ry({p})"))]),
Crz => ctl(vec![(q[1], format!("Rz({p})"))]),
Cu => ctl(vec![(q[1], format!("U({p})"))]),
Swap => Item { cells: vec![(q[0], "×".into()), (q[1], "×".into())], joined: true },
Iswap => Item { cells: vec![(q[0], "iSWAP".into()), (q[1], "iSWAP".into())], joined: true },
Rxx => Item { cells: vec![(q[0], format!("Rxx({p})")), (q[1], format!("Rxx({p})"))], joined: true },
Ryy => Item { cells: vec![(q[0], format!("Ryy({p})")), (q[1], format!("Ryy({p})"))], joined: true },
Rzz => Item { cells: vec![(q[0], format!("Rzz({p})")), (q[1], format!("Rzz({p})"))], joined: true },
Ccx => Item { cells: vec![(q[0], "●".into()), (q[1], "●".into()), (q[2], "⊕".into())], joined: true },
Ccz => Item { cells: vec![(q[0], "●".into()), (q[1], "●".into()), (q[2], "●".into())], joined: true },
Cswap => Item { cells: vec![(q[0], "●".into()), (q[1], "×".into()), (q[2], "×".into())], joined: true },
}
}
/// A condition as the legend writes it: the bits, highest first, read as an
/// unsigned integer.
fn condition(c: &Cond) -> String {
let op = if c.equal { "=" } else { "≠" };
match c.bits.as_slice() {
[b] => format!("if bit {b} {op} {}", c.value),
bits => {
let names: Vec<String> = bits.iter().rev().map(|b| b.to_string()).collect();
format!("if bits ({}) {op} {}", names.join(" "), c.value)
}
}
}
/// The items to draw, in order, and the legend of conditions.
fn items(p: &Program) -> (Vec<Item>, Vec<String>) {
let mut out = Vec::new();
let mut legend = Vec::new();
let Some(steps) = &p.dynamic else {
out.extend(p.ops.iter().map(gate));
out.extend(p.measured.iter().map(|&(q, b)| Item { cells: vec![(q, format!("M{b}"))], joined: false }));
return (out, legend);
};
// Each step's condition: a JumpUnless guards the steps up to its target;
// a Jump at the end of that region skips an else branch, up to its own.
let mut guard: Vec<Option<usize>> = vec![None; steps.len()];
for (i, s) in steps.iter().enumerate() {
if let Instr::JumpUnless(c, to) = s {
legend.push(condition(c));
let n = legend.len();
let then_end = *to;
let else_end = match steps.get(then_end.wrapping_sub(1)) {
Some(Instr::Jump(e)) if then_end > i + 1 => Some(*e),
_ => None,
};
for g in guard.iter_mut().take(then_end.min(steps.len())).skip(i + 1) {
g.get_or_insert(n);
}
if let Some(e) = else_end {
legend.push(format!("else of ?{n}"));
let m = legend.len();
for g in guard.iter_mut().take(e.min(steps.len())).skip(then_end) {
g.get_or_insert(m);
}
}
}
}
for (i, s) in steps.iter().enumerate() {
let mut item = match s {
Instr::Gate(k) => gate(&p.ops[*k]),
Instr::Measure { qubit, bit } => Item { cells: vec![(*qubit, format!("M{bit}"))], joined: false },
Instr::Reset(q) => Item { cells: vec![(*q, "|0⟩".into())], joined: false },
Instr::JumpUnless(..) | Instr::Jump(_) => continue,
};
if let Some(n) = guard[i] {
for (_, l) in item.cells.iter_mut() {
l.push_str(&format!("?{n}"));
}
}
out.push(item);
}
(out, legend)
}
/// The drawing, wrapped at `width` columns.
pub fn draw(p: &Program, width: usize) -> String {
let n = p.n_qubits as usize;
if n == 0 {
return "(no qubits)\n".into();
}
let (items, legend) = items(p);
// Columns: each item goes to the first column after the last one used on
// any wire it covers.
let mut free = vec![0usize; n];
let mut columns: Vec<Vec<Item>> = Vec::new();
for it in items {
let lo = it.cells.iter().map(|c| c.0 as usize).min().unwrap_or(0);
let hi = if it.joined { it.cells.iter().map(|c| c.0 as usize).max().unwrap_or(lo) } else { lo };
let col = free[lo..=hi].iter().copied().max().unwrap_or(0);
for f in &mut free[lo..=hi] {
*f = col + 1;
}
if columns.len() <= col {
columns.resize_with(col + 1, Vec::new);
}
columns[col].push(it);
}
let label_w = format!("q{}", n - 1).chars().count();
let prefix = |q: usize| format!("{:>label_w$}: ", format!("q{q}"));
let blank = " ".repeat(label_w + 2);
// Each column's text per wire, and its joins between wire q and q + 1.
let mut blocks: Vec<(usize, Vec<String>, Vec<bool>)> = Vec::new();
for col in &columns {
let w = col.iter().flat_map(|it| it.cells.iter().map(|c| c.1.chars().count())).max().unwrap_or(1).max(1);
let mut wires = vec![None::<String>; n];
let mut through = vec![false; n];
let mut joins = vec![false; n];
for it in col {
for (q, l) in &it.cells {
wires[*q as usize] = Some(l.clone());
}
if it.joined {
let lo = it.cells.iter().map(|c| c.0 as usize).min().unwrap();
let hi = it.cells.iter().map(|c| c.0 as usize).max().unwrap();
for (q, t) in through.iter_mut().enumerate().take(hi).skip(lo + 1) {
*t = wires[q].is_none();
}
for j in joins.iter_mut().take(hi).skip(lo) {
*j = true;
}
}
}
let text: Vec<String> = (0..n)
.map(|q| {
let l = match &wires[q] {
Some(l) => l.clone(),
None if through[q] => "┼".into(),
None => "─".into(),
};
let pad = w - l.chars().count();
format!("─{}{l}{}─", "─".repeat(pad / 2), "─".repeat(pad - pad / 2))
})
.collect();
blocks.push((w + 2, text, joins));
}
let mut out = String::new();
let avail = width.saturating_sub(label_w + 4).max(8);
let mut start = 0;
while start < blocks.len() || (start == 0 && blocks.is_empty()) {
let mut end = start;
let mut used = 0;
while end < blocks.len() && (end == start || used + blocks[end].0 <= avail) {
used += blocks[end].0;
end += 1;
}
if start > 0 {
out.push('\n');
}
for q in 0..n {
out.push_str(&prefix(q));
out.push('─');
for b in &blocks[start..end] {
out.push_str(&b.1[q]);
}
out.push_str("─\n");
if q + 1 < n {
let mut line = blank.clone();
line.push(' ');
for b in &blocks[start..end] {
// Where a one-character label sits in this column.
let w = b.0;
let mid = 1 + (w - 3) / 2;
let mut cell = vec![' '; w];
if b.2[q] {
cell[mid] = '│';
}
line.extend(cell);
}
out.push_str(line.trim_end());
out.push('\n');
}
}
if end == start {
break;
}
start = end;
}
for (i, l) in legend.iter().enumerate() {
out.push_str(&format!("?{} {l}\n", i + 1));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::qasm::parse;
#[test]
fn angles_are_written_as_multiples_of_pi_where_they_are() {
assert_eq!(angle(PI / 4.0), "π/4");
assert_eq!(angle(-PI / 2.0), "-π/2");
assert_eq!(angle(3.0 * PI / 4.0), "3π/4");
assert_eq!(angle(2.0 * PI), "2π");
assert_eq!(angle(0.0), "0");
assert_eq!(angle(0.3), "0.3");
}
#[test]
fn a_bell_pair_is_h_then_a_controlled_x_joined_across_the_wires() {
let d = draw(&parse("OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];").unwrap(), 100);
let lines: Vec<&str> = d.lines().collect();
assert_eq!(lines.len(), 3, "{d}");
assert!(lines[0].starts_with("q0: ") && lines[0].contains('H') && lines[0].contains('●'), "{d}");
assert!(lines[1].contains('│'), "{d}");
assert!(lines[2].starts_with("q1: ") && lines[2].contains('⊕') && !lines[2].contains('H'), "{d}");
// The control sits after H, in the column of the target.
assert_eq!(lines[0].chars().position(|c| c == '●'), lines[2].chars().position(|c| c == '⊕'), "{d}");
}
#[test]
fn gates_on_different_wires_share_a_column_and_a_gate_between_crosses_the_middle() {
let d = draw(&parse("qubit[3] q; h q[0]; h q[1]; h q[2]; cx q[0], q[2];").unwrap(), 100);
let lines: Vec<&str> = d.lines().collect();
let h_at: Vec<Option<usize>> = [0, 2, 4].iter().map(|&i| lines[i].chars().position(|c| c == 'H')).collect();
assert!(h_at.iter().all(|&p| p == h_at[0]), "the three H share a column: {d}");
assert!(lines[2].contains('┼'), "the CX passes through q1: {d}");
}
#[test]
fn a_measurement_names_its_bit_and_a_conditioned_gate_its_condition() {
let d = draw(&parse("OPENQASM 3.0; qubit[2] q; bit[2] c; h q[0]; c[0] = measure q[0]; if (c[0]) x q[1]; c[1] = measure q[1];").unwrap(), 100);
assert!(d.contains("M0") && d.contains("M1"), "{d}");
assert!(d.contains("X?1") && d.contains("?1 if bit 0 = 1"), "{d}");
}
#[test]
fn a_long_circuit_wraps_and_keeps_every_gate() {
let src = format!("qubit[2] q; {}", "h q[0]; cx q[0], q[1]; ".repeat(40));
let d = draw(&parse(&src).unwrap(), 60);
assert!(d.lines().all(|l| l.chars().count() <= 60), "{d}");
assert_eq!(d.matches('H').count(), 40, "{d}");
assert_eq!(d.matches('⊕').count(), 40, "{d}");
}
}

View file

@ -19,9 +19,9 @@
use crate::qasm::{Instr, Op};
use std::collections::HashMap;
use wai_quantum::gates::Gate as Matrix;
use wai_quantum::quantum_sv::StateVector;
use wai_quantum::quantum_tableau::{Clifford, Tableau};
use crate::gates::Gate as Matrix;
use crate::quantum_sv::StateVector;
use crate::quantum_tableau::{Clifford, Tableau};
/// The most branches the exact expectation walk follows.
pub const MAX_LEAVES: u64 = 4096;

View file

@ -0,0 +1,112 @@
//! A job end to end, as every host gives it: planned, run, sealed with the
//! host's key, and returned in the service's form, the request, the plan
//! and the result with its receipt. The service, the command line, the
//! desktop app and a browser tab each call [`run`] with their own key and
//! their own word on energy, and [`verify`] checks any of their answers.
use super::{catalog, receipt};
use ed25519_dalek::{SigningKey, VerifyingKey};
use serde_json::{Value, json};
/// The job body for `request`: planned within `job_bytes`, run, with
/// `energy` (the host's figure, or why it has none) inside the result, and
/// the result sealed with `key`.
pub fn run(request: &Value, job_bytes: u64, key: &SigningKey, energy: Value) -> Result<Value, String> {
if let Some(at) = receipt::inexact_number(request) {
return Err(format!("`{at}`: a whole number here must be within ±9007199254740991 (2^53 − 1), so that every JSON reader reads it alike and the receipt binds it"));
}
let plan = catalog::plan(request, job_bytes)?;
let mut result = super::run::run(request, &plan)?;
result["energy"] = energy;
let seal = receipt::seal(key, request, &result);
result["receipt"] = seal;
Ok(json!({
"status": "done",
"method": plan.method,
"why": plan.why,
"estimate": { "memory_bytes": plan.memory_bytes, "work": plan.work },
"request": request,
"result": result,
}))
}
/// Whether a job body's result carries a receipt that seals that result and
/// its request, and the key the receipt names.
pub fn verify(job: &Value) -> Result<(bool, Value), String> {
let (Some(request), Some(result)) = (job.get("request"), job.get("result")) else {
return Err("expected a job: {\"request\": ..., \"result\": ...}, the result carrying its receipt".into());
};
let Some(seal) = result.get("receipt") else { return Err("the result carries no receipt".into()) };
let mut bare = result.clone();
if let Some(o) = bare.as_object_mut() {
o.remove("receipt");
}
Ok((receipt::verify(request, &bare, seal), seal.get("key").cloned().unwrap_or(Value::Null)))
}
/// The `did:key` of a key given as 64 hex digits, as a receipt names it.
pub fn did_of(key_hex: &str) -> Option<String> {
if key_hex.len() != 64 {
return None;
}
let mut b = [0u8; 32];
for (i, x) in b.iter_mut().enumerate() {
*x = u8::from_str_radix(key_hex.get(2 * i..2 * i + 2)?, 16).ok()?;
}
Some(crate::quantum_receipt::did_key(&VerifyingKey::from_bytes(&b).ok()?))
}
/// A program read as the engine reads it: `{"ok": true}` with its qubits,
/// gates, bits and measurements (and its steps, when it is dynamic), or
/// `{"ok": false}` with the problem and its line.
pub fn lint(src: &str) -> Value {
match crate::qasm::parse(src) {
Ok(p) => json!({
"ok": true,
"qubits": p.n_qubits,
"gates": p.ops.len(),
"bits": p.n_bits,
"measured": p.measured.len(),
"dynamic_steps": p.dynamic.as_ref().map(Vec::len),
}),
Err(e) => {
let line = e.strip_prefix("line ").and_then(|r| r.split(':').next()).and_then(|n| n.parse::<u64>().ok());
json!({ "ok": false, "error": e, "line": line })
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_job_is_the_services_form_and_its_receipt_holds_only_as_given() {
let req = json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 1000, "observables": ["Z0 Z1"] });
let key = SigningKey::from_bytes(&[3; 32]);
let body = run(&req, 1 << 30, &key, json!({ "unmetered": "a test" })).unwrap();
assert_eq!(body["method"], "stabilizer");
assert_eq!(body["result"]["expectations"][0]["value"], 1.0);
assert_eq!(body["result"]["energy"]["unmetered"], "a test");
assert!(body["result"]["counts"].as_object().unwrap().keys().all(|k| k == "00" || k == "11"));
let (ok, k) = verify(&body).unwrap();
assert!(ok);
assert_eq!(did_of(k.as_str().unwrap()).unwrap(), crate::quantum_receipt::did_key(&key.verifying_key()));
for (path, v) in [("/result/expectations/0/value", json!(0.5)), ("/request/shots", json!(999)), ("/result/energy/unmetered", json!("other"))] {
let mut forged = body.clone();
*forged.pointer_mut(path).unwrap() = v;
assert!(!verify(&forged).unwrap().0, "{path}");
}
let again: Value = serde_json::from_str(&body.to_string()).unwrap();
assert!(verify(&again).unwrap().0);
assert!(verify(&json!({ "request": {} })).is_err());
}
#[test]
fn lint_gives_the_shape_or_the_line_of_the_problem() {
let ok = lint("OPENQASM 3.0; qubit[3] q; bit[3] c; h q; c = measure q;");
assert!(ok["ok"] == true && ok["qubits"] == 3 && ok["gates"] == 3 && ok["dynamic_steps"].is_null(), "{ok}");
let bad = lint("OPENQASM 3.0;\nqubit[1] q;\nfoo q[0];\n");
assert!(bad["ok"] == false && bad["line"] == 3, "{bad}");
}
}

View file

@ -0,0 +1,33 @@
//! The job service's engine: a program and what is asked of it, planned onto a
//! method, run, and answered as JSON. The service, the command line and the
//! desktop app plan and run a job the same way, so the same job gives the same
//! result on each.
//!
//! - [`catalog`]: a job read and checked ([`catalog::Request`]), the methods
//! and their limits, and [`catalog::plan`], which picks a method for a job
//! and says why, with the memory and work it will take.
//! - [`run`]: [`run::run`] carries a plan out and gives the result: counts,
//! expectation values, amplitudes, the accuracy class and what the method
//! did.
//! - [`dynamic`]: programs that measure in the middle, reset or branch on a
//! measured bit, run branch by branch.
//! - [`receipt`] (with `quantum_receipt`): a result sealed with a key over
//! the request and the result in their canonical JSON, and checked. The
//! service seals with its key and the command line with the user's, in one
//! form.
//! - [`job`] (with `quantum_receipt`): a job end to end, planned, run and
//! sealed, in the service's form; its check; and a program's lint.
//! - [`draw`]: a program's circuit as text.
//!
//! Requests and results are the service's JSON (`/v1/openapi.json` there).
//! Keys, metering a job's energy, queueing and limits per client stay with
//! whatever hosts the engine.
pub mod catalog;
pub mod draw;
pub mod dynamic;
#[cfg(feature = "quantum_receipt")]
pub mod job;
#[cfg(feature = "quantum_receipt")]
pub mod receipt;
pub mod run;

View file

@ -0,0 +1,397 @@
//! Signed receipts over a job: a result sealed with an Ed25519 key, binding
//! the request as submitted and the result as returned, so anyone can be shown
//! what was run and what came back. The service seals every result with its
//! key; the command line and the desktop app seal theirs with the user's, in
//! the same form, and one [`verify`] checks them all.
//!
//! What is signed is the JSON Canonicalization Scheme (RFC 8785) text of each:
//! keys sorted, no whitespace, and every number written as ECMAScript writes
//! the double it reads. A result therefore verifies after any JSON reader and
//! writer has carried it: a browser's `JSON.parse` and `JSON.stringify` turn
//! `1.0` into `1`, and other readers reorder keys.
use serde_json::{Value, json};
use ed25519_dalek::{Signer as _, SigningKey, Verifier as _, VerifyingKey};
/// The first receipts signed serde_json's own text, which only a reader that
/// keeps each number's spelling and each object's key order can reproduce.
/// They still verify; new ones are canonical.
const DOMAIN_V1: &[u8] = b"wai:quantum-service-receipt\x01";
const DOMAIN: &[u8] = b"wai:quantum-service-receipt\x02";
/// The receipt's name for its canonical form.
const CANON: &str = "rfc8785";
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
fn unhex(s: &str) -> Option<Vec<u8>> {
s.len().is_multiple_of(2).then_some(())?;
(0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok()).collect()
}
/// A length-prefixed byte string, so that no field reads as part of another.
fn put(o: &mut Vec<u8>, bytes: &[u8]) {
o.extend_from_slice(&(bytes.len() as u64).to_be_bytes());
o.extend_from_slice(bytes);
}
/// The digest a receipt signs: BLAKE3(domain ‖ put(request) ‖ put(result)),
/// each in its RFC 8785 canonical text.
pub fn digest(request: &Value, result: &Value) -> [u8; 32] {
let mut o = DOMAIN.to_vec();
put(&mut o, &canonical(request));
put(&mut o, &canonical(result));
*blake3::hash(&o).as_bytes()
}
/// The digest the first receipts signed (see [`DOMAIN_V1`]).
fn digest_v1(request: &Value, result: &Value) -> [u8; 32] {
let mut o = DOMAIN_V1.to_vec();
put(&mut o, request.to_string().as_bytes());
put(&mut o, result.to_string().as_bytes());
*blake3::hash(&o).as_bytes()
}
/// The RFC 8785 canonical text of `v`.
pub fn canonical(v: &Value) -> Vec<u8> {
let mut o = Vec::new();
write(v, &mut o);
o
}
fn write(v: &Value, o: &mut Vec<u8>) {
match v {
Value::Null => o.extend_from_slice(b"null"),
Value::Bool(b) => o.extend_from_slice(if *b { b"true" } else { b"false" }),
// Every number as the double a JSON reader in ECMAScript holds.
Value::Number(n) => o.extend_from_slice(es_number(n.as_f64().unwrap_or(f64::NAN)).as_bytes()),
Value::String(s) => write_str(s, o),
Value::Array(a) => {
o.push(b'[');
for (i, x) in a.iter().enumerate() {
if i > 0 {
o.push(b',');
}
write(x, o);
}
o.push(b']');
}
Value::Object(m) => {
// Keys in the order of their UTF-16 code units.
let mut keys: Vec<&String> = m.keys().collect();
keys.sort_by(|a, b| a.encode_utf16().cmp(b.encode_utf16()));
o.push(b'{');
for (i, k) in keys.into_iter().enumerate() {
if i > 0 {
o.push(b',');
}
write_str(k, o);
o.push(b':');
write(&m[k], o);
}
o.push(b'}');
}
}
}
/// A string as RFC 8785 writes it: `"` and `\\` escaped, the five short
/// control escapes, every other control character as `\u00xx`, and the rest
/// as it is.
fn write_str(s: &str, o: &mut Vec<u8>) {
o.push(b'"');
for c in s.chars() {
match c {
'"' => o.extend_from_slice(b"\\\""),
'\\' => o.extend_from_slice(b"\\\\"),
'\u{8}' => o.extend_from_slice(b"\\b"),
'\t' => o.extend_from_slice(b"\\t"),
'\n' => o.extend_from_slice(b"\\n"),
'\u{c}' => o.extend_from_slice(b"\\f"),
'\r' => o.extend_from_slice(b"\\r"),
c if (c as u32) < 0x20 => o.extend_from_slice(format!("\\u{:04x}", c as u32).as_bytes()),
c => {
let mut b = [0u8; 4];
o.extend_from_slice(c.encode_utf8(&mut b).as_bytes());
}
}
}
o.push(b'"');
}
/// A double as ECMAScript's Number::toString writes it (ECMA-262, 6.1.6.1.20):
/// the shortest digits that read back as `x`, laid out by where the decimal
/// point falls. JSON holds no infinity or NaN; they write as `null`.
pub fn es_number(x: f64) -> String {
if !x.is_finite() {
return "null".into();
}
if x == 0.0 {
return "0".into();
}
let (s, n) = shortest(x.abs());
let digits: String = s.iter().map(|d| char::from(b'0' + d)).collect();
let k = s.len() as i32;
let mut out = String::new();
if x < 0.0 {
out.push('-');
}
if k <= n && n <= 21 {
out.push_str(&digits);
out.extend(std::iter::repeat_n('0', (n - k) as usize));
} else if 0 < n && n <= 21 {
out.push_str(&digits[..n as usize]);
out.push('.');
out.push_str(&digits[n as usize..]);
} else if -6 < n && n <= 0 {
out.push_str("0.");
out.extend(std::iter::repeat_n('0', (-n) as usize));
out.push_str(&digits);
} else {
out.push_str(&digits[..1]);
if k > 1 {
out.push('.');
out.push_str(&digits[1..]);
}
out.push('e');
out.push(if n - 1 < 0 { '-' } else { '+' });
out.push_str(&(n - 1).abs().to_string());
}
out
}
/// The digits s and the exponent n, x = 0.s × 10^n, that ECMAScript chooses:
/// the fewest that read back as `x` (> 0), and of those the nearest to `x`,
/// a tie going to the even.
fn shortest(x: f64) -> (Vec<u8>, i32) {
let e = format!("{x:e}");
let (mant, exp) = e.split_once('e').unwrap_or((&e, "0"));
let mut s: Vec<u8> = mant.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
while s.len() > 1 && s.last() == Some(&0) {
s.pop();
}
let n = exp.parse::<i32>().unwrap_or(0) + 1;
// Rust's digits are the fewest, but a tie between two equally near it
// rounds up. Only when another of the same length also reads back as x
// (near 17 digits) is there a choice: then take x's exact expansion (a
// double's has at most 767 significant digits) rounded to that length.
if ![false, true].into_iter().any(|up| step(&s, up).is_some_and(|t| reads_as(&t, n, x))) {
return (s, n);
}
let k = s.len();
let exact = format!("{x:.800e}");
let (em, ee) = exact.split_once('e').unwrap_or((&exact, "0"));
let all: Vec<u8> = em.bytes().filter(u8::is_ascii_digit).map(|b| b - b'0').collect();
let mut cn = ee.parse::<i32>().unwrap_or(0) + 1;
let mut c = all[..k].to_vec();
let rest = &all[k..];
let up = match rest.first().copied().unwrap_or(0) {
d if d > 5 => true,
5 => rest[1..].iter().any(|&d| d != 0) || c[k - 1] % 2 == 1,
_ => false,
};
if up {
match step(&c, true) {
Some(t) => c = t,
None => {
// 99…9 rounds to 10…0.
c = std::iter::once(1).chain(std::iter::repeat_n(0, k - 1)).collect();
cn += 1;
}
}
}
if reads_as(&c, cn, x) { (c, cn) } else { (s, n) }
}
/// The k-digit decimal one unit above or below `s` in its last digit, if it
/// still has k digits.
fn step(s: &[u8], up: bool) -> Option<Vec<u8>> {
let mut t = s.to_vec();
for i in (0..t.len()).rev() {
match (up, t[i]) {
(true, 9) => t[i] = 0,
(false, 0) => t[i] = 9,
(true, d) | (false, d) => {
t[i] = if up { d + 1 } else { d - 1 };
return (t[0] != 0).then_some(t);
}
}
}
None
}
/// Whether 0.s × 10^n reads as `x` (std's reader is correctly rounded).
fn reads_as(s: &[u8], n: i32, x: f64) -> bool {
let digits: String = s.iter().map(|d| char::from(b'0' + d)).collect();
format!("0.{digits}e{n}").parse::<f64>().is_ok_and(|y| y == x)
}
/// The first number in `v` that a double cannot hold exactly (an integer past
/// ±2^53), by its path. Such a number would be read differently by readers that
/// hold every number as a double, so the receipt could not bind it.
pub fn inexact_number(v: &Value) -> Option<String> {
const SAFE: u64 = (1 << 53) - 1;
fn walk(v: &Value, path: &mut String) -> bool {
match v {
Value::Number(n) => n.as_u64().is_some_and(|u| u > SAFE) || n.as_i64().is_some_and(|i| i.unsigned_abs() > SAFE),
Value::Array(a) => a.iter().enumerate().any(|(i, x)| {
let at = path.len();
path.push_str(&format!("[{i}]"));
let hit = walk(x, path);
if !hit {
path.truncate(at);
}
hit
}),
Value::Object(m) => m.iter().any(|(k, x)| {
let at = path.len();
path.push('.');
path.push_str(k);
let hit = walk(x, path);
if !hit {
path.truncate(at);
}
hit
}),
_ => false,
}
}
let mut path = String::new();
walk(v, &mut path).then(|| path.trim_start_matches('.').to_string())
}
/// The receipt for `result` of `request`, sealed with `key`.
pub fn seal(key: &SigningKey, request: &Value, result: &Value) -> Value {
let d = digest(request, result);
let sig = key.sign(&d);
json!({ "alg": "ed25519", "canon": CANON, "key": hex(key.verifying_key().as_bytes()), "digest": hex(&d), "signature": hex(&sig.to_bytes()) })
}
/// Whether `receipt` is a valid seal of `result` of `request` under the key
/// it names.
pub fn verify(request: &Value, result: &Value, receipt: &Value) -> bool {
let field = |k: &str| receipt.get(k).and_then(Value::as_str).and_then(unhex);
let (Some(key), Some(sig)) = (field("key"), field("signature")) else { return false };
let (Ok(key), Ok(sig)) = (<[u8; 32]>::try_from(key.as_slice()), <[u8; 64]>::try_from(sig.as_slice())) else { return false };
let Ok(vk) = VerifyingKey::from_bytes(&key) else { return false };
let d = match receipt.get("canon").and_then(Value::as_str) {
Some(CANON) => digest(request, result),
None => digest_v1(request, result),
Some(_) => return false,
};
field("digest").is_some_and(|g| g == d) && vk.verify(&d, &ed25519_dalek::Signature::from_bytes(&sig)).is_ok()
}
#[cfg(test)]
mod tests {
use super::*;
/// A key for the tests, sealing as a host does.
struct Signer(SigningKey);
impl Signer {
fn from_seed(seed: [u8; 32]) -> Signer {
Signer(SigningKey::from_bytes(&seed))
}
fn public_key(&self) -> String {
hex(self.0.verifying_key().as_bytes())
}
fn seal(&self, request: &Value, result: &Value) -> Value {
seal(&self.0, request, result)
}
}
#[test]
fn a_receipt_verifies_and_any_change_breaks_it() {
let s = Signer::from_seed([7; 32]);
let req = json!({"qasm": "OPENQASM 3.0;", "shots": 10});
let res = json!({"method": "statevector", "counts": {"0": 10}});
let r = s.seal(&req, &res);
assert!(verify(&req, &res, &r));
assert!(!verify(&req, &json!({"method": "statevector", "counts": {"0": 9}}), &r));
assert!(!verify(&json!({"qasm": "OPENQASM 3.0;", "shots": 11}), &res, &r));
let mut forged = r.clone();
forged["key"] = json!(Signer::from_seed([8; 32]).public_key());
assert!(!verify(&req, &res, &forged));
}
#[test]
fn numbers_are_written_as_ecmascript_writes_them() {
// Bit patterns and ECMAScript's Number::toString of each, written by a
// JavaScript engine: RFC 8785's appendix B, the edges of each layout,
// and patterns spread over every exponent.
let mut n = 0;
for line in include_str!("testdata/es-number-tostring.txt").lines() {
let (bits, want) = line.split_once(' ').unwrap();
let x = f64::from_bits(u64::from_str_radix(bits, 16).unwrap());
assert_eq!(es_number(x), want, "{bits}");
// And the text reads back as the same double, as it must for a
// result to verify after a round trip (serde_json's
// float_roundtrip: its default reader rounds twice).
if x != 0.0 {
assert_eq!(serde_json::from_str::<f64>(want).unwrap().to_bits(), x.to_bits(), "{bits} {want}");
}
n += 1;
}
assert!(n > 3000);
}
/// The same against a larger set, in the same format, written by a
/// JavaScript engine: `WAI_ES_VECTORS=path cargo test -- --ignored`.
#[test]
#[ignore]
fn numbers_match_a_javascript_engine_on_a_large_set() {
let path = std::env::var("WAI_ES_VECTORS").expect("WAI_ES_VECTORS: a file of `hexbits text` lines");
let text = std::fs::read_to_string(path).unwrap();
let mut n = 0u64;
for line in text.lines() {
let (bits, want) = line.split_once(' ').unwrap();
assert_eq!(es_number(f64::from_bits(u64::from_str_radix(bits, 16).unwrap())), want, "{bits}");
n += 1;
}
eprintln!("{n} numbers agree");
}
#[test]
fn the_canonical_text_sorts_keys_and_writes_numbers_and_strings_one_way() {
let v = json!({ "b": 1.0, "a": [true, null, "x\n\u{1}\"\\é"], "\u{e000}": 0.000001, "\u{1f600}": -0.0, "c": 1e21 });
// U+1F600 is a surrogate pair, so it sorts before U+E000.
assert_eq!(String::from_utf8(canonical(&v)).unwrap(), "{\"a\":[true,null,\"x\\n\\u0001\\\"\\\\é\"],\"b\":1,\"c\":1e+21,\"\u{1f600}\":0,\"\u{e000}\":0.000001}");
}
#[test]
fn a_receipt_verifies_after_a_round_trip_through_another_json_writer() {
let s = Signer::from_seed([7; 32]);
let req = json!({ "qasm": "qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 1000, "observables": ["Z0 Z1"] });
let res = json!({ "method": "stabilizer", "expectations": [{ "observable": "Z0 Z1", "coeff": 1.0, "value": 1.0 }], "threshold": 0.00001, "elapsed_ms": 0 });
let r = s.seal(&req, &res);
// What a browser sends back: integral doubles without a fraction, keys
// in another order, the threshold in plain decimals.
let req2: Value = serde_json::from_str(r#"{"observables":["Z0 Z1"],"shots":1000,"qasm":"qubit[2] q; h q[0]; cx q[0], q[1];"}"#).unwrap();
let res2: Value = serde_json::from_str(r#"{"elapsed_ms":0,"threshold":0.00001,"method":"stabilizer","expectations":[{"value":1,"coeff":1,"observable":"Z0 Z1"}]}"#).unwrap();
assert!(verify(&req2, &res2, &r));
let res3: Value = serde_json::from_str(r#"{"elapsed_ms":0,"threshold":0.00001,"method":"stabilizer","expectations":[{"value":0.999,"coeff":1,"observable":"Z0 Z1"}]}"#).unwrap();
assert!(!verify(&req2, &res3, &r));
}
#[test]
fn the_first_receipts_still_verify_and_an_unknown_form_does_not() {
let s = Signer::from_seed([7; 32]);
let req = json!({ "qasm": "OPENQASM 3.0;", "shots": 10 });
let res = json!({ "method": "statevector", "counts": { "0": 10 } });
let d = digest_v1(&req, &res);
let old = json!({ "alg": "ed25519", "key": s.public_key(), "digest": hex(&d), "signature": hex(&s.0.sign(&d).to_bytes()) });
assert!(verify(&req, &res, &old));
let mut other = s.seal(&req, &res);
other["canon"] = json!("something-else");
assert!(!verify(&req, &res, &other));
}
#[test]
fn integers_a_double_cannot_hold_are_found_by_path() {
assert_eq!(inexact_number(&json!({ "seed": 9007199254740991u64, "shots": 10 })), None);
assert_eq!(inexact_number(&json!({ "seed": 9007199254740992u64 })).as_deref(), Some("seed"));
assert_eq!(inexact_number(&json!({ "options": { "x": [1, -9007199254740993i64] } })).as_deref(), Some("options.x[1]"));
assert_eq!(inexact_number(&json!({ "noise": { "readout": 1e300 } })), None);
}
}

View file

@ -2,10 +2,9 @@
//! terms. Bit strings are the program's classical bits, bit `n_bits − 1`
//! first; an amplitude's bit string names every qubit, qubit `n − 1` first.
use crate::catalog::{self, MAX_OUTCOMES, Plan, Request};
use crate::engine::catalog::{self, MAX_OUTCOMES, Plan, Request};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::time::Instant;
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
@ -47,7 +46,7 @@ fn index_of(bits: &str, n: u32, pos: impl Fn(u32) -> u32) -> usize {
/// `⟨ψ|P|ψ⟩` on the bit-exact state, exactly: the fixed-point amplitudes
/// multiplied and summed in 128-bit integers, then scaled once.
fn expect_fixed(amps: &[wai_quantum::quantum::Amp], string: &[(u32, char)]) -> f64 {
fn expect_fixed(amps: &[crate::quantum::Amp], string: &[(u32, char)]) -> f64 {
let (mut flip, mut zmask, mut ys) = (0usize, 0usize, 0u32);
for &(q, p) in string {
let bit = 1usize << q;
@ -85,10 +84,35 @@ fn expectation_entries(r: &Request, value: impl Fn(&[(u32, char)]) -> f64) -> Va
Value::Array(r.observables.iter().map(|(text, coeff, string)| json!({ "observable": text, "coeff": coeff, "value": coeff * value(string) })).collect())
}
/// When a run started, on a target with a clock std can read. A browser tab
/// (wasm32-unknown-unknown) has none, so a result there carries no
/// `elapsed_ms`; its host can time the call itself.
#[derive(Clone, Copy)]
struct Started(Option<std::time::Instant>);
impl Started {
fn now() -> Started {
#[cfg(all(target_arch = "wasm32", target_os = "unknown"))]
{
Started(None)
}
#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))]
{
Started(Some(std::time::Instant::now()))
}
}
fn stamp(self, out: &mut Value) {
if let Some(t) = self.0 {
out["elapsed_ms"] = json!(t.elapsed().as_millis() as u64);
}
}
}
/// Runs `plan` for the job `request`: the result, or why it failed.
pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
let r = catalog::read(request)?;
let started = Instant::now();
let started = Started::now();
let n = r.program.n_qubits;
let mut out = json!({
"method": plan.method,
@ -97,7 +121,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
});
if let Some(steps) = &r.program.dynamic {
run_dynamic(&r, plan, steps, &mut out)?;
out["elapsed_ms"] = json!(started.elapsed().as_millis() as u64);
started.stamp(&mut out);
return Ok(out);
}
match plan.method.as_str() {
@ -132,8 +156,11 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
);
}
}
#[cfg(not(feature = "quantum_sv_metal"))]
"statevector_single" => return Err("this build has no GPU state vector".into()),
#[cfg(feature = "quantum_sv_metal")]
"statevector_single" => {
use wai_quantum::quantum_sv_metal::GpuState;
use crate::quantum_sv_metal::GpuState;
let gates = r.program.matrices();
// Wider fused gates run slower on the GPU than the passes they save.
let width = r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 3) as usize;
@ -165,7 +192,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
"statevector" => {
let gates = r.program.matrices();
let width = r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 5) as usize;
let mut sv = wai_quantum::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
let mut sv = crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
sv.run(&gates, width).map_err(|e| format!("{e:?}"))?;
out["accuracy"] = json!({ "class": "exact", "fusion": width });
if r.shots > 0 {
@ -191,7 +218,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
}
}
"stabilizer" if r.noise.is_some() => {
use wai_quantum::quantum_tableau::{FrameStep, frame_counts, frame_expectations};
use crate::quantum_tableau::{FrameStep, frame_counts, frame_expectations};
let noise = r.noise.ok_or("no noise model")?;
// Each op's gates, then its noise on the qubits it touched, as the
// other noisy methods apply it.
@ -245,7 +272,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
}
}
"stabilizer" => {
use wai_quantum::quantum_tableau::Tableau;
use crate::quantum_tableau::Tableau;
let t = Tableau::run(n, &r.program.clifford()?).map_err(|e| e.to_string())?;
out["accuracy"] = json!({ "class": "exact" });
if r.shots > 0 {
@ -276,7 +303,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
}
}
"pauli" => {
use wai_quantum::quantum_spd::{Pauli, SpdConfig, expectation_capped};
use crate::quantum_spd::{Pauli, SpdConfig, expectation_capped};
let rc = r.program.rotations();
// As many terms as the job's memory holds: a term's string (two
// words of bits a 64 qubits, padded to a power of two), its
@ -323,7 +350,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
out["expectations"] = Value::Array(entries);
}
"mps" => {
use wai_quantum::quantum_mpsim::{Chain, MpsConfig};
use crate::quantum_mpsim::{Chain, MpsConfig};
let chi = catalog::mps_bond(&r);
let cutoff = r.options.get("cutoff").and_then(Value::as_f64).unwrap_or(1e-12).clamp(0.0, 1e-2);
let mut chain = Chain::run(n, &r.program.local_matrices(), MpsConfig { max_bond: chi, cutoff }).map_err(|e| e.to_string())?;
@ -362,7 +389,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
}
}
"cutrank" => {
use wai_quantum::quantum_cutrank::Precision;
use crate::quantum_cutrank::Precision;
// An order at least as cheap as the planner's (searched longer, from
// several seeds), and the precision the admitted memory allows.
let pm = catalog::product_measured(&r, true)?;
@ -403,7 +430,7 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
}
}
"tensor_network" => {
use wai_quantum::quantum_tn::{Network, PathOptions, contract_plan, find_path};
use crate::quantum_tn::{Network, PathOptions, contract_plan, find_path};
let gates = r.program.matrices();
out["accuracy"] = json!({ "class": "exact" });
let mut entries = Vec::new();
@ -417,14 +444,14 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
let plan = find_path(&net.shape(), &net.dims, &net.open, &PathOptions::default());
flops += plan.total_flops();
let t = contract_plan(&net, &plan).map_err(|e| format!("{e:?}"))?;
let a = t.data.first().copied().unwrap_or(wai_quantum::gates::complex(0.0, 0.0));
let a = t.data.first().copied().unwrap_or(crate::gates::complex(0.0, 0.0));
entries.push(json!({ "bits": b, "re": a.re, "im": a.im }));
}
out["amplitudes"] = Value::Array(entries);
out["contraction_flops"] = json!(flops);
}
"density_matrix" => {
use wai_quantum::quantum_noisy::DensityMatrix;
use crate::quantum_noisy::DensityMatrix;
let noise = r.noise.ok_or("no noise model")?;
let rho = DensityMatrix::evolve(&r.program.matrices(), n, &noise).map_err(|e| format!("{e:?}"))?;
out["accuracy"] = json!({ "class": "exact", "note": "the noisy state itself, under the model; counts are seeded samples of its distribution" });
@ -441,13 +468,13 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
}
}
"trajectories" => {
use wai_quantum::quantum_noisy::branching_trajectories;
use crate::quantum_noisy::branching_trajectories;
let noise = r.noise.ok_or("no noise model")?;
let count = catalog::trajectory_count(&r);
// The plan reserved memory for this many states.
let states = ((plan.memory_bytes.saturating_sub(256 << 20)) / (catalog::amplitude_bytes(n) * if n < 16 { 8 } else { 1 }).max(1)).clamp(1, 16) as usize;
let threads = std::thread::available_parallelism().map_or(1, |t| t.get()).min(8);
let observables: Vec<wai_quantum::quantum_noisy::Observable> = r.observables.iter().map(|(_, c, s)| vec![(*c, s.clone())]).collect();
let observables: Vec<crate::quantum_noisy::Observable> = r.observables.iter().map(|(_, c, s)| vec![(*c, s.clone())]).collect();
let run = branching_trajectories(&r.program.matrices(), n, &noise, &observables, count, r.seed, states, threads).map_err(|e| format!("{e:?}"))?;
out["accuracy"] = json!({
"class": "estimate",
@ -476,29 +503,30 @@ pub fn run(request: &Value, plan: &Plan) -> Result<Value, String> {
}
other => return Err(format!("unknown method {other}")),
}
out["elapsed_ms"] = json!(started.elapsed().as_millis() as u64);
started.stamp(&mut out);
Ok(out)
}
/// A program that measures in the middle, resets, or acts on measured bits
/// ([`crate::dynamic`]): counts by shot splitting, expectation values as the
/// ([`crate::engine::dynamic`]): counts by shot splitting, expectation values as the
/// exact mixture over its branches.
fn run_dynamic(r: &catalog::Request, plan: &Plan, steps: &[crate::qasm::Instr], out: &mut Value) -> Result<(), String> {
use crate::dynamic::{self, Dense, Stabilizer};
use crate::engine::dynamic::{self, Dense, Stabilizer};
let (n, nb) = (r.program.n_qubits, r.program.n_bits);
let strings: Vec<Vec<(u32, char)>> = r.observables.iter().map(|(_, _, s)| s.clone()).collect();
let (sampled, exact) = match plan.method.as_str() {
"statevector" => {
let gates = r.program.matrices();
let states = (plan.memory_bytes / catalog::statevector_bytes(n)).max(1) as usize;
let start = Dense { sv: wai_quantum::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?, gates: &gates };
// The states the plan admitted: its memory less the job's margin and the counts.
let states = (plan.memory_bytes.saturating_sub((256 << 20) + r.shots * 24) / catalog::amplitude_bytes(n)).max(1) as usize;
let start = Dense { sv: crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?, gates: &gates };
let s = if r.shots > 0 { Some(dynamic::sample(steps, nb, start.clone(), r.shots, r.seed, states)?) } else { None };
let e = if strings.is_empty() { None } else { Some(dynamic::expectations(steps, &r.program.ops, nb, start, &strings, states)?) };
(s, e)
}
"stabilizer" => {
let ops: Vec<Vec<wai_quantum::quantum_tableau::Clifford>> = r.program.clifford_ops()?.into_iter().map(|(g, _)| g).collect();
let start = Stabilizer { t: wai_quantum::quantum_tableau::Tableau::new(n).map_err(|e| e.to_string())?, ops: &ops };
let ops: Vec<Vec<crate::quantum_tableau::Clifford>> = r.program.clifford_ops()?.into_iter().map(|(g, _)| g).collect();
let start = Stabilizer { t: crate::quantum_tableau::Tableau::new(n).map_err(|e| e.to_string())?, ops: &ops };
let s = if r.shots > 0 { Some(dynamic::sample(steps, nb, start.clone(), r.shots, r.seed, 64)?) } else { None };
let e = if strings.is_empty() { None } else { Some(dynamic::expectations(steps, &r.program.ops, nb, start, &strings, 64)?) };
(s, e)
@ -567,7 +595,7 @@ fn sample_distribution(p: &[f64], shots: u64, seed: u64) -> Vec<u64> {
#[cfg(test)]
mod tests {
use super::*;
use crate::catalog::plan;
use crate::engine::catalog::plan;
const CAP: u64 = 8 << 30;
@ -577,12 +605,13 @@ mod tests {
}
#[test]
#[cfg(feature = "quantum_toolchain")]
fn grover_as_the_ide_exports_it_finds_the_marked_item() {
// Its oracle and its diffusion are each a Z under four controls,
// written `ctrl(4) @ z`. Four rounds on five qubits find the marked
// item with probability 0.9992.
let marked = 0b10110u32;
let qasm = wai_quantum::quantum_toolchain::to_qasm(&wai_quantum::quantum_toolchain::grover(5, marked));
let qasm = crate::quantum_toolchain::to_qasm(&crate::quantum_toolchain::grover(5, marked));
assert!(qasm.contains("ctrl(4) @ z"), "{qasm}");
let key: String = (0..5).rev().map(|q| if marked >> q & 1 == 1 { '1' } else { '0' }).collect();
for method in ["exact", "statevector"] {
@ -707,7 +736,7 @@ mod tests {
fn the_gpu_state_vector_agrees_where_there_is_one_and_is_refused_where_not() {
let qasm = "qubit[14] q; bit[14] c; h q; rx(0.3) q[0]; cx q[0], q[13]; crz(0.7) q[3], q[9]; ry(1.1) q[5]; c = measure q;";
let job = json!({ "qasm": qasm, "method": "statevector_single", "shots": 2000, "observables": ["Z0 Z13", "X5"], "amplitudes": ["00000000000000"] });
match crate::catalog::gpu_max_qubits() {
match crate::engine::catalog::gpu_max_qubits() {
Ok(_) => {
let g = go(job);
let d = go(json!({ "qasm": qasm, "method": "statevector", "observables": ["Z0 Z13", "X5"], "amplitudes": ["00000000000000"] }));
@ -908,25 +937,6 @@ mod tests {
}
}
/// The two worked examples that measure in the middle do what their
/// `shows` says.
#[test]
fn the_measuring_examples_show_what_they_say() {
let ex = crate::discovery::examples();
let job = |name: &str| ex.as_array().unwrap().iter().find(|e| e["name"] == name).unwrap()["job"].clone();
let t = go(job("teleport"));
assert_eq!(t["method"], "statevector", "{t}");
assert!((t["expectations"][0]["value"].as_f64().unwrap() - 0.7f64.cos()).abs() < 1e-12, "{t}");
assert!((t["expectations"][1]["value"].as_f64().unwrap() - 0.7f64.sin()).abs() < 1e-12, "{t}");
assert_eq!(t["dynamic"]["branches_weighed"], 4, "{t}");
let b = go(job("bit-flip-code"));
assert_eq!(b["method"], "stabilizer", "{b}");
assert_eq!(b["expectations"][0]["value"], 1.0, "{b}");
let counts = b["counts"].as_object().unwrap();
assert!(counts.keys().all(|k| k.ends_with("11")), "the syndrome names the middle qubit on every shot: {b}");
assert_eq!(counts.values().map(|v| v.as_u64().unwrap()).sum::<u64>(), 1000);
}
/// A Clifford circuit on `n` qubits (Hadamards, phases, controlled-Z and
/// CX between pseudo-random pairs), then one layer of one-qubit gates at
/// generic angles on every qubit.

View file

@ -47,6 +47,15 @@ mod linalg;
#[cfg(any(feature = "quantum_tn", feature = "quantum_sv"))]
pub mod gates;
// OpenQASM 3 (and the OpenQASM 2 forms in common use): one front end for every
// surface, lowered to each method's input.
#[cfg(feature = "quantum_qasm")]
pub mod qasm;
// The job service's engine: plan, run and answer a job, the same on every host.
#[cfg(feature = "quantum_engine")]
pub mod engine;
#[cfg(any(
feature = "quantum_spd",
feature = "quantum_pauli",

View file

@ -16,7 +16,7 @@
//! OpenQASM 2's `if(c==n) gate;`. A program that measures a qubit and then
//! acts on it, resets one after its first gate, or uses `if` is dynamic: it
//! is kept as steps ([`Instr`]) as well as gates, and runs by
//! [`crate::dynamic`]. Every other program is exactly what it was before.
//! the service's `dynamic` module. Every other program is exactly what it was before.
//!
//! Control modifiers, `ctrl @`, `ctrl(n) @`, `negctrl @` and `negctrl(n) @`,
//! on `x y z h s sdg t tdg p` and on the controlled names `cx cy cz ch cp ccx
@ -35,10 +35,10 @@
//! lowering to the others.
use std::collections::HashMap;
use wai_quantum::gates::{C, Gate as Matrix, complex};
use wai_quantum::quantum::{BaseGate, Circuit as Dyadic, Gate as DyadicGate};
use wai_quantum::quantum_spd::{Op as Rot, Pauli, RotCircuit};
use wai_quantum::quantum_tableau::Clifford;
use crate::gates::{C, Gate as Matrix, complex};
use crate::quantum::{BaseGate, Circuit as Dyadic, Gate as DyadicGate};
use crate::quantum_spd::{Op as Rot, Pauli, RotCircuit};
use crate::quantum_tableau::Clifford;
/// The most qubits a program declares.
pub const MAX_QUBITS: u32 = 65_536;
@ -1119,6 +1119,20 @@ impl Parser {
}
/// Parses an OpenQASM program.
/// A program whose measurements all come at the end, as the dyadic circuit:
/// the reader of the bit-exact state vector, the in-browser IDE and the CLI.
/// Every register, broadcast, gate definition and modifier [`parse`] reads is
/// read the same way here. A program that measures a qubit and then acts on
/// it, resets a qubit after its first gate, or acts on measured bits is
/// refused, never rewritten: its meaning needs the measurements.
pub fn dyadic_circuit(src: &str) -> Result<Dyadic, String> {
let p = parse(src)?;
if p.dynamic.is_some() {
return Err("this program measures a qubit and then acts on it, resets a qubit after its first gate, or acts on measured bits; the exact state vector here takes programs whose measurements all come at the end".into());
}
p.dyadic()
}
pub fn parse(src: &str) -> Result<Program, String> {
let toks = lex(src)?;
let mut p = Parser {
@ -1292,7 +1306,7 @@ fn lowering_phase(op: &Op, gates: &[Clifford]) -> C {
let want = op.matrix().matrix;
let mut best = (0.0f64, complex(1.0, 0.0));
for col in 0..d {
let mut sv = wai_quantum::quantum_sv::StateVector::zero(k).expect("at most three qubits");
let mut sv = crate::quantum_sv::StateVector::zero(k).expect("at most three qubits");
sv.re[0] = 0.0;
sv.re[col] = 1.0;
for g in &local_gates {
@ -2119,8 +2133,8 @@ mod tests {
Program { n_qubits: n, ops, measured: (0..n).map(|q| (q, q)).collect(), n_bits: n, dynamic: None }
}
fn dense(p: &Program) -> wai_quantum::quantum_sv::StateVector {
let mut sv = wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
fn dense(p: &Program) -> crate::quantum_sv::StateVector {
let mut sv = crate::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
sv.run(&p.matrices(), 1).unwrap();
sv
}
@ -2276,7 +2290,7 @@ mod tests {
#[test]
fn the_clifford_lowering_gives_the_matrices_expectation_values() {
use wai_quantum::quantum_tableau::Tableau;
use crate::quantum_tableau::Tableau;
for seed in 0..12 {
let p = clifford_kinds(5, seed);
let sv = dense(&p);
@ -2304,7 +2318,7 @@ mod tests {
for seed in 0..6 {
let p = every_kind(4, seed + 300);
let want = dense(&p);
let mut got = wai_quantum::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
let mut got = crate::quantum_sv::StateVector::zero(p.n_qubits).unwrap();
let local = p.local_matrices();
assert!(local.iter().all(|g| g.qubits.len() <= 2));
got.run(&local, 1).unwrap();
@ -2319,7 +2333,7 @@ mod tests {
#[test]
fn the_rotation_lowering_gives_the_matrices_expectation_values() {
use wai_quantum::quantum_spd::{SpdConfig, expectation};
use crate::quantum_spd::{SpdConfig, expectation};
for seed in 0..6 {
let p = every_kind(4, seed + 100);
let sv = dense(&p);

View file

@ -391,6 +391,34 @@ impl QuantumReceipt {
}
}
/// The `did:key` identifier of an Ed25519 public key, as the W3C did:key
/// method writes it: `did:key:z` and the base58btc text of the multicodec
/// prefix `0xed 0x01` followed by the key's 32 bytes. A signer that names itself
/// this way names the key that signed, so the name cannot claim another key.
pub fn did_key(key: &VerifyingKey) -> String {
const ALPHABET: &[u8; 58] = b"123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz";
let mut bytes = vec![0xed, 0x01];
bytes.extend_from_slice(key.as_bytes());
// Base 58 digits, least significant first. The prefix's first byte is not
// zero, so the text has no leading `1`s.
let mut digits: Vec<u8> = Vec::with_capacity(48);
for &b in &bytes {
let mut carry = u32::from(b);
for d in digits.iter_mut() {
carry += u32::from(*d) << 8;
*d = (carry % 58) as u8;
carry /= 58;
}
while carry > 0 {
digits.push((carry % 58) as u8);
carry /= 58;
}
}
let mut s = String::from("did:key:z");
s.extend(digits.iter().rev().map(|&d| ALPHABET[usize::from(d)] as char));
s
}
#[cfg(test)]
mod tests {
use super::*;
@ -558,6 +586,18 @@ mod tests {
}
}
/// The W3C did:key method's Ed25519 example, from its raw key.
#[test]
fn did_key_writes_the_methods_example() {
let raw: [u8; 32] = core::array::from_fn(|i| {
u8::from_str_radix(&"3b6a27bcceb6a42d62a3a8d02a6f0d73653215771de243a63ac048a18b59da29"[2 * i..2 * i + 2], 16).unwrap()
});
let vk = VerifyingKey::from_bytes(&raw).unwrap();
assert_eq!(did_key(&vk), "did:key:z6MkiTBz1ymuepAQ4HEHYSF1H8quG5GLVVQR3djdX3mDooWp");
assert!(did_key(&key(5).verifying_key()).starts_with("did:key:z6Mk"));
assert_ne!(did_key(&key(5).verifying_key()), did_key(&key(6).verifying_key()));
}
#[test]
fn json_refuses_counts_that_do_not_fit() {
let r = QuantumReceipt::seal(&key(5), "m", &ghz3(Gateset::V2), None, 0, None).unwrap();

View file

@ -674,7 +674,15 @@ pub fn to_qasm(c: &Circuit) -> String {
/// if its gateset is then changed.
pub fn from_qasm(src: &str) -> Result<Circuit, String> {
let mut n: Option<u8> = None;
let mut registers = 0;
// first pass: qubit count
// Registers counted statement by statement, however lines are broken.
for stmt in src.lines().map(|l| l.split("//").next().unwrap_or("")).flat_map(|l| l.split(';')) {
let stmt = stmt.trim();
if stmt.starts_with("qubit") || stmt.starts_with("qreg ") {
registers += 1;
}
}
for raw in src.lines() {
let line = strip(raw);
if let Some(rest) = line.strip_prefix("qubit[")
@ -688,20 +696,42 @@ pub fn from_qasm(src: &str) -> Result<Circuit, String> {
}
}
let n = n.ok_or("no qubit register declaration (qubit[n] q; or qreg q[n];)")?;
// What this reader does not take is refused, never dropped: the program
// would mean something else. `wai_quantum::qasm` reads all of it.
let fuller = "; the full reader (wai_quantum::qasm, the IDE's and the service's) takes it";
if registers > 1 {
return Err(format!("{registers} qubit registers; this reader takes one{fuller}"));
}
let mut c = Circuit::new(n);
let mut measured = false;
for raw in src.lines() {
let line = strip(raw);
if line.is_empty() {
continue;
}
if line.contains(';') && !line.starts_with("OPENQASM") && !line.starts_with("include") {
return Err(format!("several statements on one line (`{line}`); this reader takes one a line{fuller}"));
}
let first: String = line.chars().take_while(|c| c.is_ascii_alphanumeric() || *c == '_').collect();
match first.as_str() {
"reset" => return Err(format!("`reset` changes the state; this reader does not take it{fuller}")),
"gate" => return Err(format!("gate definitions are not read here{fuller}")),
"if" | "while" | "for" => return Err(format!("`{first}` acts on measured bits; this reader does not take it{fuller}")),
_ => {}
}
// Measurement is recorded, not simulated here, and the exporters disagree
// about how to spell it: OpenQASM 2 writes `measure q[0] -> c[0];` while
// Qiskit's OpenQASM 3 exporter writes `c[0] = measure q[0];`, whose first
// token is the *classical* register. Skip either shape before parsing a
// gate name out of the line.
if line.contains("measure") {
measured = true;
continue;
}
let gate_line = !matches!(first.as_str(), "OPENQASM" | "include" | "qubit" | "qreg" | "creg" | "bit" | "barrier");
if measured && gate_line {
return Err(format!("a gate after a measurement (`{line}`): this reader takes measurements only at the end{fuller}"));
}
// Gate modifiers: `ctrl @`, `ctrl(n) @`, `negctrl @`, `negctrl(n) @`.
if let Some((polarity, call)) = modifiers(&line)? {
push_modified(&mut c, &polarity, &call)?;
@ -1322,6 +1352,24 @@ mod tests {
assert_eq!(recommend_backend(&Circuit::qft(6)).0, Backend::Statevector);
}
/// What this reader cannot take is refused, never dropped.
#[test]
fn what_the_reader_cannot_take_is_refused() {
for src in [
"OPENQASM 3.0;\nqubit[1] a;\nqubit[1] b;\nx a[0];\nx b[0];\n",
"OPENQASM 3.0;\nqubit[1] q;\nx q[0];\nreset q[0];\n",
"OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nh q[0];\nc[0] = measure q[0];\nh q[0];\n",
"OPENQASM 3.0;\ngate flip a { x a; }\nqubit[1] q;\nflip q[0];\n",
"OPENQASM 3.0;\nqubit[2] q;\nh q[0]; cx q[0], q[1];\n",
] {
let e = from_qasm(src).unwrap_err();
assert!(e.contains("wai_quantum::qasm"), "{src}: {e}");
}
// Measurements at the end are still read (and dropped, as they change nothing).
let c = from_qasm("OPENQASM 3.0;\nqubit[1] q;\nbit[1] c;\nx q[0];\nc[0] = measure q[0];\n").unwrap();
assert_eq!(c.ops.len(), 1);
}
#[test]
fn qasm_round_trip() {
// Grover's search past three qubits uses a Z with three or more

3
wai/wai-rs/Cargo.lock generated
View file

@ -4310,10 +4310,11 @@ version = "0.1.0"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
"serde",
"serde_json",
"wgpu",
]

View file

@ -444,6 +444,11 @@ quantum_atom = ["quantum_ops", "wai-quantum/quantum_atom"]
# selection (Clifford → statevector → tensor-network cascade) + OpenQASM interop,
# all over the byte-exact fixed-point simulator. Framework-neutral, no cloud.
quantum_toolchain = ["quantum", "wai-quantum/quantum_toolchain"]
# OpenQASM 3 read one way on every surface (wai_quantum::qasm).
quantum_qasm = ["quantum", "wai-quantum/quantum_qasm"]
# The job service's engine (wai_quantum::engine): plan, run and sign a job as the
# service does, re-exported as wai::engine for the browser binding.
quantum_engine = ["quantum_qasm", "quantum_receipt", "wai-quantum/quantum_engine"]
# Matrix Product State backend (extensions/quantum-sim § MPS, `wai.quantum.mps`):
# the tensor-network "classical referee" — evolve a bounded-bond MPS gate-by-gate
# with a deterministic complex Jacobi SVD truncation, reproducible on every machine
@ -610,31 +615,6 @@ required-features = ["codecs"]
name = "wai"
path = "src/bin/wai.rs"
# The quantum stack as a CLI. Pure Rust, no host bindings — the same source builds
# for native AND `wasm32-wasip2`, producing byte-identical output under wasmtime.
[[bin]]
name = "wai-quantum"
path = "src/bin/wai_quantum.rs"
required-features = [
"quantum_toolchain",
"quantum_receipt",
"quantum_qir",
"quantum_channel",
"quantum_comm",
"quantum_qudit",
"quantum_spd_receipt",
"quantum_bptn",
"quantum_resource_receipt",
"quantum_frame",
"quantum_tn",
"quantum_gbs",
"quantum_sv",
"quantum_arch",
"quantum_ldpc",
"quantum_qio",
"quantum_fabric",
]
[[bin]]
name = "wai_int_synth_vectors"
path = "src/bin/wai_int_synth_vectors.rs"

View file

@ -775,6 +775,10 @@ pub use wai_quantum::quantum_atom;
// byte-exact fixed-point simulator. Framework-neutral and offline.
#[cfg(feature = "quantum_toolchain")]
pub use wai_quantum::quantum_toolchain;
#[cfg(feature = "quantum_qasm")]
pub use wai_quantum::qasm;
#[cfg(feature = "quantum_engine")]
pub use wai_quantum::engine;
// Matrix Product State backend (`wai.quantum.mps`) — the tensor-network classical
// referee: a bounded-bond MPS evolved with a deterministic complex Jacobi SVD

View file

@ -1241,10 +1241,11 @@ version = "0.1.0"
[[package]]
name = "wai-quantum"
version = "0.6.9"
version = "0.6.11"
dependencies = [
"blake3",
"ed25519-dalek",
"serde",
"serde_json",
"wgpu",
]

View file

@ -19,7 +19,7 @@ js-sys = "0.3"
# with the codec chain off. No second implementation: replay-equivalence
# in the browser is the identical Rust that generated the conformance
# corpus hashes.
wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] }
wai = { path = "../wai-rs", default-features = false, features = ["world", "audio_scene", "haptic", "splat4d", "provenance", "score", "film", "neural_int", "generate", "video_int", "audio_int", "world_model", "world_int", "splat_int", "quantum", "quantum_ops", "quantum_cal", "quantum_mitigate", "quantum_qec", "quantum_control", "quantum_noise", "quantum_compile", "quantum_atom", "quantum_qbom", "quantum_receipt", "quantum_toolchain", "quantum_qasm", "quantum_mps", "quantum_stabilizer", "quantum_pauli", "quantum_vml", "quantum_phasor", "quantum_qfhrr", "quantum_kernel", "quantum_phasor_meter", "quantum_qdata", "video_workflow", "staged", "stage_measure", "receipt_claims"] }
ed25519-dalek = "2" # actor keys for browser-side op authoring (same version as wai-rs)
blake3 = "1" # in-page receipt check: hash a decoded image against a pinned digest
# Awaits the WebGPU member's futures behind the promises of src/quantum_webgpu.rs;
@ -33,6 +33,11 @@ wasm-bindgen-futures = { version = "0.4.77", optional = true }
# --out-name (demo/quantum_webgpu.html); off by default, so the main bundle is
# unchanged.
quantum_webgpu = ["wai/quantum_fabric", "wai/quantum_fabric_webgpu", "dep:wasm-bindgen-futures"]
# The job service's engine in a page (src/quantum_engine.rs): a job planned, run
# and signed in the tab as the service runs it, its receipt checked there. Built
# into the IDE's bundle (scripts/wasm-pkgs.tsv on the site), which the console
# also loads; off by default, so the main bundle is unchanged.
quantum_engine = ["wai/quantum_engine"]
[dev-dependencies]
# Pinned to the wasm-bindgen release Cargo.lock holds, so the test runner and the

View file

@ -160,6 +160,11 @@ pub mod quantum_qbom;
#[cfg(all(feature = "quantum_webgpu", target_arch = "wasm32"))]
mod quantum_webgpu;
// The job service's engine in the page: plan, run and sign a job in the tab, and
// check any job's receipt there, the service's included. The IDE's bundle only.
#[cfg(feature = "quantum_engine")]
pub mod quantum_engine;
// Staged delivery (extensions/staged-delivery.md): wai::staged::StageSink, the sink a
// native player runs, bound for JS — bases added, refinement layers offered, holds
// expired and bases removed in any order, every unit's stage, producer, tier, output

View file

@ -0,0 +1,216 @@
//! The quantum job service's engine in a page ([`wai::engine`]): a job
//! planned, run and sealed in the tab exactly as the service plans, runs and
//! seals it, with the page's own key; the receipt of any job, the service's
//! included, checked in the tab; a program linted and drawn. Requests and
//! answers are the service's JSON, so a page shows a job run here and one run
//! there with the same code.
use ed25519_dalek::SigningKey;
use serde_json::{Value, json};
use wai::engine::{catalog, draw, job};
use wasm_bindgen::prelude::*;
/// What a result run in a tab says of its energy.
const UNMETERED: &str = "a browser tab does not meter a run's energy";
fn error_json(e: impl std::fmt::Display) -> String {
json!({ "error": e.to_string() }).to_string()
}
fn read(request: &str) -> Result<Value, String> {
serde_json::from_str(request).map_err(|e| format!("the job is not JSON: {e}"))
}
/// The engine, with the key it signs with and the memory a job may take.
#[wasm_bindgen]
pub struct QuantumEngine {
key: SigningKey,
job_bytes: u64,
}
#[wasm_bindgen]
impl QuantumEngine {
/// An engine that signs with the Ed25519 key whose 32-byte seed is
/// `seed` (the page makes one at random and keeps it in this browser),
/// and admits a job of at most `memory_mb` MiB.
#[wasm_bindgen(constructor)]
pub fn new(seed: &[u8], memory_mb: u32) -> Result<QuantumEngine, String> {
let bytes: [u8; 32] = seed.try_into().map_err(|_| format!("a signing key's seed is 32 bytes, not {}", seed.len()))?;
Ok(QuantumEngine { key: SigningKey::from_bytes(&bytes), job_bytes: u64::from(memory_mb.max(1)) << 20 })
}
/// The signer the receipts name: the key as a `did:key`.
pub fn signer_did(&self) -> String {
wai::quantum_receipt::did_key(&self.key.verifying_key())
}
/// Run a job (the service's request JSON): the job as the service
/// returns it, the request, the plan and the result with its receipt; or
/// `{"error"}` with why it did not run.
pub fn run(&self, request: &str) -> String {
match read(request).and_then(|req| job::run(&req, self.job_bytes, &self.key, json!({ "unmetered": UNMETERED }))) {
Ok(body) => body.to_string(),
Err(e) => error_json(e),
}
}
/// The method a job would run on and why, its memory and work, without
/// running it; or `{"error"}`.
pub fn plan(&self, request: &str) -> String {
match read(request).and_then(|req| catalog::plan(&req, self.job_bytes)) {
Ok(p) => serde_json::to_value(p).map(|v| v.to_string()).unwrap_or_else(error_json),
Err(e) => error_json(e),
}
}
/// Each method, its accuracy, what it takes, and the most qubits it holds
/// in a job's memory here.
pub fn methods(&self) -> String {
json!({ "methods": catalog::describe(self.job_bytes) }).to_string()
}
}
/// A program read as the engine reads it: `{"ok": true, ...}` with its shape,
/// or `{"ok": false, "error", "line"}`.
#[wasm_bindgen]
pub fn engine_lint(src: &str) -> String {
job::lint(src).to_string()
}
/// A program's circuit as text, wrapped at `width` columns: `{"text"}`, or
/// `{"error"}`.
#[wasm_bindgen]
pub fn engine_draw(src: &str, width: u32) -> String {
match wai::qasm::parse(src) {
Ok(p) => json!({ "text": draw::draw(&p, width.max(20) as usize) }).to_string(),
Err(e) => error_json(e),
}
}
/// Whether a job's receipt seals its request and its result, checked here:
/// `{"valid", "key", "signer"}` (the signer as a `did:key`), or `{"error"}`.
#[wasm_bindgen]
pub fn engine_verify(job_json: &str) -> String {
let v: Value = match serde_json::from_str(job_json) {
Ok(v) => v,
Err(e) => return error_json(format!("not JSON: {e}")),
};
match job::verify(&v) {
Ok((valid, key)) => {
let signer = key.as_str().and_then(job::did_of);
json!({ "valid": valid, "key": key, "signer": signer }).to_string()
}
Err(e) => error_json(e),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn bell(extra: &str) -> String {
format!("{{\"qasm\": \"OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];\"{extra}}}")
}
/// A job run in the tab is the service's form, signed with the page's key,
/// and its receipt checks there; a changed result does not.
#[test]
fn a_job_runs_signs_and_verifies_in_the_page() {
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
let body: Value = serde_json::from_str(&e.run(&bell(", \"shots\": 500, \"observables\": [\"Z0 Z1\"]"))).unwrap();
assert_eq!(body["method"], "stabilizer", "{body}");
assert_eq!(body["result"]["expectations"][0]["value"], 1.0);
assert_eq!(body["result"]["energy"]["unmetered"], UNMETERED);
let v: Value = serde_json::from_str(&engine_verify(&body.to_string())).unwrap();
assert_eq!(v["valid"], true, "{v}");
assert_eq!(v["signer"], e.signer_did());
let mut forged = body.clone();
forged["result"]["expectations"][0]["value"] = json!(-1.0);
let v: Value = serde_json::from_str(&engine_verify(&forged.to_string())).unwrap();
assert_eq!(v["valid"], false, "{v}");
}
/// Any angle runs, on the double-precision state vector.
#[test]
fn a_rotation_runs_on_the_state_vector() {
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
let req = r#"{"qasm": "OPENQASM 3.0; qubit[1] q; rx(0.3) q[0];", "observables": ["Z0"]}"#;
let body: Value = serde_json::from_str(&e.run(req)).unwrap();
assert_eq!(body["method"], "statevector", "{body}");
assert!((body["result"]["expectations"][0]["value"].as_f64().unwrap() - 0.3f64.cos()).abs() < 1e-12, "{body}");
}
/// What the page cannot run says why: a job too large for the tab's
/// memory, a program the reader refuses, a request that is not JSON.
#[test]
fn what_does_not_run_says_why() {
let e = QuantumEngine::new(&[9u8; 32], 64).unwrap();
let wide = r#"{"qasm": "OPENQASM 3.0; qubit[30] q; rx(0.3) q; cx q[0], q[29];", "amplitudes": ["000000000000000000000000000000"], "method": "statevector"}"#;
let r: Value = serde_json::from_str(&e.run(wide)).unwrap();
assert!(r["error"].as_str().unwrap().contains("statevector holds at most"), "{r}");
let r: Value = serde_json::from_str(&e.run(r#"{"qasm": "qubit[1] q; foo q[0];"}"#)).unwrap();
assert!(r["error"].as_str().unwrap().contains("foo"), "{r}");
let r: Value = serde_json::from_str(&e.run("not json")).unwrap();
assert!(r["error"].as_str().unwrap().starts_with("the job is not JSON"), "{r}");
assert!(QuantumEngine::new(&[1u8; 31], 64).is_err());
}
#[test]
fn plan_lint_draw_and_methods_answer_as_json() {
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
let p: Value = serde_json::from_str(&e.plan(&bell(""))).unwrap();
assert_eq!(p["method"], "stabilizer", "{p}");
let l: Value = serde_json::from_str(&engine_lint("qubit[1] q;\nfoo q[0];")).unwrap();
assert_eq!(l["line"], 2, "{l}");
let d: Value = serde_json::from_str(&engine_draw("qubit[2] q; h q[0]; cx q[0], q[1];", 80)).unwrap();
assert!(d["text"].as_str().unwrap().contains('⊕'), "{d}");
let m: Value = serde_json::from_str(&e.methods()).unwrap();
assert!(m["methods"].as_array().unwrap().iter().any(|x| x["method"] == "cutrank"), "{m}");
}
}
/// The engine compiled to wasm32 and run under Node, as a page runs it: no
/// clock std can read and no threads, on every method a tab holds.
#[cfg(all(test, target_arch = "wasm32"))]
mod wasm_tests {
use super::*;
use wasm_bindgen_test::*;
fn run(e: &QuantumEngine, req: Value) -> Value {
serde_json::from_str(&e.run(&req.to_string())).unwrap()
}
#[wasm_bindgen_test]
fn a_job_runs_and_verifies_in_wasm() {
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
let body = run(&e, json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 200 }));
assert_eq!(body["method"], "stabilizer", "{body}");
// A tab has no clock std reads, so no time is claimed.
assert!(body["result"].get("elapsed_ms").is_none(), "{body}");
let v: Value = serde_json::from_str(&engine_verify(&body.to_string())).unwrap();
assert_eq!(v["valid"], true, "{v}");
}
#[wasm_bindgen_test]
fn every_method_a_tab_holds_runs_in_wasm() {
let e = QuantumEngine::new(&[9u8; 32], 512).unwrap();
let ring = "OPENQASM 3.0; qubit[12] q; bit[12] c; h q; cz q[0], q[1]; cz q[1], q[2]; cz q[2], q[3]; cz q[3], q[4]; cz q[4], q[5]; cz q[5], q[6]; cz q[6], q[7]; cz q[7], q[8]; cz q[8], q[9]; cz q[9], q[10]; cz q[10], q[11]; t q; h q; c = measure q;";
let jobs = [
("exact", json!({ "qasm": "OPENQASM 3.0; qubit[3] q; h q[0]; t q[0]; cx q[0], q[1];", "shots": 100 })),
("statevector", json!({ "qasm": "OPENQASM 3.0; qubit[3] q; rx(0.3) q[0]; cx q[0], q[1];", "observables": ["Z0"] })),
("stabilizer", json!({ "qasm": "OPENQASM 3.0; qubit[40] q; h q[0]; cx q[0], q[39];", "shots": 100 })),
("mps", json!({ "qasm": "OPENQASM 3.0; qubit[30] q; rx(0.3) q; cx q[0], q[1];", "shots": 50, "method": "mps" })),
("pauli", json!({ "qasm": "OPENQASM 3.0; qubit[30] q; rx(0.3) q; cx q[0], q[1];", "observables": ["Z0 Z1"], "method": "pauli" })),
("tensor_network", json!({ "qasm": "OPENQASM 3.0; qubit[30] q; rx(0.3) q; cx q[0], q[1];", "amplitudes": ["0".repeat(30)], "method": "tensor_network" })),
("cutrank", json!({ "qasm": ring, "shots": 20, "method": "cutrank" })),
("density_matrix", json!({ "qasm": "OPENQASM 3.0; qubit[2] q; h q[0]; cx q[0], q[1];", "shots": 100, "noise": { "depolarizing_2q": 0.02 } })),
("trajectories", json!({ "qasm": "OPENQASM 3.0; qubit[3] q; h q[0]; cx q[0], q[1];", "shots": 100, "noise": { "depolarizing_1q": 0.01 }, "method": "trajectories" })),
("statevector", json!({ "qasm": "OPENQASM 3.0; qubit[2] q; bit[2] c; rx(0.4) q[0]; c[0] = measure q[0]; if (c[0]) x q[1]; c[1] = measure q[1];", "shots": 100 })),
];
for (want, req) in jobs {
let body = run(&e, req.clone());
assert_eq!(body["method"], want, "{req}: {body}");
assert_eq!(body["status"], "done", "{req}: {body}");
}
}
}

View file

@ -14,15 +14,24 @@ use wai::quantum::{circuit_hash, BaseGate, Circuit, Gate, Gateset, Measure, Stat
use wai::quantum_mps;
use wai::quantum_pauli;
use wai::quantum_receipt::QuantumReceipt;
use wai::signer_id::did_key;
use wai::quantum_stabilizer;
use wai::quantum_toolchain as tc;
use wasm_bindgen::prelude::*;
const SCALE: f64 = (1u64 << FRAC) as f64;
fn signer() -> SigningKey {
/// The key a receipt is signed with before the page gives the IDE its own
/// ([`QuantumIde::set_signing_key`]). It is public, so a receipt it signs
/// proves nothing about who made it; [`QuantumIde::key_is_demo`] says so.
fn demo_signer() -> SigningKey {
SigningKey::from_bytes(&[0x51; 32])
}
/// `{"error": message}`, the message escaped as JSON requires.
fn error_json(message: impl std::fmt::Display) -> String {
serde_json::json!({ "error": message.to_string() }).to_string()
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
@ -98,6 +107,8 @@ pub struct QuantumIde {
sv: Option<StateVector>,
/// Whether the console note has been printed for the circuit as it stands.
noted: bool,
/// The key receipts are signed with: this browser's, once the page sets it.
key: Option<SigningKey>,
}
#[wasm_bindgen]
@ -105,7 +116,30 @@ 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::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false }
QuantumIde { circuit: Circuit::with_gateset(n_qubits.clamp(1, 12), Gateset::V2), sv: None, noted: false, key: None }
}
// ---- signing key ----
/// Sign receipts with the Ed25519 key whose 32-byte seed is `seed`: the
/// page makes one at random and keeps it in this browser. Refuses any
/// other length.
pub fn set_signing_key(&mut self, seed: &[u8]) -> Result<(), String> {
let bytes: [u8; 32] = seed.try_into().map_err(|_| format!("a signing key's seed is 32 bytes, not {}", seed.len()))?;
self.key = Some(SigningKey::from_bytes(&bytes));
Ok(())
}
/// The public key receipts are signed with, as hex.
pub fn public_key_hex(&self) -> String {
hex(self.signer().verifying_key().as_bytes())
}
/// The signer a receipt names: that public key as a `did:key`.
pub fn signer_did(&self) -> String {
did_key(self.signer().verifying_key().as_bytes())
}
/// Whether receipts are still signed with the published demo key, which
/// proves nothing about who made them.
pub fn key_is_demo(&self) -> bool {
self.key.is_none()
}
// ---- gateset ----
@ -180,10 +214,20 @@ impl QuantumIde {
}
}
/// 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).
/// The program is read as the job service reads it ([`wai::qasm`]): every
/// register, broadcast, gate definition and modifier. Returns an error
/// string on failure (an unsupported construct with its line, a
/// non-dyadic angle, more qubits than the page holds). A program that
/// measures in the middle, resets after a gate or acts on measured bits is
/// refused, never rewritten, with a pointer to the console that runs it.
pub fn load_qasm(&mut self, src: &str) -> Result<(), String> {
let c = tc::from_qasm(src)?;
let c = wai::qasm::dyadic_circuit(src).map_err(|e| {
if e.starts_with("this program measures") {
format!("{e}. The console at /quantum-run/ runs it.")
} else {
e
}
})?;
if c.n_qubits > 12 {
return Err(format!("{} qubits exceeds the in-browser limit (12)", c.n_qubits));
}
@ -192,22 +236,13 @@ impl QuantumIde {
}
/// The op-log as JSON, for the editor to render.
pub fn gates_json(&self) -> String {
let items: Vec<String> = self
let items: Vec<serde_json::Value> = self
.circuit
.ops
.iter()
.map(|g| {
let ctrls: Vec<String> = g.controls.iter().map(|c| c.to_string()).collect();
format!(
"{{\"name\":\"{}\",\"controls\":[{}],\"target\":{},\"param\":{}}}",
name_of(g.base),
ctrls.join(","),
g.target,
g.param
)
})
.map(|g| serde_json::json!({ "name": name_of(g.base), "controls": g.controls, "target": g.target, "param": g.param }))
.collect();
format!("[{}]", items.join(","))
serde_json::Value::Array(items).to_string()
}
// ---- run ----
@ -314,13 +349,7 @@ impl QuantumIde {
pub fn transpile_line(&self) -> String {
let r = tc::route_line(&self.circuit);
let eq = tc::routing_equivalent(&self.circuit, &r);
format!(
"{{\"source_2q\":{},\"nonlocal\":{},\"routed_gates\":{},\"swaps\":{},\"equivalent\":{eq}}}",
r.source_2q,
r.source_nonlocal,
r.circuit.ops.len(),
r.swaps
)
serde_json::json!({ "source_2q": r.source_2q, "nonlocal": r.source_nonlocal, "routed_gates": r.circuit.ops.len(), "swaps": r.swaps, "equivalent": eq }).to_string()
}
/// Run the circuit through the tensor-network (MPS) backend at bond cap
/// `chi_max` and report `{bond, retained, fidelity}` against the exact dense
@ -328,17 +357,14 @@ impl QuantumIde {
pub fn run_mps(&self, chi_max: u32) -> String {
let dense = match self.circuit.simulate() {
Ok(d) => d,
Err(e) => return format!("{{\"error\":\"{e}\"}}"),
Err(e) => return error_json(e),
};
match quantum_mps::run(&self.circuit, chi_max.max(1) as usize) {
Ok((mps, r)) => {
let f = quantum_mps::fidelity(&mps, &dense);
format!(
"{{\"bond\":{},\"retained\":{:.6},\"fidelity\":{:.6}}}",
r.max_bond, r.retained_weight, f
)
serde_json::json!({ "bond": r.max_bond, "retained": r.retained_weight, "fidelity": f }).to_string()
}
Err(e) => format!("{{\"error\":\"{e:?}\"}}"),
Err(e) => error_json(format!("{e:?}")),
}
}
/// Sample the current circuit on the O(n²) stabilizer backend (if Clifford):
@ -347,9 +373,9 @@ impl QuantumIde {
match quantum_stabilizer::sample(&self.circuit, seed as u64, shots.max(1) as u64) {
Ok(v) => {
let (o, c) = v.first().copied().unwrap_or((0, 0));
format!("{{\"distinct\":{},\"top_index\":{o},\"top_count\":{c}}}", v.len())
serde_json::json!({ "distinct": v.len(), "top_index": o, "top_count": c }).to_string()
}
Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
Err(e) => error_json(e.0),
}
}
/// Scale demonstration: build an `n`-qubit GHZ and sample it on the stabilizer
@ -364,16 +390,16 @@ impl QuantumIde {
c.cx(i - 1, i);
}
match quantum_stabilizer::sample(&c, seed as u64, shots.max(1) as u64) {
Ok(v) => format!("{{\"n\":{n},\"distinct\":{},\"gates\":{}}}", v.len(), c.ops.len()),
Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
Ok(v) => serde_json::json!({ "n": n, "distinct": v.len(), "gates": c.ops.len() }).to_string(),
Err(e) => error_json(e.0),
}
}
/// Compute ⟨Z_q⟩ on the current circuit via sparse-Pauli / Heisenberg
/// propagation: `{value, peak}` (peak Pauli-sum size) or `{error}`.
pub fn expect_z_pauli(&self, q: u8, threshold: f64) -> String {
match quantum_pauli::expect_z(&self.circuit, q, threshold.max(0.0)) {
Ok((v, peak)) => format!("{{\"value\":{v:.6},\"peak\":{peak}}}"),
Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
Ok((v, peak)) => serde_json::json!({ "value": v, "peak": peak }).to_string(),
Err(e) => error_json(e.0),
}
}
/// Scale demonstration: ⟨Z_0⟩ on an `n`-qubit low-magic circuit — a single
@ -401,8 +427,8 @@ impl QuantumIde {
}
}
match quantum_pauli::expect_z(&c, 0, threshold.max(0.0)) {
Ok((v, peak)) => format!("{{\"n\":{n},\"value\":{v:.6},\"peak\":{peak}}}"),
Err(e) => format!("{{\"error\":\"{}\"}}", e.0),
Ok((v, peak)) => serde_json::json!({ "n": n, "value": v, "peak": peak }).to_string(),
Err(e) => error_json(e.0),
}
}
/// Seal a signed run receipt over the byte-exact reconstruction (and a shot
@ -445,10 +471,14 @@ impl QuantumIde {
}
}
fn signer(&self) -> SigningKey {
self.key.clone().unwrap_or_else(demo_signer)
}
fn seal(&self, seed: u64, shots: u64) -> Option<QuantumReceipt> {
QuantumReceipt::seal(
&signer(),
"did:key:quantum-ide",
&self.signer(),
did_key(self.signer().verifying_key().as_bytes()),
&self.circuit,
Some(Measure { seed, shots }),
0,
@ -461,11 +491,8 @@ impl QuantumIde {
/// The algorithm library catalog as JSON `[{id,name,desc}]`, for the IDE dropdown.
#[wasm_bindgen]
pub fn ide_catalog() -> String {
let items: Vec<String> = tc::catalog()
.into_iter()
.map(|(id, name, desc)| format!("{{\"id\":\"{id}\",\"name\":\"{name}\",\"desc\":\"{desc}\"}}"))
.collect();
format!("[{}]", items.join(","))
let items: Vec<serde_json::Value> = tc::catalog().into_iter().map(|(id, name, desc)| serde_json::json!({ "id": id, "name": name, "desc": desc })).collect();
serde_json::Value::Array(items).to_string()
}
#[cfg(test)]
@ -600,4 +627,82 @@ mod tests {
assert!(ide.set_gateset(2));
assert_eq!(ide.note(), "");
}
fn loaded(src: &str) -> QuantumIde {
let mut ide = QuantumIde::new(2);
ide.load_qasm(src).unwrap_or_else(|e| panic!("{src}: {e}"));
ide.run().unwrap();
ide
}
/// The outcome with probability 1, or None.
fn certain(ide: &QuantumIde) -> Option<usize> {
let p = ide.probabilities();
p.iter().position(|&x| (x - 1.0).abs() < 1e-9)
}
/// OpenQASM means what it says: registers keep their own qubits, broadcast,
/// comments, single-qubit declarations and gate definitions are read, and a
/// program whose meaning needs its measurements is refused, not rewritten.
#[test]
fn imported_programs_mean_what_they_say() {
// Two registers are two qubits: |11⟩.
let ide = loaded("OPENQASM 3.0; qubit[1] a; qubit[1] b; x a[0]; x b[0];");
assert_eq!(ide.circuit.n_qubits, 2);
assert_eq!(certain(&ide), Some(3));
// OpenQASM 2's registers too: three qubits, one of them flipped.
let ide = loaded("OPENQASM 2.0; include \"qelib1.inc\"; qreg a[2]; qreg b[1]; x b[0];");
assert_eq!(ide.circuit.n_qubits, 3);
assert_eq!(certain(&ide).map(|i| i.count_ones()), Some(1));
// Broadcast, a single-qubit declaration, a block comment, a definition.
let ide = loaded("OPENQASM 3.0; qubit[2] q; /* both */ h q;");
assert!(ide.probabilities().iter().all(|&p| (p - 0.25).abs() < 1e-9), "{:?}", ide.probabilities());
let ide = loaded("OPENQASM 3.0; qubit q; x q;");
assert_eq!((ide.circuit.n_qubits, certain(&ide)), (1, Some(1)));
let ide = loaded("OPENQASM 3.0; gate flip a { x a; } qubit[1] q; flip q[0];");
assert_eq!(certain(&ide), Some(1));
// A final measurement leaves the state as it was.
let ide = loaded("OPENQASM 3.0; qubit[1] q; bit[1] c; x q[0]; c[0] = measure q[0];");
assert_eq!(certain(&ide), Some(1));
// A measurement acted on afterwards, or a reset after a gate, is refused.
let mut ide = QuantumIde::new(2);
for src in ["OPENQASM 3.0; qubit[1] q; bit[1] c; h q[0]; c[0] = measure q[0]; h q[0];", "OPENQASM 3.0; qubit[1] q; x q[0]; reset q[0];"] {
let e = ide.load_qasm(src).unwrap_err();
assert!(e.contains("measures a qubit and then acts on it") && e.contains("/quantum-run/"), "{src}: {e}");
}
// A missing semicolon is an error with its line, as in the service.
assert!(ide.load_qasm("OPENQASM 3.0;\nqubit[2] q\nh q[0];").is_err());
}
/// Every JSON the panels read parses, whatever an error message holds.
#[test]
fn the_panels_get_json_that_parses() {
let mut ide = QuantumIde::new(8);
assert!(ide.load_algorithm("grover", 8, 0));
ide.run().unwrap();
for text in [ide.run_mps(8), ide.transpile_line(), ide.sample_stabilizer(1.0, 16), ide.stabilizer_scale(20, 1.0, 16), ide.expect_z_pauli(0, 1e-6), ide.pauli_scale(8, 1e-4), ide.gates_json(), super::ide_catalog()] {
serde_json::from_str::<serde_json::Value>(&text).unwrap_or_else(|e| panic!("{text}: {e}"));
}
assert_eq!(super::error_json("say \"no\" \\ here"), r#"{"error":"say \"no\" \\ here"}"#);
}
/// Receipts are signed with the key the page gives, and say when they are
/// not.
#[test]
fn receipts_use_the_browsers_key() {
let mut ide = QuantumIde::new(2);
assert!(ide.load_algorithm("bell", 2, 0));
ide.run().unwrap();
assert!(ide.key_is_demo());
let demo = ide.public_key_hex();
ide.set_signing_key(&[7u8; 32]).unwrap();
assert!(!ide.key_is_demo());
assert_ne!(ide.public_key_hex(), demo);
assert!(ide.receipt_verifies(1.0, 64));
let r: serde_json::Value = serde_json::from_str(&ide.receipt_json(1.0, 64)).unwrap();
assert_eq!(r["signer_pubkey"], ide.public_key_hex());
assert_eq!(r["signer_id"], ide.signer_did());
assert!(ide.signer_did().starts_with("did:key:z6Mk"));
assert!(ide.set_signing_key(&[1u8; 31]).is_err());
}
}