Skip to main content

wai_quantum/
quantum_stabilizer.rs

1//! Stabilizer (CHP) backend — the O(n²) Clifford tier (`wai.quantum.stabilizer`).
2//!
3//! The cheapest-sufficient cascade's fast tier: a Clifford circuit — however many
4//! qubits — is simulated in `O(n²)` per gate and sampled exactly by the
5//! Aaronson–Gottesman stabilizer tableau (the CHP algorithm), far past the dense
6//! simulator's `2ⁿ` wall. Where the dense sink dies at ~26 qubits, this runs a
7//! thousand-qubit Clifford circuit and draws measurement shots from it.
8//!
9//! Determinism: the tableau is exact integer bit-arithmetic (no floats at all), and
10//! measurement randomness comes from the pinned `splitmix64` — so a `(circuit,
11//! seed, shots)` histogram is **bit-identical on every machine**, the same contract
12//! the dense simulator makes. The Clifford subset it accepts is
13//! `H, S, S†, X, Y, Z, P(1)=Z, P(2)=S, CX, CZ, CP(1)` and `SWAP`; anything else
14//! (a `T`, a Toffoli) is rejected so the caller falls back to a universal backend.
15
16use crate::quantum::{BaseGate, Circuit, Gate};
17
18fn splitmix64(s: &mut u64) -> u64 {
19    *s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
20    let mut z = *s;
21    z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
22    z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
23    z ^ (z >> 31)
24}
25
26/// A gate outside the Clifford subset this backend simulates.
27#[derive(Debug)]
28pub struct NotClifford(pub String);
29
30// ===========================================================================
31// Tableau  (2n+1 rows: n destabilizers, n stabilizers, 1 scratch)
32// ===========================================================================
33
34/// A stabilizer tableau on `n` qubits. Rows `0..n` are destabilizers, `n..2n`
35/// stabilizers, `2n` is scratch. Each row is a signed Pauli stored as bit-packed
36/// `x`/`z` words plus a phase `r ∈ {0,1}`.
37#[derive(Clone)]
38pub struct Tableau {
39    n: usize,
40    words: usize,
41    xs: Vec<u64>,
42    zs: Vec<u64>,
43    r: Vec<u8>,
44}
45
46impl Tableau {
47    /// The identity tableau: destabilizer `i = X_i`, stabilizer `i = Z_i`.
48    pub fn identity(n: usize) -> Tableau {
49        let words = n.div_ceil(64).max(1);
50        let rows = 2 * n + 1;
51        let mut t = Tableau { n, words, xs: vec![0; rows * words], zs: vec![0; rows * words], r: vec![0; rows] };
52        for i in 0..n {
53            t.set_x(i, i, 1); // destabilizer i = X_i
54            t.set_z(n + i, i, 1); // stabilizer i = Z_i
55        }
56        t
57    }
58
59    #[inline]
60    fn xget(&self, row: usize, j: usize) -> u64 {
61        (self.xs[row * self.words + (j >> 6)] >> (j & 63)) & 1
62    }
63    #[inline]
64    fn zget(&self, row: usize, j: usize) -> u64 {
65        (self.zs[row * self.words + (j >> 6)] >> (j & 63)) & 1
66    }
67    #[inline]
68    fn set_x(&mut self, row: usize, j: usize, v: u64) {
69        let w = row * self.words + (j >> 6);
70        let b = 1u64 << (j & 63);
71        if v != 0 {
72            self.xs[w] |= b;
73        } else {
74            self.xs[w] &= !b;
75        }
76    }
77    #[inline]
78    fn set_z(&mut self, row: usize, j: usize, v: u64) {
79        let w = row * self.words + (j >> 6);
80        let b = 1u64 << (j & 63);
81        if v != 0 {
82            self.zs[w] |= b;
83        } else {
84            self.zs[w] &= !b;
85        }
86    }
87    #[inline]
88    fn flip_x(&mut self, row: usize, j: usize) {
89        self.xs[row * self.words + (j >> 6)] ^= 1u64 << (j & 63);
90    }
91    #[inline]
92    fn flip_z(&mut self, row: usize, j: usize) {
93        self.zs[row * self.words + (j >> 6)] ^= 1u64 << (j & 63);
94    }
95
96    // --- Clifford primitives (operate over the 2n state rows) ---
97    fn cnot(&mut self, a: usize, b: usize) {
98        for i in 0..2 * self.n {
99            let xa = self.xget(i, a);
100            let za = self.zget(i, a);
101            let xb = self.xget(i, b);
102            let zb = self.zget(i, b);
103            self.r[i] ^= (xa & zb & (xb ^ za ^ 1)) as u8;
104            if xa != 0 {
105                self.flip_x(i, b);
106            }
107            if zb != 0 {
108                self.flip_z(i, a);
109            }
110        }
111    }
112    fn hadamard(&mut self, a: usize) {
113        for i in 0..2 * self.n {
114            let xa = self.xget(i, a);
115            let za = self.zget(i, a);
116            self.r[i] ^= (xa & za) as u8;
117            // swap x,z at (i,a)
118            self.set_x(i, a, za);
119            self.set_z(i, a, xa);
120        }
121    }
122    fn phase(&mut self, a: usize) {
123        for i in 0..2 * self.n {
124            let xa = self.xget(i, a);
125            let za = self.zget(i, a);
126            self.r[i] ^= (xa & za) as u8;
127            if xa != 0 {
128                self.flip_z(i, a);
129            }
130            let _ = za;
131        }
132    }
133    fn x(&mut self, a: usize) {
134        for i in 0..2 * self.n {
135            self.r[i] ^= self.zget(i, a) as u8;
136        }
137    }
138    fn z(&mut self, a: usize) {
139        for i in 0..2 * self.n {
140            self.r[i] ^= self.xget(i, a) as u8;
141        }
142    }
143    fn y(&mut self, a: usize) {
144        for i in 0..2 * self.n {
145            self.r[i] ^= (self.xget(i, a) ^ self.zget(i, a)) as u8;
146        }
147    }
148    fn cz(&mut self, a: usize, b: usize) {
149        self.hadamard(b);
150        self.cnot(a, b);
151        self.hadamard(b);
152    }
153
154    /// Apply one circuit gate; error on a non-Clifford gate.
155    fn apply(&mut self, g: &Gate) -> Result<(), NotClifford> {
156        let t = g.target as usize;
157        match (g.base, g.controls.as_slice()) {
158            (BaseGate::I, []) => {}
159            (BaseGate::H, []) => self.hadamard(t),
160            (BaseGate::S, []) => self.phase(t),
161            (BaseGate::Sdg, []) => {
162                self.phase(t);
163                self.phase(t);
164                self.phase(t);
165            }
166            (BaseGate::X, []) => self.x(t),
167            (BaseGate::Y, []) => self.y(t),
168            (BaseGate::Z, []) => self.z(t),
169            (BaseGate::P, []) if g.param == 1 => self.z(t),
170            (BaseGate::P, []) if g.param == 2 => self.phase(t),
171            (BaseGate::X, [c]) => self.cnot(*c as usize, t),
172            (BaseGate::Z, [c]) => self.cz(*c as usize, t),
173            (BaseGate::P, [c]) if g.param == 1 => self.cz(*c as usize, t),
174            (BaseGate::Y, [c]) => {
175                // CY = Sdg(t) · CX(c,t) · S(t)
176                self.phase(t);
177                self.cnot(*c as usize, t);
178                self.phase(t);
179                self.phase(t);
180                self.phase(t);
181            }
182            (base, ctrls) => {
183                return Err(NotClifford(format!("{base:?} with controls {ctrls:?} param {}", g.param)));
184            }
185        }
186        Ok(())
187    }
188
189    // --- measurement (CHP) ---
190    fn g_exp(x1: u64, z1: u64, x2: u64, z2: u64) -> i32 {
191        let (x2, z2) = (x2 as i32, z2 as i32);
192        match (x1, z1) {
193            (0, 0) => 0,
194            (1, 1) => z2 - x2,
195            (1, 0) => z2 * (2 * x2 - 1),
196            _ => x2 * (1 - 2 * z2),
197        }
198    }
199    fn rowsum(&mut self, h: usize, i: usize) {
200        let mut s = 2 * self.r[h] as i32 + 2 * self.r[i] as i32;
201        for j in 0..self.n {
202            s += Self::g_exp(self.xget(i, j), self.zget(i, j), self.xget(h, j), self.zget(h, j));
203        }
204        self.r[h] = if s.rem_euclid(4) == 2 { 1 } else { 0 };
205        for w in 0..self.words {
206            self.xs[h * self.words + w] ^= self.xs[i * self.words + w];
207            self.zs[h * self.words + w] ^= self.zs[i * self.words + w];
208        }
209    }
210    fn copy_row(&mut self, dst: usize, src: usize) {
211        for w in 0..self.words {
212            self.xs[dst * self.words + w] = self.xs[src * self.words + w];
213            self.zs[dst * self.words + w] = self.zs[src * self.words + w];
214        }
215        self.r[dst] = self.r[src];
216    }
217    fn zero_row(&mut self, row: usize) {
218        for w in 0..self.words {
219            self.xs[row * self.words + w] = 0;
220            self.zs[row * self.words + w] = 0;
221        }
222        self.r[row] = 0;
223    }
224
225    /// Measure qubit `a` in the computational basis, collapsing the state. Returns
226    /// the outcome bit. `rng` supplies randomness for indeterminate outcomes.
227    pub fn measure(&mut self, a: usize, rng: &mut u64) -> u8 {
228        let n = self.n;
229        // is there a stabilizer that anticommutes with Z_a? (x_{p,a}=1, p in n..2n)
230        let mut p = None;
231        for row in n..2 * n {
232            if self.xget(row, a) == 1 {
233                p = Some(row);
234                break;
235            }
236        }
237        if let Some(p) = p {
238            // random outcome
239            for i in 0..2 * n {
240                if i != p && self.xget(i, a) == 1 {
241                    self.rowsum(i, p);
242                }
243            }
244            self.copy_row(p - n, p); // destabilizer <- old stabilizer
245            self.zero_row(p);
246            self.set_z(p, a, 1);
247            let bit = (splitmix64(rng) & 1) as u8;
248            self.r[p] = bit;
249            bit
250        } else {
251            // determined outcome: accumulate into scratch row 2n
252            let scratch = 2 * n;
253            self.zero_row(scratch);
254            for i in 0..n {
255                if self.xget(i, a) == 1 {
256                    self.rowsum(scratch, i + n);
257                }
258            }
259            self.r[scratch]
260        }
261    }
262}
263
264// ===========================================================================
265// Runner + sampler
266// ===========================================================================
267
268/// Run a Clifford `circuit` on the stabilizer tableau. Errors on a non-Clifford
269/// gate (the caller should fall back to a universal backend).
270pub fn run(circuit: &Circuit) -> Result<Tableau, NotClifford> {
271    let mut t = Tableau::identity(circuit.n_qubits as usize);
272    for g in &circuit.ops {
273        t.apply(g)?;
274    }
275    Ok(t)
276}
277
278/// One shot: measure every qubit of a fresh copy, packing the outcome into a `u64`
279/// (`n ≤ 64`). Bit `i` = qubit `i`, matching the dense simulator's index order.
280fn shot(base: &Tableau, rng: &mut u64) -> u64 {
281    let mut t = base.clone();
282    let mut out = 0u64;
283    for q in 0..t.n.min(64) {
284        if t.measure(q, rng) == 1 {
285            out |= 1u64 << q;
286        }
287    }
288    out
289}
290
291/// Sample `shots` measurements of a Clifford circuit as `(outcome, count)` pairs
292/// (outcomes as `u64`, `n ≤ 64`), sorted by count descending. Deterministic in
293/// `(seed, shots)`.
294pub fn sample(circuit: &Circuit, seed: u64, shots: u64) -> Result<Vec<(u64, u64)>, NotClifford> {
295    let base = run(circuit)?;
296    let mut rng = seed ^ 0x5DEE_CE66_D3A9_1B2C;
297    let mut counts: std::collections::HashMap<u64, u64> = std::collections::HashMap::new();
298    for _ in 0..shots {
299        *counts.entry(shot(&base, &mut rng)).or_insert(0) += 1;
300    }
301    let mut v: Vec<(u64, u64)> = counts.into_iter().collect();
302    v.sort_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
303    Ok(v)
304}
305
306/// The number of qubits the stabilizer path can handle for a given circuit — the
307/// cascade's scaling story (`O(n²)` per gate, no `2ⁿ` state vector).
308pub fn max_practical_qubits() -> usize {
309    4096
310}
311
312#[cfg(test)]
313mod tests {
314    use super::*;
315
316    fn ghz(n: u8) -> Circuit {
317        let mut c = Circuit::new(n);
318        c.h(0);
319        for i in 1..n {
320            c.cx(i - 1, i);
321        }
322        c
323    }
324
325    #[test]
326    fn zero_state_is_deterministic_zero() {
327        let c = Circuit::new(4);
328        let s = sample(&c, 1, 200).unwrap();
329        assert_eq!(s.len(), 1);
330        assert_eq!(s[0].0, 0);
331        assert_eq!(s[0].1, 200);
332    }
333
334    #[test]
335    fn ghz_samples_all_or_nothing() {
336        let s = sample(&ghz(5), 42, 4000).unwrap();
337        // only |00000> and |11111> appear, each ~half
338        assert_eq!(s.len(), 2, "GHZ has exactly two outcomes");
339        let outcomes: Vec<u64> = s.iter().map(|(o, _)| *o).collect();
340        assert!(outcomes.contains(&0) && outcomes.contains(&0b11111));
341        for (_, cnt) in &s {
342            assert!((*cnt as i64 - 2000).abs() < 250, "GHZ halves should be ~even: {cnt}");
343        }
344    }
345
346    #[test]
347    fn matches_dense_support_on_a_clifford_circuit() {
348        // a non-trivial Clifford circuit: every sampled outcome must have nonzero
349        // dense probability, and the count of distinct outcomes must match.
350        let mut c = Circuit::new(4);
351        c.h(0);
352        c.h(1);
353        c.cx(0, 2);
354        c.cx(1, 3);
355        c.s(2);
356        c.cz(0, 1);
357        let stab = sample(&c, 7, 8000).unwrap();
358        let dense = c.simulate().unwrap();
359        let probs = dense.prob_weights();
360        let total: i128 = probs.iter().sum();
361        // support match
362        for (o, _) in &stab {
363            assert!(probs[*o as usize] > 0, "stabilizer outcome {o} has zero dense probability");
364        }
365        let dense_support = probs.iter().filter(|&&p| p > 0).count();
366        assert_eq!(stab.len(), dense_support, "distinct-outcome count must match dense");
367        // frequencies roughly uniform over the support (Clifford ⇒ flat)
368        let expect = 8000.0 / dense_support as f64;
369        for (_, cnt) in &stab {
370            assert!((*cnt as f64 - expect).abs() < expect * 0.35, "freq off: {cnt} vs {expect}");
371        }
372        let _ = total;
373    }
374
375    #[test]
376    fn scales_past_the_dense_wall() {
377        // 200-qubit GHZ — 2^200 amplitudes is impossible densely; here it's instant,
378        // and every shot must be all-0 or all-1 (perfect parity correlation).
379        let n = 200u8;
380        let c = ghz(n);
381        let base = run(&c).unwrap();
382        let mut rng = 123u64;
383        for _ in 0..50 {
384            let mut t = base.clone();
385            let first = t.measure(0, &mut rng);
386            for q in 1..n as usize {
387                assert_eq!(t.measure(q, &mut rng), first, "GHZ parity broken at large n");
388            }
389        }
390    }
391
392    #[test]
393    fn deterministic_in_seed() {
394        let a = sample(&ghz(6), 99, 500).unwrap();
395        let b = sample(&ghz(6), 99, 500).unwrap();
396        assert_eq!(a, b, "same seed must give the same histogram");
397    }
398
399    #[test]
400    fn rejects_non_clifford() {
401        let mut c = Circuit::new(2);
402        c.h(0);
403        c.t(0); // non-Clifford
404        assert!(run(&c).is_err());
405        // Toffoli (2 controls) also rejected
406        let mut c2 = Circuit::new(3);
407        c2.ccx(0, 1, 2);
408        assert!(run(&c2).is_err());
409    }
410}