ruqu_core/stabilizer.rs
1//! Aaronson-Gottesman stabilizer simulator for Clifford circuits.
2//!
3//! Uses a tableau of 2n rows and (2n+1) columns to represent the stabilizer
4//! and destabilizer generators of an n-qubit state. Each Clifford gate is
5//! applied in O(n) time and each measurement in O(n^2), enabling simulation
6//! of millions of qubits for circuits composed entirely of Clifford gates.
7//!
8//! Reference: Aaronson & Gottesman, "Improved Simulation of Stabilizer
9//! Circuits", Phys. Rev. A 70, 052328 (2004).
10
11use crate::error::{QuantumError, Result};
12use crate::gate::Gate;
13use crate::types::MeasurementOutcome;
14use rand::rngs::StdRng;
15use rand::{Rng, SeedableRng};
16
17/// Stabilizer state for efficient Clifford circuit simulation.
18///
19/// Uses the Aaronson-Gottesman tableau representation to simulate
20/// Clifford circuits in O(n^2) time per gate, enabling simulation
21/// of millions of qubits.
22pub struct StabilizerState {
23 num_qubits: usize,
24 /// Tableau: 2n rows, each row has n X-bits, n Z-bits, and 1 phase bit.
25 /// Stored as a flat `Vec<bool>` for simplicity.
26 /// Row i occupies indices `[i * stride .. (i+1) * stride)`.
27 /// Layout within a row: `x[0..n], z[0..n], r` (total width = 2n + 1).
28 tableau: Vec<bool>,
29 rng: StdRng,
30 measurement_record: Vec<MeasurementOutcome>,
31}
32
33impl StabilizerState {
34 // -----------------------------------------------------------------------
35 // Construction
36 // -----------------------------------------------------------------------
37
38 /// Create a new stabilizer state representing |00...0>.
39 ///
40 /// The initial tableau has destabilizer i = X_i, stabilizer i = Z_i,
41 /// and all phase bits set to 0.
42 pub fn new(num_qubits: usize) -> Result<Self> {
43 Self::new_with_seed(num_qubits, 0)
44 }
45
46 /// Create a new stabilizer state with a specific RNG seed.
47 pub fn new_with_seed(num_qubits: usize, seed: u64) -> Result<Self> {
48 if num_qubits == 0 {
49 return Err(QuantumError::CircuitError(
50 "stabilizer state requires at least 1 qubit".into(),
51 ));
52 }
53
54 let n = num_qubits;
55 let stride = 2 * n + 1;
56 let total = 2 * n * stride;
57 let mut tableau = vec![false; total];
58
59 // Destabilizer i (row i): X_i => x[i]=1, rest zero
60 for i in 0..n {
61 tableau[i * stride + i] = true; // x bit for qubit i
62 }
63 // Stabilizer i (row n+i): Z_i => z[i]=1, rest zero
64 for i in 0..n {
65 tableau[(n + i) * stride + n + i] = true; // z bit for qubit i
66 }
67
68 Ok(Self {
69 num_qubits,
70 tableau,
71 rng: StdRng::seed_from_u64(seed),
72 measurement_record: Vec::new(),
73 })
74 }
75
76 // -----------------------------------------------------------------------
77 // Tableau access helpers
78 // -----------------------------------------------------------------------
79
80 #[inline]
81 fn stride(&self) -> usize {
82 2 * self.num_qubits + 1
83 }
84
85 /// Get the X bit for `(row, col)`.
86 #[inline]
87 fn x(&self, row: usize, col: usize) -> bool {
88 self.tableau[row * self.stride() + col]
89 }
90
91 /// Get the Z bit for `(row, col)`.
92 #[inline]
93 fn z(&self, row: usize, col: usize) -> bool {
94 self.tableau[row * self.stride() + self.num_qubits + col]
95 }
96
97 /// Get the phase bit for `row`.
98 #[inline]
99 fn r(&self, row: usize) -> bool {
100 self.tableau[row * self.stride() + 2 * self.num_qubits]
101 }
102
103 #[inline]
104 fn set_x(&mut self, row: usize, col: usize, val: bool) {
105 let idx = row * self.stride() + col;
106 self.tableau[idx] = val;
107 }
108
109 #[inline]
110 fn set_z(&mut self, row: usize, col: usize, val: bool) {
111 let idx = row * self.stride() + self.num_qubits + col;
112 self.tableau[idx] = val;
113 }
114
115 #[inline]
116 fn set_r(&mut self, row: usize, val: bool) {
117 let idx = row * self.stride() + 2 * self.num_qubits;
118 self.tableau[idx] = val;
119 }
120
121 /// Multiply row `target` by row `source` (left-multiply the Pauli string
122 /// of `target` by that of `source`), updating the phase of `target`.
123 ///
124 /// Uses the `g` function to accumulate the phase contribution from
125 /// each qubit position.
126 fn row_mult(&mut self, target: usize, source: usize) {
127 let n = self.num_qubits;
128 let mut phase_sum: i32 = 0;
129
130 // Accumulate phase from commutation relations
131 for j in 0..n {
132 phase_sum += g(
133 self.x(source, j),
134 self.z(source, j),
135 self.x(target, j),
136 self.z(target, j),
137 );
138 }
139
140 // Combine phases: new_r = (2*r_target + 2*r_source + phase_sum) mod 4
141 // r=1 means phase -1 (i.e. factor of i^2 = -1), so we work mod 4 in
142 // units of i. r_bit maps to 0 or 2.
143 let total = 2 * (self.r(target) as i32)
144 + 2 * (self.r(source) as i32)
145 + phase_sum;
146 // Result phase bit: total mod 4 == 2 => r=1, else r=0
147 let new_r = ((total % 4) + 4) % 4 == 2;
148 self.set_r(target, new_r);
149
150 // XOR the X and Z bits
151 let stride = self.stride();
152 for j in 0..n {
153 let sx = self.tableau[source * stride + j];
154 self.tableau[target * stride + j] ^= sx;
155 }
156 for j in 0..n {
157 let sz = self.tableau[source * stride + n + j];
158 self.tableau[target * stride + n + j] ^= sz;
159 }
160 }
161
162 // -----------------------------------------------------------------------
163 // Clifford gate operations
164 // -----------------------------------------------------------------------
165
166 /// Apply a Hadamard gate on `qubit`.
167 ///
168 /// Conjugation rules: H X H = Z, H Z H = X, H Y H = -Y.
169 /// Tableau update: swap X and Z columns for this qubit in every row,
170 /// and flip the phase bit where both X and Z were set (Y -> -Y).
171 pub fn hadamard(&mut self, qubit: usize) {
172 let n = self.num_qubits;
173 for i in 0..(2 * n) {
174 let xi = self.x(i, qubit);
175 let zi = self.z(i, qubit);
176 // phase flip for Y entries: if both x and z are set
177 if xi && zi {
178 self.set_r(i, !self.r(i));
179 }
180 // swap x and z
181 self.set_x(i, qubit, zi);
182 self.set_z(i, qubit, xi);
183 }
184 }
185
186 /// Apply the phase gate (S gate) on `qubit`.
187 ///
188 /// Conjugation rules: S X S^dag = Y, S Z S^dag = Z, S Y S^dag = -X.
189 /// Tableau update: Z_j -> Z_j XOR X_j, phase flipped where X and Z
190 /// are both set.
191 pub fn phase_gate(&mut self, qubit: usize) {
192 let n = self.num_qubits;
193 for i in 0..(2 * n) {
194 let xi = self.x(i, qubit);
195 let zi = self.z(i, qubit);
196 // Phase update: r ^= (x AND z)
197 if xi && zi {
198 self.set_r(i, !self.r(i));
199 }
200 // z -> z XOR x
201 self.set_z(i, qubit, zi ^ xi);
202 }
203 }
204
205 /// Apply a CNOT gate with `control` and `target`.
206 ///
207 /// Conjugation rules:
208 /// X_c -> X_c X_t, Z_t -> Z_c Z_t,
209 /// X_t -> X_t, Z_c -> Z_c.
210 /// Tableau update for every row:
211 /// phase ^= x_c AND z_t AND (x_t XOR z_c XOR 1)
212 /// x_t ^= x_c
213 /// z_c ^= z_t
214 pub fn cnot(&mut self, control: usize, target: usize) {
215 let n = self.num_qubits;
216 for i in 0..(2 * n) {
217 let xc = self.x(i, control);
218 let zt = self.z(i, target);
219 let xt = self.x(i, target);
220 let zc = self.z(i, control);
221 // Phase update
222 if xc && zt && (xt == zc) {
223 self.set_r(i, !self.r(i));
224 }
225 // x_target ^= x_control
226 self.set_x(i, target, xt ^ xc);
227 // z_control ^= z_target
228 self.set_z(i, control, zc ^ zt);
229 }
230 }
231
232 /// Apply a Pauli-X gate on `qubit`.
233 ///
234 /// Conjugation: X commutes with X, anticommutes with Z and Y.
235 /// Tableau update: flip phase where Z bit is set for this qubit.
236 pub fn x_gate(&mut self, qubit: usize) {
237 let n = self.num_qubits;
238 for i in 0..(2 * n) {
239 if self.z(i, qubit) {
240 self.set_r(i, !self.r(i));
241 }
242 }
243 }
244
245 /// Apply a Pauli-Y gate on `qubit`.
246 ///
247 /// Conjugation: Y anticommutes with both X and Z.
248 /// Tableau update: flip phase where X or Z (but via XOR: where x XOR z).
249 pub fn y_gate(&mut self, qubit: usize) {
250 let n = self.num_qubits;
251 for i in 0..(2 * n) {
252 let xi = self.x(i, qubit);
253 let zi = self.z(i, qubit);
254 // Y anticommutes with X and Z, commutes with Y and I
255 // phase flips when exactly one of x,z is set (i.e. X or Z, not Y or I)
256 if xi ^ zi {
257 self.set_r(i, !self.r(i));
258 }
259 }
260 }
261
262 /// Apply a Pauli-Z gate on `qubit`.
263 ///
264 /// Conjugation: Z commutes with Z, anticommutes with X and Y.
265 /// Tableau update: flip phase where X bit is set for this qubit.
266 pub fn z_gate(&mut self, qubit: usize) {
267 let n = self.num_qubits;
268 for i in 0..(2 * n) {
269 if self.x(i, qubit) {
270 self.set_r(i, !self.r(i));
271 }
272 }
273 }
274
275 /// Apply a CZ (controlled-Z) gate on `q1` and `q2`.
276 ///
277 /// CZ = (I x H) . CNOT . (I x H). Implemented by decomposition.
278 pub fn cz(&mut self, q1: usize, q2: usize) {
279 self.hadamard(q2);
280 self.cnot(q1, q2);
281 self.hadamard(q2);
282 }
283
284 /// Apply a SWAP gate on `q1` and `q2`.
285 ///
286 /// SWAP = CNOT(q1,q2) . CNOT(q2,q1) . CNOT(q1,q2).
287 pub fn swap(&mut self, q1: usize, q2: usize) {
288 self.cnot(q1, q2);
289 self.cnot(q2, q1);
290 self.cnot(q1, q2);
291 }
292
293 // -----------------------------------------------------------------------
294 // Measurement
295 // -----------------------------------------------------------------------
296
297 /// Measure `qubit` in the computational (Z) basis.
298 ///
299 /// Follows the Aaronson-Gottesman algorithm:
300 /// 1. Check if any stabilizer generator anticommutes with Z on the
301 /// measured qubit (i.e. has its X bit set for that qubit).
302 /// 2. If yes (random outcome): collapse the state and record the result.
303 /// 3. If no (deterministic outcome): compute the result from phases.
304 pub fn measure(&mut self, qubit: usize) -> Result<MeasurementOutcome> {
305 if qubit >= self.num_qubits {
306 return Err(QuantumError::InvalidQubitIndex {
307 index: qubit as u32,
308 num_qubits: self.num_qubits as u32,
309 });
310 }
311
312 let n = self.num_qubits;
313
314 // Search for a stabilizer (rows n..2n-1) that anticommutes with Z_qubit.
315 // A generator anticommutes with Z_qubit iff its X bit for that qubit is 1.
316 let p = (n..(2 * n)).find(|&i| self.x(i, qubit));
317
318 if let Some(p) = p {
319 // --- Random outcome ---
320 // For every other row that anticommutes with Z_qubit, multiply it by row p
321 // to make it commute.
322 for i in 0..(2 * n) {
323 if i != p && self.x(i, qubit) {
324 self.row_mult(i, p);
325 }
326 }
327
328 // Move row p to the destabilizer: copy stabilizer p to destabilizer (p-n),
329 // then set row p to be +/- Z_qubit.
330 let dest_row = p - n;
331 let stride = self.stride();
332 // Copy row p to destabilizer row
333 for j in 0..stride {
334 self.tableau[dest_row * stride + j] = self.tableau[p * stride + j];
335 }
336
337 // Clear row p and set it to Z_qubit with random phase
338 for j in 0..stride {
339 self.tableau[p * stride + j] = false;
340 }
341 self.set_z(p, qubit, true);
342
343 let result: bool = self.rng.gen();
344 self.set_r(p, result);
345
346 let outcome = MeasurementOutcome {
347 qubit: qubit as u32,
348 result,
349 probability: 0.5,
350 };
351 self.measurement_record.push(outcome.clone());
352 Ok(outcome)
353 } else {
354 // --- Deterministic outcome ---
355 // No stabilizer anticommutes with Z_qubit, so Z_qubit is in the
356 // stabilizer group. We need to determine its sign.
357 //
358 // Use a scratch row technique: set a temporary row to the identity,
359 // then multiply in every destabilizer whose corresponding stabilizer
360 // has x[qubit]=1... but since we confirmed no stabilizer has x set,
361 // we look at destabilizers instead.
362 //
363 // Actually per the CHP algorithm: accumulate into a scratch state
364 // by multiplying destabilizer rows whose *destabilizer* X bit for
365 // this qubit is set. The accumulated phase gives the measurement
366 // outcome.
367
368 // We'll use the first extra technique: allocate a scratch row
369 // initialized to +I and multiply in all generators from rows 0..n
370 // (destabilizers) that have x[qubit]=1 in the *stabilizer* row n+i.
371 // Wait -- let me re-read the CHP paper carefully.
372 //
373 // Per Aaronson-Gottesman (Section III.C, deterministic case):
374 // Set scratch = identity. For each i in 0..n, if destabilizer i
375 // has x[qubit]=1, multiply scratch by stabilizer (n+i).
376 // The phase of the scratch row gives the measurement result.
377
378 let stride = self.stride();
379 let mut scratch = vec![false; stride];
380
381 for i in 0..n {
382 // Check destabilizer row i: does it have x[qubit] set?
383 if self.x(i, qubit) {
384 // Multiply scratch by stabilizer row (n+i)
385 let stab_row = n + i;
386 let mut phase_sum: i32 = 0;
387 for j in 0..n {
388 let sx = scratch[j];
389 let sz = scratch[n + j];
390 let rx = self.x(stab_row, j);
391 let rz = self.z(stab_row, j);
392 phase_sum += g(rx, rz, sx, sz);
393 }
394 let scratch_r = scratch[2 * n];
395 let stab_r = self.r(stab_row);
396 let total = 2 * (scratch_r as i32)
397 + 2 * (stab_r as i32)
398 + phase_sum;
399 scratch[2 * n] = ((total % 4) + 4) % 4 == 2;
400
401 for j in 0..n {
402 scratch[j] ^= self.x(stab_row, j);
403 }
404 for j in 0..n {
405 scratch[n + j] ^= self.z(stab_row, j);
406 }
407 }
408 }
409
410 let result = scratch[2 * n]; // phase bit = measurement outcome
411
412 let outcome = MeasurementOutcome {
413 qubit: qubit as u32,
414 result,
415 probability: 1.0,
416 };
417 self.measurement_record.push(outcome.clone());
418 Ok(outcome)
419 }
420 }
421
422 // -----------------------------------------------------------------------
423 // Accessors
424 // -----------------------------------------------------------------------
425
426 /// Return the number of qubits in this stabilizer state.
427 pub fn num_qubits(&self) -> usize {
428 self.num_qubits
429 }
430
431 /// Return the measurement record accumulated so far.
432 pub fn measurement_record(&self) -> &[MeasurementOutcome] {
433 &self.measurement_record
434 }
435
436 /// Create a copy of this stabilizer state with a new RNG seed.
437 ///
438 /// The quantum state (tableau) is duplicated exactly; only the RNG
439 /// and measurement record are reset. This is used by the Clifford+T
440 /// backend to fork stabilizer terms during T-gate decomposition.
441 pub fn clone_with_seed(&self, seed: u64) -> Result<Self> {
442 Ok(Self {
443 num_qubits: self.num_qubits,
444 tableau: self.tableau.clone(),
445 rng: StdRng::seed_from_u64(seed),
446 measurement_record: Vec::new(),
447 })
448 }
449
450 /// Check whether a gate is a Clifford gate (simulable by this backend).
451 ///
452 /// Clifford gates are: H, X, Y, Z, S, Sdg, CNOT, CZ, SWAP.
453 /// Measure and Reset are also supported (non-unitary but handled).
454 /// T, Tdg, Rx, Ry, Rz, Phase, Rzz, and custom unitaries are NOT Clifford
455 /// in general.
456 pub fn is_clifford_gate(gate: &Gate) -> bool {
457 matches!(
458 gate,
459 Gate::H(_)
460 | Gate::X(_)
461 | Gate::Y(_)
462 | Gate::Z(_)
463 | Gate::S(_)
464 | Gate::Sdg(_)
465 | Gate::CNOT(_, _)
466 | Gate::CZ(_, _)
467 | Gate::SWAP(_, _)
468 | Gate::Measure(_)
469 | Gate::Barrier
470 )
471 }
472
473 // -----------------------------------------------------------------------
474 // Gate dispatch
475 // -----------------------------------------------------------------------
476
477 /// Apply a gate from the `Gate` enum, returning measurement outcomes if any.
478 ///
479 /// Returns an error for non-Clifford gates.
480 pub fn apply_gate(&mut self, gate: &Gate) -> Result<Vec<MeasurementOutcome>> {
481 match gate {
482 Gate::H(q) => {
483 self.hadamard(*q as usize);
484 Ok(vec![])
485 }
486 Gate::X(q) => {
487 self.x_gate(*q as usize);
488 Ok(vec![])
489 }
490 Gate::Y(q) => {
491 self.y_gate(*q as usize);
492 Ok(vec![])
493 }
494 Gate::Z(q) => {
495 self.z_gate(*q as usize);
496 Ok(vec![])
497 }
498 Gate::S(q) => {
499 self.phase_gate(*q as usize);
500 Ok(vec![])
501 }
502 Gate::Sdg(q) => {
503 // S^dag = S^3: apply S three times
504 let qu = *q as usize;
505 self.phase_gate(qu);
506 self.phase_gate(qu);
507 self.phase_gate(qu);
508 Ok(vec![])
509 }
510 Gate::CNOT(c, t) => {
511 self.cnot(*c as usize, *t as usize);
512 Ok(vec![])
513 }
514 Gate::CZ(q1, q2) => {
515 self.cz(*q1 as usize, *q2 as usize);
516 Ok(vec![])
517 }
518 Gate::SWAP(q1, q2) => {
519 self.swap(*q1 as usize, *q2 as usize);
520 Ok(vec![])
521 }
522 Gate::Measure(q) => {
523 let outcome = self.measure(*q as usize)?;
524 Ok(vec![outcome])
525 }
526 Gate::Barrier => Ok(vec![]),
527 _ => Err(QuantumError::CircuitError(format!(
528 "gate {:?} is not a Clifford gate and cannot be simulated \
529 by the stabilizer backend",
530 gate
531 ))),
532 }
533 }
534}
535
536// ---------------------------------------------------------------------------
537// Phase accumulation helper
538// ---------------------------------------------------------------------------
539
540/// Compute the phase contribution when multiplying two single-qubit Pauli
541/// operators encoded as (x, z) bits.
542///
543/// Returns 0, +1, or -1 representing a phase of i^0, i^1, or i^{-1}.
544///
545/// Encoding: (0,0)=I, (1,0)=X, (1,1)=Y, (0,1)=Z.
546#[inline]
547fn g(x1: bool, z1: bool, x2: bool, z2: bool) -> i32 {
548 if !x1 && !z1 {
549 return 0; // I * anything = 0 phase
550 }
551 if x1 && z1 {
552 // Y * ...
553 if x2 && z2 { 0 } else if x2 { 1 } else if z2 { -1 } else { 0 }
554 } else if x1 && !z1 {
555 // X * ...
556 if x2 && z2 { -1 } else if x2 { 0 } else if z2 { 1 } else { 0 }
557 } else {
558 // Z * ... (z1 && !x1)
559 if x2 && z2 { 1 } else if x2 { -1 } else { 0 }
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566
567 #[test]
568 fn test_initial_state_measurement() {
569 // |0> state: measuring should give 0 deterministically
570 let mut state = StabilizerState::new(1).unwrap();
571 let outcome = state.measure(0).unwrap();
572 assert!(!outcome.result, "measuring |0> should yield 0");
573 assert_eq!(outcome.probability, 1.0);
574 }
575
576 #[test]
577 fn test_x_gate_flips() {
578 // X|0> = |1>: measuring should give 1 deterministically
579 let mut state = StabilizerState::new(1).unwrap();
580 state.x_gate(0);
581 let outcome = state.measure(0).unwrap();
582 assert!(outcome.result, "measuring X|0> should yield 1");
583 assert_eq!(outcome.probability, 1.0);
584 }
585
586 #[test]
587 fn test_hadamard_creates_superposition() {
588 // H|0> = |+>: measurement should be random (prob 0.5)
589 let mut state = StabilizerState::new_with_seed(1, 42).unwrap();
590 state.hadamard(0);
591 let outcome = state.measure(0).unwrap();
592 assert_eq!(outcome.probability, 0.5);
593 }
594
595 #[test]
596 fn test_bell_state() {
597 // Create Bell state |00> + |11> (up to normalization)
598 // Both qubits should always measure the same value.
599 let mut state = StabilizerState::new_with_seed(2, 123).unwrap();
600 state.hadamard(0);
601 state.cnot(0, 1);
602 let o0 = state.measure(0).unwrap();
603 let o1 = state.measure(1).unwrap();
604 assert_eq!(
605 o0.result, o1.result,
606 "Bell state qubits must be correlated"
607 );
608 }
609
610 #[test]
611 fn test_z_gate_phase() {
612 // Z|0> = |0> (no change)
613 let mut state = StabilizerState::new(1).unwrap();
614 state.z_gate(0);
615 let outcome = state.measure(0).unwrap();
616 assert!(!outcome.result, "Z|0> should still be |0>");
617
618 // Z|1> = -|1> (global phase, same measurement)
619 let mut state2 = StabilizerState::new(1).unwrap();
620 state2.x_gate(0);
621 state2.z_gate(0);
622 let outcome2 = state2.measure(0).unwrap();
623 assert!(outcome2.result, "Z|1> should still measure as |1>");
624 }
625
626 #[test]
627 fn test_phase_gate() {
628 // S^2 = Z: applying S twice should act as Z
629 let mut s1 = StabilizerState::new_with_seed(1, 99).unwrap();
630 s1.hadamard(0);
631 s1.phase_gate(0);
632 s1.phase_gate(0);
633 // Now state is Z H|0> = Z|+> = |->
634
635 let mut s2 = StabilizerState::new_with_seed(1, 99).unwrap();
636 s2.hadamard(0);
637 s2.z_gate(0);
638 // Also |->
639
640 // Measuring in X basis: H then measure
641 s1.hadamard(0);
642 s2.hadamard(0);
643 let o1 = s1.measure(0).unwrap();
644 let o2 = s2.measure(0).unwrap();
645 assert_eq!(o1.result, o2.result, "S^2 should equal Z");
646 }
647
648 #[test]
649 fn test_cz_gate() {
650 // CZ on |+0> should give |0+> + |1-> = |00> + |01> + |10> - |11>
651 // This is a product state in the X-Z basis.
652 // After CZ, measuring qubit 0 in Z basis should still be random.
653 let mut state = StabilizerState::new_with_seed(2, 777).unwrap();
654 state.hadamard(0);
655 state.cz(0, 1);
656 let o = state.measure(0).unwrap();
657 assert_eq!(o.probability, 0.5);
658 }
659
660 #[test]
661 fn test_swap_gate() {
662 // Prepare |10>, SWAP -> |01>
663 let mut state = StabilizerState::new(2).unwrap();
664 state.x_gate(0);
665 state.swap(0, 1);
666 let o0 = state.measure(0).unwrap();
667 let o1 = state.measure(1).unwrap();
668 assert!(!o0.result, "after SWAP, qubit 0 should be |0>");
669 assert!(o1.result, "after SWAP, qubit 1 should be |1>");
670 }
671
672 #[test]
673 fn test_is_clifford_gate() {
674 assert!(StabilizerState::is_clifford_gate(&Gate::H(0)));
675 assert!(StabilizerState::is_clifford_gate(&Gate::CNOT(0, 1)));
676 assert!(StabilizerState::is_clifford_gate(&Gate::S(0)));
677 assert!(!StabilizerState::is_clifford_gate(&Gate::T(0)));
678 assert!(!StabilizerState::is_clifford_gate(&Gate::Rx(0, 0.5)));
679 }
680
681 #[test]
682 fn test_apply_gate_dispatch() {
683 let mut state = StabilizerState::new(2).unwrap();
684 state.apply_gate(&Gate::H(0)).unwrap();
685 state.apply_gate(&Gate::CNOT(0, 1)).unwrap();
686 let outcomes = state.apply_gate(&Gate::Measure(0)).unwrap();
687 assert_eq!(outcomes.len(), 1);
688 }
689
690 #[test]
691 fn test_non_clifford_rejected() {
692 let mut state = StabilizerState::new(1).unwrap();
693 let result = state.apply_gate(&Gate::T(0));
694 assert!(result.is_err());
695 }
696
697 #[test]
698 fn test_measurement_record() {
699 let mut state = StabilizerState::new(2).unwrap();
700 state.x_gate(1);
701 state.measure(0).unwrap();
702 state.measure(1).unwrap();
703 let record = state.measurement_record();
704 assert_eq!(record.len(), 2);
705 assert!(!record[0].result);
706 assert!(record[1].result);
707 }
708
709 #[test]
710 fn test_invalid_qubit_measure() {
711 let mut state = StabilizerState::new(2).unwrap();
712 let result = state.measure(5);
713 assert!(result.is_err());
714 }
715
716 #[test]
717 fn test_y_gate() {
718 // Y|0> = i|1>, so measurement should give 1
719 let mut state = StabilizerState::new(1).unwrap();
720 state.y_gate(0);
721 let outcome = state.measure(0).unwrap();
722 assert!(outcome.result, "Y|0> should measure as |1>");
723 }
724
725 #[test]
726 fn test_sdg_gate() {
727 // Sdg = S^3, and S^4 = I, so S . Sdg = I
728 let mut state = StabilizerState::new_with_seed(1, 42).unwrap();
729 state.hadamard(0);
730 state.phase_gate(0); // S
731 state.apply_gate(&Gate::Sdg(0)).unwrap(); // Sdg
732 // Should be back to H|0> = |+>
733 state.hadamard(0);
734 let outcome = state.measure(0).unwrap();
735 assert!(!outcome.result, "S.Sdg should be identity");
736 assert_eq!(outcome.probability, 1.0);
737 }
738
739 #[test]
740 fn test_g_function() {
741 // I * anything = 0
742 assert_eq!(g(false, false, true, true), 0);
743 // X * Y = iZ => phase +1
744 assert_eq!(g(true, false, true, true), -1);
745 // X * Z = -iY => phase -1... wait: g(X, Z) = g(1,0, 0,1) = 1
746 // Actually X*Z = -iY, but g returns the exponent of i in the
747 // *product* commutation, and we get +1 here because the Pauli
748 // product rule for X*Z uses a different sign convention.
749 assert_eq!(g(true, false, false, true), 1);
750 // Y * X = -iZ => phase -1... g(1,1, 1,0) = 1
751 assert_eq!(g(true, true, true, false), 1);
752 }
753
754 #[test]
755 fn test_ghz_state() {
756 // GHZ state: H on q0, then CNOT chain
757 let n = 5;
758 let mut state = StabilizerState::new_with_seed(n, 314).unwrap();
759 state.hadamard(0);
760 for i in 0..(n - 1) {
761 state.cnot(i, i + 1);
762 }
763 // All qubits should measure the same value
764 let first = state.measure(0).unwrap();
765 for i in 1..n {
766 let oi = state.measure(i).unwrap();
767 assert_eq!(
768 first.result, oi.result,
769 "GHZ state: qubit {} disagrees with qubit 0",
770 i
771 );
772 }
773 }
774}