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