Skip to main content

ppvm_tableau/
data.rs

1// SPDX-FileCopyrightText: 2026 The PPVM Authors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::{fmt::Debug, marker::PhantomData};
5
6// Only the rayon branch-coefficient helpers coalesce into a map now; the apply
7// path and the default-feature branch path both use flat-Vec sort-merge/relabel.
8#[cfg(feature = "rayon")]
9use fxhash::FxHashMap as HashMap;
10
11use bitvec::array::BitArray;
12use bitvec::view::{BitView, BitViewSized};
13use num::PrimInt;
14
15use crate::prelude::*;
16use num::{
17    One, Zero,
18    complex::{Complex, Complex64, ComplexFloat},
19};
20use rand::SeedableRng;
21use rand::rngs::SmallRng;
22
23type PhasedPauliWordNoHash<A, H> = PhasedPauliWord<A, H, PauliWord<A, H, false>>;
24
25pub use crate::qubit_status::QubitStatus;
26
27/// A `2n`-row stabilizer / destabilizer tableau.
28///
29/// Rows `0..n` hold the destabilizers; rows `n..2n` hold the
30/// stabilizers. Each row is a [`PhasedPauliWord`] tracking both its
31/// `X`/`Z` bits and a phase in `{±1, ±i}`. Implements every
32/// Clifford-only operation natively (Hadamard, phase, CNOT, CZ, etc.).
33///
34/// # Examples
35///
36/// ```
37/// use ppvm_pauli_sum::config::fxhash::ByteF64;
38/// use ppvm_traits::traits::Clifford;
39/// use ppvm_tableau::data::Tableau;
40///
41/// let mut tab: Tableau<ByteF64<1>> = Tableau::new(2);
42/// tab.h(0);
43/// tab.cnot(0, 1);
44/// assert_eq!(tab.n_qubits, 2);
45/// assert_eq!(tab.stabilizers().len(), 2);
46/// ```
47#[derive(Clone, Debug)]
48pub struct Tableau<T: Config> {
49    /// Number of qubits.
50    pub n_qubits: usize,
51    /// Destabilizer / Stabilizer tableau
52    /// * Entries 0..n are the destabilizers
53    /// * Entries n..2n are the stabilizers
54    pub data: Vec<PhasedPauliWordNoHash<T::Storage, T::BuildHasher>>,
55    pub(crate) rng: SmallRng,
56}
57
58impl<T: Config> Tableau<T> {
59    fn new_data(n_qubits: usize) -> Vec<PhasedPauliWordNoHash<T::Storage, T::BuildHasher>> {
60        // Initialize tableau for 0 state
61        let mut data: Vec<PhasedPauliWordNoHash<T::Storage, T::BuildHasher>> =
62            Vec::with_capacity(2 * n_qubits);
63        let pw_cache = PhasedPauliWordNoHash::<T::Storage, T::BuildHasher>::new(n_qubits);
64        for i in 0..n_qubits {
65            // destabilizer
66            let mut pw = pw_cache;
67            pw.set(i, Pauli::X);
68            data.push(pw);
69        }
70        for i in 0..n_qubits {
71            // stabilizer
72            let mut pw = pw_cache;
73            pw.set(i, Pauli::Z);
74            data.push(pw);
75        }
76        data
77    }
78
79    /// Construct a fresh tableau initialised to `|0…0⟩`.
80    pub fn new(n_qubits: usize) -> Self {
81        let data = Tableau::<T>::new_data(n_qubits);
82        Self {
83            n_qubits,
84            data,
85            rng: rand::make_rng(),
86        }
87    }
88
89    /// Same as [`Tableau::new`], but seed the RNG deterministically.
90    pub fn new_with_seed(n_qubits: usize, seed: u64) -> Self {
91        let mut t = Self::new(n_qubits);
92        t.rng = SmallRng::seed_from_u64(seed);
93        t
94    }
95
96    pub fn reset_all(&mut self) {
97        let data = Tableau::<T>::new_data(self.n_qubits);
98        self.data = data;
99    }
100
101    /// View of the stabilizer rows (the upper half of the tableau).
102    #[inline]
103    pub fn stabilizers(&self) -> &[PhasedPauliWordNoHash<T::Storage, T::BuildHasher>] {
104        &self.data[self.n_qubits..]
105    }
106
107    /// Mutable view of the stabilizer rows.
108    #[inline]
109    pub fn stabilizers_mut(&mut self) -> &mut [PhasedPauliWordNoHash<T::Storage, T::BuildHasher>] {
110        &mut self.data[self.n_qubits..]
111    }
112
113    /// View of the destabilizer rows (the lower half of the tableau).
114    #[inline]
115    pub fn destabilizers(&self) -> &[PhasedPauliWordNoHash<T::Storage, T::BuildHasher>] {
116        &self.data[..self.n_qubits]
117    }
118
119    /// Mutable view of the destabilizer rows.
120    #[inline]
121    pub fn destabilizers_mut(
122        &mut self,
123    ) -> &mut [PhasedPauliWordNoHash<T::Storage, T::BuildHasher>] {
124        &mut self.data[..self.n_qubits]
125    }
126
127    // some helper functions for measurement impl
128    pub(crate) fn find_z_anticommuting_stabilizer(&self, addr0: usize) -> Option<usize> {
129        // Find first stabilizer that anticommutes with Z_addr0
130        self.stabilizers()
131            .iter()
132            .position(|stab| stab.word.anticommutes_at(addr0, (false, true)))
133    }
134
135    pub(crate) fn get_deterministic_outcome(&self, addr0: usize) -> bool
136    where
137        <<T as Config>::Storage as BitView>::Store: PrimInt,
138    {
139        // find the outcome: either Z_addr0 or -Z_addr0 is a stabilizer
140        // the stabilizer can be computed as the product of all stabilizers S_i
141        // whose corresponding destabilizer D_i anticommutes with Z_addr0 (has X at addr0).
142        // We have to actually multiply Paulis to also account for products of +i/-i
143        let destabilizers = self.destabilizers();
144        let stabilizers = self.stabilizers();
145        let n = self.n_qubits;
146        let mut result = PhasedPauliWordNoHash::<T::Storage, T::BuildHasher>::new(n);
147        for (i, destab) in destabilizers.iter().enumerate() {
148            if destab.word.xbits[addr0] {
149                result *= &stabilizers[i];
150            }
151        }
152
153        // phase >= 2 means -Z eigenvalue → outcome |1⟩ (true)
154        debug_assert!(
155            result.phase == 0 || result.phase == 2,
156            "Measurement result cannot be imaginary!"
157        );
158        result.phase >= 2
159    }
160
161    pub(crate) fn update_tableau_according_to_outcome(
162        &mut self,
163        addr0: usize,
164        q_idx: usize,
165        outcome: bool,
166    ) where
167        <<T as Config>::Storage as BitView>::Store: PrimInt,
168    {
169        let n = self.n_qubits;
170        let (destabilizers, stabilizers) = self.data.split_at_mut(n);
171
172        // Copy g_q once before the loop
173        let g_q = stabilizers[q_idx];
174
175        // Check if there are other stabilizers that anticommute with Z_addr0
176        // If so, replace with g_j = g_j * g_q
177        for i in 0..n {
178            if i == q_idx {
179                continue;
180            }
181            if stabilizers[i].word.xbits[addr0] {
182                // Stabilizer i also anticommutes, so multiply by g_q to eliminate
183                stabilizers[i] *= &g_q;
184            }
185            if destabilizers[i].word.xbits[addr0] {
186                destabilizers[i] *= &g_q;
187            }
188        }
189
190        // Update destabilizer q to be the old stabilizer q (before replacement)
191        destabilizers[q_idx] = g_q;
192
193        // Finally, replace g_q by ±Z
194        let stab_q = &mut stabilizers[q_idx];
195        stab_q.word.xbits = BitArray::ZERO;
196        stab_q.word.zbits = BitArray::ZERO;
197        stab_q.word.zbits.set(addr0, true);
198        stab_q.phase = if outcome { 2 } else { 0 };
199    }
200
201    /// Apply CZ to N pairs with constant offset: (base+i, base+offset+i) for i in 0..count.
202    /// All pairs must be in the same u64 word. This replaces N individual CZ calls
203    /// with a single word-level shift+XOR operation per row.
204    ///
205    /// # Preconditions
206    ///
207    /// The fused kernel needs `offset >= count`, i.e. pairwise-disjoint
208    /// supports (pairs `i != j` collide iff `|i - j| == offset`, which needs
209    /// `offset < count`, or `offset == 0`). Both fused updates need it: the
210    /// single `count_ones() & 1` phase reads the pre-update `z` plane for
211    /// every pair at once (`Batch.lean::czSeq_phase` proves it equal to the
212    /// sequential loop only under pairwise disjointness, and
213    /// `czSeq_phase_needs_disjoint` exhibits a counterexample), and the `z`
214    /// delta `OR`s the two shifted planes, so a `z` bit written by two pairs
215    /// is set once instead of XOR-cancelled.
216    ///
217    /// Overlapping pairs (`offset < count`, including `offset == 0`) fall
218    /// back to the per-pair `cz` loop, so this method is safe in release.
219    ///
220    /// # Panics
221    /// Debug-asserts that all bits are within the same word.
222    #[inline]
223    pub fn cz_block_pairs(&mut self, base: usize, offset: usize, count: usize)
224    where
225        <<T::Storage as BitView>::Store as TryFrom<usize>>::Error: Debug,
226        <T::Storage as BitView>::Store: PrimInt + TryFrom<usize>,
227    {
228        if count == 0 {
229            return;
230        }
231        if offset < count {
232            return self.cz_pairs_each(base, offset, count);
233        }
234        let bits_per_word = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
235        let base_bit = base % bits_per_word;
236        let word_idx = base / bits_per_word;
237
238        debug_assert_eq!(
239            (base + offset + count - 1) / bits_per_word,
240            word_idx,
241            "All CZ pairs must be in the same word"
242        );
243
244        let one = <T::Storage as BitView>::Store::one();
245        let zero = <T::Storage as BitView>::Store::zero();
246        let count_mask = if count >= bits_per_word {
247            !zero
248        } else {
249            (one << count) - one
250        };
251        let mask_c = count_mask << base_bit;
252        let mask_t = count_mask << (base_bit + offset);
253
254        self.data.iter_mut().for_each(|pw| {
255            let xp = pw.word.xbits.data.as_raw_mut_slice();
256            let zp = pw.word.zbits.data.as_raw_mut_slice();
257            let x = xp[word_idx];
258            let z = zp[word_idx];
259
260            // Phase computation (must use original z before update)
261            let xc = (x >> base_bit) & count_mask;
262            let xt = (x >> (base_bit + offset)) & count_mask;
263            let zc = (z >> base_bit) & count_mask;
264            let zt = (z >> (base_bit + offset)) & count_mask;
265            let phase_bits = xc & xt & (zc ^ zt);
266            pw.phase ^= ((phase_bits.count_ones() & 1) as u8) << 1;
267
268            // Z update: z[c] ^= x[t], z[t] ^= x[c]
269            let z_delta = ((x >> offset) & mask_c) | ((x << offset) & mask_t);
270            zp[word_idx] = z ^ z_delta;
271        });
272    }
273
274    /// Per-pair `cz` fallback for overlapping [`Self::cz_block_pairs`] runs.
275    /// Outlined so the fused kernel keeps a single call on the disjoint path.
276    #[cold]
277    #[inline(never)]
278    fn cz_pairs_each(&mut self, base: usize, offset: usize, count: usize)
279    where
280        <T::Storage as BitView>::Store: PrimInt,
281    {
282        for i in 0..count {
283            Clifford::cz(self, base + i, base + offset + i);
284        }
285    }
286
287    /// Apply CZ to N pairs with constant offset across two different words.
288    /// Controls at (word_c, base_bit_c+i) and targets at (word_t, base_bit_t+i) for i in 0..count.
289    /// word_c and word_t must be different.
290    #[inline]
291    pub fn cz_block_pairs_cross_word(
292        &mut self,
293        word_c: usize,
294        base_bit_c: usize,
295        word_t: usize,
296        base_bit_t: usize,
297        count: usize,
298    ) where
299        <T::Storage as BitView>::Store: PrimInt,
300    {
301        if count == 0 {
302            return;
303        }
304        let one = <T::Storage as BitView>::Store::one();
305        let zero = <T::Storage as BitView>::Store::zero();
306        let bits_per_word = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
307
308        debug_assert!(base_bit_c + count <= bits_per_word);
309        debug_assert!(base_bit_t + count <= bits_per_word);
310        debug_assert_ne!(word_c, word_t);
311
312        let count_mask = if count >= bits_per_word {
313            !zero
314        } else {
315            (one << count) - one
316        };
317
318        self.data.iter_mut().for_each(|pw| {
319            let xp = pw.word.xbits.data.as_raw_mut_slice();
320            let zp = pw.word.zbits.data.as_raw_mut_slice();
321
322            // Extract aligned bits (shifted to 0..count-1)
323            let xc = (xp[word_c] >> base_bit_c) & count_mask;
324            let xt = (xp[word_t] >> base_bit_t) & count_mask;
325            let zc = (zp[word_c] >> base_bit_c) & count_mask;
326            let zt = (zp[word_t] >> base_bit_t) & count_mask;
327
328            // Phase: x[c] & x[t] & (z[c] ^ z[t]) per pair
329            let phase_bits = xc & xt & (zc ^ zt);
330            pw.phase ^= ((phase_bits.count_ones() & 1) as u8) << 1;
331
332            // z[c] ^= x[t]: place target x-bits at control positions
333            zp[word_c] = zp[word_c] ^ (xt << base_bit_c);
334            // z[t] ^= x[c]: place control x-bits at target positions
335            zp[word_t] = zp[word_t] ^ (xc << base_bit_t);
336        });
337    }
338}
339
340const COMPLEX_PHASE_CONVERSION: [Complex64; 4] = [
341    Complex64 { re: 1.0, im: 0.0 },  // +1
342    Complex64 { re: 0.0, im: 1.0 },  // +i
343    Complex64 { re: -1.0, im: 0.0 }, // -1
344    Complex64 { re: 0.0, im: -1.0 }, // -i
345];
346
347/// Symplectic inner product of two tableau index values — the count of
348/// shared set bits, used in stabilizer phase calculations.
349#[inline]
350pub fn symplectic_inner<I>(alpha: I, beta: I) -> u32
351where
352    I: TableauIndex,
353{
354    (alpha & beta).count_ones()
355}
356
357/// Compute the phase contribution from destabilizer anticommutation and odd-phase masks.
358/// This is a pure function of its arguments (no self access needed), extracted to enable
359/// use in parallel contexts where borrowing self is not possible.
360#[inline]
361pub(crate) fn compute_phase_with_mask_static<I: TableauIndex>(
362    destab_anticomm_bits: I,
363    basis_index: I,
364    stab_anticomm_bits: I,
365    odd_phase_mask: I,
366) -> u8 {
367    let mut phase = (2 * symplectic_inner(destab_anticomm_bits, basis_index) as u8) % 4;
368    let active = basis_index & stab_anticomm_bits;
369    let parity = (active & odd_phase_mask).count_ones() % 2;
370    phase = (phase + 2 * parity as u8) % 4;
371    phase
372}
373
374/// Minimum number of coefficients before engaging rayon parallelism.
375/// Below this threshold, the sequential path is always used even with the `rayon` feature.
376/// Benchmarked: at 8K coefficients rayon has ~24% overhead; at 32K it's 35% faster.
377/// Set to 16384 to avoid regressions while capturing the large-coefficient wins.
378#[cfg(feature = "rayon")]
379pub(crate) const RAYON_COEFF_THRESHOLD: usize = 16384;
380
381/// Parameters describing how a Pauli branch splits each coefficient.
382///
383/// Bundles the four anticommutation/phase-mask fields produced by
384/// [`compute_decomposition`](GeneralizedTableau::compute_decomposition) with the
385/// two scaling factors applied to the branch and non-branch contributions. They
386/// always travel together, so passing them as one value keeps the coefficient
387/// accumulation helpers under clippy's argument-count limit without an
388/// `#[allow]`.
389///
390/// Only the rayon coefficient helpers consume this; the default-feature build
391/// uses the inlined sort-merge path in
392/// [`branch_with_coefficients`](GeneralizedTableau::branch_with_coefficients).
393#[cfg(feature = "rayon")]
394#[derive(Clone, Copy)]
395pub(crate) struct BranchParams<I, CoeffType> {
396    pub stab_anticomm_bits: I,
397    pub destab_anticomm_bits: I,
398    pub odd_phase_mask: I,
399    pub phase_decomp: u8,
400    pub coefficient_factor: Complex<CoeffType>,
401    pub branch_factor: Complex<CoeffType>,
402}
403
404/// Sequential accumulation of branch coefficients.
405#[cfg(feature = "rayon")]
406fn branch_coefficients_seq<I, CoeffType>(
407    items: impl IntoIterator<Item = (Complex<CoeffType>, I)>,
408    capacity: usize,
409    params: BranchParams<I, CoeffType>,
410) -> HashMap<I, Complex<CoeffType>>
411where
412    I: TableauIndex,
413    CoeffType: One + Zero + Clone + num::Num,
414    Complex<CoeffType>:
415        std::ops::Mul<Output = Complex<CoeffType>> + std::ops::AddAssign + From<Complex64> + Copy,
416{
417    let BranchParams {
418        stab_anticomm_bits,
419        destab_anticomm_bits,
420        odd_phase_mask,
421        phase_decomp,
422        coefficient_factor,
423        branch_factor,
424    } = params;
425    let mut map: HashMap<I, Complex<CoeffType>> =
426        HashMap::with_capacity_and_hasher(capacity, Default::default());
427    for (coeff, idx) in items {
428        debug_assert!(
429            !(coeff.re == CoeffType::zero() && coeff.im == CoeffType::zero()),
430            "Coefficient should not be zero"
431        );
432        let branch_index = idx ^ stab_anticomm_bits;
433        let branch_phase_contribution = compute_phase_with_mask_static(
434            destab_anticomm_bits,
435            idx,
436            stab_anticomm_bits,
437            odd_phase_mask,
438        );
439        let branch_phase = (branch_phase_contribution + phase_decomp) % 4;
440        let phase_factor: Complex<CoeffType> =
441            COMPLEX_PHASE_CONVERSION[branch_phase as usize].into();
442        let branch_coefficient = phase_factor * coeff * branch_factor;
443        let nonbranch_coefficient = coeff * coefficient_factor;
444        *map.entry(branch_index).or_insert(Complex::zero()) += branch_coefficient;
445        *map.entry(idx).or_insert(Complex::zero()) += nonbranch_coefficient;
446    }
447    map
448}
449
450/// Accumulate branch coefficients. When the coefficient count exceeds
451/// `RAYON_COEFF_THRESHOLD`, uses parallel map/collect into a Vec followed
452/// by sequential accumulation. Below the threshold, falls back to sequential.
453#[cfg(feature = "rayon")]
454fn branch_coefficients_parallel<I, CoeffType>(
455    items: &[(Complex<CoeffType>, I)],
456    params: BranchParams<I, CoeffType>,
457) -> HashMap<I, Complex<CoeffType>>
458where
459    I: TableauIndex + Send + Sync,
460    CoeffType: One + Zero + Clone + Send + Sync + num::Num,
461    Complex<CoeffType>:
462        std::ops::Mul<Output = Complex<CoeffType>> + std::ops::AddAssign + From<Complex64> + Copy,
463{
464    let BranchParams {
465        stab_anticomm_bits,
466        destab_anticomm_bits,
467        odd_phase_mask,
468        phase_decomp,
469        coefficient_factor,
470        branch_factor,
471    } = params;
472    if items.len() >= RAYON_COEFF_THRESHOLD {
473        use rayon::prelude::*;
474
475        // Parallel phase: compute all (branch_idx, branch_coeff, idx, nonbranch_coeff) tuples.
476        // This is pure math with no shared mutable state.
477        let pairs: Vec<(I, Complex<CoeffType>, I, Complex<CoeffType>)> = items
478            .par_iter()
479            .map(|&(coeff, idx)| {
480                let branch_index = idx ^ stab_anticomm_bits;
481                let branch_phase_contribution = compute_phase_with_mask_static(
482                    destab_anticomm_bits,
483                    idx,
484                    stab_anticomm_bits,
485                    odd_phase_mask,
486                );
487                let branch_phase = (branch_phase_contribution + phase_decomp) % 4;
488                let phase_factor: Complex<CoeffType> =
489                    COMPLEX_PHASE_CONVERSION[branch_phase as usize].into();
490                (
491                    branch_index,
492                    phase_factor * coeff * branch_factor,
493                    idx,
494                    coeff * coefficient_factor,
495                )
496            })
497            .collect();
498
499        // Sequential phase: accumulate into a pre-sized HashMap.
500        // HashMap inserts dominate the cost but benefit from cache locality.
501        let mut map: HashMap<I, Complex<CoeffType>> =
502            HashMap::with_capacity_and_hasher(2 * pairs.len(), Default::default());
503        for (branch_idx, branch_coeff, idx, nonbranch_coeff) in pairs {
504            *map.entry(branch_idx).or_insert(Complex::zero()) += branch_coeff;
505            *map.entry(idx).or_insert(Complex::zero()) += nonbranch_coeff;
506        }
507        return map;
508    }
509
510    branch_coefficients_seq(items.iter().copied(), 2 * items.len(), params)
511}
512
513/// Sequential relabel of apply coefficients.
514///
515/// Pauli application sends every branch to `branch_index = idx ^ stab_anticomm_bits`.
516/// XOR by a fixed constant is a bijection, so distinct input indices always map
517/// to distinct branch indices: unlike the T-gate branch split (which emits two
518/// streams that genuinely collide), the apply path produces no index collisions
519/// at all. A per-index coalesce can therefore never merge two entries — the
520/// `entry()`-keyed map was pure overhead (hash every key + table allocation) for
521/// what is a straight relabel. We instead build a flat `Vec` in one sequential,
522/// prefetch-friendly pass and let the caller apply the magnitude cutoff. (The
523/// returned keys are unique by the bijection above; the `Vec` backing relies on
524/// that, exactly as the old map did implicitly.)
525fn apply_coefficients_seq<I, CoeffType>(
526    items: impl IntoIterator<Item = (Complex<CoeffType>, I)>,
527    capacity: usize,
528    stab_anticomm_bits: I,
529    destab_anticomm_bits: I,
530    odd_phase_mask: I,
531    phase_decomp: u8,
532) -> Vec<(I, Complex<CoeffType>)>
533where
534    I: TableauIndex,
535    CoeffType: One + Zero + Clone + num::Num,
536    Complex<CoeffType>:
537        std::ops::Mul<Output = Complex<CoeffType>> + std::ops::AddAssign + From<Complex64> + Copy,
538{
539    let mut out: Vec<(I, Complex<CoeffType>)> = Vec::with_capacity(capacity);
540    for (coeff, idx) in items {
541        debug_assert!(
542            !(coeff.re == CoeffType::zero() && coeff.im == CoeffType::zero()),
543            "Coefficient should not be zero"
544        );
545        let branch_index = idx ^ stab_anticomm_bits;
546        let branch_phase_contribution = compute_phase_with_mask_static(
547            destab_anticomm_bits,
548            idx,
549            stab_anticomm_bits,
550            odd_phase_mask,
551        );
552        let branch_phase = (branch_phase_contribution + phase_decomp) % 4;
553        let phase_factor: Complex<CoeffType> =
554            COMPLEX_PHASE_CONVERSION[branch_phase as usize].into();
555        out.push((branch_index, phase_factor * coeff));
556    }
557    out
558}
559
560/// Relabel coefficients for pauli application. When the coefficient count
561/// exceeds `RAYON_COEFF_THRESHOLD`, the per-element relabel runs as a parallel
562/// map; below the threshold it falls back to the sequential relabel.
563///
564/// Because the relabel `idx ^ stab_anticomm_bits` is a bijection (see
565/// [`apply_coefficients_seq`]), the parallel map's output already has unique
566/// keys — there is nothing to coalesce, so the result is collected straight into
567/// a flat `Vec` with no sequential fold afterwards.
568#[cfg(feature = "rayon")]
569fn apply_coefficients_parallel<I, CoeffType>(
570    items: &[(Complex<CoeffType>, I)],
571    stab_anticomm_bits: I,
572    destab_anticomm_bits: I,
573    odd_phase_mask: I,
574    phase_decomp: u8,
575) -> Vec<(I, Complex<CoeffType>)>
576where
577    I: TableauIndex + Send + Sync,
578    CoeffType: One + Zero + Clone + Send + Sync + num::Num,
579    Complex<CoeffType>:
580        std::ops::Mul<Output = Complex<CoeffType>> + std::ops::AddAssign + From<Complex64> + Copy,
581{
582    // See `branch_coefficients_parallel`: avoid nesting rayon inside shot-level
583    // parallelism; the main-thread (single-shot) path is unaffected.
584    if items.len() >= RAYON_COEFF_THRESHOLD && rayon::current_thread_index().is_none() {
585        use rayon::prelude::*;
586
587        return items
588            .par_iter()
589            .map(|&(coeff, idx)| {
590                let branch_index = idx ^ stab_anticomm_bits;
591                let branch_phase_contribution = compute_phase_with_mask_static(
592                    destab_anticomm_bits,
593                    idx,
594                    stab_anticomm_bits,
595                    odd_phase_mask,
596                );
597                let branch_phase = (branch_phase_contribution + phase_decomp) % 4;
598                let phase_factor: Complex<CoeffType> =
599                    COMPLEX_PHASE_CONVERSION[branch_phase as usize].into();
600                (branch_index, phase_factor * coeff)
601            })
602            .collect();
603    }
604
605    apply_coefficients_seq(
606        items.iter().copied(),
607        items.len(),
608        stab_anticomm_bits,
609        destab_anticomm_bits,
610        odd_phase_mask,
611        phase_decomp,
612    )
613}
614
615/// A [`Tableau`] extended with sparse coefficient tracking to handle
616/// non-Clifford gates.
617///
618/// Non-Clifford gates (T, rotations) split a single tableau into a sum
619/// of weighted branches indexed by bitstrings. `GeneralizedTableau`
620/// stores those weights in a [`SparseVector`] keyed by an
621/// [`IndexType`](TableauIndex). Choose:
622/// * `IndexType = usize` for up to 64 qubits,
623/// * `IndexType = u128` for up to 128,
624/// * `IndexType = bnum::types::U256` and friends for the very wide
625///   regime.
626///
627/// Per-qubit status (loss / leakage) is tracked in
628/// [`qubit_status`](GeneralizedTableau::qubit_status); gates skip any qubit that is
629/// not [`QubitStatus::Live`].
630///
631/// # Examples
632///
633/// Prepare a Bell pair and sample one shot. With a fixed seed the two
634/// measurements are perfectly correlated on every shot:
635///
636/// ```
637/// use ppvm_pauli_sum::config::fxhash::ByteF64;
638/// use ppvm_traits::traits::{Clifford, LossyMeasure};
639/// use ppvm_tableau::data::GeneralizedTableau;
640///
641/// let mut tab: GeneralizedTableau<ByteF64<1>> =
642///     GeneralizedTableau::new_with_seed(2, 1e-12, 0);
643/// tab.h(0);
644/// tab.cnot(0, 1);
645///
646/// let r0 = LossyMeasure::measure(&mut tab, 0);
647/// let r1 = LossyMeasure::measure(&mut tab, 1);
648/// assert_eq!(r0, r1);
649/// ```
650///
651/// Non-Clifford gates work through the same interface — apply a `T` gate
652/// followed by `T†` and the state is unchanged:
653///
654/// ```
655/// use ppvm_pauli_sum::config::fxhash::ByteF64;
656/// use ppvm_traits::traits::{Clifford, TGate};
657/// use ppvm_tableau::data::GeneralizedTableau;
658///
659/// let mut tab: GeneralizedTableau<ByteF64<1>> =
660///     GeneralizedTableau::new_with_seed(1, 1e-12, 0);
661/// tab.h(0);
662/// tab.t(0);
663/// tab.t_dag(0);
664/// // T followed by T† is the identity; the |+⟩ state is restored.
665/// ```
666#[derive(Clone)]
667pub struct GeneralizedTableau<
668    T: Config,
669    IndexType = usize,
670    SparseVectorType: SparseVector<Complex<T::Coeff>, IndexType> = Vec<(Complex64, IndexType)>,
671> {
672    /// Underlying Clifford tableau.
673    pub tableau: Tableau<T>,
674    /// Sparse coefficient vector indexed by bitstrings.
675    pub coefficients: SparseVectorType,
676    /// Per-qubit status relative to the computational subspace.
677    pub qubit_status: Vec<QubitStatus>,
678    /// Coefficient-magnitude threshold below which branches are dropped.
679    pub coefficient_threshold: T::Coeff,
680    /// Ordered log of every measurement performed (mirrors stim's record).
681    pub measurement_record: Vec<Option<bool>>,
682    _index_phantom: PhantomData<IndexType>,
683}
684
685impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> GeneralizedTableau<T, I, C>
686where
687    T::Coeff: One + Zero + Clone + num::Num,
688    Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
689        + std::ops::AddAssign
690        + From<Complex64>
691        + ComplexFloat
692        + Copy,
693    I: TableauIndex,
694{
695    /// Construct a generalized tableau in the `|0…0⟩` state.
696    ///
697    /// Branches whose coefficient magnitude falls below
698    /// `coefficient_threshold` are dropped during gate application.
699    pub fn new(n_qubits: usize, coefficient_threshold: T::Coeff) -> Self {
700        let mut coefficients = C::new();
701        let complex_one = Complex {
702            re: T::Coeff::one(),
703            im: T::Coeff::zero(),
704        };
705        coefficients.unsafe_insert(I::zero(), complex_one);
706        Self {
707            tableau: Tableau::new(n_qubits),
708            coefficients,
709            qubit_status: vec![QubitStatus::Live; n_qubits],
710            coefficient_threshold,
711            measurement_record: Vec::new(),
712            _index_phantom: PhantomData,
713        }
714    }
715
716    /// Same as [`GeneralizedTableau::new`], but seed the RNG deterministically.
717    pub fn new_with_seed(n_qubits: usize, coefficient_threshold: T::Coeff, seed: u64) -> Self {
718        let mut s = Self::new(n_qubits, coefficient_threshold);
719        s.tableau.rng = SmallRng::seed_from_u64(seed);
720        s
721    }
722
723    pub fn reset_all(&mut self) {
724        self.tableau.reset_all();
725
726        let mut coefficients = C::new();
727        let complex_one = Complex {
728            re: T::Coeff::one(),
729            im: T::Coeff::zero(),
730        };
731        coefficients.unsafe_insert(I::zero(), complex_one);
732        self.coefficients = coefficients;
733        for occ in self.qubit_status.iter_mut() {
734            *occ = QubitStatus::Live;
735        }
736        self.measurement_record.clear();
737    }
738
739    /// Clone the quantum state but reinitialize the RNG, producing an independent simulation
740    /// branch. If `seed` is `Some`, the new RNG is seeded deterministically; if `None`, it is
741    /// seeded from OS entropy.
742    pub fn fork(&self, seed: Option<u64>) -> Self {
743        let mut cloned = self.clone();
744        cloned.tableau.rng = match seed {
745            Some(s) => SmallRng::seed_from_u64(s),
746            None => rand::make_rng(),
747        };
748        cloned
749    }
750
751    /// Number of qubits.
752    pub fn n_qubits(&self) -> usize {
753        self.tableau.n_qubits
754    }
755
756    /// All measurement outcomes recorded so far, in order.
757    pub fn current_measurement_record(&self) -> &[Option<bool>] {
758        &self.measurement_record
759    }
760
761    /// Append an externally defined measurement result to the record.
762    ///
763    /// Used by Stim instructions such as `MPAD`, which append measurement
764    /// record bits without measuring a qubit.
765    pub fn append_measurement_record(&mut self, result: Option<bool>) {
766        self.measurement_record.push(result);
767    }
768
769    /// Replace the most recent measurement record entry.
770    ///
771    /// Used by noisy measurement paths where the quantum state follows the
772    /// true outcome but the public record should hold the reported bit.
773    pub fn overwrite_last_measurement_record(&mut self, result: Option<bool>) {
774        if let Some(last) = self.measurement_record.last_mut() {
775            *last = result;
776        }
777    }
778
779    /// Apply CZ to N pairs with constant offset: (base+i, base+offset+i) for i in 0..count.
780    /// Falls back to individual CZ calls if any qubit in the range is lost or leaked.
781    /// Overlap (`offset < count`) is owned by [`Tableau::cz_block_pairs`].
782    pub fn cz_block_pairs(&mut self, base: usize, offset: usize, count: usize)
783    where
784        <<T::Storage as BitView>::Store as TryFrom<usize>>::Error: Debug,
785        <T::Storage as BitView>::Store: PrimInt + TryFrom<usize>,
786    {
787        let any_inactive =
788            (0..count).any(|i| self.is_inactive(base + i) || self.is_inactive(base + offset + i));
789        if !any_inactive {
790            self.tableau.cz_block_pairs(base, offset, count);
791        } else {
792            self.cz_pairs_each(base, offset, count);
793        }
794    }
795
796    /// The [`Self::cz_block_pairs`] fallback: one scalar `cz` per surviving
797    /// pair, in order. Outlined `#[cold]`/`#[inline(never)]` so this `count`-way
798    /// loop of full `2n`-row sweeps stays out of the fused kernel's register
799    /// budget and cache lines — the fused path keeps a single call.
800    #[cold]
801    #[inline(never)]
802    fn cz_pairs_each(&mut self, base: usize, offset: usize, count: usize)
803    where
804        <T::Storage as BitView>::Store: PrimInt,
805    {
806        for i in 0..count {
807            let c = base + i;
808            let t = base + offset + i;
809            if !self.is_inactive(c) && !self.is_inactive(t) {
810                Clifford::cz(&mut self.tableau, c, t);
811            }
812        }
813    }
814
815    /// Apply CZ to N cross-word pairs. Controls at word_c, targets at word_t.
816    /// Falls back to individual CZ calls if any qubit is lost or leaked.
817    pub fn cz_block_pairs_cross_word(
818        &mut self,
819        word_c: usize,
820        base_bit_c: usize,
821        word_t: usize,
822        base_bit_t: usize,
823        count: usize,
824    ) where
825        <T::Storage as BitView>::Store: PrimInt,
826    {
827        let bits_per_word = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
828        let any_inactive = (0..count).any(|i| {
829            let c = word_c * bits_per_word + base_bit_c + i;
830            let t = word_t * bits_per_word + base_bit_t + i;
831            self.is_inactive(c) || self.is_inactive(t)
832        });
833        if !any_inactive {
834            self.tableau
835                .cz_block_pairs_cross_word(word_c, base_bit_c, word_t, base_bit_t, count);
836        } else {
837            for i in 0..count {
838                let c = word_c * bits_per_word + base_bit_c + i;
839                let t = word_t * bits_per_word + base_bit_t + i;
840                if !self.is_inactive(c) && !self.is_inactive(t) {
841                    Clifford::cz(&mut self.tableau, c, t);
842                }
843            }
844        }
845    }
846
847    /// Apply CZ to `count` pairs with a constant offset, given in qubit-index
848    /// terms: `(control_base + i, target_base + i)` for `i in 0..count`.
849    ///
850    /// This is the high-level entry point for a fused block of CZs: it splits
851    /// the run at storage-word boundaries internally and dispatches each
852    /// segment to [`Self::cz_block_pairs`] (control and target in the same
853    /// word) or [`Self::cz_block_pairs_cross_word`] (straddling two words), so
854    /// callers never need to reason about the `u64` packing. CZ is symmetric,
855    /// so the two bases may be passed in either order.
856    ///
857    /// Overlapping pairs — `|target_base - control_base| < count`, of which
858    /// adjacent-pair brickwork `cz_block(0, 1, n)` is the extreme case — are
859    /// handled: the same-word segments route through [`Self::cz_block_pairs`]
860    /// into [`Tableau::cz_block_pairs`], which falls back to the per-pair
861    /// loop. A cross-word segment is always disjoint (its `run` never exceeds
862    /// the offset), so the fused kernel stays live there.
863    pub fn cz_block(&mut self, control_base: usize, target_base: usize, count: usize)
864    where
865        <<T::Storage as BitView>::Store as TryFrom<usize>>::Error: Debug,
866        <T::Storage as BitView>::Store: PrimInt + TryFrom<usize>,
867    {
868        if count == 0 {
869            return;
870        }
871        // cz_block_pairs needs a non-negative offset; CZ is symmetric, so order
872        // the two bases.
873        let (lo, hi) = if control_base <= target_base {
874            (control_base, target_base)
875        } else {
876            (target_base, control_base)
877        };
878        let bits_per_word = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
879        let mut i = 0;
880        while i < count {
881            let (c, t) = (lo + i, hi + i);
882            let (wc, bc) = (c / bits_per_word, c % bits_per_word);
883            let (wt, bt) = (t / bits_per_word, t % bits_per_word);
884            // Longest run before either index crosses into the next word.
885            let run = (bits_per_word - bc).min(bits_per_word - bt).min(count - i);
886            if wc == wt {
887                self.cz_block_pairs(c, t - c, run);
888            } else {
889                self.cz_block_pairs_cross_word(wc, bc, wt, bt, run);
890            }
891            i += run;
892        }
893    }
894
895    // helper functions
896
897    /// Compute the decomposition of a pauli into stabilizer destabilizer products
898    /// Any Pauli can be written as P_addr0 = phase * prod(d_k ^ gamma_k) * prod(s_l ^ lambda_l)
899    /// where: gamma_k == 1 iff {P_addr0, s_k} = 0
900    /// lambda_l == 1 iff {P_addr0, d_l} = 0
901    /// Lemma 5. from T. J. Yoder (2012)
902    /// NOTE: this is O(n^2)
903    ///
904    /// The function returns `(phase, stab_anticomm_bits, destab_anticomm_bits)`, where
905    /// `stab_anticomm_bits[k] = 1` iff P_addr0 anticommutes with stabilizer s_k, and
906    /// `destab_anticomm_bits[l] = 1` iff P_addr0 anticommutes with destabilizer d_l.
907    /// Note that stab_anticomm_bits is equal to the shift of the index when branching
908    /// (`beta` in Eq(4) of the SOFT paper).
909    pub fn compute_decomposition(&self, addr0: usize, pauli: Pauli) -> (u8, I, I)
910    where
911        <<T as Config>::Storage as BitView>::Store: PrimInt,
912    {
913        let n = self.n_qubits();
914
915        // the actual decomposition, which we need to track the phase
916        let mut p_word = PhasedPauliWordNoHash::<T::Storage, T::BuildHasher>::new(n);
917        p_word.set(addr0, pauli);
918
919        // the bit strings defining the contributions
920        let mut destab_anticomm_bits = I::zero();
921        let mut stab_anticomm_bits = I::zero();
922
923        debug_assert_ne!(pauli, Pauli::I);
924        let pauli_bits = match pauli {
925            Pauli::I => (false, false),
926            Pauli::X => (true, false),
927            Pauli::Y => (true, true),
928            Pauli::Z => (false, true),
929            _ => unreachable!("Pauli L cannot occur in tableau"),
930        };
931
932        let stabilizers = self.tableau.stabilizers();
933        let destabilizers = self.tableau.destabilizers();
934        let one = I::one();
935
936        for (i, stab) in stabilizers.iter().enumerate() {
937            if !destabilizers[i].word.anticommutes_at(addr0, pauli_bits) {
938                // commutes
939                continue;
940            }
941
942            // contributes, so set the corresponding bit in destab_anticomm_bits to 1
943            destab_anticomm_bits |= one << i;
944
945            // destabilizer anti-commutes, so the stabilizer contributes
946            // the stabilizer is its own inverse up to its phase
947            // to avoid inverting the stabilizer, we just multiply by it
948            // and then divide by its phase squared
949            p_word *= stab;
950            p_word.add_phase(8 - 2 * stab.phase);
951        }
952
953        // NOTE: destabilizers also commute with one another in a valid tableau
954        // since the form a basis together with stabilizers
955        for (i, destab) in destabilizers.iter().enumerate() {
956            if !stabilizers[i].word.anticommutes_at(addr0, pauli_bits) {
957                // commutes
958                continue;
959            }
960
961            // contributes, so set the corresponding bit in stab_anticomm_bits to 1
962            stab_anticomm_bits |= one << i;
963
964            // stabilizer anti-commutes, so the destabilizer contributes
965            p_word *= destab;
966            p_word.add_phase(8 - 2 * destab.phase);
967        }
968
969        (p_word.phase, stab_anticomm_bits, destab_anticomm_bits)
970    }
971
972    /// Multi-qubit generalization of [`compute_decomposition`]: conjugate an
973    /// arbitrary `PauliWord` through the tableau and return the same triple
974    /// `(phase, stab_anticomm_bits, destab_anticomm_bits)`.
975    ///
976    /// Algorithm: call [`compute_decomposition`] for each non-identity qubit
977    /// in the input, then multiply the resulting single-qubit conjugates in
978    /// canonical-basis form `i^φ X^x Z^z`. Pauli multiplication picks up a
979    /// `(-1)^{popcount(z_running & x_new)}` cross-phase from
980    /// `Z^z_a X^x_b = (-1)^{z_a · x_b} X^x_b Z^z_a`.
981    pub(crate) fn compute_decomposition_word<W: PauliWordTrait>(&self, word: &W) -> (u8, I, I)
982    where
983        <<T as Config>::Storage as BitView>::Store: PrimInt,
984    {
985        let mut phase = 0u8;
986        let mut stab_anticomm = I::zero();
987        let mut destab_anticomm = I::zero();
988        for q in 0..self.n_qubits() {
989            let p_q = word.get(q);
990            if p_q == Pauli::I {
991                continue;
992            }
993            let (q_phase, q_stab, q_destab) = self.compute_decomposition(q, p_q);
994            let cross = 2 * (symplectic_inner(destab_anticomm, q_stab) as u8 % 2);
995            phase = (phase + q_phase + cross) % 4;
996            stab_anticomm = stab_anticomm ^ q_stab;
997            destab_anticomm = destab_anticomm ^ q_destab;
998        }
999        (phase, stab_anticomm, destab_anticomm)
1000    }
1001
1002    /// every basis index is a bit string alpha defining the basis state
1003    /// the phase when applying a Pauli is the product of all destabilizer phases
1004    /// and the phase contributions from the commutation relations
1005    /// we need to check every destabilizer where the basis index has a 1 bit.
1006    #[cfg(test)]
1007    pub(crate) fn compute_phase(
1008        &self,
1009        destab_anticomm_bits: I,
1010        basis_index: I,
1011        stab_anticomm_bits: I,
1012    ) -> u8 {
1013        // phase convention: 0: +1, 1: +i, 2: -1, 3: -i
1014        let one = I::one();
1015        let zero = I::zero();
1016
1017        // contribution 1: each destabilizer D_i with basis_index[i]=1 that anticommutes
1018        // with P (destab_anticomm_bits[i]=1) contributes a -1 sign; this is the symplectic inner product
1019        let mut phase = (2 * symplectic_inner(destab_anticomm_bits, basis_index) as u8) % 4;
1020
1021        // contribution 2: destabilizers that appear twice (basis_index[i]=1 and stab_anticomm_bits[i]=1)
1022        // contribute an extra -1 if their phase is imaginary
1023        let active = basis_index & stab_anticomm_bits;
1024        for (i, destab) in self.tableau.destabilizers().iter().enumerate() {
1025            if active & (one << i) == zero {
1026                continue;
1027            }
1028            if destab.phase % 2 != 0 {
1029                phase = (phase + 2) % 4;
1030            }
1031        }
1032
1033        phase
1034    }
1035
1036    /// Build a bitmask where bit i is set if destabilizer i has odd phase (phase % 2 != 0).
1037    pub fn odd_phase_destabilizer_mask(&self) -> I {
1038        let mut mask = I::zero();
1039        let one = I::one();
1040        for (i, destab) in self.tableau.destabilizers().iter().enumerate() {
1041            if destab.phase % 2 != 0 {
1042                mask |= one << i;
1043            }
1044        }
1045        mask
1046    }
1047}
1048
1049impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> GeneralizedTableau<T, I, C>
1050where
1051    T::Coeff: One + Zero + Clone + Send + Sync + num::Num + PartialOrd,
1052    Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
1053        + std::ops::AddAssign
1054        + From<Complex64>
1055        + ComplexFloat
1056        + Copy,
1057    I: TableauIndex + Send + Sync,
1058{
1059    /// Split every coefficient into a branch and non-branch term for a
1060    /// single-qubit Pauli measurement/rotation, in one pass over the whole
1061    /// coefficient array.
1062    ///
1063    /// This is a whole-batch primitive over the coefficient array — see the
1064    /// crate-level "Data-parallel / GPU-offloadable surface" section. The
1065    /// `#[cfg(feature = "rayon")]` path computes each entry's branch/non-branch
1066    /// coefficients in parallel; only the final accumulation into the
1067    /// coefficient map is sequential.
1068    pub(crate) fn branch_with_coefficients(
1069        &mut self,
1070        addr0: usize,
1071        pauli: Pauli,
1072        coefficient_factor: Complex<T::Coeff>,
1073        branch_factor: Complex<T::Coeff>,
1074    ) where
1075        <<T as Config>::Storage as BitView>::Store: PrimInt,
1076    {
1077        if self.is_inactive(addr0) {
1078            return;
1079        }
1080
1081        let (phase_decomp, stab_anticomm_bits, destab_anticomm_bits) =
1082            self.compute_decomposition(addr0, pauli);
1083
1084        let odd_phase_mask = self.odd_phase_destabilizer_mask();
1085        let old_coefficients = std::mem::replace(&mut self.coefficients, C::new());
1086        let n_coefficients = old_coefficients.len();
1087
1088        let cutoff_sq = self.coefficient_threshold.clone() * self.coefficient_threshold.clone();
1089
1090        // When rayon is enabled and above the threshold, use parallel map then apply cutoff
1091        // directly. Early-return so old_coefficients stays valid for the sequential path below.
1092        #[cfg(feature = "rayon")]
1093        if n_coefficients >= RAYON_COEFF_THRESHOLD {
1094            let items: Vec<_> = old_coefficients.into_iter().collect();
1095            let map = branch_coefficients_parallel(
1096                &items,
1097                BranchParams {
1098                    stab_anticomm_bits,
1099                    destab_anticomm_bits,
1100                    odd_phase_mask,
1101                    phase_decomp,
1102                    coefficient_factor,
1103                    branch_factor,
1104                },
1105            );
1106            self.coefficients.reserve(map.len());
1107            for (idx, coeff) in map {
1108                if coeff.norm_sqr() > cutoff_sq {
1109                    self.coefficients.unsafe_insert(idx, coeff);
1110                }
1111            }
1112            return;
1113        }
1114
1115        // Sequential sort-merge path: build nonbranch (nb), branch-values (brv), and a
1116        // packed branch-key stream, sort if needed, then 2-way merge directly into
1117        // self.coefficients with inline cutoff — no intermediate output Vec.
1118        //
1119        // Fast path: pack each (branch_key, build_pos) as a single u64
1120        // `(key << 16) | pos` and sort the u64 array. Half the data movement vs the
1121        // generic (I, u32) elements, and the most-optimised sort_unstable path.
1122        // Preconditions for packable: n_coefficients ≤ 0xFFFF (pos fits 16 bits) and
1123        // every branch key fits in 47 bits. Both hold for cultivation_d5 (≤13 active
1124        // bits, small coefficient counts). The fallback reproduces the prior behaviour
1125        // exactly for wide index types or large keys.
1126        let mut nb: Vec<(I, Complex<T::Coeff>)> = Vec::with_capacity(n_coefficients);
1127        let mut brv: Vec<Complex<T::Coeff>> = Vec::with_capacity(n_coefficients);
1128        let mut packed: Vec<u64> = Vec::with_capacity(n_coefficients);
1129        let mut packable = n_coefficients <= 0xFFFF;
1130        let mut nb_sorted = true;
1131        let mut prev: Option<I> = None;
1132        for (pos, (coeff, idx)) in (0_u32..).zip(old_coefficients) {
1133            let branch_index = idx ^ stab_anticomm_bits;
1134            let bpc = compute_phase_with_mask_static(
1135                destab_anticomm_bits,
1136                idx,
1137                stab_anticomm_bits,
1138                odd_phase_mask,
1139            );
1140            let branch_phase = (bpc + phase_decomp) % 4;
1141            let pf: Complex<T::Coeff> = COMPLEX_PHASE_CONVERSION[branch_phase as usize].into();
1142            brv.push(pf * coeff * branch_factor);
1143            match <u64 as num::NumCast>::from(branch_index) {
1144                Some(k) if k < (1u64 << 47) => packed.push((k << 16) | (pos as u64)),
1145                _ => {
1146                    packable = false;
1147                    packed.push(pos as u64);
1148                }
1149            }
1150            nb.push((idx, coeff * coefficient_factor));
1151            if let Some(p) = prev
1152                && idx < p
1153            {
1154                nb_sorted = false;
1155            }
1156            prev = Some(idx);
1157        }
1158
1159        self.coefficients.reserve(nb.len() + brv.len());
1160        let mut i = 0;
1161        if packable {
1162            if !nb_sorted {
1163                nb.sort_unstable_by_key(|a| a.0);
1164            }
1165            packed.sort_unstable();
1166            // Decode the 47-bit key from the high bits of a packed entry using byte-by-byte
1167            // construction from I::from(u8) — avoids num::NumCast which panics for bnum types.
1168            let decode_key = |w: u64| -> I {
1169                let k = w >> 16; // k < 2^47; 6 bytes suffice
1170                let mut v = I::zero();
1171                for b in 0..6usize {
1172                    let byte = ((k >> (b * 8)) & 0xFF) as u8;
1173                    v |= <I as From<u8>>::from(byte) << (b * 8);
1174                }
1175                v
1176            };
1177            let mut j = 0;
1178            while i < nb.len() && j < packed.len() {
1179                let bp = (packed[j] & 0xFFFF) as usize;
1180                let bk = decode_key(packed[j]);
1181                match nb[i].0.cmp(&bk) {
1182                    std::cmp::Ordering::Less => {
1183                        if nb[i].1.norm_sqr() > cutoff_sq {
1184                            self.coefficients.unsafe_insert(nb[i].0, nb[i].1);
1185                        }
1186                        i += 1;
1187                    }
1188                    std::cmp::Ordering::Greater => {
1189                        let v = brv[bp];
1190                        if v.norm_sqr() > cutoff_sq {
1191                            self.coefficients.unsafe_insert(bk, v);
1192                        }
1193                        j += 1;
1194                    }
1195                    std::cmp::Ordering::Equal => {
1196                        let mut sv = nb[i].1;
1197                        sv += brv[bp];
1198                        if sv.norm_sqr() > cutoff_sq {
1199                            self.coefficients.unsafe_insert(nb[i].0, sv);
1200                        }
1201                        i += 1;
1202                        j += 1;
1203                    }
1204                }
1205            }
1206            while j < packed.len() {
1207                let bp = (packed[j] & 0xFFFF) as usize;
1208                let bk = decode_key(packed[j]);
1209                let v = brv[bp];
1210                if v.norm_sqr() > cutoff_sq {
1211                    self.coefficients.unsafe_insert(bk, v);
1212                }
1213                j += 1;
1214            }
1215        } else {
1216            // Fallback for wide index types, large keys (≥ 2^47), or many coefficients
1217            // (> 65535). nb is still in build order here; reconstruct brk from it before
1218            // sorting nb, so build-position p correctly indexes brv[p].
1219            let mut brk: Vec<(I, u32)> = (0_u32..)
1220                .zip(nb.iter())
1221                .map(|(p, &(idx, _))| (idx ^ stab_anticomm_bits, p))
1222                .collect();
1223            if !nb_sorted {
1224                nb.sort_unstable_by_key(|a| a.0);
1225            }
1226            brk.sort_unstable_by_key(|a| a.0);
1227            let mut j = 0;
1228            while i < nb.len() && j < brk.len() {
1229                let (bk, bp) = brk[j];
1230                match nb[i].0.cmp(&bk) {
1231                    std::cmp::Ordering::Less => {
1232                        if nb[i].1.norm_sqr() > cutoff_sq {
1233                            self.coefficients.unsafe_insert(nb[i].0, nb[i].1);
1234                        }
1235                        i += 1;
1236                    }
1237                    std::cmp::Ordering::Greater => {
1238                        let v = brv[bp as usize];
1239                        if v.norm_sqr() > cutoff_sq {
1240                            self.coefficients.unsafe_insert(bk, v);
1241                        }
1242                        j += 1;
1243                    }
1244                    std::cmp::Ordering::Equal => {
1245                        let mut sv = nb[i].1;
1246                        sv += brv[bp as usize];
1247                        if sv.norm_sqr() > cutoff_sq {
1248                            self.coefficients.unsafe_insert(nb[i].0, sv);
1249                        }
1250                        i += 1;
1251                        j += 1;
1252                    }
1253                }
1254            }
1255            while j < brk.len() {
1256                let (bk, bp) = brk[j];
1257                let v = brv[bp as usize];
1258                if v.norm_sqr() > cutoff_sq {
1259                    self.coefficients.unsafe_insert(bk, v);
1260                }
1261                j += 1;
1262            }
1263        }
1264        while i < nb.len() {
1265            if nb[i].1.norm_sqr() > cutoff_sq {
1266                self.coefficients.unsafe_insert(nb[i].0, nb[i].1);
1267            }
1268            i += 1;
1269        }
1270    }
1271
1272    pub(crate) fn compute_coefficients_after_pauli_apply(
1273        &self,
1274        coefficients: &mut C,
1275        addr0: usize,
1276        pauli: Pauli,
1277    ) where
1278        <<T as Config>::Storage as BitView>::Store: PrimInt,
1279    {
1280        if self.is_inactive(addr0) {
1281            return;
1282        }
1283
1284        let (phase_decomp, stab_anticomm_bits, destab_anticomm_bits) =
1285            self.compute_decomposition(addr0, pauli);
1286
1287        let odd_phase_mask = self.odd_phase_destabilizer_mask();
1288        let n_coefficients = coefficients.len();
1289        let old_coefficients = std::mem::replace(coefficients, C::new());
1290
1291        #[cfg(feature = "rayon")]
1292        let new_coefficients = if n_coefficients >= RAYON_COEFF_THRESHOLD {
1293            let items: Vec<_> = old_coefficients.into_iter().collect();
1294            apply_coefficients_parallel(
1295                &items,
1296                stab_anticomm_bits,
1297                destab_anticomm_bits,
1298                odd_phase_mask,
1299                phase_decomp,
1300            )
1301        } else {
1302            apply_coefficients_seq(
1303                old_coefficients,
1304                n_coefficients,
1305                stab_anticomm_bits,
1306                destab_anticomm_bits,
1307                odd_phase_mask,
1308                phase_decomp,
1309            )
1310        };
1311
1312        #[cfg(not(feature = "rayon"))]
1313        let new_coefficients = apply_coefficients_seq(
1314            old_coefficients,
1315            n_coefficients,
1316            stab_anticomm_bits,
1317            destab_anticomm_bits,
1318            odd_phase_mask,
1319            phase_decomp,
1320        );
1321
1322        let cutoff_sq = self.coefficient_threshold.clone() * self.coefficient_threshold.clone();
1323        coefficients.reserve(new_coefficients.len());
1324        for (idx, coeff) in new_coefficients {
1325            if coeff.norm_sqr() > cutoff_sq {
1326                coefficients.unsafe_insert(idx, coeff);
1327            }
1328        }
1329    }
1330}
1331
1332#[cfg(test)]
1333mod tests {
1334    use super::*;
1335    use bnum::BUint;
1336    use ppvm_pauli_sum::config::fxhash::ByteF64;
1337
1338    type TestConfig = ByteF64<1>;
1339    type TestTableau = GeneralizedTableau<TestConfig>;
1340    type TestTableauBUint = GeneralizedTableau<TestConfig, BUint<1>>;
1341
1342    #[test]
1343    fn test_compute_phase_z_2_single_qubit_plus_state() {
1344        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
1345        tab.tableau.h(0);
1346
1347        // After H: stabilizer = +X, destabilizer = +Z
1348        // stab_anticomm_bits = 1 (stabilizer has xbit[0]=true)
1349        // both phases should be 0
1350        let (phase_decomp, stab_anticomm_bits, destab_anticomm_bits) =
1351            tab.compute_decomposition(0, Pauli::Z);
1352        let phase0 = phase_decomp + tab.compute_phase(destab_anticomm_bits, 0, stab_anticomm_bits);
1353        assert_eq!(phase0, 0);
1354        let phase1 = phase_decomp + tab.compute_phase(destab_anticomm_bits, 1, stab_anticomm_bits);
1355        assert_eq!(phase1, 0);
1356    }
1357
1358    #[test]
1359    fn test_compute_phase_z_2_y_stabilizer() {
1360        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
1361        tab.tableau.h(0);
1362        tab.tableau.s(0);
1363
1364        let (phase_decomp, stab_anticomm_bits, destab_anticomm_bits) =
1365            tab.compute_decomposition(0, Pauli::Z);
1366        let phase0 = phase_decomp + tab.compute_phase(destab_anticomm_bits, 0, stab_anticomm_bits);
1367        assert_eq!(phase0, 0);
1368        let phase1 = phase_decomp + tab.compute_phase(destab_anticomm_bits, 1, stab_anticomm_bits);
1369        assert_eq!(phase1, 0);
1370    }
1371
1372    #[test]
1373    fn test_compute_phase_z_2_mx_stabilizer() {
1374        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
1375        tab.tableau.h(0);
1376        tab.tableau.z(0);
1377
1378        let (phase_decomp, stab_anticomm_bits, destab_anticomm_bits) =
1379            tab.compute_decomposition(0, Pauli::Z);
1380        let phase0 = phase_decomp + tab.compute_phase(destab_anticomm_bits, 0, stab_anticomm_bits);
1381        assert_eq!(phase0, 0);
1382        let phase1 = phase_decomp + tab.compute_phase(destab_anticomm_bits, 1, stab_anticomm_bits);
1383        assert_eq!(phase1, 0);
1384    }
1385
1386    #[test]
1387    fn test_compute_phase_z_2_y_stabilizer_2() {
1388        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
1389        tab.tableau.h(0);
1390        tab.tableau.s(0);
1391        tab.tableau.h(0);
1392
1393        let (phase_decomp, stab_anticomm_bits, destab_anticomm_bits) =
1394            tab.compute_decomposition(0, Pauli::Z);
1395        let phase0 =
1396            (phase_decomp + tab.compute_phase(destab_anticomm_bits, 0, stab_anticomm_bits)) % 4;
1397        assert_eq!(phase0, 1);
1398        let phase1 =
1399            (phase_decomp + tab.compute_phase(destab_anticomm_bits, 1, stab_anticomm_bits)) % 4;
1400        assert_eq!(phase1, 3);
1401    }
1402
1403    #[test]
1404    fn test_index_type() {
1405        let mut tab: TestTableauBUint = GeneralizedTableau::new(1, 1e-12);
1406        tab.tableau.h(0);
1407    }
1408
1409    /// Snapshot all rows (xbits, zbits, phase) for comparison.
1410    fn snapshot_tableau<C: Config>(tab: &Tableau<C>) -> Vec<(C::Storage, C::Storage, u8)> {
1411        tab.data
1412            .iter()
1413            .map(|pw| (pw.word.xbits.data, pw.word.zbits.data, pw.phase))
1414            .collect()
1415    }
1416
1417    #[test]
1418    fn test_cz_block_pairs_matches_individual() {
1419        // Test CZ pairs (0,4), (1,5), (2,6), (3,7) — offset=4, count=4
1420        type TTab = Tableau<ByteF64<1>>;
1421        let n = 8;
1422        let base = 0;
1423        let offset = 4;
1424        let count = 4;
1425
1426        let mut tab1 = TTab::new(n);
1427        Clifford::h(&mut tab1, 0);
1428        Clifford::h(&mut tab1, 3);
1429        Clifford::s(&mut tab1, 1);
1430        let mut tab2 = tab1.clone();
1431
1432        // Individual
1433        for i in 0..count {
1434            Clifford::cz(&mut tab1, base + i, base + offset + i);
1435        }
1436
1437        // Batch
1438        tab2.cz_block_pairs(base, offset, count);
1439
1440        assert_eq!(snapshot_tableau(&tab1), snapshot_tableau(&tab2));
1441    }
1442
1443    #[test]
1444    fn test_cz_block_pairs_offset_17() {
1445        // Simulate MSD-like CZ: (0,17), (1,18), ..., (16,33) — all in one u64 word
1446        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1447        type LargeTab = Tableau<Byte8F64<2>>;
1448        let n = 34;
1449        let mut tab1 = LargeTab::new(n);
1450        // Apply some gates to create non-trivial state
1451        for i in 0..n {
1452            Clifford::h(&mut tab1, i);
1453        }
1454        let mut tab2 = tab1.clone();
1455
1456        // Individual
1457        for i in 0..17 {
1458            Clifford::cz(&mut tab1, i, 17 + i);
1459        }
1460
1461        // Batch
1462        tab2.cz_block_pairs(0, 17, 17);
1463
1464        assert_eq!(snapshot_tableau(&tab1), snapshot_tableau(&tab2));
1465    }
1466
1467    #[test]
1468    fn test_cz_block_pairs_nonzero_base() {
1469        // Test CZ pairs starting from a non-zero base: (10,27), (11,28), ..., (14,31)
1470        // All within one u64 word (bits 0-63)
1471        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1472        type LargeTab = Tableau<Byte8F64<2>>;
1473        let n = 32;
1474        let base = 10;
1475        let offset = 17;
1476        let count = 5;
1477
1478        let mut tab1 = LargeTab::new(n);
1479        for i in 0..n {
1480            Clifford::h(&mut tab1, i);
1481        }
1482        Clifford::s(&mut tab1, 12);
1483        Clifford::s(&mut tab1, 28);
1484        let mut tab2 = tab1.clone();
1485
1486        for i in 0..count {
1487            Clifford::cz(&mut tab1, base + i, base + offset + i);
1488        }
1489
1490        tab2.cz_block_pairs(base, offset, count);
1491
1492        assert_eq!(snapshot_tableau(&tab1), snapshot_tableau(&tab2));
1493    }
1494
1495    #[test]
1496    fn test_cz_block_pairs_single_pair() {
1497        // Degenerate case: count=1 should be same as one CZ
1498        type TTab = Tableau<ByteF64<1>>;
1499        let n = 8;
1500        let mut tab1 = TTab::new(n);
1501        Clifford::h(&mut tab1, 2);
1502        Clifford::s(&mut tab1, 5);
1503        let mut tab2 = tab1.clone();
1504
1505        Clifford::cz(&mut tab1, 2, 5);
1506        tab2.cz_block_pairs(2, 3, 1);
1507
1508        assert_eq!(snapshot_tableau(&tab1), snapshot_tableau(&tab2));
1509    }
1510
1511    #[test]
1512    fn test_cz_block_pairs_zero_count() {
1513        // count=0 should be a no-op
1514        type TTab = Tableau<ByteF64<1>>;
1515        let n = 8;
1516        let mut tab1 = TTab::new(n);
1517        Clifford::h(&mut tab1, 0);
1518        let before = snapshot_tableau(&tab1);
1519        tab1.cz_block_pairs(0, 4, 0);
1520        assert_eq!(before, snapshot_tableau(&tab1));
1521    }
1522
1523    #[test]
1524    fn test_generalized_tableau_cz_block_pairs() {
1525        // Test through GeneralizedTableau wrapper
1526        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1527        type GTab = GeneralizedTableau<Byte8F64<2>>;
1528        let n = 34;
1529        let mut tab1: GTab = GeneralizedTableau::new(n, 1e-12);
1530        for i in 0..n {
1531            Clifford::h(&mut tab1.tableau, i);
1532        }
1533        let mut tab2 = tab1.clone();
1534
1535        // Individual via Clifford trait
1536        for i in 0..17 {
1537            Clifford::cz(&mut tab1, i, 17 + i);
1538        }
1539
1540        // Batch
1541        tab2.cz_block_pairs(0, 17, 17);
1542
1543        assert_eq!(
1544            snapshot_tableau(&tab1.tableau),
1545            snapshot_tableau(&tab2.tableau)
1546        );
1547    }
1548
1549    #[test]
1550    fn test_generalized_tableau_cz_block_pairs_with_loss() {
1551        // When a qubit is lost, should fall back to individual CZ (skipping lost ones)
1552        type GTab = GeneralizedTableau<ByteF64<1>>;
1553        let n = 8;
1554        let mut tab1: GTab = GeneralizedTableau::new(n, 1e-12);
1555        for i in 0..n {
1556            Clifford::h(&mut tab1.tableau, i);
1557        }
1558        tab1.qubit_status[2] = QubitStatus::Lost; // Mark qubit 2 as lost
1559        let mut tab2 = tab1.clone();
1560
1561        // Individual, skipping lost qubits
1562        for i in 0..4 {
1563            let c = i;
1564            let t = 4 + i;
1565            if !tab1.is_lost(c) && !tab1.is_lost(t) {
1566                Clifford::cz(&mut tab1.tableau, c, t);
1567            }
1568        }
1569
1570        // Batch (should fall back internally)
1571        tab2.cz_block_pairs(0, 4, 4);
1572
1573        assert_eq!(
1574            snapshot_tableau(&tab1.tableau),
1575            snapshot_tableau(&tab2.tableau)
1576        );
1577    }
1578
1579    #[test]
1580    fn test_generalized_tableau_cz_block_pairs_with_leakage() {
1581        // Leaked qubits take the same inactive fallback as lost ones.
1582        type GTab = GeneralizedTableau<ByteF64<1>>;
1583        let n = 8;
1584        let mut tab1: GTab = GeneralizedTableau::new(n, 1e-12);
1585        for i in 0..n {
1586            Clifford::h(&mut tab1.tableau, i);
1587        }
1588        tab1.qubit_status[2] = QubitStatus::Leaked;
1589        let mut tab2 = tab1.clone();
1590
1591        for i in 0..4 {
1592            let c = i;
1593            let t = 4 + i;
1594            if !tab1.is_inactive(c) && !tab1.is_inactive(t) {
1595                Clifford::cz(&mut tab1.tableau, c, t);
1596            }
1597        }
1598
1599        tab2.cz_block_pairs(0, 4, 4);
1600
1601        assert_eq!(
1602            snapshot_tableau(&tab1.tableau),
1603            snapshot_tableau(&tab2.tableau)
1604        );
1605    }
1606
1607    #[test]
1608    fn test_cz_block_matches_individual_across_word_boundary() {
1609        // cz_block must split a run that straddles the u64 boundary into the
1610        // right within-word + cross-word segments. control_base=34,
1611        // target_base=51, count=17 reproduces the MSD ql[2]xql[3] sweep:
1612        // (34,51)..(46,63) in word 0, then (47,64)..(50,67) cross-word.
1613        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1614        type GTab = GeneralizedTableau<Byte8F64<2>>;
1615        let n = 85;
1616        let mut tab1: GTab = GeneralizedTableau::new(n, 1e-12);
1617        for i in 0..n {
1618            Clifford::h(&mut tab1.tableau, i);
1619        }
1620        let mut tab2 = tab1.clone();
1621
1622        let (control_base, target_base, count) = (34, 51, 17);
1623        for i in 0..count {
1624            Clifford::cz(&mut tab1, control_base + i, target_base + i);
1625        }
1626        tab2.cz_block(control_base, target_base, count);
1627
1628        assert_eq!(
1629            snapshot_tableau(&tab1.tableau),
1630            snapshot_tableau(&tab2.tableau)
1631        );
1632
1633        // Reversed bases (CZ is symmetric) must give the same result.
1634        let mut tab3 = GeneralizedTableau::<Byte8F64<2>>::new(n, 1e-12);
1635        for i in 0..n {
1636            Clifford::h(&mut tab3.tableau, i);
1637        }
1638        tab3.cz_block(target_base, control_base, count);
1639        assert_eq!(
1640            snapshot_tableau(&tab1.tableau),
1641            snapshot_tableau(&tab3.tableau)
1642        );
1643    }
1644
1645    /// Deterministic pseudo-random Clifford frame. `s`/`z` guarantee the rows
1646    /// carry pre-existing `z` bits *and* non-zero phases, which is what makes
1647    /// the fused block's `OR`ed `z` update and its single `count_ones() & 1`
1648    /// parity observable.
1649    fn prepare_frame<C: Config>(tab: &mut Tableau<C>, n: usize, seed: u64)
1650    where
1651        <C::Storage as BitView>::Store: PrimInt,
1652    {
1653        let mut s = seed.wrapping_mul(0x9E37_79B9_7F4A_7C15) | 1;
1654        let mut next = move || {
1655            s ^= s << 13;
1656            s ^= s >> 7;
1657            s ^= s << 17;
1658            (s >> 11) as usize
1659        };
1660        for _ in 0..6 * n {
1661            let q = next() % n;
1662            match next() % 6 {
1663                0 => Clifford::h(tab, q),
1664                1 => Clifford::s(tab, q),
1665                2 => Clifford::z(tab, q),
1666                3 => Clifford::x(tab, q),
1667                4 => Clifford::cnot(tab, q, (q + 1 + next() % (n - 1)) % n),
1668                _ => Clifford::cz(tab, q, (q + 1 + next() % (n - 1)) % n),
1669            }
1670        }
1671    }
1672
1673    /// The fused CZ block must agree with the sequential per-pair `cz` loop
1674    /// even when the pairs' supports **overlap** (`offset < count`, or the
1675    /// degenerate `offset == 0`) — bits *and* sign.
1676    ///
1677    /// `czSeq_phase` proves the fused `count_ones() & 1` phase equals the
1678    /// sequential loop only under pairwise-disjoint supports, and
1679    /// `czSeq_phase_needs_disjoint` exhibits the counterexample replayed below.
1680    /// Adjacent-pair brickwork — `cz_block(0, 1, n)` — is precisely the
1681    /// overlapping shape, so the most natural call is the broken one (G-060).
1682    #[test]
1683    fn test_cz_block_matches_per_pair_loop_on_overlapping_pairs() {
1684        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1685        type GTab = GeneralizedTableau<Byte8F64<1>>;
1686        let n = 16;
1687        // (control_base, target_base, count); offset = target - control.
1688        let configs = [
1689            (0usize, 1usize, 2usize), // czSeq_phase_needs_disjoint's witness
1690            (0, 1, 3),
1691            (0, 1, 15), // adjacent-pair brickwork over the whole register
1692            (3, 4, 8),
1693            (0, 2, 5), // the ledger's second witness
1694            (2, 5, 9),
1695            (4, 4, 3), // degenerate: offset == 0, i.e. pairs (q, q)
1696            (0, 1, 1), // a single pair is disjoint: must stay on the fused path
1697            (0, 8, 8), // offset == count: the disjoint boundary
1698        ];
1699        for seed in 0..8u64 {
1700            let mut prepared: GTab = GeneralizedTableau::new(n, 1e-12);
1701            prepare_frame(&mut prepared.tableau, n, seed);
1702            // The frame must actually exercise the z plane and the phase.
1703            assert!(
1704                snapshot_tableau(&prepared.tableau)
1705                    .iter()
1706                    .any(|&(_, z, _)| z != [0]),
1707                "frame (seed {seed}) has no pre-existing z bits"
1708            );
1709            assert!(
1710                snapshot_tableau(&prepared.tableau)
1711                    .iter()
1712                    .any(|&(.., phase)| phase != 0),
1713                "frame (seed {seed}) has no non-zero phase"
1714            );
1715
1716            for &(cb, tb, count) in &configs {
1717                let mut seq = prepared.clone();
1718                let mut fused = prepared.clone();
1719                let mut fused_pairs = prepared.clone();
1720
1721                for i in 0..count {
1722                    Clifford::cz(&mut seq.tableau, cb + i, tb + i);
1723                }
1724                fused.cz_block(cb, tb, count);
1725                fused_pairs.cz_block_pairs(cb, tb - cb, count);
1726
1727                assert_eq!(
1728                    snapshot_tableau(&seq.tableau),
1729                    snapshot_tableau(&fused.tableau),
1730                    "cz_block({cb}, {tb}, {count}) disagrees with the per-pair loop (seed {seed})"
1731                );
1732                assert_eq!(
1733                    snapshot_tableau(&seq.tableau),
1734                    snapshot_tableau(&fused_pairs.tableau),
1735                    "cz_block_pairs({cb}, {}, {count}) disagrees with the per-pair loop (seed {seed})",
1736                    tb - cb
1737                );
1738            }
1739        }
1740    }
1741
1742    /// `czSeq_phase_needs_disjoint`'s exact witness, with the absolute answer
1743    /// derived from the Pauli algebra rather than from the loop: `CZ`
1744    /// conjugates `X⊗X ↦ +Y⊗Y` and `Y⊗X ↦ −X⊗Y`, so `CZ₁₂·CZ₀₁` sends
1745    /// `X₀X₁X₂` to `−Y₀X₁Y₂` — row coordinates `x = 111`, `z = 101`, phase `2`.
1746    #[test]
1747    fn test_cz_block_overlapping_pairs_hits_the_algebraic_answer() {
1748        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1749        type GTab = GeneralizedTableau<Byte8F64<1>>;
1750        let mut fused: GTab = GeneralizedTableau::new(3, 1e-12);
1751        // Row 0 is the destabilizer X₀; the two CNOTs spread it to X₀X₁X₂.
1752        Clifford::cnot(&mut fused.tableau, 0, 1);
1753        Clifford::cnot(&mut fused.tableau, 0, 2);
1754        let mut seq = fused.clone();
1755        assert_eq!(snapshot_tableau(&fused.tableau)[0], ([0b111], [0b000], 0));
1756
1757        Clifford::cz(&mut seq.tableau, 0, 1);
1758        Clifford::cz(&mut seq.tableau, 1, 2);
1759        fused.cz_block(0, 1, 2);
1760
1761        assert_eq!(snapshot_tableau(&seq.tableau)[0], ([0b111], [0b101], 2));
1762        assert_eq!(snapshot_tableau(&fused.tableau)[0], ([0b111], [0b101], 2));
1763    }
1764
1765    /// Same algebraic witness as above, but through the public `Tableau`
1766    /// kernel — overlapping pairs must fall back, not `debug_assert` and
1767    /// corrupt in release.
1768    #[test]
1769    fn test_tableau_cz_block_pairs_overlapping_hits_the_algebraic_answer() {
1770        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1771        type TTab = Tableau<Byte8F64<1>>;
1772        let mut fused = TTab::new(3);
1773        Clifford::cnot(&mut fused, 0, 1);
1774        Clifford::cnot(&mut fused, 0, 2);
1775        assert_eq!(snapshot_tableau(&fused)[0], ([0b111], [0b000], 0));
1776
1777        fused.cz_block_pairs(0, 1, 2);
1778
1779        assert_eq!(snapshot_tableau(&fused)[0], ([0b111], [0b101], 2));
1780    }
1781
1782    /// `Tableau::cz_block_pairs` itself, not only the `GeneralizedTableau`
1783    /// wrapper, must match the sequential loop on overlapping pairs.
1784    #[test]
1785    fn test_tableau_cz_block_pairs_matches_per_pair_loop_on_overlapping_pairs() {
1786        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1787        type TTab = Tableau<Byte8F64<1>>;
1788        let n = 16;
1789        let configs = [
1790            (0usize, 1usize, 2usize),
1791            (0, 1, 15),
1792            (0, 2, 5),
1793            (4, 4, 3),
1794            (0, 8, 8),
1795        ];
1796        for seed in 0..8u64 {
1797            let mut prepared = TTab::new(n);
1798            prepare_frame(&mut prepared, n, seed);
1799            for &(cb, tb, count) in &configs {
1800                let mut seq = prepared.clone();
1801                let mut fused = prepared.clone();
1802                for i in 0..count {
1803                    Clifford::cz(&mut seq, cb + i, tb + i);
1804                }
1805                fused.cz_block_pairs(cb, tb - cb, count);
1806                assert_eq!(
1807                    snapshot_tableau(&seq),
1808                    snapshot_tableau(&fused),
1809                    "Tableau::cz_block_pairs({cb}, {}, {count}) disagrees \
1810                     with the per-pair loop (seed {seed})",
1811                    tb - cb
1812                );
1813            }
1814        }
1815    }
1816
1817    /// Overlapping brickwork that straddles a storage-word boundary: the
1818    /// segment split must not turn an overlapping run into a fused one.
1819    #[test]
1820    fn test_cz_block_overlapping_pairs_across_word_boundary() {
1821        use ppvm_pauli_sum::config::fx64hash::Byte8F64;
1822        type GTab = GeneralizedTableau<Byte8F64<2>>;
1823        let n = 85;
1824        let (control_base, target_base, count) = (60, 61, 10);
1825        for seed in 0..8u64 {
1826            let mut seq: GTab = GeneralizedTableau::new(n, 1e-12);
1827            prepare_frame(&mut seq.tableau, n, seed);
1828            let mut fused = seq.clone();
1829
1830            for i in 0..count {
1831                Clifford::cz(&mut seq.tableau, control_base + i, target_base + i);
1832            }
1833            fused.cz_block(control_base, target_base, count);
1834
1835            assert_eq!(
1836                snapshot_tableau(&seq.tableau),
1837                snapshot_tableau(&fused.tableau),
1838                "cz_block({control_base}, {target_base}, {count}) disagrees \
1839                 with the per-pair loop (seed {seed})"
1840            );
1841        }
1842    }
1843
1844    // ─── reset_all ────────────────────────────────────────────────────
1845
1846    /// `GeneralizedTableau::reset_all` restores the full state to a fresh
1847    /// `|0…0⟩` tableau: identical stabilizer/destabilizer rows and a single
1848    /// identity coefficient, even after non-Clifford branching.
1849    #[test]
1850    fn reset_all_restores_fresh_state() {
1851        let mut tab: TestTableau = GeneralizedTableau::new(3, 1e-12);
1852        let fresh: TestTableau = GeneralizedTableau::new(3, 1e-12);
1853
1854        tab.h(0);
1855        tab.cnot(0, 1);
1856        tab.ry(2, 0.7); // non-Clifford: branches the coefficient vector
1857        assert!(
1858            tab.coefficients.len() > 1,
1859            "rotation should branch the coefficient vector"
1860        );
1861
1862        tab.reset_all();
1863
1864        assert_eq!(
1865            snapshot_tableau(&tab.tableau),
1866            snapshot_tableau(&fresh.tableau)
1867        );
1868        let coeffs: Vec<_> = tab.coefficients.to_vec();
1869        let fresh_coeffs: Vec<_> = fresh.coefficients.to_vec();
1870        assert_eq!(coeffs, fresh_coeffs);
1871    }
1872
1873    /// A full reset clears the measurement record. Regression guard: an earlier
1874    /// version left it intact, so `current_measurement_record` returned stale
1875    /// outcomes after a reset.
1876    #[test]
1877    fn reset_all_clears_measurement_record() {
1878        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
1879        tab.append_measurement_record(Some(true));
1880        tab.append_measurement_record(None);
1881        assert_eq!(tab.current_measurement_record().len(), 2);
1882
1883        tab.reset_all();
1884
1885        assert!(tab.current_measurement_record().is_empty());
1886    }
1887
1888    /// A full reset clears per-qubit loss flags.
1889    #[test]
1890    fn reset_all_clears_loss_flags() {
1891        let mut tab: TestTableau = GeneralizedTableau::new(3, 1e-12);
1892        tab.qubit_status[0] = QubitStatus::Lost;
1893        tab.qubit_status[2] = QubitStatus::Lost;
1894
1895        tab.reset_all();
1896
1897        assert!(tab.qubit_status.iter().all(|&occ| occ == QubitStatus::Live));
1898    }
1899
1900    /// A full reset clears per-qubit leakage flags.
1901    #[test]
1902    fn reset_all_clears_leak_flags() {
1903        let mut tab: TestTableau = GeneralizedTableau::new(3, 1e-12);
1904        tab.qubit_status[0] = QubitStatus::Leaked;
1905        tab.qubit_status[2] = QubitStatus::Leaked;
1906
1907        tab.reset_all();
1908
1909        assert!(tab.qubit_status.iter().all(|&occ| occ == QubitStatus::Live));
1910    }
1911
1912    /// `Tableau::reset_all` restores the fresh identity tableau rows.
1913    #[test]
1914    fn tableau_reset_all_restores_fresh_rows() {
1915        let mut tab: Tableau<TestConfig> = Tableau::new(4);
1916        let fresh: Tableau<TestConfig> = Tableau::new(4);
1917
1918        tab.h(0);
1919        tab.s(1);
1920        tab.h(3);
1921
1922        tab.reset_all();
1923
1924        assert_eq!(snapshot_tableau(&tab), snapshot_tableau(&fresh));
1925    }
1926}