wai-quantum: jobs placed on certified GPU members or split over several, byte for byte the CPU's; Clifford+T as sums of stabilizer states #557

Merged
dcharlot merged 7 commits from wai/quantum-scale-out into main 2026-10-31 14:53:38 -04:00
49 changed files with 10009 additions and 116 deletions

View file

@ -12,10 +12,15 @@ name: wai-quantum CUDA member
# limbs, must do all of the same under its own implementation id, with its
# own mutants caught; and the dense f64 state vector must come back with the
# CPU's bits on its pinned corpus and at 16 and 20 qubits, every state-vector
# mutant caught.
# mutant caught; and jobs placed on the fabric's members found there (the GPU
# by CUDA and by OpenCL, the CPU by OpenCL) must give the CPU's answers byte
# for byte, asked for by key and by `accelerator`, alone and with one state
# split four ways over them and the CPU reference, a state past a member
# refused, and auto keeping every state on the CPU.
# - cuda-absent (a container with no GPU and no CUDA libraries): the same build
# finds no member and says why, rather than failing; the device tests run over
# no member with the declaration `none`.
# no member with the declaration `none`; and placement with no device, the
# state split over CPU partitions bit for bit the CPU's, every test by name.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@ -133,6 +138,35 @@ jobs:
key="$WAI_QUANTUM_CUDA_EXPECT"
grep -q "^cuda sv certified: key=$key .* parity=bit-exact cases=48 digest=b7d129d0593adf70dc164477664970c5c0b8c09f79f1a0573036bd60888dbcd1$" "$report" || { echo "::error::the state vector is not the CPU's bit for bit"; exit 1; }
grep -qx "cuda sv mutants: key=$key killed 4 of 4" "$report" || { echo "::error::a state-vector mutant survived"; exit 1; }
- name: Jobs placed on the fabric's members, and a state split over several devices
shell: bash
working-directory: src/wai/wai-quantum
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
# The CPU platform's runtime loads its threading and memory libraries
# from the toolkit's own directories (as in wai-quantum-opencl.yml).
oneapi=/opt/intel/oneapi
export LD_LIBRARY_PATH="$oneapi/compiler/latest/lib:$oneapi/tbb/latest/lib/intel64/gcc4.8:$oneapi/tbb/latest/lib:$oneapi/umf/latest/lib:$oneapi/tcm/latest/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
# The members placement finds here, sorted: the GPU by CUDA and by OpenCL,
# and the CPU by OpenCL (the integrated GPU has no binary64, and is not
# placed). One state is split four ways: the GPU by both, the CPU by
# OpenCL, and the CPU reference.
keys="${WAI_QUANTUM_HW_FABRIC_KEYS:-gpu-cuda-sm89,opencl-cpu-8086,opencl-gpu-10de}"
split="${WAI_QUANTUM_HW_FABRIC_SPLIT:-gpu-cuda-sm89,opencl-gpu-10de,opencl-cpu-8086,cpu}"
report="$RUNNER_TEMP/fabric-report.txt"; : > "$report"
WAI_QUANTUM_FABRIC_EXPECT="$keys" WAI_QUANTUM_FABRIC_SPLIT="$split" WAI_QUANTUM_FABRIC_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-cuda" nice -n 10 \
cargo test --locked --release --features quantum_engine,quantum_fabric,quantum_fabric_cuda,quantum_fabric_opencl --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log"
grep -qE '^test result: ok\. 5 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::not every placement test ran and passed"; exit 1; }
cat "$report"
grep -qx "fabric members: as declared ($keys)" "$report" || { echo "::error::the members placement found are not the declared ones"; exit 1; }
for key in ${keys//,/ }; do
grep -q "^fabric placed: key=$key .* give the CPU's answers byte for byte$" "$report" || { echo "::error::$key: a placed job's answers are not the CPU's"; exit 1; }
grep -q "^fabric refused: key=$key " "$report" || { echo "::error::$key: a state past what it holds was not refused"; exit 1; }
done
grep -qx "fabric split: ${split//,/+} over 14 and 19 qubits gives the CPU's answers byte for byte" "$report" || { echo "::error::the state split over the devices is not the CPU's"; exit 1; }
grep -qx "fabric auto: every state on the CPU, its result unnamed" "$report" || { echo "::error::auto placed a state on a member"; exit 1; }
grep -q "^fabric accelerator: .* the CPU's answers byte for byte" "$report" || { echo "::error::an accelerator request placed no state"; exit 1; }
cuda-absent:
name: member · CUDA absent (linux)
@ -167,3 +201,25 @@ jobs:
grep -qx 'cuda tensor devices: as declared (none)' "$report" || { echo "::error::the tensor-core absent case was not established"; exit 1; }
cargo test --locked --features quantum_fabric_cuda,quantum_sv --test cuda_sv -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/sv.log"
grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::the state-vector tests did not run over no device"; exit 1; }
WAI_QUANTUM_FABRIC_EXPECT=none WAI_QUANTUM_FABRIC_REPORT="$report" cargo test --locked --features quantum_engine,quantum_fabric,quantum_fabric_cuda --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log"
grep -qE '^test result: ok\. 5 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::the placement tests did not run over no member"; exit 1; }
grep -qx 'fabric members: as declared (none)' "$report" || { echo "::error::the placement's absent case was not established"; exit 1; }
# The placement and the split with no device: CPU partitions bit for bit the
# CPU's state vector, exchanges included, and every refusal a host gives;
# each test required by name.
- name: Placement and the split, on CPU partitions
shell: bash
working-directory: wai/wai-quantum
run: |
set -euo pipefail
cargo test --locked --features full --lib -- engine::fabric:: quantum_fabric::sv64:: 2>&1 | tee "$RUNNER_TEMP/split.log"
for t in quantum_fabric::sv64::tests::a_partitioned_run_is_the_cpu_run_bit_for_bit \
quantum_fabric::sv64::tests::exchanges_and_remaps_move_amplitudes_and_compute_nothing \
quantum_fabric::sv64::tests::a_split_refuses_what_it_cannot_hold \
engine::fabric::tests::a_job_s_fabric_reads_or_is_refused_with_its_code \
engine::fabric::tests::a_placement_changes_where_the_state_runs_and_not_a_byte_of_the_answers \
engine::fabric::tests::a_placement_this_host_cannot_honour_is_refused_never_made_elsewhere \
engine::fabric::tests::a_placed_result_and_its_provenance_follow_their_schemas; do
grep -qx "test $t ... ok" "$RUNNER_TEMP/split.log" || { echo "::error::$t did not run and pass"; exit 1; }
done
grep -qE '^test result: ok\. [1-9][0-9]* passed; 0 failed' "$RUNNER_TEMP/split.log" || { echo "::error::a test failed"; exit 1; }

View file

@ -12,7 +12,11 @@ name: wai-quantum HIP member
# GPU's bf16 matrix instruction from offset-binary 8-bit limbs, must do all
# of the same under its own implementation id, with its own mutants caught;
# and the dense f64 state vector must come back with the CPU's bits on its
# pinned corpus and at 16 and 20 qubits, every state-vector mutant caught.
# pinned corpus and at 16 and 20 qubits, every state-vector mutant caught;
# and jobs placed on the fabric's members found there (the GPU by HIP and by
# OpenCL) must give the CPU's answers byte for byte, asked for by key and by
# `accelerator`, alone and with one state split over them, a state past a
# member refused, and auto keeping every state on the CPU.
# - hip-absent (a container with no GPU and no HIP libraries): the same build
# finds no member and says why, rather than failing; the device tests run over
# no member with the declaration `none`.
@ -135,6 +139,28 @@ jobs:
cat "$report"
grep -q "^hip sv certified: key=$key .* parity=bit-exact cases=48 digest=b7d129d0593adf70dc164477664970c5c0b8c09f79f1a0573036bd60888dbcd1$" "$report" || { echo "::error::the state vector is not the CPU's bit for bit"; exit 1; }
grep -qx "hip sv mutants: key=$key killed 4 of 4" "$report" || { echo "::error::a state-vector mutant survived"; exit 1; }
- name: Jobs placed on the fabric's members, and a state split over them
shell: bash
working-directory: src/wai/wai-quantum
run: |
set -euo pipefail
export PATH="$HOME/.cargo/bin:$PATH"
# The members placement finds here, sorted (the GPU by HIP and by OpenCL),
# declared here or by the runner; one state is split over them.
keys="${WAI_QUANTUM_AMD_FABRIC_KEYS:-gpu-hip-gfx1151,opencl-gpu-1002}"
report="$RUNNER_TEMP/fabric-report.txt"; : > "$report"
WAI_QUANTUM_FABRIC_EXPECT="$keys" WAI_QUANTUM_FABRIC_SPLIT="$keys" WAI_QUANTUM_FABRIC_REPORT="$report" CARGO_TARGET_DIR="$HOME/hw-target/wai-quantum-hip" nice -n 10 \
cargo test --locked --release --features quantum_engine,quantum_fabric,quantum_fabric_hip,quantum_fabric_opencl --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log"
grep -qE '^test result: ok\. 5 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::not every placement test ran and passed"; exit 1; }
cat "$report"
grep -qx "fabric members: as declared ($keys)" "$report" || { echo "::error::the members placement found are not the declared ones"; exit 1; }
for key in ${keys//,/ }; do
grep -q "^fabric placed: key=$key .* give the CPU's answers byte for byte$" "$report" || { echo "::error::$key: a placed job's answers are not the CPU's"; exit 1; }
grep -q "^fabric refused: key=$key " "$report" || { echo "::error::$key: a state past what it holds was not refused"; exit 1; }
done
grep -qx "fabric split: ${keys//,/+} over 14 and 19 qubits gives the CPU's answers byte for byte" "$report" || { echo "::error::the state split over the members is not the CPU's"; exit 1; }
grep -qx "fabric auto: every state on the CPU, its result unnamed" "$report" || { echo "::error::auto placed a state on a member"; exit 1; }
grep -q "^fabric accelerator: .* the CPU's answers byte for byte" "$report" || { echo "::error::an accelerator request placed no state"; exit 1; }
hip-absent:
name: member · HIP absent (linux-arm64)
@ -169,3 +195,6 @@ jobs:
grep -qx 'hip wmma devices: as declared (none)' "$report" || { echo "::error::the matrix-instruction absent case was not established"; exit 1; }
cargo test --locked --features quantum_fabric_hip,quantum_sv --test hip_sv -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/sv.log"
grep -qE '^test result: ok\. 3 passed; 0 failed' "$RUNNER_TEMP/sv.log" || { echo "::error::the state-vector tests did not run over no device"; exit 1; }
WAI_QUANTUM_FABRIC_EXPECT=none WAI_QUANTUM_FABRIC_REPORT="$report" cargo test --locked --features quantum_engine,quantum_fabric,quantum_fabric_hip --test fabric_placement -- --test-threads=1 2>&1 | tee "$RUNNER_TEMP/fabric.log"
grep -qE '^test result: ok\. 5 passed; 0 failed' "$RUNNER_TEMP/fabric.log" || { echo "::error::the placement tests did not run over no member"; exit 1; }
grep -qx 'fabric members: as declared (none)' "$report" || { echo "::error::the placement's absent case was not established"; exit 1; }

View file

@ -267,6 +267,57 @@ wire format may change and such changes are recorded here.
implementation, `channel_lineup_verify.py`, from the text alone, agrees on every vector, and a
CI step runs both.
### wai-quantum: simulator scale-out, the state vector on the fabric's members
- **Placement** (`engine::fabric`): a job's new `fabric` field says where the `statevector` method's state runs:
`auto` (the default), `cpu`, `accelerator`, a member's id or key, or a list of members, a power of two of them,
the state split by its top qubits into one partition on each. The CUDA, HIP and OpenCL members' dense f64 state
vectors (`quantum_fabric::sv64`) are discovered once per process, compiled and certified against the CPU on the
pinned corpus; only a member certified bit-exact is placed, so a placed job's answers are the CPU's byte for
byte. **`auto` places on the CPU** on every host until a member is measured faster than the CPU: with one launch
per gate and the state copied to the device and back, a whole job took about as long either way, or longer on the
GPU, between 18 and 22 qubits on the fleet's GPUs. A member runs a job only when the job asks for one:
`accelerator` takes the first certified member that holds the state, GPUs first, and one that fails during the run
hands the job to the next, the receipt listing each that failed (`fell_back_from`). A placement the host cannot
honour is refused with the new code `J013`, never made elsewhere. A job that does not ask has the plan and result
it always had, on every host; the pinned digests are unchanged.
- **The fabric's receipt in the job's**: a placed result, or one whose job asked for a placement, carries `fabric`
(the kernel, the placement asked, the member or each partition's, its certificate's parity tier, digest and
corpus, and the energy, unmetered with the reason), sealed by the job's receipt; its provenance's `computer`
names the members. New optional members of `wai.quantum.job-request/1`, `wai.quantum.job-result/1` and
`wai.quantum.provenance/1`, and `fabric_members` in `wai.quantum.tools/1`.
- **Several devices on one node** (`quantum_fabric::sv64::run_split`): each partition runs the launches the CPU
plans on its own amplitudes, each runner on a thread of its own; a gate on a partition qubit first has that qubit
exchanged with a local one through the host's copy of the state. Held bit for bit to the CPU's state vector over
the pinned corpus at every partition count a case admits and on wide states with exchanges, on CPU partitions in
every container. On the fleet: a laptop GPU by CUDA and by OpenCL, the CPU by OpenCL and the CPU reference,
one state split four ways over them; an integrated GPU by HIP and by OpenCL; and each GPU beside the CPU and split
over itself, each giving the CPU's answers byte for byte. Each member states the most qubits it holds (25 on every GPU
here), and a state or partition past it is refused before anything runs.
- **Members' runs from a given state**: `CudaSv::run_launches`, `HipSv::run_launches` and
`OpenClSv::run_launches`, with `max_qubits`, each refusing a state or a launch outside what it holds.
- **Surfaces**: `wai-quantum run --fabric` (repeatable) and `wai-quantum fabric [--json]`, the command line's
`cuda`, `hip` and `opencl` features; `fabric` in both MCP servers' job tools and `list_fabric` on the command
line's; the job service reads `fabric` and places on its CPU. The generated agent docs carry it.
- **CI**: `tests/fabric_placement.rs` on jetson-hub (CUDA and OpenCL) and framework-128gb (HIP and OpenCL) in the
hardware workflows, each with its declared members and a split over them, and absent where there is no device;
the split and the planner's refusals in the containers.
- **Stabilizer-rank Clifford+T** (`quantum_stabrank`, the method `stabilizer_rank`): a T gate is a sum of two
Clifford gates of equal weight, so a circuit with `t` T and T† gates is a sum of `2^t` stabilizer states, each run
exactly on the phased tableau, whose new `PhasedTableau::amplitudes` gives every amplitude of a stabilizer state
exactly from one reduction. Every term is counted as a whole number (`ExactSum`), so the sum does not depend on
how the branches are split between threads; an expectation value is the amplitude `⟨0|U†PU|0⟩`. The exact mode's
terms grow as `2^t` (`4^t` for an expectation value); the sampled mode (`terms`, or `epsilon`, at `confidence`)
draws branches uniformly and states the bound `sqrt((sec²(π/8)^t − 1) / (terms · (1 − confidence)))` on each
value with its confidence, computed from the T count, the terms and the confidence. `Program::clifford_t` lowers
`t`, `tdg`, `p`, `rz`, `rx` and `ry` at odd multiples of π/4, `cp` at odd multiples of π/2, `ccz` and `ccx` with
the global phase kept exactly. Held to the dense state vector by every amplitude and by fidelity, never by hash.
`auto` takes it for amplitudes or expectation values past the dense state vector when its branches fit one job's
work, so it is chosen by the T count; it reaches the methods list, both MCP servers, the service and the Studio
(its plain name in English and Simplified Chinese, the views page rebuilt in the CI image) by the same name. The result's accuracy, amplitudes and expectation values gain optional `t_count`, `terms`, `bound` and
`confidence` (`wai.quantum.job-result/1`), and the class `estimate` now also covers a bound with the probability it
holds.
### wai-quantum: the variational and algorithm library
- **Parameters in a job** (`engine::catalog::bind_params`): a job's `"params"` gives each of its program's parameters

View file

@ -13,6 +13,7 @@
"method.tensor_network": "Tensor network",
"method.cutrank": "Cut-rank sum",
"method.cutrank_single": "Cut-rank sum on the GPU, single precision",
"method.stabilizer_rank": "Stabilizer-rank sum",
"method.density_matrix": "Density matrix",
"method.trajectories": "Trajectories",
"accuracy.bit-exact": "The same bits on every machine.",

View file

@ -13,6 +13,7 @@
"method.tensor_network": "张量网络",
"method.cutrank": "割秩求和",
"method.cutrank_single": "GPU 上的割秩求和,单精度",
"method.stabilizer_rank": "稳定子秩求和",
"method.density_matrix": "密度矩阵",
"method.trajectories": "轨迹采样",
"accuracy.bit-exact": "在每台机器上都得到相同的比特。",

View file

@ -6,7 +6,7 @@
import { el } from './dom.js';
import { t as tr } from './i18n.js';
export const METHOD_NAMES = Object.fromEntries(['exact', 'statevector', 'statevector_single', 'stabilizer', 'pauli', 'mps', 'tensor_network', 'cutrank', 'cutrank_single', 'density_matrix', 'trajectories'].map((m) => [m, tr(`method.${m}`)]));
export const METHOD_NAMES = Object.fromEntries(['exact', 'statevector', 'statevector_single', 'stabilizer', 'pauli', 'mps', 'tensor_network', 'cutrank', 'cutrank_single', 'stabilizer_rank', 'density_matrix', 'trajectories'].map((m) => [m, tr(`method.${m}`)]));
export const ACCURACY = Object.fromEntries(['bit-exact', 'exact', 'single', 'bounded', 'estimate', 'heuristic'].map((a) => [a, tr(`accuracy.${a}`)]));
// A bit string short enough to read.

View file

@ -126,6 +126,18 @@ version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
[[package]]
name = "cl3"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad5f4170f520224684c5c6e86e8d5474c2a10696e553b1db8389970cd82ece7"
dependencies = [
"dlopen2",
"libc",
"opencl-sys",
"thiserror",
]
[[package]]
name = "clap"
version = "4.6.7"
@ -260,6 +272,15 @@ dependencies = [
"typenum",
]
[[package]]
name = "cudarc"
version = "0.19.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7359bf1de037ddada72729c3acec9b040f3468400c6e9e740b01ba0cd2e89562"
dependencies = [
"libloading",
]
[[package]]
name = "curve25519-dalek"
version = "4.1.3"
@ -324,6 +345,29 @@ dependencies = [
"syn 3.0.9",
]
[[package]]
name = "dlopen2"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4"
dependencies = [
"dlopen2_derive",
"libc",
"once_cell",
"winapi",
]
[[package]]
name = "dlopen2_derive"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.119",
]
[[package]]
name = "document-features"
version = "0.2.12"
@ -370,7 +414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -603,6 +647,16 @@ version = "0.2.191"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a"
[[package]]
name = "libloading"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [
"cfg-if",
"windows-link",
]
[[package]]
name = "linux-raw-sys"
version = "0.12.1"
@ -697,6 +751,25 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "opencl-sys"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7c295aff010162eb6b1a59c758f0547b886c1edcbf324a41f48ee8f64dc48c4"
dependencies = [
"libc",
]
[[package]]
name = "opencl3"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecb3473c4f01afd0eea3ee5b31649fec47dbc23353a63e1b916aa67cd810fb73"
dependencies = [
"cl3",
"libc",
]
[[package]]
name = "percent-encoding"
version = "2.3.2"
@ -794,7 +867,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.61.2",
"windows-sys 0.52.0",
]
[[package]]
@ -992,6 +1065,26 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "thiserror"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.9",
]
[[package]]
name = "time"
version = "0.3.55"
@ -1123,10 +1216,12 @@ name = "wai-quantum"
version = "0.7.0"
dependencies = [
"blake3",
"cudarc",
"ed25519-dalek",
"objc2",
"objc2-foundation",
"objc2-metal",
"opencl3",
"serde",
"serde_json",
]
@ -1168,6 +1263,28 @@ dependencies = [
"rustls-pki-types",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-link"
version = "0.2.1"

View file

@ -28,6 +28,11 @@ service = ["dep:ureq"]
providers = ["dep:wai-quantum-qpu"]
# The single-precision state vector on the GPU (`--method statevector_single`), on macOS.
metal = ["wai-quantum/quantum_sv_metal", "wai-quantum/quantum_cutrank_metal"]
# The fabric's accelerator members a statevector job may be placed on (`--fabric`, `wai-quantum fabric`): each loads its
# driver and runtime compiler when first used, so a build with them runs where there are none and finds no member there.
cuda = ["wai-quantum/quantum_fabric_cuda"]
hip = ["wai-quantum/quantum_fabric_hip"]
opencl = ["wai-quantum/quantum_fabric_opencl"]
[dependencies]
clap = { version = "4.6", features = ["derive", "env", "wrap_help"] }

View file

@ -96,10 +96,19 @@ wai-quantum instance ghz-family.qasm --input n=3
| pauli | Pauli dynamics, for expectation values |
| tensor_network | amplitudes |
| cutrank | a Clifford circuit with a layer of rotations |
| stabilizer_rank | Clifford+T circuits, as a sum of stabilizer states: amplitudes and expectation values, exact, or sampled with a stated bound (`--option terms=N` or `--option epsilon=E`) |
| density_matrix, trajectories | noise |
`wai-quantum methods` lists each method's reach in a job's memory here (`--memory-gb`, default 16).
`--fabric` says where the `statevector` method's state runs: `auto` (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), `cpu`, `accelerator` (the first accelerator member certified bit-exact here that holds the state, the next taking over if it fails during the run), or a member's id or key; given more than once, the state is split by its top qubits into one partition on each member named (a power of two of them; `cpu` is a member, and one may be named twice). Every member placed gives the CPU's answers byte for byte, and the result names where its state ran, with the member's certificate, in its receipt. `wai-quantum fabric` lists the members this host can place on, each with the most qubits it holds, and the devices found and not placed, with why. A placement this host cannot honour is refused (`J013`), never made elsewhere.
```sh runnable
wai-quantum fabric
wai-quantum run circuit.qasm --method statevector --fabric cpu --fabric cpu --shots 100
wai-quantum run prog.qasm --fabric a-member-elsewhere # exits 1
```
Each result names:
- its method, and why that method was chosen;
@ -693,6 +702,7 @@ Every `--json` output and every MCP tool's answer follows a committed JSON Schem
- **Default:** `submit`, over HTTPS.
- **`--features metal`:** the single-precision state vector on the GPU (`--method statevector_single`), on macOS.
- **`--features cuda`, `hip`, `opencl`:** the fabric's accelerator members a state vector can be placed on (`--fabric`, `wai-quantum fabric`). Each loads its driver and runtime compiler when first used, so a build with them runs where there are none and finds no member there.
- **`--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

@ -99,10 +99,19 @@ wai-quantum instance ghz-family.qasm --input n=3
| pauli | Pauli 动力学,用于期望值 |
| tensor_network | 振幅 |
| cutrank | 带一层旋转的 Clifford 线路 |
| stabilizer_rank | Clifford+T 线路,写成稳定子态之和:振幅和期望值,精确,或以给定界抽样(`--option terms=N` 或 `--option epsilon=E`) |
| density_matrix, trajectories | 噪声 |
`wai-quantum methods` 列出每种方法在本机一个作业的内存(`--memory-gb`,默认 16)内能达到的规模。
`--fabric` 指定 `statevector` 方法的态在哪里运行:`auto`(默认:在每台主机上都放在 CPU 上,直到测得某个计算结构成员比 CPU 更快)、`cpu`、`accelerator`(本机第一个经认证逐位一致且能容纳该态的加速器成员;若它在运行中失败,由下一个接替),或成员的 id 或键;多次给出时,态按最高位量子比特划分为若干分区,每个所列成员一个分区(数量为 2 的幂;`cpu` 也是成员,同一成员可列两次)。放置到的每个成员都给出与 CPU 逐字节相同的答案,结果在其收据中指明态在哪里运行以及该成员的证书。`wai-quantum fabric` 列出本机可放置的成员(各附其可容纳的最多量子比特数),以及找到但未放置的设备及原因。主机无法兑现的放置会被拒绝(`J013`),绝不改放别处。
```sh runnable
wai-quantum fabric
wai-quantum run circuit.qasm --method statevector --fabric cpu --fabric cpu --shots 100
wai-quantum run prog.qasm --fabric a-member-elsewhere # exits 1:别处的成员,被拒绝
```
每个结果都注明:
- 所用的方法,以及选择该方法的原因;
@ -683,6 +692,7 @@ wai-quantum version --json # 同样的内容,以 wai.quantum.versions/1
- **默认:**包含 `submit`,通过 HTTPS。
- **`--features metal`:**在 macOS 上于 GPU 运行的单精度态矢量(`--method statevector_single`)。
- **`--features cuda`、`hip`、`opencl`:**态矢量可放置其上的计算结构加速器成员(`--fabric`、`wai-quantum fabric`)。每个成员在首次使用时才加载其驱动和运行时编译器,因此带这些特性的构建在没有它们的机器上也能运行,只是在那里找不到成员。
- **`--no-default-features --target wasm32-wasip2`:**不含 `submit` 的 WASI 组件。它在任何 WASI 运行时下给出相同的输出。
`wai-quantum completions zsh`(或 bash、fish、elvish、PowerShell)打印 shell 补全脚本。

View file

@ -58,6 +58,7 @@ Parameters:
- `options` (object): Method options: max_bond, fusion, trajectories, threshold, memory_gb.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's "compiled" states its distance from the program.
- `target` (any): A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it.
- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).
- `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
@ -81,6 +82,7 @@ Parameters:
- `options` (object): Method options: max_bond, fusion, trajectories, threshold, memory_gb.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's "compiled" states its distance from the program.
- `target` (any): A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it.
- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).
- `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
@ -387,6 +389,10 @@ Parameters:
Each simulation method: its accuracy class, the circuits it takes, and the most qubits it holds in a job's memory here.
### `list_fabric`: List the fabric's members
The members of the fabric a statevector job can be placed on here (a job's `fabric`): the CPU and each accelerator found and certified bit-exact against the CPU in this process, each with its key, its certificate and the most qubits it holds; the devices found and not placed, and why; and the backends this build reaches.
### `qasm_diagnostics`: The reader's diagnostic codes
Every code the OpenQASM reader gives (each refusal and warning carries one, as `[Q103]` at the end of its message and as `code` in lint's JSON): its outcome (refused, lowered, carried, preserved or warned) and its meaning; and beside them, under job_codes, the codes of what is asked of a program (`[J011]`: the request, the method, an intent, a repair, the host). A code never changes meaning once released. Give `code` for one of either.
@ -424,6 +430,7 @@ Parameters:
- `options` (object): Method options: max_bond, fusion, trajectories, threshold, memory_gb.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's "compiled" states its distance from the program.
- `target` (any, required): A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it.
- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's "fabric" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).
- `intent` (any): What the program must do, checked by the engine with the run: the result's "verdict" states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's "repaired" holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
@ -601,11 +608,12 @@ Parameters:
- `observables` (array): Pauli strings to take expectation values of, e.g. ["Z0", "Z0 Z1", "X2 Y3"]. Qubits count from 0.
- `amplitudes` (array): Basis states to give amplitudes of: one character per qubit, qubit n-1 first, e.g. "0011".
- `noise` (object): Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, stabilizer_rank, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `seed` (integer): Sampling seed (default 1); the same job and seed give the same counts.
- `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.
- `target` (any): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing.
- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.
- `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
@ -623,11 +631,12 @@ Parameters:
- `observables` (array): Pauli strings to take expectation values of, e.g. ["Z0", "Z0 Z1", "X2 Y3"]. Qubits count from 0.
- `amplitudes` (array): Basis states to give amplitudes of: one character per qubit, qubit n-1 first, e.g. "0011".
- `noise` (object): Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, stabilizer_rank, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `seed` (integer): Sampling seed (default 1); the same job and seed give the same counts.
- `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.
- `target` (any): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing.
- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.
- `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
@ -646,11 +655,12 @@ Parameters:
- `observables` (array): Pauli strings to take expectation values of, e.g. ["Z0", "Z0 Z1", "X2 Y3"]. Qubits count from 0.
- `amplitudes` (array): Basis states to give amplitudes of: one character per qubit, qubit n-1 first, e.g. "0011".
- `noise` (object): Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `method` (string, one of auto, exact, statevector, statevector_single, stabilizer, pauli, mps, tensor_network, cutrank, cutrank_single, stabilizer_rank, density_matrix, trajectories, pulse): Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded.
- `seed` (integer): Sampling seed (default 1); the same job and seed give the same counts.
- `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.
- `target` (any): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing.
- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.
- `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
@ -792,6 +802,7 @@ Parameters:
- `options` (object): Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories.
- `compile` (object): Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.
- `target` (any, required): A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing.
- `fabric` (any): Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.
- `intent` (any): What the program must do, checked by the engine with the run: the result's `verdict` states how far it checked (proof, evidence, executed, parsed, refused) and whether the intent holds, sealed by the receipt.
- `repair` (any): Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {"max_iterations": N, "dyadic_bits": K, "sizes": {"q": 3}}. The result's `repaired` holds the program that ran and each repair.
- `mitigate` (any): Mitigate the job's expectation values (or, for readout, its counts) under its own noise: zne (the whole program folded U(U†U)^k and extrapolated), pea (the noise learned as a sparse Pauli–Lindblad model and amplified), pec (the learned channels inverted and sampled), cdr (a line fitted over near-Clifford training circuits), vd (two copies), readout (the counts' flips inverted). Needs noise and observables (shots for readout), at most 8 qubits. The result's `mitigation` gives raw and mitigated values with standard errors, the ideal value (known because this is a simulation), the method's bias and variance assumptions and its cost. A method's name, or an object.
@ -1119,7 +1130,8 @@ A program that measures a qubit and then acts on it, resets one after its first
- On this machine: `wai-quantum run`, or the local MCP server (`wai-quantum mcp`). Results are signed with the user's key and nothing leaves the machine.
- On the free job service, for a program too large for this machine: `wai-quantum submit`, `POST https://wai.transaction.science/quantum-jobs/v1/run`, or its MCP server at https://wai.transaction.science/quantum-jobs/mcp. No account and no key.
- auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen.
- auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, stabilizer_rank, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, stabilizer_rank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen; stabilizer_rank is chosen by its T count, its exact sum taken when its branches are within one job's work.
- The statevector method's state runs where the job's `fabric` says: auto (the default) places it on the CPU, on every host, until a member of the fabric is measured faster than the CPU; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere.
### `wai-quantum://docs/diagnostics`: the reader's diagnostics
@ -1277,6 +1289,7 @@ Every code:
| `stabilizer` | exact; with Pauli noise, counts are noisy samples and expectations estimates | Clifford (rotations by whole quarter turns, controlled phases by whole half turns), optionally with depolarizing, dephasing and readout noise | counts, expectations |
| `pauli` | bounded | any | expectations |
| `mps` | exact when nothing is truncated, otherwise heuristic | any; best on low-entanglement circuits | counts, expectations, amplitudes |
| `stabilizer_rank` | exact; with terms or epsilon, an estimate with its bound | Clifford+T (t, tdg, and p, rz, rx, ry at odd multiples of π/4, cp at odd multiples of π/2, ccz and ccx), each T gate doubling the sum's terms: the state as a sum of stabilizer states | amplitudes, expectations |
| `tensor_network` | exact | any | amplitudes |
| `cutrank` | exact; single when only a single-precision table fits | a Clifford circuit, then one layer of one-qubit gates at any angles: each qubit's gates from its first non-Clifford one on act on that qubit alone; the cost is set by the state's cut-rank along an order of the qubits, not by their number | counts, amplitudes |
| `cutrank_single` | single | as for cutrank; the table on a GPU, its arithmetic in single precision | counts, amplitudes |
@ -1289,10 +1302,12 @@ The accuracy classes:
- bit-exact: the same bits on every machine: amplitudes in 30-bit fixed point, every output an exact function of them; counts are seeded samples
- exact: no approximation beyond IEEE double-precision rounding; counts are seeded samples of the exact distribution
- bounded: an approximation with a rigorous bound on |exact − value| reported beside each value
- estimate: a statistical estimate, reported with its standard error
- estimate: a statistical estimate, reported with its standard error, or with a bound on its error and the probability that the bound holds
- heuristic: an approximation reported with the standard estimate of its error (a matrix product state's fidelity estimate), not a bound
auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen.
auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, stabilizer_rank, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, stabilizer_rank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen; stabilizer_rank is chosen by its T count, its exact sum taken when its branches are within one job's work.
The statevector method's state runs where the job's `fabric` says: auto (the default) places it on the CPU, on every host, until a member of the fabric is measured faster than the CPU; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere.
### `wai-quantum://examples`: worked examples, each with what it must do
@ -1586,6 +1601,10 @@ Commands:
version This binary's version and the engine's, the schema of each document it writes, the
protocols it speaks, and where the policy is that says what each version promises
methods Each method, what it takes, and how far it reaches in a job's memory here
fabric The members of the fabric a statevector job can be placed on here (`run --fabric`):
the CPU and each accelerator this build reaches that is found and certified bit-exact
against the CPU, each with the most qubits it holds; the devices found and not
placed, and why
diagnostics The diagnostic codes: the OpenQASM reader's (each with its outcome: refused, lowered,
carried, preserved or warned) and the job layer's (refused or retry), each with its
meaning
@ -1773,6 +1792,14 @@ Options:
A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of
the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes
--fabric <PLACEMENT>
Where the state vector runs (the statevector method): auto (the default: the CPU, until a
member is measured faster), cpu, accelerator (the first certified accelerator member that
holds the state), or a member's id or key (`wai-quantum fabric` lists them); repeated, the
state is split by its top qubits into one partition on each member named, a power of two
of them. A member certified bit-exact gives the CPU's answers; a placement this host
cannot honour is refused (J013)
--json
Print the job as the service returns it: request, plan, result and receipt
@ -1911,6 +1938,14 @@ Options:
A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of
the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes
--fabric <PLACEMENT>
Where the state vector runs (the statevector method): auto (the default: the CPU, until a
member is measured faster), cpu, accelerator (the first certified accelerator member that
holds the state), or a member's id or key (`wai-quantum fabric` lists them); repeated, the
state is split by its top qubits into one partition on each member named, a power of two
of them. A member certified bit-exact gives the CPU's answers; a placement this host
cannot honour is refused (J013)
--json
Print the plan as JSON
@ -2986,6 +3021,14 @@ Options:
A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of
the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes
--fabric <PLACEMENT>
Where the state vector runs (the statevector method): auto (the default: the CPU, until a
member is measured faster), cpu, accelerator (the first certified accelerator member that
holds the state), or a member's id or key (`wai-quantum fabric` lists them); repeated, the
state is split by its top qubits into one partition on each member named, a power of two
of them. A member certified bit-exact gives the CPU's answers; a placement this host
cannot honour is refused (J013)
--json
Print the job as the service returns it: request, plan, result and receipt
@ -3589,6 +3632,43 @@ Options:
Print version
```
### `wai-quantum fabric`
```text
The members of the fabric a statevector job can be placed on here (`run --fabric`): the CPU and each
accelerator this build reaches that is found and certified bit-exact against the CPU, each with the
most qubits it holds; the devices found and not placed, and why
Usage: wai-quantum fabric [OPTIONS]
Options:
--input <NAME=VALUE>
A value for a generator's integer `input`, as n=5 (repeat for each input): the program is
read as its family's circuit at those values
--json
Print the list as JSON
--lang <LOCALE>
The language of the words the command line writes itself, en or zh-CN (by default the one
LANG names); results, codes and the engine's own messages stay in English
--memory-gb <MEMORY_GB>
The memory a job may take, GiB
[default: 16]
--from <FORMAT>
The format of every program the command reads: qasm, qir (LLVM text) or jeff; by default
the file's extension says (.ll or .qir is QIR, .jeff is jeff), and OpenQASM otherwise
-h, --help
Print help
-V, --version
Print version
```
### `wai-quantum diagnostics`
```text

View file

@ -73,6 +73,7 @@ Runs on this machine, signed with the user's key; nothing leaves the machine.
- `history_blame`: Whose hand each line and gate is in
- `history_log`: A program's history
- `list_methods`: List the methods
- `list_fabric`: List the fabric's members
- `qasm_diagnostics`: The reader's diagnostic codes
- `list_targets`: List the device targets
- `show_target`: Show a device target

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -62,6 +62,7 @@
"version": { "source": "This binary's version and the engine's, the schema of each document it writes, the protocols it speaks, and where the policy is that says what each version promises", "text": "本程序和引擎的版本、它所写的每种文档遵循的模式、它所使用的协议,以及说明每个版本作何承诺的策略所在之处" },
"key": { "source": "Show the key this user signs receipts with", "text": "显示本用户为回执签名所用的密钥" },
"methods": { "source": "Each method, what it takes, and how far it reaches in a job's memory here", "text": "每种方法、它接受什么,以及在本机作业内存中它能达到的规模" },
"fabric": { "source": "The members of the fabric a statevector job can be placed on here (`run --fabric`): the CPU and each accelerator this build reaches that is found and certified bit-exact against the CPU, each with the most qubits it holds; the devices found and not placed, and why", "text": "本机上 statevector 作业可放置其上的计算结构成员(`run --fabric`):CPU,以及本构建可达、已找到并经认证与 CPU 逐位一致的每个加速器,各附其可容纳的最多量子比特数;以及找到但未放置的设备及其原因" },
"diagnostics": { "source": "The diagnostic codes: the OpenQASM reader's (each with its outcome: refused, lowered, carried, preserved or warned) and the job layer's (refused or retry), each with its meaning", "text": "诊断代码:OpenQASM 读取器的代码(各附其结果:refused、lowered、carried、preserved 或 warned)和作业层的代码(refused 或 retry),各附其含义" },
"mcp": { "source": "Serve run, plan, lint, critique, the equivalence check, improve, the classical shortcuts, compile, draw, the debugger and verify to an agent over standard input and output (MCP), with resources and prompts; `mcp config` and `mcp install` register it with a client", "text": "通过标准输入输出(MCP)向智能体提供 run、plan、lint、critique、等价检查、improve、经典捷径、compile、draw、调试器和 verify,以及资源和提示词;`mcp config` 和 `mcp install` 把它注册到客户端" },
"fmt": { "source": "Format programs in one layout: a statement to a line, blocks indented two spaces, a space around each operator, comments and blank lines kept; a program whose brackets do not match is left as it is, with its line", "text": "按统一的版式格式化程序:每行一条语句,块缩进两个空格,运算符两侧各留一个空格,保留注释和空行;括号不匹配的程序保持原样,并给出所在的行" },

View file

@ -36,6 +36,7 @@ Generated by `wai-quantum docs skill` from the servers' own lists; `wai-quantum
- `history_blame`: Whose hand each line and gate is in
- `history_log`: A program's history
- `list_methods`: List the methods
- `list_fabric`: List the fabric's members
- `qasm_diagnostics`: The reader's diagnostic codes
- `list_targets`: List the device targets
- `show_target`: Show a device target

View file

@ -585,7 +585,7 @@ pub fn llms_full() -> String {
for (k, v) in wai_quantum::engine::catalog::accuracy_classes().as_object().into_iter().flatten() {
s.push_str(&format!("- {k}: {}\n", v.as_str().unwrap_or("")));
}
s.push_str(&format!("\n{}\n", engine::AUTO));
s.push_str(&format!("\n{}\n\n{}\n", engine::AUTO, engine::FABRIC));
s.push_str(&format!("\n### `{}`: worked examples, each with what it must do\n\n", engine::EXAMPLES));
for e in engine::examples() {
s.push_str(&format!("#### {} (`{}`)\n\n{}\n\n```qasm\n{}```\n\n", e.title, e.file(), e.shows, e.program));

View file

@ -330,6 +330,15 @@ enum Command {
#[arg(long, default_value_t = DEFAULT_MEMORY_GB)]
memory_gb: u64,
},
/// The members of the fabric a statevector job can be placed on here (`run --fabric`): the CPU and each accelerator this build reaches that is found and certified bit-exact against the CPU, each with the most qubits it holds; the devices found and not placed, and why
Fabric {
/// Print the list as JSON
#[arg(long)]
json: bool,
/// The memory a job may take, GiB
#[arg(long, default_value_t = DEFAULT_MEMORY_GB)]
memory_gb: u64,
},
/// The diagnostic codes: the OpenQASM reader's (each with its outcome: refused, lowered, carried, preserved or warned) and the job layer's (refused or retry), each with its meaning
Diagnostics {
/// Print the table as JSON
@ -532,6 +541,9 @@ struct Job {
/// A parameter's value, as theta=0.3 (repeatable): each `input float` and `input angle` of the program, bound exactly (the job's "params"); in a sweep, the parameters it fixes
#[arg(long = "param", value_name = "NAME=VALUE", value_parser = variational::param_arg)]
params: Vec<(String, f64)>,
/// Where the state vector runs (the statevector method): auto (the default: the CPU, until a member is measured faster), cpu, accelerator (the first certified accelerator member that holds the state), or a member's id or key (`wai-quantum fabric` lists them); repeated, the state is split by its top qubits into one partition on each member named, a power of two of them. A member certified bit-exact gives the CPU's answers; a placement this host cannot honour is refused (J013)
#[arg(long = "fabric", value_name = "PLACEMENT")]
fabric: Vec<String>,
}
/// `name=v1,v2,…`: each value a whole number if it reads as one, else a number.
@ -595,6 +607,27 @@ fn option_arg(s: &str) -> Result<(String, Value), String> {
Ok((k.to_string(), value))
}
/// The fabric's members as `wai-quantum fabric` prints them.
fn fabric_text(f: &Value) -> String {
let s = |v: &Value| v.as_str().unwrap_or("").to_string();
let mut o = format!("kernel {}\nauto {}\n", s(&f["kernel"]), s(&f["auto"]));
for m in f["members"].as_array().into_iter().flatten() {
let corpus = m["corpus"].as_str().map(|c| format!(", corpus {}… over {} cases", &c[..16.min(c.len())], m["cases"])).unwrap_or_default();
o.push_str(&format!("member {:<18} {:<4} up to {} qubits, {} {}{corpus}\n", s(&m["key"]), s(&m["class"]), m["detail"]["max_qubits"], s(&m["implementation"]), s(&m["parity"]["tier"])));
o.push_str(&format!(" {}\n", s(&m["member"])));
}
for x in f["refused"].as_array().into_iter().flatten() {
o.push_str(&format!("not placed {} {}: {}\n", s(&x["backend"]), if x["key"].as_str().is_some_and(|k| !k.is_empty()) { s(&x["key"]) } else { s(&x["member"]) }, s(&x["why"])));
}
for x in f["unavailable"].as_array().into_iter().flatten() {
o.push_str(&format!("none found {}: {}\n", s(&x["backend"]), s(&x["why"])));
}
if f["backends"].as_array().is_none_or(Vec::is_empty) {
o.push_str("backends none in this build: the CPU alone (build with --features cuda, hip or opencl)\n");
}
o
}
fn die(msg: &str) -> ! {
eprintln!("wai-quantum: {msg}");
std::process::exit(1)
@ -790,6 +823,11 @@ fn request(job: &Job, qasm: String) -> Value {
if !job.params.is_empty() {
r["params"] = Value::Object(job.params.iter().map(|(k, v)| (k.clone(), json!(v))).collect());
}
match &job.fabric[..] {
[] => {}
[one] => r["fabric"] = json!(one),
many => r["fabric"] = json!(many),
}
r
}
@ -926,6 +964,13 @@ fn print_job(body: &Value, top: usize) {
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(f) = r.get("fabric") {
let s = |v: &Value| v.as_str().unwrap_or("").to_string();
match f.get("partitions").and_then(Value::as_array) {
Some(parts) => println!("fabric {} partitions on {}, {} exchanges, {}", parts.len(), parts.iter().map(|p| s(&p["key"])).collect::<Vec<_>>().join(", "), f["exchanges"], s(&f["parity"]["tier"])),
None => println!("fabric {} ({}), {}, certificate {}", s(&f["key"]), s(&f["implementation"]), s(&f["parity"]["tier"]), s(&f["certificate"])),
}
}
if let Some(w) = r.get("warning").and_then(Value::as_str) {
println!("warning {w}");
}
@ -944,13 +989,16 @@ fn print_job(body: &Value, top: usize) {
println!(" … {} more (--top N)", v.len() - top);
}
}
// A bound that holds with a stated probability says so.
let held = |e: &Value| e.get("confidence").map(|c| format!(" (with probability {})", s(c))).unwrap_or_default();
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();
let bound = e.get("bound").map(|b| format!(" ± {}{}", s(b), held(e))).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());
let bound = a.get("bound").map(|b| format!(" ± {}{}", s(b), held(a))).unwrap_or_default();
println!("|{}⟩ {re:+.12} {}{:.12}i{bound}", s(&a["bits"]), if im < 0.0 { '-' } else { '+' }, im.abs());
}
if let Some(d) = r.get("dynamic") {
println!("dynamic {d}");
@ -1377,6 +1425,14 @@ fn main() {
}
}
}
Command::Fabric { json, memory_gb } => {
let f = wai_quantum::engine::fabric::list(memory_gb << 30);
if json {
println!("{f}");
} else {
print!("{}", fabric_text(&f));
}
}
Command::Diagnostics { json } => {
if json {
println!("{}", job::diagnostic_codes());
@ -1498,7 +1554,7 @@ mod tests {
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, history: None, compile: None, target: None, intent: vec![], verdict: false, repair: false, max_iterations: None, programs: vec![], sweep: vec![], mitigate: None, pulse: None, params: vec![] },
Job { file: None, shots: None, seed: None, method: None, observables: vec![], amplitudes: vec![], noise: vec![], options: vec![], memory_gb: 1, history: None, compile: None, target: None, intent: vec![], verdict: false, repair: false, max_iterations: None, programs: vec![], sweep: vec![], mitigate: None, pulse: None, params: vec![], fabric: vec![] },
qasm.to_string(),
)
}

View file

@ -72,6 +72,7 @@ fn output_of(tool: &str) -> Option<&'static str> {
"classical_shortcuts" => "tools.v1.json#/$defs/shortcuts",
"draw_circuit" => "tools.v1.json#/$defs/draw",
"list_methods" => "tools.v1.json#/$defs/methods",
"list_fabric" => "tools.v1.json#/$defs/fabric_members",
"rewrite_qasm" => "tools.v1.json#/$defs/rewrite",
"novelty_qasm" => "tools.v1.json#/$defs/novelty",
"discover_evaluate" => "tools.v1.json#/$defs/discover_evaluate",
@ -115,6 +116,7 @@ fn job_schema() -> Value {
"options": { "type": "object", "description": "Method options: max_bond, fusion, trajectories, threshold, memory_gb." },
"compile": compile_options_schema("Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's \"compiled\" states its distance from the program."),
"target": { "description": "A device target: its name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to the target's gates, placed where it is most likely to succeed and simulated under the target's noise, the target's hash in the receipt; noise may not be given beside it." },
"fabric": fabric_schema(),
"intent": intent_schema(),
"repair": { "description": "Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {\"max_iterations\": N, \"dyadic_bits\": K, \"sizes\": {\"q\": 3}}. The result's \"repaired\" holds the program that ran and each repair." },
"mitigate": wai_quantum::engine::mitigate::schema(),
@ -124,6 +126,14 @@ fn job_schema() -> Value {
})
}
/// What a job's `fabric` takes: where the statevector method's state runs.
pub(crate) fn fabric_schema() -> Value {
json!({
"description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (the first certified accelerator member that holds the state, the next taking over if it fails during the run), a member's id or key (list_fabric), or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's \"fabric\" names where the state ran, sealed by the receipt. A placement this host cannot honour is refused (J013).",
"anyOf": [{ "type": "string", "minLength": 1 }, { "type": "array", "minItems": 2, "maxItems": 64, "items": { "type": "string", "minLength": 1 } }]
})
}
/// What a job's `intent` takes: one intent or a list.
pub(crate) fn intent_schema() -> Value {
let one = json!({
@ -437,6 +447,12 @@ fn core_tools() -> Value {
"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": {} },
},
{
"name": "list_fabric",
"title": "List the fabric's members",
"description": "The members of the fabric a statevector job can be placed on here (a job's `fabric`): the CPU and each accelerator found and certified bit-exact against the CPU in this process, each with its key, its certificate and the most qubits it holds; the devices found and not placed, and why; and the backends this build reaches.",
"inputSchema": { "type": "object", "properties": {} },
},
{
"name": "qasm_diagnostics",
"title": "The reader's diagnostic codes",
@ -573,6 +589,7 @@ fn call(name: &str, args: &Value, memory_gb: u64, key: &SigningKey, client: &Cli
Ok(json!({ "valid": valid, "key": key }))
}
"list_methods" => Ok(json!({ "methods": catalog::describe(memory_gb << 30) })),
"list_fabric" => Ok(wai_quantum::engine::fabric::list(memory_gb << 30)),
"qasm_diagnostics" => match raw.get("code").and_then(Value::as_str) {
None => Ok(job::diagnostic_codes()),
Some(c) => job::diagnostic_code(c),
@ -905,7 +922,7 @@ mod tests {
let list = handle(&json!({ "jsonrpc": "2.0", "id": 2, "method": "tools/list" }), 1, &key(), &mut Client::default()).unwrap();
let names: Vec<&str> = list["result"]["tools"].as_array().unwrap().iter().map(|t| t["name"].as_str().unwrap()).collect();
#[allow(unused_mut)]
let mut want = vec!["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "repair_qasm", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "draw_circuit", "debug_step", "debug_branches", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "rewrite_qasm", "converge_cohort", "converge_history", "study_measure", "study_cohort", "taste_rank", "taste_next", "history_commit", "history_changes", "history_blame", "history_log", "list_methods", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"];
let mut want = vec!["run_quantum_circuit", "plan_quantum_circuit", "lint_qasm", "repair_qasm", "critique_qasm", "check_equivalent", "convert_program", "improve_qasm", "classical_shortcuts", "compile_qasm", "estimate_resources", "draw_circuit", "debug_step", "debug_branches", "verify_quantum_result", "discover_evaluate", "discover_submit", "discover_archive", "novelty_qasm", "discover_novelty", "rewrite_qasm", "converge_cohort", "converge_history", "study_measure", "study_cohort", "taste_rank", "taste_next", "history_commit", "history_changes", "history_blame", "history_log", "list_methods", "list_fabric", "qasm_diagnostics", "list_targets", "show_target", "run_on_target", "qec_code", "qec_decode", "qec_threshold", "pulse_schedule", "pulse_check", "gradient_qasm", "optimize_qasm", "vqe_ground_state", "qaoa_maxcut", "build_algorithm"];
#[cfg(feature = "providers")]
want.extend(["providers_list", "provider_estimate", "run_on_provider"]);
assert_eq!(names, want);
@ -1432,6 +1449,13 @@ mod tests {
call_tool("draw_circuit", json!({ "qasm": draft }), false, &mut called, &mut hold);
call_tool("verify_quantum_result", json!({ "job": job }), false, &mut called, &mut hold);
call_tool("list_methods", json!({}), false, &mut called, &mut hold);
call_tool("list_fabric", json!({}), false, &mut called, &mut hold);
// A state split over the CPU, as the fabric places it: the answers the
// CPU gives, and where it ran in the result; a member that is not here
// is refused.
let placed = call_tool("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.0;\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[3];\nrx(0.3) q[2];\nc = measure q;\n", "shots": 50, "fabric": ["cpu", "cpu"] }), false, &mut called, &mut hold);
assert_eq!(placed["result"]["fabric"]["partitions"].as_array().map(Vec::len), Some(2), "{placed}");
call_tool("run_quantum_circuit", json!({ "qasm": "OPENQASM 3.0;\nqubit[4] q;\nh q[0];\n", "shots": 5, "method": "statevector", "fabric": "a-member-elsewhere" }), true, &mut called, &mut hold);
call_tool("novelty_qasm", json!({ "qasm": star, "inputs": { "n": 4 }, "reference": bell }), false, &mut called, &mut hold);
for (op, extra) in [("options", json!({})), ("simplify", json!({})), ("explain", json!({ "other": "OPENQASM 3.0;\nqubit[2] q;\ncx q[0], q[1];\nt q[1];\n" }))] {
let mut a = json!({ "qasm": draft, "op": op });

View file

@ -372,6 +372,7 @@
"stabilizer exact; with Pauli noise, counts are noisy samples and expectations estimates\n",
"pauli bounded\n",
"mps exact when nothing is truncated, otherwise heuristic\n",
"stabilizer_rank exact; with terms or epsilon, an estimate with its bound\n",
"tensor_network exact\n",
"cutrank exact; single when only a single-precision table fits\n",
"cutrank_single single\n",

View file

@ -20,7 +20,7 @@ getrandom = "0.4"
blake3 = "=1.8.7"
# A job's status as server-sent events (src/http.rs): a stream built from its changes.
futures-util = { version = "=0.3.34", default-features = false }
wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt", "quantum_qec", "quantum_noise", "quantum_sv_metal", "quantum_cutrank_metal"] }
wai-quantum = { path = "../wai-quantum", default-features = false, features = ["quantum_engine", "quantum_receipt", "quantum_qec", "quantum_noise", "quantum_fabric", "quantum_sv_metal", "quantum_cutrank_metal"] }
# proc_pid_rusage and waitid, for each job's energy (src/energy.rs).
[target.'cfg(target_os = "macos")'.dependencies]

View file

@ -85,6 +85,7 @@ curl -s https://wai.transaction.science/quantum-jobs/v1/run -H 'content-type: ap
| `tensor_network` | any circuit | amplitudes | exact |
| `cutrank` | a Clifford circuit, then one layer of one-qubit gates at any angles, up to 4096 qubits, as wide as the state's cut-rank allows | counts, amplitudes | exact; single when only a single-precision table fits |
| `cutrank_single` | as `cutrank`, with the table on the host's GPU, where it has one; only when asked for | counts, amplitudes | single |
| `stabilizer_rank` | Clifford+T circuits, up to 32768 qubits, as many T gates as one job's sum of `2^t` (an expectation value's `4^t`) branches allows | amplitudes, expectations | exact; with `terms` or `epsilon`, an estimate with its bound and the probability it holds |
| `density_matrix` | noisy circuits, up to 16 qubits within a job's memory | counts, expectations | exact |
| `trajectories` | noisy circuits, up to 32 qubits | counts, expectations | estimate |
@ -121,7 +122,7 @@ curl -s https://wai.transaction.science/quantum-jobs/v1/run -H 'content-type: ap
"observables": ["Z2", "X2"], "shots": 1000 }'
```
`auto` picks the first method that takes the job. With noise, it tries `density_matrix`, then `stabilizer`, then `trajectories`. For counts, it tries `stabilizer`, `exact`, `statevector`, `statevector_single`, `cutrank`, then `mps`. For expectations alone, it tries `stabilizer`, `exact`, `statevector`, `statevector_single`, `pauli`, then `mps`. For amplitudes alone, it tries `exact`, `statevector`, `statevector_single`, `cutrank`, `tensor_network`, then `mps`. The job's `why` names the method chosen and the ones that could not take it.
`auto` picks the first method that takes the job. With noise, it tries `density_matrix`, then `stabilizer`, then `trajectories`. For counts, it tries `stabilizer`, `exact`, `statevector`, `statevector_single`, `cutrank`, then `mps`. For expectations alone, it tries `stabilizer`, `exact`, `statevector`, `statevector_single`, `stabilizer_rank`, `pauli`, then `mps`. For amplitudes alone, it tries `exact`, `statevector`, `statevector_single`, `cutrank`, `stabilizer_rank`, `tensor_network`, then `mps`. The job's `why` names the method chosen and the ones that could not take it.
The accuracy classes:

View file

@ -115,7 +115,7 @@ pub fn openapi(server: &str) -> Value {
"required": ["qasm"],
"properties": {
"qasm": { "type": "string", "description": "The program: OpenQASM 3, or OpenQASM 2. Registers, standard gates (h, x, y, z, s, sdg, t, tdg, sx, rx, ry, rz, p, u, cx, cy, cz, ch, swap, iswap, cp, crx, cry, crz, ccx, ccz, cswap, rxx, ryy, rzz), the control modifiers ctrl @, ctrl(n) @ and negctrl @ on x y z h s sdg t tdg p and the controlled names, user gate definitions, measurement anywhere, reset anywhere, and `if (c[0]) …` / `if (c == 3) { … } else { … }` on measured bits (OpenQASM 2's `if(c==n) gate;` too). A program that measures in the middle, resets after a gate or acts on measured bits runs on statevector or stabilizer: counts split their shots at each measurement, expectation values are the exact mixture over the branches, of the state before the final measurements. Loops, inv @ and pow @ are refused, with the line." },
"method": { "type": "string", "enum": ["auto", "exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "tensor_network", "cutrank", "cutrank_single", "density_matrix", "trajectories", "pulse"], "default": "auto", "description": "pulse: a pulse program (OpenPulse calibrations on physical qubits) integrated under its pulse options, its error bounded (POST /v1/pulse/schedule draws its schedule)." },
"method": { "type": "string", "enum": ["auto", "exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "tensor_network", "cutrank", "cutrank_single", "stabilizer_rank", "density_matrix", "trajectories", "pulse"], "default": "auto", "description": "pulse: a pulse program (OpenPulse calibrations on physical qubits) integrated under its pulse options, its error bounded (POST /v1/pulse/schedule draws its schedule)." },
"shots": { "type": "integer", "minimum": 0, "maximum": 1000000, "description": "Measurement shots (default 1024 when nothing else is asked for)." },
"seed": { "type": "integer", "default": 1, "description": "The sampling seed: the same job and seed give the same counts." },
"observables": { "type": "array", "items": pauli },

View file

@ -223,11 +223,12 @@ fn tool_list() -> Value {
"noise": { "type": "object", "description": "Optional noise, each a probability: depolarizing_1q, depolarizing_2q, amplitude_damping, dephasing (after each gate on its qubits), readout (each measured bit).", "properties": {
"depolarizing_1q": { "type": "number" }, "depolarizing_2q": { "type": "number" }, "amplitude_damping": { "type": "number" }, "dephasing": { "type": "number" }, "readout": { "type": "number" }
} },
"method": { "type": "string", "enum": ["auto", "exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "tensor_network", "cutrank", "cutrank_single", "density_matrix", "trajectories", "pulse"], "description": "Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded." },
"method": { "type": "string", "enum": ["auto", "exact", "statevector", "statevector_single", "stabilizer", "pauli", "mps", "tensor_network", "cutrank", "cutrank_single", "stabilizer_rank", "density_matrix", "trajectories", "pulse"], "description": "Leave as auto unless you need a particular method; pulse runs a pulse program (OpenPulse calibrations on physical qubits, see pulse_schedule) under the pulse options, its error bounded." },
"seed": { "type": "integer", "description": "Sampling seed (default 1); the same job and seed give the same counts." },
"options": { "type": "object", "description": "Advanced, per method: max_bond (mps), threshold and max_terms (pauli), fusion (statevector), trajectories." },
"compile": { "type": "object", "description": "Compile the program first (as compile_qasm does) and run the compiled circuit: with basis clifford_t it runs bit-exact, and the result's `compiled` states its distance from the program.", "properties": compile_props() },
"target": { "description": "A device target: a built-in target's name (list_targets) or a target document (wai.quantum.target/1). The program is then compiled to its gates, placed where it is most likely to succeed and simulated under its noise, its hash in the receipt; not with noise or amplitudes. The service reads the document given and fetches nothing." },
"fabric": { "description": "Where the statevector method's state runs: auto (the default: on the CPU, until a member is measured faster than the CPU), cpu, accelerator (any certified accelerator member), a member's id or key, or a list of members, a power of two of them, the state split by its top qubits into one partition on each. Every member placed is certified bit-exact against the CPU, so the answers are the CPU's; the result's `fabric` names where the state ran, sealed by the receipt. A placement the service cannot honour is refused (J013): it places only on its CPU.", "anyOf": [{ "type": "string", "minLength": 1 }, { "type": "array", "minItems": 2, "maxItems": 64, "items": { "type": "string", "minLength": 1 } }] },
"intent": intent_schema(),
"repair": { "description": "Repair the program first, on the lines its diagnostics flag alone (as repair_qasm does): true, or the choices {\"max_iterations\": N, \"dyadic_bits\": K, \"sizes\": {\"q\": 3}}. The result's `repaired` holds the program that ran and each repair." },
"mitigate": wai_quantum::engine::mitigate::schema(),

View file

@ -297,6 +297,10 @@ required-features = ["quantum_fabric_opencl"]
name = "opencl_sv"
required-features = ["quantum_fabric_opencl", "quantum_sv"]
[[test]]
name = "fabric_placement"
required-features = ["quantum_engine", "quantum_fabric"]
[[test]]
name = "fpga_member"
required-features = ["quantum_fabric_fpga"]

View file

@ -531,6 +531,40 @@ rotations, CX, CZ, iSWAP, fSim, …) for it and for the tensor networks. A
26-qubit, depth-20 random circuit runs in about nine seconds on one machine,
and the bits do not depend on how many threads ran it.
### Where the state runs
```sh runnable
wai-quantum fabric # the members a state vector can be placed on here
wai-quantum run circuit.qasm --method statevector --fabric cpu --fabric cpu --fabric cpu --fabric cpu --shots 100 --json > split.json
```
A job's `fabric` places the `statevector` method's state on the fabric's
members (`engine::fabric`). With the CUDA, HIP or OpenCL member compiled in
(`quantum_fabric_cuda`, `quantum_fabric_hip`, `quantum_fabric_opencl`, with
`quantum_engine`), each device this process finds runs the dense f64 state
vector (`quantum_fabric::sv64`) and is certified against the CPU on the pinned
corpus; only a member certified bit-exact is placed, so a job placed on one
gives the CPU's answers byte for byte. `auto`, the default, places the state on
the CPU, on every host, until a member is measured faster than the CPU for a
job; a member runs a job only when the job asks for one. `accelerator` places
it on the first certified member that holds it, GPUs first, the next taking
over if one fails during the run; `cpu` or a member's id or key place it
there; and a list of members, a power of two of them, splits the
state by its top qubits into one partition on each. Each partition runs the
same launches the CPU plans on its own amplitudes, and a gate on a partition
qubit first has that qubit exchanged with a local one through the host's copy
of the state (`quantum_fabric::sv64::run_split`), so the joined state is the
CPU's, bit for bit. The result's `fabric` names the member, or each
partition's, with its certificate's parity tier, digest and corpus, and the
receipt seals it; with no member asked for and none placed a result is the one
it always was. Each member states the most qubits it holds, and a state or a
partition past that is refused, as is any placement the host cannot honour
(`J013`). Measured on the fleet's GPUs (a laptop GPU and an integrated one,
each beside a CPU), with one launch per gate and the state copied to the
device and back, a whole job took about as long either way, or longer on the
GPU, between 18 and 22 qubits: the copies cost what the passes save. That is
why `auto` stays on the CPU.
## Tensor networks
An amplitude of a circuit is a tensor network, and the order it is contracted
@ -727,6 +761,56 @@ and one exact sample 1.1 to 1.5 s, with a largest table of 4 GiB in single
precision. Amplitudes agree between orders and between precisions to about
10⁻⁷ relative.
## Clifford+T circuits as sums of stabilizer states
```sh runnable
cat > ct.qasm <<'QASM'
OPENQASM 3.0;
qubit[40] q;
h q;
t q[0];
cx q[0], q[1];
tdg q[1];
cx q[1], q[39];
t q[39];
h q;
QASM
wai-quantum run ct.qasm -a 0000000000000000000000000000000000000000 -o "Z0 Z1"
wai-quantum run ct.qasm --method stabilizer_rank -a 0000000000000000000000000000000000000000 --option terms=2000 --json > ct-sampled.json
```
A T gate is a sum of two Clifford gates of equal weight,
`T = (e^{iπ/8} I + e^{−iπ/8} S) / (2 cos π/8)`, so a circuit with `t` T and
T† gates is a sum of `2^t` stabilizer states. `quantum_stabrank` walks every
branch on the phased tableau, which gives each amplitude of a stabilizer state
exactly (`PhasedTableau::amplitudes`, from one reduction of the stabilizers),
and counts every term, a power of `1/√2` at a whole number of sixteenths of a
turn, as a whole number, so the sum is the same however the branches are split
between threads and is evaluated once in double precision. An expectation
value `⟨ψ|P|ψ⟩` is the amplitude `⟨0|U†PU|0⟩`, a sum over twice the T gates.
The job engine's `stabilizer_rank` method lowers a program's `t`, `tdg`, and
`p`, `rz`, `rx` and `ry` at odd multiples of π/4, `cp` at odd multiples of π/2,
`ccz` and `ccx` to T gates, and every Clifford gate as the tableau's, with the
global phase kept exactly; `auto` takes it for amplitudes or expectation values
past the dense state vector when its branches fit one job's work, so it is
chosen by the T count.
The sampled mode (`--option terms=N`, or `--option epsilon=E` for as many terms
as that bound needs, at `--option confidence=C`, default 0.95) draws `N`
branches uniformly, seeded, each from a stream of its own: an unbiased estimate
of the state whose mean squared distance from it is `(sec²(π/8)^t − 1) / N`. So
each amplitude, and each expectation value through its own sum, is within
`sqrt((sec²(π/8)^t − 1) / (N (1 − C)))` of the exact one with probability at
least `C`. The result's class is `estimate`, with that bound and its
confidence beside each value: computed from the T count, the terms and the
confidence, never from the result.
Both modes are held to the dense state vector: every amplitude of random
Clifford+T circuits up to 7 qubits and 10 T gates, phase and all, within
`10⁻¹²`, the fidelity within `10⁻¹²` of 1, every expectation value likewise;
and the sampled bound holds in at least the fraction it states, measured on the
whole state.
## Gaussian boson sampling
```sh runnable

View file

@ -343,6 +343,15 @@ wai-quantum lab sv --file circuit.jsonl --sample 1000 --seed 7
`quantum_sv` 以双精度在最多 32 个量子比特上运行任意角度的任意门。`gates` 按名称为它和张量网络提供标准门(H、S、T、√X、√Y、√W、旋转门、CX、CZ、iSWAP、fSim……)。一条 26 量子比特、深度 20 的随机线路在一台机器上约九秒跑完,而且结果的每一位都与运行它的线程数无关。
### 态在哪里运行
```sh runnable
wai-quantum fabric # 本机上态矢量可放置其上的成员
wai-quantum run circuit.qasm --method statevector --fabric cpu --fabric cpu --fabric cpu --fabric cpu --shots 100 --json > split.json
```
作业的 `fabric` 把 `statevector` 方法的态放到计算结构的成员上(`engine::fabric`)。编译进 CUDA、HIP 或 OpenCL 成员(`quantum_fabric_cuda`、`quantum_fabric_hip`、`quantum_fabric_opencl`,配合 `quantum_engine`)后,本进程找到的每个设备都运行稠密 f64 态矢量(`quantum_fabric::sv64`),并在固定语料上对照 CPU 认证;只有认证为逐位一致的成员才会被放置,因此放到其上的作业给出与 CPU 逐字节相同的答案。默认的 `auto` 在每台主机上都把态放在 CPU 上,直到测得某个成员对作业比 CPU 更快;只有作业请求成员时,成员才运行它。`accelerator` 把它放到第一个能容纳它的已认证成员上(GPU 优先),若该成员在运行中失败,由下一个接替;`cpu` 或成员的 id 或键把它放到那里;成员列表(数量为 2 的幂)按最高位量子比特把态划分为若干分区,每个成员一个分区。每个分区在自己的振幅上运行 CPU 规划的同一组启动;作用于分区量子比特的门,先经主机上的态副本把该量子比特与一个本地量子比特交换(`quantum_fabric::sv64::run_split`),因此拼合后的态与 CPU 的逐位相同。结果的 `fabric` 指明成员(或每个分区的成员)及其证书的一致性等级、摘要和语料,并由收据封存;未请求成员、也未放置时,结果与以往相同。每个成员说明它能容纳的最多量子比特数,超过此数的态或分区会被拒绝,主机无法兑现的任何放置也会被拒绝(`J013`)。在本集群的 GPU(一块笔记本 GPU 和一块集成 GPU,各与一颗 CPU 相邻)上测得:每个门一次启动、态在设备与主机之间往返拷贝时,在 18 到 22 个量子比特之间,整个作业两种方式耗时相近,或在 GPU 上更长,拷贝的开销与节省的遍历相当。这就是 `auto` 留在 CPU 上的原因。
## 张量网络
线路的一个振幅就是一个张量网络,而缩并的顺序决定了它的代价是 `2³⁰` 次运算还是 `2⁶⁰` 次。`quantum_tn` 搜索这一顺序,将其切片以适应内存,然后进行缩并:
@ -428,6 +437,30 @@ CUTRANK_ORDER=order.txt CUTRANK_PRECISION=single \
一条 70 量子比特的图态采样线路,主体是最近邻的 Clifford 门,随后在每个量子比特上使用非 Clifford 基。采用退火得到的顺序时,每个切分都不超过 29 比特。在一台工作站上,一个振幅约需半秒,一个精确样本需 1.1 到 1.5 秒,单精度下最大的表为 4 GiB。不同顺序之间、不同精度之间,振幅的相对吻合程度约为 10⁻⁷。
## 写成稳定子态之和的 Clifford+T 线路
```sh runnable
cat > ct.qasm <<'QASM'
OPENQASM 3.0;
qubit[40] q;
h q;
t q[0];
cx q[0], q[1];
tdg q[1];
cx q[1], q[39];
t q[39];
h q;
QASM
wai-quantum run ct.qasm -a 0000000000000000000000000000000000000000 -o "Z0 Z1"
wai-quantum run ct.qasm --method stabilizer_rank -a 0000000000000000000000000000000000000000 --option terms=2000 --json > ct-sampled.json
```
一个 T 门是两个等权 Clifford 门之和,`T = (e^{iπ/8} I + e^{−iπ/8} S) / (2 cos π/8)`,因此含 `t` 个 T 和 T† 门的线路是 `2^t` 个稳定子态之和。`quantum_stabrank` 在带相位的稳定子表上遍历每个分支,表给出稳定子态每个振幅的精确值(`PhasedTableau::amplitudes`,只需对稳定子做一次约化),并把每一项(`1/√2` 的幂乘以整数个十六分之一圈的相位)作为整数计数,因此无论分支如何在线程间划分,和都相同,最后以双精度求值一次。期望值 `⟨ψ|P|ψ⟩` 是振幅 `⟨0|U†PU|0⟩`,是对两倍 T 门的求和。作业引擎的 `stabilizer_rank` 方法把程序中的 `t`、`tdg`,π/4 奇数倍的 `p`、`rz`、`rx`、`ry`,π/2 奇数倍的 `cp`,以及 `ccz` 和 `ccx` 降为 T 门,每个 Clifford 门按稳定子表的方式降低,并精确保留全局相位;当分支在一个作业的工作量之内时,`auto` 会在稠密态矢量之外为振幅或期望值选用它,因此按 T 门数来选择。
抽样模式(`--option terms=N`,或 `--option epsilon=E` 取达到该界所需的项数,置信度 `--option confidence=C`,默认 0.95)以种子均匀抽取 `N` 个分支,每个来自各自的流:这是态的无偏估计,与态的均方距离为 `(sec²(π/8)^t − 1) / N`。因此每个振幅,以及经由各自求和的每个期望值,以至少 `C` 的概率落在精确值的 `sqrt((sec²(π/8)^t − 1) / (N (1 − C)))` 之内。结果的类别为 `estimate`,每个值旁给出该界及其置信度:由 T 门数、项数和置信度算出,从不依据结果。
两种模式都以稠密态矢量为准检验:最多 7 个量子比特、10 个 T 门的随机 Clifford+T 线路,每个振幅(含相位)误差在 `10⁻¹²` 之内,保真度与 1 相差在 `10⁻¹²` 之内,每个期望值同样如此;抽样界在至少其所述的比例中成立,在整个态上测量。
## 高斯玻色采样
```sh runnable

View file

@ -64,6 +64,7 @@ browser engine. A test holds this page to the code's table.
| `J010` | refused | A method that does not exist: the methods list names them. |
| `J011` | refused | The method named cannot take this job: a gate it does not hold, more qubits, memory or work than it takes here, noise it does not model, or an answer it does not give; the message says which, and `auto` picks a method that can. |
| `J012` | refused | No method takes this job: each method's reason is listed, so the job can be changed to suit one. |
| `J013` | refused | A placement on the fabric (`fabric`) this host cannot honour: no member by that name, no certified accelerator member here, a state or a partition larger than a member holds, a partition count that is not a power of two, or a method or program the fabric's members do not run (the message says which, and what is here). |
| `J020` | refused | An intent that does not read: not one of `distribution`, `equivalent` or `prepares`, a distribution whose outcomes are not the program's bits or whose probabilities do not sum to 1, or a reference program that does not read. |
| `J021` | refused | A proposed repair that changes a line no diagnostic flags, or adds lines away from one: rejected, and the program left as it was. |
| `J022` | refused | A repair's choices that do not read (`max_iterations`, `dyadic_bits`, `sizes`), or a proposal that is not a program's text. |

View file

@ -172,6 +172,24 @@
}
]
},
"fabric": {
"description": "Where the statevector method's state runs: auto (the default: on the CPU, on every host, until a member of the fabric is measured faster than the CPU), cpu, accelerator (the first accelerator member certified bit-exact here that holds the state, falling back to the next if it fails during the run), a member's id or key (wai-quantum fabric lists them), or a list of members, a power of two of them, the state split by its top qubits into one partition on each (cpu is a member, and a member may be named more than once). A member certified bit-exact gives the CPU's bits. A placement this host cannot honour is refused (J013), never made elsewhere.",
"anyOf": [
{
"type": "string",
"minLength": 1
},
{
"type": "array",
"minItems": 2,
"maxItems": 64,
"items": {
"type": "string",
"minLength": 1
}
}
]
},
"repair": {
"description": "Repair the program first on its flagged lines: true, or the choices.",
"anyOf": [

View file

@ -332,6 +332,9 @@
}
}
},
"fabric": {
"$ref": "#/$defs/fabric"
},
"repaired": {
"type": "object",
"description": "A job asked to repair its program first: the program that ran, each repair, and what needs a person.",
@ -707,7 +710,8 @@
},
"bound": {
"type": "number",
"description": "bounded (a pulse run): every probability within this of the exact solution of the model's master equation, the truncation bound and the rounding estimate together."
"description": "The bound this result is held to, by method: a pulse run (class bounded): every probability within this of the exact solution of the model's master equation, the truncation bound and the rounding estimate together; stabilizer_rank's sampled mode (class estimate): the bound on the distance of the estimated state from the state, holding with the probability confidence states.",
"minimum": 0
},
"truncation_bound": {
"type": "number",
@ -722,6 +726,20 @@
},
"norm": {
"type": "string"
},
"t_count": {
"type": "integer",
"minimum": 0,
"description": "stabilizer_rank: the T and T† gates the program lowers to."
},
"terms": {
"type": "integer",
"minimum": 1,
"description": "stabilizer_rank: the stabilizer branches summed (2^t exactly), or drawn."
},
"confidence": {
"type": "number",
"description": "The probability a stated bound holds."
}
}
},
@ -749,7 +767,7 @@
},
"bound": {
"type": "number",
"description": "A rigorous bound on |exact − value|, for the bounded class."
"description": "A bound on |exact − value|: rigorous for the bounded class; with confidence beside it, holding with that probability."
},
"peak_terms": {
"type": "integer"
@ -788,6 +806,10 @@
}
}
}
},
"confidence": {
"type": "number",
"description": "The probability the bound holds, for a bound that holds with a stated probability (stabilizer_rank's sampled mode)."
}
}
},
@ -808,6 +830,15 @@
},
"im": {
"type": "number"
},
"bound": {
"type": "number",
"minimum": 0,
"description": "A bound on |exact − value| of the amplitude, holding with the probability confidence states (stabilizer_rank's sampled mode)."
},
"confidence": {
"type": "number",
"description": "The probability the bound holds."
}
}
},
@ -1020,6 +1051,176 @@
"description": "The noise model derived from the target's calibration: its channels, durations and idling, and each qubit's readout."
}
}
},
"fabric_parity": {
"type": "object",
"description": "A certificate's standing against the CPU reference: bit-exact, within a bound (max_ulp, max_abs), statistical (fidelity over shots) or uncertified. Only bit-exact members are placed.",
"required": [
"tier"
],
"properties": {
"tier": {
"enum": [
"bit-exact",
"within",
"statistical",
"uncertified"
]
},
"max_ulp": {
"type": "integer",
"minimum": 0
},
"max_abs": {
"type": "number"
},
"fidelity": {
"type": "number"
},
"shots": {
"type": "integer",
"minimum": 0
}
}
},
"fabric_member": {
"type": "object",
"description": "A member of the fabric and its certificate for the kernel: its id (which binds the device), its key, its class, the implementation, the parity tier, the certificate's digest (BLAKE3, hex), and the corpus it was earned on (its digest, generator and cases; the CPU reference's own standing has none).",
"required": [
"member",
"implementation",
"parity",
"certificate"
],
"properties": {
"member": {
"type": "string"
},
"key": {
"type": "string"
},
"class": {
"type": "string"
},
"implementation": {
"type": "string"
},
"parity": {
"$ref": "#/$defs/fabric_parity"
},
"certificate": {
"type": "string"
},
"corpus": {
"type": "string"
},
"generator": {
"type": "string"
},
"cases": {
"type": "integer",
"minimum": 0
},
"why": {
"type": "string",
"description": "For a member the job fell back from: how it failed."
}
}
},
"fabric": {
"type": "object",
"description": "Where the state vector ran: the kernel, the placement the job asked for, the member that ran it (or each partition's), each with its certificate, and the energy. Present when the job asked for a placement (auto places on the CPU and names nothing); the receipt seals it with the rest of the result. Every member placed is certified bit-exact, so the answers are the CPU's, byte for byte.",
"required": [
"kernel",
"asked",
"parity",
"energy"
],
"properties": {
"kernel": {
"type": "string"
},
"asked": {
"description": "The placement the job asked for, as it gave it.",
"anyOf": [
{
"type": "string"
},
{
"type": "array",
"items": {
"type": "string"
}
}
]
},
"member": {
"type": "string"
},
"key": {
"type": "string"
},
"class": {
"type": "string"
},
"implementation": {
"type": "string"
},
"parity": {
"$ref": "#/$defs/fabric_parity"
},
"certificate": {
"type": "string"
},
"corpus": {
"type": "string"
},
"generator": {
"type": "string"
},
"cases": {
"type": "integer",
"minimum": 0
},
"partitions": {
"type": "array",
"description": "Each partition's member, by the value of the state's top qubits: partition i holds the amplitudes whose top bits read i.",
"items": {
"$ref": "#/$defs/fabric_member"
}
},
"partition_qubits": {
"type": "integer",
"minimum": 0
},
"local_qubits": {
"type": "integer",
"minimum": 0
},
"segments": {
"type": "integer",
"minimum": 0,
"description": "Runs of gates the partitions took in turn, between exchanges."
},
"exchanges": {
"type": "integer",
"minimum": 0,
"description": "Partition qubits exchanged with local ones through the host's copy of the state."
},
"fell_back_from": {
"type": "array",
"description": "An accelerator request whose member failed during the run: each member that failed, in turn, with how; the member named above ran the job.",
"items": {
"$ref": "#/$defs/fabric_member"
}
},
"energy": {
"$ref": "#/$defs/energy"
},
"why": {
"type": "string"
}
}
}
}
}

View file

@ -41,7 +41,7 @@
},
"computer": {
"type": "object",
"description": "What ran it: a classical simulator, the method and its accuracy class, and a device target's hash when the job named one.",
"description": "What ran it: a classical simulator, the method and its accuracy class, a device target's hash when the job named one, and the fabric's members when the result names where its state ran.",
"required": [
"kind",
"method"
@ -62,6 +62,21 @@
"target": {
"type": "string",
"description": "The device target's hash; its layout, routes and noise are in the result's own target."
},
"fabric": {
"type": "object",
"description": "The kernel and the member that ran the state, or each partition's, as the result's fabric names them.",
"properties": {
"kernel": {
"type": "string"
},
"members": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
},

View file

@ -2227,6 +2227,123 @@
"description": "The output read back and held to the program read (or, for a program read into OpenQASM, written back to its format and read again), or null when no check was asked."
}
}
},
"fabric_members": {
"type": "object",
"description": "The members of the fabric a statevector job can be placed on here: the CPU reference and each accelerator certified bit-exact in this process, each with the most qubits it holds; the devices found and not placed, with why; why each backend the build reaches found none; and the backends it reaches.",
"required": [
"kernel",
"members"
],
"properties": {
"kernel": {
"type": "string"
},
"auto": {
"type": "string",
"description": "Where auto places a state: on the CPU, until a member is measured faster than the CPU."
},
"members": {
"type": "array",
"items": {
"type": "object",
"required": [
"member",
"implementation",
"parity"
],
"properties": {
"member": {
"type": "string"
},
"key": {
"type": "string"
},
"class": {
"type": "string"
},
"implementation": {
"type": "string"
},
"certificate": {
"type": "string"
},
"corpus": {
"type": "string"
},
"generator": {
"type": "string"
},
"parity": {
"$ref": "job-result.v1.json#/$defs/fabric_parity"
},
"cases": {
"type": "integer",
"minimum": 0
},
"detail": {
"type": "object",
"description": "The most qubits it holds and what bounds it; for an accelerator, its backend and what discovery found.",
"properties": {
"max_qubits": {
"type": "integer",
"minimum": 0
},
"memory": {
"type": "string"
},
"backend": {
"type": "string"
},
"device": {
"type": "string"
}
}
}
}
}
},
"refused": {
"type": "array",
"items": {
"type": "object",
"properties": {
"backend": {
"type": "string"
},
"member": {
"type": "string"
},
"key": {
"type": "string"
},
"why": {
"type": "string"
}
}
}
},
"unavailable": {
"type": "array",
"items": {
"type": "object",
"properties": {
"backend": {
"type": "string"
},
"why": {
"type": "string"
}
}
}
},
"backends": {
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
}

View file

@ -13,6 +13,7 @@
//! | `stabilizer` | Clifford, ≤ 32768 qubits, optionally with Pauli noise | counts, expectations | exact; an estimate with noise |
//! | `pauli` | any, ≤ 1024 qubits | expectations | bounded |
//! | `mps` | any, ≤ 4096 qubits, best on low-entanglement circuits | counts, expectations, amplitudes | exact, or heuristic with its fidelity estimate |
//! | `stabilizer_rank` | Clifford+T, ≤ 32768 qubits, as many T gates as its terms allow | amplitudes, expectations | exact, or an estimate with its bound |
//! | `tensor_network` | any | amplitudes | exact |
//! | `cutrank` | Clifford, then one layer of one-qubit gates at any angles, ≤ 4096 qubits, as wide as its cut-rank allows | counts, amplitudes | exact, or single |
//! | `cutrank_single` | as `cutrank`, the table on a GPU (never picked by `auto`) | counts, amplitudes | single |
@ -22,9 +23,11 @@
//! `auto` picks, in order: with noise, `density_matrix`, `stabilizer`, then `trajectories`;
//! for counts, `stabilizer`, `exact`, `statevector`, `statevector_single`,
//! `cutrank`, `mps`; for expectations only, `stabilizer`, `exact`, `statevector`,
//! `statevector_single`, `pauli`, `mps`; for amplitudes only, `exact`,
//! `statevector`, `statevector_single`, `cutrank`, `tensor_network`, `mps`. A method that cannot take the circuit is
//! skipped, and the plan says why the one chosen was chosen.
//! `statevector_single`, `stabilizer_rank`, `pauli`, `mps`; for amplitudes only, `exact`,
//! `statevector`, `statevector_single`, `cutrank`, `stabilizer_rank`, `tensor_network`, `mps`. A method that cannot take the circuit is
//! skipped, and the plan says why the one chosen was chosen. `stabilizer_rank`
//! is chosen by its T count: `auto` takes its exact sum when its terms (`2^t`
//! for amplitudes, `4^t` for an expectation value) are within one job's work.
//!
//! A job with `"compile": {"epsilon", "level", "basis"}` runs its program
//! compiled first ([`super::compile`]): with the basis `clifford_t` every
@ -70,6 +73,15 @@ pub const CUTRANK_MAX_QUBITS: u32 = 4096;
pub const CUTRANK_MAX_CUT: usize = 34;
/// The table entries a `cutrank` job's passes may touch: about ten minutes.
pub const CUTRANK_MAX_WORK: u64 = 4_000_000_000_000;
/// The most qubits `stabilizer_rank` takes: a tableau's.
pub const STABILIZER_RANK_MAX_QUBITS: u32 = crate::quantum_tableau::MAX_QUBITS;
/// The tableau word operations a `stabilizer_rank` job may take, in the unit
/// [`stabilizer_rank_work`] counts: about a minute on a 16-core host, which
/// walks about 4·10¹⁰ a second.
pub const STABILIZER_RANK_MAX_WORK: u64 = 2_000_000_000_000;
/// The most branches the sampled mode draws.
pub const STABILIZER_RANK_MAX_TERMS: u64 = 100_000_000;
/// The word operations spent searching for an order of the qubits with
/// narrower cuts: the planner's one search, about a second, and each of the
/// run's searches.
@ -102,7 +114,8 @@ pub struct Plan {
/// host that has timed none of its own jobs of the method: the planner's own
/// budgets over the times they are stated to take (a stabilizer job's
/// [`STABILIZER_MAX_WORK`] about a minute, an `mps` job's [`MPS_MAX_WORK`]
/// and a `cutrank` job's [`CUTRANK_MAX_WORK`] about ten minutes, a density
/// and a `cutrank` job's [`CUTRANK_MAX_WORK`] about ten minutes, a
/// `stabilizer_rank` job's [`STABILIZER_RANK_MAX_WORK`] about one, a density
/// matrix's [`DENSITY_MAX_WORK`] about three), and for the state vectors
/// 10⁹ amplitude updates a second in double precision, 2·10⁸ in fixed point
/// and 10¹⁰ on a GPU. None where the work the planner counts does not
@ -117,6 +130,7 @@ pub fn nominal_rate(method: &str) -> Option<f64> {
"stabilizer" => Some(STABILIZER_MAX_WORK as f64 / 60.0),
"mps" => Some(MPS_MAX_WORK as f64 / 600.0),
"cutrank" | "cutrank_single" => Some(CUTRANK_MAX_WORK as f64 / 600.0),
"stabilizer_rank" => Some(STABILIZER_RANK_MAX_WORK as f64 / 60.0),
"density_matrix" => Some(DENSITY_MAX_WORK as f64 / 180.0),
"pulse" => Some(1e9),
_ => None,
@ -129,7 +143,7 @@ pub fn accuracy_classes() -> Value {
"bit-exact": "the same bits on every machine: amplitudes in 30-bit fixed point, every output an exact function of them; counts are seeded samples",
"exact": "no approximation beyond IEEE double-precision rounding; counts are seeded samples of the exact distribution",
"bounded": "an approximation with a rigorous bound on |exact − value| reported beside each value",
"estimate": "a statistical estimate, reported with its standard error",
"estimate": "a statistical estimate, reported with its standard error, or with a bound on its error and the probability that the bound holds",
"heuristic": "an approximation reported with the standard estimate of its error (a matrix product state's fidelity estimate), not a bound",
})
}
@ -144,6 +158,7 @@ pub fn describe(job_bytes: u64) -> Value {
{ "method": "stabilizer", "accuracy": "exact; with Pauli noise, counts are noisy samples and expectations estimates", "circuits": "Clifford (rotations by whole quarter turns, controlled phases by whole half turns), optionally with depolarizing, dephasing and readout noise", "max_qubits": STABILIZER_MAX_QUBITS, "answers": ["counts", "expectations"], "options": { "trajectories": "with noise: the shots expectation values are estimated from (default: the shots, at least 10000)" } },
{ "method": "pauli", "accuracy": "bounded", "circuits": "any", "max_qubits": PAULI_MAX_QUBITS, "answers": ["expectations"], "options": { "threshold": "coefficients below it are dropped (default 1e-5); the bound covers what was dropped", "max_weight": "Pauli strings heavier than this are dropped", "ladder": "also report the value at up to three thresholds a decade apart above it, to show it settling (default true)", "max_terms": "the most Pauli terms to hold (default: what the job's memory holds); past it the threshold rises, and the bound covers what drops" } },
{ "method": "mps", "accuracy": "exact when nothing is truncated, otherwise heuristic", "circuits": "any; best on low-entanglement circuits", "max_qubits": MPS_MAX_QUBITS, "answers": ["counts", "expectations", "amplitudes"], "options": { "max_bond": "the largest bond dimension kept (default 64, at most 256)", "cutoff": "singular values whose squared share is below it are dropped (default 1e-12)" } },
{ "method": "stabilizer_rank", "accuracy": "exact; with terms or epsilon, an estimate with its bound", "circuits": "Clifford+T (t, tdg, and p, rz, rx, ry at odd multiples of π/4, cp at odd multiples of π/2, ccz and ccx), each T gate doubling the sum's terms: the state as a sum of stabilizer states", "max_qubits": STABILIZER_RANK_MAX_QUBITS, "answers": ["amplitudes", "expectations"], "options": { "terms": "the sampled mode: this many branches drawn, seeded, the result an estimate with the bound on its distance from the state that holds with the confidence stated", "epsilon": "the sampled mode with as many branches as that bound needs", "confidence": "the probability the sampled bound holds (default 0.95)" } },
{ "method": "tensor_network", "accuracy": "exact", "circuits": "any", "max_qubits": qasm::MAX_QUBITS, "answers": ["amplitudes"], "options": { "path_restarts": "independent contraction-path searches, the cheapest kept (default 8; 1 in a browser tab)" } },
{ "method": "cutrank", "accuracy": "exact; single when only a single-precision table fits", "circuits": "a Clifford circuit, then one layer of one-qubit gates at any angles: each qubit's gates from its first non-Clifford one on act on that qubit alone; the cost is set by the state's cut-rank along an order of the qubits, not by their number", "max_qubits": CUTRANK_MAX_QUBITS, "answers": ["counts", "amplitudes"] },
{ "method": "cutrank_single", "accuracy": "single", "circuits": "as for cutrank; the table on a GPU, its arithmetic in single precision", "max_qubits": CUTRANK_MAX_QUBITS, "max_cut": gpu_cutrank_max_cut().unwrap_or(0), "available": gpu_cutrank_max_cut().is_ok(), "answers": ["counts", "amplitudes"] },
@ -700,6 +715,74 @@ fn try_dynamic(method: &str, r: &Request, clifford: &Result<Vec<crate::quantum_t
}
}
/// How a `stabilizer_rank` job sums its branches: every one, or `terms`
/// drawn, with the bound failing with probability at most `delta`.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct StabilizerRankMode {
pub sampled: Option<(u64, f64)>,
}
impl StabilizerRankMode {
fn terms_per_amplitude(&self, t: u32) -> u64 {
self.sampled.map_or(1u64.checked_shl(t).unwrap_or(u64::MAX), |(k, _)| k)
}
fn terms_per_expectation(&self, t: u32) -> u64 {
self.sampled.map_or(1u64.checked_shl(2 * t).unwrap_or(u64::MAX), |(k, _)| k)
}
fn terms_said(&self, t: u32, expectations: bool) -> String {
match self.sampled {
Some((k, _)) => format!("{k} branches drawn"),
None if expectations => format!("4^{t} branches for an expectation value"),
None => format!("2^{t} branches"),
}
}
}
/// The mode a `stabilizer_rank` job asks for: `terms` or `epsilon` (with
/// `confidence`, default 0.95) for the sampled mode, otherwise the exact sum.
pub fn stabilizer_rank_mode(r: &Request, t: u32) -> Result<StabilizerRankMode, String> {
let confidence = r.options.get("confidence").and_then(Value::as_f64).unwrap_or(0.95);
if !(confidence > 0.0 && confidence < 1.0) {
return Err(tag(refusal::BAD_FIELD, "options.confidence is a probability strictly between 0 and 1"));
}
let delta = 1.0 - confidence;
let terms = match (r.options.get("terms").and_then(Value::as_u64), r.options.get("epsilon").and_then(Value::as_f64)) {
(Some(_), Some(_)) => return Err(tag(refusal::BAD_FIELD, "give options.terms or options.epsilon, not both")),
(Some(k), None) => k,
(None, Some(e)) if e > 0.0 && e.is_finite() => {
let k = crate::quantum_stabrank::terms_for(t, e, delta);
if k > STABILIZER_RANK_MAX_TERMS as f64 {
return Err(format!("a bound of {e} at confidence {confidence} with {t} T gates needs {k} branches, more than {STABILIZER_RANK_MAX_TERMS}"));
}
k.max(1.0) as u64
}
(None, Some(_)) => return Err(tag(refusal::BAD_FIELD, "options.epsilon is a positive number")),
(None, None) => return Ok(StabilizerRankMode { sampled: None }),
};
if terms == 0 || terms > STABILIZER_RANK_MAX_TERMS {
return Err(tag(refusal::BAD_FIELD, format!("options.terms is 1 to {STABILIZER_RANK_MAX_TERMS}")));
}
Ok(StabilizerRankMode { sampled: Some((terms, delta)) })
}
/// A `stabilizer_rank` job's work, in tableau word operations: for each
/// branch, its gates (a pass over a qubit's run of rows each, a reduction for
/// each Hadamard) and the reduction that reads its amplitudes; amplitudes'
/// branches once, each expectation value's of its own.
pub fn stabilizer_rank_work(c: &crate::quantum_stabrank::CliffordT, amplitude_terms: u64, expectation_terms: u64, amplitudes: u64, observables: u64) -> u64 {
use crate::quantum_tableau::Clifford;
let nn = c.n as u64;
let words = nn.div_ceil(64).max(1);
let reduction = (nn * nn * words).max(1);
let hs = c.gates.iter().filter(|g| matches!(g, crate::quantum_stabrank::CtGate::Clifford(Clifford::H(_) | Clifford::SX(_) | Clifford::SXdg(_)))).count() as u64;
let per = (hs + 1).saturating_mul(reduction) + c.gates.len() as u64 * 2 * words + amplitudes * nn * words;
let a = if amplitudes > 0 { amplitude_terms.saturating_mul(per) } else { 0 };
let e = observables.saturating_mul(expectation_terms).saturating_mul(per.saturating_mul(2));
a.saturating_add(e)
}
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;
@ -849,6 +932,30 @@ fn try_method(method: &str, r: &Request, dyadic: &Result<crate::quantum::Circuit
}
Ok(plan(memory, work, format!("Clifford circuit then one layer of one-qubit gates on {n} qubits: summed by cut-rank on the GPU in single precision, the widest cut {cut} bits")))
}
"stabilizer_rank" => {
if counts {
return Err("stabilizer_rank answers amplitudes and expectation values".into());
}
let c = r.program.clifford_t().map_err(|e| format!("stabilizer_rank needs a Clifford+T circuit: {e}"))?;
if n > STABILIZER_RANK_MAX_QUBITS {
return Err(format!("stabilizer_rank holds at most {STABILIZER_RANK_MAX_QUBITS} qubits"));
}
let t = c.t_count();
let mode = stabilizer_rank_mode(r, t)?;
let work = stabilizer_rank_work(&c, mode.terms_per_amplitude(t), mode.terms_per_expectation(t), r.amplitudes.len() as u64, r.observables.len() as u64);
if work > STABILIZER_RANK_MAX_WORK {
let hint = if mode.sampled.is_none() { "; give options.terms or options.epsilon for the sampled mode, an estimate with its bound" } else { "; ask for fewer terms" };
return Err(format!("{t} T gates: {} is more work than one job may take ({work} tableau word operations, at most {STABILIZER_RANK_MAX_WORK}){hint}", mode.terms_said(t, wants_expectations)));
}
// A tableau for each branch walked at once, about n² bits each.
let nn = n as u64;
let memory = 64 * MIB + 32 * (nn * nn / 2 + 4096) + (r.amplitudes.len() as u64 + 1) * 64 * nn;
let said = match mode.sampled {
None => format!("Clifford+T circuit with {t} T gates on {n} qubits: the exact sum of its stabilizer branches"),
Some((terms, delta)) => format!("Clifford+T circuit with {t} T gates on {n} qubits: {terms} of its stabilizer branches drawn, the bound holding with probability {}", 1.0 - delta),
};
Ok(plan(memory, work, said))
}
"tensor_network" => {
if !wants_amplitudes || counts || wants_expectations {
return Err("tensor_network answers amplitudes only".into());
@ -936,6 +1043,9 @@ pub fn plan(v: &Value, job_bytes: u64) -> Result<Plan, String> {
// the device would run it (engine::targets::job).
#[cfg(feature = "quantum_receipt")]
if crate::engine::targets::job::names_target(v) {
if !matches!(super::fabric::asked(v)?, super::fabric::Ask::Auto | super::fabric::Ask::Cpu) {
return Err(tag(refusal::FABRIC, "fabric: a job on a device target runs on the CPU, as the device's simulation; give fabric cpu, or leave it out"));
}
let p = crate::engine::targets::job::plan(v, job_bytes)?;
return with_intent(v, &read(v)?, p, job_bytes);
}
@ -944,10 +1054,24 @@ pub fn plan(v: &Value, job_bytes: u64) -> Result<Plan, String> {
return Err("this build has no device targets (the engine's quantum_receipt feature)".into());
}
let r = read(v)?;
let p = choose(&r, job_bytes)?;
let mut p = choose_placed(v, &r, job_bytes)?;
super::fabric::admit(v, &r, &mut p)?;
with_intent(v, &r, p, job_bytes)
}
/// [`choose`], except that a job asking for a member of the fabric
/// (`accelerator`, a member, or partitions) under `auto` takes the one method
/// the fabric's members run, `statevector`.
fn choose_placed(v: &Value, r: &Request, job_bytes: u64) -> Result<Plan, String> {
use super::fabric::Ask;
let ask = super::fabric::asked(v)?;
if r.method != "auto" || matches!(ask, Ask::Auto | Ask::Cpu) {
return choose(r, job_bytes);
}
let (dyadic, clifford) = (r.program.dyadic(), r.program.clifford());
try_method("statevector", r, &dyadic, &clifford, job_bytes).map_err(|e| tag(refusal::FABRIC, format!("fabric: only the statevector method is placed on the fabric's members, and it cannot take this job: {e}")))
}
/// `p` for the job `v`, read as `r`, admitting the memory its intents' check
/// takes beside the run, where it fits (super::verdict).
fn with_intent(v: &Value, r: &Request, mut p: Plan, job_bytes: u64) -> Result<Plan, String> {
@ -970,9 +1094,9 @@ fn choose(r: &Request, job_bytes: u64) -> Result<Plan, String> {
} else if r.shots > 0 {
&["stabilizer", "exact", "statevector", "statevector_single", "cutrank", "mps"]
} else if !r.observables.is_empty() && r.amplitudes.is_empty() {
&["stabilizer", "exact", "statevector", "statevector_single", "pauli", "mps"]
&["stabilizer", "exact", "statevector", "statevector_single", "stabilizer_rank", "pauli", "mps"]
} else {
&["exact", "statevector", "statevector_single", "cutrank", "tensor_network", "mps"]
&["exact", "statevector", "statevector_single", "cutrank", "stabilizer_rank", "tensor_network", "mps"]
};
let mut refusals = Vec::new();
for m in order {

View file

@ -0,0 +1,887 @@
//! Where a job's state vector runs: the members of the fabric
//! (`wai.quantum.fabric`) a `statevector` job may be placed on, the placement
//! a job asks for, and the fabric's receipt inside the job's result.
//!
//! - **Members.** The CPU reference, and each accelerator the build reaches
//! (the features `quantum_fabric_cuda`, `quantum_fabric_hip` and
//! `quantum_fabric_opencl`) that this process finds, compiles the dense f64
//! state vector for ([`crate::quantum_fabric::sv64`]) and certifies against
//! the CPU on the pinned corpus. Only a member certified bit-exact is
//! placed: it gives the CPU's bits, so a job placed on it has the answers it
//! would have had on the CPU, byte for byte. Discovery runs once per
//! process, the first time a job asks for a member.
//! - **Asking.** A job's `fabric` is `auto` (the default), `cpu`,
//! `accelerator` (any certified accelerator member), a member's id or key,
//! or a list of them: the state split by its top qubits into that many
//! partitions, a power of two, one on each member named (a member may be
//! named more than once, and `cpu` is a member). A placement this host
//! cannot honour is refused ([`super::refusal::FABRIC`]), never made
//! somewhere else.
//! - **Auto stays on the CPU** until a member is measured faster than the CPU
//! for a job. With one launch per gate and the state copied to the device
//! and back, a whole job measured about as fast, or slower, either way
//! between 18 and 22 qubits on the fleet's GPUs, so `auto` places nothing
//! and looks for no member: a job that does not ask runs as it always did,
//! on every host, and its result has no `fabric`. A member runs a job only
//! when the job asks for one.
//! - **Accelerator.** `accelerator` takes the first certified member that
//! holds the state, GPUs first. One that fails during the run hands the job
//! to the next that holds it, and the receipt lists each that failed
//! (`fell_back_from`); when every one fails the job is refused. A member
//! named by its id or key is that member alone.
//! - **Partitions** ([`crate::quantum_fabric::sv64::run_split`]). Each
//! partition runs the same launches the CPU plans, on its own amplitudes; a
//! gate on a partition qubit first has that qubit exchanged with a local one
//! through the host's copy of the state. The joined state is the CPU's, bit
//! for bit.
//! - **The receipt.** A result placed on a member, or one whose job asked for
//! a placement, carries `fabric`: the kernel, the member (or each
//! partition's), its certificate's parity tier, digest and corpus, and the
//! energy, unmetered with the reason. The job's receipt seals it with the
//! rest of the result.
//! - **Memory.** Each member states the most qubits it holds ([`list`]); a
//! state or a partition past that is refused.
use super::catalog::{Plan, Request};
use super::refusal::{self, tag};
use serde_json::{Value, json};
/// The kernel a placement runs: a gate list on a dense f64 state.
pub const KERNEL: &str = "wai.quantum.sv/run";
/// Where `auto` places a state: on the CPU, on every host, until a member
/// is measured faster than the CPU for a job.
pub const AUTO: &str = "cpu: auto places on the CPU until a member is measured faster than the CPU; a member runs a job that asks for one";
/// The most partitions a state may be split into.
pub const MAX_PARTITIONS: usize = 64;
/// The words `fabric` takes besides a member's id or key.
pub const WORDS: [&str; 3] = ["auto", "cpu", "accelerator"];
/// What a job's `fabric` asks for. A build without the fabric reads every
/// ask and honours only `auto`.
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(not(feature = "quantum_fabric"), allow(dead_code))]
pub(crate) enum Ask {
Auto,
Cpu,
Accelerator,
Member(String),
Split(Vec<String>),
}
#[cfg(feature = "quantum_fabric")]
impl Ask {
/// The ask as the job gave it, for the receipt.
fn json(&self) -> Value {
match self {
Ask::Auto => json!("auto"),
Ask::Cpu => json!("cpu"),
Ask::Accelerator => json!("accelerator"),
Ask::Member(m) => json!(m),
Ask::Split(v) => json!(v),
}
}
}
/// What the job's `fabric` asks, read and checked.
pub(crate) fn asked(v: &Value) -> Result<Ask, String> {
let bad = |m: &str| tag(refusal::BAD_FIELD, m);
const TAKES: &str = "\"fabric\" is auto, cpu, accelerator, a member's id or key (wai-quantum fabric lists them), or a list of members, one for each partition of the state";
match v.get("fabric") {
None | Some(Value::Null) => Ok(Ask::Auto),
Some(Value::String(s)) => Ok(match s.as_str() {
"auto" => Ask::Auto,
"cpu" => Ask::Cpu,
"accelerator" => Ask::Accelerator,
"" => return Err(bad(TAKES)),
id => Ask::Member(id.to_string()),
}),
Some(Value::Array(a)) => {
let names: Vec<String> = a.iter().map(|x| x.as_str().filter(|s| !s.is_empty() && !matches!(*s, "auto" | "accelerator")).map(str::to_string)).collect::<Option<_>>().ok_or_else(|| bad("\"fabric\" as a list names the member each partition runs on: cpu, or a member's id or key"))?;
if names.len() < 2 || names.len() > MAX_PARTITIONS || !names.len().is_power_of_two() {
return Err(tag(refusal::FABRIC, format!("fabric: {} partitions; a state splits into a power of two of them, 2 to {MAX_PARTITIONS}", names.len())));
}
Ok(Ask::Split(names))
}
Some(_) => Err(bad(TAKES)),
}
}
/// The fewest index bits a launch of `r`'s circuit may touch at once: its
/// widest gate, or the fusion width, whichever is wider.
fn widest(r: &Request) -> u32 {
let width = r.options.get("fusion").and_then(Value::as_u64).unwrap_or(3).clamp(1, 5) as u32;
let op = r.program.ops.iter().map(|o| o.qubits.len() as u32).max().unwrap_or(1);
if width >= 2 { op.max(width) } else { op }
}
/// Checks the placement `v` asks for against `plan`, and states it in the
/// plan's `why` when it is not the CPU by default. Refused, with
/// [`refusal::FABRIC`], when this host cannot honour it.
pub(crate) fn admit(v: &Value, r: &Request, plan: &mut Plan) -> Result<(), String> {
let ask = asked(v)?;
let placed_method = plan.method == "statevector" && r.program.dynamic.is_none();
if !placed_method {
return match ask {
Ask::Auto => Ok(()),
Ask::Cpu if matches!(plan.method.as_str(), "statevector_single" | "cutrank_single") => Err(tag(refusal::FABRIC, format!("fabric cpu: {} runs on the GPU in single precision; give another method, or leave fabric out", plan.method))),
Ask::Cpu => Ok(()),
_ if plan.method == "statevector" => Err(tag(refusal::FABRIC, "fabric: a program that measures in the middle, resets or acts on measured bits runs on the CPU; give fabric cpu, or leave it out")),
_ => Err(tag(refusal::FABRIC, format!("fabric: only the statevector method is placed on the fabric's members; this job runs {}. Give method statevector, or leave fabric out", plan.method))),
};
}
let placed = imp::resolve(&ask, r.program.n_qubits, widest(r))?;
if let Some(said) = imp::said(&ask, &placed) {
plan.why = format!("{}; {said}", plan.why);
}
Ok(())
}
/// The state of `gates` on `n` qubits at fusion `width`, run where the job
/// `v` asks (as [`admit`] admitted it), and the fabric's receipt when the
/// result is to carry one. The state is the CPU's, bit for bit, wherever it
/// ran.
pub(crate) fn statevector(v: &Value, n: u32, gates: &[crate::gates::Gate], width: usize, widest: u32) -> Result<(crate::quantum_sv::StateVector, Option<Value>), String> {
let ask = asked(v)?;
let placed = imp::resolve(&ask, n, widest)?;
imp::run(&ask, &placed, n, gates, width)
}
/// The widest launch the job `r` can make, for [`statevector`].
pub(crate) fn widest_of(r: &Request) -> u32 {
widest(r)
}
/// The members a job can be placed on here, each with the most qubits it
/// holds (the CPU's in `job_bytes`), its implementation and its certificate;
/// the devices found and not placed, with why; and why each backend the build
/// reaches found none. Discovers the members if no job has yet.
pub fn list(job_bytes: u64) -> Value {
imp::list(job_bytes)
}
fn cpu_detail(job_bytes: u64) -> Value {
json!({ "max_qubits": super::catalog::statevector_max_qubits(job_bytes), "memory": "the job's memory on this host" })
}
/// The energy a member's run carries: none is metered here.
#[cfg(feature = "quantum_fabric")]
fn unmetered() -> Value {
json!({ "unmetered": "no meter brackets a member's run in the engine: the job's own energy is its host process's, not the device's" })
}
#[cfg(feature = "quantum_fabric")]
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
#[cfg(not(feature = "quantum_fabric"))]
mod imp {
use super::*;
pub(super) enum Placed {
Cpu,
}
pub(super) fn resolve(ask: &Ask, _n: u32, _widest: u32) -> Result<Placed, String> {
match ask {
Ask::Auto => Ok(Placed::Cpu),
_ => Err(tag(refusal::FABRIC, "fabric: this build has no fabric (the engine's quantum_fabric feature); leave fabric out")),
}
}
pub(super) fn said(_ask: &Ask, _p: &Placed) -> Option<String> {
None
}
pub(super) fn run(_ask: &Ask, _p: &Placed, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(crate::quantum_sv::StateVector, Option<Value>), String> {
let mut sv = crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
sv.run(gates, width).map_err(|e| format!("{e:?}"))?;
Ok((sv, None))
}
pub(super) fn list(job_bytes: u64) -> Value {
json!({
"kernel": KERNEL,
"auto": AUTO,
"members": [{ "member": "cpu", "key": "cpu", "class": "cpu", "implementation": "reference", "parity": { "tier": "bit-exact" }, "detail": cpu_detail(job_bytes) }],
"refused": [],
"unavailable": [{ "backend": "fabric", "why": "this build has no fabric (the engine's quantum_fabric feature)" }],
"backends": [],
})
}
}
#[cfg(feature = "quantum_fabric")]
mod imp {
use super::*;
use crate::quantum_fabric::sv64::{self, Launch, PartRunner};
use crate::quantum_fabric::{Certificate, FabricClass, Parity};
use std::sync::OnceLock;
/// An accelerator that runs the plan's launches on a state it is given.
pub(super) trait Device: Send + Sync {
fn max_qubits(&self) -> u32;
fn run(&self, re: &[f64], im: &[f64], launches: &[Launch]) -> Result<(Vec<f64>, Vec<f64>), String>;
}
/// A certified member.
pub(super) struct Accelerator {
id: String,
key: String,
class: FabricClass,
backend: &'static str,
certificate: Certificate,
detail: String,
device: Box<dyn Device>,
}
impl Accelerator {
/// A member for the tests, on a device of their own.
#[cfg(test)]
pub(super) fn for_tests(key: &str, certificate: Certificate, class: FabricClass, device: Box<dyn Device>) -> Accelerator {
Accelerator { id: key.into(), key: key.into(), class, backend: "test", certificate, detail: String::new(), device }
}
}
struct Registry {
members: Vec<Accelerator>,
refused: Vec<Value>,
unavailable: Vec<Value>,
}
fn registry() -> &'static Registry {
static R: OnceLock<Registry> = OnceLock::new();
R.get_or_init(discover)
}
/// The backends this build reaches.
fn backends() -> Vec<&'static str> {
let mut b = Vec::new();
if cfg!(all(feature = "quantum_fabric_cuda", not(target_arch = "wasm32"))) {
b.push("cuda");
}
if cfg!(all(feature = "quantum_fabric_hip", not(target_arch = "wasm32"))) {
b.push("hip");
}
if cfg!(all(feature = "quantum_fabric_opencl", not(target_arch = "wasm32"))) {
b.push("opencl");
}
b
}
// Unused in a build that reaches no accelerator.
#[allow(clippy::too_many_arguments, dead_code)]
fn enrol(r: &mut Registry, backend: &'static str, id: &str, key: &str, class: FabricClass, detail: &str, implementation: &str, cert: Result<Certificate, String>, device: Box<dyn Device>) {
match cert {
Ok(c) if c.parity == Parity::BitExact && c.cases > 0 && c.kernel == KERNEL && c.member == id && c.implementation == implementation => {
r.members.push(Accelerator { id: id.into(), key: key.into(), class, backend, certificate: c, detail: detail.into(), device });
}
Ok(c) => r.refused.push(json!({ "backend": backend, "member": id, "key": key, "why": format!("certified {}, not bit-exact: never placed", tier(&c.parity)["tier"].as_str().unwrap_or("uncertified")) })),
Err(e) => r.refused.push(json!({ "backend": backend, "member": id, "key": key, "why": format!("the state vector did not certify: {e}") })),
}
}
#[allow(unused_mut)]
fn discover() -> Registry {
let mut r = Registry { members: Vec::new(), refused: Vec::new(), unavailable: Vec::new() };
#[cfg(all(feature = "quantum_fabric_cuda", not(target_arch = "wasm32")))]
{
struct Cuda(crate::quantum_fabric_cuda::CudaSv);
impl Device for Cuda {
fn max_qubits(&self) -> u32 {
self.0.max_qubits()
}
fn run(&self, re: &[f64], im: &[f64], launches: &[Launch]) -> Result<(Vec<f64>, Vec<f64>), String> {
self.0.run_launches(re, im, launches).map_err(|e| e.to_string())
}
}
let d = crate::quantum_fabric_cuda::discover();
if let Some(why) = d.unavailable {
r.unavailable.push(json!({ "backend": "cuda", "why": why }));
}
for x in d.refused {
r.refused.push(json!({ "backend": "cuda", "member": x.device, "key": x.key, "why": x.error.to_string() }));
}
for m in d.found {
match m.sv() {
Ok(sv) => {
let (implementation, cert) = (sv.implementation().to_string(), sv.certify(sv64::SV_DEFAULT).map_err(|e| e.to_string()));
enrol(&mut r, "cuda", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, Box::new(Cuda(sv)));
}
Err(e) => r.refused.push(json!({ "backend": "cuda", "member": m.id(), "key": m.key(), "why": e.to_string() })),
}
}
}
#[cfg(all(feature = "quantum_fabric_hip", not(target_arch = "wasm32")))]
{
struct Hip(crate::quantum_fabric_hip::HipSv);
impl Device for Hip {
fn max_qubits(&self) -> u32 {
self.0.max_qubits()
}
fn run(&self, re: &[f64], im: &[f64], launches: &[Launch]) -> Result<(Vec<f64>, Vec<f64>), String> {
self.0.run_launches(re, im, launches).map_err(|e| e.to_string())
}
}
let d = crate::quantum_fabric_hip::discover();
if let Some(why) = d.unavailable {
r.unavailable.push(json!({ "backend": "hip", "why": why }));
}
for x in d.refused {
r.refused.push(json!({ "backend": "hip", "member": x.device, "key": x.key, "why": x.error.to_string() }));
}
for m in d.found {
match m.sv() {
Ok(sv) => {
let (implementation, cert) = (sv.implementation().to_string(), sv.certify(sv64::SV_DEFAULT).map_err(|e| e.to_string()));
enrol(&mut r, "hip", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, Box::new(Hip(sv)));
}
Err(e) => r.refused.push(json!({ "backend": "hip", "member": m.id(), "key": m.key(), "why": e.to_string() })),
}
}
}
#[cfg(all(feature = "quantum_fabric_opencl", not(target_arch = "wasm32")))]
{
struct OpenCl(crate::quantum_fabric_opencl::OpenClSv);
impl Device for OpenCl {
fn max_qubits(&self) -> u32 {
self.0.max_qubits()
}
fn run(&self, re: &[f64], im: &[f64], launches: &[Launch]) -> Result<(Vec<f64>, Vec<f64>), String> {
self.0.run_launches(re, im, launches).map_err(|e| e.to_string())
}
}
let d = crate::quantum_fabric_opencl::discover();
if let Some(why) = d.unavailable {
r.unavailable.push(json!({ "backend": "opencl", "why": why }));
}
for x in d.refused {
r.refused.push(json!({ "backend": "opencl", "member": x.device, "key": x.key, "why": x.error.to_string() }));
}
for m in d.found {
if !m.has_binary64() {
r.refused.push(json!({ "backend": "opencl", "member": m.id(), "key": m.key(), "why": "the device has no binary64" }));
continue;
}
match m.sv() {
Ok(sv) => {
let (implementation, cert) = (sv.implementation().to_string(), sv.certify(sv64::SV_DEFAULT).map_err(|e| e.to_string()));
enrol(&mut r, "opencl", m.id(), m.key(), m.class(), m.detail(), &implementation, cert, Box::new(OpenCl(sv)));
}
Err(e) => r.refused.push(json!({ "backend": "opencl", "member": m.id(), "key": m.key(), "why": e.to_string() })),
}
}
}
// GPUs first, then by backend as listed, then by id: the same order
// on every run.
let order = |b: &str| backends().iter().position(|x| *x == b).unwrap_or(usize::MAX);
r.members.sort_by(|a, b| (a.class != FabricClass::Gpu, order(a.backend), &a.id).cmp(&(b.class != FabricClass::Gpu, order(b.backend), &b.id)));
r
}
fn tier(p: &Parity) -> Value {
match *p {
Parity::BitExact => json!({ "tier": "bit-exact" }),
Parity::Within { max_ulp, max_abs } => json!({ "tier": "within", "max_ulp": max_ulp, "max_abs": max_abs }),
Parity::Statistical { fidelity, shots } => json!({ "tier": "statistical", "fidelity": fidelity, "shots": shots }),
Parity::Uncertified => json!({ "tier": "uncertified" }),
}
}
/// A partition's runner, owned.
type BoxedRunner<'a> = Box<dyn Fn(&mut [f64], &mut [f64], &[Launch]) -> Result<(), String> + Sync + 'a>;
/// One partition's member, or the whole state's.
pub(super) enum Part<'a> {
Cpu,
Member(&'a Accelerator),
}
pub(super) enum Placed<'a> {
Cpu,
/// The member the state runs on, then those it falls back to in
/// turn (for `accelerator`; a member named is alone here).
Members(Vec<&'a Accelerator>),
Split(Vec<Part<'a>>),
}
fn cpu_id() -> String {
crate::quantum_fabric::detect_local().id
}
/// The member `name` names: `cpu` or the CPU's id, or an accelerator by
/// its id, or by its key when one member alone has it.
fn named<'a>(reg: &'a Registry, name: &str) -> Result<Part<'a>, String> {
if name == "cpu" || name == cpu_id() {
return Ok(Part::Cpu);
}
if let Some(m) = reg.members.iter().find(|m| m.id == name) {
return Ok(Part::Member(m));
}
let by_key: Vec<&Accelerator> = reg.members.iter().filter(|m| m.key == name).collect();
match by_key[..] {
[m] => Ok(Part::Member(m)),
[] => Err(tag(refusal::FABRIC, format!("fabric: no member {name} here; {}", here(reg)))),
_ => Err(tag(refusal::FABRIC, format!("fabric: {} members have the key {name}; name one by its id ({})", by_key.len(), by_key.iter().map(|m| m.id.as_str()).collect::<Vec<_>>().join(", ")))),
}
}
/// What this host has to place on, in a sentence.
fn here(reg: &Registry) -> String {
let mut parts = vec![format!("the members are cpu{}", reg.members.iter().map(|m| format!(", {} ({})", m.key, m.id)).collect::<String>())];
for u in &reg.unavailable {
parts.push(format!("{}: {}", u["backend"].as_str().unwrap_or(""), u["why"].as_str().unwrap_or("")));
}
for x in &reg.refused {
parts.push(format!("{} {} not placed: {}", x["backend"].as_str().unwrap_or(""), x["key"].as_str().unwrap_or(""), x["why"].as_str().unwrap_or("")));
}
if backends().is_empty() {
parts.push("this build reaches no accelerator (the engine's quantum_fabric_cuda, quantum_fabric_hip and quantum_fabric_opencl features)".into());
}
parts.join("; ")
}
pub(super) fn resolve<'a>(ask: &Ask, n: u32, widest: u32) -> Result<Placed<'a>, String> {
match ask {
// Auto stays on the CPU and looks for no member (see the module).
Ask::Cpu | Ask::Auto => Ok(Placed::Cpu),
Ask::Accelerator => {
let reg = registry();
if reg.members.is_empty() {
return Err(tag(refusal::FABRIC, format!("fabric accelerator: no certified accelerator member here; {}", here(reg))));
}
let holds: Vec<&Accelerator> = reg.members.iter().filter(|m| m.device.max_qubits() >= n).collect();
if holds.is_empty() {
return Err(tag(refusal::FABRIC, format!("fabric accelerator: a state of {n} qubits; the largest member here holds {} (give a list of members to split it)", reg.members.iter().map(|m| m.device.max_qubits()).max().unwrap_or(0))));
}
Ok(Placed::Members(holds))
}
Ask::Member(name) => {
let reg = registry();
match named(reg, name)? {
Part::Cpu => Ok(Placed::Cpu),
Part::Member(m) if m.device.max_qubits() >= n => Ok(Placed::Members(vec![m])),
Part::Member(m) => Err(tag(refusal::FABRIC, format!("fabric {name}: a state of {n} qubits; {} holds at most {} (give a list of members to split it)", m.key, m.device.max_qubits()))),
}
}
Ask::Split(names) => {
let p = names.len().trailing_zeros();
if n <= p {
return Err(tag(refusal::FABRIC, format!("fabric: {} partitions of a {n}-qubit state leave no qubit in a partition", names.len())));
}
let local = n - p;
if local < widest {
return Err(tag(refusal::FABRIC, format!("fabric: {} partitions leave {local} qubits in each, fewer than the {widest} a gate of this job acts on at once; give fewer partitions or a smaller fusion", names.len())));
}
let needs = names.iter().any(|x| x != "cpu");
let reg = if needs { Some(registry()) } else { None };
let mut parts = Vec::with_capacity(names.len());
for name in names {
let part = match reg {
Some(reg) => named(reg, name)?,
None => Part::Cpu,
};
if let Part::Member(m) = &part
&& m.device.max_qubits() < local
{
return Err(tag(refusal::FABRIC, format!("fabric: a partition of {local} qubits; {} holds at most {}", m.key, m.device.max_qubits())));
}
parts.push(part);
}
Ok(Placed::Split(parts))
}
}
}
fn label(p: &Part<'_>) -> String {
match p {
Part::Cpu => "cpu".into(),
Part::Member(m) => m.key.clone(),
}
}
pub(super) fn said(ask: &Ask, p: &Placed<'_>) -> Option<String> {
match p {
Placed::Cpu if *ask == Ask::Auto => None,
Placed::Cpu => Some("on the CPU, as the job asked".into()),
Placed::Members(ms) => ms.first().map(|m| format!("its state on {} ({}), certified bit-exact against the CPU", m.key, m.id)),
Placed::Split(parts) => Some(format!("its state split by its top {} qubits into {} partitions, on {}, each certified bit-exact against the CPU", parts.len().trailing_zeros(), parts.len(), parts.iter().map(label).collect::<Vec<_>>().join(", "))),
}
}
fn cpu_json() -> Value {
let id = cpu_id();
let c = Certificate::reference(KERNEL, &id);
json!({ "member": id, "key": "cpu", "class": "cpu", "implementation": crate::quantum_fabric::REFERENCE, "parity": tier(&c.parity), "certificate": hex(&c.hash()), "generator": c.generator, "cases": c.cases })
}
fn member_json(m: &Accelerator) -> Value {
let c = &m.certificate;
json!({ "member": m.id, "key": m.key, "class": m.class.label(), "implementation": c.implementation, "parity": tier(&c.parity), "certificate": hex(&c.hash()), "corpus": hex(&c.corpus), "generator": c.generator, "cases": c.cases })
}
fn part_json(p: &Part<'_>) -> Value {
match p {
Part::Cpu => cpu_json(),
Part::Member(m) => member_json(m),
}
}
fn cpu_run(n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<crate::quantum_sv::StateVector, String> {
let mut sv = crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
sv.run(gates, width).map_err(|e| format!("{e:?}"))?;
Ok(sv)
}
fn state(n: u32, re: Vec<f64>, im: Vec<f64>) -> crate::quantum_sv::StateVector {
crate::quantum_sv::StateVector { n, re, im }
}
pub(super) fn run(ask: &Ask, p: &Placed<'_>, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(crate::quantum_sv::StateVector, Option<Value>), String> {
let receipt = |mut body: Value, why: Option<String>| -> Value {
body["kernel"] = json!(KERNEL);
body["asked"] = ask.json();
body["energy"] = unmetered();
if let Some(w) = why {
body["why"] = json!(w);
}
body
};
match p {
Placed::Cpu => {
let sv = cpu_run(n, gates, width)?;
let r = (*ask != Ask::Auto).then(|| receipt(cpu_json(), said(ask, p)));
Ok((sv, r))
}
Placed::Members(ms) => {
let launches = sv64::plan(n, gates, width).map_err(|e| format!("{e:?}"))?;
let len = 1usize << n;
let mut re = vec![0.0; len];
re[0] = 1.0;
let im = vec![0.0; len];
// Each in turn until one runs it: a member that fails hands the
// job to the next, and the receipt lists each that failed.
let mut failed: Vec<(&Accelerator, String)> = Vec::new();
for m in ms {
match m.device.run(&re, &im, &launches) {
Ok((re, im)) => {
let mut why = said(ask, &Placed::Members(vec![m]));
if !failed.is_empty() {
why = Some(format!("{}; {} failed during the run first", why.unwrap_or_default(), failed.iter().map(|(f, _)| f.key.as_str()).collect::<Vec<_>>().join(", ")));
}
let mut body = receipt(member_json(m), why);
if !failed.is_empty() {
body["fell_back_from"] = Value::Array(
failed
.iter()
.map(|(f, e)| {
let mut j = member_json(f);
j["why"] = json!(e);
j
})
.collect(),
);
}
return Ok((state(n, re, im), Some(body)));
}
Err(e) => failed.push((m, e)),
}
}
let all: Vec<String> = failed.iter().map(|(f, e)| format!("{} failed during the run: {e}", f.key)).collect();
Err(tag(refusal::FABRIC, format!("fabric: {}", all.join("; "))))
}
Placed::Split(parts) => {
let launches = sv64::plan(n, gates, width).map_err(|e| format!("{e:?}"))?;
// One runner for the CPU and one for each member named, so a
// member's partitions run one after another.
let mut keys: Vec<Option<&str>> = Vec::new();
let mut owner = Vec::with_capacity(parts.len());
for part in parts {
let k = match part {
Part::Cpu => None,
Part::Member(m) => Some(m.id.as_str()),
};
owner.push(keys.iter().position(|x| *x == k).unwrap_or_else(|| {
keys.push(k);
keys.len() - 1
}));
}
let cpu = |re: &mut [f64], im: &mut [f64], l: &[Launch]| -> Result<(), String> {
sv64::replay(re, im, l);
Ok(())
};
let devices: Vec<BoxedRunner<'_>> = keys
.iter()
.map(|k| -> BoxedRunner<'_> {
match k.and_then(|id| parts.iter().find_map(|x| if let Part::Member(m) = x { (m.id == id).then_some(*m) } else { None })) {
None => Box::new(cpu),
Some(m) => Box::new(move |re: &mut [f64], im: &mut [f64], l: &[Launch]| {
let (r, i) = m.device.run(re, im, l).map_err(|e| format!("{} failed during the run: {e}", m.key))?;
re.copy_from_slice(&r);
im.copy_from_slice(&i);
Ok(())
}),
}
})
.collect();
let runners: Vec<PartRunner<'_>> = devices.iter().map(|b| &**b as PartRunner<'_>).collect();
let len = 1usize << n;
let (mut re, mut im) = (vec![0.0; len], vec![0.0; len]);
re[0] = 1.0;
let done = sv64::run_split(&mut re, &mut im, &launches, &owner, &runners).map_err(|e| tag(refusal::FABRIC, format!("fabric: {e}")))?;
let body = receipt(
json!({
"partitions": parts.iter().map(part_json).collect::<Vec<_>>(),
"partition_qubits": parts.len().trailing_zeros(),
"local_qubits": n - parts.len().trailing_zeros(),
"segments": done.segments,
"exchanges": done.exchanges,
"parity": { "tier": "bit-exact" },
}),
said(ask, p),
);
Ok((state(n, re, im), Some(body)))
}
}
}
pub(super) fn list(job_bytes: u64) -> Value {
let reg = registry();
let mut members = vec![cpu_json()];
members[0]["detail"] = cpu_detail(job_bytes);
for m in &reg.members {
let mut j = member_json(m);
j["detail"] = json!({ "max_qubits": m.device.max_qubits(), "memory": "two f64 arrays of 2^n within half the device's free memory at discovery, n at most 25", "backend": m.backend, "device": m.detail });
members.push(j);
}
json!({
"kernel": KERNEL,
"auto": AUTO,
"members": members,
"refused": reg.refused,
"unavailable": reg.unavailable,
"backends": backends(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
/// An `accelerator` request whose member fails during the run hands the
/// job to the next member that holds the state, with the CPU's bits and
/// the failure in the receipt; when every member fails it is refused.
#[cfg(feature = "quantum_fabric")]
#[test]
fn an_accelerator_that_fails_hands_the_job_to_the_next_and_the_receipt_says_so() {
use crate::quantum_fabric::sv64::{Launch, replay};
use crate::quantum_fabric::{Certificate, FabricClass};
use super::imp::{self, Accelerator, Device, Placed};
struct Lost;
impl Device for Lost {
fn max_qubits(&self) -> u32 {
25
}
fn run(&self, _: &[f64], _: &[f64], _: &[Launch]) -> Result<(Vec<f64>, Vec<f64>), String> {
Err("device lost".into())
}
}
struct Replay;
impl Device for Replay {
fn max_qubits(&self) -> u32 {
25
}
fn run(&self, re: &[f64], im: &[f64], l: &[Launch]) -> Result<(Vec<f64>, Vec<f64>), String> {
let (mut re, mut im) = (re.to_vec(), im.to_vec());
replay(&mut re, &mut im, l);
Ok((re, im))
}
}
let member = |key: &str, device: Box<dyn Device>| Accelerator::for_tests(key, Certificate::reference(KERNEL, key), FabricClass::Gpu, device);
let (a, b) = (member("lost", Box::new(Lost)), member("kept", Box::new(Replay)));
let gates: Vec<crate::gates::Gate> = (0..6u32).flat_map(|q| [crate::gates::Gate::rx(q, 0.3 + f64::from(q)), crate::gates::Gate::cz(q, (q + 1) % 6)]).collect();
let mut cpu = crate::quantum_sv::StateVector::zero(6).unwrap();
cpu.run(&gates, 3).unwrap();
let (sv, receipt) = imp::run(&Ask::Accelerator, &Placed::Members(vec![&a, &b]), 6, &gates, 3).unwrap();
assert!(sv.re.iter().zip(&cpu.re).chain(sv.im.iter().zip(&cpu.im)).all(|(x, y)| x.to_bits() == y.to_bits()), "the CPU's bits");
let r = receipt.unwrap();
assert_eq!((r["key"].as_str(), r["fell_back_from"][0]["key"].as_str(), r["fell_back_from"][0]["why"].as_str()), (Some("kept"), Some("lost"), Some("device lost")), "{r}");
assert!(r["why"].as_str().unwrap().contains("lost failed during the run first"), "{r}");
assert!(super::super::schema::Registry::engine().check("job-result.v1.json#/$defs/fabric", &r).is_empty(), "{r}");
let e = imp::run(&Ask::Accelerator, &Placed::Members(vec![&a]), 6, &gates, 3).unwrap_err();
assert!(e.ends_with("[J013]") && e.contains("lost failed during the run: device lost"), "{e}");
}
#[test]
fn a_job_s_fabric_reads_or_is_refused_with_its_code() {
let ask = |f: Value| asked(&json!({ "qasm": "", "fabric": f }));
assert_eq!(asked(&json!({ "qasm": "" })).unwrap(), Ask::Auto);
assert_eq!(ask(json!("auto")).unwrap(), Ask::Auto);
assert_eq!(ask(json!("cpu")).unwrap(), Ask::Cpu);
assert_eq!(ask(json!("accelerator")).unwrap(), Ask::Accelerator);
assert_eq!(ask(json!("gpu-cuda-sm89")).unwrap(), Ask::Member("gpu-cuda-sm89".into()));
assert_eq!(ask(json!(["cpu", "cpu"])).unwrap(), Ask::Split(vec!["cpu".into(), "cpu".into()]));
for bad in [json!(""), json!(3), json!({ "member": "cpu" }), json!(["cpu", 2]), json!(["auto", "cpu"])] {
assert!(ask(bad.clone()).unwrap_err().ends_with("[J003]"), "{bad}");
}
for odd in [json!(["cpu"]), json!(["cpu", "cpu", "cpu"]), json!(vec!["cpu"; 128])] {
assert!(ask(odd.clone()).unwrap_err().ends_with("[J013]"), "{odd}");
}
}
const CAP: u64 = 8 << 30;
/// A circuit at arbitrary angles whose gates reach every qubit, the top
/// ones included, and a job of it asking for every answer.
fn job(n: u32, extra: Value) -> Value {
let mut q = format!("OPENQASM 3.0;\nqubit[{n}] q;\nbit[{n}] c;\n");
for layer in 0..4 {
for i in 0..n {
q.push_str(&format!("rx({}) q[{i}];\nry({}) q[{i}];\n", 0.11 + 0.37 * f64::from(i + layer), 0.29 * f64::from(layer + 1)));
}
for i in (layer % 2..n - 1).step_by(2) {
q.push_str(&format!("cx q[{i}], q[{}];\nrzz({}) q[{}], q[{i}];\n", i + 1, 0.4 + 0.1 * f64::from(i), (i + 2) % n));
}
q.push_str(&format!("cp(0.7) q[0], q[{}];\n", n - 1));
}
q.push_str("c = measure q;\n");
let mut v = json!({ "qasm": q, "shots": 2000, "seed": 9, "method": "statevector", "observables": ["Z0 Z1", "0.5 X2", format!("Y{} Z0", n - 1)], "amplitudes": ["0".repeat(n as usize), format!("1{}", "0".repeat(n as usize - 1))] });
if let (Some(o), Some(e)) = (v.as_object_mut(), extra.as_object()) {
o.extend(e.clone());
}
v
}
fn run(v: &Value) -> Result<Value, String> {
let p = super::super::catalog::plan(v, CAP)?;
super::super::run::run(v, &p)
}
/// The answers: the result without where it ran and how long it took.
#[cfg(feature = "quantum_fabric")]
fn answers(out: &Value) -> Vec<u8> {
let mut o = out.clone();
for k in ["fabric", "elapsed_ms"] {
o.as_object_mut().unwrap().remove(k);
}
super::super::receipt::canonical(&o)
}
/// With no member asked for and none placed, a job runs as it always did:
/// no `fabric` in its result and none in its plan's reasons. A job that
/// asks for the CPU, or for partitions on the CPU, has the same answers
/// byte for byte, and its result names where its state ran.
#[cfg(feature = "quantum_fabric")]
#[test]
fn a_placement_changes_where_the_state_runs_and_not_a_byte_of_the_answers() {
for n in [6u32, 9] {
let plain = run(&job(n, json!({}))).unwrap();
assert!(plain.get("fabric").is_none(), "{plain}");
let p = super::super::catalog::plan(&job(n, json!({})), CAP).unwrap();
assert!(!p.why.contains("partition") && !p.why.contains("CPU, as"), "{}", p.why);
let want = answers(&plain);
let cpu = run(&job(n, json!({ "fabric": "cpu" }))).unwrap();
assert_eq!(answers(&cpu), want, "n={n}");
assert_eq!((cpu["fabric"]["key"].as_str(), cpu["fabric"]["asked"].as_str()), (Some("cpu"), Some("cpu")), "{}", cpu["fabric"]);
assert_eq!(cpu["fabric"]["parity"]["tier"], "bit-exact");
assert_eq!(cpu["fabric"]["certificate"].as_str().map(str::len), Some(64));
for parts in [2usize, 4, 8] {
if n - parts.trailing_zeros() < 3 {
continue;
}
let split = run(&job(n, json!({ "fabric": vec!["cpu"; parts] }))).unwrap();
assert_eq!(answers(&split), want, "n={n} partitions={parts}");
let f = &split["fabric"];
assert_eq!(f["partitions"].as_array().map(Vec::len), Some(parts), "{f}");
assert_eq!(f["partition_qubits"], parts.trailing_zeros());
assert!(f["exchanges"].as_u64().unwrap() > 0 && f["segments"].as_u64().unwrap() > 1, "{f}");
let plan = super::super::catalog::plan(&job(n, json!({ "fabric": vec!["cpu"; parts] })), CAP).unwrap();
assert!(plan.why.contains(&format!("into {parts} partitions")), "{}", plan.why);
}
}
// A fusion width the partitions cannot hold is refused, a smaller one taken.
let e = run(&job(6, json!({ "fabric": vec!["cpu"; 16] }))).unwrap_err();
assert!(e.ends_with("[J013]") && e.contains("fewer than the 3"), "{e}");
let fine = run(&job(6, json!({ "fabric": vec!["cpu"; 16], "options": { "fusion": 2 } }))).unwrap();
assert_eq!(answers(&fine), answers(&run(&job(6, json!({ "options": { "fusion": 2 } }))).unwrap()));
}
#[cfg(feature = "quantum_fabric")]
#[test]
fn a_placement_this_host_cannot_honour_is_refused_never_made_elsewhere() {
let code = |v: Value| run(&v).map(|_| ()).unwrap_err();
let bell = |extra: Value| {
let mut v = json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n", "shots": 100 });
v.as_object_mut().unwrap().extend(extra.as_object().unwrap().clone());
v
};
// Another method is never placed; auto with a member asked for takes the state vector.
assert!(code(bell(json!({ "method": "stabilizer", "fabric": "accelerator" }))).ends_with("[J013]"));
assert!(code(bell(json!({ "method": "exact", "fabric": ["cpu", "cpu"] }))).ends_with("[J013]"));
let ghz = json!({ "qasm": "OPENQASM 3.0;\nqubit[4] q;\nbit[4] c;\nh q[0];\ncx q[0], q[1];\ncx q[1], q[2];\ncx q[2], q[3];\nc = measure q;\n", "shots": 100, "fabric": ["cpu", "cpu"] });
let p = super::super::catalog::plan(&ghz, CAP).unwrap();
assert_eq!(p.method, "statevector", "{}", p.why);
assert!(run(&bell(json!({ "fabric": ["cpu", "cpu"] }))).unwrap_err().ends_with("[J013]"), "a Bell pair in two partitions leaves one qubit in each");
// The CPU runs every CPU method as asked.
assert!(run(&bell(json!({ "method": "stabilizer", "fabric": "cpu" }))).is_ok());
// A member this host does not have, by name or by asking for any.
if backends_reached().is_empty() {
let e = code(job(6, json!({ "fabric": "accelerator" })));
assert!(e.ends_with("[J013]") && e.contains("no certified accelerator member here"), "{e}");
let e = code(job(6, json!({ "fabric": "gpu-somewhere" })));
assert!(e.ends_with("[J013]") && e.contains("no member gpu-somewhere"), "{e}");
let e = code(job(6, json!({ "fabric": ["cpu", "gpu-somewhere"] })));
assert!(e.ends_with("[J013]"), "{e}");
}
// A program that measures in the middle runs on the CPU alone.
let mid = json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nbit[2] c;\nrx(0.3) q[0];\nc[0] = measure q[0];\ncx q[0], q[1];\nc[1] = measure q[1];\n", "shots": 50, "method": "statevector", "fabric": ["cpu", "cpu"] });
assert!(code(mid).ends_with("[J013]"));
// Partitions that leave no qubit.
assert!(code(bell(json!({ "method": "statevector", "fabric": ["cpu", "cpu", "cpu", "cpu"] }))).ends_with("[J013]"));
}
/// The result that names where its state ran follows the job result's
/// schema, read strictly, and its provenance names the members.
#[cfg(feature = "quantum_fabric")]
#[test]
fn a_placed_result_and_its_provenance_follow_their_schemas() {
let r = super::super::schema::Registry::engine();
for f in [json!("cpu"), json!(["cpu", "cpu", "cpu", "cpu"])] {
let v = job(7, json!({ "fabric": f }));
let p = super::super::catalog::plan(&v, CAP).unwrap();
let out = super::super::run::run_with_provenance(&v, &p, "a test 1.0").unwrap();
let errs = r.check("job-result.v1.json", &out);
assert!(errs.is_empty(), "{errs:?}");
let errs = r.check("job-request.v1.json", &v);
assert!(errs.is_empty(), "{errs:?}");
assert!(out["provenance"]["computer"]["fabric"]["members"].as_array().is_some_and(|m| !m.is_empty()), "{}", out["provenance"]);
assert!(r.check("provenance.v1.json", &out["provenance"]).is_empty());
}
let listed = list(CAP);
let errs = r.check("tools.v1.json#/$defs/fabric_members", &listed);
assert!(errs.is_empty(), "{errs:?} {listed}");
assert_eq!(listed["members"][0]["key"], "cpu");
}
/// A build without the fabric runs every job as it always did, and
/// refuses any placement it was asked for.
#[cfg(not(feature = "quantum_fabric"))]
#[test]
fn without_the_fabric_only_auto_is_honoured() {
assert!(run(&job(6, json!({}))).unwrap().get("fabric").is_none());
assert!(run(&job(6, json!({ "fabric": "auto" }))).is_ok());
for f in [json!("cpu"), json!("accelerator"), json!(["cpu", "cpu"])] {
assert!(run(&job(6, json!({ "fabric": f }))).unwrap_err().ends_with("[J013]"));
}
assert_eq!(list(CAP)["members"][0]["key"], "cpu");
}
#[cfg(feature = "quantum_fabric")]
fn backends_reached() -> Vec<String> {
list(CAP)["backends"].as_array().into_iter().flatten().filter_map(|b| b.as_str().map(str::to_string)).collect()
}
}

View file

@ -94,7 +94,10 @@ pub const EXAMPLES: &str = "wai-quantum://examples";
pub const VERDICTS: &str = "wai-quantum://docs/verdicts";
/// The order `auto` tries the methods in.
pub const AUTO: &str = "auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen.";
pub const AUTO: &str = "auto picks, in order: with noise, density_matrix, stabilizer, then trajectories; for counts, stabilizer, exact, statevector, statevector_single, cutrank, mps; for expectation values only, stabilizer, exact, statevector, statevector_single, stabilizer_rank, pauli, mps; for amplitudes only, exact, statevector, statevector_single, cutrank, stabilizer_rank, tensor_network, mps. A method that cannot take the program is skipped, and the plan says why the one chosen was chosen; stabilizer_rank is chosen by its T count, its exact sum taken when its branches are within one job's work.";
/// Where the statevector method's state runs, in a paragraph.
pub const FABRIC: &str = "The statevector method's state runs where the job's `fabric` says: auto (the default) places it on the CPU, on every host, until a member of the fabric is measured faster than the CPU; accelerator places it on the first accelerator member found here and certified bit-exact against the CPU that holds it (`wai-quantum fabric`, or list_fabric, lists them), the next taking over if one fails during the run; cpu or a member's id or key places it there; a list of members, a power of two of them, splits the state by its top qubits into one partition on each. Every member placed gives the CPU's bits, so the answers are the same byte for byte; the result's `fabric` names where the state ran, with the member's certificate, and the receipt seals it. A placement this host cannot honour is refused (J013), never made elsewhere.";
/// The language in a paragraph, for `AGENTS.md`.
pub const SUMMARY: &str = "Programs are OpenQASM 3.1, or the OpenQASM 2 forms in common use, read by one reader on every surface: registers, the standard gates, `gate` and `def` definitions, the `ctrl @`, `negctrl @`, `inv @` and `pow @` modifiers, classical types and expressions, measurement and `reset` anywhere, `if`, `switch` and bounded `while` on measured bits, timing carried, and calibrations kept for export. Generators (integer `input`s, `const`s and `for` loops) describe a family of circuits. Every refusal and warning names its line and ends with a stable code (`[Q103]`), which the diagnostics resource explains.";
@ -139,6 +142,7 @@ A program that measures a qubit and then acts on it, resets one after its first
- On this machine: `wai-quantum run`, or the local MCP server (`wai-quantum mcp`). Results are signed with the user's key and nothing leaves the machine.
- On the free job service, for a program too large for this machine: `wai-quantum submit`, `POST https://wai.transaction.science/quantum-jobs/v1/run`, or its MCP server at https://wai.transaction.science/quantum-jobs/mcp. No account and no key.
- {AUTO}
- {FABRIC}
",
feature_matrix().trim_end(),
catalog::NOISE_CHANNELS.join(", "),
@ -238,7 +242,7 @@ pub fn verdicts() -> Value {
/// The methods, their reach in `job_bytes` and their accuracy classes, as the
/// `methods` resource gives them.
pub fn methods(job_bytes: u64) -> Value {
json!({ "methods": catalog::describe(job_bytes), "accuracy_classes": catalog::accuracy_classes(), "auto": AUTO })
json!({ "methods": catalog::describe(job_bytes), "accuracy_classes": catalog::accuracy_classes(), "auto": AUTO, "fabric": FABRIC })
}
/// A worked example: a program, what it must do, and how it is run.

View file

@ -202,6 +202,7 @@ pub mod energy;
pub mod equiv;
pub mod estimate;
pub mod formats;
pub mod fabric;
pub mod guide;
pub mod latex;
#[cfg(feature = "quantum_noise")]

View file

@ -87,6 +87,7 @@ table! {
UNKNOWN_METHOD = "J010", Refused, "A method that does not exist: the methods list names them.";
METHOD_REFUSES = "J011", Refused, "The method named cannot take this job: a gate it does not hold, more qubits, memory or work than it takes here, noise it does not model, or an answer it does not give; the message says which, and `auto` picks a method that can.";
NO_METHOD = "J012", Refused, "No method takes this job: each method's reason is listed, so the job can be changed to suit one.";
FABRIC = "J013", Refused, "A placement on the fabric (`fabric`) this host cannot honour: no member by that name, no certified accelerator member here, a state or a partition larger than a member holds, a partition count that is not a power of two, or a method or program the fabric's members do not run (the message says which, and what is here).";
// Intents and repairs.
BAD_INTENT = "J020", Refused, "An intent that does not read: not one of `distribution`, `equivalent` or `prepares`, a distribution whose outcomes are not the program's bits or whose probabilities do not sum to 1, or a reference program that does not read.";

View file

@ -180,6 +180,15 @@ pub fn provenance(request: &Value, plan: &Plan, result: &Value, passes: Option<V
if let Some(h) = result.pointer("/target/hash") {
computer["target"] = h.clone();
}
// Where the state ran, when the result names it: the member, or each
// partition's.
if let Some(f) = result.get("fabric") {
let members: Vec<Value> = match f.get("partitions").and_then(Value::as_array) {
Some(parts) => parts.iter().map(|p| p["member"].clone()).collect(),
None => vec![f["member"].clone()],
};
computer["fabric"] = json!({ "kernel": f["kernel"], "members": members });
}
json!({
"schema": PROVENANCE,
"circuit": {
@ -304,9 +313,13 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option<Value>) -> Result<V
"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 = crate::quantum_sv::StateVector::zero(n).map_err(|e| format!("{e:?}"))?;
sv.run(&gates, width).map_err(|e| format!("{e:?}"))?;
// On the CPU, or on the fabric's member the job's placement
// names: the CPU's bits either way.
let (sv, placed) = crate::engine::fabric::statevector(request, n, &gates, width, crate::engine::fabric::widest_of(&r))?;
out["accuracy"] = json!({ "class": "exact", "fusion": width });
if let Some(f) = placed {
out["fabric"] = f;
}
if r.shots > 0 {
let mut map: HashMap<String, u64> = HashMap::new();
for x in sv.sample(r.shots as usize, r.seed) {
@ -584,6 +597,58 @@ fn run_job(request: &Value, plan: &Plan, passes: &mut Option<Value>) -> Result<V
}
}
}
"stabilizer_rank" => {
use crate::quantum_stabrank::{amplitudes as exact_sums, exact_scale, sampled, sampled_bound, sampled_scale};
let c = r.program.clifford_t()?;
let t = c.t_count();
let mode = catalog::stabilizer_rank_mode(&r, t)?;
let threads = if cfg!(target_arch = "wasm32") { 1 } else { std::thread::available_parallelism().map_or(1, |t| t.get()).min(16) };
// Every branch, or `terms` drawn: the sums and the scale they are read at.
let sums = |c: &crate::quantum_stabrank::CliffordT, xs: &[Vec<bool>], seed: u64| -> Result<(Vec<(f64, f64)>, Option<f64>), String> {
let tc = c.t_count();
match mode.sampled {
None => Ok((exact_sums(c, xs, threads).map_err(|e| e.to_string())?.iter().map(|s| s.value(exact_scale(tc))).collect(), None)),
Some((k, delta)) => Ok((sampled(c, xs, k, seed, threads).map_err(|e| e.to_string())?.iter().map(|s| s.value(sampled_scale(tc, k))).collect(), Some(sampled_bound(tc, k, delta)))),
}
};
out["accuracy"] = match mode.sampled {
None => json!({ "class": "exact", "t_count": t, "terms": 1u64.checked_shl(t).unwrap_or(u64::MAX), "note": "the state as the exact sum of its stabilizer branches, each T gate a sum of two Clifford gates of equal weight; every term is a whole number of sixteenths of a turn at a power of 1/√2, counted exactly, and the sum is evaluated once in double precision; an expectation value is the amplitude ⟨0|U†PU|0⟩, a sum over twice the T gates" }),
Some((k, delta)) => json!({ "class": "estimate", "t_count": t, "terms": k, "bound": sampled_bound(t, k, delta), "confidence": 1.0 - delta, "note": "the state's stabilizer branches drawn uniformly, seeded, each with its coefficient's phase: an unbiased estimate whose mean squared distance from the state is (sec²(π/8)^t − 1)/terms, so each amplitude is within the bound of the exact one with the probability stated (Markov's inequality), the bound computed from the T count, the terms and the confidence; an expectation value is the amplitude ⟨0|U†PU|0⟩, drawn over twice the T gates, with its own bound" }),
};
if !r.amplitudes.is_empty() {
let xs: Vec<Vec<bool>> = r.amplitudes.iter().map(|b| (0..n).map(|q| b.as_bytes()[(n - 1 - q) as usize] == b'1').collect()).collect();
let (values, bound) = sums(&c, &xs, r.seed)?;
out["amplitudes"] = Value::Array(
r.amplitudes
.iter()
.zip(values)
.map(|(b, (re, im))| {
let mut e = json!({ "bits": b, "re": re, "im": im });
if let (Some(eps), Some((_, delta))) = (bound, mode.sampled) {
e["bound"] = json!(eps);
e["confidence"] = json!(1.0 - delta);
}
e
})
.collect(),
);
}
if !r.observables.is_empty() {
let zero = vec![vec![false; n as usize]];
let mut entries = Vec::new();
for (i, (text, coeff, string)) in r.observables.iter().enumerate() {
let w = c.sandwich(string).map_err(|e| e.to_string())?;
let (values, bound) = sums(&w, &zero, r.seed.wrapping_add((i as u64 + 1).wrapping_mul(0x9E37_79B9_7F4A_7C15)))?;
let mut e = json!({ "observable": text, "coeff": coeff, "value": coeff * values[0].0 });
if let (Some(eps), Some((_, delta))) = (bound, mode.sampled) {
e["bound"] = json!(coeff.abs() * eps);
e["confidence"] = json!(1.0 - delta);
}
entries.push(e);
}
out["expectations"] = Value::Array(entries);
}
}
"tensor_network" => {
use crate::quantum_tn::{Network, PathOptions, contract_plan, find_path_restarts_seeded, sweep_orders, sweep_path, tensor_places};
// Independent path searches, side by side: the search's cost varies with its
@ -1339,4 +1404,128 @@ mod tests {
let p = run_with_provenance(&bell, &catalog::plan(&bell, 1 << 30).unwrap(), "h").unwrap()["provenance"].clone();
assert_eq!((p["compilation"]["asked"].as_bool(), p["compilation"]["status"].as_str()), (Some(false), Some("not-applicable")), "{p}");
}
/// A random Clifford+T program in the language, with every gate the
/// lowering turns into T gates, its global phases included.
fn clifford_t_program(n: u32, len: usize, seed: u64) -> String {
let mut s = seed;
let mut next = || {
s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
};
let mut q = format!("OPENQASM 3.0;\nqubit[{n}] q;\n");
for _ in 0..len {
let a = (next() % u64::from(n)) as u32;
let b = (a + 1 + (next() % u64::from(n - 1)) as u32) % n;
let c = (0..n).find(|&x| x != a && x != b).unwrap();
let odd = ["pi/4", "3*pi/4", "-pi/4", "5*pi/4"][(next() % 4) as usize];
q.push_str(&match next() % 22 {
0 | 1 => format!("h q[{a}];\n"),
2 => format!("t q[{a}];\n"),
3 => format!("tdg q[{a}];\n"),
4 => format!("s q[{a}];\n"),
5 => format!("cx q[{a}], q[{b}];\n"),
6 => format!("cz q[{a}], q[{b}];\n"),
7 => format!("rz({odd}) q[{a}];\n"),
8 => format!("rx({odd}) q[{a}];\n"),
9 => format!("ry({odd}) q[{a}];\n"),
10 => format!("p({odd}) q[{a}];\n"),
11 => format!("cp(pi/2) q[{a}], q[{b}];\n"),
12 => format!("ccx q[{a}], q[{b}], q[{c}];\n"),
13 => format!("ccz q[{a}], q[{b}], q[{c}];\n"),
14 => format!("sx q[{a}];\n"),
15 | 16 => format!("cx q[{b}], q[{a}];\n"),
17 => format!("swap q[{a}], q[{b}];\n"),
18 => format!("h q[{b}];\n"),
19 => format!("y q[{a}];\n"),
_ => format!("rz(pi/2) q[{a}];\n"),
});
}
q
}
/// Ground truth first: the stabilizer-rank sum of a program, every
/// amplitude (phase and all) and every expectation value, is the dense
/// state vector's, on every size a test affords; checked amplitude by
/// amplitude and by fidelity.
#[test]
fn the_stabilizer_rank_method_is_the_dense_state_vector() {
let mut cases = 0;
for n in 3..=6u32 {
for seed in 0..10u64 {
let qasm = clifford_t_program(n, 2 * n as usize + 2, seed * 7 + u64::from(n));
let t = crate::qasm::read(&qasm).unwrap().program.clifford_t().unwrap().t_count();
// An expectation value sums 4^t branches.
if t > 9 {
continue;
}
let bits: Vec<String> = (0..1u32 << n).map(|x| format!("{x:0w$b}", w = n as usize)).collect();
let job = |method: &str| json!({ "qasm": qasm, "method": method, "amplitudes": bits, "observables": ["Z0", "X1 Z2", format!("Y{} X0", n - 1)] });
let sv = run(&job("statevector"), &plan(&job("statevector"), CAP).unwrap()).unwrap();
let sr_plan = plan(&job("stabilizer_rank"), CAP).unwrap();
let sr = run(&job("stabilizer_rank"), &sr_plan).unwrap();
assert_eq!((sr["accuracy"]["class"].as_str(), sr["accuracy"]["t_count"].as_u64()), (Some("exact"), Some(u64::from(t))), "{}", sr["accuracy"]);
let (mut ore, mut oim, mut norm) = (0.0, 0.0, 0.0);
for (a, b) in sv["amplitudes"].as_array().unwrap().iter().zip(sr["amplitudes"].as_array().unwrap()) {
let (ar, ai, br, bi) = (a["re"].as_f64().unwrap(), a["im"].as_f64().unwrap(), b["re"].as_f64().unwrap(), b["im"].as_f64().unwrap());
assert!((ar - br).abs() < 1e-12 && (ai - bi).abs() < 1e-12, "n={n} seed={seed} {}: {ar} {ai} against {br} {bi}\n{qasm}", a["bits"]);
ore += ar * br + ai * bi;
oim += ar * bi - ai * br;
norm += br * br + bi * bi;
}
assert!(((ore * ore + oim * oim) / norm - 1.0).abs() < 1e-12 && (ore - 1.0).abs() < 1e-12, "n={n} seed={seed}: the fidelity, and the overlap with the phase");
for (a, b) in sv["expectations"].as_array().unwrap().iter().zip(sr["expectations"].as_array().unwrap()) {
assert!((a["value"].as_f64().unwrap() - b["value"].as_f64().unwrap()).abs() < 1e-12, "n={n} seed={seed} {}", a["observable"]);
}
cases += 1;
}
}
assert!(cases >= 16, "{cases}");
}
/// Auto takes the stabilizer-rank sum by its T count, the sampled mode
/// states its bound and confidence, and the method refuses what it does not
/// take.
#[test]
fn auto_chooses_the_stabilizer_rank_sum_by_its_t_count() {
let wide = |ts: usize| {
let mut q = String::from("OPENQASM 3.0;\nqubit[40] q;\nh q;\n");
for i in 0..ts {
q.push_str(&format!("t q[{}];\ncx q[{}], q[{}];\n", i % 40, i % 40, (i + 7) % 40));
}
q.push_str("h q;\n");
json!({ "qasm": q, "amplitudes": ["0".repeat(40)], "observables": [] })
};
let p = plan(&wide(6), CAP).unwrap();
assert_eq!(p.method, "stabilizer_rank", "{}", p.why);
let many = plan(&wide(60), CAP).unwrap();
assert_eq!(many.method, "tensor_network", "too many T gates for one job's sum: {}", many.why);
assert!(many.why.contains("stabilizer_rank: 60 T gates"), "{}", many.why);
// Expectation values alone, past the dense state vector.
let mut e = wide(4);
e["amplitudes"] = json!([]);
e["observables"] = json!(["Z0 Z7", "X3"]);
assert_eq!(plan(&e, CAP).unwrap().method, "stabilizer_rank");
// The sampled mode states its bound and confidence, and its schema holds.
let mut s = wide(10);
s["method"] = json!("stabilizer_rank");
s["options"] = json!({ "terms": 2000, "confidence": 0.9 });
let out = run(&s, &plan(&s, CAP).unwrap()).unwrap();
assert_eq!(out["accuracy"]["class"], "estimate");
let b = out["accuracy"]["bound"].as_f64().unwrap();
assert!((b - crate::quantum_stabrank::sampled_bound(10, 2000, 0.1)).abs() < 1e-15 && out["amplitudes"][0]["bound"].as_f64() == Some(b), "{out}");
let errs = crate::engine::schema::Registry::engine().check("job-result.v1.json", &out);
assert!(errs.is_empty(), "{errs:?}");
// Refusals: counts, a gate past Clifford+T, noise, and options that do not read.
let refuse = |v: Value| plan(&v, CAP).unwrap_err();
assert!(refuse(json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "method": "stabilizer_rank", "shots": 10 })).contains("amplitudes and expectation values"));
assert!(refuse(json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nrx(0.3) q[0];\n", "method": "stabilizer_rank", "amplitudes": ["00"] })).contains("needs a Clifford+T circuit"));
assert!(refuse(json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "method": "stabilizer_rank", "amplitudes": ["00"], "noise": { "dephasing": 0.01 } })).contains("does not model noise"));
assert!(refuse(json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "method": "stabilizer_rank", "amplitudes": ["00"], "options": { "terms": 10, "epsilon": 0.1 } })).ends_with("[J003]"));
let fine = json!({ "qasm": "OPENQASM 3.0;\nqubit[2] q;\nt q[0];\n", "method": "stabilizer_rank", "amplitudes": ["00"], "options": { "epsilon": 0.1 } });
assert!(plan(&fine, CAP).unwrap().why.contains("drawn"));
}
}

View file

@ -281,6 +281,9 @@ pub mod quantum_receipt;
pub mod quantum_stabilizer;
#[cfg(feature = "quantum_tableau")]
pub mod quantum_tableau;
// Clifford+T circuits as sums of stabilizer states, on the phased tableau.
#[cfg(feature = "quantum_tableau")]
pub mod quantum_stabrank;
#[cfg(feature = "quantum_cutrank")]
pub mod quantum_cutrank;
#[cfg(feature = "quantum_cutrank_metal")]

View file

@ -1252,6 +1252,95 @@ impl Program {
Ok((bulk, tail.into_iter().map(|t| t.unwrap_or([one, zero, zero, one])).collect(), complex(phase.re / m, phase.im / m)))
}
/// The circuit as Clifford gates and T and T† gates, exactly, with its
/// global phase: the input of the `stabilizer_rank` method. A Clifford op
/// lowers as [`Program::clifford`] lowers it, its phase read off its
/// matrix; `t`, `tdg`, `p`, `rz`, `rx` and `ry` at odd multiples of π/4,
/// `cp` at odd multiples of π/2, `ccz` and `ccx` (seven T gates each) lower
/// to T gates. Refused for any other gate, and for a global phase that is
/// not a whole number of sixteenths of a turn.
#[cfg(feature = "quantum_qasm")]
pub fn clifford_t(&self) -> Result<crate::quantum_stabrank::CliffordT, String> {
use crate::quantum_stabrank::{CliffordT, CtGate};
use Kind::*;
let mut gates = Vec::new();
let mut phase: i64 = 0;
// P(mπ/4) for odd m: S^((m−1)/2) then T.
let eighth_z = |gates: &mut Vec<CtGate>, q: u32, m: i64| {
let m = m.rem_euclid(8);
let mut quarter = Vec::new();
quarter_z(&mut quarter, q, ((m - 1) / 2) as u32);
gates.extend(quarter.into_iter().map(CtGate::Clifford));
gates.push(CtGate::T(q));
};
// An odd number of eighth turns (π/4) within 1e-12, or none.
let odd_eighth = |theta: f64| -> Option<i64> {
let x = theta / std::f64::consts::FRAC_PI_4;
let m = x.round();
((x - m).abs() <= 1e-12 && (m as i64).rem_euclid(2) == 1).then_some(m as i64)
};
for op in &self.ops {
let q = &op.qubits;
if let Ok((cl, _)) = Self::lower_clifford(std::slice::from_ref(op)) {
let ph = lowering_phase(op, &cl);
let x = ph.im.atan2(ph.re) / (std::f64::consts::PI / 8.0);
let j = x.round();
if (x - j).abs() > 1e-9 || (ph.re.hypot(ph.im) - 1.0).abs() > 1e-9 {
return Err(format!("{:?}: its phase is not a whole number of sixteenths of a turn", op.kind));
}
phase += j as i64;
gates.extend(cl.into_iter().map(CtGate::Clifford));
continue;
}
let not = || format!("{:?}{} is not a Clifford+T gate", op.kind, if op.params.is_empty() { String::new() } else { format!("({})", op.params.iter().map(|x| format!("{x}")).collect::<Vec<_>>().join(", ")) });
match op.kind {
T => gates.push(CtGate::T(q[0])),
Tdg => gates.push(CtGate::Tdg(q[0])),
P => eighth_z(&mut gates, q[0], odd_eighth(op.params[0]).ok_or_else(not)?),
// Rz(θ) = e^{−iθ/2} P(θ); Rx and Ry are Rz between Cliffords.
Rz | Rx | Ry => {
let m = odd_eighth(op.params[0]).ok_or_else(not)?;
let (pre, post): (Vec<Clifford>, Vec<Clifford>) = match op.kind {
Rx => (vec![Clifford::H(q[0])], vec![Clifford::H(q[0])]),
Ry => (vec![Clifford::Sdg(q[0]), Clifford::H(q[0])], vec![Clifford::H(q[0]), Clifford::S(q[0])]),
_ => (vec![], vec![]),
};
gates.extend(pre.into_iter().map(CtGate::Clifford));
eighth_z(&mut gates, q[0], m);
gates.extend(post.into_iter().map(CtGate::Clifford));
phase -= m;
}
// CP(φ) = P(φ/2)_a P(φ/2)_b CX P(−φ/2)_b CX, φ/2 an odd eighth turn.
Cp => {
let x = op.params[0] / std::f64::consts::FRAC_PI_2;
let m = x.round();
if (x - m).abs() > 1e-12 || (m as i64).rem_euclid(2) != 1 {
return Err(not());
}
let (a, b, m) = (q[0], q[1], m as i64);
eighth_z(&mut gates, a, m);
eighth_z(&mut gates, b, m);
gates.push(CtGate::Clifford(Clifford::CX(a, b)));
eighth_z(&mut gates, b, -m);
gates.push(CtGate::Clifford(Clifford::CX(a, b)));
}
Ccz | Ccx => {
let (a, b, c) = (q[0], q[1], q[2]);
let cx = |x: u32, y: u32| CtGate::Clifford(Clifford::CX(x, y));
if op.kind == Ccx {
gates.push(CtGate::Clifford(Clifford::H(c)));
}
gates.extend([cx(b, c), CtGate::Tdg(c), cx(a, c), CtGate::T(c), cx(b, c), CtGate::Tdg(c), cx(a, c), CtGate::T(b), CtGate::T(c), cx(a, b), CtGate::T(a), CtGate::Tdg(b), cx(a, b)]);
if op.kind == Ccx {
gates.push(CtGate::Clifford(Clifford::H(c)));
}
}
_ => return Err(not()),
}
}
Ok(CliffordT { n: self.n_qubits, gates, phase: phase.rem_euclid(16) as u8 })
}
/// The Clifford lowering, and where each op's gates end in it.
#[cfg(feature = "quantum_qasm")]
fn clifford_bounds(&self) -> Result<(Vec<Clifford>, Vec<usize>), String> {

View file

@ -246,6 +246,207 @@ pub fn replay(re: &mut [f64], im: &mut [f64], launches: &[Launch]) {
}
}
/// The index bits `l` reads and writes, as its parameters name them: a
/// diagonal's in its gate order, a one-qubit gate's one, a wider gate's
/// ascending.
pub fn launch_bits(l: &Launch) -> Vec<u32> {
match l {
Launch::Diag { bitpos, .. } => bitpos.clone(),
Launch::One { s, .. } => vec![s.trailing_zeros()],
Launch::Many { sorted, .. } => sorted.clone(),
}
}
/// Whether every launch acts within a state of `2^n` amplitudes: each bit it
/// names below `n`, a one-qubit gate's stride a power of two, and a wider
/// gate's offsets made of its own bits. A device that runs a launch outside
/// its state writes past its buffers, so a runner checks this first.
pub fn fits(launches: &[Launch], n: u32) -> bool {
launches.iter().all(|l| match l {
Launch::Diag { bitpos, diag } => bitpos.len() <= MAX_D.trailing_zeros() as usize && bitpos.iter().all(|&p| p < n) && diag.len() == 1 << bitpos.len(),
Launch::One { s, .. } => s.is_power_of_two() && s.trailing_zeros() < n,
Launch::Many { sorted, offsets, m } => {
let mask = sorted.iter().fold(0u64, |a, &p| a | (1u64 << p.min(63)));
sorted.len() <= MAX_D.trailing_zeros() as usize && offsets.len() == 1 << sorted.len() && sorted.iter().all(|&p| p < n) && sorted.windows(2).all(|w| w[0] < w[1]) && m.len() == offsets.len() * offsets.len() && offsets.iter().all(|&o| o & !mask == 0)
}
})
}
/// `l` with each index bit `b` moved to `pos[b]`: the same formula on the same
/// amplitudes, held at other indices. Each group keeps its members in their
/// local order, so every output is computed exactly as before.
pub fn remap(l: &Launch, pos: &[u32]) -> Launch {
let bits = |o: u64| (0..64u32).filter(|&b| o >> b & 1 == 1).fold(0u64, |a, b| a | (1u64 << pos[b as usize]));
match l {
Launch::Diag { bitpos, diag } => Launch::Diag { bitpos: bitpos.iter().map(|&b| pos[b as usize]).collect(), diag: diag.clone() },
Launch::One { s, m } => Launch::One { s: 1u64 << pos[s.trailing_zeros() as usize], m: *m },
Launch::Many { sorted, offsets, m } => {
let mut s: Vec<u32> = sorted.iter().map(|&b| pos[b as usize]).collect();
s.sort_unstable();
Launch::Many { sorted: s, offsets: offsets.iter().map(|&o| bits(o)).collect(), m: m.clone() }
}
}
}
/// Exchanges index bits `a` and `b` of a state: the amplitude at each index
/// whose two bits differ moves to the index with them swapped. Data movement
/// alone; no amplitude is computed.
pub fn swap_bits(re: &mut [f64], im: &mut [f64], a: u32, b: u32) {
if a == b {
return;
}
let (lo, hi) = (a.min(b), a.max(b));
let (l, h) = (1usize << lo, 1usize << hi);
for x in 0..re.len() {
if x & h != 0 && x & l == 0 {
let y = x ^ h ^ l;
re.swap(x, y);
im.swap(x, y);
}
}
}
/// What a partitioned run did ([`run_split`]).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SplitRun {
/// The runs of launches each partition took in turn, between exchanges.
pub segments: u64,
/// The index bits exchanged between a partition bit and a local one, each
/// through the host's copy of the state.
pub exchanges: u64,
}
/// A partition's runner: `launches`, every index bit below the partition's
/// own qubits, on its amplitudes, in place.
pub type PartRunner<'a> = &'a (dyn Fn(&mut [f64], &mut [f64], &[Launch]) -> Result<(), String> + Sync);
/// Runs `launches` (planned for the whole state, [`plan`]) on `re`/`im` held
/// as `owner.len()` partitions by the top index bits: partition `i` is the
/// amplitudes whose top `log2(owner.len())` bits read `i`, run by
/// `runners[owner[i]]`.
///
/// A launch whose bits are all below the partitions' bits runs on each
/// partition, the same formula on its own amplitudes. One that touches a
/// partition bit first has that bit exchanged with a local bit it does not
/// touch, through these arrays, and runs on the new layout ([`remap`]).
/// Every amplitude goes through the formulas [`replay`] applies to it, in the
/// same order, so a runner that is [`replay`] bit for bit gives the state
/// [`replay`] gives. The layout is put back at the end.
///
/// Partitions that share a runner run one after another; each runner runs on
/// a thread of its own where the target has threads. Refused: a partition count that is not a power of
/// two of at least two, a state smaller than that, an owner past the
/// runners, or a launch wider than a partition holds.
pub fn run_split(re: &mut [f64], im: &mut [f64], launches: &[Launch], owner: &[usize], runners: &[PartRunner<'_>]) -> Result<SplitRun, String> {
let len = re.len();
if !len.is_power_of_two() || im.len() != len {
return Err("the state is not 2^n amplitudes".into());
}
let parts = owner.len();
if parts < 2 || !parts.is_power_of_two() {
return Err(format!("{parts} partitions: a partitioned state takes a power of two of them, at least two"));
}
let n = len.trailing_zeros();
let p = parts.trailing_zeros();
if p >= n {
return Err(format!("{parts} partitions of a {n}-qubit state leave no qubit in a partition"));
}
if let Some(&o) = owner.iter().find(|&&o| o >= runners.len()) {
return Err(format!("partition owner {o} names no runner"));
}
if !fits(launches, n) {
return Err("a launch acts outside the state".into());
}
let local = n - p;
// pos[b]: where logical index bit b is held; at[q]: which logical bit
// physical bit q holds.
let mut pos: Vec<u32> = (0..n).collect();
let mut at: Vec<u32> = (0..n).collect();
let mut run = SplitRun::default();
let mut segment: Vec<Launch> = Vec::new();
let flush = |re: &mut [f64], im: &mut [f64], segment: &mut Vec<Launch>, run: &mut SplitRun| -> Result<(), String> {
if segment.is_empty() {
return Ok(());
}
run.segments += 1;
let size = 1usize << local;
let mut mine: Vec<Vec<(&mut [f64], &mut [f64])>> = (0..runners.len()).map(|_| Vec::new()).collect();
for (i, (r, m)) in re.chunks_mut(size).zip(im.chunks_mut(size)).enumerate() {
mine[owner[i]].push((r, m));
}
let seg: &[Launch] = segment;
// One runner, or a target with no threads (WebAssembly): one after
// another, the same arithmetic on each partition.
if cfg!(target_arch = "wasm32") || mine.iter().filter(|p| !p.is_empty()).count() < 2 {
for (k, parts) in mine.into_iter().enumerate() {
for (r, m) in parts {
runners[k](r, m, seg)?;
}
}
segment.clear();
return Ok(());
}
let results: Vec<Result<(), String>> = std::thread::scope(|scope| {
let handles: Vec<_> = mine
.into_iter()
.enumerate()
.filter(|(_, parts)| !parts.is_empty())
.map(|(k, parts)| {
let runner = runners[k];
scope.spawn(move || {
for (r, m) in parts {
runner(r, m, seg)?;
}
Ok(())
})
})
.collect();
handles.into_iter().map(|h| h.join().unwrap_or_else(|_| Err("a partition's runner panicked".into()))).collect()
});
segment.clear();
results.into_iter().collect::<Result<Vec<()>, String>>().map(|_| ())
};
for l in launches {
let bits = launch_bits(l);
if bits.len() as u32 > local {
return Err(format!("a gate on {} qubits is wider than a partition's {local}", bits.len()));
}
if bits.iter().any(|&b| pos[b as usize] >= local) {
flush(re, im, &mut segment, &mut run)?;
for &b in &bits {
let from = pos[b as usize];
if from < local {
continue;
}
// The highest local bit this launch leaves alone.
let to = (0..local).rev().find(|&q| !bits.iter().any(|&c| pos[c as usize] == q)).ok_or("no local bit is free for an exchange")?;
swap_bits(re, im, from, to);
let other = at[to as usize];
pos[other as usize] = from;
at[from as usize] = other;
pos[b as usize] = to;
at[to as usize] = b;
run.exchanges += 1;
}
}
segment.push(remap(l, &pos));
}
flush(re, im, &mut segment, &mut run)?;
// The layout back: logical bit b at physical bit b.
for b in 0..n {
let from = pos[b as usize];
if from != b {
swap_bits(re, im, from, b);
let other = at[b as usize];
pos[other as usize] = from;
at[from as usize] = other;
pos[b as usize] = b;
at[b as usize] = b;
}
}
Ok(run)
}
/// A corpus of gate lists: the state vector's named gates at angles drawn
/// from a seed, run at fusion widths 0, 2 and 3 in turn.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@ -453,6 +654,124 @@ mod tests {
assert!(differs, "the corpus cannot tell a reassociated formula from the reference");
}
/// The CPU's own formulas as a partition's runner.
fn cpu(re: &mut [f64], im: &mut [f64], l: &[Launch]) -> Result<(), String> {
replay(re, im, l);
Ok(())
}
/// Brickwork on `n` qubits whose gates reach the top qubits often, so a
/// partitioned run must exchange.
fn reaching(n: u32, layers: u32) -> Vec<Gate> {
let mut gates = Vec::new();
let mut a = 0.3f64;
for layer in 0..layers {
for q in 0..n {
a += 0.917;
gates.push(match (q + layer) % 6 {
0 => Gate::rx(q, a),
1 => Gate::ry(q, a),
2 => Gate::rz(q, a),
3 => Gate::sqrt_w(q),
4 => Gate::h(q),
_ => Gate::t(q),
});
}
let mut q = layer % 2;
while q + 1 < n {
gates.push(if q % 3 == 0 { Gate::fsim(q, q + 1, a, 0.5 * a) } else { Gate::cz(q, q + 1) });
q += 2;
}
gates.push(Gate::iswap(0, n - 1));
gates.push(Gate::fsim(1, n - 2, a, 0.25 * a));
}
gates
}
/// Ground truth first: the partitioned run on CPU partitions is the CPU's
/// state vector bit for bit, over the pinned corpus at every partition
/// count a case's width admits and on wide states whose gates reach the
/// partition bits, with exchanges made.
#[test]
fn a_partitioned_run_is_the_cpu_run_bit_for_bit() {
let mut runs = 0;
let mut exchanged = 0;
let runner: PartRunner<'_> = &cpu;
for case in SV_DEFAULT.cases() {
let launches = plan(case.n, &case.gates, case.width).unwrap();
let widest = launches.iter().map(|l| launch_bits(l).len() as u32).max().unwrap_or(1);
let want = bits(&case.reference().unwrap());
for p in 1..case.n {
if case.n - p < widest {
break;
}
let dim = 1usize << case.n;
let (mut re, mut im) = (vec![0.0; dim], vec![0.0; dim]);
re[0] = 1.0;
let owner = vec![0usize; 1 << p];
let r = run_split(&mut re, &mut im, &launches, &owner, &[runner]).unwrap();
assert_eq!(bits(&state_output(&re, &im)), want, "n={} width={} partitions={}", case.n, case.width, 1 << p);
runs += 1;
exchanged += u64::from(r.exchanges > 0);
}
}
for (n, width, p) in [(12u32, 0usize, 1u32), (12, 3, 2), (14, 2, 3), (16, 3, 2)] {
let gates = reaching(n, 6);
let launches = plan(n, &gates, width).unwrap();
let mut sv = StateVector::zero(n).unwrap();
sv.run(&gates, width).unwrap();
let dim = 1usize << n;
let (mut re, mut im) = (vec![0.0; dim], vec![0.0; dim]);
re[0] = 1.0;
// Two runners, owning alternate partitions.
let owner: Vec<usize> = (0..1usize << p).map(|i| i % 2).collect();
let r = run_split(&mut re, &mut im, &launches, &owner, &[runner, runner]).unwrap();
assert!(r.exchanges > 0 && r.segments > 1, "n={n}: {r:?}");
let same = |a: &[f64], b: &[f64]| a.iter().zip(b).all(|(x, y)| x.to_bits() == y.to_bits());
assert!(same(&re, &sv.re) && same(&im, &sv.im), "n={n} width={width} partitions={}", 1 << p);
runs += 1;
exchanged += 1;
}
assert!(runs > 100 && exchanged > 50, "{runs} runs, {exchanged} with exchanges");
}
#[test]
fn exchanges_and_remaps_move_amplitudes_and_compute_nothing() {
let (mut re, mut im): (Vec<f64>, Vec<f64>) = ((0..32).map(|x| x as f64).collect(), (0..32).map(|x| -(x as f64)).collect());
swap_bits(&mut re, &mut im, 0, 4);
for x in 0..32usize {
let y = (x & !0b10001) | ((x & 1) << 4) | ((x >> 4) & 1);
assert_eq!((re[x], im[x]), (y as f64, -(y as f64)));
}
swap_bits(&mut re, &mut im, 4, 0);
assert!(re.iter().enumerate().all(|(x, &v)| v == x as f64));
// A launch remapped by the identity is itself, and every remap fits.
let launches = plan(5, &reaching(5, 2), 3).unwrap();
let id: Vec<u32> = (0..5).collect();
let rev: Vec<u32> = (0..5).rev().collect();
for l in &launches {
assert_eq!(&remap(l, &id), l);
assert!(fits(std::slice::from_ref(&remap(l, &rev)), 5));
}
assert!(fits(&launches, 5) && !fits(&launches, 4));
}
#[test]
fn a_split_refuses_what_it_cannot_hold() {
let runner: PartRunner<'_> = &cpu;
let launches = plan(4, &[Gate::fsim(0, 1, 0.3, 0.1), Gate::h(3)], 3).unwrap();
let (mut re, mut im) = (vec![0.0; 16], vec![0.0; 16]);
re[0] = 1.0;
assert!(run_split(&mut re, &mut im, &launches, &[0, 0, 0], &[runner]).unwrap_err().contains("power of two"));
assert!(run_split(&mut re, &mut im, &launches, &[0], &[runner]).unwrap_err().contains("power of two"));
assert!(run_split(&mut re, &mut im, &launches, &[0, 1], &[runner]).unwrap_err().contains("names no runner"));
// Eight partitions of four qubits leave one qubit each: a two-qubit gate does not fit.
assert!(run_split(&mut re, &mut im, &launches, &[0; 8], &[runner]).unwrap_err().contains("wider than a partition"));
let failing = |_: &mut [f64], _: &mut [f64], _: &[Launch]| -> Result<(), String> { Err("device lost".into()) };
let f: PartRunner<'_> = &failing;
assert_eq!(run_split(&mut re, &mut im, &launches, &[0, 1], &[runner, f]).unwrap_err(), "device lost");
}
#[test]
fn plans_refuse_what_the_state_vector_refuses() {
let bad = Gate { qubits: vec![0, 0], matrix: vec![C { re: 1.0, im: 0.0 }; 16] };

View file

@ -686,25 +686,49 @@ impl CudaSv {
&self.implementation
}
/// The most qubits a state of this member holds: two f64 arrays of
/// `2^n` in half its free memory at discovery.
pub fn max_qubits(&self) -> u32 {
u32::from(self.member.max_qubits.saturating_sub(1))
}
/// `gates` on `n` qubits from `|0…0⟩`, fused to `width` as the CPU fuses
/// them: the real and imaginary parts.
pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec<f64>, Vec<f64>), CudaError> {
use crate::quantum_fabric::sv64::{Launch, plan};
use crate::quantum_fabric::sv64::plan;
let launches = plan(n, gates, width).map_err(|_| CudaError::Internal("a gate the state vector refuses"))?;
// Two f64 arrays of 2^n.
if n == 0 || n > u32::from(self.member.max_qubits.saturating_sub(1)) {
if n == 0 || n > self.max_qubits() {
return Err(CudaError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) });
}
let len = 1usize << n;
let mut re = vec![0.0f64; len];
re[0] = 1.0;
self.run_launches(&re, &vec![0.0f64; len], &launches)
}
/// `launches` ([`sv64::plan`](crate::quantum_fabric::sv64::plan)'s, or a
/// partition's) on the state `(re, im)`: the state after them. Refused
/// unless the state is `2^n` amplitudes this member holds and every launch
/// acts within it.
pub fn run_launches(&self, re: &[f64], im: &[f64], launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec<f64>, Vec<f64>), CudaError> {
use crate::quantum_fabric::sv64::{Launch, fits};
let len = re.len();
if !len.is_power_of_two() || len < 2 || im.len() != len {
return Err(CudaError::Internal("a state that is not 2^n amplitudes, n at least 1"));
}
let n = len.trailing_zeros();
if n > self.max_qubits() {
return Err(CudaError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) });
}
if !fits(launches, n) {
return Err(CudaError::Internal("a launch outside the state"));
}
let stream = &self.member.stream;
let mut init = vec![0.0f64; len];
init[0] = 1.0;
let mut re = stream.clone_htod(&init).map_err(driver)?;
init[0] = 0.0;
let mut im = stream.clone_htod(&init).map_err(driver)?;
drop(init);
let mut re = stream.clone_htod(re).map_err(driver)?;
let mut im = stream.clone_htod(im).map_err(driver)?;
let flat = |m: &[crate::linalg::C]| -> Vec<f64> { m.iter().flat_map(|c| [c.re, c.im]).collect() };
for l in &launches {
for l in launches {
match l {
Launch::Diag { bitpos, diag } => {
let bp = stream.clone_htod(bitpos).map_err(driver)?;

View file

@ -949,27 +949,51 @@ impl HipSv {
&self.implementation
}
/// The most qubits a state of this member holds: two f64 arrays of
/// `2^n` in half its free memory at discovery.
pub fn max_qubits(&self) -> u32 {
u32::from(self.member.max_qubits.saturating_sub(1))
}
/// `gates` on `n` qubits from `|0…0⟩`, fused to `width` as the CPU fuses
/// them: the real and imaginary parts.
pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec<f64>, Vec<f64>), HipError> {
use crate::quantum_fabric::sv64::{Launch, plan};
use crate::quantum_fabric::sv64::plan;
let launches = plan(n, gates, width).map_err(|_| HipError::Internal("a gate the state vector refuses"))?;
// Two f64 arrays of 2^n.
if n == 0 || n > u32::from(self.member.max_qubits.saturating_sub(1)) {
if n == 0 || n > self.max_qubits() {
return Err(HipError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) });
}
let len = 1usize << n;
let mut re = vec![0.0f64; len];
re[0] = 1.0;
self.run_launches(&re, &vec![0.0f64; len], &launches)
}
/// `launches` ([`sv64::plan`](crate::quantum_fabric::sv64::plan)'s, or a
/// partition's) on the state `(re, im)`: the state after them. Refused
/// unless the state is `2^n` amplitudes this member holds and every launch
/// acts within it.
pub fn run_launches(&self, re: &[f64], im: &[f64], launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec<f64>, Vec<f64>), HipError> {
use crate::quantum_fabric::sv64::{Launch, fits};
let len = re.len();
if !len.is_power_of_two() || len < 2 || im.len() != len {
return Err(HipError::Internal("a state that is not 2^n amplitudes, n at least 1"));
}
let n = len.trailing_zeros();
if n > self.max_qubits() {
return Err(HipError::TooWide { n_qubits: n.min(255) as u8, max: self.member.max_qubits.saturating_sub(1) });
}
if !fits(launches, n) {
return Err(HipError::Internal("a launch outside the state"));
}
let a = self.member.api;
self.member.select()?;
let len = 1usize << n;
let mut init = vec![0.0f64; len];
init[0] = 1.0;
let re = DeviceBuffer::upload(a, &init)?;
init[0] = 0.0;
let im = DeviceBuffer::upload(a, &init)?;
drop(init);
let re = DeviceBuffer::upload(a, re)?;
let im = DeviceBuffer::upload(a, im)?;
let flat = |m: &[crate::linalg::C]| -> Vec<f64> { m.iter().flat_map(|c| [c.re, c.im]).collect() };
let [diag, one, many] = [0, 1, 2].map(|k| self.kernels.functions[k]);
for l in &launches {
for l in launches {
match l {
Launch::Diag { bitpos, diag: d } => {
let bp = DeviceBuffer::upload(a, bitpos)?;

View file

@ -722,26 +722,51 @@ impl OpenClSv {
&self.implementation
}
/// The most qubits a state of this member holds: two f64 arrays of
/// `2^n`, each within one allocation and together within half its memory.
pub fn max_qubits(&self) -> u32 {
u32::from(self.member.max_qubits.saturating_sub(1))
}
/// `gates` on `n` qubits from `|0…0⟩`, fused to `width` as the CPU fuses
/// them: the real and imaginary parts.
pub fn run(&self, n: u32, gates: &[crate::gates::Gate], width: usize) -> Result<(Vec<f64>, Vec<f64>), OpenClError> {
use crate::quantum_fabric::sv64::{Launch, plan};
use crate::quantum_fabric::sv64::plan;
let launches = plan(n, gates, width).map_err(|_| OpenClError::Internal("a gate the state vector refuses"))?;
let max = self.member.max_qubits.saturating_sub(1);
if n == 0 || n > u32::from(max) {
return Err(OpenClError::TooWide { n_qubits: n.min(255) as u8, max });
}
let len = 1usize << n;
let mut init = vec![0.0f64; len];
init[0] = 1.0;
let re = self.member.upload(&init)?;
init[0] = 0.0;
let im = self.member.upload(&init)?;
drop(init);
let mut re = vec![0.0f64; len];
re[0] = 1.0;
self.run_launches(&re, &vec![0.0f64; len], &launches)
}
/// `launches` ([`sv64::plan`](crate::quantum_fabric::sv64::plan)'s, or a
/// partition's) on the state `(re, im)`: the state after them. Refused
/// unless the state is `2^n` amplitudes this member holds and every launch
/// acts within it.
pub fn run_launches(&self, re: &[f64], im: &[f64], launches: &[crate::quantum_fabric::sv64::Launch]) -> Result<(Vec<f64>, Vec<f64>), OpenClError> {
use crate::quantum_fabric::sv64::{Launch, fits};
let len = re.len();
if !len.is_power_of_two() || len < 2 || im.len() != len {
return Err(OpenClError::Internal("a state that is not 2^n amplitudes, n at least 1"));
}
let n = len.trailing_zeros();
let max = self.member.max_qubits.saturating_sub(1);
if n > u32::from(max) {
return Err(OpenClError::TooWide { n_qubits: n.min(255) as u8, max });
}
if !fits(launches, n) {
return Err(OpenClError::Internal("a launch outside the state"));
}
let re = self.member.upload(re)?;
let im = self.member.upload(im)?;
let flat = |m: &[crate::linalg::C]| -> Vec<f64> { m.iter().flat_map(|c| [c.re, c.im]).collect() };
{
let k = self.kernels.lock().unwrap_or_else(|p| p.into_inner());
for l in &launches {
for l in launches {
match l {
Launch::Diag { bitpos, diag } => {
let bp = self.member.upload(bitpos)?;

View file

@ -0,0 +1,644 @@
//! Clifford+T circuits as sums of stabilizer states — `wai.quantum.stabrank`.
//!
//! A T gate is a sum of two Clifford gates,
//! `T = (e^{iπ/8} I + e^{−iπ/8} S) / (2 cos π/8)`, and `T†` is its conjugate,
//! `(e^{−iπ/8} I + e^{iπ/8} S†) / (2 cos π/8)`. A circuit with `t` of them
//! is therefore a sum of `2^t` Clifford circuits, each a stabilizer state
//! whose every amplitude the phased tableau gives exactly
//! ([`PhasedTableau::amplitudes`]). Both coefficients of a T gate have the same
//! magnitude, `1 / (2 cos π/8)`, the least the stabilizer extent of a T gate
//! allows, so every branch weighs the same.
//!
//! - **Exact** ([`amplitudes`]). Every branch, walked depth first so branches
//! share the Clifford gates before their first difference. Each term is
//! `2^{−k/2} e^{iπj/8}`, held as a whole-number count per `(k, j)`
//! ([`ExactSum`]), so the sum is the same however the branches are split
//! between threads, and it is evaluated once, in a fixed order, in double
//! precision: no approximation beyond that rounding. The terms grow as `2^t`.
//! - **Expectation values** ([`CliffordT::sandwich`]). `⟨ψ|P|ψ⟩` is the
//! amplitude `⟨0|U†PU|0⟩` of one circuit with twice the T gates, so its terms
//! grow as `4^t`.
//! - **Sampled** ([`sampled`]). `K` branches drawn uniformly, each with its
//! coefficient's phase, scaled by `‖c‖₁ / K` with `‖c‖₁ = sec(π/8)^t`: an
//! unbiased estimate `ψ̃` of the state whose mean squared distance is exactly
//! `(‖c‖₁² − 1) / K` (the sparsification lemma). By Markov's inequality,
//! `‖ψ̃ − ψ‖ ≤ ε = sqrt((sec²(π/8)^t − 1) / (K δ))` with probability at least
//! `1 − δ` over the draw ([`sampled_bound`]); every amplitude, and every
//! amplitude of the sandwich (an expectation value), is within `ε` of the
//! exact one with that probability. The bound is computed from `t`, `K` and
//! `δ`, never from the result. The draw is seeded, each term from a stream of
//! its own, so the same seed gives the same bits at any thread count.
//!
//! # Checked
//!
//! Against the dense state vector of the same program, on random Clifford+T
//! circuits up to 7 qubits and 10 T gates: every amplitude, phase and all,
//! within `10⁻¹²`, the fidelity within `10⁻¹²` of 1, and every expectation
//! value within `10⁻¹²`. The sampled mode's bound holds in at least the
//! fraction `1 − δ` of seeds, measured on the whole state.
use crate::quantum_tableau::{Clifford, PhasedTableau, TableauError};
/// `1 / (2 cos π/8)`: the magnitude of each coefficient of a T gate, correctly
/// rounded.
pub const T_COEFFICIENT: f64 = 0.541196100146197;
/// `sec(π/8)`: `‖c‖₁` of one T gate's two coefficients, correctly rounded.
pub const SEC_PI_8: f64 = 1.082392200292394;
/// `sec²(π/8) = 4 − 2√2`: the stabilizer extent of a T gate, correctly rounded.
pub const SEC2_PI_8: f64 = 1.17157287525381;
/// `(cos πj/8, sin πj/8)` for `j` in `0..8`, correctly rounded.
const TURN16: [(f64, f64); 8] = [
(1.0, 0.0),
(0.9238795325112867, 0.3826834323650898),
(std::f64::consts::FRAC_1_SQRT_2, std::f64::consts::FRAC_1_SQRT_2),
(0.3826834323650898, 0.9238795325112867),
(0.0, 1.0),
(-0.3826834323650898, 0.9238795325112867),
(-std::f64::consts::FRAC_1_SQRT_2, std::f64::consts::FRAC_1_SQRT_2),
(-0.9238795325112867, 0.3826834323650898),
];
/// One gate of a Clifford+T circuit.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CtGate {
Clifford(Clifford),
T(u32),
Tdg(u32),
}
/// A Clifford+T circuit on `n` qubits from `|0…0⟩`, its matrix the product of
/// its gates times `e^{iπ·phase/8}`.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CliffordT {
pub n: u32,
pub gates: Vec<CtGate>,
/// The global phase, in sixteenths of a turn.
pub phase: u8,
}
/// Why a Clifford+T sum could not be taken.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum StabrankError {
Tableau(TableauError),
/// A Pauli factor that is not `X`, `Y`, `Z` or `I`, or a qubit named
/// twice or out of range.
BadPauli(String),
}
impl std::fmt::Display for StabrankError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
StabrankError::Tableau(e) => write!(f, "{e}"),
StabrankError::BadPauli(p) => write!(f, "{p}: not a Pauli string of this circuit"),
}
}
}
impl std::error::Error for StabrankError {}
impl From<TableauError> for StabrankError {
fn from(e: TableauError) -> StabrankError {
StabrankError::Tableau(e)
}
}
fn inverse(c: Clifford) -> Clifford {
match c {
Clifford::S(q) => Clifford::Sdg(q),
Clifford::Sdg(q) => Clifford::S(q),
Clifford::SX(q) => Clifford::SXdg(q),
Clifford::SXdg(q) => Clifford::SX(q),
other => other,
}
}
impl CliffordT {
/// The T and T† gates.
pub fn t_count(&self) -> u32 {
self.gates.iter().filter(|g| !matches!(g, CtGate::Clifford(_))).count() as u32
}
/// The inverse: each gate inverted, in reverse order, the phase negated.
pub fn inverse(&self) -> CliffordT {
let gates = self
.gates
.iter()
.rev()
.map(|&g| match g {
CtGate::Clifford(c) => CtGate::Clifford(inverse(c)),
CtGate::T(q) => CtGate::Tdg(q),
CtGate::Tdg(q) => CtGate::T(q),
})
.collect();
CliffordT { n: self.n, gates, phase: (16 - self.phase % 16) % 16 }
}
/// `U† P U` as one circuit, for the Pauli string `P` (`(qubit, 'X'|'Y'|'Z'|'I')`):
/// its amplitude at `|0…0⟩` is `⟨ψ|P|ψ⟩`, with twice the T gates.
pub fn sandwich(&self, pauli: &[(u32, char)]) -> Result<CliffordT, StabrankError> {
let mut gates = self.gates.clone();
let mut seen = std::collections::BTreeSet::new();
for &(q, p) in pauli {
if q >= self.n || !seen.insert(q) {
return Err(StabrankError::BadPauli(format!("{p}{q}")));
}
match p.to_ascii_uppercase() {
'X' => gates.push(CtGate::Clifford(Clifford::X(q))),
'Y' => gates.push(CtGate::Clifford(Clifford::Y(q))),
'Z' => gates.push(CtGate::Clifford(Clifford::Z(q))),
'I' => {}
other => return Err(StabrankError::BadPauli(format!("{other}{q}"))),
}
}
gates.extend(self.inverse().gates);
Ok(CliffordT { n: self.n, gates, phase: 0 })
}
}
/// A sum of terms `2^{−k/2} e^{iπj/8}`, held as whole numbers: entry `[k][j]`
/// counts the terms of magnitude `2^{−k/2}` at `j` sixteenths of a turn, a
/// term at `j + 8` counted as minus one at `j`. Sums merge by addition, so a
/// sum is the same however its terms were split.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ExactSum {
counts: Vec<[i64; 8]>,
}
impl ExactSum {
/// Adds `2^{−k/2} e^{iπ·sixteenth/8}`.
pub fn add(&mut self, k: u32, sixteenth: u8) {
let k = k as usize;
if self.counts.len() <= k {
self.counts.resize(k + 1, [0; 8]);
}
let j = sixteenth % 16;
if j < 8 {
self.counts[k][j as usize] += 1;
} else {
self.counts[k][(j - 8) as usize] -= 1;
}
}
/// Adds every term of `other`.
pub fn merge(&mut self, other: &ExactSum) {
if self.counts.len() < other.counts.len() {
self.counts.resize(other.counts.len(), [0; 8]);
}
for (a, b) in self.counts.iter_mut().zip(&other.counts) {
for j in 0..8 {
a[j] += b[j];
}
}
}
/// The terms counted, net of cancellations.
pub fn is_zero(&self) -> bool {
self.counts.iter().all(|c| c.iter().all(|&x| x == 0))
}
/// The sum times `scale`, in double precision, its terms added in a fixed
/// order: the same bits on every machine.
pub fn value(&self, scale: f64) -> (f64, f64) {
let (mut re, mut im) = (0.0f64, 0.0f64);
for (k, c) in self.counts.iter().enumerate() {
let (mut kr, mut ki) = (0.0f64, 0.0f64);
for (j, &n) in c.iter().enumerate() {
kr += n as f64 * TURN16[j].0;
ki += n as f64 * TURN16[j].1;
}
let m = half_root_power(k as u32);
re += m * kr;
im += m * ki;
}
(re * scale, im * scale)
}
}
/// `2^{−k/2}`, exactly as a power of two times `1/√2` for odd `k`.
fn half_root_power(k: u32) -> f64 {
let mut m = if k % 2 == 1 { std::f64::consts::FRAC_1_SQRT_2 } else { 1.0 };
for _ in 0..k / 2 {
m *= 0.5;
}
m
}
/// `x` multiplied by itself `t` times, one multiply at a time: the same bits
/// on every machine.
fn power(x: f64, t: u32) -> f64 {
let mut p = 1.0f64;
for _ in 0..t {
p *= x;
}
p
}
/// The scale an exact sum of a circuit with `t` T gates is evaluated at.
pub fn exact_scale(t: u32) -> f64 {
power(T_COEFFICIENT, t)
}
/// The scale a sampled sum of `terms` branches of a circuit with `t` T gates
/// is evaluated at: `sec(π/8)^t / terms`.
pub fn sampled_scale(t: u32, terms: u64) -> f64 {
power(SEC_PI_8, t) / terms as f64
}
/// The bound the sampled mode states: `‖ψ̃ − ψ‖ ≤ sqrt((sec²(π/8)^t − 1) / (terms · δ))`
/// with probability at least `1 − δ`, for a normalised `ψ`.
pub fn sampled_bound(t: u32, terms: u64, delta: f64) -> f64 {
((power(SEC2_PI_8, t) - 1.0).max(0.0) / (terms.max(1) as f64 * delta)).sqrt()
}
/// The terms the sampled mode needs for the bound `epsilon` at `1 − δ`.
pub fn terms_for(t: u32, epsilon: f64, delta: f64) -> f64 {
((power(SEC2_PI_8, t) - 1.0).max(0.0) / (epsilon * epsilon * delta)).ceil()
}
/// One branch point: the Clifford gate the S branch applies, and the
/// sixteenths each branch's coefficient turns by.
fn branch(g: CtGate) -> Option<(Clifford, u8, u8)> {
match g {
CtGate::Clifford(_) => None,
CtGate::T(q) => Some((Clifford::S(q), 1, 15)),
CtGate::Tdg(q) => Some((Clifford::Sdg(q), 15, 1)),
}
}
/// A leaf: its amplitudes at `xs` added to `sums`.
fn leaf(st: &PhasedTableau, phase: u8, xs: &[Vec<bool>], sums: &mut [ExactSum]) -> Result<(), TableauError> {
for (s, a) in sums.iter_mut().zip(st.amplitudes(xs)?) {
if let Some((k, m)) = a {
s.add(k, (phase + 2 * m) % 16);
}
}
Ok(())
}
/// Every branch below `(st, pos, phase)`, depth first.
fn walk(c: &CliffordT, mut st: PhasedTableau, mut pos: usize, mut phase: u8, xs: &[Vec<bool>], sums: &mut [ExactSum]) -> Result<(), TableauError> {
while pos < c.gates.len() {
match branch(c.gates[pos]) {
None => {
if let CtGate::Clifford(g) = c.gates[pos] {
st.apply(g)?;
}
}
Some((s, keep, turn)) => {
walk(c, st.clone(), pos + 1, (phase + keep) % 16, xs, sums)?;
st.apply(s)?;
phase = (phase + turn) % 16;
}
}
pos += 1;
}
leaf(&st, phase, xs, sums)
}
/// The branches down to the first `depth` branch points, as starting points
/// for [`walk`]: each a state, the gate it stops at, and its phase.
fn prefixes(c: &CliffordT, depth: u32) -> Result<Vec<(PhasedTableau, usize, u8)>, TableauError> {
let mut out = vec![(PhasedTableau::new(c.n)?, 0usize, c.phase % 16)];
for _ in 0..depth {
let mut next = Vec::with_capacity(2 * out.len());
let mut grew = false;
for (mut st, mut pos, phase) in out {
while pos < c.gates.len() && branch(c.gates[pos]).is_none() {
if let CtGate::Clifford(g) = c.gates[pos] {
st.apply(g)?;
}
pos += 1;
}
match c.gates.get(pos).and_then(|&g| branch(g)) {
Some((s, keep, turn)) => {
let mut other = st.clone();
next.push((st, pos + 1, (phase + keep) % 16));
other.apply(s)?;
next.push((other, pos + 1, (phase + turn) % 16));
grew = true;
}
None => next.push((st, pos, phase)),
}
}
out = next;
if !grew {
break;
}
}
Ok(out)
}
/// Runs `work(i, sums)` for `i` in `0..tasks` on up to `threads` threads, each
/// with sums of its own for `width` amplitudes, and adds them: the same sums
/// at any thread count.
fn spread(tasks: usize, width: usize, threads: usize, work: &(dyn Fn(usize, &mut [ExactSum]) -> Result<(), TableauError> + Sync)) -> Result<Vec<ExactSum>, TableauError> {
let threads = if cfg!(target_arch = "wasm32") { 1 } else { threads.clamp(1, tasks.max(1)) };
let next = std::sync::atomic::AtomicUsize::new(0);
let run = || -> Result<Vec<ExactSum>, TableauError> {
let mut sums = vec![ExactSum::default(); width];
loop {
let i = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if i >= tasks {
return Ok(sums);
}
work(i, &mut sums)?;
}
};
let parts: Vec<Result<Vec<ExactSum>, TableauError>> = if threads == 1 {
vec![run()]
} else {
std::thread::scope(|s| {
let handles: Vec<_> = (0..threads).map(|_| s.spawn(&run)).collect();
handles.into_iter().map(|h| h.join().expect("a branch walk does not panic")).collect()
})
};
let mut total = vec![ExactSum::default(); width];
for p in parts {
for (t, s) in total.iter_mut().zip(p?) {
t.merge(&s);
}
}
Ok(total)
}
/// Every amplitude `⟨x|ψ⟩` of `c` at the basis states `xs` (qubit `q` at
/// index `q`), exactly, as sums of every branch; evaluate each with
/// [`ExactSum::value`] at [`exact_scale`]`(c.t_count())`. `threads` walk the
/// branches side by side; the sums do not depend on it.
pub fn amplitudes(c: &CliffordT, xs: &[Vec<bool>], threads: usize) -> Result<Vec<ExactSum>, StabrankError> {
let depth = if threads > 1 { (threads.next_power_of_two().trailing_zeros() + 3).min(c.t_count()) } else { 0 };
let starts = prefixes(c, depth)?;
let work = |i: usize, sums: &mut [ExactSum]| -> Result<(), TableauError> {
let (st, pos, phase) = &starts[i];
walk(c, st.clone(), *pos, *phase, xs, sums)
};
Ok(spread(starts.len(), xs.len(), threads, &work)?)
}
fn splitmix(s: &mut u64) -> u64 {
*s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
let mut z = *s;
z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
z ^ (z >> 31)
}
/// `terms` branches of `c` drawn uniformly with `seed`, each from a stream of
/// its own, their amplitudes at `xs` summed exactly; evaluate each with
/// [`ExactSum::value`] at [`sampled_scale`]`(c.t_count(), terms)` for the
/// sampled estimate, whose distance from the state [`sampled_bound`] bounds.
pub fn sampled(c: &CliffordT, xs: &[Vec<bool>], terms: u64, seed: u64, threads: usize) -> Result<Vec<ExactSum>, StabrankError> {
const CHUNK: u64 = 64;
let chunks = terms.div_ceil(CHUNK) as usize;
let work = |i: usize, sums: &mut [ExactSum]| -> Result<(), TableauError> {
for term in (i as u64 * CHUNK)..((i as u64 + 1) * CHUNK).min(terms) {
let mut rng = seed ^ term.wrapping_mul(0xD1B5_4A32_D192_ED03);
let mut bits = 0u64;
let mut left = 0;
let mut st = PhasedTableau::new(c.n)?;
let mut phase = c.phase % 16;
for &g in &c.gates {
match branch(g) {
None => {
if let CtGate::Clifford(g) = g {
st.apply(g)?;
}
}
Some((s, keep, turn)) => {
if left == 0 {
bits = splitmix(&mut rng);
left = 64;
}
let take_s = bits & 1 == 1;
bits >>= 1;
left -= 1;
if take_s {
st.apply(s)?;
phase = (phase + turn) % 16;
} else {
phase = (phase + keep) % 16;
}
}
}
}
leaf(&st, phase, xs, sums)?;
}
Ok(())
};
Ok(spread(chunks, xs.len(), threads, &work)?)
}
#[cfg(all(test, feature = "quantum_sv"))]
mod tests {
use super::*;
use crate::gates::{Gate, complex};
use crate::quantum_sv::StateVector;
fn gate(g: CtGate) -> Gate {
let (z, o, i) = (complex(0.0, 0.0), complex(1.0, 0.0), complex(0.0, 1.0));
match g {
CtGate::T(q) => Gate::t(q),
CtGate::Tdg(q) => Gate { qubits: vec![q], matrix: vec![o, z, z, complex(std::f64::consts::FRAC_1_SQRT_2, -std::f64::consts::FRAC_1_SQRT_2)] },
CtGate::Clifford(c) => match c {
Clifford::H(q) => Gate::h(q),
Clifford::S(q) => Gate::s(q),
Clifford::Sdg(q) => Gate::sdg(q),
Clifford::X(q) => Gate::x(q),
Clifford::Y(q) => Gate::y(q),
Clifford::Z(q) => Gate::z(q),
Clifford::SX(q) => Gate::sqrt_x(q),
Clifford::SXdg(q) => {
let (a, b) = (complex(0.5, -0.5), complex(0.5, 0.5));
Gate { qubits: vec![q], matrix: vec![a, b, b, a] }
}
Clifford::CX(c, t) => Gate::cx(c, t),
Clifford::CY(c, t) => Gate { qubits: vec![c, t], matrix: vec![o, z, z, z, z, o, z, z, z, z, z, complex(0.0, -1.0), z, z, i, z] },
Clifford::CZ(a, b) => Gate::cz(a, b),
Clifford::Swap(a, b) => Gate::swap(a, b),
},
}
}
/// A random Clifford+T circuit on `n` qubits with exactly `t` T or T†
/// gates among `len` gates, and a global phase.
fn circuit(n: u32, len: usize, t: u32, seed: u64) -> CliffordT {
let mut s = seed;
let mut gates = Vec::with_capacity(len + t as usize);
let mut placed = 0;
for i in 0..len + t as usize {
let a = (splitmix(&mut s) % u64::from(n)) as u32;
let b = (a + 1 + (splitmix(&mut s) % u64::from(n - 1)) as u32) % n;
let left = len + t as usize - i;
let want_t = (t - placed) as usize;
if want_t > 0 && (splitmix(&mut s) % left as u64) < want_t as u64 {
placed += 1;
gates.push(if splitmix(&mut s) % 2 == 0 { CtGate::T(a) } else { CtGate::Tdg(a) });
continue;
}
gates.push(CtGate::Clifford(match splitmix(&mut s) % 12 {
0..=2 => Clifford::H(a),
3 => Clifford::S(a),
4 => Clifford::Sdg(a),
5 => Clifford::SX(a),
6 => Clifford::X(a),
7 => Clifford::Y(a),
8 => Clifford::CX(a, b),
9 => Clifford::CY(a, b),
10 => Clifford::CZ(a, b),
_ => Clifford::Swap(a, b),
}));
}
CliffordT { n, gates, phase: (splitmix(&mut s) % 16) as u8 }
}
fn dense(c: &CliffordT) -> StateVector {
let mut sv = StateVector::zero(c.n).unwrap();
for &g in &c.gates {
sv.apply(&gate(g)).unwrap();
}
let (pc, ps) = {
let j = c.phase as usize % 16;
let (a, b) = TURN16[j % 8];
if j < 8 { (a, b) } else { (-a, -b) }
};
for x in 0..sv.re.len() {
let (r, i) = (sv.re[x], sv.im[x]);
sv.re[x] = pc * r - ps * i;
sv.im[x] = pc * i + ps * r;
}
sv
}
fn basis(n: u32) -> Vec<Vec<bool>> {
(0..1usize << n).map(|i| (0..n).map(|q| i >> (n - 1 - q) & 1 == 1).collect()).collect()
}
/// Ground truth first: every amplitude of the exact sum, phase and all, is
/// the dense state vector's, on every circuit size a test affords; the
/// fidelity is 1 to `10⁻¹²`; and the sums do not depend on the threads.
#[test]
fn the_exact_sum_is_the_dense_state_amplitude_by_amplitude() {
let mut cases = 0;
for n in 2..=7u32 {
for t in 0..=10u32 {
if n >= 6 && t > 8 {
continue;
}
for seed in 0..3u64 {
let c = circuit(n, 6 * n as usize, t, seed * 1000 + u64::from(n * 31 + t));
assert_eq!(c.t_count(), t);
let sv = dense(&c);
let xs = basis(n);
let sums = amplitudes(&c, &xs, 1).unwrap();
let scale = exact_scale(t);
let (mut overlap_re, mut overlap_im, mut norm) = (0.0, 0.0, 0.0);
for (x, s) in sums.iter().enumerate() {
let (re, im) = s.value(scale);
let w = sv.amplitude(x);
assert!((w.re - re).abs() < 1e-12 && (w.im - im).abs() < 1e-12, "n={n} t={t} seed={seed} x={x}: sum {re} {im}, dense {} {}", w.re, w.im);
overlap_re += w.re * re + w.im * im;
overlap_im += w.re * im - w.im * re;
norm += re * re + im * im;
}
let fidelity = (overlap_re * overlap_re + overlap_im * overlap_im) / norm;
assert!((fidelity - 1.0).abs() < 1e-12 && (overlap_re - 1.0).abs() < 1e-12, "n={n} t={t}: fidelity {fidelity}, overlap {overlap_re}");
if t >= 4 {
assert_eq!(amplitudes(&c, &xs, 8).unwrap(), sums, "the sums do not depend on the threads");
}
cases += 1;
}
}
}
assert!(cases > 150, "{cases}");
}
/// Every expectation value through the sandwich is the dense state's.
#[test]
fn expectation_values_through_the_sandwich_are_the_dense_states() {
for seed in 0..24u64 {
let n = 2 + (seed % 5) as u32;
let t = (seed % 6) as u32;
let c = circuit(n, 5 * n as usize, t, seed ^ 0xabc);
let sv = dense(&c);
let mut s = seed;
for _ in 0..4 {
let p: Vec<(u32, char)> = (0..n).filter_map(|q| match splitmix(&mut s) % 4 {
0 => None,
1 => Some((q, 'X')),
2 => Some((q, 'Y')),
_ => Some((q, 'Z')),
}).collect();
let w = c.sandwich(&p).unwrap();
assert_eq!(w.t_count(), 2 * t);
let sum = &amplitudes(&w, &[vec![false; n as usize]], 2).unwrap()[0];
let (re, im) = sum.value(exact_scale(w.t_count()));
let want = sv.expectation(&[(1.0, p.clone())]);
assert!((re - want).abs() < 1e-12 && im.abs() < 1e-12, "seed {seed} {p:?}: {re} {im}, dense {want}");
}
}
let c = circuit(2, 4, 1, 1);
assert!(c.sandwich(&[(2, 'Z')]).is_err() && c.sandwich(&[(0, 'Q')]).is_err() && c.sandwich(&[(0, 'Z'), (0, 'X')]).is_err());
}
/// The sampled mode: an unbiased estimate whose distance from the state is
/// within the stated bound in at least the fraction `1 − δ` of seeds,
/// measured on the whole state; and the same bits at any thread count.
#[test]
fn the_sampled_bound_holds_as_often_as_it_says() {
let (n, t, terms, delta) = (5u32, 8u32, 400u64, 0.2);
let c = circuit(n, 30, t, 77);
let sv = dense(&c);
let xs = basis(n);
let bound = sampled_bound(t, terms, delta);
assert!(bound > 0.0 && bound < 1.0, "{bound}");
let mut within = 0;
let seeds = 40;
let mut mean = vec![(0.0, 0.0); xs.len()];
for seed in 0..seeds {
let sums = sampled(&c, &xs, terms, seed, 1).unwrap();
let scale = sampled_scale(t, terms);
let mut dist = 0.0;
for (x, s) in sums.iter().enumerate() {
let (re, im) = s.value(scale);
let w = sv.amplitude(x);
dist += (re - w.re).powi(2) + (im - w.im).powi(2);
mean[x].0 += re / seeds as f64;
mean[x].1 += im / seeds as f64;
}
within += u32::from(dist.sqrt() <= bound);
if seed < 2 {
assert_eq!(sampled(&c, &xs, terms, seed, 8).unwrap(), sums, "the same bits at any thread count");
}
}
assert!(f64::from(within) >= (1.0 - delta) * seeds as f64, "{within} of {seeds} within {bound}");
// Unbiased: the mean of the estimates is closer to the state than one estimate's bound.
let err: f64 = mean.iter().enumerate().map(|(x, m)| (m.0 - sv.amplitude(x).re).powi(2) + (m.1 - sv.amplitude(x).im).powi(2)).sum::<f64>().sqrt();
assert!(err < bound / 2.0, "{err} {bound}");
// The terms for a bound give that bound.
let k = terms_for(t, 0.05, 0.05) as u64;
assert!(sampled_bound(t, k, 0.05) <= 0.05 + 1e-12 && sampled_bound(t, k - 1, 0.05) > 0.05);
assert_eq!(sampled_bound(0, 10, 0.05), 0.0, "a Clifford circuit's one branch is the state");
}
#[test]
fn exact_sums_add_in_any_order_and_evaluate_one_way() {
let mut a = ExactSum::default();
let mut b = ExactSum::default();
for (k, j) in [(0u32, 0u8), (3, 5), (3, 13), (2, 9), (1, 15)] {
a.add(k, j);
}
for (k, j) in [(1u32, 15u8), (2, 9), (3, 13), (3, 5), (0, 0)] {
b.add(k, j);
}
assert_eq!(a, b);
let mut c = ExactSum::default();
c.add(2, 3);
c.add(2, 11);
assert!(c.is_zero(), "a term and its negation cancel exactly");
assert_eq!(half_root_power(4), 0.25);
assert_eq!(power(2.0, 10), 1024.0);
}
}

View file

@ -758,6 +758,64 @@ impl PhasedTableau {
&self.t
}
/// `⟨x|ψ⟩` for each basis state `x` (qubit `q` at index `q`), exactly:
/// `None` outside the support, otherwise `(k, m)` for `2^{−k/2} e^{iπm/4}`.
/// The stabilizers are reduced on their X parts once; then for each `x`,
/// `d = x ⊕ r` lies in their span exactly when the product `P` of the
/// pivot rows whose pivots `d` sets has X part `d`, and then
/// `⟨x|ψ⟩ = ⟨x|P|ψ⟩ = (−1)^{s + z·r} i^{|d∧z|} ⟨r|ψ⟩` with `P` written as
/// `(−1)^s` times its letters. Refused when an `x` is not `n` bits long.
pub fn amplitudes(&self, xs: &[Vec<bool>]) -> Result<Vec<Option<(u32, u8)>>, TableauError> {
let n = self.t.n;
if let Some(x) = xs.iter().find(|x| x.len() != n) {
return Err(TableauError::QubitOutOfRange { qubit: x.len() as u32, n: n as u32 });
}
let mut rows = self.t.rows(n, 2 * n);
let wn = rows.wn;
let mut pivots = Vec::new();
for q in 0..n {
let k = pivots.len();
let Some(p) = (k..n).find(|&i| get(rows.xr(i), q)) else { continue };
rows.swap(k, p);
for i in 0..n {
if i != k && get(rows.xr(i), q) {
rows.mul_into(i, k);
}
}
pivots.push(q);
}
Ok(xs
.iter()
.map(|x| {
let mut d = vec![0u64; wn];
for (q, &b) in x.iter().enumerate() {
if b != get(&self.r, q) {
set(&mut d, q);
}
}
// Row 0 the product so far (the identity, sign +), row 1 the
// pivot row multiplied in.
let mut acc = Rows::new(2, n);
for (i, &c) in pivots.iter().enumerate() {
if get(&d, c) {
acc.x[wn..].copy_from_slice(rows.xr(i));
acc.z[wn..].copy_from_slice(rows.zr(i));
acc.r[1] = rows.r[i];
acc.mul_into(0, 1);
}
}
if acc.xr(0) != &d[..] {
return None;
}
let z = acc.zr(0);
let parity = z.iter().zip(&self.r).map(|(a, b)| (a & b).count_ones()).sum::<u32>() % 2 == 1;
let ys = z.iter().zip(&d).map(|(a, b)| (a & b).count_ones()).sum::<u32>() % 4;
let quarter = (2 * u32::from(acc.r[0] ^ parity) + ys) % 4;
Some((self.k, (self.m + 2 * quarter as u8) % 8))
})
.collect())
}
/// The reference basis state (qubit `q` at index `q`) and `⟨r|ψ⟩`.
pub fn reference(&self) -> (Vec<bool>, (f64, f64)) {
let bits = (0..self.t.n).map(|q| get(&self.r, q)).collect();
@ -1875,4 +1933,38 @@ mod phase_tests {
}
assert!(checked > 2000);
}
/// Every amplitude, phase and all, is the state vector's: exactly zero
/// outside the support and `2^{−k/2} e^{iπm/4}` within it, over random
/// Clifford circuits and every basis state.
#[test]
fn every_amplitude_is_the_state_vectors() {
let mut checked = 0;
for seed in 0..60u64 {
let n = 2 + (seed % 7) as u32;
let ops = circuit(n, 10 * n as usize + 3, seed ^ 0x5eed);
let t = PhasedTableau::run(n, &ops).unwrap();
let mut sv = StateVector::zero(n).unwrap();
for &op in &ops {
sv.apply(&gate(op)).unwrap();
}
let xs: Vec<Vec<bool>> = (0..1usize << n).map(|i| (0..n).map(|q| i >> (n - 1 - q) & 1 == 1).collect()).collect();
for (i, a) in t.amplitudes(&xs).unwrap().into_iter().enumerate() {
let want = sv.amplitude(i);
let (re, im) = match a {
None => (0.0, 0.0),
Some((k, m)) => {
let mag = (-(k as f64) / 2.0).exp2();
let ang = std::f64::consts::FRAC_PI_4 * f64::from(m);
(mag * ang.cos(), mag * ang.sin())
}
};
assert!((want.re - re).abs() < 1e-12 && (want.im - im).abs() < 1e-12, "seed {seed}, basis state {i}: tableau {re} {im}, state vector {} {}", want.re, want.im);
assert_eq!(a.is_none(), want.re.hypot(want.im) < 1e-9, "seed {seed}, basis state {i}: the support");
checked += 1;
}
}
assert!(checked > 4000, "{checked}");
assert!(PhasedTableau::new(3).unwrap().amplitudes(&[vec![false; 2]]).is_err());
}
}

View file

@ -0,0 +1,191 @@
//! Jobs placed on the fabric's members of the machine that runs this: a
//! `statevector` job on each accelerator member certified bit-exact here, and
//! split over several, must give the CPU's answers byte for byte, and name
//! where its state ran.
//!
//! The machine declares the members it expects placement to find in
//! `WAI_QUANTUM_FABRIC_EXPECT` (their keys, sorted and comma-separated, or
//! `none`; a build that reaches no accelerator expects none unless told); `WAI_QUANTUM_FABRIC_SPLIT` (optional) names a list of members,
//! comma-separated, to split one state over; `WAI_QUANTUM_FABRIC_REPORT` names
//! a file the established lines are appended to.
#![cfg(not(target_arch = "wasm32"))]
use serde_json::{Value, json};
use std::io::Write;
use std::time::Instant;
use wai_quantum::engine::{catalog, fabric, receipt, run};
const CAP: u64 = 12 << 30;
fn report(line: &str) {
println!("{line}");
if let Some(path) = std::env::var_os("WAI_QUANTUM_FABRIC_REPORT") {
let mut f = std::fs::OpenOptions::new().create(true).append(true).open(&path).expect("the report file opens");
writeln!(f, "{line}").expect("the report line is written");
}
}
/// The accelerator members placement found, by key, each with the most
/// qubits it holds, as declared.
fn members() -> Vec<(String, u64)> {
let listed = fabric::list(CAP);
let mut found: Vec<(String, u64)> = listed["members"].as_array().unwrap().iter().filter(|m| m["key"] != "cpu").map(|m| (m["key"].as_str().unwrap().to_string(), m["detail"]["max_qubits"].as_u64().unwrap())).collect();
found.sort();
let keys: Vec<&str> = found.iter().map(|m| m.0.as_str()).collect();
let found_list = if keys.is_empty() { "none".to_string() } else { keys.join(",") };
// A build that reaches no accelerator has nothing to declare; one that
// does must say what it expects to find.
let reaches = listed["backends"].as_array().is_some_and(|b| !b.is_empty());
let expect = match std::env::var("WAI_QUANTUM_FABRIC_EXPECT") {
Ok(e) => e,
Err(_) if !reaches => "none".to_string(),
Err(_) => panic!("WAI_QUANTUM_FABRIC_EXPECT is not set; found {found_list}; listed {listed}"),
};
assert_eq!(found_list, expect, "the members placement found are not the declared ones: {listed}");
found
}
/// Brickwork at arbitrary angles on `n` qubits that reaches the top qubits,
/// asking for counts, expectation values and amplitudes.
fn job(n: u32, extra: Value) -> Value {
let mut q = format!("OPENQASM 3.0;\nqubit[{n}] q;\nbit[{n}] c;\n");
for layer in 0..5u32 {
for i in 0..n {
q.push_str(&format!("rx({}) q[{i}];\nrz({}) q[{i}];\n", 0.13 + 0.41 * f64::from(i + layer), 0.23 * f64::from(layer + 1) + 0.01 * f64::from(i)));
}
for i in (layer % 2..n - 1).step_by(2) {
q.push_str(&format!("cx q[{i}], q[{}];\nry({}) q[{}];\n", i + 1, 0.3 + 0.07 * f64::from(i), i + 1));
}
q.push_str(&format!("cp(0.9) q[0], q[{}];\nswap q[1], q[{}];\n", n - 1, n - 2));
}
q.push_str("c = measure q;\n");
let mut v = json!({ "qasm": q, "shots": 4000, "seed": 3, "method": "statevector", "observables": ["Z0 Z1", "X2", format!("Y{} Z0", n - 1)], "amplitudes": ["0".repeat(n as usize), format!("1{}", "0".repeat(n as usize - 1))] });
if let (Some(o), Some(e)) = (v.as_object_mut(), extra.as_object()) {
o.extend(e.clone());
}
v
}
fn ran(v: &Value) -> Result<Value, String> {
let p = catalog::plan(v, CAP)?;
run::run(v, &p)
}
/// The answers, without where the state ran and how long it took.
fn answers(out: &Value) -> Vec<u8> {
let mut o = out.clone();
for k in ["fabric", "elapsed_ms"] {
o.as_object_mut().unwrap().remove(k);
}
receipt::canonical(&o)
}
#[test]
fn p0_each_member_gives_the_cpus_answers_byte_for_byte() {
let found = members();
report(&format!("fabric members: as declared ({})", if found.is_empty() { "none".to_string() } else { found.iter().map(|m| m.0.as_str()).collect::<Vec<_>>().join(",") }));
for (key, max) in &found {
for n in [12u32, 18] {
let cpu = ran(&job(n, json!({ "fabric": "cpu" }))).unwrap();
let placed = ran(&job(n, json!({ "fabric": key }))).unwrap_or_else(|e| panic!("{key}: {e}"));
assert_eq!(placed["fabric"]["key"], json!(key), "{}", placed["fabric"]);
assert_eq!(placed["fabric"]["parity"]["tier"], "bit-exact");
assert_eq!(answers(&placed), answers(&cpu), "{key}: n={n}: the answers differ from the CPU's");
}
report(&format!("fabric placed: key={key} max_qubits={max} 12 and 18 qubits give the CPU's answers byte for byte"));
}
}
#[test]
fn p1_auto_stays_on_the_cpu_and_accelerator_takes_the_first_member() {
let found = members();
// Auto places nothing, however wide the state and whatever members there
// are: the result is the one it always was, with no `fabric`.
for n in [8u32, 21] {
let auto = ran(&job(n, json!({}))).unwrap();
assert!(auto.get("fabric").is_none(), "auto at {n} qubits stays on the CPU, unnamed: {}", auto["fabric"]);
let plan = catalog::plan(&job(n, json!({})), CAP).unwrap();
assert!(!plan.why.contains("its state on"), "{}", plan.why);
}
report("fabric auto: every state on the CPU, its result unnamed");
let Some((first, _)) = found.first() else {
let e = catalog::plan(&job(8, json!({ "fabric": "accelerator" })), CAP).unwrap_err();
assert!(e.ends_with("[J013]") && e.contains("no certified accelerator member here"), "{e}");
report("fabric accelerator: no member here; refused");
return;
};
let n = 21;
let t = Instant::now();
let cpu = ran(&job(n, json!({ "fabric": "cpu" }))).unwrap();
let cpu_ms = t.elapsed().as_millis();
let t = Instant::now();
let placed = ran(&job(n, json!({ "fabric": "accelerator" }))).unwrap();
let placed_ms = t.elapsed().as_millis();
let listed = fabric::list(CAP);
let want_first = listed["members"][1]["key"].as_str().unwrap();
assert_eq!(placed["fabric"]["key"].as_str(), Some(want_first), "accelerator takes the first member listed: {}", placed["fabric"]);
assert_eq!(placed["fabric"]["asked"], "accelerator");
assert!(placed["fabric"].get("fell_back_from").is_none(), "{}", placed["fabric"]);
assert_eq!(answers(&placed), answers(&cpu), "accelerator: the answers differ from the CPU's");
report(&format!("fabric accelerator: {n} qubits on {want_first} (of {first} and the rest), the CPU's answers byte for byte; cpu {cpu_ms} ms, placed {placed_ms} ms"));
}
#[test]
fn p2_a_state_split_over_members_is_the_cpus() {
let found = members();
let mut lists: Vec<Vec<String>> = Vec::new();
if let Some((k, _)) = found.first() {
lists.push(vec![k.clone(), "cpu".into()]);
lists.push(vec![k.clone(), k.clone()]);
lists.push(vec![k.clone(), "cpu".into(), k.clone(), "cpu".into()]);
}
if let Ok(s) = std::env::var("WAI_QUANTUM_FABRIC_SPLIT") {
lists.push(s.split(',').map(str::to_string).collect());
}
lists.push(vec!["cpu".into(), "cpu".into()]);
for list in &lists {
for n in [14u32, 19] {
let cpu = ran(&job(n, json!({ "fabric": "cpu" }))).unwrap();
let split = ran(&job(n, json!({ "fabric": list }))).unwrap_or_else(|e| panic!("{list:?}: {e}"));
let f = &split["fabric"];
assert_eq!(f["partitions"].as_array().map(Vec::len), Some(list.len()), "{f}");
assert!(f["exchanges"].as_u64().unwrap() > 0, "{f}");
assert_eq!(answers(&split), answers(&cpu), "{list:?}: n={n}: the answers differ from the CPU's");
}
report(&format!("fabric split: {} over 14 and 19 qubits gives the CPU's answers byte for byte", list.join("+")));
}
}
#[test]
fn p3_a_state_past_a_member_is_refused_before_it_runs() {
for (key, max) in members() {
let n = (max + 1) as u32;
// Planned only: the refusal comes before any memory is taken.
let e = catalog::plan(&job(n, json!({ "fabric": key, "shots": 1, "observables": [], "amplitudes": [] })), 64 << 30).unwrap_err();
assert!(e.ends_with("[J013]") && e.contains(&format!("holds at most {max}")), "{key}: {e}");
report(&format!("fabric refused: key={key} a state of {n} qubits, past the {max} it holds"));
}
}
/// Where auto's threshold comes from: the time of the same job on the CPU and
/// on each member, from 14 to 24 qubits, reported and not asserted.
#[test]
fn p4_the_time_of_a_job_on_the_cpu_and_on_each_member() {
for (key, max) in members() {
for n in (14u32..=24).step_by(2).filter(|&n| u64::from(n) <= max) {
let shots = json!({ "shots": 1, "observables": [], "amplitudes": [] });
let mut cpu = job(n, json!({ "fabric": "cpu" }));
let mut placed = job(n, json!({ "fabric": key }));
cpu.as_object_mut().unwrap().extend(shots.as_object().unwrap().clone());
placed.as_object_mut().unwrap().extend(shots.as_object().unwrap().clone());
let t = Instant::now();
let a = ran(&cpu).unwrap();
let cpu_ms = t.elapsed().as_secs_f64() * 1e3;
let t = Instant::now();
let b = ran(&placed).unwrap();
let placed_ms = t.elapsed().as_secs_f64() * 1e3;
assert_eq!(answers(&a), answers(&b), "{key}: n={n}");
report(&format!("fabric timing: key={key} n={n} cpu={cpu_ms:.1}ms placed={placed_ms:.1}ms"));
}
}
}

View file

@ -4,19 +4,19 @@
"placeholder": "__WAI_QUANTUM_VIEW__",
"limit_bytes": 1048576,
"bytes": {
"page": 804920,
"script": 94428,
"page": 804985,
"script": 94493,
"styles": 50804,
"engine_wasm": 193526,
"engine_wasm_base64": 258036,
"engine_js": 401202
},
"sha256": {
"page": "a9601c1ba22d4bb5b38a9bb3c73b28b7f7aff413059e04724236fe531c70f9c8",
"script": "47c5c3d1921b34a8f5fbf3e6d65f9436f8e6ed5f216615f4c0544e40847b3c02",
"page": "8bf0b0cfd064a5fb53fedbe34d15bd2954e756fdf985452dbb08c9e5722311ff",
"script": "152484a251cd06eea4e65c45f6e6b8810be8d0932ec4dcdf3ae3cb4131e1f036",
"styles": "11ebcd4416c68e9eaf1156ca6f7c92b9a96744895ed13c377eba98fdd8dca9a6",
"engine_wasm": "3033a060d86fd9b76b5aa27c729ea4cf08a2de5d9af10fe62f0308f0450e92df",
"engine_js": "8c302982ec0f4bbc07cab8eb0f1228adf8a55b92b19449b74fe431fa916b3b96"
"engine_wasm": "ba6e11c8c0a256723b826be4167a973cf8bc82193e73d5205455e5d3454fa3f1",
"engine_js": "a7e3cf66d42fce8af4afc4395ebdacc76831a1ab54f826054ce9fa5f8df16f34"
},
"sources": {
"wai/quantum-studio/build-views.mjs": "da5b161829a990b3d1bb4c1b3ec7c8f254ff7b5078476156e504ae322bc4f010",
@ -32,7 +32,7 @@
"wai/quantum-studio/locales/en/estimate_view.json": "b20c94f96751eee548b011ea19f6ded9ecd8648cae32885b6b71b7d5e631b2b1",
"wai/quantum-studio/locales/en/examples.json": "dd2f2f4aef033b00b3700c6eb7151c18dd1b43537ef0beeb17a8ffc069a564b2",
"wai/quantum-studio/locales/en/flags.json": "d2beeef25cee8ec742d692e424ec54bdc821449733ae98b2882cc97711efffdd",
"wai/quantum-studio/locales/en/frame.json": "608565c19197c6210b37a917ed27bbb3dbf6338cbeff23eebae2219140a3650e",
"wai/quantum-studio/locales/en/frame.json": "8ed953be6f391aad92f56da73e5a4f4188de9f1d46dc125a7c2a228c48e59e82",
"wai/quantum-studio/locales/en/gallery.json": "5d0c2992e39ea0f82541e0491b46f4fd724085d95a988f8c15a1e3657d2eb149",
"wai/quantum-studio/locales/en/generators.json": "85eb826cc6353cf12bc3fa45bbb8721210a311f145b9b46493c3e2bb44e7a3c2",
"wai/quantum-studio/locales/en/hardware.json": "745786e872062468680655326362f49ae85dabae6ae9021145072dd4dbd7fbf1",
@ -55,7 +55,7 @@
"wai/quantum-studio/src/dom.js": "08bfb43472f0f07ef46353593c8fd14ed025fc4d0f1256c35cf7a3861a2cf607",
"wai/quantum-studio/src/estimate-view.js": "40cb4bd738afb628dd686cdad44813d9d09910fbc60905098852dcca6cfe1b27",
"wai/quantum-studio/src/i18n.js": "f661474772b05ef6614f2f6096e85d94cd479c4fcd9448f34e8d312a2fd43498",
"wai/quantum-studio/src/result-parts.js": "c0c0bceae031a45e6e9862533706eda4cf119b6f65456237c2572ed534f9513e",
"wai/quantum-studio/src/result-parts.js": "f919bd1a76498443ff7bf996023405a61e662ba52020addf1733caab459dfb9c",
"wai/quantum-studio/src/studio.css": "8891f2a1b71e1ada344ee1dc41939ba9cd41232f25ab2703d05394c141855997",
"wai/quantum-studio/src/tokens.css": "492fbaf8a6cc5b1f7e89346336af3e0a1930a0b48a23731d80d86c128a9032c3",
"wai/quantum-studio/src/verdict-view.js": "aec18c93c6f502657ffe2981ca4d9ae0956e1277638d1a9bb1b0846ac8e80160",

File diff suppressed because one or more lines are too long