Skip to main content

ppvm_tableau/gates/
rot1.rs

1// SPDX-FileCopyrightText: 2026 The PPVM Authors
2// SPDX-License-Identifier: Apache-2.0
3
4use bitvec::view::BitView;
5use num::PrimInt;
6use num::{
7    Complex, One, Zero,
8    complex::{Complex64, ComplexFloat},
9};
10
11use crate::prelude::*;
12
13impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> RotationOne<T>
14    for GeneralizedTableau<T, I, C>
15where
16    <<T as Config>::Storage as BitView>::Store: PrimInt,
17    T::Coeff: Zero + One + Send + Sync + num::Num + PartialOrd,
18    I: TableauIndex + Send + Sync,
19    Complex<T::Coeff>: std::ops::Mul<Output = Complex<T::Coeff>>
20        + std::ops::AddAssign
21        + From<Complex64>
22        + ComplexFloat
23        + Copy,
24{
25    fn rotate_1(&mut self, axis: Pauli, addr0: usize, theta: <T as Config>::Coeff) {
26        if self.is_inactive(addr0) {
27            return;
28        }
29        let (sin, cos) = (theta * 0.5.into()).sin_cos();
30
31        let complex_cos: Complex<T::Coeff> = Complex {
32            re: cos,
33            im: T::Coeff::zero(),
34        };
35
36        let i_complex_sin: Complex<T::Coeff> = Complex {
37            re: T::Coeff::zero(),
38            im: -sin,
39        };
40
41        self.branch_with_coefficients(addr0, axis, complex_cos, i_complex_sin);
42    }
43}
44
45impl<T: Config, I, C: SparseVector<Complex<T::Coeff>, I>> RotXY<T> for GeneralizedTableau<T, I, C>
46where
47    GeneralizedTableau<T, I, C>: RotationOne<T>,
48{
49    fn r(&mut self, addr0: usize, axis_angle: T::Coeff, theta: T::Coeff) {
50        // R(axis_angle, θ) = RZ(axis_angle)·RX(θ)·RZ(−axis_angle). The tableau
51        // runs in the Schrödinger picture, so the sub-rotations are applied in
52        // forward order: RZ(−axis_angle) first, then RX(θ), then RZ(axis_angle).
53        self.rz(addr0, -axis_angle.clone());
54        self.rx(addr0, theta);
55        self.rz(addr0, axis_angle);
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use ppvm_pauli_sum::config::fxhash::ByteF64;
63    use std::f64::consts::{FRAC_PI_2, PI};
64
65    type TestConfig = ByteF64<1>;
66    type TestTableau = GeneralizedTableau<TestConfig>;
67
68    /// rx(π) = exp(-i·π/2·X) acts as -iX on |0⟩, giving -i|1⟩.
69    #[test]
70    fn test_rx_pi_flips_zero_to_one() {
71        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
72        tab.rx(0, PI);
73
74        // make sure we don't branch
75        assert_eq!(tab.coefficients.len(), 1);
76
77        let result = tab.measure(0);
78        assert!(result.unwrap(), "Expected to measure 1, got {:?}", result);
79    }
80
81    #[test]
82    fn test_ry_pi_flips_zero_to_one() {
83        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
84        tab.ry(0, PI);
85        // make sure we don't branch
86        assert_eq!(tab.coefficients.len(), 1);
87        let result = tab.measure(0);
88        assert!(result.unwrap(), "Expected to measure 1, got {:?}", result);
89    }
90
91    #[test]
92    fn test_rz_leaves_zero_invariant() {
93        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
94        tab.rz(0, 0.123);
95        // make sure we don't branch
96        assert_eq!(tab.coefficients.len(), 1);
97        let result = tab.measure(0);
98        assert!(!result.unwrap(), "Expected to measure 0, got {:?}", result);
99    }
100
101    #[test]
102    fn test_rx_pi_2_statistics() {
103        let mut tab: TestTableau = GeneralizedTableau::new_with_seed(1, 1e-12, 0);
104        tab.rx(0, FRAC_PI_2);
105        // make sure we branch
106        assert_eq!(tab.coefficients.len(), 2);
107
108        let trials = 100;
109        let mut count_one = 0;
110        for i in 0..trials {
111            let mut tmp_tab = tab.fork(Some(i));
112            let result = tmp_tab.measure(0);
113            count_one += result.unwrap() as u8;
114        }
115        println!("{}", count_one);
116        assert!(35 < count_one && count_one < 65);
117    }
118
119    #[test]
120    fn test_ry_round_trip() {
121        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-10);
122        tab.ry(0, 2.0 * PI);
123
124        assert_eq!(tab.coefficients.len(), 1);
125
126        assert!(!tab.measure(0).unwrap());
127    }
128
129    /// R(axis_angle=0, θ=0) = identity: |0⟩ stays |0⟩, no branching.
130    #[test]
131    fn test_r_identity() {
132        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
133        tab.r(0, 0.0, 0.0);
134        assert_eq!(tab.coefficients.len(), 1);
135        assert!(!tab.measure(0).unwrap());
136    }
137
138    /// R(axis_angle=0, θ=π) = RX(π): flips |0⟩ → |1⟩ with no branching.
139    #[test]
140    fn test_r_axis_zero_is_rx() {
141        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
142        tab.r(0, 0.0, PI);
143        assert_eq!(tab.coefficients.len(), 1);
144        assert!(tab.measure(0).unwrap());
145    }
146
147    /// R(axis_angle=π/2, θ=π) = RY(π): flips |0⟩ → |1⟩ with no branching.
148    #[test]
149    fn test_r_axis_half_pi_is_ry() {
150        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
151        tab.r(0, FRAC_PI_2, PI);
152        assert_eq!(tab.coefficients.len(), 1);
153        assert!(tab.measure(0).unwrap());
154    }
155
156    /// A partial rotation about an in-plane axis branches into two terms.
157    #[test]
158    fn test_r_branches() {
159        let mut tab: TestTableau = GeneralizedTableau::new(1, 1e-12);
160        tab.r(0, 0.37 * PI, FRAC_PI_2);
161        assert_eq!(tab.coefficients.len(), 2);
162    }
163
164    /// R(axis_angle, θ) must give identical per-seed measurement statistics
165    /// to the manual decomposition RZ(axis_angle)·RX(θ)·RZ(−axis_angle).
166    #[test]
167    fn test_r_matches_rz_rx_rz() {
168        let (axis_angle, theta) = (0.21 * PI, 0.34 * PI);
169
170        let mut tab_r: TestTableau = GeneralizedTableau::new_with_seed(1, 1e-12, 0);
171        tab_r.r(0, axis_angle, theta);
172
173        let mut tab_manual: TestTableau = GeneralizedTableau::new_with_seed(1, 1e-12, 0);
174        tab_manual.rz(0, -axis_angle);
175        tab_manual.rx(0, theta);
176        tab_manual.rz(0, axis_angle);
177
178        for seed in 0..200 {
179            let result_r = tab_r.fork(Some(seed)).measure(0).unwrap();
180            let result_manual = tab_manual.fork(Some(seed)).measure(0).unwrap();
181            assert_eq!(result_r, result_manual, "mismatch at seed {}", seed);
182        }
183    }
184
185    #[test]
186    fn test_two_qubit_case() {
187        let mut tab: TestTableau = GeneralizedTableau::new(2, 1e-10);
188        tab.rx(0, PI);
189
190        assert_eq!(tab.coefficients.len(), 1);
191
192        assert!(!tab.measure(1).unwrap());
193        assert!(tab.measure(0).unwrap());
194
195        tab.rx(1, PI);
196
197        assert_eq!(tab.coefficients.len(), 1);
198
199        assert!(tab.measure(1).unwrap());
200        assert!(tab.measure(0).unwrap());
201
202        tab.rx(0, PI);
203
204        assert_eq!(tab.coefficients.len(), 1);
205
206        assert!(tab.measure(1).unwrap());
207        assert!(!tab.measure(0).unwrap());
208
209        tab.rx(1, PI);
210
211        assert_eq!(tab.coefficients.len(), 1);
212
213        assert!(!tab.measure(1).unwrap());
214        assert!(!tab.measure(0).unwrap());
215    }
216}