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,
}
}
pub fn odd_cycle(g: &Graph, max_len: usize) -> Bound {
let base = decoupled(g);
if max_len < 3 {
return base;
}
let mut eid = std::collections::BTreeMap::new();
let mut ew: Vec<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 {
eid.insert((i, j), ew.len());
ew.push(g.w[k]);
}
}
}
let key = |a: usize, b: usize| if a < b { (a, b) } else { (b, a) };
let mut claimed = vec![false; ew.len()];
let mut penalty = 0.0;
let mut cycles = 0usize;
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 {
continue;
}
let e0 = eid[&key(i, j)];
if claimed[e0] {
continue;
}
let mut prev: Vec<Option<usize>> = vec![None; g.n];
let mut seen = vec![false; g.n];
seen[j] = true;
let mut q = std::collections::VecDeque::from([(j, 0usize)]);
let mut path: Option<Vec<usize>> = None;
while let Some((u, d)) = q.pop_front() {
if d + 1 >= max_len {
continue;
}
for kk in g.offset[u]..g.offset[u + 1] {
let v = g.nbr[kk] as usize;
let e = eid[&key(u, v)];
if e == e0 || claimed[e] {
continue;
}
if v == i {
let mut p = vec![i, u];
let mut cur = u;
while let Some(pp) = prev[cur] {
p.push(pp);
cur = pp;
}
path = Some(p);
break;
}
if !seen[v] {
seen[v] = true;
prev[v] = Some(u);
q.push_back((v, d + 1));
}
}
if path.is_some() {
break;
}
}
let Some(p) = path else { continue };
let mut edges = vec![e0];
let mut ok = true;
for w in p.windows(2) {
let e = eid[&key(w[0], w[1])];
if claimed[e] || edges.contains(&e) {
ok = false;
break;
}
edges.push(e);
}
if !ok || edges.len() < 3 {
continue;
}
let negatives = edges.iter().filter(|&&e| ew[e] < 0.0).count();
if negatives % 2 == 0 {
continue;
}
let min_abs = edges.iter().map(|&e| ew[e].abs()).fold(f64::INFINITY, f64::min);
if !min_abs.is_finite() || min_abs <= 0.0 {
continue;
}
for e in edges {
claimed[e] = true;
}
penalty += 2.0 * min_abs;
cycles += 1;
}
}
Bound {
value: base.value + penalty,
parts: cycles,
method: "decoupled floor plus 2*min|J| per edge-disjoint frustrated cycle",
rounds: 0,
best_round: 0,
}
}
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_odd_cycle_bound_is_sound_too() {
for seed in 0..150u64 {
let g = random_graph(9, 0.45, seed);
let truth = true_min(&g);
let b = odd_cycle(&g, 6);
assert!(
b.value <= truth + 1e-9,
"seed {seed}: odd_cycle gave {} above the true minimum {truth}",
b.value
);
}
}
#[test]
fn a_triangle_is_frustrated_and_the_bound_says_so() {
let mut gb = GraphBuilder::new(3);
for (i, j) in [(0, 1), (1, 2), (2, 0)] {
gb.couple(i, j, -1.0);
}
let g = gb.build();
assert!((true_min(&g) - (-1.0)).abs() < 1e-9, "a frustrated triangle bottoms out at -1");
assert!((decoupled(&g).value - (-3.0)).abs() < 1e-9, "the trivial floor is -3");
let b = odd_cycle(&g, 4);
assert_eq!(b.parts, 1, "one cycle claimed");
assert!((b.value - (-1.0)).abs() < 1e-9, "cycle bound {} should be exact here", b.value);
}
#[test]
fn an_unfrustrated_cycle_is_not_charged_for() {
let mut gb = GraphBuilder::new(4);
for (i, j) in [(0, 1), (1, 2), (2, 3), (3, 0)] {
gb.couple(i, j, -1.0);
}
let g = gb.build();
let b = odd_cycle(&g, 5);
assert_eq!(b.parts, 0, "no frustrated cycle exists here");
assert!((b.value - decoupled(&g).value).abs() < 1e-12);
}
#[test]
fn the_cycle_bound_beats_the_forest_bound_where_the_forest_bound_is_blind() {
let mut gb = GraphBuilder::new(6);
for (i, j) in [(0, 1), (1, 2), (2, 0), (3, 4), (4, 5), (5, 3)] {
gb.couple(i, j, -1.0);
}
let g = gb.build();
assert!(g.h.iter().all(|&h| h == 0.0), "no fields, which is the G-set case");
let (f, c) = (forest(&g, 40), odd_cycle(&g, 4));
assert!(
(f.value - decoupled(&g).value).abs() < 1e-9,
"forest {} should degenerate to decoupled {}",
f.value,
decoupled(&g).value
);
assert!(c.value > f.value + 1e-9, "cycle {} must beat forest {}", c.value, f.value);
assert!(c.value <= true_min(&g) + 1e-9, "and still be sound");
}
#[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);
}
}