1use super::TruncationPolicy;
4use crate::collectives::Collectives;
5use crate::engine::LOG_TARGET;
6use crate::pauli_sum::PauliSum;
7use crate::rng::Rng;
8use num_complex::Complex64;
9use rayon::prelude::*;
10use std::cell::RefCell;
11use std::sync::atomic::{AtomicU64, Ordering};
12
13thread_local! {
14 static MAGNITUDES: RefCell<Vec<f64>> = const { RefCell::new(Vec::new()) };
17}
18
19#[derive(Clone, Copy, Debug, PartialEq)]
23pub struct CoefficientThreshold(
24 pub f64,
26);
27
28impl<const W: usize> TruncationPolicy<W> for CoefficientThreshold {
29 fn keep_term(&self, _x: &[u64; W], _z: &[u64; W], c: Complex64) -> bool {
30 let eps = self.0;
31 eps < 0.0 || c.norm_sqr() > eps * eps
32 }
33
34 fn finalizes_layer(&self) -> bool {
35 false
36 }
37}
38
39#[derive(Clone, Copy, Debug, PartialEq)]
41pub struct WeightCutoff(
42 pub u32,
44);
45
46impl<const W: usize> TruncationPolicy<W> for WeightCutoff {
47 fn keep_term(&self, x: &[u64; W], z: &[u64; W], _c: Complex64) -> bool {
48 let weight: u32 = (0..W).map(|i| (x[i] | z[i]).count_ones()).sum();
49 weight <= self.0
50 }
51
52 fn finalizes_layer(&self) -> bool {
53 false
54 }
55}
56
57#[derive(Clone, Copy, Debug, PartialEq)]
63pub struct TopN(
64 pub usize,
66);
67
68impl<const W: usize> TruncationPolicy<W> for TopN {
69 fn finalize_layer(&self, sum: &mut PauliSum<W>) {
70 let n = self.0;
71 if sum.len() <= n {
72 return;
73 }
74 if n == 0 {
75 sum.clear();
76 return;
77 }
78
79 let total = sum.len();
80 let mut buffer = MAGNITUDES.take();
81 if buffer.len() < total {
82 buffer.resize(total, 0.0);
83 }
84 let magnitudes = &mut buffer[..total];
85 {
86 let view = &*sum;
87 let num_buckets = view.num_buckets();
88 let mut handles: Vec<&mut [f64]> = Vec::with_capacity(num_buckets);
89 let mut rest: &mut [f64] = magnitudes;
90 for b in 0..num_buckets {
91 let (head, tail) = rest.split_at_mut(view.bucket_len(b));
92 handles.push(head);
93 rest = tail;
94 }
95 debug_assert!(rest.is_empty(), "bucket lengths must sum to len()");
96 handles.into_par_iter().enumerate().for_each(|(b, dst)| {
97 for (d, c) in dst.iter_mut().zip(view.bucket(b).2.iter()) {
98 *d = c.norm_sqr();
99 }
100 });
101 }
102
103 magnitudes.select_nth_unstable_by(n - 1, |a, b| {
105 b.partial_cmp(a).unwrap_or_else(|| a.is_nan().cmp(&b.is_nan()))
106 });
107 let boundary = magnitudes[n - 1];
108 let above = magnitudes[..n - 1].par_iter().filter(|&&m| m > boundary).count();
110
111 MAGNITUDES.set(buffer);
112
113 retain_with_boundary_quota(
114 sum,
115 |m| {
116 if m.is_nan() {
117 core::cmp::Ordering::Less
118 } else {
119 m.partial_cmp(&boundary).unwrap_or(core::cmp::Ordering::Greater)
121 }
122 },
123 n - above,
124 );
125 }
126
127 fn finalize_layer_partitioned(&self, local: &mut PauliSum<W>, collectives: &dyn Collectives) {
129 assert!(
130 collectives.size() == 1,
131 "TopN({}) on {} partitions: partitioned exact TopN is not yet supported; ApproxTopN \
132 is the partitioned alternative.",
133 self.0,
134 collectives.size(),
135 );
136 self.finalize_layer(local);
137 }
138}
139
140fn bucket_quotas(boundary_counts: &[usize], quota: usize) -> Vec<usize> {
142 let mut remaining = quota;
143 boundary_counts
144 .iter()
145 .map(|&count| {
146 let taken = count.min(remaining);
147 remaining -= taken;
148 taken
149 })
150 .collect()
151}
152
153pub(crate) fn retain_with_boundary_quota<const W: usize>(
155 sum: &mut PauliSum<W>,
156 classify: impl Fn(f64) -> core::cmp::Ordering + Sync,
157 quota: usize,
158) {
159 use core::cmp::Ordering;
160 let boundary_counts: Vec<usize> = (0..sum.num_buckets())
161 .into_par_iter()
162 .map(|b| {
163 sum.bucket(b).2.iter().filter(|c| classify(c.norm_sqr()) == Ordering::Equal).count()
164 })
165 .collect();
166 let quotas = bucket_quotas(&boundary_counts, quota);
167 sum.retain_per_bucket(|b| {
168 let mut remaining = quotas[b];
169 let classify = &classify;
170 move |_: &[u64; W], _: &[u64; W], c: Complex64| match classify(c.norm_sqr()) {
171 Ordering::Greater => true,
172 Ordering::Equal if remaining > 0 => {
173 remaining -= 1;
174 true
175 }
176 Ordering::Equal | Ordering::Less => false,
177 }
178 });
179}
180
181pub(crate) const APPROX_BINS: usize = 2048;
183
184#[derive(Clone, Copy, Debug, PartialEq)]
191pub struct ApproxTopN(
192 pub usize,
194);
195
196#[derive(Clone, Copy, Debug, PartialEq)]
198pub(crate) enum EdgeDecision {
199 KeepAll,
201 Clear,
203 Boundary {
205 bin: usize,
207 kept_above: usize,
209 quota: usize,
211 },
212}
213
214fn octave(norm_sqr: f64) -> usize {
216 (norm_sqr.to_bits() >> 52) as usize
217}
218
219pub(crate) fn classify_octave(norm_sqr: f64, bin: usize) -> core::cmp::Ordering {
221 if norm_sqr.is_nan() {
222 core::cmp::Ordering::Less
223 } else {
224 octave(norm_sqr).cmp(&bin)
225 }
226}
227
228pub(crate) fn octave_histogram<const W: usize>(sum: &PauliSum<W>) -> [u32; APPROX_BINS] {
230 debug_assert!(sum.len() <= u32::MAX as usize, "len exceeds bin counters");
231 let num_buckets = sum.num_buckets();
232 let tasks = (rayon::current_num_threads() * 4).clamp(1, num_buckets);
233 (0..tasks)
234 .into_par_iter()
235 .map(|t| {
236 let mut h = [0u32; APPROX_BINS];
237 for b in (num_buckets * t / tasks)..(num_buckets * (t + 1) / tasks) {
238 for c in sum.bucket(b).2 {
239 h[octave(c.norm_sqr())] += 1;
240 }
241 }
242 h
243 })
244 .reduce(
245 || [0u32; APPROX_BINS],
246 |mut a, b| {
247 for (x, y) in a.iter_mut().zip(b.iter()) {
248 *x += y;
249 }
250 a
251 },
252 )
253}
254
255pub(crate) fn octave_edge<C>(hist: &[C], total_len: usize, n: usize) -> EdgeDecision
257where
258 C: Copy + Into<u64>,
259{
260 debug_assert_eq!(hist.len(), APPROX_BINS, "octave_edge: wrong histogram size");
261 if total_len <= n {
262 return EdgeDecision::KeepAll;
263 }
264 if n == 0 {
265 return EdgeDecision::Clear;
266 }
267
268 let mut kept_above = 0usize;
269 for bin in (0..APPROX_BINS).rev() {
270 let population = hist[bin].into() as usize;
271 if kept_above + population > n {
272 return EdgeDecision::Boundary { bin, kept_above, quota: n - kept_above };
273 }
274 kept_above += population;
275 }
276 unreachable!("a histogram of more than n terms overflows n at some octave")
277}
278
279pub(crate) fn retain_at_edge<const W: usize>(
281 sum: &mut PauliSum<W>,
282 edge: EdgeDecision,
283 local_quota: usize,
284) {
285 match edge {
286 EdgeDecision::KeepAll => {}
287 EdgeDecision::Clear => sum.clear(),
288 EdgeDecision::Boundary { bin, .. } => {
289 retain_with_boundary_quota(sum, |m| classify_octave(m, bin), local_quota);
290 }
291 }
292}
293
294pub(crate) fn local_boundary_quota(
298 collectives: &dyn Collectives,
299 local_count: usize,
300 quota: usize,
301) -> usize {
302 let rank = collectives.rank() as usize;
303 let mut counts = vec![0u64; collectives.size() as usize];
304 counts[rank] = local_count as u64;
305 collectives.allreduce_sum_u64(&mut counts);
306 let below: u64 = counts[..rank].iter().sum();
307 (quota as u64).saturating_sub(below).min(local_count as u64) as usize
308}
309
310impl<const W: usize> TruncationPolicy<W> for ApproxTopN {
311 fn finalize_layer(&self, sum: &mut PauliSum<W>) {
312 let n = self.0;
313 let total = sum.len();
314 if total <= n {
315 return;
316 }
317 if n == 0 {
318 sum.clear();
319 return;
320 }
321
322 let edge = octave_edge(&octave_histogram(sum), total, n);
323 let quota = match edge {
324 EdgeDecision::Boundary { quota, .. } => quota,
325 EdgeDecision::KeepAll | EdgeDecision::Clear => 0,
326 };
327 retain_at_edge(sum, edge, quota);
328 }
329
330 fn finalize_layer_partitioned(&self, local: &mut PauliSum<W>, collectives: &dyn Collectives) {
332 if collectives.size() == 1 {
333 self.finalize_layer(local);
334 return;
335 }
336 let histogram = octave_histogram(local);
337
338 let mut packed = [0u64; 1 + APPROX_BINS];
340 packed[0] = local.len() as u64;
341 for (slot, &count) in packed[1..].iter_mut().zip(histogram.iter()) {
342 *slot = u64::from(count);
343 }
344 collectives.allreduce_sum_u64(&mut packed);
345
346 let total = packed[0] as usize;
347 let edge = octave_edge(&packed[1..], total, self.0);
348 let local_quota = match edge {
349 EdgeDecision::Boundary { bin, quota, .. } => {
350 local_boundary_quota(collectives, histogram[bin] as usize, quota)
351 }
352 EdgeDecision::KeepAll | EdgeDecision::Clear => 0,
353 };
354 retain_at_edge(local, edge, local_quota);
355 }
356
357 fn supports_partitioned(&self) -> bool {
358 true
359 }
360}
361
362#[derive(Debug)]
368pub struct CollapseSample {
369 pub cache: usize,
371 pub seed: u64,
373 calls: AtomicU64,
375 collapses: AtomicU64,
377}
378
379impl CollapseSample {
380 pub fn new(cache: usize, seed: u64) -> Self {
382 Self { cache, seed, calls: AtomicU64::new(0), collapses: AtomicU64::new(0) }
383 }
384
385 pub fn collapses(&self) -> u64 {
387 self.collapses.load(Ordering::Relaxed)
388 }
389
390 pub(crate) fn next_call(&self) -> u64 {
392 self.calls.fetch_add(1, Ordering::Relaxed)
393 }
394
395 pub(crate) fn count_collapse(&self) -> u64 {
396 self.collapses.fetch_add(1, Ordering::Relaxed) + 1
397 }
398
399 pub(crate) fn target(&self, call: u64, total: f64, len: usize) -> f64 {
401 assert!(
402 total.is_finite() && total > 0.0,
403 "CollapseSample: cannot draw by weight from {len} terms with Σ|c|² = {total}",
404 );
405 Rng::from_key(&[self.seed, call]).uniform() * total
406 }
407}
408
409pub(crate) fn bucket_norms<const W: usize>(sum: &PauliSum<W>) -> Vec<f64> {
411 (0..sum.num_buckets())
412 .into_par_iter()
413 .map(|b| sum.bucket(b).2.iter().map(|c| c.norm_sqr()).sum())
414 .collect()
415}
416
417pub(crate) fn pick_slot(
419 weights: impl IntoIterator<Item = f64>,
420 target: f64,
421) -> Option<(usize, f64)> {
422 let mut cumulative = 0.0f64;
423 let mut last = None;
424 for (i, w) in weights.into_iter().enumerate() {
425 if w > 0.0 {
426 if target < cumulative + w {
427 return Some((i, target - cumulative));
428 }
429 last = Some((i, target - cumulative));
430 }
431 cumulative += w;
432 }
433 last
434}
435
436pub(crate) fn collapse_to_target<const W: usize>(
438 sum: &mut PauliSum<W>,
439 norms: &[f64],
440 target: f64,
441) {
442 let (b, rest) = pick_slot(norms.iter().copied(), target)
443 .expect("collapse_to_target: no bucket has positive weight");
444 let (pos, _) = pick_slot(sum.bucket(b).2.iter().map(|c| c.norm_sqr()), rest)
445 .expect("collapse_to_target: a positive bucket has a positive term");
446 let (x, z) = (sum.bucket(b).0[pos], sum.bucket(b).1[pos]);
447 sum.clear();
448 sum.buckets_mut()[b].push(x, z, Complex64::new(1.0, 0.0));
449 sum.recount();
450}
451
452impl PartialEq for CollapseSample {
454 fn eq(&self, other: &Self) -> bool {
455 self.cache == other.cache
456 && self.seed == other.seed
457 && self.calls.load(Ordering::Relaxed) == other.calls.load(Ordering::Relaxed)
458 && self.collapses() == other.collapses()
459 }
460}
461
462impl<const W: usize> TruncationPolicy<W> for CollapseSample {
463 fn finalize_layer(&self, sum: &mut PauliSum<W>) {
464 let call = self.next_call();
465 let len = sum.len();
466 if len <= self.cache {
467 return;
468 }
469 let norms = bucket_norms(sum);
470 let total: f64 = norms.iter().sum();
471 collapse_to_target(sum, &norms, self.target(call, total, len));
472 let n = self.count_collapse();
473 log::debug!(
474 target: LOG_TARGET,
475 "collapse_sample: {len} terms, sum |c|^2 = {total:.6e}, collapsed to one (collapse {n})",
476 );
477 }
478
479 fn finalize_layer_partitioned(&self, local: &mut PauliSum<W>, collectives: &dyn Collectives) {
481 if collectives.size() == 1 {
482 self.finalize_layer(local);
483 return;
484 }
485 let rank = collectives.rank() as usize;
486 let call = if rank == 0 { self.next_call() } else { 0 };
487 let mut head = [local.len() as u64, call];
488 collectives.allreduce_sum_u64(&mut head);
489 let [len, call] = head;
490 if len as usize <= self.cache {
491 return;
492 }
493
494 let norms = bucket_norms(local);
495 let mut weights = vec![0.0f64; collectives.size() as usize];
496 weights[rank] = norms.iter().sum();
497 collectives.allreduce_sum_f64(&mut weights);
498 let total: f64 = weights.iter().sum();
499 let target = self.target(call, total, len as usize);
500 let (chosen, rest) = pick_slot(weights.iter().copied(), target)
501 .expect("CollapseSample: a positive total has a positive partition");
502 if rank == chosen {
503 collapse_to_target(local, &norms, rest);
504 } else {
505 local.clear();
506 }
507 if rank == 0 {
508 let collapses = self.count_collapse();
509 log::debug!(
510 target: LOG_TARGET,
511 "collapse_sample: {len} terms over {} partitions, sum |c|^2 = {total:.6e}, \
512 collapsed to one on partition {chosen} (collapse {collapses})",
513 collectives.size(),
514 );
515 }
516 }
517
518 fn supports_partitioned(&self) -> bool {
519 true
520 }
521}
522
523#[derive(Clone, Copy, Debug, PartialEq)]
525pub struct And<A, B>(
526 pub A,
528 pub B,
530);
531
532impl<const W: usize, A, B> TruncationPolicy<W> for And<A, B>
533where
534 A: TruncationPolicy<W>,
535 B: TruncationPolicy<W>,
536{
537 fn keep_term(&self, x: &[u64; W], z: &[u64; W], c: Complex64) -> bool {
538 self.0.keep_term(x, z, c) && self.1.keep_term(x, z, c)
539 }
540
541 fn finalize_layer(&self, sum: &mut PauliSum<W>) {
542 self.0.finalize_layer(sum);
543 self.1.finalize_layer(sum);
544 }
545
546 fn finalizes_layer(&self) -> bool {
547 self.0.finalizes_layer() || self.1.finalizes_layer()
548 }
549
550 fn finalize_layer_partitioned(&self, local: &mut PauliSum<W>, collectives: &dyn Collectives) {
551 self.0.finalize_layer_partitioned(local, collectives);
552 self.1.finalize_layer_partitioned(local, collectives);
553 }
554
555 fn supports_partitioned(&self) -> bool {
556 self.0.supports_partitioned() && self.1.supports_partitioned()
557 }
558}
559
560#[derive(Clone, Copy, Debug, PartialEq)]
565pub struct Or<A, B>(pub(crate) A, pub(crate) B);
566
567impl<A, B> Or<A, B> {
568 pub fn new(first: A, second: B) -> Self {
572 Self(first, second)
573 }
574}
575
576pub(crate) fn or_operand_error(first_has_pass: bool, second_has_pass: bool) -> Option<String> {
578 let which = match (first_has_pass, second_has_pass) {
579 (false, false) => return None,
580 (true, false) => "its first operand has",
581 (false, true) => "its second operand has",
582 (true, true) => "both operands have",
583 };
584 Some(format!(
585 "Or combines per-term filters only, but {which} a layer pass (TopN, ApproxTopN or \
586 CollapseSample, alone or inside an And); use And to add a filter to a layer pass"
587 ))
588}
589
590impl<const W: usize, A, B> TruncationPolicy<W> for Or<A, B>
591where
592 A: TruncationPolicy<W>,
593 B: TruncationPolicy<W>,
594{
595 fn keep_term(&self, x: &[u64; W], z: &[u64; W], c: Complex64) -> bool {
596 self.0.keep_term(x, z, c) || self.1.keep_term(x, z, c)
597 }
598
599 fn finalizes_layer(&self) -> bool {
601 if let Some(message) = or_operand_error(self.0.finalizes_layer(), self.1.finalizes_layer())
602 {
603 panic!("{message}");
604 }
605 false
606 }
607}
608
609#[cfg(test)]
610mod tests;