Skip to main content

ppvm_tableau/gates/
rot2.rs

1// SPDX-FileCopyrightText: 2026 The PPVM Authors
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::HashMap;
5
6use bitvec::view::BitView;
7use num::PrimInt;
8use num::{
9    Complex, One, Zero,
10    complex::{Complex64, ComplexFloat},
11};
12
13use crate::prelude::*;
14
15const PAULIS: [Pauli; 4] = [Pauli::I, Pauli::X, Pauli::Z, Pauli::Y];
16
17impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> RotationTwo<T>
18    for GeneralizedTableau<T, I, C>
19where
20    <<T as Config>::Storage as BitView>::Store: PrimInt,
21    T::Coeff: Zero + One + Send + Sync + num::Num + PartialOrd,
22    I: TableauIndex + Send + Sync,
23    Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
24        + std::ops::AddAssign
25        + From<Complex64>
26        + ComplexFloat
27        + Copy,
28{
29    fn rotate_2(
30        &mut self,
31        axis_a: [u8; 2],
32        axis_b: [u8; 2],
33        a: usize,
34        b: usize,
35        theta: <T as Config>::Coeff,
36    ) {
37        let [axis_a_x, axis_a_z] = axis_a;
38        let [axis_b_x, axis_b_z] = axis_b;
39        let pauli_a = PAULIS[(axis_a_z << 1 | axis_a_x) as usize];
40        let pauli_b = PAULIS[(axis_b_z << 1 | axis_b_x) as usize];
41        // NOTE: if both qubits are lost/leaked, the rot1 will be a no-op
42        if self.is_inactive(a) {
43            return self.rotate_1(pauli_b, b, theta);
44        } else if self.is_inactive(b) {
45            return self.rotate_1(pauli_a, a, theta);
46        }
47
48        let (sin, cos) = (theta * 0.5.into()).sin_cos();
49
50        let complex_cos: Complex<T::Coeff> = Complex {
51            re: cos,
52            im: T::Coeff::zero(),
53        };
54
55        let i_complex_sin: Complex<T::Coeff> = Complex {
56            re: T::Coeff::zero(),
57            im: -sin,
58        };
59
60        let mut branch_coefficients = self.coefficients.clone();
61        self.compute_coefficients_after_pauli_apply(&mut branch_coefficients, b, pauli_b);
62        self.compute_coefficients_after_pauli_apply(&mut branch_coefficients, a, pauli_a);
63
64        let old_coefficients = std::mem::replace(&mut self.coefficients, C::new());
65        let mut new_coefficients: HashMap<I, Complex<T::Coeff>> = HashMap::new();
66
67        for (coeff, idx) in old_coefficients {
68            *new_coefficients.entry(idx).or_insert(Complex::zero()) += complex_cos * coeff;
69        }
70
71        for (branch_coeff, idx) in branch_coefficients {
72            *new_coefficients.entry(idx).or_insert(Complex::zero()) += i_complex_sin * branch_coeff;
73        }
74
75        let cutoff = Complex {
76            re: self.coefficient_threshold.clone(),
77            im: T::Coeff::zero(),
78        };
79
80        for (idx, coeff) in new_coefficients {
81            if coeff.abs() > cutoff.abs() {
82                self.coefficients.unsafe_insert(idx, coeff);
83            }
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91    use ppvm_pauli_sum::config::fxhash::ByteF64;
92    use std::f64::consts::{FRAC_PI_2, PI};
93
94    type TestConfig = ByteF64<1>;
95    type TestTableau = GeneralizedTableau<TestConfig>;
96
97    // --- rxx ---
98
99    /// rxx(π) = exp(-i·π/2·XX): Clifford, no branching, maps |00⟩ → -i|11⟩.
100    #[test]
101    fn test_rxx_pi_flips_both_to_one() {
102        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
103        tab.rxx(0, 1, PI);
104        assert_eq!(tab.coefficients.len(), 1, "rxx(π) should not branch");
105        assert!(tab.measure(0).unwrap());
106        assert!(tab.measure(1).unwrap());
107    }
108
109    /// rxx(π/2) on |00⟩ creates a Bell-like state (|00⟩ - i|11⟩)/√2: two branches.
110    #[test]
111    fn test_rxx_half_pi_branches() {
112        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
113        tab.rxx(0, 1, FRAC_PI_2);
114        assert_eq!(
115            tab.coefficients.len(),
116            2,
117            "rxx(π/2) should create 2 branches"
118        );
119    }
120
121    /// Two rxx(π/2) compose to rxx(π): |00⟩ → -i|11⟩ (deterministic).
122    #[test]
123    fn test_rxx_half_pi_twice_flips_both() {
124        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
125        tab.rxx(0, 1, FRAC_PI_2);
126        tab.rxx(0, 1, FRAC_PI_2);
127        assert!(tab.measure(0).unwrap());
128        assert!(tab.measure(1).unwrap());
129    }
130
131    // --- ryy ---
132
133    /// ryy(π) = exp(-i·π/2·YY): Clifford, no branching.
134    /// YY|00⟩ = (i|1⟩)⊗(i|1⟩) = -|11⟩, so ryy(π)|00⟩ = i|11⟩.
135    #[test]
136    fn test_ryy_pi_flips_both_to_one() {
137        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
138        tab.ryy(0, 1, PI);
139        assert_eq!(tab.coefficients.len(), 1, "ryy(π) should not branch");
140        assert!(tab.measure(0).unwrap());
141        assert!(tab.measure(1).unwrap());
142    }
143
144    /// ryy(π/2) on |00⟩ creates a Bell-like state (|00⟩ + i|11⟩)/√2: two branches.
145    #[test]
146    fn test_ryy_half_pi_branches() {
147        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
148        tab.ryy(0, 1, FRAC_PI_2);
149        assert_eq!(
150            tab.coefficients.len(),
151            2,
152            "ryy(π/2) should create 2 branches"
153        );
154    }
155
156    /// Two ryy(π/2) compose to ryy(π): |00⟩ → i|11⟩ (deterministic).
157    #[test]
158    fn test_ryy_half_pi_twice_flips_both() {
159        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
160        tab.ryy(0, 1, FRAC_PI_2);
161        tab.ryy(0, 1, FRAC_PI_2);
162        assert!(tab.measure(0).unwrap());
163        assert!(tab.measure(1).unwrap());
164    }
165
166    // --- rzz ---
167
168    /// rzz(π) on |00⟩: ZZ|00⟩ = |00⟩ (eigenvalue +1), so rzz(π)|00⟩ = -i|00⟩.
169    /// No branching since ZZ is already a stabilizer of |00⟩.
170    #[test]
171    fn test_rzz_pi_leaves_00() {
172        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
173        tab.rzz(0, 1, PI);
174        assert_eq!(
175            tab.coefficients.len(),
176            1,
177            "rzz(π) on |00⟩ should not branch"
178        );
179        assert!(!tab.measure(0).unwrap());
180        assert!(!tab.measure(1).unwrap());
181    }
182
183    /// rzz never branches on a computational basis state: ZZ is diagonal in the Z basis.
184    #[test]
185    fn test_rzz_does_not_branch_on_comp_basis() {
186        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
187        tab.rzz(0, 1, FRAC_PI_2);
188        assert_eq!(
189            tab.coefficients.len(),
190            1,
191            "rzz(π/2) on |00⟩ should not branch: ZZ is a stabilizer"
192        );
193    }
194
195    /// rzz(π) on |10⟩: ZZ|10⟩ = Z|1⟩⊗Z|0⟩ = -|10⟩ (eigenvalue -1), so rzz(π)|10⟩ = i|10⟩.
196    #[test]
197    fn test_rzz_pi_on_10() {
198        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
199        tab.x(0);
200        tab.rzz(0, 1, PI);
201        assert_eq!(
202            tab.coefficients.len(),
203            1,
204            "rzz(π) on |10⟩ should not branch"
205        );
206        assert!(tab.measure(0).unwrap());
207        assert!(!tab.measure(1).unwrap());
208    }
209
210    // --- branching + physical correlation checks ---
211
212    /// rxx(π/2)|00⟩ = (|00⟩ - i|11⟩)/√2: XX only connects |00⟩↔|11⟩, so both
213    /// qubits always give the same measurement outcome.
214    #[test]
215    fn test_rxx_half_pi_correlated_measurements() {
216        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
217        tab.rxx(0, 1, FRAC_PI_2);
218        assert_eq!(
219            tab.coefficients.len(),
220            2,
221            "rxx(π/2) should create 2 branches"
222        );
223        let r0 = tab.measure(0);
224        let r1 = tab.measure(1);
225        assert_eq!(
226            r0, r1,
227            "rxx(π/2)|00⟩ = (|00⟩-i|11⟩)/√2: measurements must agree"
228        );
229    }
230
231    /// rxx(π/2)|10⟩ = (|10⟩ - i|01⟩)/√2: XX connects |10⟩↔|01⟩, so the two
232    /// qubits always give opposite measurement outcomes.
233    #[test]
234    fn test_rxx_half_pi_on_10_anticorrelated_measurements() {
235        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
236        tab.x(0);
237        tab.rxx(0, 1, FRAC_PI_2);
238        assert_eq!(
239            tab.coefficients.len(),
240            2,
241            "rxx(π/2) on |10⟩ should create 2 branches"
242        );
243        let r0 = tab.measure(0);
244        let r1 = tab.measure(1);
245        assert_ne!(
246            r0, r1,
247            "rxx(π/2)|10⟩ = (|10⟩-i|01⟩)/√2: measurements must differ"
248        );
249    }
250
251    /// ryy(π/2)|00⟩ = (|00⟩ + i|11⟩)/√2: YY|00⟩ = -|11⟩, so ryy(π/2) only
252    /// populates |00⟩ and |11⟩. Both qubits always give the same outcome.
253    #[test]
254    fn test_ryy_half_pi_correlated_measurements() {
255        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
256        tab.ryy(0, 1, FRAC_PI_2);
257        assert_eq!(
258            tab.coefficients.len(),
259            2,
260            "ryy(π/2) should create 2 branches"
261        );
262        let r0 = tab.measure(0);
263        let r1 = tab.measure(1);
264        assert_eq!(
265            r0, r1,
266            "ryy(π/2)|00⟩ = (|00⟩+i|11⟩)/√2: measurements must agree"
267        );
268    }
269
270    /// H·rzz(π/2)·H on both qubits equals rxx(π/2) (since H·Z·H = X).
271    /// Applying rzz(π/2) to |++⟩ must create 2 branches; after the second H⊗H the
272    /// state is (|00⟩ - i|11⟩)/√2 and both qubits must give the same outcome.
273    #[test]
274    fn test_rzz_half_pi_on_plus_plus_branches_and_correlates() {
275        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-12);
276        tab.h(0);
277        tab.h(1);
278        tab.rzz(0, 1, FRAC_PI_2);
279        assert_eq!(
280            tab.coefficients.len(),
281            2,
282            "rzz(π/2) on |++⟩ should create 2 branches"
283        );
284        tab.h(0);
285        tab.h(1);
286        // H⊗H · rzz(π/2) · H⊗H |00⟩ = rxx(π/2)|00⟩ = (|00⟩ - i|11⟩)/√2
287        let r0 = tab.measure(0);
288        let r1 = tab.measure(1);
289        assert_eq!(r0, r1, "H·rzz(π/2)·H = rxx(π/2): measurements must agree");
290    }
291}