paulistrings/
pauli_string.rs

1//! [`PauliString<W>`], the symplectic `(x, z)` encoding of a Pauli operator on up to `64·W` qubits.
2//! The derived `Ord`, lexicographic in `x` then `z`, must agree with the `(x, z)` tuple order every bucket of a [`PauliSum`](crate::PauliSum) is sorted in.
3
4use num_complex::Complex64;
5
6use crate::phase::Phase;
7
8/// A Pauli operator on up to `64·W` qubits, `W` being the number of 64-bit words per `x`/`z` part.
9///
10/// Pick the smallest `W` that fits; the Python bindings monomorphize `W ∈ {1, 2, 4, 8, 16}`.
11/// The phase of a product is not stored: [`Self::mul_assign`] returns it as a [`Phase`] for the caller to fold into a coefficient.
12/// `Y` is the key `(x=1, z=1)` with no phase factor.
13#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
14#[repr(C)]
15pub struct PauliString<const W: usize> {
16    /// X-part bitmask: bit `q` set iff qubit `q` is `X` or `Y`.
17    pub x: [u64; W],
18    /// Z-part bitmask: bit `q` set iff qubit `q` is `Z` or `Y`.
19    pub z: [u64; W],
20}
21
22impl<const W: usize> PauliString<W> {
23    /// Identity Pauli string (all qubits `I`).
24    pub const fn identity() -> Self {
25        Self { x: [0u64; W], z: [0u64; W] }
26    }
27
28    /// Single-qubit `X` Pauli on `qubit`.
29    ///
30    /// # Panics
31    ///
32    /// Panics in debug builds if `qubit >= 64 · W`.
33    pub fn x(qubit: usize) -> Self {
34        debug_assert!(qubit < 64 * W);
35        let mut p = Self::identity();
36        p.x[qubit / 64] = 1u64 << (qubit % 64);
37        p
38    }
39
40    /// Single-qubit `Y = (x=1, z=1)` on `qubit`; the `i` in `Y = i·X·Z` is the caller's concern.
41    ///
42    /// # Panics
43    ///
44    /// Panics in debug builds if `qubit >= 64 · W`.
45    pub fn y(qubit: usize) -> Self {
46        debug_assert!(qubit < 64 * W);
47        let mut p = Self::identity();
48        let word = qubit / 64;
49        let bit = 1u64 << (qubit % 64);
50        p.x[word] = bit;
51        p.z[word] = bit;
52        p
53    }
54
55    /// Single-qubit `Z` Pauli on `qubit`.
56    ///
57    /// # Panics
58    ///
59    /// Panics in debug builds if `qubit >= 64 · W`.
60    pub fn z(qubit: usize) -> Self {
61        debug_assert!(qubit < 64 * W);
62        let mut p = Self::identity();
63        p.z[qubit / 64] = 1u64 << (qubit % 64);
64        p
65    }
66
67    /// Number of non-identity qubits.
68    pub fn weight(&self) -> u32 {
69        (0..W).map(|i| (self.x[i] | self.z[i]).count_ones()).sum()
70    }
71
72    /// Multiply `self * other` in place, returning the phase `i^k` such that the true product is `phase · self`.
73    pub fn mul_assign(&mut self, other: &Self) -> Phase {
74        // Per qubit, P(a,b)·P(c,d) = i^δ·P(a⊕c, b⊕d) with δ = 2bc + ab + cd − (a⊕c)(b⊕d) mod 4, from P(a,b) = i^{ab}·X^a·Z^b.
75        let mut delta: u32 = 0;
76        for i in 0..W {
77            let a = self.x[i];
78            let b = self.z[i];
79            let c = other.x[i];
80            let d = other.z[i];
81            let zc_x = (b & c).count_ones();
82            let y_self = (a & b).count_ones();
83            let y_other = (c & d).count_ones();
84            let y_result = ((a ^ c) & (b ^ d)).count_ones();
85            delta = delta.wrapping_add(zc_x.wrapping_mul(2));
86            delta = delta.wrapping_add(y_self);
87            delta = delta.wrapping_add(y_other);
88            delta = delta.wrapping_sub(y_result);
89            self.x[i] ^= c;
90            self.z[i] ^= d;
91        }
92        Phase::new(delta as u8)
93    }
94
95    /// Value-returning multiply: `(self * other, phase)`.
96    pub fn product(mut self, other: &Self) -> (Self, Phase) {
97        let phase = self.mul_assign(other);
98        (self, phase)
99    }
100
101    /// `true` iff every set bit lies on a qubit index `< num_qubits`.
102    ///
103    /// # Panics
104    ///
105    /// Panics in debug builds if `num_qubits > 64 · W`.
106    pub fn is_within(&self, num_qubits: usize) -> bool {
107        debug_assert!(num_qubits <= 64 * W);
108        let mut leak: u64 = 0;
109        for i in 0..W {
110            leak |= (self.x[i] | self.z[i]) & !word_mask(num_qubits, i);
111        }
112        leak == 0
113    }
114
115    /// `true` iff `self` and `other` commute as Pauli operators.
116    pub fn commutes_with(&self, other: &Self) -> bool {
117        // Parity is GF(2)-linear, so XOR-fold the words and take one popcount.
118        let mut folded: u64 = 0;
119        for i in 0..W {
120            folded ^= (self.x[i] & other.z[i]) ^ (self.z[i] & other.x[i]);
121        }
122        folded.count_ones() & 1 == 0
123    }
124
125    /// Commutator `[self, other] = self·other − other·self`, as `(product, coefficient)`.
126    ///
127    /// The coefficient is `2·i^k` when the strings anticommute and exactly `0` when they commute; the returned string is [`Self::product`]'s result either way.
128    pub fn commutator(self, other: &Self) -> (Self, Complex64) {
129        let vanishes = self.commutes_with(other);
130        let (product, phase) = self.product(other);
131        (product, scaled_phase(phase, vanishes))
132    }
133
134    /// Anticommutator `{self, other} = self·other + other·self`, as `(product, coefficient)`.
135    ///
136    /// The coefficient is `2·i^k` when the strings commute and exactly `0` when they anticommute.
137    pub fn anticommutator(self, other: &Self) -> (Self, Complex64) {
138        let vanishes = !self.commutes_with(other);
139        let (product, phase) = self.product(other);
140        (product, scaled_phase(phase, vanishes))
141    }
142}
143
144/// Mask of the live qubit bits in word `word`, given `num_qubits` total.
145pub(crate) fn word_mask(num_qubits: usize, word: usize) -> u64 {
146    let first_qubit = 64 * word;
147    if num_qubits >= first_qubit + 64 {
148        !0u64
149    } else if num_qubits <= first_qubit {
150        0
151    } else {
152        (1u64 << (num_qubits - first_qubit)) - 1
153    }
154}
155
156/// The bit mask of `qubits`, each asserted below `num_qubits`.
157pub(crate) fn qubit_mask<const W: usize>(
158    qubits: impl IntoIterator<Item = usize>,
159    num_qubits: usize,
160) -> [u64; W] {
161    let mut mask = [0u64; W];
162    for q in qubits {
163        assert!(q < num_qubits, "qubit {q} is outside the {num_qubits}-qubit register");
164        mask[q / 64] |= 1u64 << (q % 64);
165    }
166    mask
167}
168
169/// `0` when the bracket vanishes, `2·i^k` otherwise.
170fn scaled_phase(phase: Phase, vanishes: bool) -> Complex64 {
171    if vanishes {
172        Complex64::new(0.0, 0.0)
173    } else {
174        phase.to_complex() * 2.0
175    }
176}
177
178impl<const W: usize> Default for PauliString<W> {
179    fn default() -> Self {
180        Self::identity()
181    }
182}
183
184#[cfg(test)]
185mod tests;