Skip to main content

oxicuda_quantum/stabilizer/
tableau.rs

1//! Stabilizer-formalism (CHP) tableau simulator.
2//!
3//! Implements the Aaronson–Gottesman 2004 "Improved Simulation of Stabilizer
4//! Circuits" tableau algorithm. Clifford circuits (built from `H`, `S`, `CNOT`
5//! and Pauli gates) acting on `n` qubits are simulated in time polynomial in
6//! `n` by tracking a binary tableau of stabilizer/destabilizer generators
7//! rather than the exponentially large state vector.
8//!
9//! Tableau layout (Aaronson–Gottesman): there are `2n + 1` rows of `n` columns.
10//! Rows `0..n` are the *destabilizer* generators, rows `n..2n` are the
11//! *stabilizer* generators and row `2n` is a scratch row used by deterministic
12//! measurement. Each row `i` stores `x[i][0..n]`, `z[i][0..n]` (the symplectic
13//! representation of a Pauli operator) and a phase bit `r[i]` (0 → `+`,
14//! 1 → `−`). This module is fully self-contained and does NOT use the
15//! state-vector simulator.
16
17use crate::error::{QuantumError, QuantumResult};
18use crate::handle::LcgRng;
19
20/// CHP stabilizer tableau for poly-time Clifford-circuit simulation.
21///
22/// The `x` and `z` fields are flat row-major buffers of length `(2n + 1) * n`;
23/// element `(row, col)` lives at `row * n + col`. The phase buffer `r` has
24/// length `2n + 1`.
25#[derive(Debug, Clone)]
26pub struct StabilizerTableau {
27    n_qubits: usize,
28    /// X bits, row-major `(2n+1) × n`.
29    x: Vec<bool>,
30    /// Z bits, row-major `(2n+1) × n`.
31    z: Vec<bool>,
32    /// Phase bits, length `2n + 1`.
33    r: Vec<bool>,
34}
35
36impl StabilizerTableau {
37    /// Build the tableau for the computational-basis state |0…0⟩.
38    ///
39    /// Destabilizers are initialized to `X_i` (row `i`, `i < n`) and stabilizers
40    /// to `Z_i` (row `n + i`); all phases are `+`.
41    pub fn new(n_qubits: usize) -> QuantumResult<Self> {
42        if n_qubits == 0 {
43            return Err(QuantumError::InvalidQubitCount { n: 0 });
44        }
45        let n_rows = 2 * n_qubits + 1;
46        let mut x = vec![false; n_rows * n_qubits];
47        let mut z = vec![false; n_rows * n_qubits];
48        let r = vec![false; n_rows];
49
50        for i in 0..n_qubits {
51            // Destabilizer i = X_i.
52            x[i * n_qubits + i] = true;
53            // Stabilizer i (row n + i) = Z_i.
54            z[(n_qubits + i) * n_qubits + i] = true;
55        }
56
57        Ok(Self { n_qubits, x, z, r })
58    }
59
60    /// Number of qubits the tableau tracks.
61    #[must_use]
62    pub fn n_qubits(&self) -> usize {
63        self.n_qubits
64    }
65
66    /// Number of rows in the tableau (`2n + 1`).
67    #[inline]
68    fn n_rows(&self) -> usize {
69        2 * self.n_qubits + 1
70    }
71
72    /// Flat index for tableau element `(row, col)`.
73    #[inline]
74    fn idx(&self, row: usize, col: usize) -> usize {
75        row * self.n_qubits + col
76    }
77
78    /// Validate that `a` is a legal qubit index.
79    #[inline]
80    fn check_qubit(&self, a: usize) -> QuantumResult<()> {
81        if a >= self.n_qubits {
82            return Err(QuantumError::QubitIndexOutOfRange {
83                index: a,
84                n_qubits: self.n_qubits,
85            });
86        }
87        Ok(())
88    }
89
90    /// Apply a Hadamard on qubit `a`.
91    ///
92    /// For each row `i`: `r[i] ^= x[i][a] & z[i][a]` then swap `x[i][a]`,
93    /// `z[i][a]`.
94    pub fn h(&mut self, a: usize) -> QuantumResult<()> {
95        self.check_qubit(a)?;
96        let n_rows = self.n_rows();
97        for i in 0..n_rows {
98            let xi = self.idx(i, a);
99            let xa = self.x[xi];
100            let za = self.z[xi];
101            self.r[i] ^= xa & za;
102            self.x[xi] = za;
103            self.z[xi] = xa;
104        }
105        Ok(())
106    }
107
108    /// Apply a phase gate `S` on qubit `a`.
109    ///
110    /// For each row `i`: `r[i] ^= x[i][a] & z[i][a]` then `z[i][a] ^= x[i][a]`.
111    pub fn s(&mut self, a: usize) -> QuantumResult<()> {
112        self.check_qubit(a)?;
113        let n_rows = self.n_rows();
114        for i in 0..n_rows {
115            let xi = self.idx(i, a);
116            let xa = self.x[xi];
117            self.r[i] ^= xa & self.z[xi];
118            self.z[xi] ^= xa;
119        }
120        Ok(())
121    }
122
123    /// Apply a controlled-NOT with the given `control` and `target` qubits.
124    ///
125    /// For each row `i`:
126    /// `r[i] ^= x[i][c] & z[i][t] & (x[i][t] ^ z[i][c] ^ true)`,
127    /// `x[i][t] ^= x[i][c]`, `z[i][c] ^= z[i][t]`.
128    pub fn cnot(&mut self, control: usize, target: usize) -> QuantumResult<()> {
129        self.check_qubit(control)?;
130        self.check_qubit(target)?;
131        if control == target {
132            return Err(QuantumError::InvalidParameter {
133                name: "control and target must differ".into(),
134            });
135        }
136        let n_rows = self.n_rows();
137        for i in 0..n_rows {
138            let ci = self.idx(i, control);
139            let ti = self.idx(i, target);
140            let xc = self.x[ci];
141            let zc = self.z[ci];
142            let xt = self.x[ti];
143            let zt = self.z[ti];
144            self.r[i] ^= xc & zt & (xt ^ zc ^ true);
145            self.x[ti] = xt ^ xc;
146            self.z[ci] = zc ^ zt;
147        }
148        Ok(())
149    }
150
151    /// Apply a Pauli-X on qubit `a`, realized as `X = H S S H`.
152    pub fn x(&mut self, a: usize) -> QuantumResult<()> {
153        self.check_qubit(a)?;
154        self.h(a)?;
155        self.s(a)?;
156        self.s(a)?;
157        self.h(a)
158    }
159
160    /// Apply a Pauli-Z on qubit `a`, realized as `Z = S S`.
161    pub fn z(&mut self, a: usize) -> QuantumResult<()> {
162        self.check_qubit(a)?;
163        self.s(a)?;
164        self.s(a)
165    }
166
167    /// Apply a Pauli-Y on qubit `a`.
168    ///
169    /// `Y = i·X·Z`; applying `Z` then `X` realizes `Y` up to a global phase,
170    /// which is unobservable in the stabilizer formalism.
171    pub fn y(&mut self, a: usize) -> QuantumResult<()> {
172        self.check_qubit(a)?;
173        self.z(a)?;
174        self.x(a)
175    }
176
177    /// Phase contribution `g` (power of `i`, in `{-1, 0, 1}`) accrued when
178    /// left-multiplying the Pauli `(x1, z1)` by `(x2, z2)` — the function from
179    /// the Aaronson–Gottesman paper.
180    #[inline]
181    fn g(x1: bool, z1: bool, x2: bool, z2: bool) -> i32 {
182        let x2i = i32::from(x2);
183        let z2i = i32::from(z2);
184        match (x1, z1) {
185            (false, false) => 0,
186            (true, true) => z2i - x2i,
187            (true, false) => z2i * (2 * x2i - 1),
188            (false, true) => x2i * (1 - 2 * z2i),
189        }
190    }
191
192    /// `rowsum(h, i)`: left-multiply row `h` by row `i`, accumulating the phase
193    /// exactly as in the paper. Sets row `h` to the product `R_i · R_h`.
194    fn rowsum(&mut self, h: usize, i: usize) {
195        let mut acc: i32 = 2 * i32::from(self.r[h]) + 2 * i32::from(self.r[i]);
196        for j in 0..self.n_qubits {
197            let hj = self.idx(h, j);
198            let ij = self.idx(i, j);
199            acc += Self::g(self.x[ij], self.z[ij], self.x[hj], self.z[hj]);
200        }
201        // acc mod 4 ∈ {0, 2}: 0 → phase +, 2 → phase −.
202        let m = acc.rem_euclid(4);
203        self.r[h] = m == 2;
204        for j in 0..self.n_qubits {
205            let hj = self.idx(h, j);
206            let ij = self.idx(i, j);
207            self.x[hj] ^= self.x[ij];
208            self.z[hj] ^= self.z[ij];
209        }
210    }
211
212    /// Whether a Z-basis measurement of qubit `a` is deterministic.
213    ///
214    /// The outcome is random iff some stabilizer generator anticommutes with
215    /// `Z_a`, i.e. some row `p` in `n..2n` has `x[p][a] == true`.
216    pub fn is_deterministic(&self, a: usize) -> QuantumResult<bool> {
217        self.check_qubit(a)?;
218        for p in self.n_qubits..(2 * self.n_qubits) {
219            if self.x[self.idx(p, a)] {
220                return Ok(false);
221            }
222        }
223        Ok(true)
224    }
225
226    /// Copy row `src` into row `dst` (x, z and phase bits).
227    fn copy_row(&mut self, dst: usize, src: usize) {
228        for j in 0..self.n_qubits {
229            let d = self.idx(dst, j);
230            let s = self.idx(src, j);
231            self.x[d] = self.x[s];
232            self.z[d] = self.z[s];
233        }
234        self.r[dst] = self.r[src];
235    }
236
237    /// Zero every x/z/phase bit of row `row`.
238    fn zero_row(&mut self, row: usize) {
239        for j in 0..self.n_qubits {
240            let idx = self.idx(row, j);
241            self.x[idx] = false;
242            self.z[idx] = false;
243        }
244        self.r[row] = false;
245    }
246
247    /// Perform a Z-basis measurement of qubit `a`, collapsing the tableau and
248    /// returning the outcome (`true` = |1⟩).
249    pub fn measure(&mut self, a: usize, rng: &mut LcgRng) -> QuantumResult<bool> {
250        self.check_qubit(a)?;
251        let n = self.n_qubits;
252
253        // Find a stabilizer row that anticommutes with Z_a (x[p][a] == true).
254        let mut pivot: Option<usize> = None;
255        for p in n..(2 * n) {
256            if self.x[self.idx(p, a)] {
257                pivot = Some(p);
258                break;
259            }
260        }
261
262        match pivot {
263            Some(p) => {
264                // Random outcome.
265                for i in 0..(2 * n) {
266                    if i != p && self.x[self.idx(i, a)] {
267                        self.rowsum(i, p);
268                    }
269                }
270                // Destabilizer p-n becomes the old stabilizer p.
271                self.copy_row(p - n, p);
272                // Row p becomes Z_a with a uniformly random phase.
273                self.zero_row(p);
274                let bit = (rng.next_u32() & 1) == 1;
275                self.r[p] = bit;
276                let pa = self.idx(p, a);
277                self.z[pa] = true;
278                Ok(bit)
279            }
280            None => {
281                // Deterministic outcome: assemble the sign via the scratch row.
282                let scratch = 2 * n;
283                self.zero_row(scratch);
284                for i in 0..n {
285                    if self.x[self.idx(i, a)] {
286                        self.rowsum(scratch, i + n);
287                    }
288                }
289                Ok(self.r[scratch])
290            }
291        }
292    }
293}
294
295#[cfg(test)]
296mod tests {
297    use super::*;
298
299    #[test]
300    fn new_zero_state_measures_all_zero_deterministically() {
301        let mut tab = StabilizerTableau::new(4).expect("valid 4-qubit tableau");
302        let mut rng = LcgRng::new(1);
303        for q in 0..4 {
304            assert!(
305                tab.is_deterministic(q).expect("qubit index in range"),
306                "qubit {q} not deterministic"
307            );
308            assert!(
309                !tab.measure(q, &mut rng).expect("measure qubit in range"),
310                "qubit {q} measured 1"
311            );
312        }
313    }
314
315    #[test]
316    fn hadamard_makes_qubit_non_deterministic() {
317        let mut tab = StabilizerTableau::new(2).expect("valid 2-qubit tableau");
318        tab.h(0).expect("hadamard on qubit 0");
319        assert!(!tab.is_deterministic(0).expect("qubit 0 determinism check"));
320        // Untouched qubit stays deterministic.
321        assert!(tab.is_deterministic(1).expect("qubit 1 determinism check"));
322    }
323
324    #[test]
325    fn hadamard_measure_reproducible_fixed_seed() {
326        let outcome = |seed: u64| {
327            let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
328            tab.h(0).expect("hadamard on qubit 0");
329            let mut rng = LcgRng::new(seed);
330            tab.measure(0, &mut rng).expect("measure qubit 0")
331        };
332        assert_eq!(outcome(123), outcome(123));
333        assert_eq!(outcome(999), outcome(999));
334    }
335
336    #[test]
337    fn bell_state_measurements_correlated() {
338        for seed in [1u64, 2, 3, 7, 42, 100, 2024] {
339            let mut tab = StabilizerTableau::new(2).expect("valid 2-qubit tableau");
340            tab.h(0).expect("hadamard on qubit 0");
341            tab.cnot(0, 1).expect("CNOT with control 0 and target 1");
342            let mut rng = LcgRng::new(seed);
343            let m0 = tab.measure(0, &mut rng).expect("measure qubit 0");
344            let m1 = tab.measure(1, &mut rng).expect("measure qubit 1");
345            assert_eq!(m0, m1, "Bell pair not correlated for seed {seed}");
346        }
347    }
348
349    #[test]
350    fn ghz_state_all_measurements_equal() {
351        for seed in [0u64, 5, 11, 77, 256, 9999] {
352            let mut tab = StabilizerTableau::new(3).expect("valid 3-qubit tableau");
353            tab.h(0).expect("hadamard on qubit 0");
354            tab.cnot(0, 1).expect("CNOT with control 0 and target 1");
355            tab.cnot(1, 2).expect("CNOT with control 1 and target 2");
356            let mut rng = LcgRng::new(seed);
357            let m0 = tab.measure(0, &mut rng).expect("measure qubit 0");
358            let m1 = tab.measure(1, &mut rng).expect("measure qubit 1");
359            let m2 = tab.measure(2, &mut rng).expect("measure qubit 2");
360            assert_eq!(m0, m1, "GHZ q0!=q1 seed {seed}");
361            assert_eq!(m1, m2, "GHZ q1!=q2 seed {seed}");
362        }
363    }
364
365    #[test]
366    fn x_gate_flips_zero_to_one() {
367        let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
368        tab.x(0).expect("X gate on qubit 0");
369        let mut rng = LcgRng::new(1);
370        assert!(tab.is_deterministic(0).expect("qubit 0 determinism check"));
371        assert!(tab.measure(0, &mut rng).expect("measure qubit 0"));
372    }
373
374    #[test]
375    fn double_hadamard_is_identity() {
376        let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
377        tab.h(0).expect("first hadamard on qubit 0");
378        tab.h(0).expect("second hadamard on qubit 0");
379        let mut rng = LcgRng::new(5);
380        assert!(tab.is_deterministic(0).expect("qubit 0 determinism check"));
381        assert!(!tab.measure(0, &mut rng).expect("measure qubit 0"));
382    }
383
384    #[test]
385    fn z_from_s_squared_leaves_zero_deterministic() {
386        let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
387        tab.z(0).expect("Z gate on qubit 0");
388        let mut rng = LcgRng::new(8);
389        assert!(tab.is_deterministic(0).expect("qubit 0 determinism check"));
390        assert!(!tab.measure(0, &mut rng).expect("measure qubit 0"));
391    }
392
393    #[test]
394    fn measuring_same_qubit_twice_gives_same_result() {
395        for seed in [1u64, 13, 88, 314] {
396            let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
397            tab.h(0).expect("hadamard on qubit 0");
398            let mut rng = LcgRng::new(seed);
399            let first = tab
400                .measure(0, &mut rng)
401                .expect("first measurement of qubit 0");
402            // After collapse the qubit is deterministic and yields the same bit.
403            assert!(
404                tab.is_deterministic(0)
405                    .expect("qubit 0 determinism check after collapse")
406            );
407            let second = tab
408                .measure(0, &mut rng)
409                .expect("second measurement of qubit 0");
410            assert_eq!(first, second, "collapse not respected for seed {seed}");
411        }
412    }
413
414    #[test]
415    fn n_qubits_getter() {
416        let tab = StabilizerTableau::new(5).expect("valid 5-qubit tableau");
417        assert_eq!(tab.n_qubits(), 5);
418    }
419
420    #[test]
421    fn out_of_range_qubit_errors() {
422        let mut tab = StabilizerTableau::new(2).expect("valid 2-qubit tableau");
423        let mut rng = LcgRng::new(1);
424        assert!(tab.h(2).is_err());
425        assert!(tab.s(5).is_err());
426        assert!(tab.cnot(0, 9).is_err());
427        assert!(tab.cnot(7, 1).is_err());
428        assert!(tab.measure(3, &mut rng).is_err());
429        assert!(tab.is_deterministic(4).is_err());
430        assert!(tab.x(2).is_err());
431        assert!(tab.y(2).is_err());
432        assert!(tab.z(2).is_err());
433    }
434
435    #[test]
436    fn zero_qubit_count_errors() {
437        assert!(StabilizerTableau::new(0).is_err());
438    }
439
440    #[test]
441    fn cnot_control_equals_target_errors() {
442        let mut tab = StabilizerTableau::new(3).expect("valid 3-qubit tableau");
443        assert!(tab.cnot(1, 1).is_err());
444    }
445
446    #[test]
447    fn deterministic_given_seed_identical_sequences() {
448        let run = || {
449            let mut tab = StabilizerTableau::new(3).expect("valid 3-qubit tableau");
450            let mut rng = LcgRng::new(2024);
451            tab.h(0).expect("hadamard on qubit 0");
452            tab.cnot(0, 1).expect("CNOT with control 0 and target 1");
453            tab.cnot(1, 2).expect("CNOT with control 1 and target 2");
454            tab.h(2).expect("hadamard on qubit 2");
455            let a = tab.measure(0, &mut rng).expect("measure qubit 0");
456            let b = tab.measure(1, &mut rng).expect("measure qubit 1");
457            let c = tab.measure(2, &mut rng).expect("measure qubit 2");
458            (a, b, c)
459        };
460        assert_eq!(run(), run());
461    }
462
463    #[test]
464    fn x_via_h_s_s_h_flips_zero() {
465        let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
466        // X = H S S H (exactly the decomposition used internally).
467        tab.h(0).expect("hadamard on qubit 0");
468        tab.s(0).expect("first S gate on qubit 0");
469        tab.s(0).expect("second S gate on qubit 0");
470        tab.h(0).expect("second hadamard on qubit 0");
471        let mut rng = LcgRng::new(3);
472        assert!(tab.measure(0, &mut rng).expect("measure qubit 0"));
473    }
474
475    #[test]
476    fn y_gate_flips_zero_to_one() {
477        // Y|0⟩ = i|1⟩, so a Z-basis measurement is deterministically 1.
478        let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
479        tab.y(0).expect("Y gate on qubit 0");
480        let mut rng = LcgRng::new(4);
481        assert!(tab.is_deterministic(0).expect("qubit 0 determinism check"));
482        assert!(tab.measure(0, &mut rng).expect("measure qubit 0"));
483    }
484
485    #[test]
486    fn double_x_returns_to_zero() {
487        let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
488        tab.x(0).expect("first X gate on qubit 0");
489        tab.x(0).expect("second X gate on qubit 0");
490        let mut rng = LcgRng::new(6);
491        assert!(!tab.measure(0, &mut rng).expect("measure qubit 0"));
492    }
493
494    #[test]
495    fn random_outcome_distribution_balanced() {
496        // A single H on |0⟩ should give both outcomes across many seeds.
497        let mut zeros = 0_u32;
498        let mut ones = 0_u32;
499        for seed in 0..200u64 {
500            let mut tab = StabilizerTableau::new(1).expect("valid 1-qubit tableau");
501            tab.h(0).expect("hadamard on qubit 0");
502            let mut rng = LcgRng::new(seed.wrapping_mul(2_654_435_761));
503            if tab.measure(0, &mut rng).expect("measure qubit 0") {
504                ones += 1;
505            } else {
506                zeros += 1;
507            }
508        }
509        assert!(
510            zeros > 20 && ones > 20,
511            "imbalanced: zeros={zeros} ones={ones}"
512        );
513    }
514
515    #[test]
516    fn two_qubit_independent_hadamards_uncorrelated_marginals() {
517        // H on each of two qubits, then measure: both qubits individually random.
518        let mut q0_ones = 0_u32;
519        let mut q1_ones = 0_u32;
520        for seed in 0..150u64 {
521            let mut tab = StabilizerTableau::new(2).expect("valid 2-qubit tableau");
522            tab.h(0).expect("hadamard on qubit 0");
523            tab.h(1).expect("hadamard on qubit 1");
524            let mut rng = LcgRng::new(seed.wrapping_mul(1_000_003).wrapping_add(17));
525            if tab.measure(0, &mut rng).expect("measure qubit 0") {
526                q0_ones += 1;
527            }
528            if tab.measure(1, &mut rng).expect("measure qubit 1") {
529                q1_ones += 1;
530            }
531        }
532        assert!(q0_ones > 15 && q0_ones < 135, "q0 not random: {q0_ones}");
533        assert!(q1_ones > 15 && q1_ones < 135, "q1 not random: {q1_ones}");
534    }
535}