use crate::rng::Rng;
pub trait Anneal {
type State: Clone;
type Move;
fn initial(&self, rng: &mut Rng) -> Self::State;
fn energy(&self, state: &Self::State) -> f64;
fn propose(&self, state: &Self::State, rng: &mut Rng) -> Self::Move;
fn delta(&self, state: &Self::State, mv: &Self::Move) -> f64;
fn apply(&self, state: &mut Self::State, mv: Self::Move);
fn sweep_len(&self) -> usize {
1
}
}
#[derive(Debug, Clone, Copy)]
pub enum Schedule {
Geometric { t_start: f64, t_end: f64 },
Adaptive { fallback: (f64, f64) },
}
impl Schedule {
pub fn adaptive() -> Self {
Schedule::Adaptive {
fallback: (1.0, 1e-3),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct Sa {
pub iterations: usize,
pub restarts: usize,
pub schedule: Schedule,
pub seed: u64,
}
impl Default for Sa {
fn default() -> Self {
Sa {
iterations: 10_000,
restarts: 8,
schedule: Schedule::adaptive(),
seed: 0,
}
}
}
#[derive(Debug, Clone)]
pub struct SaResult<S> {
pub best: S,
pub energy: f64,
}
impl Sa {
pub fn optimize<A: Anneal>(&self, problem: &A) -> SaResult<A::State> {
let mut rng = Rng::new(self.seed);
let (t_start, t_end) = match self.schedule {
Schedule::Geometric { t_start, t_end } => (t_start, t_end),
Schedule::Adaptive { fallback } => adaptive_schedule(problem, fallback, &mut rng),
};
let mut best: Option<(A::State, f64)> = None;
for _ in 0..self.restarts.max(1) {
let (state, energy) = one_run(problem, self.iterations, t_start, t_end, &mut rng);
if best.as_ref().is_none_or(|(_, b)| energy < *b) {
best = Some((state, energy));
}
}
let (best, energy) = best.expect("at least one restart runs");
SaResult { best, energy }
}
}
fn one_run<A: Anneal>(
problem: &A,
iterations: usize,
t_start: f64,
t_end: f64,
rng: &mut Rng,
) -> (A::State, f64) {
let mut current = problem.initial(rng);
let mut current_e = problem.energy(¤t);
let mut best = current.clone();
let mut best_e = current_e;
if iterations == 0 {
return (best, best_e);
}
let ratio = (t_end / t_start).powf(1.0 / iterations as f64);
let mut t = t_start;
#[cfg(debug_assertions)]
let mut accepted = 0usize;
for _ in 0..iterations {
let mv = problem.propose(¤t, rng);
let delta = problem.delta(¤t, &mv);
let accept = delta < 0.0 || rng.uniform() < (-delta / t).exp();
if accept {
problem.apply(&mut current, mv);
current_e += delta;
#[cfg(debug_assertions)]
{
accepted += 1;
if accepted % 1024 == 0 {
let full = problem.energy(¤t);
debug_assert!(
(current_e - full).abs() <= 1e-6 * full.abs().max(1.0),
"Anneal::delta drifted from Anneal::energy: accumulated {current_e}, actual {full}"
);
}
}
if current_e < best_e {
best_e = current_e;
best = current.clone();
}
}
t *= ratio;
}
let best_e = problem.energy(&best);
(best, best_e)
}
pub(crate) fn adaptive_schedule<A: Anneal>(
problem: &A,
fallback: (f64, f64),
rng: &mut Rng,
) -> (f64, f64) {
const SAMPLES: usize = 2_000;
const P_START: f64 = 0.8;
const P_END: f64 = 0.01;
let mut current = problem.initial(rng);
let mut sum_abs = 0.0;
let mut min_abs = f64::INFINITY;
let mut count = 0usize;
for _ in 0..SAMPLES {
let mv = problem.propose(¤t, rng);
let d = problem.delta(¤t, &mv).abs();
if d > 0.0 {
sum_abs += d;
min_abs = min_abs.min(d);
count += 1;
}
problem.apply(&mut current, mv); }
if count == 0 {
return fallback;
}
let mean = sum_abs / count as f64;
let t_start = mean / (-P_START.ln());
let t_end = min_abs / (-P_END.ln());
if t_end > 0.0 && t_end < t_start {
(t_start, t_end)
} else {
fallback
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Partition {
weights: Vec<f64>,
}
impl Partition {
fn diff(&self, x: &[bool]) -> f64 {
let mut d = 0.0;
for (w, &b) in self.weights.iter().zip(x) {
d += if b { *w } else { -*w };
}
d
}
}
impl Anneal for Partition {
type State = Vec<bool>;
type Move = usize;
fn initial(&self, _rng: &mut Rng) -> Vec<bool> {
vec![false; self.weights.len()]
}
fn energy(&self, x: &Vec<bool>) -> f64 {
self.diff(x).powi(2)
}
fn propose(&self, x: &Vec<bool>, rng: &mut Rng) -> usize {
rng.index(x.len())
}
fn delta(&self, x: &Vec<bool>, &i: &usize) -> f64 {
let d = self.diff(x);
let step = if x[i] {
-2.0 * self.weights[i]
} else {
2.0 * self.weights[i]
};
(d + step).powi(2) - d.powi(2)
}
fn apply(&self, x: &mut Vec<bool>, i: usize) {
x[i] = !x[i];
}
}
#[test]
fn finds_perfect_partition() {
let p = Partition {
weights: vec![8.0, 7.0, 6.0, 5.0, 4.0],
};
let sa = Sa {
iterations: 4000,
restarts: 8,
schedule: Schedule::adaptive(),
seed: 1,
};
let res = sa.optimize(&p);
assert!(res.energy < 1e-9, "energy {}", res.energy);
assert!((p.diff(&res.best)).abs() < 1e-9);
}
#[test]
fn incremental_delta_matches_full_energy() {
let p = Partition {
weights: vec![3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0],
};
let mut rng = Rng::new(42);
let mut x = p.initial(&mut rng);
for _ in 0..500 {
let mv = p.propose(&x, &mut rng);
let predicted = p.delta(&x, &mv);
let before = p.energy(&x);
p.apply(&mut x, mv);
let after = p.energy(&x);
assert!((predicted - (after - before)).abs() < 1e-9);
}
}
#[test]
fn is_deterministic() {
let p = Partition {
weights: vec![5.0, 3.0, 9.0, 7.0, 1.0, 8.0],
};
let sa = Sa {
iterations: 2000,
schedule: Schedule::Geometric {
t_start: 10.0,
t_end: 0.01,
},
seed: 7,
restarts: 4,
};
let a = sa.optimize(&p);
let b = sa.optimize(&p);
assert_eq!(a.best, b.best);
assert_eq!(a.energy, b.energy);
}
}