Skip to main content

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}