paulistrings/
phase.rs

1//! [`Phase`] — `i^k` factors arising from Pauli algebra, with `k ∈ {0, 1, 2, 3}`.
2//!
3//! Multiplication of two Pauli bitstrings produces an `i^k` factor wherever
4//! X- and Z-bits coincide (see [`PauliString::mul_assign`]); callers fold
5//! this into the relevant `Complex64` coefficient at the boundary
6//! ([`PauliSum`], [`BuildAccumulator`], channel `apply`). [`Phase`]
7//! centralizes that arithmetic so every site uses the same combine/apply
8//! rules instead of open-coded `u8 & 3` masks and four-arm match statements.
9//!
10//! # Examples
11//!
12//! Combine phases with `+` (mod 4) and fold one into a `Complex64`
13//! coefficient with [`Phase::apply`]:
14//!
15//! ```
16//! use paulistrings::Phase;
17//! use num_complex::Complex64;
18//!
19//! let p = Phase::I + Phase::I; // i · i = -1
20//! assert_eq!(p, Phase::MINUS_ONE);
21//!
22//! let folded = Phase::I.apply(Complex64::new(2.0, 3.0)); // i · (2 + 3i) = -3 + 2i
23//! assert_eq!(folded, Complex64::new(-3.0, 2.0));
24//! ```
25//!
26//! [`PauliString::mul_assign`]: crate::PauliString::mul_assign
27//! [`PauliSum`]: crate::PauliSum
28//! [`BuildAccumulator`]: crate::BuildAccumulator
29
30use num_complex::Complex64;
31use std::ops::{Add, AddAssign};
32
33/// A phase factor `i^k` where `k ∈ {0, 1, 2, 3}`.
34///
35/// Layout is `#[repr(transparent)] u8`, so `Phase` is zero-cost: a plain
36/// arithmetic byte at the ABI level. Construction via `Phase::new` reduces
37/// mod 4; the `Add` impl preserves that invariant.
38#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
39#[repr(transparent)]
40pub struct Phase(u8);
41
42impl Phase {
43    /// `i^0 = 1`.
44    pub const ONE: Self = Self(0);
45    /// `i^1 = i`.
46    pub const I: Self = Self(1);
47    /// `i^2 = -1`.
48    pub const MINUS_ONE: Self = Self(2);
49    /// `i^3 = -i`.
50    pub const MINUS_I: Self = Self(3);
51
52    /// Construct from a raw exponent. Reduces mod 4, so `Phase::new(5) ==
53    /// Phase::I`.
54    #[inline]
55    pub const fn new(k: u8) -> Self {
56        Self(k & 3)
57    }
58
59    /// The raw exponent in `0..=3`.
60    #[inline]
61    pub const fn exponent(self) -> u8 {
62        self.0
63    }
64
65    /// `i^k` as a `Complex64`.
66    #[inline]
67    pub fn to_complex(self) -> Complex64 {
68        match self.0 {
69            0 => Complex64::new(1.0, 0.0),
70            1 => Complex64::new(0.0, 1.0),
71            2 => Complex64::new(-1.0, 0.0),
72            3 => Complex64::new(0.0, -1.0),
73            _ => unreachable!(),
74        }
75    }
76
77    /// Multiply `c` by `i^k` without going through `to_complex`. Each branch
78    /// is a single sign/swap on the `(re, im)` parts — no FP multiply.
79    #[inline]
80    pub fn apply(self, c: Complex64) -> Complex64 {
81        match self.0 {
82            0 => c,
83            1 => Complex64::new(-c.im, c.re),
84            2 => Complex64::new(-c.re, -c.im),
85            3 => Complex64::new(c.im, -c.re),
86            _ => unreachable!(),
87        }
88    }
89}
90
91impl Add for Phase {
92    type Output = Phase;
93    #[inline]
94    fn add(self, other: Phase) -> Phase {
95        // Both operands are already in `0..=3`, so the sum is in `0..=6` and
96        // a single mask suffices.
97        Phase((self.0 + other.0) & 3)
98    }
99}
100
101impl AddAssign for Phase {
102    #[inline]
103    fn add_assign(&mut self, other: Phase) {
104        *self = *self + other;
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn constants_have_expected_exponents() {
114        assert_eq!(Phase::ONE.exponent(), 0);
115        assert_eq!(Phase::I.exponent(), 1);
116        assert_eq!(Phase::MINUS_ONE.exponent(), 2);
117        assert_eq!(Phase::MINUS_I.exponent(), 3);
118    }
119
120    #[test]
121    fn new_reduces_mod_4() {
122        assert_eq!(Phase::new(0), Phase::ONE);
123        assert_eq!(Phase::new(1), Phase::I);
124        assert_eq!(Phase::new(4), Phase::ONE);
125        assert_eq!(Phase::new(5), Phase::I);
126        assert_eq!(Phase::new(255), Phase::MINUS_I); // 255 & 3 == 3
127    }
128
129    #[test]
130    fn to_complex_matches_i_powers() {
131        assert_eq!(Phase::ONE.to_complex(), Complex64::new(1.0, 0.0));
132        assert_eq!(Phase::I.to_complex(), Complex64::new(0.0, 1.0));
133        assert_eq!(Phase::MINUS_ONE.to_complex(), Complex64::new(-1.0, 0.0));
134        assert_eq!(Phase::MINUS_I.to_complex(), Complex64::new(0.0, -1.0));
135    }
136
137    #[test]
138    fn apply_agrees_with_to_complex_times_c() {
139        let c = Complex64::new(2.0, 3.0);
140        for p in [Phase::ONE, Phase::I, Phase::MINUS_ONE, Phase::MINUS_I] {
141            assert_eq!(p.apply(c), p.to_complex() * c);
142        }
143    }
144
145    #[test]
146    fn add_wraps_mod_4() {
147        assert_eq!(Phase::I + Phase::I, Phase::MINUS_ONE);
148        assert_eq!(Phase::MINUS_ONE + Phase::I, Phase::MINUS_I);
149        assert_eq!(Phase::MINUS_I + Phase::I, Phase::ONE);
150        assert_eq!(Phase::MINUS_I + Phase::MINUS_I, Phase::MINUS_ONE);
151    }
152
153    #[test]
154    fn add_assign_wraps_mod_4() {
155        let mut p = Phase::I;
156        p += Phase::I;
157        assert_eq!(p, Phase::MINUS_ONE);
158        p += Phase::MINUS_I;
159        assert_eq!(p, Phase::I);
160    }
161
162    #[test]
163    fn repr_transparent_size_is_one_byte() {
164        use std::mem::size_of;
165        assert_eq!(size_of::<Phase>(), 1);
166    }
167}