use ferrotherm::graph::{Graph, GraphBuilder};
use ferrotherm::host::Timing;
use ferrotherm::rng::Pcg;
use ferrotherm::{bound, branch, popanneal, sdp, tabu};
fn instance(n: usize, p: f64, seed: u64) -> Graph {
let mut rng = Pcg::new(seed, 0x8AC_E70);
let mut gb = GraphBuilder::new(n);
for i in 0..n {
gb.bias(i, rng.f64() - 0.5);
for j in (i + 1)..n {
if rng.f64() < p {
gb.couple(i, j, rng.f64() * 2.0 - 1.0);
}
}
}
gb.build()
}
fn main() {
let n = 22;
let seeds = 6u64;
println!("bounds against a PROVED optimum, {n} spins x {seeds} instances\n");
println!(
" {:>4} {:>10} {:>10} {:>10} {:>10} {:>10} {:>10} {:>9}",
"seed", "optimum", "decoupled", "odd-cycle", "sdp", "tabu", "pop-ann", "nodes"
);
let mut bad = 0usize;
let mut total_nodes = 0u64;
let (_, t) = Timing::around(|| {
for seed in 0..seeds {
let g = instance(n, 0.18, seed);
let t = tabu::search(
&g,
&tabu::Params { iterations: 20_000, tenure: n / 4, restart_after: Some(2_000) },
seed,
);
let pa = popanneal::run(
&g,
&popanneal::Params::linear_from_zero(200, 3, 8.0, 40),
seed,
);
let ex = branch::solve(
&g,
&branch::Params { max_nodes: 20_000_000, incumbent: Some(t.state.clone()) },
);
total_nodes += ex.nodes;
if !ex.proved_optimal {
eprintln!(
"\n ** seed {seed}: the search hit its node budget after {} nodes, so there \
is no proved optimum to check the bounds against. This example is sized \
wrong, not merely slow: without the proof it checks nothing. **",
ex.nodes
);
std::process::exit(2);
}
let d = bound::decoupled(&g);
let c = bound::odd_cycle(&g, 6);
let (sd, cert) = sdp::certified(&g, &sdp::Params::default(), seed);
let sdp_value = match cert.verify(&g) {
Ok(v) => v,
Err(e) => {
eprintln!("\n ** seed {seed}: the SDP certificate did not re-verify: {e} **");
std::process::exit(1);
}
};
println!(
" {seed:>4} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {:>10.3} {:>9}",
ex.energy, d.value, c.value, sd.value, t.energy, pa.energy, ex.nodes
);
let tol = 1e-9;
for (name, v) in [
("decoupled", d.value),
("odd_cycle", c.value),
("sdp", sd.value),
("sdp (re-verified)", sdp_value),
] {
if v > ex.energy + tol {
eprintln!(
"\n ** UNSOUND: seed {seed}, `{name}` returned {v:.9} as a LOWER bound on \
the ground energy, and the proved ground energy is {:.9}. A state with \
that energy exists, so the bound is not one. **",
ex.energy
);
bad += 1;
}
}
for (name, v) in [("tabu", t.energy), ("popanneal", pa.energy)] {
if v < ex.energy - tol {
eprintln!(
"\n ** IMPOSSIBLE: seed {seed}, `{name}` reports energy {v:.9}, below the \
PROVED minimum {:.9}. Either the search is scoring states with a \
different energy function or the proof is wrong. **",
ex.energy
);
bad += 1;
}
}
}
});
println!("\n {total_nodes} nodes across {seeds} proofs ({t})");
if bad > 0 {
eprintln!("\n{bad} bound violation(s). This is a soundness failure, not a regression.");
std::process::exit(1);
}
println!(" every bound sits at or below the proved optimum, and every heuristic at or above.");
println!("\nREADING: the gap between `decoupled` and `optimum` is what the harder bounds are");
println!("competing to close, and the gap between `optimum` and `tabu` is what the heuristics");
println!("are competing to close. At this size both are measurable against truth; at G-set size");
println!("neither is, which is why `gset_gap` reports a bracket instead of a number.");
}