ppvm_pauli_sum/strategy.rs
1// SPDX-FileCopyrightText: 2026 The PPVM Authors
2// SPDX-License-Identifier: Apache-2.0
3
4use ppvm_traits::traits::*;
5
6/// Two strategies run in sequence: `S1` then `S2`.
7///
8/// The capacity hint is the smaller of the two; truncation applies both
9/// policies in order, so use this when you want, e.g.,
10/// "drop by coefficient threshold *and* cap maximum Pauli weight".
11#[derive(Debug, Clone, Copy, PartialEq, Default)]
12pub struct CombinedStrategy<S1: Strategy, S2: Strategy>(pub S1, pub S2);
13
14impl<S1: Strategy, S2: Strategy> Strategy for CombinedStrategy<S1, S2> {
15 fn capacity(&self, n_qubits: usize) -> usize {
16 self.0.capacity(n_qubits).min(self.1.capacity(n_qubits))
17 }
18
19 fn truncate<S, V, H, M, W>(&self, map: &mut M)
20 where
21 S: ppvm_traits::traits::PauliStorage,
22 V: Coefficient,
23 H: std::hash::BuildHasher + Clone + Default,
24 W: PauliWordTrait,
25 M: ppvm_traits::traits::ACMap<S, V, H, W>,
26 {
27 self.0.truncate(map);
28 self.1.truncate(map);
29 }
30}
31
32/// Drop terms whose Pauli weight (number of non-identity slots) exceeds
33/// the given bound.
34///
35/// # Examples
36///
37/// ```
38/// use ppvm_pauli_sum::prelude::*;
39/// use ppvm_pauli_sum::strategy::MaxPauliWeight;
40///
41/// // Keep only weight-1 terms.
42/// let strat = MaxPauliWeight(1);
43/// let mut state: PauliSum<config::indexmap::ByteFxHashF64<1, MaxPauliWeight>> =
44/// PauliSum::builder().n_qubits(3).strategy(strat).build();
45/// state += ("XII", 1.0); // weight 1, kept
46/// state += ("XYI", 1.0); // weight 2, dropped
47/// state.truncate();
48/// assert_eq!(state.len(), 1);
49/// ```
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51pub struct MaxPauliWeight(pub usize);
52
53impl MaxPauliWeight {
54 /// Maximum Pauli weight retained.
55 pub fn max_weight(&self) -> usize {
56 self.0
57 }
58}
59
60impl Default for MaxPauliWeight {
61 fn default() -> Self {
62 Self(usize::MAX)
63 }
64}
65
66impl Strategy for MaxPauliWeight {
67 fn capacity(&self, n_qubits: usize) -> usize {
68 // the number here should scale binomially, but that can get large
69 // since the capacity has a direct impact on performance, let's be conservative
70 n_qubits * 10
71 }
72
73 fn truncate<S, V, H, M, W>(&self, map: &mut M)
74 where
75 S: PauliStorage,
76 V: Coefficient,
77 H: std::hash::BuildHasher + Clone + Default,
78 W: PauliWordTrait,
79 M: ACMap<S, V, H, W>,
80 {
81 // `usize::MAX` is the conventional "disabled" sentinel (matches
82 // `Default::default()`). Skip the retain pass entirely — saves a
83 // full bucket walk per `truncate()` call when callers (e.g. the
84 // Python binding) pass `MaxPauliWeight(usize::MAX)` to opt out
85 // of weight truncation without changing the strategy type.
86 if self.0 == usize::MAX {
87 return;
88 }
89 map.retain(|k, _| k.weight() <= self.max_weight());
90 }
91}
92
93/// Drop terms whose coefficient magnitude falls below the given threshold.
94/// Defaults to `1e-12`.
95///
96/// # Examples
97///
98/// ```
99/// use ppvm_pauli_sum::prelude::*;
100/// use ppvm_pauli_sum::strategy::CoefficientThreshold;
101///
102/// let strict = CoefficientThreshold(1e-6);
103/// let mut state: PauliSum<config::indexmap::ByteFxHashF64<1, CoefficientThreshold>> =
104/// PauliSum::builder().n_qubits(2).strategy(strict).build();
105/// state += ("ZZ", 1.0);
106/// state += ("XX", 1e-9); // below threshold
107/// state.truncate();
108/// assert_eq!(state.len(), 1); // the XX term was dropped
109/// ```
110#[derive(Debug, Clone, Copy, PartialEq)]
111pub struct CoefficientThreshold(pub f64);
112
113impl Default for CoefficientThreshold {
114 fn default() -> Self {
115 Self(1e-12)
116 }
117}
118
119impl Strategy for CoefficientThreshold {
120 fn capacity(&self, n_qubits: usize) -> usize {
121 // clearing maps scales as O(capacity), so let's be conservative here
122 n_qubits * 10
123 }
124
125 fn truncate<S, V, H, M, W>(&self, map: &mut M)
126 where
127 S: PauliStorage,
128 V: Coefficient,
129 H: std::hash::BuildHasher + Clone + Default,
130 W: PauliWordTrait,
131 M: ACMap<S, V, H, W>,
132 {
133 map.retain(|_, v| !v.cutoff(self.0));
134 }
135}
136
137/// Drop terms whose loss weight (number of lost qubits) exceeds the
138/// given bound. Only meaningful for [`LossyPauliWord`](ppvm_pauli_word::loss::LossyPauliWord).
139#[derive(Debug, Clone, Copy, PartialEq, Eq)]
140pub struct MaxLossWeight(pub usize);
141
142impl Default for MaxLossWeight {
143 fn default() -> Self {
144 Self(10)
145 }
146}
147
148impl Strategy for MaxLossWeight {
149 fn capacity(&self, n_qubits: usize) -> usize {
150 // the number here should scale binomially, but that can get large
151 // since the capacity has a direct impact on performance, let's be conservative
152 n_qubits * 10
153 }
154
155 fn truncate<S, V, H, M, W>(&self, map: &mut M)
156 where
157 S: PauliStorage,
158 V: Coefficient,
159 H: std::hash::BuildHasher + Clone + Default,
160 M: ACMap<S, V, H, W>,
161 W: PauliWordTrait,
162 {
163 // Skip the retain pass when callers pass `usize::MAX` to opt out
164 // of loss-weight truncation without changing the strategy type.
165 if self.0 == usize::MAX {
166 return;
167 }
168 map.retain(|k, _| k.loss_weight() <= self.0);
169 }
170}