Skip to main content

ppvm_tableau/gates/
clifford.rs

1// SPDX-FileCopyrightText: 2026 The PPVM Authors
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::prelude::*;
5use bitvec::view::BitView;
6use bitvec::view::BitViewSized;
7use num::complex::Complex;
8use num::{One, PrimInt, Zero};
9use smallvec::{SmallVec, smallvec};
10
11/// Per-word bitmask buffer used by the batched Clifford gates.
12/// Stack-allocates for up to 8 storage words; spills to heap beyond.
13type MaskBuf<T> = SmallVec<[<<T as Config>::Storage as BitView>::Store; 8]>;
14
15// Single-qubit gate on a `GeneralizedTableau`: skip lost/leaked qubits, delegate
16// to the inner tableau's canonical (word-level) method.
17macro_rules! impl_generalized_tableau_clifford {
18    ($name:ident) => {
19        fn $name(&mut self, index: usize) {
20            if self.is_inactive(index) {
21                return;
22            }
23            self.tableau.$name(index);
24        }
25    };
26}
27
28// Two-qubit gate on a `GeneralizedTableau`: skip pairs with a lost/leaked qubit.
29macro_rules! impl_generalized_tableau_clifford_pair {
30    ($name:ident) => {
31        fn $name(&mut self, control: usize, target: usize) {
32            if self.is_inactive(control) || self.is_inactive(target) {
33                return;
34            }
35            self.tableau.$name(control, target);
36        }
37    };
38}
39
40// Single source of truth for the per-gate Clifford phase/bit logic: every
41// method below loops over the rows operating directly on the packed Pauli
42// words (raw integer slices via `as_raw_slice`/`as_raw_mut_slice` plus a
43// hoisted `index/bits`, `index%bits`, mask) rather than going through
44// `bitvec`'s bounds-checked single-bit indexing inside the per-row loop.
45//
46// Every caller — a bare `Tableau`, a `GeneralizedTableau` (which delegates
47// here via the `impl_generalized_tableau_clifford*` macros), and the fused
48// batch path — runs through this one implementation, so there is no parallel
49// copy that can silently diverge.
50impl<T: Config> Clifford for Tableau<T>
51where
52    <T::Storage as BitView>::Store: PrimInt,
53{
54    #[inline]
55    fn x(&mut self, index: usize) {
56        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
57        let wi = index / bits;
58        let off = index % bits;
59        let one = <T::Storage as BitView>::Store::one();
60        let zero = <T::Storage as BitView>::Store::zero();
61        let mask = one << off;
62        self.data.iter_mut().for_each(|pw| {
63            let zp = pw.word.zbits.data.as_raw_slice();
64            let zw = zp[wi];
65            pw.phase ^= (((zw & mask) != zero) as u8) << 1;
66        });
67    }
68
69    #[inline]
70    fn y(&mut self, index: usize) {
71        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
72        let wi = index / bits;
73        let off = index % bits;
74        let one = <T::Storage as BitView>::Store::one();
75        let zero = <T::Storage as BitView>::Store::zero();
76        let mask = one << off;
77        self.data.iter_mut().for_each(|pw| {
78            let xp = pw.word.xbits.data.as_raw_slice();
79            let zp = pw.word.zbits.data.as_raw_slice();
80            let xw = xp[wi];
81            let zw = zp[wi];
82            pw.phase ^= ((((xw ^ zw) & mask) != zero) as u8) << 1;
83        });
84    }
85
86    #[inline]
87    fn z(&mut self, index: usize) {
88        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
89        let wi = index / bits;
90        let off = index % bits;
91        let one = <T::Storage as BitView>::Store::one();
92        let zero = <T::Storage as BitView>::Store::zero();
93        let mask = one << off;
94        self.data.iter_mut().for_each(|pw| {
95            let xp = pw.word.xbits.data.as_raw_slice();
96            let xw = xp[wi];
97            pw.phase ^= (((xw & mask) != zero) as u8) << 1;
98        });
99    }
100
101    #[inline]
102    fn h(&mut self, index: usize) {
103        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
104        let wi = index / bits;
105        let off = index % bits;
106        let one = <T::Storage as BitView>::Store::one();
107        let zero = <T::Storage as BitView>::Store::zero();
108        let mask = one << off;
109        self.data.iter_mut().for_each(|pw| {
110            let xp = pw.word.xbits.data.as_raw_mut_slice();
111            let zp = pw.word.zbits.data.as_raw_mut_slice();
112            let xw = xp[wi];
113            let zw = zp[wi];
114            let xb = xw & mask;
115            let zb = zw & mask;
116            xp[wi] = (xw & !mask) | zb;
117            zp[wi] = (zw & !mask) | xb;
118            pw.phase ^= (((xb & zb) != zero) as u8) << 1;
119        });
120    }
121
122    #[inline]
123    fn s(&mut self, index: usize) {
124        // NOTE: S is the only clifford where forward and backward propagation
125        // differ since it's non-hermitian; only the phase rule differs.
126        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
127        let wi = index / bits;
128        let off = index % bits;
129        let one = <T::Storage as BitView>::Store::one();
130        let zero = <T::Storage as BitView>::Store::zero();
131        let mask = one << off;
132        self.data.iter_mut().for_each(|pw| {
133            let xp = pw.word.xbits.data.as_raw_slice();
134            let zp = pw.word.zbits.data.as_raw_mut_slice();
135            let xw = xp[wi];
136            let zw = zp[wi];
137            pw.phase ^= ((((xw & zw) & mask) != zero) as u8) << 1;
138            zp[wi] = zw ^ (xw & mask);
139        });
140    }
141
142    #[inline]
143    fn cnot(&mut self, control: usize, target: usize) {
144        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
145        let one = <T::Storage as BitView>::Store::one();
146        let zero = <T::Storage as BitView>::Store::zero();
147        let (wc, sc) = (control / bits, control % bits);
148        let (wt, st) = (target / bits, target % bits);
149        self.data.iter_mut().for_each(|pw| {
150            let xp = pw.word.xbits.data.as_raw_mut_slice();
151            let zp = pw.word.zbits.data.as_raw_mut_slice();
152            let xa = (xp[wc] >> sc) & one;
153            let za = (zp[wc] >> sc) & one;
154            let xb = (xp[wt] >> st) & one;
155            let zb = (zp[wt] >> st) & one;
156            let phase_flip = (xa & zb) & (xb ^ za ^ one);
157            pw.phase ^= (((phase_flip & one) != zero) as u8) << 1;
158            zp[wc] = zp[wc] ^ (zb << sc);
159            xp[wt] = xp[wt] ^ (xa << st);
160        });
161    }
162
163    #[inline]
164    fn cz(&mut self, control: usize, target: usize) {
165        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
166        let one = <T::Storage as BitView>::Store::one();
167        let zero = <T::Storage as BitView>::Store::zero();
168        let (wc, sc) = (control / bits, control % bits);
169        let (wt, st) = (target / bits, target % bits);
170        self.data.iter_mut().for_each(|pw| {
171            let xp = pw.word.xbits.data.as_raw_slice();
172            let zp = pw.word.zbits.data.as_raw_mut_slice();
173            let xa = (xp[wc] >> sc) & one;
174            let za = (zp[wc] >> sc) & one;
175            let xb = (xp[wt] >> st) & one;
176            let zb = (zp[wt] >> st) & one;
177            let phase_flip = (xa & xb) & (za ^ zb);
178            pw.phase ^= (((phase_flip & one) != zero) as u8) << 1;
179            zp[wc] = zp[wc] ^ (xb << sc);
180            zp[wt] = zp[wt] ^ (xa << st);
181        });
182    }
183}
184
185impl<T: Config> CliffordExtensions for Tableau<T>
186where
187    <T::Storage as BitView>::Store: PrimInt,
188{
189    // |    Gate    |  X  |  Y  |  Z  |
190    // |:----------:|:---:|:---:|:---:|
191    // |     s      |  Y  | -X  |  Z  |
192    // |   s_dag    | -Y  |  X  |  Z  |
193    // |   sqrt_x   |  X  |  Z  | -Y  |
194    // | sqrt_x_dag |  X  | -Z  |  Y  |
195    // |   sqrt_y   | -Z  |  Y  |  X  |
196    // | sqrt_y_dag |  Z  |  Y  | -X  |
197
198    #[inline]
199    fn s_dag(&mut self, index: usize) {
200        // NOTE: the backwards-prop version of S is just S†: same bit mapping,
201        // phase rule differs (flip where x & !z).
202        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
203        let wi = index / bits;
204        let off = index % bits;
205        let one = <T::Storage as BitView>::Store::one();
206        let zero = <T::Storage as BitView>::Store::zero();
207        let mask = one << off;
208        self.data.iter_mut().for_each(|pw| {
209            let xp = pw.word.xbits.data.as_raw_slice();
210            let zp = pw.word.zbits.data.as_raw_mut_slice();
211            let xw = xp[wi];
212            let zw = zp[wi];
213            pw.phase ^= ((((xw & !zw) & mask) != zero) as u8) << 1;
214            zp[wi] = zw ^ (xw & mask);
215        });
216    }
217
218    #[inline]
219    fn sqrt_x(&mut self, index: usize) {
220        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
221        let wi = index / bits;
222        let off = index % bits;
223        let one = <T::Storage as BitView>::Store::one();
224        let zero = <T::Storage as BitView>::Store::zero();
225        let mask = one << off;
226        self.data.iter_mut().for_each(|pw| {
227            let xp = pw.word.xbits.data.as_raw_mut_slice();
228            let zp = pw.word.zbits.data.as_raw_slice();
229            let xw = xp[wi];
230            let zw = zp[wi];
231            pw.phase ^= ((((zw & !xw) & mask) != zero) as u8) << 1;
232            xp[wi] = xw ^ (zw & mask);
233        });
234    }
235
236    #[inline]
237    fn sqrt_x_dag(&mut self, index: usize) {
238        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
239        let wi = index / bits;
240        let off = index % bits;
241        let one = <T::Storage as BitView>::Store::one();
242        let zero = <T::Storage as BitView>::Store::zero();
243        let mask = one << off;
244        self.data.iter_mut().for_each(|pw| {
245            let xp = pw.word.xbits.data.as_raw_mut_slice();
246            let zp = pw.word.zbits.data.as_raw_slice();
247            let xw = xp[wi];
248            let zw = zp[wi];
249            pw.phase ^= ((((xw & zw) & mask) != zero) as u8) << 1;
250            xp[wi] = xw ^ (zw & mask);
251        });
252    }
253
254    #[inline]
255    fn sqrt_y(&mut self, index: usize) {
256        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
257        let wi = index / bits;
258        let off = index % bits;
259        let one = <T::Storage as BitView>::Store::one();
260        let zero = <T::Storage as BitView>::Store::zero();
261        let mask = one << off;
262        self.data.iter_mut().for_each(|pw| {
263            let xp = pw.word.xbits.data.as_raw_mut_slice();
264            let zp = pw.word.zbits.data.as_raw_mut_slice();
265            let xw = xp[wi];
266            let zw = zp[wi];
267            let xb = xw & mask;
268            let zb = zw & mask;
269            xp[wi] = (xw & !mask) | zb;
270            zp[wi] = (zw & !mask) | xb;
271            pw.phase ^= ((((xw & !zw) & mask) != zero) as u8) << 1;
272        });
273    }
274
275    #[inline]
276    fn sqrt_y_dag(&mut self, index: usize) {
277        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
278        let wi = index / bits;
279        let off = index % bits;
280        let one = <T::Storage as BitView>::Store::one();
281        let zero = <T::Storage as BitView>::Store::zero();
282        let mask = one << off;
283        self.data.iter_mut().for_each(|pw| {
284            let xp = pw.word.xbits.data.as_raw_mut_slice();
285            let zp = pw.word.zbits.data.as_raw_mut_slice();
286            let xw = xp[wi];
287            let zw = zp[wi];
288            let xb = xw & mask;
289            let zb = zw & mask;
290            xp[wi] = (xw & !mask) | zb;
291            zp[wi] = (zw & !mask) | xb;
292            pw.phase ^= ((((zw & !xw) & mask) != zero) as u8) << 1;
293        });
294    }
295
296    // control: row, target: col
297    // | CY  |  I  |  X  |  Y  |  Z  |
298    // |:---:|:---:|:---:|:---:|:---:|
299    // |  I  | II  | ZX  | IY  | ZZ  |
300    // |  X  | XY  | -YZ | XI  | YX  |
301    // |  Y  | YY  | XZ  | YI  | -XX |
302    // |  Z  | ZI  | IX  | ZY  | IZ  |
303    //
304    // Bit transforms: xc'=xc, zc'=zc^xt^zt, xt'=xt^xc, zt'=zt^xc
305    // Phase +2 when: xc & (xt ^ zt) & !(zc ^ zt)
306    #[inline]
307    fn cy(&mut self, control: usize, target: usize) {
308        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
309        let one = <T::Storage as BitView>::Store::one();
310        let zero = <T::Storage as BitView>::Store::zero();
311        let (wc, sc) = (control / bits, control % bits);
312        let (wt, st) = (target / bits, target % bits);
313        self.data.iter_mut().for_each(|pw| {
314            let xp = pw.word.xbits.data.as_raw_mut_slice();
315            let zp = pw.word.zbits.data.as_raw_mut_slice();
316            let xc = (xp[wc] >> sc) & one;
317            let zc = (zp[wc] >> sc) & one;
318            let xt = (xp[wt] >> st) & one;
319            let zt = (zp[wt] >> st) & one;
320            let phase_flip = (xc & (xt ^ zt)) & (zc ^ zt ^ one);
321            pw.phase ^= (((phase_flip & one) != zero) as u8) << 1;
322            zp[wc] = zp[wc] ^ ((xt ^ zt) << sc);
323            xp[wt] = xp[wt] ^ (xc << st);
324            zp[wt] = zp[wt] ^ (xc << st);
325        });
326    }
327}
328
329impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> Clifford for GeneralizedTableau<T, I, C>
330where
331    Complex<<T as Config>::Coeff>: From<Complex<f64>>,
332    <T::Storage as BitView>::Store: PrimInt,
333{
334    impl_generalized_tableau_clifford!(x);
335    impl_generalized_tableau_clifford!(y);
336    impl_generalized_tableau_clifford!(z);
337    impl_generalized_tableau_clifford!(h);
338    impl_generalized_tableau_clifford!(s);
339    impl_generalized_tableau_clifford_pair!(cnot);
340    impl_generalized_tableau_clifford_pair!(cz);
341}
342
343impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> CliffordExtensions
344    for GeneralizedTableau<T, I, C>
345where
346    Complex<<T as Config>::Coeff>: From<Complex<f64>>,
347    <T::Storage as BitView>::Store: PrimInt,
348{
349    impl_generalized_tableau_clifford!(s_dag);
350    impl_generalized_tableau_clifford!(sqrt_x);
351    impl_generalized_tableau_clifford!(sqrt_x_dag);
352    impl_generalized_tableau_clifford!(sqrt_y);
353    impl_generalized_tableau_clifford!(sqrt_y_dag);
354    impl_generalized_tableau_clifford_pair!(cy);
355}
356
357/// How many sites a per-word mask covers — one bit per distinct index, so this
358/// equals the index count exactly when the indices are distinct.
359#[inline]
360fn mask_bits<S: PrimInt>(masks: &[S]) -> u64 {
361    masks.iter().map(|m| u64::from(m.count_ones())).sum()
362}
363
364/// Take the fused per-word masks, or bail out of the caller: `return` when there
365/// is nothing to do, and on a **repeated** index hand the whole batch to the
366/// gate's outlined `$fallback`.
367///
368/// A mask carries one bit per site, so a repeated index would make the fused
369/// sweep apply the gate once where the per-index loop — the `CliffordBatch`
370/// contract, and what a legal `X 0 0` in a `.stim` file means — conjugates by
371/// `Gᵏ`. XOR-cancelling a repeated bit would recover `Gᵏ` only when
372/// `G² = I` (`X`, `Y`, `Z`, `H`). For `S`, `√X`, `√Y`, even `k = 2` is
373/// another Pauli (`S² = Z`, `(√X)² = X`, `(√Y)² = Y`), so neither one bit
374/// nor an XOR-cancelled bit is right for every family; only the per-index
375/// loop is (G-061).
376///
377/// The fast path costs exactly one branch on a flag `build_masks` has already
378/// computed, and the slow path is a *call* — no scalar loop is inlined into the
379/// fused body. Spelling the fallback out here instead (so each gate inlined a
380/// whole `2n`-row sweep per index into its own preamble) cost 22–31% on the five
381/// gates that map the x/z bit planes (`h`, `√X`, `√X†`, `√Y`, `√Y†`) when it was
382/// measured on the `-2` crates: the bodies with real work to do lose registers
383/// and I-cache to code that never runs. The phase-only gates (`x`/`y`/`z`/`s`)
384/// never move, which is why A/B'ing only those rows hides it.
385macro_rules! masks_or_scalar_fallback {
386    ($self:ident, $indices:ident, $fallback:ident) => {
387        match $self.build_masks($indices) {
388            // Distinct indices: the fused sweep is licensed.
389            Some((masks, n_words, false)) => (masks, n_words),
390            // No rows or no indices: the batch is a no-op.
391            None => return,
392            // A repeated index: owe `Gᵏ`, i.e. the per-index loop.
393            Some((.., true)) => return $self.$fallback($indices),
394        }
395    };
396}
397
398/// Define the outlined per-index loops the batched gates fall back to on a
399/// repeated index (`$fallback` runs the canonical `$one` once per occurrence).
400///
401/// `#[cold]` + `#[inline(never)]` is the point: each loop is a full `2n`-row
402/// sweep per index, and keeping it out of line keeps it off the fused bodies'
403/// register budget and out of their cache lines — the caller keeps a call.
404macro_rules! repeated_index_loops {
405    ($($fallback:ident => $one:ident),+ $(,)?) => {
406        impl<T: Config> Tableau<T>
407        where
408            <T::Storage as BitView>::Store: PrimInt,
409        {
410            $(
411                #[cold]
412                #[inline(never)]
413                fn $fallback(&mut self, indices: &[usize]) {
414                    for &q in indices {
415                        self.$one(q);
416                    }
417                }
418            )+
419        }
420    };
421}
422
423repeated_index_loops! {
424    x_each => x,
425    y_each => y,
426    z_each => z,
427    h_each => h,
428    s_each => s,
429    s_dag_each => s_dag,
430    sqrt_x_each => sqrt_x,
431    sqrt_x_dag_each => sqrt_x_dag,
432    sqrt_y_each => sqrt_y,
433    sqrt_y_dag_each => sqrt_y_dag,
434}
435
436impl<T: Config> Tableau<T>
437where
438    <T::Storage as BitView>::Store: PrimInt,
439{
440    /// Build per-word bitmasks from a list of qubit indices.
441    /// Returns `(masks, n_words, repeated)`. Stack-allocates for up to 8 storage
442    /// words; spills to the heap beyond that, so there is no hard qubit cap.
443    ///
444    /// Distinctness is **checked**, not assumed, and checked here, in the third
445    /// tuple slot: the mask holds one bit per *distinct* index, so it is short of
446    /// `indices.len()` bits exactly when an index repeats. That is `n_words`
447    /// popcounts (typically one or two) *after* the index walk, which is left
448    /// byte-for-byte as it was — folding the test *into* the walk, as a
449    /// `repeated |= word & bit` accumulator, measured worse on the `-2` crates
450    /// (it adds a loop-carried dependency to the walk), and outlining this whole
451    /// function was worse again. `repeated` sends the caller, via
452    /// [`masks_or_scalar_fallback`], to its outlined per-index loop.
453    #[inline]
454    fn build_masks(&self, indices: &[usize]) -> Option<(MaskBuf<T>, usize, bool)> {
455        if self.data.is_empty() || indices.is_empty() {
456            return None;
457        }
458        let n_words = self.data[0].word.xbits.data.as_raw_slice().len();
459        let bits_per_word = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
460        let one = <T::Storage as BitView>::Store::one();
461        let zero = <T::Storage as BitView>::Store::zero();
462        let mut masks: MaskBuf<T> = smallvec![zero; n_words];
463        for &addr0 in indices {
464            masks[addr0 / bits_per_word] =
465                masks[addr0 / bits_per_word] | (one << (addr0 % bits_per_word));
466        }
467        let repeated = mask_bits(&masks) != indices.len() as u64;
468        Some((masks, n_words, repeated))
469    }
470}
471
472impl<T: Config> CliffordBatch for Tableau<T>
473where
474    <T::Storage as BitView>::Store: PrimInt,
475{
476    /// `X` is bit-preserving: phase flips for each masked qubit where z=1.
477    #[inline]
478    fn x_many(&mut self, indices: &[usize]) {
479        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, x_each);
480        let zero = <T::Storage as BitView>::Store::zero();
481
482        self.data.iter_mut().for_each(|pw| {
483            let zp = pw.word.zbits.data.as_raw_slice();
484            let mut popcount = 0u32;
485            for wi in 0..n_words {
486                let mask = masks[wi];
487                if mask == zero {
488                    continue;
489                }
490                popcount += (zp[wi] & mask).count_ones();
491            }
492            pw.phase ^= ((popcount & 1) as u8) << 1;
493        });
494    }
495
496    /// `Y` is bit-preserving: phase flips for each masked qubit where x⊕z=1.
497    #[inline]
498    fn y_many(&mut self, indices: &[usize]) {
499        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, y_each);
500        let zero = <T::Storage as BitView>::Store::zero();
501
502        self.data.iter_mut().for_each(|pw| {
503            let xp = pw.word.xbits.data.as_raw_slice();
504            let zp = pw.word.zbits.data.as_raw_slice();
505            let mut popcount = 0u32;
506            for wi in 0..n_words {
507                let mask = masks[wi];
508                if mask == zero {
509                    continue;
510                }
511                popcount += ((xp[wi] ^ zp[wi]) & mask).count_ones();
512            }
513            pw.phase ^= ((popcount & 1) as u8) << 1;
514        });
515    }
516
517    /// `Z` is bit-preserving: phase flips for each masked qubit where x=1.
518    #[inline]
519    fn z_many(&mut self, indices: &[usize]) {
520        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, z_each);
521        let zero = <T::Storage as BitView>::Store::zero();
522
523        self.data.iter_mut().for_each(|pw| {
524            let xp = pw.word.xbits.data.as_raw_slice();
525            let mut popcount = 0u32;
526            for wi in 0..n_words {
527                let mask = masks[wi];
528                if mask == zero {
529                    continue;
530                }
531                popcount += (xp[wi] & mask).count_ones();
532            }
533            pw.phase ^= ((popcount & 1) as u8) << 1;
534        });
535    }
536
537    /// Forward `S`: phase flips where x&z=1, then z ^= x for masked qubits.
538    #[inline]
539    fn s_many(&mut self, indices: &[usize]) {
540        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, s_each);
541        let zero = <T::Storage as BitView>::Store::zero();
542
543        self.data.iter_mut().for_each(|pw| {
544            let xp = pw.word.xbits.data.as_raw_mut_slice();
545            let zp = pw.word.zbits.data.as_raw_mut_slice();
546            let mut popcount = 0u32;
547            for wi in 0..n_words {
548                let mask = masks[wi];
549                if mask == zero {
550                    continue;
551                }
552                let xw = xp[wi];
553                let zw = zp[wi];
554                popcount += ((xw & zw) & mask).count_ones();
555                zp[wi] = zw ^ (xw & mask);
556            }
557            pw.phase ^= ((popcount & 1) as u8) << 1;
558        });
559    }
560
561    /// Apply CNOT to many pairs, operating on raw storage words to avoid
562    /// per-bit `bitvec` addressing. Pairs are applied sequentially per row,
563    /// so semantics match the per-pair `cnot` loop exactly.
564    fn cnot_many(&mut self, pairs: &[(usize, usize)]) {
565        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
566        let one = <T::Storage as BitView>::Store::one();
567        let zero = <T::Storage as BitView>::Store::zero();
568
569        self.data.iter_mut().for_each(|pw| {
570            let xp = pw.word.xbits.data.as_raw_mut_slice();
571            let zp = pw.word.zbits.data.as_raw_mut_slice();
572            let mut phase_flips = zero;
573            for &(control, target) in pairs {
574                let (wc, sc) = (control / bits, control % bits);
575                let (wt, st) = (target / bits, target % bits);
576                let xa = (xp[wc] >> sc) & one;
577                let za = (zp[wc] >> sc) & one;
578                let xb = (xp[wt] >> st) & one;
579                let zb = (zp[wt] >> st) & one;
580                // +2 phase when x_a & z_b & !(x_b ^ z_a); 2+2 == 0 mod 4 so XOR-accumulate
581                phase_flips = phase_flips ^ ((xa & zb) & (xb ^ za ^ one));
582                // z_a ^= z_b ; x_b ^= x_a
583                zp[wc] = zp[wc] ^ (zb << sc);
584                xp[wt] = xp[wt] ^ (xa << st);
585            }
586            pw.phase ^= (((phase_flips & one) != zero) as u8) << 1;
587        });
588    }
589
590    /// Apply `H` to multiple qubits using combined bitmask operations.
591    /// H swaps x<->z bits (same as sqrt_y) but with different phase:
592    /// phase += 2 when x=1 & z=1 (Y goes to -Y).
593    #[inline]
594    fn h_many(&mut self, indices: &[usize]) {
595        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, h_each);
596        let zero = <T::Storage as BitView>::Store::zero();
597
598        self.data.iter_mut().for_each(|pw| {
599            let xp = pw.word.xbits.data.as_raw_mut_slice();
600            let zp = pw.word.zbits.data.as_raw_mut_slice();
601            for wi in 0..n_words {
602                let mask = masks[wi];
603                if mask == zero {
604                    continue;
605                }
606                let not_mask = !mask;
607                let xw = xp[wi];
608                let zw = zp[wi];
609                let x_bits = xw & mask;
610                let z_bits = zw & mask;
611                xp[wi] = (xw & not_mask) | z_bits;
612                zp[wi] = (zw & not_mask) | x_bits;
613                let phase_bits = x_bits & z_bits;
614                pw.phase ^= ((phase_bits.count_ones() & 1) as u8) << 1;
615            }
616        });
617    }
618
619    /// Apply CZ to many pairs on raw storage words. CZ is symmetric and touches
620    /// only z-bits; pairs are applied sequentially per row, so semantics match
621    /// the per-pair `cz` loop exactly.
622    fn cz_many(&mut self, pairs: &[(usize, usize)]) {
623        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
624        let one = <T::Storage as BitView>::Store::one();
625        let zero = <T::Storage as BitView>::Store::zero();
626
627        self.data.iter_mut().for_each(|pw| {
628            let xp = pw.word.xbits.data.as_raw_slice();
629            let zp = pw.word.zbits.data.as_raw_mut_slice();
630            let mut phase_flips = zero;
631            for &(control, target) in pairs {
632                let (wc, sc) = (control / bits, control % bits);
633                let (wt, st) = (target / bits, target % bits);
634                let xa = (xp[wc] >> sc) & one;
635                let za = (zp[wc] >> sc) & one;
636                let xb = (xp[wt] >> st) & one;
637                let zb = (zp[wt] >> st) & one;
638                // +2 phase when x_a & x_b & (z_a ^ z_b)
639                phase_flips = phase_flips ^ ((xa & xb) & (za ^ zb));
640                // z_a ^= x_b ; z_b ^= x_a
641                zp[wc] = zp[wc] ^ (xb << sc);
642                zp[wt] = zp[wt] ^ (xa << st);
643            }
644            pw.phase ^= (((phase_flips & one) != zero) as u8) << 1;
645        });
646    }
647}
648
649impl<T: Config> CliffordExtensionsBatch for Tableau<T>
650where
651    <T::Storage as BitView>::Store: PrimInt,
652{
653    /// Backward `S` (i.e. `S†`): same bit mapping as `S`, phase rule differs.
654    /// Phase flips where x&!z=1, then z ^= x for masked qubits.
655    #[inline]
656    fn s_dag_many(&mut self, indices: &[usize]) {
657        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, s_dag_each);
658        let zero = <T::Storage as BitView>::Store::zero();
659
660        self.data.iter_mut().for_each(|pw| {
661            let xp = pw.word.xbits.data.as_raw_mut_slice();
662            let zp = pw.word.zbits.data.as_raw_mut_slice();
663            let mut popcount = 0u32;
664            for wi in 0..n_words {
665                let mask = masks[wi];
666                if mask == zero {
667                    continue;
668                }
669                let xw = xp[wi];
670                let zw = zp[wi];
671                popcount += ((xw & !zw) & mask).count_ones();
672                zp[wi] = zw ^ (xw & mask);
673            }
674            pw.phase ^= ((popcount & 1) as u8) << 1;
675        });
676    }
677
678    /// Apply CY to many pairs on raw storage words. Pairs are applied
679    /// sequentially per row, so semantics match the per-pair `cy` loop exactly.
680    fn cy_many(&mut self, pairs: &[(usize, usize)]) {
681        let bits = std::mem::size_of::<<T::Storage as BitView>::Store>() * 8;
682        let one = <T::Storage as BitView>::Store::one();
683        let zero = <T::Storage as BitView>::Store::zero();
684
685        self.data.iter_mut().for_each(|pw| {
686            let xp = pw.word.xbits.data.as_raw_mut_slice();
687            let zp = pw.word.zbits.data.as_raw_mut_slice();
688            let mut phase_flips = zero;
689            for &(control, target) in pairs {
690                let (wc, sc) = (control / bits, control % bits);
691                let (wt, st) = (target / bits, target % bits);
692                let xc = (xp[wc] >> sc) & one;
693                let zc = (zp[wc] >> sc) & one;
694                let xt = (xp[wt] >> st) & one;
695                let zt = (zp[wt] >> st) & one;
696                // +2 phase when x_c & (x_t ^ z_t) & !(z_c ^ z_t)
697                phase_flips = phase_flips ^ ((xc & (xt ^ zt)) & (zc ^ zt ^ one));
698                // z_c ^= x_t ^ z_t ; x_t ^= x_c ; z_t ^= x_c
699                zp[wc] = zp[wc] ^ ((xt ^ zt) << sc);
700                xp[wt] = xp[wt] ^ (xc << st);
701                zp[wt] = zp[wt] ^ (xc << st);
702            }
703            pw.phase ^= (((phase_flips & one) != zero) as u8) << 1;
704        });
705    }
706
707    /// Apply `√Y` to multiple qubits using combined bitmask operations.
708    /// All qubits targeting the same word are merged into a single mask,
709    /// reducing N individual operations to O(n_words) per row.
710    #[inline]
711    fn sqrt_y_many(&mut self, indices: &[usize]) {
712        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, sqrt_y_each);
713        let zero = <T::Storage as BitView>::Store::zero();
714
715        self.data.iter_mut().for_each(|pw| {
716            let xp = pw.word.xbits.data.as_raw_mut_slice();
717            let zp = pw.word.zbits.data.as_raw_mut_slice();
718            for wi in 0..n_words {
719                let mask = masks[wi];
720                if mask == zero {
721                    continue;
722                }
723                let not_mask = !mask;
724                let xw = xp[wi];
725                let zw = zp[wi];
726                let x_bits = xw & mask;
727                let z_bits = zw & mask;
728                xp[wi] = (xw & not_mask) | z_bits;
729                zp[wi] = (zw & not_mask) | x_bits;
730                let phase_bits = x_bits & !z_bits;
731                pw.phase ^= ((phase_bits.count_ones() & 1) as u8) << 1;
732            }
733        });
734    }
735
736    /// Apply `(√Y)†` to multiple qubits using combined bitmask operations.
737    #[inline]
738    fn sqrt_y_dag_many(&mut self, indices: &[usize]) {
739        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, sqrt_y_dag_each);
740        let zero = <T::Storage as BitView>::Store::zero();
741
742        self.data.iter_mut().for_each(|pw| {
743            let xp = pw.word.xbits.data.as_raw_mut_slice();
744            let zp = pw.word.zbits.data.as_raw_mut_slice();
745            for wi in 0..n_words {
746                let mask = masks[wi];
747                if mask == zero {
748                    continue;
749                }
750                let not_mask = !mask;
751                let xw = xp[wi];
752                let zw = zp[wi];
753                let x_bits = xw & mask;
754                let z_bits = zw & mask;
755                xp[wi] = (xw & not_mask) | z_bits;
756                zp[wi] = (zw & not_mask) | x_bits;
757                let phase_bits = z_bits & !x_bits;
758                pw.phase ^= ((phase_bits.count_ones() & 1) as u8) << 1;
759            }
760        });
761    }
762
763    /// Apply `√X` to multiple qubits using combined bitmask operations.
764    #[inline]
765    fn sqrt_x_many(&mut self, indices: &[usize]) {
766        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, sqrt_x_each);
767        let zero = <T::Storage as BitView>::Store::zero();
768
769        self.data.iter_mut().for_each(|pw| {
770            let xp = pw.word.xbits.data.as_raw_mut_slice();
771            let zp = pw.word.zbits.data.as_raw_mut_slice();
772            for wi in 0..n_words {
773                let mask = masks[wi];
774                if mask == zero {
775                    continue;
776                }
777                let xw = xp[wi];
778                let zw = zp[wi];
779                let phase_bits = (zw & !xw) & mask;
780                pw.phase ^= ((phase_bits.count_ones() & 1) as u8) << 1;
781                xp[wi] = xw ^ (zw & mask);
782            }
783        });
784    }
785
786    /// Apply `(√X)†` to multiple qubits using combined bitmask operations.
787    #[inline]
788    fn sqrt_x_dag_many(&mut self, indices: &[usize]) {
789        let (masks, n_words) = masks_or_scalar_fallback!(self, indices, sqrt_x_dag_each);
790        let zero = <T::Storage as BitView>::Store::zero();
791
792        self.data.iter_mut().for_each(|pw| {
793            let xp = pw.word.xbits.data.as_raw_mut_slice();
794            let zp = pw.word.zbits.data.as_raw_mut_slice();
795            for wi in 0..n_words {
796                let mask = masks[wi];
797                if mask == zero {
798                    continue;
799                }
800                let xw = xp[wi];
801                let zw = zp[wi];
802                let phase_bits = (xw & zw) & mask;
803                pw.phase ^= ((phase_bits.count_ones() & 1) as u8) << 1;
804                xp[wi] = xw ^ (zw & mask);
805            }
806        });
807    }
808}
809
810macro_rules! impl_gen_tableau_batch_single {
811    ($name:ident) => {
812        fn $name(&mut self, indices: &[usize]) {
813            if !self.any_inactive(indices) {
814                self.tableau.$name(indices);
815                return;
816            }
817            let filtered: Vec<usize> = indices
818                .iter()
819                .copied()
820                .filter(|&i| !self.is_inactive(i))
821                .collect();
822            self.tableau.$name(&filtered);
823        }
824    };
825}
826
827macro_rules! impl_gen_tableau_batch_pair {
828    ($name:ident) => {
829        fn $name(&mut self, pairs: &[(usize, usize)]) {
830            if !self.any_inactive_pair(pairs) {
831                self.tableau.$name(pairs);
832                return;
833            }
834            let filtered: Vec<(usize, usize)> = pairs
835                .iter()
836                .copied()
837                .filter(|&(c, t)| !self.is_inactive(c) && !self.is_inactive(t))
838                .collect();
839            self.tableau.$name(&filtered);
840        }
841    };
842}
843
844impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> CliffordBatch
845    for GeneralizedTableau<T, I, C>
846where
847    Complex<<T as Config>::Coeff>: From<Complex<f64>>,
848    <T::Storage as BitView>::Store: PrimInt,
849{
850    impl_gen_tableau_batch_single!(x_many);
851    impl_gen_tableau_batch_single!(y_many);
852    impl_gen_tableau_batch_single!(z_many);
853    impl_gen_tableau_batch_single!(h_many);
854    impl_gen_tableau_batch_single!(s_many);
855    impl_gen_tableau_batch_pair!(cnot_many);
856    impl_gen_tableau_batch_pair!(cz_many);
857}
858
859impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> CliffordExtensionsBatch
860    for GeneralizedTableau<T, I, C>
861where
862    Complex<<T as Config>::Coeff>: From<Complex<f64>>,
863    <T::Storage as BitView>::Store: PrimInt,
864{
865    impl_gen_tableau_batch_single!(s_dag_many);
866    impl_gen_tableau_batch_single!(sqrt_x_many);
867    impl_gen_tableau_batch_single!(sqrt_x_dag_many);
868    impl_gen_tableau_batch_single!(sqrt_y_many);
869    impl_gen_tableau_batch_single!(sqrt_y_dag_many);
870    impl_gen_tableau_batch_pair!(cy_many);
871}
872
873#[cfg(test)]
874mod tests {
875    use super::*;
876    use ppvm_pauli_sum::config::fxhash::ByteF64;
877
878    type TestConfig = ByteF64<1>;
879    type TestTableau = GeneralizedTableau<TestConfig>;
880
881    /// Returns (xbit, zbit, phase) for each tableau row: (destabilizer, stabilizer).
882    fn rows(tab: &TestTableau) -> [(bool, bool, u8); 2] {
883        [0, 1].map(|i| {
884            let pw = &tab.tableau.data[i];
885            (pw.word.xbits[0], pw.word.zbits[0], pw.phase)
886        })
887    }
888
889    // Initial |0⟩: destabilizer = X (1,0,0), stabilizer = Z (0,1,0)
890
891    #[test]
892    fn test_sqrt_x_stabilizer() {
893        // Z → -Y: forward prop √X P √X†
894        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
895        tab.sqrt_x(0);
896        let r = rows(&tab);
897        assert_eq!(r[0], (true, false, 0), "destabilizer X should stay X");
898        assert_eq!(r[1], (true, true, 2), "stabilizer Z should become -Y");
899    }
900
901    #[test]
902    fn test_sqrt_x_dag_stabilizer() {
903        // Z → +Y
904        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
905        tab.sqrt_x_dag(0);
906        let r = rows(&tab);
907        assert_eq!(r[0], (true, false, 0), "destabilizer X should stay X");
908        assert_eq!(r[1], (true, true, 0), "stabilizer Z should become +Y");
909    }
910
911    #[test]
912    fn test_sqrt_y_stabilizer() {
913        // Z → +X, X → -Z
914        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
915        tab.sqrt_y(0);
916        let r = rows(&tab);
917        assert_eq!(r[0], (false, true, 2), "destabilizer X should become -Z");
918        assert_eq!(r[1], (true, false, 0), "stabilizer Z should become +X");
919    }
920
921    #[test]
922    fn test_sqrt_y_dag_stabilizer() {
923        // Z → -X, X → +Z
924        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
925        tab.sqrt_y_dag(0);
926        let r = rows(&tab);
927        assert_eq!(r[0], (false, true, 0), "destabilizer X should become +Z");
928        assert_eq!(r[1], (true, false, 2), "stabilizer Z should become -X");
929    }
930
931    #[test]
932    fn test_sqrt_x_round_trip() {
933        let initial = rows(&GeneralizedTableau::new(1, 1e-12));
934        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
935        tab.sqrt_x(0);
936        tab.sqrt_x_dag(0);
937        assert_eq!(rows(&tab), initial);
938    }
939
940    #[test]
941    fn test_sqrt_y_round_trip() {
942        let initial = rows(&GeneralizedTableau::new(1, 1e-12));
943        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
944        tab.sqrt_y(0);
945        tab.sqrt_y_dag(0);
946        assert_eq!(rows(&tab), initial);
947    }
948
949    #[test]
950    fn test_sqrt_x_fourth_power_is_identity() {
951        let initial = rows(&GeneralizedTableau::new(1, 1e-12));
952        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
953        for _ in 0..4 {
954            tab.sqrt_x(0);
955        }
956        assert_eq!(rows(&tab), initial);
957    }
958
959    #[test]
960    fn test_sqrt_y_fourth_power_is_identity() {
961        let initial = rows(&GeneralizedTableau::new(1, 1e-12));
962        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
963        for _ in 0..4 {
964            tab.sqrt_y(0);
965        }
966        assert_eq!(rows(&tab), initial);
967    }
968
969    #[test]
970    fn test_sqrt_x_on_lost_qubit_is_noop() {
971        let initial = rows(&GeneralizedTableau::new(1, 1e-12));
972        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
973        tab.qubit_status[0] = QubitStatus::Lost;
974        tab.sqrt_x(0);
975        assert_eq!(rows(&tab), initial);
976    }
977
978    #[test]
979    fn test_sqrt_y_on_lost_qubit_is_noop() {
980        let initial = rows(&GeneralizedTableau::new(1, 1e-12));
981        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
982        tab.qubit_status[0] = QubitStatus::Lost;
983        tab.sqrt_y(0);
984        assert_eq!(rows(&tab), initial);
985    }
986
987    /// Returns (x0, z0, x1, z1, phase) for each of the 4 tableau rows of a 2-qubit tableau.
988    fn rows2(tab: &GeneralizedTableau<TestConfig>) -> [(bool, bool, bool, bool, u8); 4] {
989        [0, 1, 2, 3].map(|i| {
990            let pw = &tab.tableau.data[i];
991            (
992                pw.word.xbits[0],
993                pw.word.zbits[0],
994                pw.word.xbits[1],
995                pw.word.zbits[1],
996                pw.phase,
997            )
998        })
999    }
1000
1001    #[test]
1002    fn test_cy_stabilizers() {
1003        // CY (control=0, target=1) forward-propagates Paulis as CY P CY†.
1004        // From the truth table: xc'=xc, zc'=zc^xt^zt, xt'=xt^xc, zt'=zt^xc.
1005        // Phase +2 when xc & (xt^zt) & !(zc^zt), i.e. only for XX→-YZ and YZ→-XX.
1006        //   XI → +XY  (xt'=0^1=1, zt'=0^1=1; no phase since xt^zt=0)
1007        //   IX →  ZX  (zc'=0^1^0=1, xt'=1^0=1; no phase since xc=0)
1008        //   ZI →  ZI  (zc'=1^0^0=1; no phase)
1009        //   IZ →  ZZ  (zc'=0^0^1=1; no phase since xc=0)
1010        let mut tab: GeneralizedTableau<TestConfig> = GeneralizedTableau::new(2, 1e-12);
1011        tab.cy(0, 1);
1012        let r = rows2(&tab);
1013        assert_eq!(r[0], (true, false, true, true, 0), "XI should become +XY");
1014        assert_eq!(r[1], (false, true, true, false, 0), "IX should become ZX");
1015        assert_eq!(r[2], (false, true, false, false, 0), "ZI should stay ZI");
1016        assert_eq!(r[3], (false, true, false, true, 0), "IZ should become ZZ");
1017    }
1018
1019    #[test]
1020    fn test_cy_round_trip() {
1021        // CY is self-inverse: CY² = I
1022        let initial = rows2(&GeneralizedTableau::new(2, 1e-12));
1023        let mut tab: GeneralizedTableau<TestConfig> = GeneralizedTableau::new(2, 1e-12);
1024        tab.cy(0, 1);
1025        tab.cy(0, 1);
1026        assert_eq!(rows2(&tab), initial);
1027    }
1028
1029    // ---- Batch method tests ----
1030
1031    mod batch_tests {
1032        use super::*;
1033        use ppvm_pauli_sum::config::fxhash::ByteF64;
1034
1035        type TC = ByteF64<2>; // 2 u8 words = up to 16 qubits
1036        type TTab = Tableau<TC>;
1037
1038        /// Helper: extract all (xbits_raw, zbits_raw, phase) from a Tableau.
1039        fn snapshot(tab: &TTab) -> Vec<(Vec<u8>, Vec<u8>, u8)> {
1040            tab.data
1041                .iter()
1042                .map(|pw| {
1043                    (
1044                        pw.word.xbits.data.as_raw_slice().to_vec(),
1045                        pw.word.zbits.data.as_raw_slice().to_vec(),
1046                        pw.phase,
1047                    )
1048                })
1049                .collect()
1050        }
1051
1052        /// Apply individual gate calls and return the resulting snapshot.
1053        fn apply_individual_sqrt_y(n: usize, indices: &[usize]) -> Vec<(Vec<u8>, Vec<u8>, u8)> {
1054            let mut tab = TTab::new(n);
1055            // Put tableau in a non-trivial state
1056            tab.h(0);
1057            tab.h(3);
1058            tab.s(1);
1059            for &i in indices {
1060                tab.sqrt_y(i);
1061            }
1062            snapshot(&tab)
1063        }
1064
1065        fn apply_batch_sqrt_y(n: usize, indices: &[usize]) -> Vec<(Vec<u8>, Vec<u8>, u8)> {
1066            let mut tab = TTab::new(n);
1067            tab.h(0);
1068            tab.h(3);
1069            tab.s(1);
1070            tab.sqrt_y_many(indices);
1071            snapshot(&tab)
1072        }
1073
1074        #[test]
1075        fn test_sqrt_y_many_matches_individual() {
1076            let n = 8;
1077            let indices = vec![0, 2, 5, 7];
1078            assert_eq!(
1079                apply_individual_sqrt_y(n, &indices),
1080                apply_batch_sqrt_y(n, &indices)
1081            );
1082        }
1083
1084        #[test]
1085        fn test_sqrt_y_dag_many_matches_individual() {
1086            let n = 8;
1087            let indices = vec![1, 3, 4, 6];
1088            let mut tab_ind = TTab::new(n);
1089            tab_ind.h(0);
1090            tab_ind.s(2);
1091            for &i in &indices {
1092                tab_ind.sqrt_y_dag(i);
1093            }
1094            let mut tab_batch = TTab::new(n);
1095            tab_batch.h(0);
1096            tab_batch.s(2);
1097            tab_batch.sqrt_y_dag_many(&indices);
1098            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1099        }
1100
1101        #[test]
1102        fn test_sqrt_x_many_matches_individual() {
1103            let n = 8;
1104            let indices = vec![0, 1, 4, 7];
1105            let mut tab_ind = TTab::new(n);
1106            tab_ind.h(2);
1107            tab_ind.s(5);
1108            for &i in &indices {
1109                tab_ind.sqrt_x(i);
1110            }
1111            let mut tab_batch = TTab::new(n);
1112            tab_batch.h(2);
1113            tab_batch.s(5);
1114            tab_batch.sqrt_x_many(&indices);
1115            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1116        }
1117
1118        #[test]
1119        fn test_sqrt_x_dag_many_matches_individual() {
1120            let n = 8;
1121            let indices = vec![2, 3, 5, 6];
1122            let mut tab_ind = TTab::new(n);
1123            tab_ind.h(1);
1124            tab_ind.s(4);
1125            for &i in &indices {
1126                tab_ind.sqrt_x_dag(i);
1127            }
1128            let mut tab_batch = TTab::new(n);
1129            tab_batch.h(1);
1130            tab_batch.s(4);
1131            tab_batch.sqrt_x_dag_many(&indices);
1132            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1133        }
1134
1135        #[test]
1136        fn test_h_many_matches_individual() {
1137            let n = 8;
1138            let indices = vec![0, 3, 5, 7];
1139            let mut tab_ind = TTab::new(n);
1140            tab_ind.s(1);
1141            tab_ind.sqrt_y(2);
1142            for &i in &indices {
1143                tab_ind.h(i);
1144            }
1145            let mut tab_batch = TTab::new(n);
1146            tab_batch.s(1);
1147            tab_batch.sqrt_y(2);
1148            tab_batch.h_many(&indices);
1149            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1150        }
1151
1152        #[test]
1153        fn test_cz_many_matches_individual() {
1154            let n = 8;
1155            let pairs = vec![(0, 1), (2, 3), (4, 5)];
1156            let mut tab_ind = TTab::new(n);
1157            tab_ind.h(0);
1158            tab_ind.h(2);
1159            tab_ind.h(4);
1160            for &(c, t) in &pairs {
1161                tab_ind.cz(c, t);
1162            }
1163            let mut tab_batch = TTab::new(n);
1164            tab_batch.h(0);
1165            tab_batch.h(2);
1166            tab_batch.h(4);
1167            tab_batch.cz_many(&pairs);
1168            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1169        }
1170
1171        #[test]
1172        fn test_cz_many_cross_word() {
1173            // Pairs straddle the two storage words (qubits 0..8 and 8..16).
1174            let n = 16;
1175            let pairs = vec![(1, 9), (8, 2), (7, 15), (10, 0)];
1176            let setup = [0usize, 7, 8, 9, 15];
1177            let mut tab_ind = TTab::new(n);
1178            for &q in &setup {
1179                tab_ind.h(q);
1180            }
1181            tab_ind.s(2);
1182            for &(c, t) in &pairs {
1183                tab_ind.cz(c, t);
1184            }
1185            let mut tab_batch = TTab::new(n);
1186            for &q in &setup {
1187                tab_batch.h(q);
1188            }
1189            tab_batch.s(2);
1190            tab_batch.cz_many(&pairs);
1191            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1192        }
1193
1194        #[test]
1195        fn test_batch_empty_indices() {
1196            let n = 4;
1197            let initial = {
1198                let tab = TTab::new(n);
1199                snapshot(&tab)
1200            };
1201            let mut tab = TTab::new(n);
1202            tab.sqrt_y_many(&[]);
1203            assert_eq!(snapshot(&tab), initial);
1204            tab.sqrt_x_many(&[]);
1205            assert_eq!(snapshot(&tab), initial);
1206            tab.h_many(&[]);
1207            assert_eq!(snapshot(&tab), initial);
1208            tab.x_many(&[]);
1209            assert_eq!(snapshot(&tab), initial);
1210            tab.y_many(&[]);
1211            assert_eq!(snapshot(&tab), initial);
1212            tab.z_many(&[]);
1213            assert_eq!(snapshot(&tab), initial);
1214            tab.s_many(&[]);
1215            assert_eq!(snapshot(&tab), initial);
1216            tab.s_dag_many(&[]);
1217            assert_eq!(snapshot(&tab), initial);
1218        }
1219
1220        #[test]
1221        fn test_batch_all_qubits() {
1222            let n = 8;
1223            let all: Vec<usize> = (0..n).collect();
1224            let mut tab_ind = TTab::new(n);
1225            for &i in &all {
1226                tab_ind.sqrt_y(i);
1227            }
1228            let mut tab_batch = TTab::new(n);
1229            tab_batch.sqrt_y_many(&all);
1230            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1231        }
1232
1233        #[test]
1234        fn test_batch_round_trip() {
1235            let n = 8;
1236            let indices = vec![1, 3, 5, 7];
1237            let initial = {
1238                let tab = TTab::new(n);
1239                snapshot(&tab)
1240            };
1241            let mut tab = TTab::new(n);
1242            tab.sqrt_y_many(&indices);
1243            tab.sqrt_y_dag_many(&indices);
1244            assert_eq!(snapshot(&tab), initial);
1245        }
1246
1247        #[test]
1248        fn test_x_many_matches_individual() {
1249            let n = 8;
1250            let indices = vec![0, 2, 5, 7];
1251            let mut tab_ind = TTab::new(n);
1252            tab_ind.h(0);
1253            tab_ind.s(3);
1254            for &i in &indices {
1255                tab_ind.x(i);
1256            }
1257            let mut tab_batch = TTab::new(n);
1258            tab_batch.h(0);
1259            tab_batch.s(3);
1260            tab_batch.x_many(&indices);
1261            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1262        }
1263
1264        #[test]
1265        fn test_y_many_matches_individual() {
1266            let n = 8;
1267            let indices = vec![1, 3, 4, 6];
1268            let mut tab_ind = TTab::new(n);
1269            tab_ind.h(0);
1270            tab_ind.s(2);
1271            for &i in &indices {
1272                tab_ind.y(i);
1273            }
1274            let mut tab_batch = TTab::new(n);
1275            tab_batch.h(0);
1276            tab_batch.s(2);
1277            tab_batch.y_many(&indices);
1278            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1279        }
1280
1281        #[test]
1282        fn test_z_many_matches_individual() {
1283            let n = 8;
1284            let indices = vec![0, 1, 4, 7];
1285            let mut tab_ind = TTab::new(n);
1286            tab_ind.h(2);
1287            tab_ind.s(5);
1288            for &i in &indices {
1289                tab_ind.z(i);
1290            }
1291            let mut tab_batch = TTab::new(n);
1292            tab_batch.h(2);
1293            tab_batch.s(5);
1294            tab_batch.z_many(&indices);
1295            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1296        }
1297
1298        #[test]
1299        fn test_s_many_matches_individual() {
1300            let n = 8;
1301            let indices = vec![0, 2, 5, 7];
1302            let mut tab_ind = TTab::new(n);
1303            tab_ind.h(1);
1304            tab_ind.h(4);
1305            for &i in &indices {
1306                tab_ind.s(i);
1307            }
1308            let mut tab_batch = TTab::new(n);
1309            tab_batch.h(1);
1310            tab_batch.h(4);
1311            tab_batch.s_many(&indices);
1312            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1313        }
1314
1315        #[test]
1316        fn test_s_dag_many_matches_individual() {
1317            let n = 8;
1318            let indices = vec![1, 3, 4, 6];
1319            let mut tab_ind = TTab::new(n);
1320            tab_ind.h(2);
1321            tab_ind.h(5);
1322            for &i in &indices {
1323                tab_ind.s_dag(i);
1324            }
1325            let mut tab_batch = TTab::new(n);
1326            tab_batch.h(2);
1327            tab_batch.h(5);
1328            tab_batch.s_dag_many(&indices);
1329            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1330        }
1331
1332        #[test]
1333        fn test_cnot_many_matches_individual() {
1334            let n = 8;
1335            let pairs = vec![(0, 1), (2, 3), (4, 5)];
1336            let mut tab_ind = TTab::new(n);
1337            tab_ind.h(0);
1338            tab_ind.h(2);
1339            tab_ind.h(4);
1340            for &(c, t) in &pairs {
1341                tab_ind.cnot(c, t);
1342            }
1343            let mut tab_batch = TTab::new(n);
1344            tab_batch.h(0);
1345            tab_batch.h(2);
1346            tab_batch.h(4);
1347            tab_batch.cnot_many(&pairs);
1348            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1349        }
1350
1351        #[test]
1352        fn test_cnot_many_cross_word() {
1353            // Pairs straddle the two storage words (qubits 0..8 and 8..16).
1354            let n = 16;
1355            let pairs = vec![(1, 9), (8, 2), (7, 15), (10, 0)];
1356            let setup = [0usize, 7, 8, 9, 15];
1357            let mut tab_ind = TTab::new(n);
1358            for &q in &setup {
1359                tab_ind.h(q);
1360            }
1361            tab_ind.s(2);
1362            for &(c, t) in &pairs {
1363                tab_ind.cnot(c, t);
1364            }
1365            let mut tab_batch = TTab::new(n);
1366            for &q in &setup {
1367                tab_batch.h(q);
1368            }
1369            tab_batch.s(2);
1370            tab_batch.cnot_many(&pairs);
1371            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1372        }
1373
1374        #[test]
1375        fn test_cy_many_matches_individual() {
1376            let n = 8;
1377            let pairs = vec![(0, 1), (2, 3), (4, 5)];
1378            let mut tab_ind = TTab::new(n);
1379            tab_ind.h(0);
1380            tab_ind.h(2);
1381            tab_ind.s(4);
1382            for &(c, t) in &pairs {
1383                tab_ind.cy(c, t);
1384            }
1385            let mut tab_batch = TTab::new(n);
1386            tab_batch.h(0);
1387            tab_batch.h(2);
1388            tab_batch.s(4);
1389            tab_batch.cy_many(&pairs);
1390            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1391        }
1392
1393        #[test]
1394        fn test_cy_many_cross_word() {
1395            // Pairs straddle the two storage words (qubits 0..8 and 8..16).
1396            let n = 16;
1397            let pairs = vec![(1, 9), (8, 2), (7, 15), (10, 0)];
1398            let setup = [0usize, 7, 8, 9, 15];
1399            let mut tab_ind = TTab::new(n);
1400            for &q in &setup {
1401                tab_ind.h(q);
1402            }
1403            tab_ind.s(2);
1404            for &(c, t) in &pairs {
1405                tab_ind.cy(c, t);
1406            }
1407            let mut tab_batch = TTab::new(n);
1408            for &q in &setup {
1409                tab_batch.h(q);
1410            }
1411            tab_batch.s(2);
1412            tab_batch.cy_many(&pairs);
1413            assert_eq!(snapshot(&tab_ind), snapshot(&tab_batch));
1414        }
1415
1416        #[test]
1417        fn test_batch_more_than_8_storage_words() {
1418            // 16 storage words exceeds the previous fixed-array limit of 8;
1419            // verifies the SmallVec heap-spill path in `build_masks`.
1420            type BigConfig = ByteF64<16>;
1421            type BigTab = Tableau<BigConfig>;
1422
1423            fn big_snapshot(tab: &BigTab) -> Vec<(Vec<u8>, Vec<u8>, u8)> {
1424                tab.data
1425                    .iter()
1426                    .map(|pw| {
1427                        (
1428                            pw.word.xbits.data.as_raw_slice().to_vec(),
1429                            pw.word.zbits.data.as_raw_slice().to_vec(),
1430                            pw.phase,
1431                        )
1432                    })
1433                    .collect()
1434            }
1435
1436            let n = 128;
1437            // Indices straddle word 0 (qubits 0..8), middle words, and the
1438            // last word (qubits 120..128).
1439            let indices: Vec<usize> = vec![0, 7, 8, 63, 64, 71, 72, 120, 127];
1440
1441            let mut tab_ind = BigTab::new(n);
1442            tab_ind.h(3);
1443            tab_ind.s(60);
1444            for &i in &indices {
1445                tab_ind.h(i);
1446            }
1447
1448            let mut tab_batch = BigTab::new(n);
1449            tab_batch.h(3);
1450            tab_batch.s(60);
1451            tab_batch.h_many(&indices);
1452
1453            assert_eq!(big_snapshot(&tab_ind), big_snapshot(&tab_batch));
1454        }
1455
1456        #[test]
1457        fn test_batch_fourth_power_identity() {
1458            let n = 8;
1459            let indices = vec![0, 2, 4, 6];
1460            let initial = {
1461                let tab = TTab::new(n);
1462                snapshot(&tab)
1463            };
1464            let mut tab = TTab::new(n);
1465            for _ in 0..4 {
1466                tab.sqrt_x_many(&indices);
1467            }
1468            assert_eq!(snapshot(&tab), initial);
1469        }
1470    }
1471
1472    // ---- Canonical (single-gate) path on a MULTI-WORD tableau ----
1473    //
1474    // The canonical `Clifford`/`CliffordExtensions` methods on `Tableau<T>`
1475    // now operate directly on the packed storage words (the former `_word`
1476    // implementations, folded in as the single source of truth). These tests
1477    // lock that path against the INDEPENDENT batch (`_many`) implementation —
1478    // which uses a wholly separate `build_masks` + popcount code path — on a
1479    // tableau whose `data` spans more than one storage word (qubit indices
1480    // >= the per-word bit width). Single-element batch calls are
1481    // behaviorally identical to one canonical call, so any divergence between
1482    // the two implementations (especially around the `index / bits`,
1483    // `index % bits`, mask, and shift arithmetic for high qubit indices)
1484    // surfaces here.
1485    mod multiword_canonical_tests {
1486        use super::*;
1487        use ppvm_pauli_sum::config::fxhash::ByteF64;
1488
1489        // 2 u8 words → qubits 0..8 live in word 0, qubits 8..16 in word 1.
1490        type TC = ByteF64<2>;
1491        type TTab = Tableau<TC>;
1492
1493        fn snapshot(tab: &TTab) -> Vec<(Vec<u8>, Vec<u8>, u8)> {
1494            tab.data
1495                .iter()
1496                .map(|pw| {
1497                    (
1498                        pw.word.xbits.data.as_raw_slice().to_vec(),
1499                        pw.word.zbits.data.as_raw_slice().to_vec(),
1500                        pw.phase,
1501                    )
1502                })
1503                .collect()
1504        }
1505
1506        // Put the tableau in a non-trivial state straddling both words before
1507        // exercising the gate under test.
1508        fn seed(tab: &mut TTab) {
1509            for &q in &[0usize, 3, 7, 8, 11, 15] {
1510                tab.h(q);
1511            }
1512            tab.s(2);
1513            tab.s(10);
1514            tab.sqrt_y(5);
1515            tab.sqrt_x(13);
1516            tab.cnot(1, 9);
1517            tab.cz(6, 14);
1518        }
1519
1520        // Indices chosen to hit word 0 (0..8), word 1 (8..16), and both word
1521        // boundaries (7 = last bit of word 0, 8 = first bit of word 1).
1522        const SINGLE_INDICES: [usize; 6] = [0, 7, 8, 11, 15, 4];
1523
1524        /// For each index, apply the canonical single-qubit gate and the
1525        /// single-element batch call to freshly-seeded multi-word tableaus and
1526        /// assert the resulting storage words + phases match exactly.
1527        macro_rules! single_gate_matches_batch {
1528            ($test:ident, $gate:ident, $gate_many:ident) => {
1529                #[test]
1530                fn $test() {
1531                    let n = 16;
1532                    for &i in &SINGLE_INDICES {
1533                        let mut tab_canon = TTab::new(n);
1534                        seed(&mut tab_canon);
1535                        tab_canon.$gate(i);
1536
1537                        let mut tab_batch = TTab::new(n);
1538                        seed(&mut tab_batch);
1539                        tab_batch.$gate_many(&[i]);
1540
1541                        assert_eq!(
1542                            snapshot(&tab_canon),
1543                            snapshot(&tab_batch),
1544                            "canonical {} disagrees with batch {} at qubit {i}",
1545                            stringify!($gate),
1546                            stringify!($gate_many),
1547                        );
1548                    }
1549                }
1550            };
1551        }
1552
1553        single_gate_matches_batch!(canonical_x_matches_batch_multiword, x, x_many);
1554        single_gate_matches_batch!(canonical_y_matches_batch_multiword, y, y_many);
1555        single_gate_matches_batch!(canonical_z_matches_batch_multiword, z, z_many);
1556        single_gate_matches_batch!(canonical_h_matches_batch_multiword, h, h_many);
1557        single_gate_matches_batch!(canonical_s_matches_batch_multiword, s, s_many);
1558        single_gate_matches_batch!(canonical_s_dag_matches_batch_multiword, s_dag, s_dag_many);
1559        single_gate_matches_batch!(
1560            canonical_sqrt_x_matches_batch_multiword,
1561            sqrt_x,
1562            sqrt_x_many
1563        );
1564        single_gate_matches_batch!(
1565            canonical_sqrt_x_dag_matches_batch_multiword,
1566            sqrt_x_dag,
1567            sqrt_x_dag_many
1568        );
1569        single_gate_matches_batch!(
1570            canonical_sqrt_y_matches_batch_multiword,
1571            sqrt_y,
1572            sqrt_y_many
1573        );
1574        single_gate_matches_batch!(
1575            canonical_sqrt_y_dag_matches_batch_multiword,
1576            sqrt_y_dag,
1577            sqrt_y_dag_many
1578        );
1579
1580        // Two-qubit pairs covering: both in word 0, both in word 1, control
1581        // in word 0 / target in word 1, and the reverse.
1582        const PAIRS: [(usize, usize); 4] = [(0, 3), (9, 12), (2, 10), (13, 5)];
1583
1584        /// Canonical two-qubit gate vs single-pair batch call, for same-word
1585        /// and cross-word pairs on a multi-word tableau.
1586        macro_rules! pair_gate_matches_batch {
1587            ($test:ident, $gate:ident, $gate_many:ident) => {
1588                #[test]
1589                fn $test() {
1590                    let n = 16;
1591                    for &(c, t) in &PAIRS {
1592                        let mut tab_canon = TTab::new(n);
1593                        seed(&mut tab_canon);
1594                        tab_canon.$gate(c, t);
1595
1596                        let mut tab_batch = TTab::new(n);
1597                        seed(&mut tab_batch);
1598                        tab_batch.$gate_many(&[(c, t)]);
1599
1600                        assert_eq!(
1601                            snapshot(&tab_canon),
1602                            snapshot(&tab_batch),
1603                            "canonical {} disagrees with batch {} on pair ({c}, {t})",
1604                            stringify!($gate),
1605                            stringify!($gate_many),
1606                        );
1607                    }
1608                }
1609            };
1610        }
1611
1612        pair_gate_matches_batch!(canonical_cnot_matches_batch_multiword, cnot, cnot_many);
1613        pair_gate_matches_batch!(canonical_cz_matches_batch_multiword, cz, cz_many);
1614        pair_gate_matches_batch!(canonical_cy_matches_batch_multiword, cy, cy_many);
1615
1616        /// Independent of the batch path: known forward-propagation rules for
1617        /// a single-qubit gate applied to a qubit living in the SECOND storage
1618        /// word. This checks the high-index mask and shift arithmetic against
1619        /// hand-derived expectations.
1620        ///
1621        /// Row layout (see `Tableau::new`): rows `0..n` are destabilizers
1622        /// `X_q` (row `q`), rows `n..2n` are stabilizers `Z_q` (row `n + q`).
1623        #[test]
1624        fn sqrt_x_on_second_word_qubit_known_transform() {
1625            // √X sends Z → -Y (x stays, z gains x, phase +2) and leaves X
1626            // unchanged.
1627            let n = 16;
1628            let q = 11; // word 1, bit 3
1629            let bit = 1u8 << (q % 8);
1630            let wi = q / 8; // 1
1631
1632            let mut tab = TTab::new(n);
1633            tab.sqrt_x(q);
1634
1635            // Destabilizer row (X_q): unchanged, no phase.
1636            let dz = &tab.data[q];
1637            assert_eq!(dz.word.xbits.data.as_raw_slice()[wi] & bit, bit);
1638            assert_eq!(dz.word.zbits.data.as_raw_slice()[wi] & bit, 0);
1639            assert_eq!(dz.phase, 0);
1640
1641            // Stabilizer row (Z_q → -Y_q): x set, z set, phase +2.
1642            let st = &tab.data[n + q];
1643            assert_eq!(st.word.xbits.data.as_raw_slice()[wi] & bit, bit);
1644            assert_eq!(st.word.zbits.data.as_raw_slice()[wi] & bit, bit);
1645            assert_eq!(st.phase, 2);
1646        }
1647
1648        /// CNOT with control and target in DIFFERENT storage words, checked
1649        /// against the known stabilizer-propagation truth table. Control = 2
1650        /// (word 0), target = 10 (word 1). Fresh tableau, so the relevant rows
1651        /// are the single-qubit destabilizers/stabilizers of qubits 2 and 10.
1652        ///
1653        /// Row layout (see `Tableau::new`): destabilizer `X_q` is row `q`,
1654        /// stabilizer `Z_q` is row `n + q`.
1655        #[test]
1656        fn cnot_cross_word_known_transform() {
1657            let n = 16;
1658            let (c, t) = (2usize, 10usize);
1659            let mut tab = TTab::new(n);
1660            tab.cnot(c, t);
1661
1662            let cbit = 1u8 << (c % 8);
1663            let tbit = 1u8 << (t % 8);
1664            let (cw, tw) = (c / 8, t / 8);
1665
1666            // Read (xc, zc, xt, zt, phase) for a given row index.
1667            let read = |row: usize| {
1668                let pw = &tab.data[row];
1669                let x = pw.word.xbits.data.as_raw_slice();
1670                let z = pw.word.zbits.data.as_raw_slice();
1671                (
1672                    x[cw] & cbit != 0,
1673                    z[cw] & cbit != 0,
1674                    x[tw] & tbit != 0,
1675                    z[tw] & tbit != 0,
1676                    pw.phase,
1677                )
1678            };
1679
1680            // X_c (destabilizer of control) → X_c X_t, no phase.
1681            assert_eq!(read(c), (true, false, true, false, 0));
1682            // Z_c (stabilizer of control) → Z_c, unchanged.
1683            assert_eq!(read(n + c), (false, true, false, false, 0));
1684            // X_t (destabilizer of target) → X_t, unchanged.
1685            assert_eq!(read(t), (false, false, true, false, 0));
1686            // Z_t (stabilizer of target) → Z_c Z_t, no phase.
1687            assert_eq!(read(n + t), (false, true, false, true, 0));
1688        }
1689    }
1690
1691    // G-061: a repeated qubit index in a batched Clifford owes conjugation by
1692    // `Gᵏ`, i.e. the per-index loop — `X 0 0` is legal Stim meaning
1693    // apply-to-each-target-in-order, and the fused mask carries one bit per
1694    // *distinct* site. XOR-cancelling a repeated bit would recover `Gᵏ` only
1695    // when `G² = I` (`X`, `Y`, `Z`, `H`). For `S`, `√X`, `√Y`, even `k = 2`
1696    // is another Pauli (`S² = Z`, `(√X)² = X`, `(√Y)² = Y`), so neither one
1697    // bit nor an XOR-cancelled bit is right for every family.
1698    mod duplicate_index_tests {
1699        use super::*;
1700        use ppvm_pauli_sum::config::fxhash::ByteF64;
1701
1702        // 2 u8 words → qubits 0..8 in word 0, qubits 8..16 in word 1.
1703        type TC = ByteF64<2>;
1704        type TTab = Tableau<TC>;
1705        type GenTab = GeneralizedTableau<TC>;
1706
1707        fn snapshot(tab: &TTab) -> Vec<(Vec<u8>, Vec<u8>, u8)> {
1708            tab.data
1709                .iter()
1710                .map(|pw| {
1711                    (
1712                        pw.word.xbits.data.as_raw_slice().to_vec(),
1713                        pw.word.zbits.data.as_raw_slice().to_vec(),
1714                        pw.phase,
1715                    )
1716                })
1717                .collect()
1718        }
1719
1720        /// A frame with all four bit patterns (`I`/`X`/`Y`/`Z`) live in the
1721        /// rows, so a wrong number of gate applications cannot cancel out by
1722        /// accident.
1723        fn prepared(n: usize) -> TTab {
1724            let mut tab = TTab::new(n);
1725            for q in 0..n {
1726                tab.h(q);
1727                if q % 2 == 0 {
1728                    tab.s(q);
1729                }
1730            }
1731            for q in 1..n {
1732                tab.cnot(q - 1, q);
1733            }
1734            tab
1735        }
1736
1737        /// The batch on `$indices` must equal `$one` applied once per
1738        /// occurrence.
1739        macro_rules! check {
1740            ($many:ident, $one:ident, $indices:expr) => {{
1741                let indices: &[usize] = $indices;
1742                let mut batched = prepared(16);
1743                let mut looped = prepared(16);
1744                batched.$many(indices);
1745                for &q in indices {
1746                    looped.$one(q);
1747                }
1748                assert_eq!(
1749                    snapshot(&batched),
1750                    snapshot(&looped),
1751                    "{} on repeated indices {:?} must equal the {} loop",
1752                    stringify!($many),
1753                    indices,
1754                    stringify!($one),
1755                );
1756            }};
1757        }
1758
1759        macro_rules! check_all {
1760            ($indices:expr) => {{
1761                check!(x_many, x, $indices);
1762                check!(y_many, y, $indices);
1763                check!(z_many, z, $indices);
1764                check!(h_many, h, $indices);
1765                check!(s_many, s, $indices);
1766                check!(s_dag_many, s_dag, $indices);
1767                check!(sqrt_x_many, sqrt_x, $indices);
1768                check!(sqrt_x_dag_many, sqrt_x_dag, $indices);
1769                check!(sqrt_y_many, sqrt_y, $indices);
1770                check!(sqrt_y_dag_many, sqrt_y_dag, $indices);
1771            }};
1772        }
1773
1774        #[test]
1775        fn duplicate_indices_batch_equals_per_index_loop() {
1776            check_all!(&[0, 0]);
1777            check_all!(&[1, 1, 1]);
1778            check_all!(&[0, 2, 2, 3]);
1779        }
1780
1781        /// Independent of the loop: `G²` is a Clifford in its own right, so
1782        /// `g_many(&[q, q])` must equal conjugation by `G²`.
1783        #[test]
1784        fn duplicate_indices_equal_conjugation_by_g_squared() {
1785            // Involutory: G² = I, so the batch must leave the frame untouched.
1786            for (name, apply) in [
1787                ("x_many", TTab::x_many as fn(&mut TTab, &[usize])),
1788                ("y_many", TTab::y_many),
1789                ("z_many", TTab::z_many),
1790                ("h_many", TTab::h_many),
1791            ] {
1792                let mut batched = prepared(16);
1793                let untouched = prepared(16);
1794                apply(&mut batched, &[1, 1]);
1795                assert_eq!(
1796                    snapshot(&batched),
1797                    snapshot(&untouched),
1798                    "{name}(&[1, 1]) must be the identity (G² = I)"
1799                );
1800            }
1801
1802            // S² = Z, (S†)² = Z, (√X)² = X, (√X†)² = X, (√Y)² = Y, (√Y†)² = Y.
1803            for (name, apply, square) in [
1804                (
1805                    "s_many",
1806                    TTab::s_many as fn(&mut TTab, &[usize]),
1807                    TTab::z as fn(&mut TTab, usize),
1808                ),
1809                ("s_dag_many", TTab::s_dag_many, TTab::z),
1810                ("sqrt_x_many", TTab::sqrt_x_many, TTab::x),
1811                ("sqrt_x_dag_many", TTab::sqrt_x_dag_many, TTab::x),
1812                ("sqrt_y_many", TTab::sqrt_y_many, TTab::y),
1813                ("sqrt_y_dag_many", TTab::sqrt_y_dag_many, TTab::y),
1814            ] {
1815                let mut batched = prepared(16);
1816                let mut squared = prepared(16);
1817                apply(&mut batched, &[1, 1]);
1818                square(&mut squared, 1);
1819                assert_eq!(
1820                    snapshot(&batched),
1821                    snapshot(&squared),
1822                    "{name}(&[1, 1]) must equal conjugation by G²"
1823                );
1824            }
1825        }
1826
1827        /// The same defect across storage words: the duplicate sits in word 1
1828        /// while another index sits in word 0.
1829        #[test]
1830        fn duplicate_indices_batch_equals_loop_multiword() {
1831            check!(h_many, h, &[3, 10, 10]);
1832            check!(s_many, s, &[3, 10, 10]);
1833            check!(sqrt_y_many, sqrt_y, &[3, 10, 10]);
1834        }
1835
1836        /// The `GeneralizedTableau` wrappers filter lost qubits and forward the
1837        /// rest, duplicates included, so they inherit the same contract.
1838        #[test]
1839        fn duplicate_indices_generalized_tableau_equals_loop() {
1840            let indices: &[usize] = &[0, 2, 2];
1841            for lost in [None, Some(0usize)] {
1842                let mut batched: GenTab = GeneralizedTableau::new(16, 1e-12);
1843                let mut looped: GenTab = GeneralizedTableau::new(16, 1e-12);
1844                for tab in [&mut batched, &mut looped] {
1845                    tab.tableau = prepared(16);
1846                    if let Some(q) = lost {
1847                        tab.qubit_status[q] = QubitStatus::Lost;
1848                    }
1849                }
1850                batched.sqrt_x_many(indices);
1851                for &q in indices {
1852                    looped.sqrt_x(q);
1853                }
1854                assert_eq!(
1855                    snapshot(&batched.tableau),
1856                    snapshot(&looped.tableau),
1857                    "sqrt_x_many(&[0, 2, 2]) with lost = {lost:?}"
1858                );
1859            }
1860        }
1861
1862        /// The user-visible symptom from G-061's `.stim` reproduction:
1863        /// `X 0 0` on `|0…0⟩` is the identity, so the sampled bit must be 0.
1864        #[test]
1865        fn batched_x_twice_on_ground_state_measures_zero() {
1866            let mut tab: GenTab = GeneralizedTableau::new(1, 1e-12);
1867            tab.x_many(&[0, 0]);
1868            assert_eq!(tab.measure(0), Some(false));
1869        }
1870    }
1871}