cqlib_core/qis/state/stabilizer.rs
1// This code is part of Cqlib.
2// Modified to support reproducible state sampling.
3//
4// (C) Copyright China Telecom Quantum Group 2026
5//
6// This code is licensed under the Apache License, Version 2.0. You may
7// obtain a copy of this license in the LICENSE.txt file in the root directory
8// of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
9//
10// Any modifications or derivative works of this code must retain this
11// copyright notice, and modified files need to carry a notice indicating
12// that they have been altered from the originals.
13//
14// Modified to validate terminal measurements and correct density matrix reset.
15
16//! Stabilizer state simulator using the Aaronson-Gottesman tableau algorithm.
17//!
18//! Implements efficient simulation of Clifford circuits using the symplectic tableau
19//! representation from:
20//!
21//! > Aaronson & Gottesman, "Improved Simulation of Stabilizer Circuits",
22//! > Phys. Rev. A 70, 052328 (2004).
23//!
24//! Non-Clifford gates (T, TDG, RX, RZ, U, …) are rejected immediately with
25//! [`QisError::NonCliffordGate`] — use [`crate::qis::Statevector`] for universal circuits.
26//!
27//! # Supported Gates
28//!
29//! | Gate | Symbol | Kind |
30//! |------|--------|------|
31//! | Hadamard | H | single-qubit |
32//! | Phase | S | single-qubit |
33//! | Phase-dagger | S† / SDG | single-qubit |
34//! | Pauli-X | X | single-qubit |
35//! | Pauli-Y | Y | single-qubit |
36//! | Pauli-Z | Z | single-qubit |
37//! | √X | X2P / SX | single-qubit |
38//! | √X† | X2M / SXdg | single-qubit |
39//! | √Y | Y2P / SY | single-qubit |
40//! | √Y† | Y2M / SYdg | single-qubit |
41//! | Controlled-NOT | CX / CNOT | two-qubit |
42//! | Controlled-Y | CY | two-qubit |
43//! | Controlled-Z | CZ | two-qubit |
44//! | SWAP | SWAP | two-qubit |
45//!
46//! # Performance
47//!
48//! | Operation | Complexity | Notes |
49//! |-----------|-----------|-------|
50//! | Single-qubit gate | O(n) | one pass over 2n rows |
51//! | Two-qubit gate | O(n) | one pass over 2n rows |
52//! | Measurement (random) | O(n²/w) | SIMD-accelerated rowsum, w = 64 |
53//! | Measurement (deterministic) | O(n²/w) | scratch-row accumulation via rowsum |
54//! | Memory | O(n²/w) | ~50 MB for n = 10 000 |
55//!
56//! Multi-shot sampling uses Rayon for parallel execution; each shot gets an
57//! independent clone of the state.
58//!
59//! # Memory Layout
60//!
61//! The tableau uses a single flat `Box<[u64]>` for cache efficiency:
62//! - `2n + 1` rows total: rows `0..n` = destabilizers, rows `n..2n` = stabilizers,
63//! row `2n` = pre-allocated scratch for deterministic measurement.
64//! - Each row: `row_len` u64 words for the X-block, then `row_len` words for the Z-block:
65//! `[x₀..xₙ | z₀..zₙ]` packed into `row_len` words each.
66//! - `row_len` is padded to a multiple of 8 (512 bits) for AVX-512 alignment.
67//! - Phases stored separately in `phases: Box<[u8]>` as `0` (= +1), `1` (= +i), `2` (= −1), `3` (= −i).
68//!
69//! # Bit Addressing
70//!
71//! Qubit `q` maps to word `q / 64` and bit `q % 64` within both the X and Z blocks
72//! of every row.
73//!
74//! # Examples
75//!
76//! **Bell state preparation and measurement:**
77//! ```rust
78//! use cqlib_core::qis::StabilizerState;
79//!
80//! let mut s = StabilizerState::new(2);
81//! s.apply_h(0).unwrap();
82//! s.apply_cx(0, 1).unwrap(); // |Φ⁺⟩ = (|00⟩ + |11⟩)/√2
83//!
84//! // Outcomes are always correlated
85//! let shots = s.sample_shots(500);
86//! assert!(shots.iter().all(|v| v.is_one(0) == v.is_one(1)));
87//! ```
88//!
89//! **GHZ state — 1000 qubits:**
90//! ```rust
91//! use cqlib_core::qis::StabilizerState;
92//!
93//! let n = 1000;
94//! let mut s = StabilizerState::new(n);
95//! s.apply_h(0).unwrap();
96//! for q in 0..n - 1 {
97//! s.apply_cx(q, q + 1).unwrap();
98//! }
99//! // All qubits measure identically
100//! let mut copy = s.clone();
101//! let result = copy.measure_all();
102//! let first = result.is_one(0);
103//! assert!((0..n).all(|q| result.is_one(q) == first));
104//! ```
105//!
106//! **Deterministic measurement on |0⟩:**
107//! ```rust
108//! use cqlib_core::qis::StabilizerState;
109//!
110//! let mut s = StabilizerState::new(3);
111//! // |000⟩ state — all measurements deterministically 0
112//! assert_eq!(s.measure(0).unwrap(), false);
113//! assert_eq!(s.measure(1).unwrap(), false);
114//! assert_eq!(s.measure(2).unwrap(), false);
115//! ```
116//!
117//! **From a Clifford circuit:**
118//! ```rust
119//! use cqlib_core::circuit::{Circuit, Qubit};
120//! use cqlib_core::qis::StabilizerState;
121//!
122//! let mut c = Circuit::new(2);
123//! c.h(Qubit::new(0)).unwrap();
124//! c.cx(Qubit::new(0), Qubit::new(1)).unwrap();
125//! let stab = StabilizerState::from_circuit(&c).unwrap();
126//! let stabilizers = stab.get_stabilizers();
127//! // Bell state is stabilized by +XX and +ZZ
128//! assert_eq!(stabilizers.len(), 2);
129//! ```
130//!
131//! **Circuit measurement results:**
132//! ```rust
133//! use cqlib_core::circuit::{Circuit, ClassicalType, Qubit};
134//! use cqlib_core::qis::{RuntimeValue, StabilizerState};
135//!
136//! let mut c = Circuit::new(3);
137//! c.x(Qubit::new(0)).unwrap();
138//! c.x(Qubit::new(2)).unwrap();
139//!
140//! // The returned measurement carries both the IR value and measured qubit order.
141//! let bit = c.measure(Qubit::new(0)).unwrap();
142//!
143//! // `measure_bits_into` also writes a mutable variable copy for later classical use.
144//! let latest = c.var(ClassicalType::bit_vec(3).unwrap());
145//! let bits = c
146//! .measure_bits_into([Qubit::new(0), Qubit::new(1), Qubit::new(2)], latest)
147//! .unwrap();
148//!
149//! let execution = StabilizerState::run_circuit(&c).unwrap();
150//! assert_eq!(execution.classical.value(bit.value()), Some(&RuntimeValue::Bit(true)));
151//! assert_eq!(
152//! execution.classical.value(bits.value()).and_then(RuntimeValue::to_bitstring).as_deref(),
153//! Some("101")
154//! );
155//! assert_eq!(execution.classical.var(latest), execution.classical.value(bits.value()));
156//! ```
157//!
158//! **Non-Clifford gate returns an error:**
159//! ```rust
160//! use cqlib_core::circuit::{Circuit, Qubit};
161//! use cqlib_core::qis::{StabilizerState, QisError};
162//!
163//! let mut c = Circuit::new(1);
164//! c.t(Qubit::new(0)).unwrap(); // T gate is not Clifford
165//! let result = StabilizerState::from_circuit(&c);
166//! assert!(matches!(result, Err(QisError::NonCliffordGate(_))));
167//! ```
168
169use super::aligned_buffer::AlignedBuffer;
170use crate::circuit::circuit_impl::Circuit;
171use crate::circuit::circuit_param::CircuitParam;
172use crate::circuit::error::CircuitError;
173use crate::circuit::gate::directive::Directive;
174use crate::circuit::gate::{ClassicalDataOp, Instruction, StandardGate};
175use crate::circuit::operation::Operation;
176use crate::circuit::{Measurement, Qubit};
177use crate::device::{ExecutionResult, Outcome};
178use crate::qis::error::QisError;
179use crate::qis::pauli::{Pauli, PauliString, Phase};
180use crate::qis::state::{ClassicalState, RuntimeValue};
181use rand::rngs::SmallRng;
182use rand::{Rng, SeedableRng};
183use rayon::prelude::*;
184use smallvec::SmallVec;
185use std::collections::HashMap;
186
187/// Result returned by [`StabilizerState::run_circuit`].
188///
189/// Contains both the final quantum state and runtime classical data produced
190/// while executing measurement and store operations.
191#[derive(Debug)]
192pub struct CircuitExecutionResult {
193 /// Final stabilizer state after all circuit operations.
194 pub state: StabilizerState,
195 /// Runtime classical values and variables indexed by circuit-local handles.
196 pub classical: ClassicalState,
197}
198
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200enum CircuitClassicalMode {
201 Ignore,
202 Execute,
203}
204
205/// Stabilizer state simulator based on the Aaronson-Gottesman symplectic tableau.
206///
207/// Represents an n-qubit stabilizer state using 2n generator rows, each composed
208/// of an X-block and Z-block of packed `u64` words, plus a phase per row.
209#[derive(Debug)]
210pub struct StabilizerState {
211 /// Circuit qubit identifiers mapped to storage positions.
212 qubit_map: super::QubitMap,
213 /// Number of qubits.
214 pub num_qubits: usize,
215 /// Number of `u64` words per block (X-block or Z-block) per row.
216 /// Padded to a multiple of 8 for 512-bit SIMD alignment.
217 row_len: usize,
218 /// Flat tableau storage: `(2n+1)` rows × `2 * row_len` words, 64-byte aligned.
219 /// Row `i` occupies `tableau[i * 2 * row_len .. (i+1) * 2 * row_len]`.
220 /// Within a row: first `row_len` words = X-block, next `row_len` words = Z-block.
221 /// Row `2n` is the pre-allocated scratch row for deterministic measurement.
222 tableau: AlignedBuffer<u64>,
223 /// Phase for each of the `2n+1` rows: `0`=+1, `1`=+i, `2`=−1, `3`=−i.
224 phases: Box<[u8]>,
225 /// RNG for random measurement outcomes.
226 rng: SmallRng,
227}
228
229// `xor_rows_dispatch` XORs `count` u64 words from `src_base` into `dst_base`
230// within `tableau`, dispatching to the best available SIMD tier at runtime.
231// `count` is always a multiple of 16 (= 2 × row_len, row_len ≥ 8), so no tail
232// handling is required for any SIMD width up to 512 bits.
233//
234// SAFETY contract shared by all variants:
235// • `dst_base + count ≤ tableau.len()` and `src_base + count ≤ tableau.len()`
236// • `[dst_base, dst_base+count)` and `[src_base, src_base+count)` do not overlap
237// (guaranteed by the caller via distinct row indices h ≠ i).
238
239/// AVX2 path: 256-bit XOR, processing 4 u64 words per iteration.
240///
241/// Requires 32-byte alignment. Our `AlignedBuffer` provides 64-byte alignment,
242/// so aligned loads (`_mm256_load_si256`) are safe for every row start.
243#[cfg(target_arch = "x86_64")]
244#[target_feature(enable = "avx2")]
245unsafe fn xor_rows_avx2(tableau: &mut [u64], dst_base: usize, src_base: usize, count: usize) {
246 use std::arch::x86_64::{__m256i, _mm256_load_si256, _mm256_store_si256, _mm256_xor_si256};
247 // SAFETY: avx2 feature enabled; AlignedBuffer guarantees 64-byte alignment
248 // (≥ required 32-byte AVX2 alignment); caller guarantees bounds + non-overlap.
249 unsafe {
250 let ptr = tableau.as_mut_ptr();
251 let dst = ptr.add(dst_base) as *mut __m256i;
252 let src = ptr.add(src_base) as *const __m256i;
253 let chunks = count / 4; // 4 u64 per __m256i (256 bits)
254 for i in 0..chunks {
255 let a = _mm256_load_si256(dst.add(i));
256 let b = _mm256_load_si256(src.add(i));
257 _mm256_store_si256(dst.add(i), _mm256_xor_si256(a, b));
258 }
259 }
260}
261
262/// SSE2 path: 128-bit XOR, processing 2 u64 words per iteration.
263///
264/// Requires 16-byte alignment. Our `AlignedBuffer` provides 64-byte alignment.
265#[cfg(target_arch = "x86_64")]
266#[target_feature(enable = "sse2")]
267unsafe fn xor_rows_sse2(tableau: &mut [u64], dst_base: usize, src_base: usize, count: usize) {
268 use std::arch::x86_64::{__m128i, _mm_load_si128, _mm_store_si128, _mm_xor_si128};
269 // SAFETY: sse2 feature enabled; AlignedBuffer guarantees 64-byte alignment
270 // (≥ required 16-byte SSE2 alignment); caller guarantees bounds + non-overlap.
271 unsafe {
272 let ptr = tableau.as_mut_ptr();
273 let dst = ptr.add(dst_base) as *mut __m128i;
274 let src = ptr.add(src_base) as *const __m128i;
275 let chunks = count / 2; // 2 u64 per __m128i (128 bits)
276 for i in 0..chunks {
277 let a = _mm_load_si128(dst.add(i));
278 let b = _mm_load_si128(src.add(i));
279 _mm_store_si128(dst.add(i), _mm_xor_si128(a, b));
280 }
281 }
282}
283
284/// NEON path: 128-bit XOR, processing 2 u64 words per iteration.
285/// NEON is mandatory on aarch64; no runtime feature detection required.
286#[cfg(target_arch = "aarch64")]
287#[target_feature(enable = "neon")]
288unsafe fn xor_rows_neon(tableau: &mut [u64], dst_base: usize, src_base: usize, count: usize) {
289 use std::arch::aarch64::{veorq_u64, vld1q_u64, vst1q_u64};
290 // SAFETY: NEON is mandatory on aarch64; caller guarantees bounds and non-overlap.
291 unsafe {
292 let ptr = tableau.as_mut_ptr();
293 let dst = ptr.add(dst_base);
294 let src = ptr.add(src_base) as *const u64;
295 let chunks = count / 2; // 2 u64 per uint64x2_t (128 bits)
296 for i in 0..chunks {
297 let a = vld1q_u64(dst.add(i * 2) as *const u64);
298 let b = vld1q_u64(src.add(i * 2));
299 vst1q_u64(dst.add(i * 2), veorq_u64(a, b));
300 }
301 }
302}
303
304/// Scalar fallback: word loop that the compiler auto-vectorizes at -O2.
305/// Used on unsupported architectures or when SIMD features are unavailable.
306#[inline]
307fn xor_rows_scalar(tableau: &mut [u64], dst_base: usize, src_base: usize, count: usize) {
308 // SAFETY: use raw pointer to read src while dst is mutably borrowed from the
309 // same slice. Non-overlap is guaranteed by the caller.
310 let ptr = tableau.as_mut_ptr();
311 for w in 0..count {
312 // SAFETY: dst_base+w < tableau.len() and src_base+w < tableau.len().
313 unsafe { *ptr.add(dst_base + w) ^= *ptr.add(src_base + w) };
314 }
315}
316
317/// Runtime-dispatched XOR of `count` u64 words from `src_base` into `dst_base`.
318///
319/// # Safety
320/// * `dst_base + count ≤ tableau.len()` and `src_base + count ≤ tableau.len()`
321/// * `[dst_base, dst_base+count)` and `[src_base, src_base+count)` must not overlap.
322#[inline]
323#[allow(unreachable_code)] // scalar tail unreachable on aarch64 (always takes NEON path)
324unsafe fn xor_rows_dispatch(tableau: &mut [u64], dst_base: usize, src_base: usize, count: usize) {
325 #[cfg(target_arch = "x86_64")]
326 {
327 if is_x86_feature_detected!("avx2") {
328 // SAFETY: avx2 confirmed; bounds + non-overlap guaranteed by caller.
329 return unsafe { xor_rows_avx2(tableau, dst_base, src_base, count) };
330 }
331 if is_x86_feature_detected!("sse2") {
332 // SAFETY: sse2 confirmed; bounds + non-overlap guaranteed by caller.
333 return unsafe { xor_rows_sse2(tableau, dst_base, src_base, count) };
334 }
335 }
336 #[cfg(target_arch = "aarch64")]
337 {
338 // SAFETY: NEON mandatory on aarch64; bounds + non-overlap guaranteed by caller.
339 return unsafe { xor_rows_neon(tableau, dst_base, src_base, count) };
340 }
341 xor_rows_scalar(tableau, dst_base, src_base, count);
342}
343
344impl StabilizerState {
345 /// Creates a new stabilizer state initialized to |0...0⟩.
346 ///
347 /// In the Aaronson-Gottesman convention the initial generators are:
348 /// - Destabilizer `i` (row `i`): X on qubit `i`, identity elsewhere, phase +1.
349 /// - Stabilizer `i` (row `n+i`): Z on qubit `i`, identity elsewhere, phase +1.
350 ///
351 /// # Arguments
352 /// * `n` — number of qubits
353 pub fn new(n: usize) -> Self {
354 assert!(n > 0, "StabilizerState requires at least 1 qubit");
355
356 // Pad row_len to the next multiple of 8 u64 words (= 512 bits).
357 let words_needed = n.div_ceil(64);
358 let row_len = ((words_needed + 7) & !7).max(8);
359
360 // Allocate 2n+1 rows: rows 0..n = destabilizers, n..2n = stabilizers,
361 // row 2n = persistent scratch (used by deterministic measurement).
362 let total_rows = 2 * n + 1;
363 let total_words = total_rows * 2 * row_len;
364 let mut tableau = AlignedBuffer::new_zeroed(total_words);
365 let phases = vec![0u8; total_rows].into_boxed_slice();
366
367 // Initialise destabilizers: row i has X bit set at column i.
368 for i in 0..n {
369 let word = i / 64;
370 let bit = i % 64;
371 tableau[Self::x_offset(i, i, row_len, word)] |= 1u64 << bit;
372 }
373
374 // Initialise stabilizers: row n+i has Z bit set at column i.
375 for i in 0..n {
376 let word = i / 64;
377 let bit = i % 64;
378 tableau[Self::z_offset(n + i, i, row_len, word)] |= 1u64 << bit;
379 }
380
381 StabilizerState {
382 num_qubits: n,
383 qubit_map: None,
384 row_len,
385 tableau,
386 phases,
387 rng: SmallRng::from_os_rng(),
388 }
389 }
390
391 /// Copies tableau and phase data from `source` into `self` without allocating.
392 ///
393 /// The two states must have the same number of qubits (and therefore the same
394 /// `row_len`). The RNG is **not** copied — the caller is expected to manage it
395 /// separately (e.g., re-seeding for each shot).
396 ///
397 /// This is used by [`sample_shots`](Self::sample_shots) to reuse a single
398 /// pre-allocated working buffer per Rayon worker thread.
399 fn reset_from(&mut self, source: &StabilizerState) {
400 debug_assert_eq!(self.num_qubits, source.num_qubits);
401 self.qubit_map.clone_from(&source.qubit_map);
402 debug_assert_eq!(self.row_len, source.row_len);
403 self.tableau
404 .as_mut_slice()
405 .copy_from_slice(source.tableau.as_slice());
406 self.phases.copy_from_slice(&source.phases);
407 }
408
409 /// Index of the persistent scratch row in the tableau (= `2n`).
410 #[inline(always)]
411 fn scratch_row(&self) -> usize {
412 2 * self.num_qubits
413 }
414
415 /// Zeroes the scratch row's bit-columns and phase, ready for a new accumulation.
416 fn clear_scratch(&mut self) {
417 let s = self.scratch_row();
418 let base = Self::row_base(s, self.row_len);
419 for w in 0..2 * self.row_len {
420 self.tableau[base + w] = 0;
421 }
422 self.phases[s] = 0;
423 }
424
425 /// Base byte offset (in u64 words) of row `row` in the tableau.
426 #[inline(always)]
427 fn row_base(row: usize, row_len: usize) -> usize {
428 row * 2 * row_len
429 }
430
431 /// Flat index into `tableau` for the `word`-th u64 in the X-block of `row`,
432 /// where `col_qubit` determines which X-block column we're accessing.
433 /// (The `word` parameter is `col_qubit / 64`.)
434 #[inline(always)]
435 fn x_offset(row: usize, _col_qubit: usize, row_len: usize, word: usize) -> usize {
436 Self::row_base(row, row_len) + word
437 }
438
439 /// Flat index into `tableau` for the `word`-th u64 in the Z-block of `row`.
440 #[inline(always)]
441 fn z_offset(row: usize, _col_qubit: usize, row_len: usize, word: usize) -> usize {
442 Self::row_base(row, row_len) + row_len + word
443 }
444
445 /// Returns the X-bit of `row` at qubit column `q`.
446 #[inline(always)]
447 pub(crate) fn x_bit(&self, row: usize, q: usize) -> bool {
448 let base = Self::row_base(row, self.row_len);
449 let word = q / 64;
450 let bit = q % 64;
451 (self.tableau[base + word] >> bit) & 1 == 1
452 }
453
454 /// Returns the Z-bit of `row` at qubit column `q`.
455 #[inline(always)]
456 pub(crate) fn z_bit(&self, row: usize, q: usize) -> bool {
457 let base = Self::row_base(row, self.row_len);
458 let word = q / 64;
459 let bit = q % 64;
460 (self.tableau[base + self.row_len + word] >> bit) & 1 == 1
461 }
462
463 /// Sets or clears the X-bit of `row` at qubit column `q`.
464 #[allow(dead_code)]
465 #[inline(always)]
466 pub(crate) fn set_x_bit(&mut self, row: usize, q: usize, val: bool) {
467 let base = Self::row_base(row, self.row_len);
468 let word = q / 64;
469 let bit = q % 64;
470 if val {
471 self.tableau[base + word] |= 1u64 << bit;
472 } else {
473 self.tableau[base + word] &= !(1u64 << bit);
474 }
475 }
476
477 /// Sets or clears the Z-bit of `row` at qubit column `q`.
478 #[inline(always)]
479 pub(crate) fn set_z_bit(&mut self, row: usize, q: usize, val: bool) {
480 let base = Self::row_base(row, self.row_len);
481 let word = q / 64;
482 let bit = q % 64;
483 if val {
484 self.tableau[base + self.row_len + word] |= 1u64 << bit;
485 } else {
486 self.tableau[base + self.row_len + word] &= !(1u64 << bit);
487 }
488 }
489
490 /// Returns the encoded phase of row `row`.
491 #[inline(always)]
492 pub(crate) fn phase(&self, row: usize) -> u8 {
493 self.phases[row]
494 }
495
496 /// Sets the phase for `row` to `val` (0–3 for +1, +i, −1, −i respectively).
497 #[inline(always)]
498 pub(crate) fn set_phase(&mut self, row: usize, val: u8) {
499 self.phases[row] = val & 3;
500 }
501
502 pub(crate) fn validate_qubit(&self, q: usize) -> Result<(), QisError> {
503 if self.num_qubits == 0 {
504 return Err(QisError::InvalidParameterValue(
505 "no valid qubit indices in a zero-qubit system".to_string(),
506 ));
507 }
508 if q >= self.num_qubits {
509 return Err(QisError::IndexOutOfBounds {
510 index: q,
511 max: self.num_qubits.saturating_sub(1),
512 });
513 }
514 Ok(())
515 }
516
517 pub(crate) fn validate_two_qubits(&self, q0: usize, q1: usize) -> Result<(), QisError> {
518 self.validate_qubit(q0)?;
519 self.validate_qubit(q1)?;
520 if q0 == q1 {
521 return Err(QisError::InvalidParameterValue(
522 "Two-qubit gate requires distinct qubit indices".to_string(),
523 ));
524 }
525 Ok(())
526 }
527
528 /// Applies the CNOT (CX) gate with `control` as control and `target` as target.
529 ///
530 /// Aaronson-Gottesman rule (Eq. 11) for each row `i`:
531 /// - `phase[i] ^= 2 * x[i][c] * z[i][t] * (x[i][t] ^ z[i][c] ^ 1)`
532 /// - `x[i][t] ^= x[i][c]`
533 /// - `z[i][c] ^= z[i][t]`
534 pub fn apply_cx(&mut self, control: usize, target: usize) -> Result<(), QisError> {
535 self.validate_two_qubits(control, target)?;
536 let c_word = control / 64;
537 let c_mask = 1u64 << (control % 64);
538 let t_word = target / 64;
539 let t_mask = 1u64 << (target % 64);
540 let rl = self.row_len;
541 for row in 0..(2 * self.num_qubits) {
542 let base = row * 2 * rl;
543 let xc = (self.tableau[base + c_word] & c_mask) != 0;
544 let xt = (self.tableau[base + t_word] & t_mask) != 0;
545 let zc = (self.tableau[base + rl + c_word] & c_mask) != 0;
546 let zt = (self.tableau[base + rl + t_word] & t_mask) != 0;
547 if xc && zt && (xt ^ zc ^ true) {
548 self.phases[row] ^= 2;
549 }
550 if xc {
551 self.tableau[base + t_word] ^= t_mask;
552 }
553 if zt {
554 self.tableau[base + rl + c_word] ^= c_mask;
555 }
556 }
557 Ok(())
558 }
559
560 /// Applies the SWAP gate between qubits `q0` and `q1`.
561 ///
562 /// Native rule: for each row `i`, swap (x[i][q0], z[i][q0]) with (x[i][q1], z[i][q1]).
563 /// No phase change — SWAP maps Paulis without sign.
564 pub fn apply_swap(&mut self, q0: usize, q1: usize) -> Result<(), QisError> {
565 self.validate_two_qubits(q0, q1)?;
566 let w0 = q0 / 64;
567 let m0 = 1u64 << (q0 % 64);
568 let w1 = q1 / 64;
569 let m1 = 1u64 << (q1 % 64);
570 let rl = self.row_len;
571 for row in 0..(2 * self.num_qubits) {
572 let base = row * 2 * rl;
573 // Read bits
574 let x0 = (self.tableau[base + w0] & m0) != 0;
575 let z0 = (self.tableau[base + rl + w0] & m0) != 0;
576 let x1 = (self.tableau[base + w1] & m1) != 0;
577 let z1 = (self.tableau[base + rl + w1] & m1) != 0;
578 // Write swapped bits
579 if x1 {
580 self.tableau[base + w0] |= m0;
581 } else {
582 self.tableau[base + w0] &= !m0;
583 }
584 if z1 {
585 self.tableau[base + rl + w0] |= m0;
586 } else {
587 self.tableau[base + rl + w0] &= !m0;
588 }
589 if x0 {
590 self.tableau[base + w1] |= m1;
591 } else {
592 self.tableau[base + w1] &= !m1;
593 }
594 if z0 {
595 self.tableau[base + rl + w1] |= m1;
596 } else {
597 self.tableau[base + rl + w1] &= !m1;
598 }
599 }
600 Ok(())
601 }
602
603 /// Applies the CZ gate (controlled-Z) between `q0` and `q1`.
604 ///
605 /// Implemented as (I⊗H)·CNOT·(I⊗H) inlined per-row — no heap allocations.
606 pub fn apply_cz(&mut self, q0: usize, q1: usize) -> Result<(), QisError> {
607 self.validate_two_qubits(q0, q1)?;
608 let w0 = q0 / 64;
609 let m0 = 1u64 << (q0 % 64);
610 let w1 = q1 / 64;
611 let m1 = 1u64 << (q1 % 64);
612 let rl = self.row_len;
613 for row in 0..(2 * self.num_qubits) {
614 let base = row * 2 * rl;
615 // H on q1: swap x1 ↔ z1, phase if both set
616 let x1 = (self.tableau[base + w1] & m1) != 0;
617 let z1 = (self.tableau[base + rl + w1] & m1) != 0;
618 if x1 && z1 {
619 self.phases[row] ^= 2;
620 }
621 if z1 {
622 self.tableau[base + w1] |= m1;
623 } else {
624 self.tableau[base + w1] &= !m1;
625 }
626 if x1 {
627 self.tableau[base + rl + w1] |= m1;
628 } else {
629 self.tableau[base + rl + w1] &= !m1;
630 }
631
632 // CNOT(q0, q1)
633 let xc = (self.tableau[base + w0] & m0) != 0;
634 let xt = (self.tableau[base + w1] & m1) != 0;
635 let zc = (self.tableau[base + rl + w0] & m0) != 0;
636 let zt = (self.tableau[base + rl + w1] & m1) != 0;
637 if xc && zt && (xt ^ zc ^ true) {
638 self.phases[row] ^= 2;
639 }
640 if xc {
641 self.tableau[base + w1] ^= m1;
642 }
643 if zt {
644 self.tableau[base + rl + w0] ^= m0;
645 }
646
647 // H on q1 again
648 let x1 = (self.tableau[base + w1] & m1) != 0;
649 let z1 = (self.tableau[base + rl + w1] & m1) != 0;
650 if x1 && z1 {
651 self.phases[row] ^= 2;
652 }
653 if z1 {
654 self.tableau[base + w1] |= m1;
655 } else {
656 self.tableau[base + w1] &= !m1;
657 }
658 if x1 {
659 self.tableau[base + rl + w1] |= m1;
660 } else {
661 self.tableau[base + rl + w1] &= !m1;
662 }
663 }
664 Ok(())
665 }
666
667 /// Applies the CY gate (controlled-Y) between `control` and `target`.
668 ///
669 /// Implemented as (I⊗S)·CX·(I⊗S†) inlined per-row, i.e. in time order:
670 /// S†(target) → CX(control, target) → S(target).
671 ///
672 /// Derivation: Y = SXS†, so CY = (I⊗S)·CX·(I⊗S†).
673 pub fn apply_cy(&mut self, control: usize, target: usize) -> Result<(), QisError> {
674 self.validate_two_qubits(control, target)?;
675 let cw = control / 64;
676 let cm = 1u64 << (control % 64);
677 let tw = target / 64;
678 let tm = 1u64 << (target % 64);
679 let rl = self.row_len;
680 for row in 0..(2 * self.num_qubits) {
681 let base = row * 2 * rl;
682 // S† on target: phase flip when x=1 and z=0 (i.e. X → -Y), then z ^= x
683 let x = (self.tableau[base + tw] & tm) != 0;
684 let z = (self.tableau[base + rl + tw] & tm) != 0;
685 if x && !z {
686 self.phases[row] ^= 2;
687 }
688 if x {
689 self.tableau[base + rl + tw] ^= tm;
690 }
691
692 // CNOT(control, target)
693 let xc = (self.tableau[base + cw] & cm) != 0;
694 let xt = (self.tableau[base + tw] & tm) != 0;
695 let zc = (self.tableau[base + rl + cw] & cm) != 0;
696 let zt = (self.tableau[base + rl + tw] & tm) != 0;
697 if xc && zt && (xt ^ zc ^ true) {
698 self.phases[row] ^= 2;
699 }
700 if xc {
701 self.tableau[base + tw] ^= tm;
702 }
703 if zt {
704 self.tableau[base + rl + cw] ^= cm;
705 }
706
707 // S on target: phase flip when x=1 and z=1 (i.e. Y → -X), then z ^= x
708 let x = (self.tableau[base + tw] & tm) != 0;
709 let z = (self.tableau[base + rl + tw] & tm) != 0;
710 if x && z {
711 self.phases[row] ^= 2;
712 }
713 if x {
714 self.tableau[base + rl + tw] ^= tm;
715 }
716 }
717 Ok(())
718 }
719
720 /// Returns the number of qubits.
721 pub fn num_qubits(&self) -> usize {
722 self.num_qubits
723 }
724
725 /// Applies the Hadamard gate to qubit `qubit`.
726 ///
727 /// Aaronson-Gottesman update rule for each row `i` (0..2n):
728 /// - `phase[i] ^= 2 * x[i][qubit] & z[i][qubit]` (adds 2 mod 4 when Y, i.e. sign flip)
729 /// - swap `x[i][qubit]` and `z[i][qubit]`
730 pub fn apply_h(&mut self, qubit: usize) -> Result<(), QisError> {
731 self.validate_qubit(qubit)?;
732 let word = qubit / 64;
733 let mask = 1u64 << (qubit % 64);
734 let rl = self.row_len;
735 for row in 0..(2 * self.num_qubits) {
736 let base = row * 2 * rl;
737 let x = (self.tableau[base + word] & mask) != 0;
738 let z = (self.tableau[base + rl + word] & mask) != 0;
739 if x && z {
740 self.phases[row] ^= 2;
741 }
742 // Swap x and z bits.
743 if x {
744 self.tableau[base + rl + word] |= mask;
745 } else {
746 self.tableau[base + rl + word] &= !mask;
747 }
748 if z {
749 self.tableau[base + word] |= mask;
750 } else {
751 self.tableau[base + word] &= !mask;
752 }
753 }
754 Ok(())
755 }
756
757 /// Applies the S gate (phase gate, √Z) to qubit `qubit`.
758 ///
759 /// Aaronson-Gottesman rule for each row `i`:
760 /// - `phase[i] ^= 2 * x[i][qubit] * z[i][qubit]`
761 /// - `z[i][qubit] ^= x[i][qubit]`
762 ///
763 /// Effect: X→Y, Y→-X, Z→Z.
764 pub fn apply_s(&mut self, qubit: usize) -> Result<(), QisError> {
765 self.validate_qubit(qubit)?;
766 let word = qubit / 64;
767 let mask = 1u64 << (qubit % 64);
768 let rl = self.row_len;
769 for row in 0..(2 * self.num_qubits) {
770 let base = row * 2 * rl;
771 let x = (self.tableau[base + word] & mask) != 0;
772 let z = (self.tableau[base + rl + word] & mask) != 0;
773 if x && z {
774 self.phases[row] ^= 2;
775 }
776 if x {
777 // z ^= x (x=1): flip z bit
778 self.tableau[base + rl + word] ^= mask;
779 }
780 }
781 Ok(())
782 }
783
784 /// Applies the S† gate (S-dagger, inverse phase gate) to qubit `qubit`.
785 ///
786 /// Native rule for each row `i` (derived from S†: X→-Y, Y→X, Z→Z):
787 /// - `phase[i] ^= 2 * x[i][qubit] * (1 - z[i][qubit])` (flip sign when X but not yet Y)
788 /// - `z[i][qubit] ^= x[i][qubit]`
789 pub fn apply_sdg(&mut self, qubit: usize) -> Result<(), QisError> {
790 self.validate_qubit(qubit)?;
791 let word = qubit / 64;
792 let mask = 1u64 << (qubit % 64);
793 let rl = self.row_len;
794 for row in 0..(2 * self.num_qubits) {
795 let base = row * 2 * rl;
796 let x = (self.tableau[base + word] & mask) != 0;
797 let z = (self.tableau[base + rl + word] & mask) != 0;
798 if x && !z {
799 self.phases[row] ^= 2;
800 }
801 if x {
802 self.tableau[base + rl + word] ^= mask;
803 }
804 }
805 Ok(())
806 }
807
808 /// Applies the Pauli-X (bit-flip) gate to qubit `qubit`.
809 ///
810 /// Effect: X→X, Y→-Y, Z→-Z.
811 /// Rule per row `i`: `phase[i] ^= 2 * z[i][qubit]`. No bit changes.
812 pub fn apply_x(&mut self, qubit: usize) -> Result<(), QisError> {
813 self.validate_qubit(qubit)?;
814 let word = qubit / 64;
815 let mask = 1u64 << (qubit % 64);
816 let rl = self.row_len;
817 for row in 0..(2 * self.num_qubits) {
818 let base = row * 2 * rl;
819 if (self.tableau[base + rl + word] & mask) != 0 {
820 self.phases[row] ^= 2;
821 }
822 }
823 Ok(())
824 }
825
826 /// Applies the Pauli-Z (phase-flip) gate to qubit `qubit`.
827 ///
828 /// Effect: X→-X, Y→-Y, Z→Z.
829 /// Rule per row `i`: `phase[i] ^= 2 * x[i][qubit]`. No bit changes.
830 pub fn apply_z(&mut self, qubit: usize) -> Result<(), QisError> {
831 self.validate_qubit(qubit)?;
832 let word = qubit / 64;
833 let mask = 1u64 << (qubit % 64);
834 let rl = self.row_len;
835 for row in 0..(2 * self.num_qubits) {
836 let base = row * 2 * rl;
837 if (self.tableau[base + word] & mask) != 0 {
838 self.phases[row] ^= 2;
839 }
840 }
841 Ok(())
842 }
843
844 /// Applies the Pauli-Y gate to qubit `qubit`.
845 ///
846 /// Effect: X→-X, Y→Y, Z→-Z.
847 /// Rule per row `i`: `phase[i] ^= 2 * (x[i][qubit] XOR z[i][qubit])`. No bit changes.
848 pub fn apply_y(&mut self, qubit: usize) -> Result<(), QisError> {
849 self.validate_qubit(qubit)?;
850 let word = qubit / 64;
851 let mask = 1u64 << (qubit % 64);
852 let rl = self.row_len;
853 for row in 0..(2 * self.num_qubits) {
854 let base = row * 2 * rl;
855 let x = (self.tableau[base + word] & mask) != 0;
856 let z = (self.tableau[base + rl + word] & mask) != 0;
857 if x ^ z {
858 self.phases[row] ^= 2;
859 }
860 }
861 Ok(())
862 }
863
864 /// Applies the √X gate (X2P, SX, Rx(π/2)) to qubit `qubit`.
865 ///
866 /// Conjugation: X→X, Y→Z, Z→−Y.
867 /// Rule per row `i`:
868 /// - `phase[i] ^= 2 * (!x[i][qubit] & z[i][qubit])` (flip for Z→−Y)
869 /// - `x[i][qubit] ^= z[i][qubit]`
870 pub fn apply_x2p(&mut self, qubit: usize) -> Result<(), QisError> {
871 self.validate_qubit(qubit)?;
872 let word = qubit / 64;
873 let mask = 1u64 << (qubit % 64);
874 let rl = self.row_len;
875 for row in 0..(2 * self.num_qubits) {
876 let base = row * 2 * rl;
877 let x = (self.tableau[base + word] & mask) != 0;
878 let z = (self.tableau[base + rl + word] & mask) != 0;
879 if !x && z {
880 self.phases[row] ^= 2;
881 }
882 // x ^= z
883 if z {
884 self.tableau[base + word] ^= mask;
885 }
886 }
887 Ok(())
888 }
889
890 /// Applies the √X† gate (X2M, SXdg, Rx(−π/2)) to qubit `qubit`.
891 ///
892 /// Conjugation: X→X, Y→−Z, Z→Y.
893 /// Rule per row `i`:
894 /// - `phase[i] ^= 2 * (x[i][qubit] & z[i][qubit])` (flip for Y→−Z)
895 /// - `x[i][qubit] ^= z[i][qubit]`
896 pub fn apply_x2m(&mut self, qubit: usize) -> Result<(), QisError> {
897 self.validate_qubit(qubit)?;
898 let word = qubit / 64;
899 let mask = 1u64 << (qubit % 64);
900 let rl = self.row_len;
901 for row in 0..(2 * self.num_qubits) {
902 let base = row * 2 * rl;
903 let x = (self.tableau[base + word] & mask) != 0;
904 let z = (self.tableau[base + rl + word] & mask) != 0;
905 if x && z {
906 self.phases[row] ^= 2;
907 }
908 if z {
909 self.tableau[base + word] ^= mask;
910 }
911 }
912 Ok(())
913 }
914
915 /// Applies the √Y gate (Y2P, SY, Ry(π/2)) to qubit `qubit`.
916 ///
917 /// Conjugation U P U†: X→−Z, Y→Y, Z→X.
918 /// Rule per row `i`:
919 /// - `phase[i] ^= 2 * (x[i][qubit] & !z[i][qubit])` (flip for X→−Z)
920 /// - swap `x[i][qubit]` ↔ `z[i][qubit]`
921 pub fn apply_y2p(&mut self, qubit: usize) -> Result<(), QisError> {
922 self.validate_qubit(qubit)?;
923 let word = qubit / 64;
924 let mask = 1u64 << (qubit % 64);
925 let rl = self.row_len;
926 for row in 0..(2 * self.num_qubits) {
927 let base = row * 2 * rl;
928 let x = (self.tableau[base + word] & mask) != 0;
929 let z = (self.tableau[base + rl + word] & mask) != 0;
930 if x && !z {
931 self.phases[row] ^= 2;
932 }
933 // swap x ↔ z
934 if x {
935 self.tableau[base + rl + word] |= mask;
936 } else {
937 self.tableau[base + rl + word] &= !mask;
938 }
939 if z {
940 self.tableau[base + word] |= mask;
941 } else {
942 self.tableau[base + word] &= !mask;
943 }
944 }
945 Ok(())
946 }
947
948 /// Applies the √Y† gate (Y2M, SYdg, Ry(−π/2)) to qubit `qubit`.
949 ///
950 /// Conjugation U P U†: X→Z, Y→Y, Z→−X.
951 /// Rule per row `i`:
952 /// - `phase[i] ^= 2 * (!x[i][qubit] & z[i][qubit])` (flip for Z→−X)
953 /// - swap `x[i][qubit]` ↔ `z[i][qubit]`
954 pub fn apply_y2m(&mut self, qubit: usize) -> Result<(), QisError> {
955 self.validate_qubit(qubit)?;
956 let word = qubit / 64;
957 let mask = 1u64 << (qubit % 64);
958 let rl = self.row_len;
959 for row in 0..(2 * self.num_qubits) {
960 let base = row * 2 * rl;
961 let x = (self.tableau[base + word] & mask) != 0;
962 let z = (self.tableau[base + rl + word] & mask) != 0;
963 if !x && z {
964 self.phases[row] ^= 2;
965 }
966 // swap x ↔ z
967 if x {
968 self.tableau[base + rl + word] |= mask;
969 } else {
970 self.tableau[base + rl + word] &= !mask;
971 }
972 if z {
973 self.tableau[base + word] |= mask;
974 } else {
975 self.tableau[base + word] &= !mask;
976 }
977 }
978 Ok(())
979 }
980
981 /// Phase contribution from multiplying Pauli `P_h` (from h-row) by `P_i` (from i-row).
982 ///
983 /// Returns an integer exponent `e` such that `P_h * P_i = i^e * P_result`.
984 /// Kept for reference/testing; the hot path uses `g_phase_word` instead.
985 #[allow(dead_code)]
986 #[inline(always)]
987 pub(crate) fn g_phase(x1: bool, z1: bool, x2: bool, z2: bool) -> i32 {
988 match (x1, z1) {
989 (false, false) => 0,
990 (true, true) => z2 as i32 - x2 as i32,
991 (true, false) => z2 as i32 * (2 * x2 as i32 - 1),
992 (false, true) => x2 as i32 * (1 - 2 * z2 as i32),
993 }
994 }
995
996 /// Accumulates the g_phase contribution of a single 64-bit word pair using
997 /// word-level popcount — **2 `count_ones()` calls** per 64-bit word.
998 ///
999 /// Algebraic derivation:
1000 /// - `anti_comm = (xh & zi) XOR (xi & zh)` — positions where the two Paulis
1001 /// anti-commute (anti-commuting ↔ g ≠ 0).
1002 /// - `pos = (xh & zi & (zh XOR xi)) | (!xh & zh & xi & !zi)` — positions
1003 /// where g = +1 (the "forward cyclic" Pauli pairs X→Y, Y→Z, Z→X).
1004 /// - `sum = 2 · popcount(pos) − popcount(anti_comm)`
1005 /// (since popcount(neg) = popcount(anti_comm) − popcount(pos),
1006 /// and popcount(pos) − popcount(neg) = 2·popcount(pos) − popcount(anti_comm)).
1007 ///
1008 /// Verified against the scalar `g_phase` for all 9 single-qubit Pauli pairs.
1009 #[inline(always)]
1010 pub(crate) fn g_phase_word(xh: u64, zh: u64, xi: u64, zi: u64) -> i32 {
1011 // Positions where the Paulis anti-commute (g ≠ 0).
1012 let anti_comm = (xh & zi) ^ (xi & zh);
1013 // Positions where g = +1 (cyclic order X→Y, Y→Z, Z→X).
1014 let pos = (xh & zi & (zh ^ xi)) | (!xh & zh & xi & !zi);
1015 2 * pos.count_ones() as i32 - anti_comm.count_ones() as i32
1016 }
1017
1018 /// Multiplies row `h` by row `i` in-place: `row_h := row_h * row_i`.
1019 ///
1020 /// Phase is computed via `g_phase_word` (word-level popcount, O(n/64) vs O(n)).
1021 /// Bit XOR is dispatched to the best available SIMD instruction set at runtime.
1022 fn rowsum(&mut self, h: usize, i: usize) {
1023 debug_assert_ne!(h, i, "rowsum requires distinct rows");
1024
1025 let h_base = Self::row_base(h, self.row_len);
1026 let i_base = Self::row_base(i, self.row_len);
1027 let rl = self.row_len;
1028
1029 let mut sum = self.phases[h] as i32 + self.phases[i] as i32;
1030 // Only the active words (0..n_words) contain qubit data; the padding words
1031 // in the range n_words..row_len are always zero and contribute 0 to the sum.
1032 let n_words = self.num_qubits.div_ceil(64);
1033 for w in 0..n_words {
1034 let xh = self.tableau[h_base + w];
1035 let zh = self.tableau[h_base + rl + w];
1036 let xi = self.tableau[i_base + w];
1037 let zi = self.tableau[i_base + rl + w];
1038 sum += Self::g_phase_word(xh, zh, xi, zi);
1039 }
1040 // New phase is sum mod 4, normalised to {0,1,2,3}.
1041 self.phases[h] = (((sum % 4) + 4) % 4) as u8;
1042
1043 // SAFETY: h ≠ i guarantees [h_base, h_base+2*rl) and [i_base, i_base+2*rl)
1044 // are non-overlapping regions within the same 64-byte aligned allocation.
1045 unsafe {
1046 xor_rows_dispatch(self.tableau.as_mut_slice(), h_base, i_base, 2 * rl);
1047 }
1048 }
1049
1050 /// Measures qubit `qubit` in the Z basis, collapsing the state.
1051 ///
1052 /// Returns `false` for outcome `0` (+1 eigenvalue) and `true` for outcome `1` (-1 eigenvalue).
1053 ///
1054 /// # Algorithm (Aaronson-Gottesman §IV)
1055 ///
1056 /// **Random case** — some stabilizer generator has X or Y on qubit `qubit`:
1057 /// 1. Find any stabilizer row `p` (index `n..2n`) with `x[p][qubit] = 1`.
1058 /// 2. XOR row `p` into every other row that also has `x[.][qubit] = 1`.
1059 /// 3. Move row `p` to its paired destabilizer row `p - n`.
1060 /// 4. Replace stabilizer row `p` with `±Z_qubit`, where the sign is random.
1061 ///
1062 /// **Deterministic case** — all stabilizers commute with `Z_qubit`:
1063 /// 1. Compute the product of destabilizer rows `i` (for which the paired
1064 /// stabilizer `n+i` has `x[n+i][qubit] = 1`) into a scratch accumulator.
1065 /// 2. The measurement outcome is the phase of the scratch row.
1066 pub fn measure(&mut self, qubit: usize) -> Result<bool, QisError> {
1067 self.validate_qubit(qubit)?;
1068
1069 // Find first stabilizer row with x[.][qubit] = 1.
1070 let maybe_p = (self.num_qubits..2 * self.num_qubits).find(|&row| self.x_bit(row, qubit));
1071
1072 match maybe_p {
1073 Some(p) => {
1074 // Make p the unique row with x[.][qubit] = 1 by XOR-ing p into all others.
1075 for i in 0..2 * self.num_qubits {
1076 if i != p && self.x_bit(i, qubit) {
1077 self.rowsum(i, p);
1078 }
1079 }
1080
1081 // Copy stabilizer p → destabilizer p-n.
1082 let dest = p - self.num_qubits;
1083 let p_base = Self::row_base(p, self.row_len);
1084 let d_base = Self::row_base(dest, self.row_len);
1085 // SAFETY: dest ≠ p (since dest < n ≤ p < 2n).
1086 unsafe {
1087 let src = self.tableau.as_slice()[p_base..p_base + 2 * self.row_len].as_ptr();
1088 let dst =
1089 self.tableau.as_mut_slice()[d_base..d_base + 2 * self.row_len].as_mut_ptr();
1090 std::ptr::copy_nonoverlapping(src, dst, 2 * self.row_len);
1091 }
1092 self.phases[dest] = self.phases[p];
1093
1094 // Reset stabilizer p to Z_qubit with random phase.
1095 let p_base = Self::row_base(p, self.row_len);
1096 for w in 0..2 * self.row_len {
1097 self.tableau[p_base + w] = 0;
1098 }
1099 self.set_z_bit(p, qubit, true);
1100 let b: bool = self.rng.random();
1101 self.set_phase(p, if b { 2 } else { 0 });
1102 Ok(b)
1103 }
1104 None => {
1105 // Accumulate the product of paired stabilizer rows into the
1106 // pre-allocated scratch row (row 2n) using the SIMD-accelerated
1107 // rowsum, giving O(n²/w) complexity instead of O(n²) scalar work.
1108 let scratch = self.scratch_row();
1109 self.clear_scratch();
1110
1111 for i in 0..self.num_qubits {
1112 // Include destabilizer i only if it has an X-component at qubit `qubit`.
1113 // Then rowsum the paired stabilizer n+i into scratch.
1114 if self.x_bit(i, qubit) {
1115 self.rowsum(scratch, self.num_qubits + i);
1116 }
1117 }
1118
1119 // Phase 0 = +1 eigenvalue (outcome 0); phase 2 = -1 eigenvalue (outcome 1).
1120 Ok(self.phases[scratch] == 2)
1121 }
1122 }
1123 }
1124
1125 /// Measures all qubits in order, returning a bit-packed [`Outcome`].
1126 ///
1127 /// Qubit `q`'s result is stored at bit `q` inside the `Outcome`.
1128 /// Use [`Outcome::is_one(q)`](crate::device::Outcome::is_one) to read it.
1129 pub fn measure_all(&mut self) -> Outcome {
1130 let num_chunks = self.num_qubits.div_ceil(64);
1131 let mut chunks = SmallVec::from_elem(0u64, num_chunks);
1132 for qubit in 0..self.num_qubits {
1133 if self.measure(qubit).unwrap() {
1134 chunks[qubit / 64] |= 1u64 << (qubit % 64);
1135 }
1136 }
1137 Outcome::new(chunks)
1138 }
1139
1140 /// Resets `qubit` to the |0⟩ state.
1141 ///
1142 /// Implemented by measuring in the Z basis and applying X if the outcome is |1⟩.
1143 /// This correctly handles both deterministic and random measurement cases.
1144 ///
1145 /// # Example
1146 ///
1147 /// ```rust
1148 /// use cqlib_core::qis::StabilizerState;
1149 ///
1150 /// let mut s = StabilizerState::new(2);
1151 /// s.apply_x(0).unwrap(); // |0⟩ → |1⟩
1152 /// s.reset(0).unwrap(); // back to |0⟩
1153 /// assert_eq!(s.probabilities().unwrap()[0], 1.0); // P(|00⟩) = 1
1154 /// ```
1155 pub fn reset(&mut self, qubit: usize) -> Result<(), QisError> {
1156 self.validate_qubit(qubit)?;
1157 if self.measure(qubit)? {
1158 self.apply_x(qubit)?;
1159 }
1160 Ok(())
1161 }
1162
1163 /// Performs a forced (post-selected) Z-basis measurement on `qubit`.
1164 ///
1165 /// Like `measure`, but the outcome is forced to `outcome` rather than random.
1166 /// Returns `true` if the forced outcome has non-zero probability (measurement is
1167 /// consistent with the state), `false` if probability is zero (outcome is impossible).
1168 ///
1169 /// Used internally by `probability_of`.
1170 fn force_measure(&mut self, qubit: usize, outcome: bool) -> bool {
1171 // Find first stabilizer row with x[.][qubit] = 1.
1172 let maybe_p = (self.num_qubits..2 * self.num_qubits).find(|&row| self.x_bit(row, qubit));
1173
1174 match maybe_p {
1175 Some(p) => {
1176 // Random measurement: force to `outcome`.
1177 for i in 0..2 * self.num_qubits {
1178 if i != p && self.x_bit(i, qubit) {
1179 self.rowsum(i, p);
1180 }
1181 }
1182 let dest = p - self.num_qubits;
1183 let p_base = Self::row_base(p, self.row_len);
1184 let d_base = Self::row_base(dest, self.row_len);
1185 // SAFETY: dest < n ≤ p.
1186 unsafe {
1187 let src = self.tableau.as_slice()[p_base..p_base + 2 * self.row_len].as_ptr();
1188 let dst =
1189 self.tableau.as_mut_slice()[d_base..d_base + 2 * self.row_len].as_mut_ptr();
1190 std::ptr::copy_nonoverlapping(src, dst, 2 * self.row_len);
1191 }
1192 self.phases[dest] = self.phases[p];
1193 let p_base = Self::row_base(p, self.row_len);
1194 for w in 0..2 * self.row_len {
1195 self.tableau[p_base + w] = 0;
1196 }
1197 self.set_z_bit(p, qubit, true);
1198 self.set_phase(p, if outcome { 2 } else { 0 });
1199 true // random case: forced outcome is always possible
1200 }
1201 None => {
1202 // Deterministic case: check whether outcome matches.
1203 let scratch = self.scratch_row();
1204 self.clear_scratch();
1205 for i in 0..self.num_qubits {
1206 if self.x_bit(i, qubit) {
1207 self.rowsum(scratch, self.num_qubits + i);
1208 }
1209 }
1210 let actual = self.phases[scratch] == 2;
1211 actual == outcome // false → this outcome is impossible
1212 }
1213 }
1214 }
1215
1216 /// Returns the probability of measuring the given computational basis state.
1217 ///
1218 /// Performs O(n²) work per qubit, O(n³) total, on a cloned (non-destructive) copy.
1219 ///
1220 /// # Returns
1221 /// - `Ok(0.0)` if the bitstring is inconsistent with the stabilizer state.
1222 /// - `Ok(1.0 / 2.0^k)` where `k` is the number of qubits with random outcomes,
1223 /// if the bitstring is in the support.
1224 /// - `Err(QubitMismatch)` if `bits.len() != n`.
1225 ///
1226 /// # Example
1227 /// ```rust
1228 /// use cqlib_core::qis::StabilizerState;
1229 ///
1230 /// let mut s = StabilizerState::new(2);
1231 /// s.apply_h(0).unwrap();
1232 /// s.apply_cx(0, 1).unwrap(); // Bell state |Φ⁺⟩
1233 ///
1234 /// // Only |00⟩ and |11⟩ are possible, each with probability 0.5
1235 /// assert!((s.probability_of(&[false, false]).unwrap() - 0.5).abs() < 1e-10);
1236 /// assert!((s.probability_of(&[true, true ]).unwrap() - 0.5).abs() < 1e-10);
1237 /// assert_eq!(s.probability_of(&[false, true]).unwrap(), 0.0);
1238 /// assert_eq!(s.probability_of(&[true, false]).unwrap(), 0.0);
1239 /// ```
1240 pub fn probability_of(&self, bits: &[bool]) -> Result<f64, QisError> {
1241 if bits.len() != self.num_qubits {
1242 return Err(QisError::QubitMismatch {
1243 expected: self.num_qubits,
1244 actual: bits.len(),
1245 });
1246 }
1247 let mut s = self.clone();
1248 let mut prob = 1.0_f64;
1249 for (qubit, &desired) in bits.iter().enumerate() {
1250 // Check if measurement is random (some stabilizer has X on this qubit).
1251 let is_random = (s.num_qubits..2 * s.num_qubits).any(|row| s.x_bit(row, qubit));
1252 if is_random {
1253 prob *= 0.5;
1254 s.force_measure(qubit, desired);
1255 } else {
1256 // Deterministic: force_measure returns false if outcome is impossible.
1257 if !s.force_measure(qubit, desired) {
1258 return Ok(0.0);
1259 }
1260 }
1261 }
1262 Ok(prob)
1263 }
1264
1265 /// Returns the full probability distribution over all computational basis states.
1266 ///
1267 /// Returns a `Vec<f64>` of length `2^n` where entry `i` is the probability of
1268 /// measuring the state whose binary representation is `i` (qubit 0 = LSB).
1269 ///
1270 /// # Limitations
1271 /// Only feasible for small systems. Returns `Err(InvalidParameterValue)` if `n > 20`
1272 /// (which would require a 8 MB vector and O(2^20 × n³) computation).
1273 ///
1274 /// # Example
1275 /// ```rust
1276 /// use cqlib_core::qis::StabilizerState;
1277 ///
1278 /// let mut s = StabilizerState::new(2);
1279 /// s.apply_h(0).unwrap();
1280 /// s.apply_cx(0, 1).unwrap(); // Bell state
1281 ///
1282 /// let probs = s.probabilities().unwrap();
1283 /// assert_eq!(probs.len(), 4);
1284 /// assert!((probs[0b00] - 0.5).abs() < 1e-10); // |00⟩
1285 /// assert_eq!(probs[0b01], 0.0); // |10⟩ (qubit-0 is LSB)
1286 /// assert_eq!(probs[0b10], 0.0); // |01⟩
1287 /// assert!((probs[0b11] - 0.5).abs() < 1e-10); // |11⟩
1288 /// ```
1289 pub fn probabilities(&self) -> Result<Vec<f64>, QisError> {
1290 const MAX_QUBITS: usize = 20;
1291 if self.num_qubits > MAX_QUBITS {
1292 return Err(QisError::InvalidParameterValue(format!(
1293 "probabilities() is only supported for n ≤ {MAX_QUBITS} qubits \
1294 (requested n = {}); use sample_shots() for large systems",
1295 self.num_qubits
1296 )));
1297 }
1298 let size = 1usize << self.num_qubits;
1299 let mut probs = vec![0.0_f64; size];
1300 for (i, prob) in probs.iter_mut().enumerate() {
1301 let bits: Vec<bool> = (0..self.num_qubits).map(|q| (i >> q) & 1 == 1).collect();
1302 *prob = self.probability_of(&bits)?;
1303 }
1304 Ok(probs)
1305 }
1306
1307 /// Runs `shots` independent measurements in parallel using Rayon.
1308 ///
1309 /// Returns a [`Vec<Outcome>`] of bit-packed measurement results, one per shot.
1310 /// Each Rayon worker thread reuses a single pre-allocated working copy of the
1311 /// state (via [`reset_from`](Self::reset_from)), avoiding per-shot heap allocation.
1312 ///
1313 /// Seeds for each shot are derived sequentially from the primary RNG (ensuring
1314 /// reproducibility), then each shot runs on an independently-seeded clone in
1315 /// parallel. This produces the correct marginal distribution while being
1316 /// deterministic: the same initial state always yields the same joint sample set.
1317 ///
1318 /// # Example
1319 /// ```rust
1320 /// use cqlib_core::qis::StabilizerState;
1321 /// let mut s = StabilizerState::new(2);
1322 /// s.apply_h(0).unwrap();
1323 /// s.apply_cx(0, 1).unwrap(); // Bell state
1324 /// let results = s.sample_shots(1000);
1325 /// assert_eq!(results.len(), 1000);
1326 /// for shot in &results { assert_eq!(shot.is_one(0), shot.is_one(1)); }
1327 /// ```
1328 pub fn sample_shots(&self, shots: usize) -> Vec<Outcome> {
1329 self.sample_shots_with_seed(shots, None)
1330 }
1331
1332 /// Samples using per-shot random streams independent of Rayon scheduling.
1333 /// With no seed, preserves the state's existing random stream.
1334 pub fn sample_shots_with_seed(&self, shots: usize, seed: Option<u64>) -> Vec<Outcome> {
1335 // Derive independent seeds sequentially so the overall sequence is
1336 // deterministic and reproducible from the same initial RNG state.
1337 let seeds: Vec<u64> = {
1338 let mut rng = seed
1339 .map(SmallRng::seed_from_u64)
1340 .unwrap_or_else(|| self.rng.clone());
1341 (0..shots).map(|_| rng.random()).collect()
1342 };
1343 seeds
1344 .into_par_iter()
1345 .map_with(self.clone(), |work, seed| {
1346 work.reset_from(self);
1347 work.rng = SmallRng::seed_from_u64(seed);
1348 work.measure_all()
1349 })
1350 .collect()
1351 }
1352
1353 /// Seeds the internal RNG for direct circuit sampling.
1354 pub(crate) fn seed_rng(&mut self, seed: u64) {
1355 self.rng = SmallRng::seed_from_u64(seed);
1356 }
1357
1358 /// Samples the state using a circuit [`Measurement`] as the output contract.
1359 ///
1360 /// Unlike [`apply_circuit`](Self::apply_circuit), this method does not read
1361 /// or execute the circuit IR. The [`Measurement`] already carries the qubits
1362 /// and their result bit order:
1363 /// - `measurement.qubits()[i]` becomes bit `i` in each [`Outcome`].
1364 /// - [`Outcome::to_bitstring`] displays the most-significant result bit first,
1365 /// so string order is the reverse of `measurement.qubits()`.
1366 ///
1367 /// # Example
1368 /// ```rust
1369 /// use cqlib_core::circuit::{Circuit, Qubit};
1370 /// use cqlib_core::qis::StabilizerState;
1371 ///
1372 /// let mut c = Circuit::new(2);
1373 /// c.h(Qubit::new(0)).unwrap();
1374 /// c.cx(Qubit::new(0), Qubit::new(1)).unwrap();
1375 /// let out = c.measure_bits([Qubit::new(1), Qubit::new(0)]).unwrap();
1376 ///
1377 /// // `from_circuit` ignores terminal measurements and keeps the Bell state.
1378 /// let state = StabilizerState::from_circuit(&c).unwrap();
1379 /// let result = state.sample(&out, 1000).unwrap();
1380 ///
1381 /// assert_eq!(result.shots(), 1000);
1382 /// assert!(result
1383 /// .counts()
1384 /// .keys()
1385 /// .all(|bits| bits.to_bitstring(out.width()) == "00"
1386 /// || bits.to_bitstring(out.width()) == "11"));
1387 /// ```
1388 pub fn sample(
1389 &self,
1390 measurement: &Measurement,
1391 shots: usize,
1392 ) -> Result<ExecutionResult, QisError> {
1393 self.sample_with_seed(measurement, shots, None)
1394 }
1395
1396 /// Projects seeded full-state samples onto the requested measurement.
1397 pub fn sample_with_seed(
1398 &self,
1399 measurement: &Measurement,
1400 shots: usize,
1401 seed: Option<u64>,
1402 ) -> Result<ExecutionResult, QisError> {
1403 let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?;
1404
1405 let mut counts = HashMap::new();
1406 for full in self.sample_shots_with_seed(shots, seed) {
1407 *counts.entry(projection.project(&full)).or_insert(0usize) += 1;
1408 }
1409
1410 Ok(ExecutionResult::from_counts(
1411 "stabilizer-sample".to_string(),
1412 measurement.qubits().to_vec(),
1413 shots,
1414 measurement.width(),
1415 Some("stabilizer".to_string()),
1416 counts,
1417 ))
1418 }
1419
1420 /// Returns the probability distribution selected by a circuit [`Measurement`].
1421 ///
1422 /// This is a marginal distribution over `measurement.qubits()`, not the full
1423 /// `2^n` state distribution. The returned [`Outcome`] keys use the same bit
1424 /// order as [`sample`](Self::sample).
1425 ///
1426 /// v1 intentionally reuses [`probabilities`](Self::probabilities), so it has
1427 /// the same `n <= 20` limit. Use [`sample`](Self::sample) for larger states.
1428 pub fn probs(&self, measurement: &Measurement) -> Result<HashMap<Outcome, f64>, QisError> {
1429 let projection = super::resolve_measurement(measurement, &self.qubit_map, self.num_qubits)?;
1430
1431 let mut marginal = HashMap::new();
1432 for (basis, prob) in self.probabilities()?.into_iter().enumerate() {
1433 if prob == 0.0 {
1434 continue;
1435 }
1436 *marginal
1437 .entry(projection.project_basis(basis))
1438 .or_insert(0.0) += prob;
1439 }
1440 Ok(marginal)
1441 }
1442
1443 /// Constructs a `StabilizerState` by simulating a Clifford circuit.
1444 ///
1445 /// This state-level entry point only performs quantum state evolution.
1446 /// Terminal measurement and store operations produced by `Circuit::measure*` are
1447 /// treated as output declarations and ignored here: they do not collapse
1448 /// the state and do not populate runtime classical data.
1449 ///
1450 /// Use [`sample`](Self::sample) or [`probs`](Self::probs) with the returned
1451 /// [`Measurement`] to query measurement distributions from the final state.
1452 /// Use [`run_circuit`](Self::run_circuit) when you need execution
1453 /// semantics where measurements collapse the state and write classical data.
1454 ///
1455 /// # Supported instructions
1456 /// - Gates: `I, H, X, Y, Z, S, SDG, X2P, X2M, Y2P, Y2M, CX, CY, CZ, SWAP`
1457 /// - Classical data: `MeasureBit`, `MeasureBits`, `Store` are ignored
1458 /// - Directives: `Reset` (returns qubit to |0⟩), `Barrier`/`Delay` (no-op)
1459 ///
1460 /// [`run_circuit`]: StabilizerState::run_circuit
1461 ///
1462 /// # Example
1463 /// ```rust
1464 /// use cqlib_core::circuit::{Circuit, Qubit};
1465 /// use cqlib_core::qis::StabilizerState;
1466 ///
1467 /// let mut c = Circuit::new(2);
1468 /// c.h(Qubit::new(0)).unwrap();
1469 /// c.cx(Qubit::new(0), Qubit::new(1)).unwrap();
1470 /// let out = c
1471 /// .measure_bits([Qubit::new(1), Qubit::new(0)])
1472 /// .unwrap();
1473 ///
1474 /// // The measurement above is an output declaration for state sampling.
1475 /// // It does not collapse the Bell state during `from_circuit`.
1476 /// let stab = StabilizerState::from_circuit(&c).unwrap();
1477 /// let probs = stab.probs(&out).unwrap();
1478 /// assert!((probs.values().sum::<f64>() - 1.0).abs() < 1e-10);
1479 /// ```
1480 pub fn from_circuit(circuit: &Circuit) -> Result<Self, QisError> {
1481 let mut state = StabilizerState::new(circuit.num_qubits());
1482 state.apply_circuit(circuit)?;
1483 Ok(state)
1484 }
1485
1486 /// Applies a Clifford circuit to this stabilizer state in-place.
1487 ///
1488 /// Once bound to circuit qubit IDs, subsequent circuits must have the same
1489 /// IDs in the same order. The mapping is committed only on success; gates
1490 /// completed before an error are not rolled back.
1491 ///
1492 /// As in `from_circuit`, terminal measurements are ignored. A measured qubit
1493 /// used by a later gate or Reset is rejected before the state is changed.
1494 /// Independent gates, repeated measurements, Barrier, Delay, and Store are allowed.
1495 ///
1496 /// This state-level entry point ignores terminal measurement declarations,
1497 /// matching [`from_circuit`](Self::from_circuit). Use
1498 /// [`run_circuit`](Self::run_circuit) when runtime classical measurement
1499 /// values are required.
1500 pub fn apply_circuit(&mut self, input_circuit: &Circuit) -> Result<(), QisError> {
1501 if input_circuit.num_qubits() != self.num_qubits {
1502 return Err(QisError::InvalidStateDimension(input_circuit.num_qubits()));
1503 }
1504 if input_circuit
1505 .operations()
1506 .iter()
1507 .any(|op| matches!(op.instruction, Instruction::ClassicalControl(_)))
1508 {
1509 return Err(QisError::UnsupportedOperation(
1510 "classical control flow is not supported in stabilizer simulation".to_string(),
1511 ));
1512 }
1513
1514 let circuit = input_circuit.decompose()?;
1515 super::validate_terminal_measurements(&circuit)?;
1516 let qubit_map = super::prepare_qubit_map(&circuit.qubits(), &self.qubit_map)?;
1517
1518 let parameter_values: Vec<Option<f64>> = circuit
1519 .parameters()
1520 .iter()
1521 .map(|p| p.evaluate(&None).ok())
1522 .collect();
1523
1524 let mut classical = ClassicalState::for_circuit(&circuit);
1525 StabilizerState::execute_operations(
1526 self,
1527 circuit.operations(),
1528 &qubit_map,
1529 ¶meter_values,
1530 &mut classical,
1531 CircuitClassicalMode::Ignore,
1532 )?;
1533 self.qubit_map = Some(qubit_map);
1534 Ok(())
1535 }
1536
1537 /// Executes a Clifford circuit and returns both the final state and runtime
1538 /// classical data.
1539 ///
1540 /// Returns a [`CircuitExecutionResult`] containing:
1541 /// - `state`: the final [`StabilizerState`] after all operations
1542 /// - `classical`: measurement results and mutable variables indexed by
1543 /// [`crate::circuit::ClassicalValue`] and [`crate::circuit::ClassicalVar`]
1544 ///
1545 /// # Supported instructions
1546 /// Same quantum gates as [`from_circuit`]. Unlike `from_circuit`, this
1547 /// method executes `MeasureBit`, `MeasureBits`, and `Store`: measurements
1548 /// collapse the state immediately and write runtime classical data.
1549 /// Control-flow gates are not supported.
1550 ///
1551 /// [`from_circuit`]: StabilizerState::from_circuit
1552 ///
1553 /// # Example
1554 /// ```rust
1555 /// use cqlib_core::circuit::{Circuit, ClassicalExpr, ClassicalType, Qubit};
1556 /// use cqlib_core::qis::{RuntimeValue, StabilizerState};
1557 ///
1558 /// let mut c = Circuit::new(1);
1559 /// c.x(Qubit::new(0)).unwrap();
1560 ///
1561 /// // `measure` creates an immutable runtime value.
1562 /// let measured = c.measure(Qubit::new(0)).unwrap();
1563 ///
1564 /// // `store` can copy or transform measured values into mutable variables.
1565 /// let flag = c.var(ClassicalType::Bool);
1566 /// c.store(flag, ClassicalExpr::bit_to_bool(measured.expr()).unwrap()).unwrap();
1567 ///
1568 /// let result = StabilizerState::run_circuit(&c).unwrap();
1569 /// assert_eq!(result.classical.value(measured.value()), Some(&RuntimeValue::Bit(true)));
1570 /// assert_eq!(result.classical.var(flag), Some(&RuntimeValue::Bool(true)));
1571 /// ```
1572 pub fn run_circuit(circuit: &Circuit) -> Result<CircuitExecutionResult, QisError> {
1573 StabilizerState::run_circuit_with_mode(circuit, CircuitClassicalMode::Execute)
1574 }
1575
1576 fn run_circuit_with_mode(
1577 input_circuit: &Circuit,
1578 classical_mode: CircuitClassicalMode,
1579 ) -> Result<CircuitExecutionResult, QisError> {
1580 if input_circuit
1581 .operations()
1582 .iter()
1583 .any(|op| matches!(op.instruction, Instruction::ClassicalControl(_)))
1584 {
1585 return Err(QisError::UnsupportedOperation(
1586 "classical control flow is not supported in stabilizer simulation".to_string(),
1587 ));
1588 }
1589
1590 let circuit = input_circuit.decompose()?;
1591 let mut state = StabilizerState::new(circuit.num_qubits());
1592
1593 let qubits = circuit.qubits();
1594 let qubit_map: std::collections::HashMap<_, _> = qubits
1595 .iter()
1596 .enumerate()
1597 .map(|(idx, q)| (*q, idx))
1598 .collect();
1599 let qubit_map = std::sync::Arc::new(qubit_map);
1600 state.qubit_map = Some(qubit_map.clone());
1601
1602 let parameter_values: Vec<Option<f64>> = circuit
1603 .parameters()
1604 .iter()
1605 .map(|p| p.evaluate(&None).ok())
1606 .collect();
1607
1608 let mut classical = ClassicalState::for_circuit(&circuit);
1609
1610 StabilizerState::execute_operations(
1611 &mut state,
1612 circuit.operations(),
1613 &qubit_map,
1614 ¶meter_values,
1615 &mut classical,
1616 classical_mode,
1617 )?;
1618 classical.rebind_to_circuit(input_circuit)?;
1619
1620 Ok(CircuitExecutionResult { state, classical })
1621 }
1622
1623 /// Recursive operation executor used by [`apply_circuit`].
1624 ///
1625 /// Processes a slice of operations, updating the quantum state and runtime
1626 /// classical state in place.
1627 ///
1628 /// [`apply_circuit`]: StabilizerState::apply_circuit
1629 fn execute_operations(
1630 state: &mut StabilizerState,
1631 ops: &[Operation],
1632 qubit_map: &std::collections::HashMap<Qubit, usize>,
1633 parameter_values: &[Option<f64>],
1634 classical: &mut ClassicalState,
1635 classical_mode: CircuitClassicalMode,
1636 ) -> Result<(), QisError> {
1637 for op in ops {
1638 let qubit_indices: Vec<usize> =
1639 op.qubits
1640 .iter()
1641 .map(|q| {
1642 qubit_map.get(q).copied().ok_or_else(|| {
1643 QisError::CircuitError(CircuitError::QubitNotFound(q.id()))
1644 })
1645 })
1646 .collect::<Result<_, _>>()?;
1647
1648 let params: Vec<f64> = op
1649 .params
1650 .iter()
1651 .map(|p| match p {
1652 CircuitParam::Fixed(v) => Ok(*v),
1653 CircuitParam::Index(idx) => parameter_values
1654 .get(*idx as usize)
1655 .copied()
1656 .flatten()
1657 .ok_or(QisError::CircuitError(CircuitError::SymbolicParameterError)),
1658 })
1659 .collect::<Result<_, _>>()?;
1660
1661 match &op.instruction {
1662 Instruction::Standard(gate) => {
1663 state.apply_clifford_gate(*gate, &qubit_indices, ¶ms)?;
1664 }
1665 Instruction::McGate(mc) if mc.num_ctrl_qubits() == 1 => match mc.base_gate() {
1666 StandardGate::X => state.apply_cx(qubit_indices[0], qubit_indices[1])?,
1667 StandardGate::Y => state.apply_cy(qubit_indices[0], qubit_indices[1])?,
1668 StandardGate::Z => state.apply_cz(qubit_indices[0], qubit_indices[1])?,
1669 g => {
1670 return Err(QisError::UnsupportedOperation(format!(
1671 "Controlled-{g} is not supported in stabilizer simulation"
1672 )));
1673 }
1674 },
1675 Instruction::McGate(mc) => {
1676 return Err(QisError::UnsupportedOperation(format!(
1677 "{}-controlled gates are not supported in stabilizer simulation",
1678 mc.num_ctrl_qubits()
1679 )));
1680 }
1681 Instruction::Directive(directive) => match directive {
1682 Directive::Barrier => {} // no-op
1683 Directive::Measure => {
1684 return Err(QisError::UnsupportedOperation(
1685 "legacy Measure directive is not supported by stabilizer circuit execution; use Circuit::measure".to_string(),
1686 ));
1687 }
1688 Directive::Reset => {
1689 let q = qubit_indices[0];
1690 state.reset(q)?;
1691 }
1692 },
1693 Instruction::Delay => {} // no-op
1694 Instruction::UnitaryGate(_) => {
1695 return Err(QisError::UnsupportedOperation(
1696 "Arbitrary unitary gates are not Clifford".to_string(),
1697 ));
1698 }
1699 Instruction::CircuitGate(_) => {
1700 return Err(QisError::CircuitError(CircuitError::InvalidOperation(
1701 "CircuitGate should have been decomposed".to_string(),
1702 )));
1703 }
1704 Instruction::ClassicalControl(_) => {
1705 return Err(QisError::UnsupportedOperation(
1706 "classical control flow is not supported in stabilizer simulation"
1707 .to_string(),
1708 ));
1709 }
1710 Instruction::ClassicalData(data) => {
1711 if classical_mode == CircuitClassicalMode::Ignore {
1712 continue;
1713 }
1714 match data {
1715 ClassicalDataOp::MeasureBit { result } => {
1716 let bit = state.measure(qubit_indices[0])?;
1717 classical.set_value(*result, RuntimeValue::Bit(bit))?;
1718 }
1719 ClassicalDataOp::MeasureBits { result } => {
1720 let bits = qubit_indices
1721 .iter()
1722 .copied()
1723 .map(|q| state.measure(q))
1724 .collect::<Result<Vec<_>, _>>()?;
1725 classical.set_value(*result, RuntimeValue::bit_vec_from_lsb(&bits))?;
1726 }
1727 ClassicalDataOp::Store { target, value } => {
1728 classical.store(*target, value)?;
1729 }
1730 }
1731 }
1732 }
1733 }
1734 Ok(())
1735 }
1736
1737 /// Applies a standard gate to the stabilizer tableau.
1738 ///
1739 /// Returns [`QisError::NonCliffordGate`] for gates outside the Clifford set.
1740 pub fn apply_standard_gate(
1741 &mut self,
1742 gate: StandardGate,
1743 qubits: &[usize],
1744 params: &[f64],
1745 ) -> Result<(), QisError> {
1746 if qubits.len() != gate.num_qubits() {
1747 return Err(QisError::InvalidParameterValue(format!(
1748 "Gate {:?} requires {} qubits, got {}",
1749 gate,
1750 gate.num_qubits(),
1751 qubits.len()
1752 )));
1753 }
1754 if params.len() != gate.num_params() {
1755 return Err(QisError::InvalidParameterValue(format!(
1756 "Gate {:?} requires {} parameters, got {}",
1757 gate,
1758 gate.num_params(),
1759 params.len()
1760 )));
1761 }
1762 self.apply_clifford_gate(gate, qubits, params)
1763 }
1764
1765 /// Dispatches a [`StandardGate`] to the appropriate Clifford tableau method.
1766 /// Returns `Err` for any non-Clifford gate.
1767 fn apply_clifford_gate(
1768 &mut self,
1769 gate: StandardGate,
1770 qubits: &[usize],
1771 _params: &[f64],
1772 ) -> Result<(), QisError> {
1773 match gate {
1774 StandardGate::I => {}
1775 StandardGate::H => self.apply_h(qubits[0])?,
1776 StandardGate::X => self.apply_x(qubits[0])?,
1777 StandardGate::Y => self.apply_y(qubits[0])?,
1778 StandardGate::Z => self.apply_z(qubits[0])?,
1779 StandardGate::S => self.apply_s(qubits[0])?,
1780 StandardGate::SDG => self.apply_sdg(qubits[0])?,
1781 StandardGate::X2P => self.apply_x2p(qubits[0])?,
1782 StandardGate::X2M => self.apply_x2m(qubits[0])?,
1783 StandardGate::Y2P => self.apply_y2p(qubits[0])?,
1784 StandardGate::Y2M => self.apply_y2m(qubits[0])?,
1785 StandardGate::CX => self.apply_cx(qubits[0], qubits[1])?,
1786 StandardGate::CY => self.apply_cy(qubits[0], qubits[1])?,
1787 StandardGate::CZ => self.apply_cz(qubits[0], qubits[1])?,
1788 StandardGate::SWAP => self.apply_swap(qubits[0], qubits[1])?,
1789 g => {
1790 return Err(QisError::NonCliffordGate(format!(
1791 "Gate '{g}' is not a Clifford gate — use Statevector for universal simulation"
1792 )));
1793 }
1794 }
1795 Ok(())
1796 }
1797
1798 /// There are `n` stabilizer generators (rows `n..2n`).
1799 pub fn get_stabilizers(&self) -> Vec<PauliString> {
1800 (0..self.num_qubits)
1801 .map(|i| self.row_to_pauli_string(self.num_qubits + i))
1802 .collect()
1803 }
1804
1805 /// Returns the destabilizer generators as [`PauliString`]s.
1806 pub fn get_destabilizers(&self) -> Vec<PauliString> {
1807 (0..self.num_qubits)
1808 .map(|i| self.row_to_pauli_string(i))
1809 .collect()
1810 }
1811
1812 /// Converts tableau row `row` to a [`PauliString`].
1813 fn row_to_pauli_string(&self, row: usize) -> PauliString {
1814 let mut ps = PauliString::new(self.num_qubits);
1815 ps.phase = Phase::from(self.phase(row));
1816 for q in 0..self.num_qubits {
1817 let x = self.x_bit(row, q);
1818 let z = self.z_bit(row, q);
1819 let pauli = match (x, z) {
1820 (false, false) => Pauli::I,
1821 (true, false) => Pauli::X,
1822 (true, true) => Pauli::Y,
1823 (false, true) => Pauli::Z,
1824 };
1825 ps.set_pauli(q, pauli);
1826 }
1827 ps
1828 }
1829
1830 /// Returns the expectation value ⟨ψ|P|ψ⟩ for a Pauli string `pauli`.
1831 ///
1832 /// Uses GF(2) Gaussian elimination over the symplectic representation of the
1833 /// stabilizer generators to determine whether `pauli` is in the stabilizer
1834 /// group (expectation +1), the negative stabilizer group (expectation -1),
1835 /// or neither (expectation 0).
1836 ///
1837 /// This correctly handles products of generators, not just individual generators.
1838 pub fn pauli_expectation(&self, pauli: &PauliString) -> Result<i32, QisError> {
1839 if pauli.num_qubits != self.num_qubits {
1840 return Err(QisError::QubitMismatch {
1841 expected: self.num_qubits,
1842 actual: pauli.num_qubits,
1843 });
1844 }
1845
1846 let n = self.num_qubits;
1847 let n_words = n.div_ceil(64);
1848 let rl = self.row_len;
1849
1850 // combo_words: one bit per generator (row), tracking which generators are
1851 // XOR'd together to match the query. n generators → n bits → n_words words.
1852 let combo_words = n_words;
1853
1854 // Build working matrix: each entry is [x-block (n_words) | z-block (n_words) | combo (combo_words)].
1855 // Row i corresponds to stabilizer generator i (tableau row n+i).
1856 let row_stride = 2 * n_words + combo_words;
1857 let mut mat: Vec<u64> = vec![0u64; n * row_stride];
1858 let mut mat_phase: Vec<i32> = vec![0i32; n];
1859
1860 for i in 0..n {
1861 let src = n + i; // stabilizer row index
1862 let src_base = Self::row_base(src, rl);
1863 let dst = &mut mat[i * row_stride..(i + 1) * row_stride];
1864 // Copy x-block
1865 dst[..n_words].copy_from_slice(&self.tableau[src_base..src_base + n_words]);
1866 // Copy z-block
1867 dst[n_words..2 * n_words]
1868 .copy_from_slice(&self.tableau[src_base + rl..src_base + rl + n_words]);
1869 // Set combo bit i → 1
1870 dst[2 * n_words + i / 64] |= 1u64 << (i % 64);
1871 // Phase as i32 in {0,1,2,3}
1872 mat_phase[i] = self.phases[src] as i32;
1873 }
1874
1875 // Build the query symplectic vector from the PauliString.
1876 let mut qx = vec![0u64; n_words];
1877 let mut qz = vec![0u64; n_words];
1878 for (q, p) in pauli.iter().enumerate() {
1879 match p {
1880 Pauli::X | Pauli::Y => qx[q / 64] |= 1u64 << (q % 64),
1881 _ => {}
1882 }
1883 match p {
1884 Pauli::Z | Pauli::Y => qz[q / 64] |= 1u64 << (q % 64),
1885 _ => {}
1886 }
1887 }
1888 let mut q_combo = vec![0u64; combo_words];
1889 let mut q_phase: i32 = pauli.phase as i32; // 0=+1,1=+i,2=-1,3=-i
1890
1891 // Reduced row echelon form (RREF) over GF(2), iterating over 2n columns:
1892 // columns 0..n → x-block (qubit q = col, word offset 0)
1893 // columns n..2n → z-block (qubit q = col-n, word offset n_words)
1894 let mut pivot_row = 0usize;
1895 for col in 0..(2 * n) {
1896 let (word_off, bit_idx) = if col < n {
1897 (0usize, col % 64)
1898 } else {
1899 (n_words, (col - n) % 64)
1900 };
1901 let word_col = if col < n { col / 64 } else { (col - n) / 64 };
1902 let word_abs = word_off + word_col;
1903 let mask = 1u64 << bit_idx;
1904
1905 // Find a pivot in rows pivot_row..n
1906 let found = (pivot_row..n).find(|&r| (mat[r * row_stride + word_abs] & mask) != 0);
1907 let p = match found {
1908 Some(p) => p,
1909 None => continue, // no pivot in this column → skip
1910 };
1911
1912 // Swap pivot row to position pivot_row
1913 if p != pivot_row {
1914 let (lo, hi) = if p < pivot_row {
1915 (p, pivot_row)
1916 } else {
1917 (pivot_row, p)
1918 };
1919 let (left, right) = mat.split_at_mut(hi * row_stride);
1920 left[lo * row_stride..(lo + 1) * row_stride]
1921 .swap_with_slice(&mut right[..row_stride]);
1922 mat_phase.swap(p, pivot_row);
1923 }
1924
1925 // Eliminate all other rows (full RREF, not just lower triangular)
1926 for r in 0..n {
1927 if r == pivot_row {
1928 continue;
1929 }
1930 if (mat[r * row_stride + word_abs] & mask) == 0 {
1931 continue;
1932 }
1933 // XOR row pivot_row into row r
1934 // phase update: same formula as rowsum but non-mutating for pivot_row
1935 let mut sum = mat_phase[r] + mat_phase[pivot_row];
1936 for w in 0..n_words {
1937 let xh = mat[r * row_stride + w];
1938 let zh = mat[r * row_stride + n_words + w];
1939 let xi = mat[pivot_row * row_stride + w];
1940 let zi = mat[pivot_row * row_stride + n_words + w];
1941 sum += Self::g_phase_word(xh, zh, xi, zi);
1942 }
1943 mat_phase[r] = ((sum % 4) + 4) % 4;
1944 for w in 0..row_stride {
1945 mat[r * row_stride + w] ^= mat[pivot_row * row_stride + w];
1946 }
1947 }
1948
1949 // Eliminate the query vector at this column
1950 let q_word = if col < n { &qx } else { &qz };
1951 if (q_word[word_col] & mask) != 0 {
1952 // XOR pivot_row into query
1953 let mut sum = q_phase + mat_phase[pivot_row];
1954 for w in 0..n_words {
1955 let xh = qx[w];
1956 let zh = qz[w];
1957 let xi = mat[pivot_row * row_stride + w];
1958 let zi = mat[pivot_row * row_stride + n_words + w];
1959 sum += Self::g_phase_word(xh, zh, xi, zi);
1960 }
1961 q_phase = ((sum % 4) + 4) % 4;
1962 for w in 0..n_words {
1963 qx[w] ^= mat[pivot_row * row_stride + w];
1964 qz[w] ^= mat[pivot_row * row_stride + n_words + w];
1965 }
1966 for w in 0..combo_words {
1967 q_combo[w] ^= mat[pivot_row * row_stride + 2 * n_words + w];
1968 }
1969 }
1970
1971 pivot_row += 1;
1972 }
1973
1974 // If any x or z bit remains in the query, the Pauli is not in the stabilizer group.
1975 let in_group = qx.iter().chain(qz.iter()).all(|&w| w == 0);
1976 if !in_group {
1977 return Ok(0);
1978 }
1979
1980 // q_phase tracks the phase of the running product:
1981 // Q_orig · G_combo (where G_combo is the generators eliminated so far)
1982 // After elimination q_phase = phase(Q_orig · G_combo) = phase(λI), so
1983 // Q_orig = λ · G_combo^{-1} and ⟨Q_orig⟩ = i^{q_phase}:
1984 // q_phase = 0 (+1) → Q_orig is the stabilizer → expectation +1
1985 // q_phase = 2 (-1) → Q_orig is −(stabilizer) → expectation −1
1986 // q_phase = 1 or 3 → imaginary, not a Hermitian stabilizer element
1987 Ok(match q_phase {
1988 0 => 1,
1989 2 => -1,
1990 _ => 0,
1991 })
1992 }
1993
1994 /// Exports the stabilizer tableau in Stim-compatible text format.
1995 pub fn to_stim_format(&self) -> String {
1996 let mut out = String::new();
1997 for i in 0..self.num_qubits {
1998 let stab = self.row_to_pauli_string(self.num_qubits + i);
1999 out.push_str(&stab.to_string());
2000 out.push('\n');
2001 }
2002 out
2003 }
2004}
2005
2006impl Clone for StabilizerState {
2007 /// O(n²) deep copy — clones the flat tableau, phases, and RNG state.
2008 ///
2009 /// The RNG is cloned (not re-seeded) so that parallel samples produced from
2010 /// independent clones diverge deterministically from the same seed, which is
2011 /// required for reproducible multi-shot sampling with Rayon.
2012 fn clone(&self) -> Self {
2013 StabilizerState {
2014 num_qubits: self.num_qubits,
2015 qubit_map: self.qubit_map.clone(),
2016 row_len: self.row_len,
2017 tableau: self.tableau.clone(),
2018 phases: self.phases.clone(),
2019 rng: self.rng.clone(),
2020 }
2021 }
2022}
2023
2024#[cfg(test)]
2025#[path = "./stabilizer_test.rs"]
2026mod stabilizer_test;