paulistrings/
pauli_string.rs

1//! [`PauliString<W>`] — symplectic-encoded Pauli operator on up to `64·W` qubits.
2//!
3//! Encoding: `I = (0, 0)`, `X = (1, 0)`, `Z = (0, 1)`, `Y = (1, 1)`.
4//! Multiplication XORs the `(x, z)` parts and returns the `i^k` phase factor
5//! — for `k ∈ 0..4` — that arises where X- and Z-bits coincide. The phase is
6//! not stored on the type; callers fold it into a `Complex64` coefficient at
7//! the boundary ([`PauliSum`], [`BuildAccumulator`], or [`Channel::apply`]).
8//!
9//! The load-bearing trait is [`Ord`] — the propagation engine is sort-based,
10//! not hashmap-based. `Hash` is implemented as an auxiliary for
11//! [`BuildAccumulator`].
12//!
13//! See design doc §3.1.
14//!
15//! # Examples
16//!
17//! `X · Z = -i·Y`: the XOR gives the Y bits and the returned [`Phase`] is the
18//! `-i` factor.
19//!
20//! ```
21//! use paulistrings::{PauliString, Phase};
22//!
23//! let mut p = PauliString::<1>::x(0);
24//! let phase = p.mul_assign(&PauliString::<1>::z(0));
25//! assert_eq!(p, PauliString::<1>::y(0));
26//! assert_eq!(phase, Phase::MINUS_I);
27//! ```
28//!
29//! [`PauliSum`]: crate::PauliSum
30//! [`BuildAccumulator`]: crate::BuildAccumulator
31//! [`Channel::apply`]: crate::Channel::apply
32//! [`Phase`]: crate::Phase
33
34#![allow(unused)]
35
36use bytemuck::{Pod, Zeroable};
37use std::cmp::Ordering;
38use std::hash::{Hash, Hasher};
39
40use crate::phase::Phase;
41
42/// A Pauli operator on up to `64 · W` qubits.
43///
44/// Layout is `#[repr(C)]` so the type is `Pod` and can be reinterpreted as
45/// bytes for serialization or upload to a GPU device. There is no stored
46/// phase: multiplication returns the `i^k` phase as a separate [`Phase`]
47/// and callers fold it into their coefficient at the boundary.
48///
49/// [`Ord`] is the load-bearing trait (the engine is sort-based, not
50/// hashmap-based).
51///
52/// # Examples
53///
54/// ```
55/// use paulistrings::PauliString;
56///
57/// let p = PauliString::<1>::x(3);
58/// assert_eq!(p.weight(), 1);
59/// assert!(p.commutes_with(&PauliString::<1>::x(0)));
60/// ```
61#[derive(Clone, Copy, PartialEq, Eq, Debug)]
62#[repr(C)]
63pub struct PauliString<const W: usize> {
64    /// X-part bitmask: bit `q` is set iff the Pauli on qubit `q` has an
65    /// X-component (i.e. is `X` or `Y`).
66    pub x: [u64; W],
67    /// Z-part bitmask: bit `q` is set iff the Pauli on qubit `q` has a
68    /// Z-component (i.e. is `Z` or `Y`).
69    pub z: [u64; W],
70}
71
72unsafe impl<const W: usize> Zeroable for PauliString<W> {}
73unsafe impl<const W: usize> Pod for PauliString<W> {}
74
75impl<const W: usize> PauliString<W> {
76    /// Identity Pauli string (all qubits `I`).
77    ///
78    /// # Examples
79    ///
80    /// ```
81    /// use paulistrings::PauliString;
82    ///
83    /// let id = PauliString::<1>::identity();
84    /// assert_eq!(id.weight(), 0);
85    /// ```
86    pub const fn identity() -> Self {
87        Self {
88            x: [0u64; W],
89            z: [0u64; W],
90        }
91    }
92
93    /// Single-qubit `X` Pauli on `qubit`.
94    ///
95    /// # Panics
96    ///
97    /// Panics in debug builds if `qubit >= 64 · W`.
98    ///
99    /// # Examples
100    ///
101    /// ```
102    /// use paulistrings::PauliString;
103    ///
104    /// let p = PauliString::<1>::x(2);
105    /// assert_eq!(p.x, [0b100]);
106    /// assert_eq!(p.z, [0]);
107    /// ```
108    #[inline]
109    pub fn x(qubit: u32) -> Self {
110        debug_assert!((qubit as usize) < 64 * W);
111        let mut p = Self::identity();
112        p.x[(qubit / 64) as usize] = 1u64 << (qubit % 64);
113        p
114    }
115
116    /// Canonical Pauli `Y = (1, 1)` on `qubit`. The `i` factor in `Y = i · X · Z`
117    /// is the caller's concern — fold it into a [`Phase`] or coefficient at
118    /// the boundary.
119    ///
120    /// # Panics
121    ///
122    /// Panics in debug builds if `qubit >= 64 · W`.
123    #[inline]
124    pub fn y(qubit: u32) -> Self {
125        debug_assert!((qubit as usize) < 64 * W);
126        let mut p = Self::identity();
127        let w = (qubit / 64) as usize;
128        let bit = 1u64 << (qubit % 64);
129        p.x[w] = bit;
130        p.z[w] = bit;
131        p
132    }
133
134    /// Single-qubit `Z` Pauli on `qubit`.
135    ///
136    /// # Panics
137    ///
138    /// Panics in debug builds if `qubit >= 64 · W`.
139    #[inline]
140    pub fn z(qubit: u32) -> Self {
141        debug_assert!((qubit as usize) < 64 * W);
142        let mut p = Self::identity();
143        p.z[(qubit / 64) as usize] = 1u64 << (qubit % 64);
144        p
145    }
146
147    /// Number of non-identity qubits (Hamming weight of `x | z`).
148    ///
149    /// # Examples
150    ///
151    /// ```
152    /// use paulistrings::PauliString;
153    ///
154    /// let mut p = PauliString::<1>::x(0);
155    /// p.mul_assign(&PauliString::<1>::z(1));
156    /// assert_eq!(p.weight(), 2);
157    /// ```
158    #[inline]
159    pub fn weight(&self) -> u32 {
160        (0..W).map(|i| (self.x[i] | self.z[i]).count_ones()).sum()
161    }
162
163    /// Multiply `self * other` in place. Returns the `i^k` phase factor such
164    /// that the true product is `phase · self_after_xor`.
165    ///
166    /// # Examples
167    ///
168    /// `X · Z = -i·Y`: the bits XOR to `Y` and the phase is `-i`.
169    ///
170    /// ```
171    /// use paulistrings::{PauliString, Phase};
172    ///
173    /// let mut p = PauliString::<1>::x(0);
174    /// let phase = p.mul_assign(&PauliString::<1>::z(0));
175    /// assert_eq!(p, PauliString::<1>::y(0));
176    /// assert_eq!(phase, Phase::MINUS_I);
177    /// ```
178    #[inline]
179    pub fn mul_assign(&mut self, other: &Self) -> Phase {
180        // Per-qubit: P(a,b) · P(c,d) = i^δ · P(a⊕c, b⊕d) where
181        //   δ = 2·(b·c) + a·b + c·d − (a⊕c)·(b⊕d)   (mod 4)
182        // (derived from P(a,b) = i^{a·b} X^a Z^b and ZX = -XZ).
183        let mut delta: u32 = 0;
184        for i in 0..W {
185            let a = self.x[i];
186            let b = self.z[i];
187            let c = other.x[i];
188            let d = other.z[i];
189            let zc_x = (b & c).count_ones();
190            let y_self = (a & b).count_ones();
191            let y_other = (c & d).count_ones();
192            let y_result = ((a ^ c) & (b ^ d)).count_ones();
193            delta = delta.wrapping_add(zc_x.wrapping_mul(2));
194            delta = delta.wrapping_add(y_self);
195            delta = delta.wrapping_add(y_other);
196            delta = delta.wrapping_sub(y_result);
197            self.x[i] ^= c;
198            self.z[i] ^= d;
199        }
200        Phase::new(delta as u8)
201    }
202
203    /// Value-returning multiply: `(self * other, phase)`.
204    ///
205    /// Not implemented as `std::ops::Mul` because the operation also
206    /// returns a `Phase` — calling sites need both the bits and the phase
207    /// to fold into a `Complex64` coefficient at the boundary.
208    #[allow(clippy::should_implement_trait)]
209    #[inline]
210    pub fn mul(mut self, other: &Self) -> (Self, Phase) {
211        let phase = self.mul_assign(other);
212        (self, phase)
213    }
214
215    /// `true` iff every set bit lies on a qubit index `< num_qubits`.
216    ///
217    /// The engine and built-in channels preserve this bound by construction
218    /// (a channel only flips bits inside [`Channel::support`], which is
219    /// bounded at [`Circuit`] build time), so this check is not on the hot
220    /// path. Use it in `debug_assert!` at boundaries with custom [`Channel`]
221    /// impls, in `PauliSum::assert_invariants`, and in tests that exercise
222    /// the invariant directly.
223    ///
224    /// # Panics
225    ///
226    /// Panics in debug builds if `num_qubits > 64 · W`.
227    ///
228    /// [`Channel`]: crate::Channel
229    /// [`Channel::support`]: crate::Channel::support
230    /// [`Circuit`]: crate::Circuit
231    #[inline]
232    pub fn is_within(&self, num_qubits: usize) -> bool {
233        debug_assert!(num_qubits <= 64 * W);
234        let mut leak: u64 = 0;
235        for i in 0..W {
236            let lo = 64 * i;
237            let in_bounds: u64 = if num_qubits >= lo + 64 {
238                !0u64
239            } else if num_qubits <= lo {
240                0
241            } else {
242                (1u64 << (num_qubits - lo)) - 1
243            };
244            leak |= (self.x[i] | self.z[i]) & !in_bounds;
245        }
246        leak == 0
247    }
248
249    /// `true` iff `self` and `other` commute as Pauli operators.
250    ///
251    /// # Examples
252    ///
253    /// ```
254    /// use paulistrings::PauliString;
255    ///
256    /// // X and Z anticommute.
257    /// assert!(!PauliString::<1>::x(0).commutes_with(&PauliString::<1>::z(0)));
258    /// // X on disjoint qubits commutes with anything on the other qubit.
259    /// assert!(PauliString::<1>::x(0).commutes_with(&PauliString::<1>::z(1)));
260    /// ```
261    #[inline]
262    pub fn commutes_with(&self, other: &Self) -> bool {
263        let mut parity: u32 = 0;
264        for i in 0..W {
265            parity ^= (self.x[i] & other.z[i]).count_ones();
266            parity ^= (self.z[i] & other.x[i]).count_ones();
267        }
268        parity & 1 == 0
269    }
270}
271
272impl<const W: usize> Default for PauliString<W> {
273    fn default() -> Self {
274        Self::identity()
275    }
276}
277
278impl<const W: usize> Ord for PauliString<W> {
279    /// Lexicographic compare on the concatenation `(x, z)` interpreted as an
280    /// unsigned-integer array, low-to-high word order.
281    fn cmp(&self, other: &Self) -> Ordering {
282        for i in 0..W {
283            match self.x[i].cmp(&other.x[i]) {
284                Ordering::Equal => continue,
285                ord => return ord,
286            }
287        }
288        for i in 0..W {
289            match self.z[i].cmp(&other.z[i]) {
290                Ordering::Equal => continue,
291                ord => return ord,
292            }
293        }
294        Ordering::Equal
295    }
296}
297
298impl<const W: usize> PartialOrd for PauliString<W> {
299    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
300        Some(self.cmp(other))
301    }
302}
303
304impl<const W: usize> Hash for PauliString<W> {
305    /// Auxiliary; only used by `BuildAccumulator` (§8.2). Not on the hot path.
306    fn hash<H: Hasher>(&self, state: &mut H) {
307        self.x.hash(state);
308        self.z.hash(state);
309    }
310}