use num_complex::Complex64;
use super::*;
use crate::channel::PtmChannel;
use crate::pauli_string::PauliString;
use crate::pauli_sum::accumulator::BuildAccumulator;
use crate::pauli_sum::PauliSum;
use crate::truncation::TruncationPolicy;
pub fn approx_eq(a: Complex64, b: Complex64, tol: f64) -> bool {
(a - b).norm() <= tol
}
pub fn canonical_triples<const W: usize>(s: &PauliSum<W>) -> Vec<([u64; W], [u64; W], Complex64)> {
let mut v: Vec<([u64; W], [u64; W], Complex64)> =
s.iter().map(|(x, z, c)| (*x, *z, c)).collect();
v.sort_unstable_by_key(|&(x, z, _)| (x, z));
v
}
pub fn storage_triples<const W: usize>(s: &PauliSum<W>) -> Vec<([u64; W], [u64; W], Complex64)> {
s.iter().map(|(x, z, c)| (*x, *z, c)).collect()
}
pub fn first_n_by_rank<const W: usize>(
terms: &[([u64; W], [u64; W], Complex64)],
n: usize,
rank: impl Fn(Complex64) -> u64,
) -> Vec<([u64; W], [u64; W], Complex64)> {
let mut order: Vec<usize> = (0..terms.len()).collect();
order.sort_by_key(|&i| std::cmp::Reverse(rank(terms[i].2)));
let mut kept: Vec<usize> = order.into_iter().take(n).collect();
kept.sort_unstable();
kept.into_iter().map(|i| terms[i]).collect()
}
pub fn magnitude_rank(c: Complex64) -> u64 {
c.norm_sqr().to_bits()
}
pub fn octave_rank(c: Complex64) -> u64 {
c.norm_sqr().to_bits() >> 52
}
pub fn assert_same_terms<const W: usize>(got: &PauliSum<W>, want: &PauliSum<W>, what: &str) {
assert_eq!(got.len(), want.len(), "{what}: term count");
let got = canonical_triples(got);
let want = canonical_triples(want);
for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
assert_eq!((g.0, g.1), (w.0, w.1), "{what}: term {i} key mismatch");
assert_eq!(
g.2, w.2,
"{what}: term {i} key {:?}/{:?} coeff {} vs {} (not bitwise equal)",
g.0, g.1, g.2, w.2,
);
}
}
pub fn with_hash<const W: usize>(sum: PauliSum<W>, hash: crate::Gf2Hash<W>) -> PauliSum<W> {
sum.with_hash(hash)
}
pub fn bucket<const W: usize>(
sum: &PauliSum<W>,
b: usize,
) -> (&[[u64; W]], &[[u64; W]], &[Complex64]) {
sum.bucket(b)
}
pub fn bucket_len<const W: usize>(sum: &PauliSum<W>, b: usize) -> usize {
sum.bucket_len(b)
}
pub fn refine<const W: usize>(sum: &mut PauliSum<W>) {
sum.refine();
}
pub fn coarsen<const W: usize>(sum: &mut PauliSum<W>) {
sum.coarsen();
}
pub fn assert_invariants<const W: usize>(sum: &PauliSum<W>) {
sum.assert_invariants();
}
pub fn assert_terms_close<const W: usize>(
got: &PauliSum<W>,
want: &PauliSum<W>,
tol: f64,
what: &str,
) {
assert_eq!(got.len(), want.len(), "{what}: term count");
let got = canonical_triples(got);
let want = canonical_triples(want);
for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
assert_eq!((g.0, g.1), (w.0, w.1), "{what}: term {i} key mismatch");
let d = (g.2 - w.2).norm();
assert!(
d < tol,
"{what}: term {i} key {:?}/{:?} coeff {} vs {} (delta {d:e})",
g.0,
g.1,
g.2,
w.2,
);
}
}
pub struct DiscardMeter<'p, P: ?Sized> {
policy: &'p P,
pending: std::sync::atomic::AtomicU64,
total: std::sync::atomic::AtomicU64,
}
fn add_f64(cell: &std::sync::atomic::AtomicU64, v: f64) {
use std::sync::atomic::Ordering::Relaxed;
let _ = cell.fetch_update(Relaxed, Relaxed, |b| Some((f64::from_bits(b) + v).to_bits()));
}
impl<'p, P: ?Sized> DiscardMeter<'p, P> {
pub fn new(policy: &'p P) -> Self {
Self { policy, pending: 0f64.to_bits().into(), total: 0f64.to_bits().into() }
}
pub fn discarded(&self) -> f64 {
f64::from_bits(self.total.load(std::sync::atomic::Ordering::Relaxed))
}
}
impl<const W: usize, P: TruncationPolicy<W> + ?Sized> TruncationPolicy<W> for DiscardMeter<'_, P> {
fn keep_term(&self, x: &[u64; W], z: &[u64; W], c: Complex64) -> bool {
let keep = self.policy.keep_term(x, z, c);
if !keep && self.policy.keep_term(x, z, Complex64::new(1e300, 0.0)) {
add_f64(&self.pending, c.norm_sqr());
}
keep
}
fn finalize_layer(&self, sum: &mut PauliSum<W>) {
let mut dropped = 0.0;
if self.policy.finalizes_layer() {
let before = sum.clone();
self.policy.finalize_layer(sum);
if sum.len() != before.len() {
dropped = norm_sqr(&before.add(&scaled(sum, -1.0)));
}
}
let pending = f64::from_bits(self.pending.swap(0, std::sync::atomic::Ordering::Relaxed));
add_f64(&self.total, (pending + dropped).sqrt());
}
}
fn norm_sqr<const W: usize>(sum: &PauliSum<W>) -> f64 {
sum.overlap(sum).re
}
fn scaled<const W: usize>(sum: &PauliSum<W>, factor: f64) -> PauliSum<W> {
let mut out = sum.clone();
out.scale(Complex64::new(factor, 0.0));
out
}
pub fn propagate_discarding<const W: usize, P: TruncationPolicy<W> + ?Sized>(
circuit: &crate::Circuit<W>,
sum: PauliSum<W>,
policy: &P,
direction: crate::Direction,
) -> (PauliSum<W>, f64) {
let meter = DiscardMeter::new(policy);
let out = crate::propagate(circuit, sum, &meter, direction);
(out, meter.discarded())
}
pub fn assert_runs_agree<const W: usize>(
policy: impl Into<crate::truncation::BuiltinTruncation>,
discarded: f64,
got: &PauliSum<W>,
want: &PauliSum<W>,
tol: f64,
what: &str,
) {
let cuts = <crate::truncation::BuiltinTruncation as TruncationPolicy<W>>::finalizes_layer(
&policy.into(),
);
assert_eq!(got.len(), want.len(), "{what}: term count");
if !cuts || discarded == 0.0 || want.is_empty() {
assert_terms_close(got, want, tol, what);
return;
}
let overlap = Overlap::of(discarded, got, want, tol);
assert!(
overlap.sees_a_sign_flip,
"{what}: Δ = {discarded:e} of ‖want‖ = {:e} is too heavy a cut to compare",
overlap.want_norm,
);
assert!(
overlap.distance <= overlap.bound,
"{what}: 1 − cos = {:e} exceeds 2Δ²/(‖got‖‖want‖) = {:e} (Δ = {:e})",
overlap.distance,
overlap.bound,
overlap.slack,
);
}
struct Overlap {
distance: f64,
bound: f64,
slack: f64,
want_norm: f64,
sees_a_sign_flip: bool,
}
impl Overlap {
fn of<const W: usize>(discarded: f64, got: &PauliSum<W>, want: &PauliSum<W>, tol: f64) -> Self {
let (got_norm, want_norm) = (norm_sqr(got).sqrt(), norm_sqr(want).sqrt());
let difference = scaled(got, 1.0 / got_norm).add(&scaled(want, -1.0 / want_norm));
let slack = discarded + (want.len() as f64).sqrt() * tol;
let bound = 2.0 * slack * slack / (got_norm * want_norm);
let largest = want.iter().map(|t| t.2.norm_sqr()).fold(0.0, f64::max);
Self {
distance: norm_sqr(&difference) / 2.0,
bound,
slack,
want_norm,
sees_a_sign_flip: bound < 2.0 * largest / (want_norm * want_norm),
}
}
}
pub fn assert_runs_agree_count_only<const W: usize>(
policy: impl Into<crate::truncation::BuiltinTruncation>,
discarded: f64,
got: &PauliSum<W>,
want: &PauliSum<W>,
what: &str,
) {
use crate::truncation::BuiltinTruncation as T;
let n = match policy.into() {
T::TopN(n) | T::ApproxTopN(n) => n,
other => {
panic!("{what}: a count-only comparison needs a bare TopN or ApproxTopN, got {other:?}")
}
};
assert_eq!(got.len(), want.len(), "{what}: term count");
assert_eq!(want.len(), n, "{what}: the last layer must cut");
for (run, sum) in [("got", got), ("want", want)] {
assert!(
sum.iter().all(|t| t.2.re.is_finite() && t.2.im.is_finite()),
"{what}: {run} has a non-finite coefficient"
);
}
assert!(
!Overlap::of(discarded, got, want, 0.0).sees_a_sign_flip,
"{what}: Δ = {discarded:e} is light enough for assert_runs_agree"
);
}
pub fn heavy_cut<const W: usize>(
num_qubits: usize,
steps: usize,
terms: usize,
seed: u64,
) -> (crate::Circuit<W>, PauliSum<W>) {
(trotter_steps(num_qubits, 0.4, steps), tie_heavy_sum(terms, num_qubits, seed))
}
pub fn single_cuts<const W: usize>(
num_qubits: usize,
seed: u64,
) -> Vec<(&'static str, crate::Circuit<W>, PauliSum<W>, usize)> {
let mut dense = crate::Circuit::<W>::new(num_qubits);
dense.push(PtmChannel::unitary_2q(0, 1, haar_su4_matrix()));
let mut hadamard = crate::Circuit::<W>::new(num_qubits);
hadamard.push(PtmChannel::hadamard(0));
let mut tied = BuildAccumulator::<W>::new(num_qubits);
for (x, z, c) in rand_sum_real::<W>(2_000, num_qubits, seed ^ 1).iter() {
let class = (c.re.abs() * 3.0).floor().min(2.0) as i32;
let p = PauliString::<W> { x: *x, z: *z };
let magnitude = 2f64.powi(20 * class - 40);
tied.add_term(p, Complex64::new(magnitude.copysign(c.re), 0.0));
}
let random = rand_sum::<W>(1_500, num_qubits, seed);
vec![
("dense cut", dense, spread_magnitudes(&random, 40, seed), 2_000),
("tied cut", hadamard, tied.finalize(), 1_300),
]
}
pub fn four_term_keys<const W: usize>() -> [PauliString<W>; 4] {
let key = |x: u64, z: u64| {
let mut p = PauliString::<W>::identity();
p.x[0] = x;
p.z[0] = z;
p
};
[
key(1, 0),
key(1 << 5, 1 << 3),
key((1 << 2) | (1 << 7), (1 << 2) | (1 << 7)),
key(1 << 1, 1 << 6),
]
}
pub const FOUR_TERM_WEIGHTS: [f64; 4] = [0.1, 0.2, 0.3, 0.4];
pub fn weighted_four_term_sum<const W: usize>(num_qubits: usize) -> PauliSum<W> {
assert!(num_qubits >= 8, "the fixture's keys reach qubit 7");
let phases = [0.0f64, 1.0, 2.5, -2.0];
let mut acc = BuildAccumulator::<W>::new(num_qubits);
for ((p, w), phi) in four_term_keys::<W>().into_iter().zip(FOUR_TERM_WEIGHTS).zip(phases) {
acc.add_term(p, Complex64::from_polar(w.sqrt(), phi));
}
acc.finalize()
}
pub fn collapsed_index<const W: usize>(sum: &PauliSum<W>, keys: &[PauliString<W>]) -> usize {
assert_eq!(sum.len(), 1, "a collapse leaves exactly one string");
let (x, z, c) = sum.iter().next().unwrap();
assert_eq!(c, Complex64::new(1.0, 0.0), "the survivor's coefficient is 1");
keys.iter()
.position(|p| &p.x == x && &p.z == z)
.expect("the survivor is one of the input strings")
}
pub fn assert_frequencies(counts: &[usize], probs: &[f64], what: &str) {
let trials: usize = counts.iter().sum();
for (i, (&k, &p)) in counts.iter().zip(probs).enumerate() {
let f = k as f64 / trials as f64;
let sigma = (p * (1.0 - p) / trials as f64).sqrt();
assert!(
(f - p).abs() < 4.0 * sigma,
"{what}: slot {i} drawn {k}/{trials} = {f:.4}, expected {p} ± {sigma:.4}",
);
}
}