use crate::model::IsingModel;
use crate::parallel::{best_of, run_replicas};
use crate::result::SolveResult;
use crate::rng::Rng;
use num_traits::Float;
#[derive(Debug, Clone)]
pub struct SaConfig {
pub sweeps: usize,
pub restarts: usize,
pub t_initial: Option<f64>,
pub t_final: Option<f64>,
pub seed: u64,
}
impl Default for SaConfig {
fn default() -> Self {
Self {
sweeps: 1000,
restarts: 8,
t_initial: None,
t_final: None,
seed: 0,
}
}
}
pub(crate) fn temp_scale<T: Float>(model: &IsingModel<T>) -> f64 {
let n = model.n();
let sigma = model.coupling_rms().to_f64().unwrap_or(0.0);
let h_max = model.field_max_abs().to_f64().unwrap_or(0.0);
let scale = sigma * (n as f64).sqrt() + h_max;
if scale > 0.0 {
scale
} else {
1.0
}
}
pub(crate) fn metropolis_sweep<T: Float>(
model: &IsingModel<T>,
spins: &mut [i8],
fields: &mut [T],
energy: &mut T,
temp: f64,
rng: &mut Rng,
) {
let n = model.n();
let two = T::from(2.0).unwrap();
for i in 0..n {
let s = T::from(spins[i]).unwrap();
let delta = two * s * fields[i];
let accept = delta <= T::zero() || {
let d = delta.to_f64().unwrap();
rng.uniform() < (-d / temp).exp()
};
if accept {
spins[i] = -spins[i];
*energy = *energy + delta;
let s_new = T::from(spins[i]).unwrap();
let row = model.j_row(i);
for (f, &jv) in fields.iter_mut().zip(row.iter()) {
*f = *f + two * jv * s_new;
}
}
}
}
pub(crate) fn random_spins(n: usize, rng: &mut Rng) -> Vec<i8> {
(0..n)
.map(|_| if rng.below(2) == 0 { -1 } else { 1 })
.collect()
}
pub fn solve<T: Float + Send + Sync>(model: &IsingModel<T>, cfg: &SaConfig) -> SolveResult<T> {
assert!(cfg.sweeps > 0, "SaConfig.sweeps must be positive");
assert!(cfg.restarts > 0, "SaConfig.restarts must be positive");
let n = model.n();
if n == 0 {
return SolveResult {
spins: Vec::new(),
energy: T::zero(),
steps: 0,
seed: cfg.seed,
replica: 0,
};
}
let t0 = cfg.t_initial.unwrap_or_else(|| 2.0 * temp_scale(model));
let t1 = cfg.t_final.unwrap_or(t0 / 1000.0).min(t0);
assert!(t0 > 0.0 && t1 > 0.0, "temperatures must be positive");
let ratio = t1 / t0;
let results = run_replicas(cfg.restarts, |r| {
let mut rng = Rng::for_replica(cfg.seed, r as u64);
let mut spins = random_spins(n, &mut rng);
let mut fields = model.local_fields(&spins);
let mut energy = model.energy(&spins);
let mut best_energy = energy;
let mut best_spins = spins.clone();
for k in 0..cfg.sweeps {
let frac = if cfg.sweeps > 1 {
k as f64 / (cfg.sweeps - 1) as f64
} else {
1.0
};
let temp = t0 * ratio.powf(frac);
metropolis_sweep(model, &mut spins, &mut fields, &mut energy, temp, &mut rng);
if energy < best_energy {
best_energy = energy;
best_spins.copy_from_slice(&spins);
}
}
let energy = model.energy(&best_spins);
SolveResult {
spins: best_spins,
energy,
steps: cfg.sweeps as u64,
seed: cfg.seed,
replica: r,
}
});
best_of(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn solves_two_spin_ferromagnet() {
let mut m = IsingModel::<f64>::new(2);
m.set_coupling(0, 1, 1.0);
let r = solve(&m, &SaConfig { seed: 5, ..SaConfig::default() });
assert_eq!(r.energy, -1.0);
}
#[test]
fn incremental_energy_stays_consistent() {
let mut m = IsingModel::<f64>::new(6);
for i in 0..6 {
m.set_field(i, (i as f64) - 2.5);
for j in (i + 1)..6 {
m.set_coupling(i, j, ((i + 2 * j) % 3) as f64 - 1.0);
}
}
let mut rng = Rng::new(11);
let mut spins = random_spins(6, &mut rng);
let mut fields = m.local_fields(&spins);
let mut energy = m.energy(&spins);
for _ in 0..50 {
metropolis_sweep(&m, &mut spins, &mut fields, &mut energy, 1.5, &mut rng);
}
assert!((energy - m.energy(&spins)).abs() < 1e-9);
let expect_fields = m.local_fields(&spins);
for (a, b) in fields.iter().zip(expect_fields.iter()) {
assert!((*a - *b).abs() < 1e-9);
}
}
}