paulistrings/
accumulator.rs

1//! [`BuildAccumulator<W>`] — hashmap-based ingestion path.
2//!
3//! Used to incrementally build a [`PauliSum`] from unsorted inputs
4//! (Hamiltonian parsing, Python dict construction, etc.). The accumulator is
5//! **not** used during propagation — that path is sort-merge only (see
6//! [`engine`]).
7//!
8//! See the [`PauliSum`] module for a worked example that walks
9//! [`BuildAccumulator::new`] → [`BuildAccumulator::add_term`] →
10//! [`BuildAccumulator::finalize`].
11//!
12//! See design doc §8.2.
13//!
14//! [`PauliSum`]: crate::PauliSum
15//! [`engine`]: crate::engine
16
17#![allow(unused)]
18
19use crate::pauli_string::PauliString;
20use crate::pauli_sum::PauliSum;
21use crate::phase::Phase;
22use hashbrown::HashMap;
23use num_complex::Complex64;
24use rustc_hash::FxBuildHasher;
25
26/// Incremental builder for a `PauliSum`.
27///
28/// Uses `FxBuildHasher` rather than the default `SipHash` since Pauli
29/// bitstrings are already high-entropy.
30pub struct BuildAccumulator<const W: usize> {
31    map: HashMap<PauliString<W>, Complex64, FxBuildHasher>,
32    num_qubits: usize,
33}
34
35impl<const W: usize> BuildAccumulator<W> {
36    /// New empty accumulator targeting `num_qubits` qubits.
37    pub fn new(num_qubits: usize) -> Self {
38        Self {
39            map: HashMap::with_hasher(FxBuildHasher),
40            num_qubits,
41        }
42    }
43
44    /// Allocate up-front for at least `cap` distinct Pauli keys.
45    pub fn with_capacity(num_qubits: usize, cap: usize) -> Self {
46        Self {
47            map: HashMap::with_capacity_and_hasher(cap, FxBuildHasher),
48            num_qubits,
49        }
50    }
51
52    /// Add `phase · c · p` to the accumulator. The phase factor is folded
53    /// into `c` before the upsert. `p` is taken as-is and used as the map
54    /// key.
55    ///
56    /// # Examples
57    ///
58    /// ```
59    /// use paulistrings::{BuildAccumulator, PauliString, Phase};
60    /// use num_complex::Complex64;
61    ///
62    /// let mut acc = BuildAccumulator::<1>::new(2);
63    /// // Add a Y-like term written as i · (X · Z) on qubit 0.
64    /// acc.add_term(
65    ///     PauliString::<1> { x: [1], z: [1] },
66    ///     Phase::I,
67    ///     Complex64::new(1.0, 0.0),
68    /// );
69    /// let sum = acc.finalize();
70    /// assert_eq!(sum.coeff()[0], Complex64::new(0.0, 1.0));
71    /// ```
72    pub fn add_term(&mut self, p: PauliString<W>, phase: Phase, c: Complex64) {
73        let contribution = phase.apply(c);
74        self.map
75            .entry(p)
76            .and_modify(|e| *e += contribution)
77            .or_insert(contribution);
78    }
79
80    /// Sort, deduplicate, and emit a `PauliSum`. Entries whose accumulated
81    /// coefficient is exactly `0+0i` are dropped.
82    pub fn finalize(self) -> PauliSum<W> {
83        let zero = Complex64::new(0.0, 0.0);
84        let mut entries: Vec<(PauliString<W>, Complex64)> =
85            self.map.into_iter().filter(|(_, c)| *c != zero).collect();
86        entries.sort_by(|a, b| (&a.0.x, &a.0.z).cmp(&(&b.0.x, &b.0.z)));
87        let n = entries.len();
88        let mut x = Vec::with_capacity(n);
89        let mut z = Vec::with_capacity(n);
90        let mut coeff = Vec::with_capacity(n);
91        for (p, c) in entries {
92            x.push(p.x);
93            z.push(p.z);
94            coeff.push(c);
95        }
96        PauliSum {
97            x,
98            z,
99            coeff,
100            num_qubits: self.num_qubits,
101        }
102    }
103}
104
105#[cfg(all(test, debug_assertions))]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn finalize_empty_accumulator_is_empty() {
111        let acc = BuildAccumulator::<1>::new(4);
112        let s = acc.finalize();
113        assert!(s.is_empty());
114        assert_eq!(s.num_qubits(), 4);
115        s.assert_invariants();
116    }
117
118    #[test]
119    fn add_term_dup_sums_coeffs() {
120        let mut acc = BuildAccumulator::<1>::new(4);
121        let p = PauliString::<1>::x(2);
122        acc.add_term(p, Phase::ONE, Complex64::new(1.0, 0.0));
123        acc.add_term(p, Phase::ONE, Complex64::new(2.5, -1.0));
124        let s = acc.finalize();
125        assert_eq!(s.len(), 1);
126        assert_eq!(s.coeff()[0], Complex64::new(3.5, -1.0));
127        s.assert_invariants();
128    }
129
130    #[test]
131    fn add_term_phase_one_is_identity_factor() {
132        let mut acc = BuildAccumulator::<1>::new(4);
133        let p = PauliString::<1>::x(0);
134        acc.add_term(p, Phase::ONE, Complex64::new(2.0, 3.0));
135        let s = acc.finalize();
136        assert_eq!(s.coeff()[0], Complex64::new(2.0, 3.0));
137    }
138
139    #[test]
140    fn add_term_phase_i_multiplies_by_i() {
141        let mut acc = BuildAccumulator::<1>::new(4);
142        let p = PauliString::<1>::x(0);
143        // (2 + 3i) * i = -3 + 2i
144        acc.add_term(p, Phase::I, Complex64::new(2.0, 3.0));
145        let s = acc.finalize();
146        assert_eq!(s.coeff()[0], Complex64::new(-3.0, 2.0));
147    }
148
149    #[test]
150    fn add_term_phase_minus_one_negates() {
151        let mut acc = BuildAccumulator::<1>::new(4);
152        let p = PauliString::<1>::x(0);
153        acc.add_term(p, Phase::MINUS_ONE, Complex64::new(2.0, 3.0));
154        let s = acc.finalize();
155        assert_eq!(s.coeff()[0], Complex64::new(-2.0, -3.0));
156    }
157
158    #[test]
159    fn add_term_phase_minus_i_multiplies_by_minus_i() {
160        let mut acc = BuildAccumulator::<1>::new(4);
161        let p = PauliString::<1>::x(0);
162        // (2 + 3i) * -i = 3 - 2i
163        acc.add_term(p, Phase::MINUS_I, Complex64::new(2.0, 3.0));
164        let s = acc.finalize();
165        assert_eq!(s.coeff()[0], Complex64::new(3.0, -2.0));
166    }
167
168    #[test]
169    fn add_term_cancellation_drops() {
170        let mut acc = BuildAccumulator::<1>::new(4);
171        let p = PauliString::<1>::x(0);
172        acc.add_term(p, Phase::ONE, Complex64::new(1.0, 0.0));
173        acc.add_term(p, Phase::MINUS_ONE, Complex64::new(1.0, 0.0));
174        let s = acc.finalize();
175        assert!(s.is_empty());
176        s.assert_invariants();
177    }
178
179    #[test]
180    fn finalize_sorts_by_lex_key() {
181        // Insert keys out of order and confirm finalize emits them sorted by
182        // (x, z) lex. Use Z(0), X(0), X(1) — sorted: Z(0)=(0,1), X(0)=(1,0),
183        // X(1)=(2,0) (lex on x first, then z).
184        let mut acc = BuildAccumulator::<1>::new(4);
185        acc.add_term(PauliString::<1>::x(1), Phase::ONE, Complex64::new(3.0, 0.0));
186        acc.add_term(PauliString::<1>::z(0), Phase::ONE, Complex64::new(1.0, 0.0));
187        acc.add_term(PauliString::<1>::x(0), Phase::ONE, Complex64::new(2.0, 0.0));
188        let s = acc.finalize();
189        assert_eq!(s.len(), 3);
190        assert_eq!(s.x()[0], [0u64]);
191        assert_eq!(s.z()[0], [1u64]);
192        assert_eq!(s.coeff()[0], Complex64::new(1.0, 0.0));
193        assert_eq!(s.x()[1], [1u64]);
194        assert_eq!(s.coeff()[1], Complex64::new(2.0, 0.0));
195        assert_eq!(s.x()[2], [2u64]);
196        assert_eq!(s.coeff()[2], Complex64::new(3.0, 0.0));
197        s.assert_invariants();
198    }
199
200    #[test]
201    fn finalize_w2_across_word_boundary() {
202        let mut acc = BuildAccumulator::<2>::new(128);
203        acc.add_term(
204            PauliString::<2>::x(64),
205            Phase::ONE,
206            Complex64::new(1.0, 0.0),
207        );
208        acc.add_term(PauliString::<2>::x(0), Phase::ONE, Complex64::new(2.0, 0.0));
209        acc.add_term(
210            PauliString::<2>::z(127),
211            Phase::ONE,
212            Complex64::new(3.0, 0.0),
213        );
214        let s = acc.finalize();
215        assert_eq!(s.len(), 3);
216        s.assert_invariants();
217    }
218
219    #[test]
220    fn add_term_phase_new_wraps_mod_4() {
221        // Phase::new(5) reduces to Phase::I — multiplies by i.
222        let mut acc = BuildAccumulator::<1>::new(4);
223        let p = PauliString::<1>::x(0);
224        acc.add_term(p, Phase::new(5), Complex64::new(2.0, 0.0));
225        let s = acc.finalize();
226        assert_eq!(s.coeff()[0], Complex64::new(0.0, 2.0));
227    }
228
229    #[test]
230    fn finalize_with_capacity_preallocated() {
231        // with_capacity should behave identically to new() for correctness.
232        let mut acc = BuildAccumulator::<1>::with_capacity(4, 16);
233        acc.add_term(PauliString::<1>::x(0), Phase::ONE, Complex64::new(1.0, 0.0));
234        let s = acc.finalize();
235        assert_eq!(s.len(), 1);
236        s.assert_invariants();
237    }
238}