paulistrings/channel/
clifford.rs

1//! Clifford gates (table-driven, branchless). See §6.
2//!
3//! A Clifford `G` conjugates each single-qubit Pauli `P` to `± P'` for some
4//! Pauli `P'`. We precompute the full lookup table at construction so
5//! `apply` is a single indexed read on the support qubit(s) — no runtime
6//! Pauli multiplication.
7//!
8//! Encoding: a single-qubit Pauli is indexed by `(x_bit | (z_bit << 1))` —
9//! `I = 0, X = 1, Z = 2, Y = 3`. The output Pauli uses the same packing.
10
11use super::{Channel, OutputBuffer};
12use crate::phase::Phase;
13use num_complex::Complex64;
14
15/// Single-qubit Clifford gate stored as a 4-entry conjugation table.
16///
17/// `out_pauli[i]` and `phase[i]` give the result of `G · P_i · G†` for the
18/// four input Paulis (indexed as above): the new packed Pauli on the
19/// support qubit and the `i^k` phase to fold into the coefficient.
20///
21/// Generic over `W` so the same gate value can act on any Pauli width.
22#[derive(Clone, Copy, Debug)]
23pub struct Clifford1Q {
24    /// Single qubit this gate acts on. Held as `[u32; 1]` so `support()`
25    /// returns a slice without allocation.
26    pub support: [u32; 1],
27    /// Output Pauli bits per input Pauli. Same packing as the index:
28    /// `(out_x | (out_z << 1))`. `out_pauli[0]` is always `0` (`I → I`).
29    pub out_pauli: [u8; 4],
30    /// Phase factor (`i^k`) per input Pauli. `phase[0]` is always
31    /// `Phase::ONE`.
32    pub phase: [Phase; 4],
33}
34
35impl Clifford1Q {
36    /// Hadamard. Conjugation: `I → I, X → Z, Z → X, Y → −Y`.
37    pub fn h(qubit: u32) -> Self {
38        Self {
39            support: [qubit],
40            out_pauli: [0, 2, 1, 3],
41            phase: [Phase::ONE, Phase::ONE, Phase::ONE, Phase::MINUS_ONE],
42        }
43    }
44
45    /// Phase gate `S = diag(1, i)`. Conjugation:
46    /// `I → I, X → Y, Z → Z, Y → −X`.
47    pub fn s(qubit: u32) -> Self {
48        Self {
49            support: [qubit],
50            out_pauli: [0, 3, 2, 1],
51            phase: [Phase::ONE, Phase::ONE, Phase::ONE, Phase::MINUS_ONE],
52        }
53    }
54
55    /// Pauli-X gate. Conjugation: `I → I, X → X, Z → −Z, Y → −Y`.
56    pub fn x(qubit: u32) -> Self {
57        Self {
58            support: [qubit],
59            out_pauli: [0, 1, 2, 3],
60            phase: [Phase::ONE, Phase::ONE, Phase::MINUS_ONE, Phase::MINUS_ONE],
61        }
62    }
63
64    /// Pauli-Y gate. Conjugation: `I → I, X → −X, Z → −Z, Y → Y`.
65    pub fn y(qubit: u32) -> Self {
66        Self {
67            support: [qubit],
68            out_pauli: [0, 1, 2, 3],
69            phase: [Phase::ONE, Phase::MINUS_ONE, Phase::MINUS_ONE, Phase::ONE],
70        }
71    }
72
73    /// Pauli-Z gate. Conjugation: `I → I, X → −X, Z → Z, Y → −Y`.
74    pub fn z(qubit: u32) -> Self {
75        Self {
76            support: [qubit],
77            out_pauli: [0, 1, 2, 3],
78            phase: [Phase::ONE, Phase::MINUS_ONE, Phase::ONE, Phase::MINUS_ONE],
79        }
80    }
81
82    /// Conjugation table for `G†`. Inverts the Pauli permutation and conjugates
83    /// the per-input phases: if `G P_a G† = c_a · P_{f(a)}` then
84    /// `G† P_{f(a)} G = c_a* · P_a`.
85    ///
86    /// Self-inverse 1Q Cliffords (H, X, Y, Z) round-trip to themselves; `S`
87    /// returns `S†` (a distinct gate).
88    pub fn adjoint(&self) -> Self {
89        let mut out_pauli = [0u8; 4];
90        let mut phase = [Phase::ONE; 4];
91        for input_idx in 0u8..4 {
92            let f_a = self.out_pauli[input_idx as usize] as usize;
93            let c_a = self.phase[input_idx as usize];
94            out_pauli[f_a] = input_idx;
95            phase[f_a] = Phase::new((4 - c_a.exponent()) & 3);
96        }
97        Self {
98            support: self.support,
99            out_pauli,
100            phase,
101        }
102    }
103}
104
105impl Clifford1Q {
106    /// Shared body of `apply` and `apply_adjoint`. The two paths differ
107    /// only in which lookup table to use; pulling the lookup out of
108    /// `Channel::apply` keeps the bit-fiddling unduplicated.
109    #[inline]
110    fn apply_table<const W: usize>(
111        &self,
112        out_pauli: &[u8; 4],
113        phase: &[Phase; 4],
114        input_x: &[u64; W],
115        input_z: &[u64; W],
116        coeff: Complex64,
117        out: &mut OutputBuffer<'_, W>,
118    ) {
119        let q = self.support[0] as usize;
120        debug_assert!(q < 64 * W);
121        let word = q / 64;
122        let bit = q % 64;
123        let mask = 1u64 << bit;
124        let x_bit = ((input_x[word] >> bit) & 1) as u8;
125        let z_bit = ((input_z[word] >> bit) & 1) as u8;
126        let idx = (x_bit | (z_bit << 1)) as usize;
127        let op = out_pauli[idx];
128        let ox = (op & 1) as u64;
129        let oz = ((op >> 1) & 1) as u64;
130        let mut nx = *input_x;
131        let mut nz = *input_z;
132        nx[word] = (nx[word] & !mask) | (ox << bit);
133        nz[word] = (nz[word] & !mask) | (oz << bit);
134        out.push(nx, nz, phase[idx].apply(coeff));
135    }
136}
137
138impl<const W: usize> Channel<W> for Clifford1Q {
139    #[inline]
140    fn max_fanout(&self) -> usize {
141        1
142    }
143
144    #[inline]
145    fn support(&self) -> &[u32] {
146        &self.support
147    }
148
149    #[inline]
150    fn apply(
151        &self,
152        input_x: &[u64; W],
153        input_z: &[u64; W],
154        coeff: Complex64,
155        out: &mut OutputBuffer<'_, W>,
156    ) {
157        self.apply_table(&self.out_pauli, &self.phase, input_x, input_z, coeff, out);
158    }
159
160    #[inline]
161    fn apply_adjoint(
162        &self,
163        input_x: &[u64; W],
164        input_z: &[u64; W],
165        coeff: Complex64,
166        out: &mut OutputBuffer<'_, W>,
167    ) {
168        let adj = self.adjoint();
169        self.apply_table(&adj.out_pauli, &adj.phase, input_x, input_z, coeff, out);
170    }
171}
172
173/// Two-qubit Clifford gate stored as a 16-entry conjugation table.
174///
175/// Index encoding: low 2 bits select the input Pauli on `support[0]`,
176/// high 2 bits select it on `support[1]`. Each `out_pauli[i]` packs four
177/// bits `(ox0 | (oz0 << 1) | (ox1 << 2) | (oz1 << 3))`.
178#[derive(Clone, Copy, Debug)]
179pub struct Clifford2Q {
180    /// The two qubits this gate acts on. Convention: `support[0]` is the
181    /// "first" qubit (e.g. CNOT control), `support[1]` the second.
182    pub support: [u32; 2],
183    /// Output Pauli bits per input. 16 entries indexed by
184    /// `(x0 | (z0 << 1) | (x1 << 2) | (z1 << 3))`; each entry packs the
185    /// output bits in the same layout. `out_pauli[0]` is always `0`.
186    pub out_pauli: [u8; 16],
187    /// Phase factor (`i^k`) per input. `phase[0]` is always [`Phase::ONE`].
188    pub phase: [Phase; 16],
189}
190
191impl Clifford2Q {
192    /// CNOT with `control` and `target`. Conjugation generators:
193    /// `X⊗I → X⊗X, I⊗X → I⊗X, Z⊗I → Z⊗I, I⊗Z → Z⊗Z` (all phase `+1`).
194    /// The full 16-entry table follows by linearity over Pauli products.
195    pub fn cnot(control: u32, target: u32) -> Self {
196        Self::from_2q_generators(
197            [control, target],
198            // X⊗I → X⊗X
199            (pack4(1, 0, 1, 0), Phase::ONE),
200            // Z⊗I → Z⊗I
201            (pack4(0, 1, 0, 0), Phase::ONE),
202            // I⊗X → I⊗X
203            (pack4(0, 0, 1, 0), Phase::ONE),
204            // I⊗Z → Z⊗Z
205            (pack4(0, 1, 0, 1), Phase::ONE),
206        )
207    }
208
209    /// CZ on `q0` and `q1`. Conjugation generators:
210    /// `X⊗I → X⊗Z, I⊗X → Z⊗X, Z⊗I → Z⊗I, I⊗Z → I⊗Z` (all phase `+1`).
211    pub fn cz(q0: u32, q1: u32) -> Self {
212        Self::from_2q_generators(
213            [q0, q1],
214            // X⊗I → X⊗Z
215            (pack4(1, 0, 0, 1), Phase::ONE),
216            // Z⊗I → Z⊗I
217            (pack4(0, 1, 0, 0), Phase::ONE),
218            // I⊗X → Z⊗X
219            (pack4(0, 1, 1, 0), Phase::ONE),
220            // I⊗Z → I⊗Z
221            (pack4(0, 0, 0, 1), Phase::ONE),
222        )
223    }
224
225    /// SWAP on `q0` and `q1`. Conjugation: `(P ⊗ Q) → (Q ⊗ P)` for all
226    /// `P, Q` (all phase `+1`).
227    pub fn swap(q0: u32, q1: u32) -> Self {
228        Self::from_2q_generators(
229            [q0, q1],
230            // X⊗I → I⊗X
231            (pack4(0, 0, 1, 0), Phase::ONE),
232            // Z⊗I → I⊗Z
233            (pack4(0, 0, 0, 1), Phase::ONE),
234            // I⊗X → X⊗I
235            (pack4(1, 0, 0, 0), Phase::ONE),
236            // I⊗Z → Z⊗I
237            (pack4(0, 1, 0, 0), Phase::ONE),
238        )
239    }
240
241    /// Build a 2Q Clifford table from the four single-Pauli generators
242    /// `(X₀, Z₀, X₁, Z₁)`. For each of the 16 inputs we multiply the
243    /// corresponding generator outputs together (using `PauliString`
244    /// multiplication on a single word) and accumulate the `i^k` phase.
245    fn from_2q_generators(
246        support: [u32; 2],
247        x0_image: (u8, Phase),
248        z0_image: (u8, Phase),
249        x1_image: (u8, Phase),
250        z1_image: (u8, Phase),
251    ) -> Self {
252        let gens = [x0_image, z0_image, x1_image, z1_image];
253        let mut out_pauli = [0u8; 16];
254        let mut phase = [Phase::ONE; 16];
255        for idx in 0..16usize {
256            // Decompose the input across the four generators.
257            let bits = [
258                (idx & 1) as u8,        // x0
259                ((idx >> 1) & 1) as u8, // z0
260                ((idx >> 2) & 1) as u8, // x1
261                ((idx >> 3) & 1) as u8, // z1
262            ];
263
264            // Multiply the four generator images in X₀ Z₀ X₁ Z₁ order.
265            // The result is the image of `X^{x0} Z^{z0} X^{x1} Z^{z1}`,
266            // which is *not* the same as the Hermitian input Pauli when
267            // any qubit holds Y: `Y = i · X · Z`, so each Y in the input
268            // contributes an extra `i` factor that we must add to the
269            // accumulated phase below.
270            let mut acc_x = [0u64; 1];
271            let mut acc_z = [0u64; 1];
272            let mut acc_phase = Phase::ONE;
273            for (b, (img, ph)) in bits.iter().zip(gens.iter()) {
274                if *b == 1 {
275                    let (gx, gz) = unpack4_to_word(*img);
276                    let mut acc = crate::pauli_string::PauliString::<1> { x: acc_x, z: acc_z };
277                    let other = crate::pauli_string::PauliString::<1> { x: gx, z: gz };
278                    let mul_phase = acc.mul_assign(&other);
279                    acc_x = acc.x;
280                    acc_z = acc.z;
281                    acc_phase = acc_phase + mul_phase + *ph;
282                }
283            }
284            // Add `i` for each Y in the input (qubits where both x and z
285            // bits are set). The output bits encode their Hermitian Pauli
286            // directly — no symmetric cancellation factor on the output
287            // side.
288            let y_count = (bits[0] & bits[1]) + (bits[2] & bits[3]);
289            acc_phase += Phase::new(y_count);
290            out_pauli[idx] = pack4_from_word(acc_x, acc_z, support);
291            phase[idx] = acc_phase;
292        }
293        Self {
294            support,
295            out_pauli,
296            phase,
297        }
298    }
299}
300
301/// Pack `(x0, z0, x1, z1)` bits into the 4-bit `out_pauli` encoding.
302const fn pack4(x0: u8, z0: u8, x1: u8, z1: u8) -> u8 {
303    (x0 & 1) | ((z0 & 1) << 1) | ((x1 & 1) << 2) | ((z1 & 1) << 3)
304}
305
306/// Convert a packed 4-bit Pauli on the two support qubits back into a
307/// `PauliString<1>`-style `(x, z)` word pair, with the support qubits at
308/// positions 0 and 1 of the word. Used only inside table construction.
309fn unpack4_to_word(packed: u8) -> ([u64; 1], [u64; 1]) {
310    let x0 = (packed & 1) as u64;
311    let z0 = ((packed >> 1) & 1) as u64;
312    let x1 = ((packed >> 2) & 1) as u64;
313    let z1 = ((packed >> 3) & 1) as u64;
314    let x = x0 | (x1 << 1);
315    let z = z0 | (z1 << 1);
316    ([x], [z])
317}
318
319/// Inverse of `unpack4_to_word`: reads bits 0 and 1 of a single-word
320/// `(x, z)` pair and packs them back into the 4-bit encoding. The
321/// `support` argument is unused at runtime — kept for future-proofing
322/// once the helper moves out of test-only construction.
323fn pack4_from_word(x: [u64; 1], z: [u64; 1], _support: [u32; 2]) -> u8 {
324    let x0 = (x[0] & 1) as u8;
325    let z0 = (z[0] & 1) as u8;
326    let x1 = ((x[0] >> 1) & 1) as u8;
327    let z1 = ((z[0] >> 1) & 1) as u8;
328    pack4(x0, z0, x1, z1)
329}
330
331impl<const W: usize> Channel<W> for Clifford2Q {
332    #[inline]
333    fn max_fanout(&self) -> usize {
334        1
335    }
336
337    #[inline]
338    fn support(&self) -> &[u32] {
339        &self.support
340    }
341
342    #[inline]
343    fn apply(
344        &self,
345        input_x: &[u64; W],
346        input_z: &[u64; W],
347        coeff: Complex64,
348        out: &mut OutputBuffer<'_, W>,
349    ) {
350        let q0 = self.support[0] as usize;
351        let q1 = self.support[1] as usize;
352        debug_assert!(q0 < 64 * W);
353        debug_assert!(q1 < 64 * W);
354        debug_assert!(q0 != q1);
355
356        let w0 = q0 / 64;
357        let b0 = q0 % 64;
358        let w1 = q1 / 64;
359        let b1 = q1 % 64;
360        let m0 = 1u64 << b0;
361        let m1 = 1u64 << b1;
362
363        let x0 = ((input_x[w0] >> b0) & 1) as u8;
364        let z0 = ((input_z[w0] >> b0) & 1) as u8;
365        let x1 = ((input_x[w1] >> b1) & 1) as u8;
366        let z1 = ((input_z[w1] >> b1) & 1) as u8;
367        let idx = (x0 | (z0 << 1) | (x1 << 2) | (z1 << 3)) as usize;
368
369        let op = self.out_pauli[idx];
370        let ox0 = (op & 1) as u64;
371        let oz0 = ((op >> 1) & 1) as u64;
372        let ox1 = ((op >> 2) & 1) as u64;
373        let oz1 = ((op >> 3) & 1) as u64;
374
375        let mut nx = *input_x;
376        let mut nz = *input_z;
377        nx[w0] = (nx[w0] & !m0) | (ox0 << b0);
378        nz[w0] = (nz[w0] & !m0) | (oz0 << b0);
379        nx[w1] = (nx[w1] & !m1) | (ox1 << b1);
380        nz[w1] = (nz[w1] & !m1) | (oz1 << b1);
381
382        out.push(nx, nz, self.phase[idx].apply(coeff));
383    }
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use crate::pauli_string::PauliString;
390
391    #[allow(clippy::type_complexity)]
392    fn alloc_buf<const W: usize>() -> (Vec<[u64; W]>, Vec<[u64; W]>, Vec<Complex64>, usize) {
393        (
394            vec![[0u64; W]; 1],
395            vec![[0u64; W]; 1],
396            vec![Complex64::new(0.0, 0.0); 1],
397            0usize,
398        )
399    }
400
401    /// Apply `gate` to `input` with coefficient 1 and return `(output, coeff)`.
402    fn apply_1q<const W: usize>(
403        gate: &Clifford1Q,
404        input: &PauliString<W>,
405    ) -> (PauliString<W>, Complex64) {
406        let (mut x, mut z, mut c, mut len) = alloc_buf::<W>();
407        let mut buf = OutputBuffer::<W> {
408            x: &mut x,
409            z: &mut z,
410            coeff: &mut c,
411            len: &mut len,
412        };
413        gate.apply(&input.x, &input.z, Complex64::new(1.0, 0.0), &mut buf);
414        assert_eq!(*buf.len, 1);
415        let out = PauliString::<W> { x: x[0], z: z[0] };
416        (out, c[0])
417    }
418
419    fn apply_2q<const W: usize>(
420        gate: &Clifford2Q,
421        input: &PauliString<W>,
422    ) -> (PauliString<W>, Complex64) {
423        let (mut x, mut z, mut c, mut len) = alloc_buf::<W>();
424        let mut buf = OutputBuffer::<W> {
425            x: &mut x,
426            z: &mut z,
427            coeff: &mut c,
428            len: &mut len,
429        };
430        gate.apply(&input.x, &input.z, Complex64::new(1.0, 0.0), &mut buf);
431        assert_eq!(*buf.len, 1);
432        let out = PauliString::<W> { x: x[0], z: z[0] };
433        (out, c[0])
434    }
435
436    fn pauli_y<const W: usize>(qubit: u32) -> PauliString<W> {
437        PauliString::<W>::y(qubit)
438    }
439
440    // ---- Slice 4.3: Clifford1Q tables ----
441
442    #[test]
443    fn h_on_qubit_0_w1() {
444        let h = Clifford1Q::h(0);
445        // I → I, +1
446        let (out, c) = apply_1q::<1>(&h, &PauliString::<1>::identity());
447        assert_eq!(out, PauliString::<1>::identity());
448        assert_eq!(c, Complex64::new(1.0, 0.0));
449        // X → Z, +1
450        let (out, c) = apply_1q::<1>(&h, &PauliString::<1>::x(0));
451        assert_eq!(out, PauliString::<1>::z(0));
452        assert_eq!(c, Complex64::new(1.0, 0.0));
453        // Z → X, +1
454        let (out, c) = apply_1q::<1>(&h, &PauliString::<1>::z(0));
455        assert_eq!(out, PauliString::<1>::x(0));
456        assert_eq!(c, Complex64::new(1.0, 0.0));
457        // Y → -Y, -1
458        let (out, c) = apply_1q::<1>(&h, &pauli_y::<1>(0));
459        assert_eq!(out, pauli_y::<1>(0));
460        assert_eq!(c, Complex64::new(-1.0, 0.0));
461    }
462
463    #[test]
464    fn s_on_qubit_0_w1() {
465        let s = Clifford1Q::s(0);
466        // X → Y, +1
467        let (out, c) = apply_1q::<1>(&s, &PauliString::<1>::x(0));
468        assert_eq!(out, pauli_y::<1>(0));
469        assert_eq!(c, Complex64::new(1.0, 0.0));
470        // Z → Z, +1
471        let (out, c) = apply_1q::<1>(&s, &PauliString::<1>::z(0));
472        assert_eq!(out, PauliString::<1>::z(0));
473        assert_eq!(c, Complex64::new(1.0, 0.0));
474        // Y → -X, -1
475        let (out, c) = apply_1q::<1>(&s, &pauli_y::<1>(0));
476        assert_eq!(out, PauliString::<1>::x(0));
477        assert_eq!(c, Complex64::new(-1.0, 0.0));
478    }
479
480    #[test]
481    fn x_gate_w1() {
482        let g = Clifford1Q::x(0);
483        // X → X, Z → -Z, Y → -Y
484        let (o, c) = apply_1q::<1>(&g, &PauliString::<1>::x(0));
485        assert_eq!(o, PauliString::<1>::x(0));
486        assert_eq!(c, Complex64::new(1.0, 0.0));
487        let (o, c) = apply_1q::<1>(&g, &PauliString::<1>::z(0));
488        assert_eq!(o, PauliString::<1>::z(0));
489        assert_eq!(c, Complex64::new(-1.0, 0.0));
490        let (o, c) = apply_1q::<1>(&g, &pauli_y::<1>(0));
491        assert_eq!(o, pauli_y::<1>(0));
492        assert_eq!(c, Complex64::new(-1.0, 0.0));
493    }
494
495    #[test]
496    fn y_gate_w1() {
497        let g = Clifford1Q::y(0);
498        let (o, c) = apply_1q::<1>(&g, &PauliString::<1>::x(0));
499        assert_eq!(o, PauliString::<1>::x(0));
500        assert_eq!(c, Complex64::new(-1.0, 0.0));
501        let (o, c) = apply_1q::<1>(&g, &PauliString::<1>::z(0));
502        assert_eq!(o, PauliString::<1>::z(0));
503        assert_eq!(c, Complex64::new(-1.0, 0.0));
504        let (o, c) = apply_1q::<1>(&g, &pauli_y::<1>(0));
505        assert_eq!(o, pauli_y::<1>(0));
506        assert_eq!(c, Complex64::new(1.0, 0.0));
507    }
508
509    #[test]
510    fn z_gate_w1() {
511        let g = Clifford1Q::z(0);
512        let (o, c) = apply_1q::<1>(&g, &PauliString::<1>::x(0));
513        assert_eq!(o, PauliString::<1>::x(0));
514        assert_eq!(c, Complex64::new(-1.0, 0.0));
515        let (o, c) = apply_1q::<1>(&g, &PauliString::<1>::z(0));
516        assert_eq!(o, PauliString::<1>::z(0));
517        assert_eq!(c, Complex64::new(1.0, 0.0));
518        let (o, c) = apply_1q::<1>(&g, &pauli_y::<1>(0));
519        assert_eq!(o, pauli_y::<1>(0));
520        assert_eq!(c, Complex64::new(-1.0, 0.0));
521    }
522
523    #[test]
524    fn h_on_qubit_64_w2_word_boundary() {
525        let h = Clifford1Q::h(64);
526        // X(64) → Z(64), bits live in word 1.
527        let (out, c) = apply_1q::<2>(&h, &PauliString::<2>::x(64));
528        assert_eq!(out, PauliString::<2>::z(64));
529        assert_eq!(c, Complex64::new(1.0, 0.0));
530        assert_eq!(out.x[0], 0); // word 0 untouched
531        assert_eq!(out.z[0], 0);
532        // Z(64) → X(64).
533        let (out, _c) = apply_1q::<2>(&h, &PauliString::<2>::z(64));
534        assert_eq!(out, PauliString::<2>::x(64));
535    }
536
537    #[test]
538    fn support_outside_bits_untouched_w2() {
539        // Build X(0) · X(70) and apply H(70). Bit 0 must stay X, bit 70
540        // must become Z, and the coefficient stays +1.
541        let mut input = PauliString::<2>::x(0);
542        let _ = input.mul_assign(&PauliString::<2>::x(70));
543        let h = Clifford1Q::h(70);
544        let (out, c) = apply_1q::<2>(&h, &input);
545
546        // Expected: X(0) · Z(70).
547        let mut expected = PauliString::<2>::x(0);
548        let _ = expected.mul_assign(&PauliString::<2>::z(70));
549        assert_eq!(out, expected);
550        assert_eq!(c, Complex64::new(1.0, 0.0));
551    }
552
553    #[test]
554    fn h_squared_is_identity() {
555        // Apply H twice; the result is the input with phase +1.
556        let h = Clifford1Q::h(0);
557        for input in [
558            PauliString::<1>::identity(),
559            PauliString::<1>::x(0),
560            PauliString::<1>::z(0),
561            pauli_y::<1>(0),
562        ] {
563            let (mid, c1) = apply_1q::<1>(&h, &input);
564            let (out, c2) = apply_1q::<1>(&h, &mid);
565            assert_eq!(out, input, "H·H should be identity on {:?}", input);
566            assert_eq!(
567                c1 * c2,
568                Complex64::new(1.0, 0.0),
569                "phase should square to +1"
570            );
571        }
572    }
573
574    // ---- Slice 6.5: Clifford1Q adjoint table ----
575
576    /// Self-adjoint 1Q Cliffords round-trip through `adjoint()` to themselves.
577    #[test]
578    fn h_x_y_z_are_self_adjoint() {
579        for gate in [
580            Clifford1Q::h(0),
581            Clifford1Q::x(0),
582            Clifford1Q::y(0),
583            Clifford1Q::z(0),
584        ] {
585            let adj = gate.adjoint();
586            assert_eq!(adj.out_pauli, gate.out_pauli);
587            assert_eq!(adj.phase, gate.phase);
588        }
589    }
590
591    /// `S` is not self-adjoint: its adjoint table differs in the phase
592    /// pattern, but `(S†)† = S` (involution).
593    #[test]
594    fn s_adjoint_inverts_table_and_is_involutive() {
595        let s = Clifford1Q::s(0);
596        let s_dag = s.adjoint();
597        // Forward: I→I(+1), X→Y(+1), Z→Z(+1), Y→-X(-1)
598        // Adjoint: I→I(+1), X→-Y(-1), Z→Z(+1), Y→X(+1)
599        assert_eq!(s_dag.out_pauli, [0, 3, 2, 1]);
600        assert_eq!(
601            s_dag.phase,
602            [Phase::ONE, Phase::MINUS_ONE, Phase::ONE, Phase::ONE]
603        );
604        // Involution: (S†)† = S.
605        let s_again = s_dag.adjoint();
606        assert_eq!(s_again.out_pauli, s.out_pauli);
607        assert_eq!(s_again.phase, s.phase);
608    }
609
610    /// `apply_adjoint` on `S` followed by `apply` on `S` round-trips X.
611    #[test]
612    fn s_apply_then_apply_adjoint_round_trips() {
613        let s = Clifford1Q::s(0);
614        let x_in = PauliString::<1>::x(0);
615        let (mid, c1) = apply_1q::<1>(&s, &x_in);
616        // mid = Y. Now apply S†.
617        let (mut bx, mut bz, mut bc, mut len) = alloc_buf::<1>();
618        let mut buf = OutputBuffer::<1> {
619            x: &mut bx,
620            z: &mut bz,
621            coeff: &mut bc,
622            len: &mut len,
623        };
624        s.apply_adjoint(&mid.x, &mid.z, c1, &mut buf);
625        assert_eq!(*buf.len, 1);
626        assert_eq!(bx[0], x_in.x);
627        assert_eq!(bz[0], x_in.z);
628        assert_eq!(bc[0], Complex64::new(1.0, 0.0));
629    }
630
631    // ---- Slice 4.4: Clifford2Q tables ----
632
633    /// Build `P_a ⊗ P_b` on qubits `(q0, q1)` of a `PauliString<W>` using
634    /// `mul_assign`, where `pa` and `pb` are 2-bit single-qubit Pauli
635    /// codes (`I=0, X=1, Z=2, Y=3`).
636    fn tensor<const W: usize>(q0: u32, q1: u32, pa: u8, pb: u8) -> PauliString<W> {
637        let mut p = PauliString::<W>::identity();
638        let put = |p: &mut PauliString<W>, q: u32, code: u8| {
639            let g = match code {
640                0 => return,
641                1 => PauliString::<W>::x(q),
642                2 => PauliString::<W>::z(q),
643                3 => PauliString::<W>::y(q),
644                _ => unreachable!(),
645            };
646            // Y on a fresh identity contributes phase 0 (Y = (1,1) bits, no
647            // pre-existing X·Z to worry about), and the partial products are
648            // on disjoint qubits, so phases are always +1 here.
649            let _ = p.mul_assign(&g);
650        };
651        put(&mut p, q0, pa);
652        put(&mut p, q1, pb);
653        p
654    }
655
656    #[test]
657    fn cnot_generator_rules_w1() {
658        let cnot = Clifford2Q::cnot(0, 1);
659        // X⊗I → X⊗X
660        let (o, c) = apply_2q::<1>(&cnot, &tensor::<1>(0, 1, 1, 0));
661        assert_eq!(o, tensor::<1>(0, 1, 1, 1));
662        assert_eq!(c, Complex64::new(1.0, 0.0));
663        // I⊗X → I⊗X
664        let (o, c) = apply_2q::<1>(&cnot, &tensor::<1>(0, 1, 0, 1));
665        assert_eq!(o, tensor::<1>(0, 1, 0, 1));
666        assert_eq!(c, Complex64::new(1.0, 0.0));
667        // Z⊗I → Z⊗I
668        let (o, c) = apply_2q::<1>(&cnot, &tensor::<1>(0, 1, 2, 0));
669        assert_eq!(o, tensor::<1>(0, 1, 2, 0));
670        assert_eq!(c, Complex64::new(1.0, 0.0));
671        // I⊗Z → Z⊗Z
672        let (o, c) = apply_2q::<1>(&cnot, &tensor::<1>(0, 1, 0, 2));
673        assert_eq!(o, tensor::<1>(0, 1, 2, 2));
674        assert_eq!(c, Complex64::new(1.0, 0.0));
675    }
676
677    /// Reference for `CNOT (P⊗Q) CNOT†` derived from generator rules and
678    /// `PauliString::mul_assign`. Each input Pauli on each qubit maps:
679    ///   qubit 0:  I→I,  X→X⊗X,  Z→Z⊗I,  Y→i·X·Z → Y⊗X (with phase from XZ·X)
680    ///   qubit 1:  I→I,  X→I⊗X,  Z→Z⊗Z,  Y→i·X·Z → Z⊗Y
681    /// We compute the image as the product of these two qubit images and
682    /// fold in any phase the multiplication picks up.
683    fn cnot_reference<const W: usize>(
684        control: u32,
685        target: u32,
686        pa: u8,
687        pb: u8,
688    ) -> (PauliString<W>, Phase) {
689        // Image of `pa` on `control`: a `PauliString<W>` plus a phase.
690        let (img_a, ph_a) = match pa {
691            0 => (PauliString::<W>::identity(), Phase::ONE),
692            1 => {
693                // X⊗X on (control, target).
694                let mut p = PauliString::<W>::x(control);
695                let _ = p.mul_assign(&PauliString::<W>::x(target));
696                (p, Phase::ONE)
697            }
698            2 => (PauliString::<W>::z(control), Phase::ONE),
699            3 => {
700                // Y → i · X · Z (decompose `pa = Y` into X·Z; image of X
701                // is X⊗X, image of Z is Z⊗I; product picks up phase from
702                // mul_assign, then multiply by `i` for the Y-decomposition).
703                let mut p = PauliString::<W>::x(control);
704                let _ = p.mul_assign(&PauliString::<W>::x(target));
705                let mut q = PauliString::<W>::z(control);
706                let mp = p.mul_assign(&q);
707                // Y = i · X · Z, so the image is i · (X-image)(Z-image).
708                (p, Phase::I + mp)
709            }
710            _ => unreachable!(),
711        };
712        let (img_b, ph_b) = match pb {
713            0 => (PauliString::<W>::identity(), Phase::ONE),
714            1 => (PauliString::<W>::x(target), Phase::ONE),
715            2 => {
716                // I⊗Z → Z⊗Z.
717                let mut p = PauliString::<W>::z(control);
718                let _ = p.mul_assign(&PauliString::<W>::z(target));
719                (p, Phase::ONE)
720            }
721            3 => {
722                // I⊗Y → I⊗(i·X·Z) → i · (I⊗X) · (Z⊗Z) = i · X(target) · Z(c) · Z(t).
723                let mut p = PauliString::<W>::x(target);
724                let mut q = PauliString::<W>::z(control);
725                let _ = q.mul_assign(&PauliString::<W>::z(target));
726                let mp = p.mul_assign(&q);
727                (p, Phase::I + mp)
728            }
729            _ => unreachable!(),
730        };
731        // Combine the two images: image of `pa ⊗ pb` is image(pa) · image(pb)
732        // since they commute as operators on different input qubits — but
733        // the *image* operators may overlap, so phases come from mul_assign.
734        let mut prod = img_a;
735        let mp = prod.mul_assign(&img_b);
736        (prod, ph_a + ph_b + mp)
737    }
738
739    #[test]
740    fn cnot_full_table_w1() {
741        let cnot = Clifford2Q::cnot(0, 1);
742        for pa in 0u8..4 {
743            for pb in 0u8..4 {
744                let input = tensor::<1>(0, 1, pa, pb);
745                let (got_out, got_c) = apply_2q::<1>(&cnot, &input);
746                let (exp_out, exp_phase) = cnot_reference::<1>(0, 1, pa, pb);
747                assert_eq!(
748                    got_out, exp_out,
749                    "CNOT output mismatch on (pa={}, pb={})",
750                    pa, pb
751                );
752                assert_eq!(
753                    got_c,
754                    exp_phase.to_complex(),
755                    "CNOT phase mismatch on (pa={}, pb={})",
756                    pa,
757                    pb
758                );
759            }
760        }
761    }
762
763    #[test]
764    fn cnot_word_boundary_w2() {
765        // Control on qubit 63 (last bit of word 0), target on qubit 64
766        // (first bit of word 1) — exercises the per-qubit word/bit math
767        // independently for each support qubit.
768        let cnot = Clifford2Q::cnot(63, 64);
769        // X⊗X on (63, 64) → X⊗I (per CNOT rules: X⊗X → X⊗I).
770        let input = tensor::<2>(63, 64, 1, 1);
771        let (out, c) = apply_2q::<2>(&cnot, &input);
772        let expected = tensor::<2>(63, 64, 1, 0);
773        assert_eq!(out, expected);
774        assert_eq!(c, Complex64::new(1.0, 0.0));
775        // Y⊗X on (63, 64). Reference computes the image.
776        let input = tensor::<2>(63, 64, 3, 1);
777        let (got_out, got_c) = apply_2q::<2>(&cnot, &input);
778        let (exp_out, exp_phase) = cnot_reference::<2>(63, 64, 3, 1);
779        assert_eq!(got_out, exp_out);
780        assert_eq!(got_c, exp_phase.to_complex());
781    }
782
783    #[test]
784    fn cz_symmetry_w1() {
785        // CZ is symmetric in its qubits.
786        let cz_ab = Clifford2Q::cz(0, 1);
787        let cz_ba = Clifford2Q::cz(1, 0);
788        for pa in 0u8..4 {
789            for pb in 0u8..4 {
790                let input = tensor::<1>(0, 1, pa, pb);
791                let (a, ca) = apply_2q::<1>(&cz_ab, &input);
792                let (b, cb) = apply_2q::<1>(&cz_ba, &input);
793                assert_eq!(a, b, "CZ(0,1) and CZ(1,0) disagree on ({}, {})", pa, pb);
794                assert_eq!(ca, cb);
795            }
796        }
797    }
798
799    #[test]
800    fn cz_generator_rules_w1() {
801        let cz = Clifford2Q::cz(0, 1);
802        // X⊗I → X⊗Z
803        let (o, c) = apply_2q::<1>(&cz, &tensor::<1>(0, 1, 1, 0));
804        assert_eq!(o, tensor::<1>(0, 1, 1, 2));
805        assert_eq!(c, Complex64::new(1.0, 0.0));
806        // I⊗X → Z⊗X
807        let (o, c) = apply_2q::<1>(&cz, &tensor::<1>(0, 1, 0, 1));
808        assert_eq!(o, tensor::<1>(0, 1, 2, 1));
809        assert_eq!(c, Complex64::new(1.0, 0.0));
810        // Z⊗I → Z⊗I
811        let (o, c) = apply_2q::<1>(&cz, &tensor::<1>(0, 1, 2, 0));
812        assert_eq!(o, tensor::<1>(0, 1, 2, 0));
813        assert_eq!(c, Complex64::new(1.0, 0.0));
814        // I⊗Z → I⊗Z
815        let (o, c) = apply_2q::<1>(&cz, &tensor::<1>(0, 1, 0, 2));
816        assert_eq!(o, tensor::<1>(0, 1, 0, 2));
817        assert_eq!(c, Complex64::new(1.0, 0.0));
818    }
819
820    #[test]
821    fn swap_table_w1() {
822        let swap = Clifford2Q::swap(0, 1);
823        // II → II, XI → IX, IY → YI, XZ → ZX
824        let (o, c) = apply_2q::<1>(&swap, &tensor::<1>(0, 1, 0, 0));
825        assert_eq!(o, tensor::<1>(0, 1, 0, 0));
826        assert_eq!(c, Complex64::new(1.0, 0.0));
827        let (o, c) = apply_2q::<1>(&swap, &tensor::<1>(0, 1, 1, 0));
828        assert_eq!(o, tensor::<1>(0, 1, 0, 1));
829        assert_eq!(c, Complex64::new(1.0, 0.0));
830        let (o, c) = apply_2q::<1>(&swap, &tensor::<1>(0, 1, 0, 3));
831        assert_eq!(o, tensor::<1>(0, 1, 3, 0));
832        assert_eq!(c, Complex64::new(1.0, 0.0));
833        let (o, c) = apply_2q::<1>(&swap, &tensor::<1>(0, 1, 1, 2));
834        assert_eq!(o, tensor::<1>(0, 1, 2, 1));
835        assert_eq!(c, Complex64::new(1.0, 0.0));
836    }
837}