open-standards/wai/wai-quantum/src/quantum_stabilizer.rs
Transaction Science e531c2ddd7
Some checks failed
no-vanity-metrics / Scan committed Markdown for vanity metrics (push) Successful in 3s
standards workspaces build + test / aac-rs (push) Successful in 37s
standards workspaces build + test / joule-code-rs (push) Successful in 42s
standards workspaces build + test / eoc-rs (push) Successful in 1m47s
standards workspaces build + test / jcp-rs (push) Successful in 1m14s
standards workspaces build + test / proof-rs (push) Successful in 1m9s
standards workspaces build + test / jouleclaw-rs (push) Successful in 4m6s
standards workspaces build + test / joulecontract-rs (push) Successful in 38s
standards workspaces build + test / sandbox-rs (push) Successful in 2m1s
standards workspaces build + test / map-rs (push) Successful in 36s
standards workspaces build + test / mesh-rs (push) Successful in 34s
standards workspaces build + test / openpay (push) Successful in 1m23s
standards workspaces build + test / smart-byte-rs (push) Successful in 2m28s
WAI conformance / cargo-class corpora · linux-arm64 (push) Successful in 33s
WAI conformance / cargo-class corpora · linux (push) Successful in 28s
WAI conformance / cross-arch byte-equality · linux-arm64 (push) Successful in 17s
WAI conformance / cross-arch byte-equality · linux (push) Successful in 14s
standards workspaces build + test / arl-rs (push) Successful in 8m41s
standards workspaces build + test / joule-ui-rs (push) Successful in 7m56s
standards workspaces build + test / jouledesk-rs (push) Successful in 8m24s
standards workspaces build + test / joulehook-rs (push) Successful in 8m34s
standards workspaces build + test / sense-rs (push) Successful in 7m58s
WAI conformance / cargo-class corpora · macos (push) Successful in 1m4s
WAI conformance / cross-arch byte-equality · macos (push) Successful in 23s
WAI conformance / cargo-class corpora · windows (push) Has been cancelled
WAI conformance / cross-arch byte-equality · windows (push) Has been cancelled
wai: extract the quantum stack into a standalone wai-quantum crate
crates.io publish was blocked: the name `wai` is taken (an unrelated 2017 crate),
and `default = ["codecs"]` would make `cargo add` pull libjxl/libavif/opus/FLAC/
rav1e/dav1d system libraries — so most builds would fail for someone who only
wants quantum. Extracting the quantum stack solves both.

`wai-quantum` is dependency-light and pure Rust (blake3 + serde_json, with
ed25519-dalek only for the sealing features). No system libraries, so it builds
everywhere the stack already runs: native, browser wasm, and wasm32-wasip2.

The split follows the seam that was already there — 12,975 lines moved with only
four coupling points:
- `world:🧾:merkle_root` (used by 3 modules) is reproduced verbatim in
  `wai-quantum::merkle`, domain separators included, so every previously issued
  receipt and published conformance vector still verifies;
- `wrap_envelope` is a WAI-envelope concern, not a quantum one, so it stays in
  `wai` as `quantum_envelope` (it had no external callers).

`wai` keeps every quantum feature name and re-exports the modules unchanged, so
wai-web, wai-quantum-serve and the deployed site build without edits. Also
re-exports `ed25519_dalek` from wai-quantum: the sealing API takes a `SigningKey`,
and a consumer picking their own version could get an incompatible one.

Verified the extraction is observably a no-op — identical to the values recorded
before it:
  ghz -n 5      circuit-id 355d1166…  state-hash 0b5f4201…
  grover -n 4 -p 9            state-hash 25a7d930…
  receipt JSON sha256         117ff588…
208 tests pass (173 wai-quantum + 35 wai); wai-web and the WASI-HTTP component
build unchanged, and the edge endpoint still returns the identical hash.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
2026-09-03 15:29:50 -04:00

410 lines
14 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Stabilizer (CHP) backend — the O(n²) Clifford tier (`wai.quantum.stabilizer`).
//!
//! The cheapest-sufficient cascade's fast tier: a Clifford circuit — however many
//! qubits — is simulated in `O(n²)` per gate and sampled exactly by the
//! Aaronson–Gottesman stabilizer tableau (the CHP algorithm), far past the dense
//! simulator's `2ⁿ` wall. Where the dense sink dies at ~26 qubits, this runs a
//! thousand-qubit Clifford circuit and draws measurement shots from it.
//!
//! Determinism: the tableau is exact integer bit-arithmetic (no floats at all), and
//! measurement randomness comes from the pinned `splitmix64` — so a `(circuit,
//! seed, shots)` histogram is **bit-identical on every machine**, the same contract
//! the dense simulator makes. The Clifford subset it accepts is
//! `H, S, S†, X, Y, Z, P(1)=Z, P(2)=S, CX, CZ, CP(1)` and `SWAP`; anything else
//! (a `T`, a Toffoli) is rejected so the caller falls back to a universal backend.
use crate::quantum::{BaseGate, Circuit, Gate};
fn splitmix64(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)
}
/// A gate outside the Clifford subset this backend simulates.
#[derive(Debug)]
pub struct NotClifford(pub String);
// ===========================================================================
// Tableau (2n+1 rows: n destabilizers, n stabilizers, 1 scratch)
// ===========================================================================
/// A stabilizer tableau on `n` qubits. Rows `0..n` are destabilizers, `n..2n`
/// stabilizers, `2n` is scratch. Each row is a signed Pauli stored as bit-packed
/// `x`/`z` words plus a phase `r ∈ {0,1}`.
#[derive(Clone)]
pub struct Tableau {
n: usize,
words: usize,
xs: Vec<u64>,
zs: Vec<u64>,
r: Vec<u8>,
}
impl Tableau {
/// The identity tableau: destabilizer `i = X_i`, stabilizer `i = Z_i`.
pub fn identity(n: usize) -> Tableau {
let words = n.div_ceil(64).max(1);
let rows = 2 * n + 1;
let mut t = Tableau { n, words, xs: vec![0; rows * words], zs: vec![0; rows * words], r: vec![0; rows] };
for i in 0..n {
t.set_x(i, i, 1); // destabilizer i = X_i
t.set_z(n + i, i, 1); // stabilizer i = Z_i
}
t
}
#[inline]
fn xget(&self, row: usize, j: usize) -> u64 {
(self.xs[row * self.words + (j >> 6)] >> (j & 63)) & 1
}
#[inline]
fn zget(&self, row: usize, j: usize) -> u64 {
(self.zs[row * self.words + (j >> 6)] >> (j & 63)) & 1
}
#[inline]
fn set_x(&mut self, row: usize, j: usize, v: u64) {
let w = row * self.words + (j >> 6);
let b = 1u64 << (j & 63);
if v != 0 {
self.xs[w] |= b;
} else {
self.xs[w] &= !b;
}
}
#[inline]
fn set_z(&mut self, row: usize, j: usize, v: u64) {
let w = row * self.words + (j >> 6);
let b = 1u64 << (j & 63);
if v != 0 {
self.zs[w] |= b;
} else {
self.zs[w] &= !b;
}
}
#[inline]
fn flip_x(&mut self, row: usize, j: usize) {
self.xs[row * self.words + (j >> 6)] ^= 1u64 << (j & 63);
}
#[inline]
fn flip_z(&mut self, row: usize, j: usize) {
self.zs[row * self.words + (j >> 6)] ^= 1u64 << (j & 63);
}
// --- Clifford primitives (operate over the 2n state rows) ---
fn cnot(&mut self, a: usize, b: usize) {
for i in 0..2 * self.n {
let xa = self.xget(i, a);
let za = self.zget(i, a);
let xb = self.xget(i, b);
let zb = self.zget(i, b);
self.r[i] ^= (xa & zb & (xb ^ za ^ 1)) as u8;
if xa != 0 {
self.flip_x(i, b);
}
if zb != 0 {
self.flip_z(i, a);
}
}
}
fn hadamard(&mut self, a: usize) {
for i in 0..2 * self.n {
let xa = self.xget(i, a);
let za = self.zget(i, a);
self.r[i] ^= (xa & za) as u8;
// swap x,z at (i,a)
self.set_x(i, a, za);
self.set_z(i, a, xa);
}
}
fn phase(&mut self, a: usize) {
for i in 0..2 * self.n {
let xa = self.xget(i, a);
let za = self.zget(i, a);
self.r[i] ^= (xa & za) as u8;
if xa != 0 {
self.flip_z(i, a);
}
let _ = za;
}
}
fn x(&mut self, a: usize) {
for i in 0..2 * self.n {
self.r[i] ^= self.zget(i, a) as u8;
}
}
fn z(&mut self, a: usize) {
for i in 0..2 * self.n {
self.r[i] ^= self.xget(i, a) as u8;
}
}
fn y(&mut self, a: usize) {
for i in 0..2 * self.n {
self.r[i] ^= (self.xget(i, a) ^ self.zget(i, a)) as u8;
}
}
fn cz(&mut self, a: usize, b: usize) {
self.hadamard(b);
self.cnot(a, b);
self.hadamard(b);
}
/// Apply one circuit gate; error on a non-Clifford gate.
fn apply(&mut self, g: &Gate) -> Result<(), NotClifford> {
let t = g.target as usize;
match (g.base, g.controls.as_slice()) {
(BaseGate::I, []) => {}
(BaseGate::H, []) => self.hadamard(t),
(BaseGate::S, []) => self.phase(t),
(BaseGate::Sdg, []) => {
self.phase(t);
self.phase(t);
self.phase(t);
}
(BaseGate::X, []) => self.x(t),
(BaseGate::Y, []) => self.y(t),
(BaseGate::Z, []) => self.z(t),
(BaseGate::P, []) if g.param == 1 => self.z(t),
(BaseGate::P, []) if g.param == 2 => self.phase(t),
(BaseGate::X, [c]) => self.cnot(*c as usize, t),
(BaseGate::Z, [c]) => self.cz(*c as usize, t),
(BaseGate::P, [c]) if g.param == 1 => self.cz(*c as usize, t),
(BaseGate::Y, [c]) => {
// CY = Sdg(t) · CX(c,t) · S(t)
self.phase(t);
self.cnot(*c as usize, t);
self.phase(t);
self.phase(t);
self.phase(t);
}
(base, ctrls) => {
return Err(NotClifford(format!("{base:?} with controls {ctrls:?} param {}", g.param)));
}
}
Ok(())
}
// --- measurement (CHP) ---
fn g_exp(x1: u64, z1: u64, x2: u64, z2: u64) -> i32 {
let (x2, z2) = (x2 as i32, z2 as i32);
match (x1, z1) {
(0, 0) => 0,
(1, 1) => z2 - x2,
(1, 0) => z2 * (2 * x2 - 1),
_ => x2 * (1 - 2 * z2),
}
}
fn rowsum(&mut self, h: usize, i: usize) {
let mut s = 2 * self.r[h] as i32 + 2 * self.r[i] as i32;
for j in 0..self.n {
s += Self::g_exp(self.xget(i, j), self.zget(i, j), self.xget(h, j), self.zget(h, j));
}
self.r[h] = if s.rem_euclid(4) == 2 { 1 } else { 0 };
for w in 0..self.words {
self.xs[h * self.words + w] ^= self.xs[i * self.words + w];
self.zs[h * self.words + w] ^= self.zs[i * self.words + w];
}
}
fn copy_row(&mut self, dst: usize, src: usize) {
for w in 0..self.words {
self.xs[dst * self.words + w] = self.xs[src * self.words + w];
self.zs[dst * self.words + w] = self.zs[src * self.words + w];
}
self.r[dst] = self.r[src];
}
fn zero_row(&mut self, row: usize) {
for w in 0..self.words {
self.xs[row * self.words + w] = 0;
self.zs[row * self.words + w] = 0;
}
self.r[row] = 0;
}
/// Measure qubit `a` in the computational basis, collapsing the state. Returns
/// the outcome bit. `rng` supplies randomness for indeterminate outcomes.
pub fn measure(&mut self, a: usize, rng: &mut u64) -> u8 {
let n = self.n;
// is there a stabilizer that anticommutes with Z_a? (x_{p,a}=1, p in n..2n)
let mut p = None;
for row in n..2 * n {
if self.xget(row, a) == 1 {
p = Some(row);
break;
}
}
if let Some(p) = p {
// random outcome
for i in 0..2 * n {
if i != p && self.xget(i, a) == 1 {
self.rowsum(i, p);
}
}
self.copy_row(p - n, p); // destabilizer <- old stabilizer
self.zero_row(p);
self.set_z(p, a, 1);
let bit = (splitmix64(rng) & 1) as u8;
self.r[p] = bit;
bit
} else {
// determined outcome: accumulate into scratch row 2n
let scratch = 2 * n;
self.zero_row(scratch);
for i in 0..n {
if self.xget(i, a) == 1 {
self.rowsum(scratch, i + n);
}
}
self.r[scratch]
}
}
}
// ===========================================================================
// Runner + sampler
// ===========================================================================
/// Run a Clifford `circuit` on the stabilizer tableau. Errors on a non-Clifford
/// gate (the caller should fall back to a universal backend).
pub fn run(circuit: &Circuit) -> Result<Tableau, NotClifford> {
let mut t = Tableau::identity(circuit.n_qubits as usize);
for g in &circuit.ops {
t.apply(g)?;
}
Ok(t)
}
/// One shot: measure every qubit of a fresh copy, packing the outcome into a `u64`
/// (`n ≤ 64`). Bit `i` = qubit `i`, matching the dense simulator's index order.
fn shot(base: &Tableau, rng: &mut u64) -> u64 {
let mut t = base.clone();
let mut out = 0u64;
for q in 0..t.n.min(64) {
if t.measure(q, rng) == 1 {
out |= 1u64 << q;
}
}
out
}
/// Sample `shots` measurements of a Clifford circuit as `(outcome, count)` pairs
/// (outcomes as `u64`, `n ≤ 64`), sorted by count descending. Deterministic in
/// `(seed, shots)`.
pub fn sample(circuit: &Circuit, seed: u64, shots: u64) -> Result<Vec<(u64, u64)>, NotClifford> {
let base = run(circuit)?;
let mut rng = seed ^ 0x5DEE_CE66_D3A9_1B2C;
let mut counts: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
for _ in 0..shots {
*counts.entry(shot(&base, &mut rng)).or_insert(0) += 1;
}
let mut v: Vec<(u64, u64)> = counts.into_iter().collect();
v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
Ok(v)
}
/// The number of qubits the stabilizer path can handle for a given circuit — the
/// cascade's scaling story (`O(n²)` per gate, no `2ⁿ` state vector).
pub fn max_practical_qubits() -> usize {
4096
}
#[cfg(test)]
mod tests {
use super::*;
fn ghz(n: u8) -> Circuit {
let mut c = Circuit::new(n);
c.h(0);
for i in 1..n {
c.cx(i - 1, i);
}
c
}
#[test]
fn zero_state_is_deterministic_zero() {
let c = Circuit::new(4);
let s = sample(&c, 1, 200).unwrap();
assert_eq!(s.len(), 1);
assert_eq!(s[0].0, 0);
assert_eq!(s[0].1, 200);
}
#[test]
fn ghz_samples_all_or_nothing() {
let s = sample(&ghz(5), 42, 4000).unwrap();
// only |00000> and |11111> appear, each ~half
assert_eq!(s.len(), 2, "GHZ has exactly two outcomes");
let outcomes: Vec<u64> = s.iter().map(|(o, _)| *o).collect();
assert!(outcomes.contains(&0) && outcomes.contains(&0b11111));
for (_, cnt) in &s {
assert!((*cnt as i64 - 2000).abs() < 250, "GHZ halves should be ~even: {cnt}");
}
}
#[test]
fn matches_dense_support_on_a_clifford_circuit() {
// a non-trivial Clifford circuit: every sampled outcome must have nonzero
// dense probability, and the count of distinct outcomes must match.
let mut c = Circuit::new(4);
c.h(0);
c.h(1);
c.cx(0, 2);
c.cx(1, 3);
c.s(2);
c.cz(0, 1);
let stab = sample(&c, 7, 8000).unwrap();
let dense = c.simulate().unwrap();
let probs = dense.prob_weights();
let total: i128 = probs.iter().sum();
// support match
for (o, _) in &stab {
assert!(probs[*o as usize] > 0, "stabilizer outcome {o} has zero dense probability");
}
let dense_support = probs.iter().filter(|&&p| p > 0).count();
assert_eq!(stab.len(), dense_support, "distinct-outcome count must match dense");
// frequencies roughly uniform over the support (Clifford ⇒ flat)
let expect = 8000.0 / dense_support as f64;
for (_, cnt) in &stab {
assert!((*cnt as f64 - expect).abs() < expect * 0.35, "freq off: {cnt} vs {expect}");
}
let _ = total;
}
#[test]
fn scales_past_the_dense_wall() {
// 200-qubit GHZ — 2^200 amplitudes is impossible densely; here it's instant,
// and every shot must be all-0 or all-1 (perfect parity correlation).
let n = 200u8;
let c = ghz(n);
let base = run(&c).unwrap();
let mut rng = 123u64;
for _ in 0..50 {
let mut t = base.clone();
let first = t.measure(0, &mut rng);
for q in 1..n as usize {
assert_eq!(t.measure(q, &mut rng), first, "GHZ parity broken at large n");
}
}
}
#[test]
fn deterministic_in_seed() {
let a = sample(&ghz(6), 99, 500).unwrap();
let b = sample(&ghz(6), 99, 500).unwrap();
assert_eq!(a, b, "same seed must give the same histogram");
}
#[test]
fn rejects_non_clifford() {
let mut c = Circuit::new(2);
c.h(0);
c.t(0); // non-Clifford
assert!(run(&c).is_err());
// Toffoli (2 controls) also rejected
let mut c2 = Circuit::new(3);
c2.ccx(0, 1, 2);
assert!(run(&c2).is_err());
}
}