use crate::graph::Graph;
use crate::rng::Pcg;
pub trait Solver {
fn name(&self) -> &str;
fn solve(&self, g: &Graph) -> (Vec<i8>, f64);
}
pub struct Exhaustive;
impl Exhaustive {
pub const MAX_SPINS: usize = 26;
}
impl Solver for Exhaustive {
fn name(&self) -> &str {
"exhaustive"
}
fn solve(&self, g: &Graph) -> (Vec<i8>, f64) {
assert!(
g.n <= Self::MAX_SPINS,
"exhaustive search over {} spins is 2^{} states; use a planted instance instead",
g.n,
g.n
);
let mut best = vec![-1i8; g.n];
let mut best_e = f64::INFINITY;
let mut s = vec![-1i8; g.n];
for mask in 0..(1u64 << g.n) {
for i in 0..g.n {
s[i] = if mask >> i & 1 == 1 { 1 } else { -1 };
}
let e = g.energy(&s);
if e < best_e {
best_e = e;
best.copy_from_slice(&s);
}
}
(best, best_e)
}
}
pub struct SteepestDescent {
pub restarts: usize,
pub seed: u64,
}
impl Solver for SteepestDescent {
fn name(&self) -> &str {
"steepest-descent"
}
fn solve(&self, g: &Graph) -> (Vec<i8>, f64) {
let mut rng = Pcg::new(self.seed, 0);
let mut best = vec![1i8; g.n];
let mut best_e = f64::INFINITY;
for _ in 0..self.restarts.max(1) {
let mut s: Vec<i8> = (0..g.n).map(|_| if rng.f64() < 0.5 { 1 } else { -1 }).collect();
loop {
let mut best_gain = 0.0;
let mut pick = usize::MAX;
for i in 0..g.n {
let d = crate::kernel::delta_e(g.field(i, &s), s[i]);
if d < best_gain - 1e-12 {
best_gain = d;
pick = i;
}
}
if pick == usize::MAX {
break;
}
s[pick] = -s[pick];
}
let e = g.energy(&s);
if e < best_e {
best_e = e;
best = s;
}
}
(best, best_e)
}
}
pub struct RandomGuess {
pub tries: usize,
pub seed: u64,
}
impl Solver for RandomGuess {
fn name(&self) -> &str {
"random-guess"
}
fn solve(&self, g: &Graph) -> (Vec<i8>, f64) {
let mut rng = Pcg::new(self.seed, 1);
let mut best = vec![1i8; g.n];
let mut best_e = f64::INFINITY;
for _ in 0..self.tries.max(1) {
let s: Vec<i8> = (0..g.n).map(|_| if rng.f64() < 0.5 { 1 } else { -1 }).collect();
let e = g.energy(&s);
if e < best_e {
best_e = e;
best = s;
}
}
(best, best_e)
}
}
pub struct Annealer {
pub schedule: crate::schedule::Schedule,
pub seed: u64,
}
impl Solver for Annealer {
fn name(&self) -> &str {
"anneal"
}
fn solve(&self, g: &Graph) -> (Vec<i8>, f64) {
crate::tempering::anneal_scheduled(g, &self.schedule, self.seed, None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schedule::Schedule;
#[test]
fn exhaustive_finds_the_ferromagnetic_ground_state() {
let g = crate::ising::lattice2d(4, 1.0);
let (_s, e) = Exhaustive.solve(&g);
assert_eq!(e, -32.0);
}
#[test]
fn exhaustive_finds_the_frustrated_optimum() {
let mut b = crate::graph::GraphBuilder::new(5);
for i in 0..5 {
b.couple(i, (i + 1) % 5, -1.0);
}
let (_s, e) = Exhaustive.solve(&b.build());
assert_eq!(e, -3.0, "an odd antiferromagnetic ring cannot do better than -3");
}
#[test]
fn the_noise_oracle_loses_to_everything() {
let g = crate::ising::lattice2d(8, 1.0);
let noise = RandomGuess { tries: 2000, seed: 1 }.solve(&g).1;
let greedy = SteepestDescent { restarts: 20, seed: 1 }.solve(&g).1;
let annealed = Annealer { schedule: Schedule::geometric(0.05, 4.0, 60, 20), seed: 1 }
.solve(&g)
.1;
assert!(greedy < noise, "greedy {greedy} should beat noise {noise}");
assert!(annealed <= greedy, "annealing {annealed} should be at least greedy {greedy}");
assert!(annealed < noise);
}
#[test]
fn greedy_really_is_a_local_optimum() {
let g = crate::ising::lattice2d(6, 1.0);
let (s, _e) = SteepestDescent { restarts: 5, seed: 3 }.solve(&g);
for i in 0..g.n {
assert!(
crate::kernel::delta_e(g.field(i, &s), s[i]) >= -1e-12,
"spin {i} still improves the energy"
);
}
}
#[test]
fn restarts_are_a_real_knob() {
let g = crate::planted::frustrated_loops(6, 40, 7);
let one = SteepestDescent { restarts: 1, seed: 5 }.solve(&g.graph).1;
let many = SteepestDescent { restarts: 200, seed: 5 }.solve(&g.graph).1;
assert!(many <= one, "more restarts must not do worse: {many} vs {one}");
}
}