use crate::bound::Bound;
use crate::graph::Graph;
use crate::rng::Pcg;
#[derive(Clone, Debug)]
pub struct Certificate {
pub y: Vec<f64>,
pub value: f64,
pub homogenised: bool,
pub rump_c: f64,
pub sweeps: usize,
pub rank: usize,
}
#[derive(Clone, Debug, PartialEq)]
pub enum CertError {
Shape { got: usize, want: usize },
NotPsd,
NotFinite,
}
impl core::fmt::Display for CertError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
CertError::Shape { got, want } => write!(f, "y has {got} entries and this graph needs {want}"),
CertError::NotPsd => write!(
f,
"C - Diag(y) did not verify as positive definite, so this y is not dual feasible \
and certifies nothing"
),
CertError::NotFinite => write!(f, "the certificate contains a non-finite value"),
}
}
}
struct Cost {
n: usize,
dense: Vec<f64>,
nz_off: Vec<usize>,
nz_col: Vec<u32>,
nz_val: Vec<f64>,
homogenised: bool,
}
impl Cost {
fn build(g: &Graph) -> Cost {
let homog = g.h.iter().any(|&h| h != 0.0);
let n = if homog { g.n + 1 } else { g.n };
let off = usize::from(homog);
let mut dense = vec![0.0; n * n];
for i in 0..g.n {
if homog && g.h[i] != 0.0 {
let v = -g.h[i] / 2.0;
dense[i + off] = v;
dense[(i + off) * n] = v;
}
for k in g.offset[i]..g.offset[i + 1] {
let j = g.nbr[k] as usize;
let v = -g.w[k] / 2.0;
dense[(i + off) * n + (j + off)] = v;
dense[(j + off) * n + (i + off)] = v;
}
}
for a in 0..n {
dense[a * n + a] = 0.0;
}
let mut nz_off = Vec::with_capacity(n + 1);
let mut nz_col = Vec::new();
let mut nz_val = Vec::new();
nz_off.push(0);
for a in 0..n {
for b in 0..n {
let v = dense[a * n + b];
if v != 0.0 {
nz_col.push(b as u32);
nz_val.push(v);
}
}
nz_off.push(nz_col.len());
}
Cost { n, dense, nz_off, nz_col, nz_val, homogenised: homog }
}
#[inline]
fn row(&self, a: usize) -> &[f64] {
&self.dense[a * self.n..(a + 1) * self.n]
}
#[inline]
fn gather(&self, a: usize, v: &[f64], k: usize, g: &mut [f64]) {
g.iter_mut().for_each(|x| *x = 0.0);
let (s, e) = (self.nz_off[a], self.nz_off[a + 1]);
for (&b, &c) in self.nz_col[s..e].iter().zip(&self.nz_val[s..e]) {
let vb = &v[b as usize * k..b as usize * k + k];
for t in 0..k {
g[t] += c * vb[t];
}
}
}
}
fn rump_c(cost: &Cost, y: &[f64]) -> f64 {
let n = cost.n;
let eps = f64::EPSILON / 2.0; let eta = f64::from_bits(1); let k = (n + 1) as f64;
let gam = k * eps / (1.0 - k * eps);
let tr: f64 = (0..n).map(|a| -y[a]).sum();
let maxd = (0..n).map(|a| -y[a]).fold(f64::NEG_INFINITY, f64::max);
let m = 3.0 * (2.0 * n as f64 + maxd);
let c = gam / (1.0 - gam) * tr + n as f64 * m * eta;
2.0 * c + f64::from_bits(0x03d0_0000_0000_0000)
}
fn verify_psd(cost: &Cost, y: &[f64], c: f64) -> bool {
let n = cost.n;
let mut a = vec![0.0f64; n * n];
for i in 0..n {
let row = cost.row(i);
a[i * n..i * n + n].copy_from_slice(row);
let d = -y[i] - c;
a[i * n + i] = if d.is_finite() { next_down(d) } else { return false };
}
for j in 0..n {
let mut d = a[j * n + j];
for k in 0..j {
let v = a[j * n + k];
d -= v * v;
}
if !(d > 0.0) || !d.is_finite() {
return false;
}
let l = d.sqrt();
a[j * n + j] = l;
for i in (j + 1)..n {
let mut s = a[i * n + j];
for k in 0..j {
s -= a[i * n + k] * a[j * n + k];
}
let v = s / l;
if !v.is_finite() {
return false;
}
a[i * n + j] = v;
}
}
true
}
fn next_down(x: f64) -> f64 {
if x.is_nan() || x == f64::NEG_INFINITY {
return x;
}
if x == 0.0 {
return -f64::from_bits(1);
}
if x > 0.0 {
f64::from_bits(x.to_bits() - 1)
} else {
f64::from_bits(x.to_bits() + 1)
}
}
fn gershgorin_verified(cost: &Cost) -> Option<(Vec<f64>, f64)> {
let rows: Vec<f64> =
(0..cost.n).map(|a| cost.row(a).iter().map(|v| v.abs()).sum::<f64>()).collect();
let scale = rows.iter().cloned().fold(0.0f64, f64::max).max(1.0);
let mut nudge = scale * (2.0f64).powi(-40);
for _ in 0..80 {
let y: Vec<f64> = rows.iter().map(|r| -(r + nudge)).collect();
let ys = snap_down(&y, 0.0);
let c = rump_c(cost, &ys);
if verify_psd(cost, &ys, c) {
return Some((ys, c));
}
nudge *= 4.0;
}
None
}
fn mixing(cost: &Cost, rank: usize, sweeps: usize, seed: u64) -> (Vec<f64>, Vec<f64>, usize) {
let n = cost.n;
let k = rank.clamp(1, n.max(1));
let mut rng = Pcg::new(seed, 0x5D_9A);
let mut v = vec![0.0f64; n * k];
for a in 0..n {
loop {
for t in 0..k {
let u1 = rng.f64().max(1e-12);
let u2 = rng.f64();
v[a * k + t] = (-2.0 * u1.ln()).sqrt() * (core::f64::consts::TAU * u2).cos();
}
let nrm = v[a * k..a * k + k].iter().map(|x| x * x).sum::<f64>().sqrt();
if nrm > 0.0 && nrm.is_finite() {
for t in 0..k {
v[a * k + t] /= nrm;
}
break;
}
}
}
let mut g = vec![0.0f64; k];
for _ in 0..sweeps {
for a in 0..n {
cost.gather(a, &v, k, &mut g);
let nrm = g.iter().map(|x| x * x).sum::<f64>().sqrt();
if nrm > 0.0 && nrm.is_finite() {
for t in 0..k {
v[a * k + t] = -g[t] / nrm;
}
}
}
}
let y = (0..n)
.map(|a| {
cost.gather(a, &v, k, &mut g);
(0..k).map(|t| v[a * k + t] * g[t]).sum::<f64>()
})
.collect();
(y, v, k)
}
fn snap_down(y: &[f64], shift: f64) -> Vec<f64> {
let n = y.len() as f64;
let maxa = y.iter().map(|v| (v + shift).abs()).fold(0.0f64, f64::max);
if maxa == 0.0 || !maxa.is_finite() {
return y.iter().map(|v| v + shift).collect();
}
let e = (n * maxa).log2().ceil() - 52.0;
let gr = (2.0f64).powf(e);
y.iter().map(|v| ((v + shift) / gr).floor() * gr).collect()
}
fn lanczos_min(cost: &Cost, y: &[f64], steps: usize, seed: u64) -> f64 {
let n = cost.n;
if n == 0 {
return 0.0;
}
let m = steps.min(n).max(1);
let mut rng = Pcg::new(seed, 0x1A_9C05);
let mut q: Vec<Vec<f64>> = Vec::with_capacity(m + 1);
let mut v: Vec<f64> = (0..n).map(|_| rng.f64() * 2.0 - 1.0).collect();
let nrm = v.iter().map(|x| x * x).sum::<f64>().sqrt();
if !(nrm > 0.0) {
return 0.0;
}
v.iter_mut().for_each(|x| *x /= nrm);
q.push(v);
let (mut alpha, mut beta) = (Vec::new(), Vec::new());
for j in 0..m {
let qj = &q[j];
let mut w: Vec<f64> = (0..n)
.map(|a| {
let (s, e) = (cost.nz_off[a], cost.nz_off[a + 1]);
let mut acc = -y[a] * qj[a];
for (&b, &c) in cost.nz_col[s..e].iter().zip(&cost.nz_val[s..e]) {
acc += c * qj[b as usize];
}
acc
})
.collect();
let aj: f64 = (0..n).map(|i| w[i] * q[j][i]).sum();
alpha.push(aj);
for _ in 0..2 {
for qi in q.iter() {
let d: f64 = (0..n).map(|i| w[i] * qi[i]).sum();
for i in 0..n {
w[i] -= d * qi[i];
}
}
}
let bj = w.iter().map(|x| x * x).sum::<f64>().sqrt();
if bj < 1e-14 * aj.abs().max(1.0) {
break;
}
beta.push(bj);
w.iter_mut().for_each(|x| *x /= bj);
q.push(w);
}
let k = alpha.len();
if k == 0 {
return 0.0;
}
let mut t = vec![0.0f64; k * k];
for i in 0..k {
t[i * k + i] = alpha[i];
if i + 1 < k {
t[i * k + i + 1] = beta[i];
t[(i + 1) * k + i] = beta[i];
}
}
let _vectors = crate::linalg::jacobi_eig(&mut t, k);
(0..k).map(|i| t[i * k + i]).fold(f64::INFINITY, f64::min)
}
#[derive(Clone, Debug)]
pub struct Rounding {
pub state: Vec<i8>,
pub cut: f64,
pub energy: f64,
pub hyperplanes: usize,
pub guaranteed: bool,
}
pub fn goemans_williamson(g: &Graph, p: &Params, seed: u64, hyperplanes: usize) -> Rounding {
let n = g.n;
if n == 0 {
return Rounding { state: Vec::new(), cut: 0.0, energy: 0.0, hyperplanes: 0, guaranteed: true };
}
let guaranteed = g.w.iter().all(|&w| w <= 0.0) && g.h.iter().all(|&h| h == 0.0);
let cost = Cost::build(g);
let rank = p.rank.unwrap_or_else(|| ((2.0 * cost.n as f64).sqrt().ceil() as usize + 1).min(cost.n));
let (_y, v, k) = mixing(&cost, rank, p.sweeps, seed);
let mut rng = Pcg::new(seed, 0x060E_3A15);
let draws = hyperplanes.max(1);
let mut best: Vec<i8> = vec![1; n];
let mut best_e = f64::INFINITY;
let mut s = vec![1i8; n];
for _ in 0..draws {
let r: Vec<f64> = (0..k)
.map(|_| {
let u1 = rng.f64().max(1e-12);
let u2 = rng.f64();
(-2.0 * u1.ln()).sqrt() * (core::f64::consts::TAU * u2).cos()
})
.collect();
let off = usize::from(cost.homogenised);
for i in 0..n {
let a = i + off;
let dot: f64 = (0..k).map(|t| v[a * k + t] * r[t]).sum();
s[i] = if dot >= 0.0 { 1 } else { -1 };
}
let e = g.energy(&s);
if e < best_e {
best_e = e;
best.copy_from_slice(&s);
}
}
let mut cut = 0.0f64;
for u in 0..n {
for kk in g.offset[u]..g.offset[u + 1] {
let vtx = g.nbr[kk] as usize;
if vtx > u && best[u] != best[vtx] {
cut -= g.w[kk];
}
}
}
let energy = g.energy(&best);
Rounding { state: best, cut, energy, hyperplanes: draws, guaranteed }
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Params {
pub sweeps: usize,
pub rank: Option<usize>,
pub lanczos: usize,
}
impl Default for Params {
fn default() -> Self {
Params { sweeps: 200, rank: None, lanczos: 64 }
}
}
pub fn certified(g: &Graph, p: &Params, seed: u64) -> (Bound, Certificate) {
let cost = Cost::build(g);
let n = cost.n;
if n == 0 {
let b = Bound { value: 0.0, parts: 0, method: "sdp: empty graph", rounds: 0, best_round: 0 };
let c = Certificate {
y: Vec::new(), value: 0.0, homogenised: false, rump_c: 0.0, sweeps: 0, rank: 0,
};
return (b, c);
}
let rank = p.rank.unwrap_or_else(|| ((2.0 * n as f64).sqrt().ceil() as usize + 1).min(n));
let Some((mut best_y, mut rc)) = gershgorin_verified(&cost) else {
let b = Bound { value: f64::NEG_INFINITY, parts: 0, method: "sdp: no dual point verified", rounds: 0, best_round: 0 };
let c = Certificate { y: Vec::new(), value: f64::NEG_INFINITY, homogenised: cost.homogenised, rump_c: 0.0, sweeps: 0, rank };
return (b, c);
};
let mut best_val: f64 = best_y.iter().sum();
let (y, _v, _k) = mixing(&cost, rank, p.sweeps, seed);
let theta = lanczos_min(&cost, &y, p.lanczos, seed);
let mut delta = (theta.abs() * 1e-6).max(1e-13);
let mut accepted: Option<(Vec<f64>, f64, f64)> = None;
let mut last_fail = 0.0f64;
for _ in 0..64 {
let cand = theta - delta;
let ys = snap_down(&y, cand);
let c = rump_c(&cost, &ys);
if verify_psd(&cost, &ys, c) {
let v: f64 = ys.iter().sum();
accepted = Some((ys, v, c));
break;
}
last_fail = delta;
delta *= 2.0;
}
if let Some((_, _, _)) = &accepted {
let mut lo = last_fail;
let mut hi = delta;
for _ in 0..8 {
let mid = 0.5 * (lo + hi);
let ys = snap_down(&y, theta - mid);
let c = rump_c(&cost, &ys);
if verify_psd(&cost, &ys, c) {
hi = mid;
let v: f64 = ys.iter().sum();
accepted = Some((ys, v, c));
} else {
lo = mid;
}
}
}
let mut sweeps_used = 0usize;
if let Some((ys, v, c)) = accepted {
if v > best_val {
best_val = v;
best_y = ys;
rc = c;
sweeps_used = p.sweeps;
}
}
let cert = Certificate {
y: best_y,
value: best_val,
homogenised: cost.homogenised,
rump_c: rc,
sweeps: sweeps_used,
rank,
};
let b = Bound {
value: best_val,
parts: 1,
method: "sdp: mixing-method primal, dual point verified positive definite (Rump)",
rounds: p.sweeps,
best_round: p.sweeps,
};
(b, cert)
}
impl Certificate {
pub fn verify(&self, g: &Graph) -> Result<f64, CertError> {
let cost = Cost::build(g);
if self.y.len() != cost.n {
return Err(CertError::Shape { got: self.y.len(), want: cost.n });
}
if self.y.iter().any(|v| !v.is_finite()) || !self.value.is_finite() {
return Err(CertError::NotFinite);
}
let c = rump_c(&cost, &self.y);
if !verify_psd(&cost, &self.y, c) {
return Err(CertError::NotPsd);
}
Ok(self.y.iter().sum())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
use crate::ising::lattice2d;
fn random_graph(n: usize, p: f64, seed: u64, fields: bool) -> Graph {
let mut rng = Pcg::new(seed, 0xD0);
let mut gb = GraphBuilder::new(n);
for i in 0..n {
if fields {
gb.bias(i, rng.f64() * 2.0 - 1.0);
}
for j in (i + 1)..n {
if rng.f64() < p {
gb.couple(i, j, rng.f64() * 2.0 - 1.0);
}
}
}
gb.build()
}
fn brute_min(g: &Graph) -> f64 {
(0..(1u32 << g.n))
.map(|m| {
let s: Vec<i8> = (0..g.n).map(|i| if m >> i & 1 == 1 { 1 } else { -1 }).collect();
g.energy(&s)
})
.fold(f64::INFINITY, f64::min)
}
#[test]
fn the_bound_never_exceeds_the_true_minimum() {
let p = Params { sweeps: 60, rank: None, lanczos: 24 };
for seed in 0..60u64 {
for fields in [false, true] {
let g = random_graph(9, 0.45, seed, fields);
let truth = brute_min(&g);
let (b, _) = certified(&g, &p, seed);
assert!(
b.value <= truth + 1e-9,
"seed {seed} fields={fields}: sdp gave {} above the true minimum {truth}",
b.value
);
}
}
}
#[test]
fn a_certificate_verifies_independently_of_how_it_was_found() {
let g = random_graph(12, 0.4, 5, true);
let (b, cert) = certified(&g, &Params::default(), 5);
let v = cert.verify(&g).expect("its own certificate must re-verify");
assert!((v - b.value).abs() < 1e-12, "verify gave {v}, bound said {}", b.value);
assert!(v <= brute_min(&g) + 1e-9);
}
#[test]
fn a_tampered_certificate_is_refused() {
let g = random_graph(10, 0.5, 2, false);
let (_, mut cert) = certified(&g, &Params::default(), 2);
let honest = cert.verify(&g).unwrap();
for a in 0..cert.y.len() {
cert.y[a] += 1.0;
}
assert_eq!(cert.verify(&g), Err(CertError::NotPsd), "an inflated y must not verify");
cert.y.push(0.0);
assert!(matches!(cert.verify(&g), Err(CertError::Shape { .. })));
assert!(honest.is_finite());
}
#[test]
fn it_is_never_worse_than_the_trivial_floor() {
for seed in 0..25u64 {
let g = random_graph(11, 0.5, seed + 40, seed % 2 == 0);
let (b, _) = certified(&g, &Params { sweeps: 5, rank: Some(2), lanczos: 8 }, seed);
let dec = crate::bound::decoupled(&g);
assert!(
b.value >= dec.value - 1e-9,
"seed {seed}: sdp {} fell below decoupled {}",
b.value,
dec.value
);
}
}
#[test]
fn a_bad_search_loosens_the_bound_without_invalidating_it() {
let p = Params { sweeps: 1, rank: Some(1), lanczos: 4 };
for seed in 0..30u64 {
let g = random_graph(10, 0.45, seed + 90, false);
let truth = brute_min(&g);
let (b, cert) = certified(&g, &p, seed);
assert!(b.value <= truth + 1e-9, "seed {seed}: {} > {truth}", b.value);
assert!(cert.verify(&g).is_ok(), "seed {seed}: certificate must still verify");
}
}
#[test]
fn the_verifier_rejects_a_matrix_that_is_not_definite() {
let g = lattice2d(4, 1.0);
let cost = Cost::build(&g);
let zero = vec![0.0; cost.n];
let c = rump_c(&cost, &zero);
assert!(!verify_psd(&cost, &zero, c), "a zero-diagonal C with edges cannot be PSD");
let (yg, cg) = gershgorin_verified(&cost).expect("a dominant diagonal must verify");
assert!(verify_psd(&cost, &yg, cg));
let dec = crate::bound::decoupled(&g).value;
let got: f64 = yg.iter().sum();
assert!(got <= dec + 1e-9, "the floor {got} must not exceed decoupled {dec}");
}
#[test]
fn homogenisation_is_used_exactly_when_there_are_fields() {
let no_h = random_graph(8, 0.5, 1, false);
let with_h = random_graph(8, 0.5, 1, true);
let (_, c1) = certified(&no_h, &Params { sweeps: 10, rank: Some(4), lanczos: 8 }, 1);
let (_, c2) = certified(&with_h, &Params { sweeps: 10, rank: Some(4), lanczos: 8 }, 1);
assert!(!c1.homogenised && c1.y.len() == no_h.n);
assert!(c2.homogenised && c2.y.len() == with_h.n + 1, "a gauge spin is prepended");
}
#[test]
fn the_guarantee_holds_where_the_guarantee_applies() {
let mut worst = f64::INFINITY;
for seed in 0..24u64 {
let mut rng = Pcg::new(seed, 0x0000_60E3);
let n = 14;
let mut gb = GraphBuilder::new(n);
for i in 0..n {
for j in (i + 1)..n {
if rng.f64() < 0.4 {
gb.couple(i, j, -(rng.f64() + 0.05));
}
}
}
let g = gb.build();
let r = goemans_williamson(&g, &Params::default(), seed, 64);
assert!(r.guaranteed, "seed {seed}: an antiferromagnet must be inside the hypothesis");
assert_eq!(r.state.len(), n);
assert!(r.state.iter().all(|&v| v == 1 || v == -1));
assert!((r.energy - g.energy(&r.state)).abs() < 1e-9);
let o = crate::branch::solve(&g, &crate::branch::Params::default());
assert!(o.proved_optimal);
let w: f64 = (0..n)
.flat_map(|u| (g.offset[u]..g.offset[u + 1]).map(move |k| (u, k)))
.filter(|&(u, k)| (g.nbr[k] as usize) > u)
.map(|(_, k)| -g.w[k])
.sum();
let max_cut = (w - o.energy) / 2.0;
if max_cut > 1e-9 {
let ratio = r.cut / max_cut;
worst = worst.min(ratio);
assert!(
ratio >= 0.87856,
"seed {seed}: rounded {} against a proved maximum of {max_cut} is {ratio:.4}, \
below the Goemans-Williamson ratio",
r.cut
);
}
}
assert!(worst < 1.0 - 1e-12 || worst.is_infinite(), "worst ratio was {worst}");
}
#[test]
fn the_guarantee_flag_refuses_a_mixed_sign_instance() {
let g = random_graph(12, 0.5, 3, false); let r = goemans_williamson(&g, &Params::default(), 3, 32);
assert!(!r.guaranteed, "a mixed-sign instance is outside the theorem's hypothesis");
assert!((r.energy - g.energy(&r.state)).abs() < 1e-9);
assert!(r.state.iter().all(|&v| v == 1 || v == -1));
let fielded = random_graph(10, 0.5, 4, true);
assert!(!goemans_williamson(&fielded, &Params::default(), 4, 8).guaranteed);
}
#[test]
fn more_hyperplanes_never_lose() {
for seed in 0..8u64 {
let g = random_graph(20, 0.35, seed, false);
let few = goemans_williamson(&g, &Params::default(), seed, 1);
let many = goemans_williamson(&g, &Params::default(), seed, 128);
assert_eq!(few.hyperplanes, 1);
assert_eq!(many.hyperplanes, 128);
assert!(
many.energy <= few.energy + 1e-9,
"seed {seed}: 128 draws gave {} against 1 draw's {}",
many.energy,
few.energy
);
}
}
#[test]
fn an_empty_graph_returns_rather_than_panicking() {
let g = GraphBuilder::new(0).build();
let (b, c) = certified(&g, &Params::default(), 1);
assert_eq!(b.value, 0.0);
assert!(c.y.is_empty());
}
fn gather_dense(cost: &Cost, a: usize, v: &[f64], k: usize, g: &mut [f64]) {
g.iter_mut().for_each(|x| *x = 0.0);
let row = cost.row(a);
for b in 0..cost.n {
let c = row[b];
if c != 0.0 {
for t in 0..k {
g[t] += c * v[b * k + t];
}
}
}
}
#[test]
fn the_sparse_gather_is_bit_identical_to_the_dense_one() {
for seed in 0..6u64 {
let g = random_graph(26, 0.35, seed, seed % 2 == 0);
let cost = Cost::build(&g);
let k = 5;
let mut rng = Pcg::new(seed, 0xC5B);
let v: Vec<f64> = (0..cost.n * k).map(|_| rng.f64() * 2.0 - 1.0).collect();
let (mut sparse, mut dense) = (vec![0.0; k], vec![0.0; k]);
for a in 0..cost.n {
cost.gather(a, &v, k, &mut sparse);
gather_dense(&cost, a, &v, k, &mut dense);
for t in 0..k {
assert_eq!(
sparse[t].to_bits(),
dense[t].to_bits(),
"seed {seed} row {a} component {t}: sparse {:e}, dense {:e}",
sparse[t],
dense[t]
);
}
}
}
}
#[test]
fn the_lanczos_estimate_is_a_ritz_value_of_the_actual_matrix() {
for seed in 0..5u64 {
let g = random_graph(16, 0.4, seed, seed % 2 == 0);
let cost = Cost::build(&g);
let n = cost.n;
let y: Vec<f64> = (0..n).map(|a| -0.5 - 0.1 * a as f64).collect();
let mut dense = vec![0.0f64; n * n];
for a in 0..n {
dense[a * n..a * n + n].copy_from_slice(cost.row(a));
dense[a * n + a] = -y[a]; }
let _ = crate::linalg::jacobi_eig(&mut dense, n);
let truth = (0..n).map(|i| dense[i * n + i]).fold(f64::INFINITY, f64::min);
let est = lanczos_min(&cost, &y, n, seed);
assert!(est >= truth - 1e-6, "seed {seed}: Ritz value {est} sits BELOW lambda_min {truth}");
assert!(
(est - truth).abs() <= 1e-3 * truth.abs().max(1.0),
"seed {seed}: Lanczos says {est}, the dense eigensolver says {truth}"
);
}
}
#[test]
fn the_sparse_index_enumerates_exactly_the_non_zeros() {
let g = random_graph(20, 0.3, 7, true);
let cost = Cost::build(&g);
let mut counted = 0usize;
for a in 0..cost.n {
let (s, e) = (cost.nz_off[a], cost.nz_off[a + 1]);
let cols = &cost.nz_col[s..e];
assert!(cols.windows(2).all(|w| w[0] < w[1]), "row {a} is not strictly ascending");
for (&b, &val) in cols.iter().zip(&cost.nz_val[s..e]) {
assert_eq!(val.to_bits(), cost.row(a)[b as usize].to_bits());
assert!(val != 0.0);
}
counted += e - s;
}
let dense_nz = cost.dense.iter().filter(|v| **v != 0.0).count();
assert_eq!(counted, dense_nz, "the index missed a non-zero or invented one");
}
}