use crate::exact::Elimination;
use crate::graph::{Graph, GraphBuilder};
#[derive(Clone, Debug)]
pub struct Bound {
pub value: f64,
pub parts: usize,
pub method: &'static str,
pub rounds: usize,
pub best_round: usize,
}
impl Bound {
pub fn gap(&self, g: &Graph, s: &[i8]) -> f64 {
g.energy(s) - self.value
}
#[must_use = "false does not mean the state is suboptimal, only that this bound does not prove it optimal"]
pub fn proves_optimal(&self, g: &Graph, s: &[i8], tol: f64) -> bool {
self.gap(g, s) <= tol
}
}
pub fn decoupled(g: &Graph) -> Bound {
let mut v = 0.0;
for i in 0..g.n {
v -= g.h[i].abs();
for k in g.offset[i]..g.offset[i + 1] {
if g.nbr[k] as usize > i {
v -= g.w[k].abs();
}
}
}
Bound {
value: v,
parts: 1,
method: "decoupled: every term at its own minimum",
rounds: 0,
best_round: 0,
}
}
pub fn forest(g: &Graph, rounds: usize) -> Bound {
let parts = forest_partition(g);
if parts.is_empty() {
let mut b = decoupled(g);
b.method = "no couplings: the field-only optimum, which is exact";
return b;
}
let k = parts.len();
let elim = Elimination::default();
let mut share: Vec<Vec<f64>> = (0..k).map(|_| g.h.iter().map(|&h| h / k as f64).collect()).collect();
let mut best = f64::NEG_INFINITY;
let mut best_round = 0usize;
let mut used = 0usize;
for r in 0..=rounds {
let mut total = 0.0;
let mut states: Vec<Vec<i8>> = Vec::with_capacity(k);
for (p, edges) in parts.iter().enumerate() {
let mut gb = GraphBuilder::new(g.n);
for &(i, j, w) in edges {
gb.couple(i, j, w);
}
for i in 0..g.n {
gb.bias(i, share[p][i]);
}
let part = gb.build();
match elim.ground_state(&part) {
Ok(ex) => {
total += ex.ground_energy.unwrap_or(f64::NEG_INFINITY);
states.push(ex.ground_state.unwrap_or_else(|| vec![1; g.n]));
}
Err(_) => return decoupled(g),
}
}
if total > best {
best = total;
best_round = r;
}
used = r;
if r == rounds {
break;
}
let step = 1.0 / (r as f64 + 1.0);
for i in 0..g.n {
let mean: f64 = states.iter().map(|s| s[i] as f64).sum::<f64>() / k as f64;
for p in 0..k {
share[p][i] += step * (mean - states[p][i] as f64);
}
}
if (0..g.n).all(|i| states.iter().all(|s| s[i] == states[0][i])) {
break;
}
}
Bound {
value: best,
parts: k,
method: "forest decomposition, tightened by subgradient ascent on the field split",
rounds: used,
best_round,
}
}
fn forest_partition(g: &Graph) -> Vec<Vec<(usize, usize, f64)>> {
let mut remaining: Vec<(usize, usize, f64)> = Vec::new();
for i in 0..g.n {
for k in g.offset[i]..g.offset[i + 1] {
let j = g.nbr[k] as usize;
if j > i {
remaining.push((i, j, g.w[k]));
}
}
}
let mut parts = Vec::new();
while !remaining.is_empty() {
let mut uf: Vec<usize> = (0..g.n).collect();
let mut forest = Vec::new();
let mut left = Vec::new();
for &(i, j, w) in &remaining {
let (ri, rj) = (find(&mut uf, i), find(&mut uf, j));
if ri == rj {
left.push((i, j, w));
} else {
uf[ri] = rj;
forest.push((i, j, w));
}
}
parts.push(forest);
remaining = left;
}
parts
}
fn find(uf: &mut [usize], mut x: usize) -> usize {
while uf[x] != x {
uf[x] = uf[uf[x]];
x = uf[x];
}
x
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
use crate::ising::lattice2d;
use crate::rng::Pcg;
fn true_min(g: &Graph) -> f64 {
let mut best = f64::INFINITY;
for mask in 0u32..(1u32 << g.n) {
let s: Vec<i8> = (0..g.n).map(|i| if mask >> i & 1 == 1 { 1 } else { -1 }).collect();
best = best.min(g.energy(&s));
}
best
}
fn random_graph(n: usize, p: f64, seed: u64) -> Graph {
let mut rng = Pcg::new(seed, 0xB0);
let mut gb = GraphBuilder::new(n);
for i in 0..n {
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()
}
#[test]
fn a_bound_is_never_above_the_true_minimum() {
for seed in 0..200u64 {
let g = random_graph(10, 0.4, seed);
let truth = true_min(&g);
for b in [decoupled(&g), forest(&g, 0), forest(&g, 25)] {
assert!(
b.value <= truth + 1e-9,
"seed {seed}: {} gave {} above the true minimum {truth}",
b.method,
b.value
);
}
}
}
#[test]
fn tightening_helps_and_never_hurts() {
let mut improved = 0;
for seed in 0..40u64 {
let g = random_graph(12, 0.35, seed);
let cold = forest(&g, 0).value;
let warm = forest(&g, 40).value;
assert!(warm >= cold - 1e-9, "seed {seed}: tightening lost ground, {cold} -> {warm}");
if warm > cold + 1e-6 {
improved += 1;
}
}
assert!(improved > 20, "tightening improved only {improved}/40; it is not earning its cost");
}
#[test]
fn the_bound_is_the_best_round_not_the_last_one() {
let g = random_graph(14, 0.35, 1);
let b = forest(&g, 40);
assert_eq!(b.rounds, 40, "this instance should run the full ladder, not stop early");
assert!(
b.best_round < b.rounds,
"seed 1 was chosen because its trajectory dips; if it no longer does, this test is \
blind and needs a new instance rather than deleting"
);
let truncated = forest(&g, b.best_round);
assert!(
(truncated.value - b.value).abs() < 1e-9,
"stopping at the peak must reproduce the bound: {} vs {}",
truncated.value,
b.value
);
}
#[test]
fn a_forest_is_solved_exactly_so_the_gap_closes() {
let mut gb = GraphBuilder::new(9);
for i in 0..8 {
gb.couple(i, i + 1, if i % 2 == 0 { 1.0 } else { -0.7 });
}
gb.bias(3, 0.4);
gb.bias(7, -0.9);
let g = gb.build();
let b = forest(&g, 5);
assert_eq!(b.parts, 1, "a chain needs one forest");
let truth = true_min(&g);
assert!((b.value - truth).abs() < 1e-9, "chain bound {} vs exact {truth}", b.value);
}
#[test]
fn the_ferromagnet_is_proven_optimal_rather_than_merely_unbeaten() {
let g = lattice2d(6, 1.0);
let b = forest(&g, 80);
let up = vec![1i8; g.n];
assert!(
b.proves_optimal(&g, &up, 1e-6),
"gap {:.6} on an unfrustrated lattice; the bound should close",
b.gap(&g, &up)
);
let mut mixed = up.clone();
mixed[0] = -1;
assert!(b.gap(&g, &mixed) > 1.0, "flipping a spin must open the gap");
}
#[test]
fn the_forest_split_beats_the_decoupled_floor_where_there_is_room_to() {
let g = random_graph(14, 0.35, 7);
let (d, f) = (decoupled(&g), forest(&g, 60));
assert!(f.parts >= 2, "a graph this dense does not fit in one forest");
assert!(
f.value > d.value + 1e-6,
"forest {} did not beat decoupled {} on a frustrated instance",
f.value,
d.value
);
assert_eq!(d.parts, 1);
let ferro = lattice2d(6, 1.0);
let (dd, ff) = (decoupled(&ferro), forest(&ferro, 20));
assert!((dd.value - ff.value).abs() < 1e-9, "{} vs {}", dd.value, ff.value);
assert!((ff.value - ferro.energy(&vec![1i8; ferro.n])).abs() < 1e-9);
}
#[test]
fn a_graph_with_no_couplings_is_solved_rather_than_bounded() {
let mut gb = GraphBuilder::new(5);
for i in 0..5 {
gb.bias(i, (i as f64) - 2.0);
}
let g = gb.build();
let b = forest(&g, 3);
assert!(b.method.contains("exact"), "{}", b.method);
assert!((b.value - true_min(&g)).abs() < 1e-12);
}
#[test]
fn the_partition_covers_every_edge_exactly_once() {
let g = lattice2d(5, 1.0);
let parts = forest_partition(&g);
let mut seen: Vec<(usize, usize)> = parts
.iter()
.flat_map(|p| p.iter().map(|&(i, j, _)| (i, j)))
.collect();
let total: usize = parts.iter().map(|p| p.len()).sum();
seen.sort_unstable();
seen.dedup();
assert_eq!(seen.len(), total, "an edge appears in two parts");
let edges = (0..g.n).map(|i| g.offset[i + 1] - g.offset[i]).sum::<usize>() / 2;
assert_eq!(total, edges, "the parts drop {} edge(s)", edges - total);
}
}