use crate::graph::Graph;
use crate::rng::Pcg;
#[derive(Clone, Debug, PartialEq)]
pub struct Params {
pub population: usize,
pub sweeps: usize,
pub betas: Vec<f64>,
}
impl Params {
pub fn linear_from_zero(population: usize, sweeps: usize, beta_max: f64, stages: usize) -> Params {
let stages = stages.max(1);
let betas = (0..=stages).map(|i| beta_max * i as f64 / stages as f64).collect();
Params { population, sweeps, betas }
}
}
#[derive(Clone, Debug)]
pub struct Outcome {
pub state: Vec<i8>,
pub energy: f64,
pub ln_z: f64,
pub ln_z_is_absolute: bool,
pub rho: Vec<f64>,
pub rho_max: f64,
pub sizes: Vec<usize>,
}
impl Outcome {
pub fn free_energy_per_spin(&self, beta_end: f64, n: usize) -> Option<f64> {
(self.ln_z_is_absolute && beta_end > 0.0 && n > 0)
.then(|| -self.ln_z / (beta_end * n as f64))
}
}
pub fn run(g: &Graph, p: &Params, seed: u64) -> Outcome {
let n = g.n;
let r_target = p.population.max(1);
if n == 0 || p.betas.is_empty() {
return Outcome {
state: vec![0i8; n],
energy: 0.0,
ln_z: 0.0,
ln_z_is_absolute: false,
rho: Vec::new(),
rho_max: 1.0,
sizes: Vec::new(),
};
}
let mut rng = Pcg::new(seed, 0x9A_11E4);
let mut smp = crate::gibbs::Sampler::new(g, p.betas[0], seed ^ 0x9E37_79B9);
let mut pop: Vec<Vec<i8>> = Vec::with_capacity(r_target);
let mut fam: Vec<u32> = Vec::with_capacity(r_target);
for i in 0..r_target {
pop.push((0..n).map(|_| rng.spin(0.5)).collect());
fam.push(i as u32);
}
let mut energy: Vec<f64> = pop.iter().map(|s| g.energy(s)).collect();
let mut best_i = 0usize;
for i in 1..energy.len() {
if energy[i] < energy[best_i] {
best_i = i;
}
}
let mut best_state = pop[best_i].clone();
let mut best_energy = energy[best_i];
let absolute = p.betas[0] == 0.0;
let mut ln_z = if absolute { n as f64 * core::f64::consts::LN_2 } else { 0.0 };
smp.beta = p.betas[0];
for i in 0..pop.len() {
smp.s.copy_from_slice(&pop[i]);
smp.sweeps(p.sweeps, None);
pop[i].copy_from_slice(&smp.s);
energy[i] = g.energy(&pop[i]);
if energy[i] < best_energy {
best_energy = energy[i];
best_state.copy_from_slice(&pop[i]);
}
}
let mut rho = Vec::with_capacity(p.betas.len());
let mut sizes = Vec::with_capacity(p.betas.len());
for k in 1..p.betas.len() {
let d_beta = p.betas[k] - p.betas[k - 1];
let r_now = pop.len();
if r_now == 0 {
break;
}
let x: Vec<f64> = energy.iter().map(|e| -d_beta * e).collect();
let m = x.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
if !m.is_finite() {
break;
}
let ex: Vec<f64> = x.iter().map(|v| (v - m).exp()).collect();
let sum_ex: f64 = ex.iter().sum();
if !(sum_ex > 0.0) || !sum_ex.is_finite() {
break;
}
ln_z += m + (sum_ex / r_now as f64).ln();
let scale = r_target as f64 / sum_ex;
let mut next: Vec<Vec<i8>> = Vec::with_capacity(r_target);
let mut next_fam: Vec<u32> = Vec::with_capacity(r_target);
let mut next_e: Vec<f64> = Vec::with_capacity(r_target);
for i in 0..r_now {
let tau = ex[i] * scale;
let mut copies = tau.floor();
if rng.f64() < tau - copies {
copies += 1.0;
}
let copies = (copies as usize).min(r_target * 4);
for _ in 0..copies {
next.push(pop[i].clone());
next_fam.push(fam[i]);
next_e.push(energy[i]);
}
}
if next.is_empty() {
let mut bi = 0usize;
for i in 1..r_now {
if energy[i] < energy[bi] {
bi = i;
}
}
next.push(pop[bi].clone());
next_fam.push(fam[bi]);
next_e.push(energy[bi]);
}
pop = next;
fam = next_fam;
energy = next_e;
let mut counts = vec![0u32; r_target];
for &f in &fam {
let idx = f as usize;
if idx < counts.len() {
counts[idx] += 1;
}
}
let sq: f64 = counts.iter().map(|&c| (c as f64) * (c as f64)).sum();
rho.push(sq / pop.len() as f64);
sizes.push(pop.len());
smp.beta = p.betas[k];
for i in 0..pop.len() {
smp.s.copy_from_slice(&pop[i]);
smp.sweeps(p.sweeps, None);
pop[i].copy_from_slice(&smp.s);
energy[i] = g.energy(&pop[i]);
if energy[i] < best_energy {
best_energy = energy[i];
best_state.copy_from_slice(&pop[i]);
}
}
}
let rho_max = rho.iter().cloned().fold(1.0f64, f64::max);
let energy = g.energy(&best_state);
Outcome { state: best_state, energy, ln_z, ln_z_is_absolute: absolute, rho, rho_max, sizes }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
fn random_graph(n: usize, p: f64, seed: u64, fields: bool) -> Graph {
let mut rng = Pcg::new(seed, 0xC0FFEE);
let mut gb = GraphBuilder::new(n);
for i in 0..n {
if fields {
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()
}
fn brute(g: &Graph, beta: f64) -> (f64, f64) {
let n = g.n;
let mut s = vec![1i8; n];
let mut min = f64::INFINITY;
let mut xs = Vec::with_capacity(1usize << n);
for mask in 0..(1u64 << n) {
for i in 0..n {
s[i] = if mask >> i & 1 == 1 { 1 } else { -1 };
}
let e = g.energy(&s);
min = min.min(e);
xs.push(-beta * e);
}
let m = xs.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let ln_z = m + xs.iter().map(|x| (x - m).exp()).sum::<f64>().ln();
(ln_z, min)
}
#[test]
fn a_flat_landscape_is_reproduced_exactly() {
let g = GraphBuilder::new(9).build();
let p = Params::linear_from_zero(64, 2, 4.0, 12);
let o = run(&g, &p, 5);
assert_eq!(o.ln_z, 9.0 * core::f64::consts::LN_2, "ln Z must be exactly n ln 2");
assert!(o.ln_z_is_absolute);
assert_eq!(o.rho, vec![1.0; 12], "no replica may be copied when all weights are equal");
assert_eq!(o.sizes, vec![64; 12], "the population is preserved exactly");
assert_eq!(o.energy, 0.0);
}
#[test]
fn ln_z_matches_exact_enumeration_on_a_small_graph() {
for seed in 0..3u64 {
let g = random_graph(8, 0.5, seed, seed % 2 == 0);
let beta_end = 1.5;
let (exact, _) = brute(&g, beta_end);
let p = Params::linear_from_zero(3000, 4, beta_end, 30);
let o = run(&g, &p, 100 + seed);
let err = (o.ln_z - exact).abs();
assert!(
err < 0.15,
"seed {seed}: ln Z estimate {:.4} vs exact {exact:.4} (err {err:.4}), rho_max {:.1}",
o.ln_z,
o.rho_max
);
}
}
#[test]
fn a_reweighting_factor_that_would_overflow_does_not_end_the_run() {
let n = 200;
let mut gb = GraphBuilder::new(n);
for i in 0..n {
gb.couple(i, (i + 1) % n, 100.0);
gb.couple(i, (i + 7) % n, -100.0);
}
let g = gb.build();
let stages = 20;
let p = Params::linear_from_zero(64, 1, 1.0, stages);
let o = run(&g, &p, 3);
assert_eq!(o.rho.len(), stages, "the ladder stopped early: overflow was not handled");
assert!(o.ln_z.is_finite(), "ln Z = {}", o.ln_z);
assert!(o.energy.is_finite() && o.energy < 0.0);
}
#[test]
fn rho_reports_a_collapsed_population() {
let g = random_graph(30, 0.3, 11, false);
let r = 128;
let p = Params { population: r, sweeps: 1, betas: vec![0.0, 40.0] };
let o = run(&g, &p, 9);
assert_eq!(o.rho.len(), 1);
assert!(
o.rho_max > 0.5 * r as f64,
"rho {:.1} on a one-step quench of {r} replicas -- expected near-total collapse",
o.rho_max
);
let gentle = Params::linear_from_zero(r, 4, 4.0, 60);
let o2 = run(&g, &gentle, 9);
assert!(o2.rho_max < 0.25 * r as f64, "gentle ladder rho_max {:.1}", o2.rho_max);
}
#[test]
fn it_reaches_the_true_minimum_on_an_enumerable_graph() {
for seed in 0..4u64 {
let g = random_graph(14, 0.45, seed, true);
let (_, min) = brute(&g, 1.0);
let p = Params::linear_from_zero(400, 4, 6.0, 40);
let o = run(&g, &p, 200 + seed);
assert!(
o.energy <= min + 1e-9,
"seed {seed}: found {:.6}, true minimum {min:.6}",
o.energy
);
assert_eq!(o.energy, g.energy(&o.state), "energy must match the state returned");
}
}
#[test]
fn a_ladder_that_skips_infinite_temperature_reports_a_relative_free_energy() {
let g = random_graph(8, 0.5, 4, false);
let p = Params { population: 200, sweeps: 2, betas: vec![0.2, 0.6, 1.0] };
let o = run(&g, &p, 4);
assert!(!o.ln_z_is_absolute);
assert!(o.free_energy_per_spin(1.0, g.n).is_none(), "a ratio is not a free energy");
assert!(o.ln_z > 0.0, "ln(Z(1.0)/Z(0.2)) = {}", o.ln_z);
}
#[test]
fn an_empty_graph_or_ladder_returns() {
let g = GraphBuilder::new(0).build();
let o = run(&g, &Params::linear_from_zero(10, 1, 1.0, 3), 1);
assert!(o.state.is_empty() && o.rho.is_empty());
let g2 = random_graph(6, 0.5, 1, false);
let o2 = run(&g2, &Params { population: 10, sweeps: 1, betas: Vec::new() }, 1);
assert!(o2.rho.is_empty() && !o2.ln_z_is_absolute);
}
}