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}