use crate::graph::Graph;
use crate::ledger::Ledger;
use crate::rng::Pcg;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Params {
pub trotter: usize,
pub beta: f64,
pub gamma_max: f64,
pub gamma_min: f64,
pub steps: usize,
pub sweeps_per_step: usize,
}
impl Default for Params {
fn default() -> Self {
Params {
trotter: 4,
beta: 10.0,
gamma_max: 3.0,
gamma_min: 0.05,
steps: 200,
sweeps_per_step: 1,
}
}
}
#[derive(Clone, Debug)]
pub struct Outcome {
pub state: Vec<i8>,
pub energy: f64,
pub proposals: u64,
pub accepted: u64,
pub max_j_perp: f64,
}
pub fn run(g: &Graph, p: &Params, seed: u64) -> Outcome {
run_metered(g, p, seed, None)
}
pub fn run_metered(g: &Graph, p: &Params, seed: u64, mut ledger: Option<&mut Ledger>) -> Outcome {
let n = g.n;
let m = p.trotter.max(1);
if n == 0 {
return Outcome { state: Vec::new(), energy: 0.0, proposals: 0, accepted: 0, max_j_perp: 0.0 };
}
let mut rng = Pcg::new(seed, 0x0005_9A11);
let mut s: Vec<i8> = (0..m * n).map(|_| rng.spin(0.5)).collect();
let mut best: Vec<i8> = s[..n].to_vec();
let mut best_e = g.energy(&best);
let (mut proposals, mut accepted) = (0u64, 0u64);
let mut max_j_perp = 0.0f64;
let steps = p.steps.max(1);
let (gmax, gmin) = (p.gamma_max.max(1e-9), p.gamma_min.max(1e-9));
for step in 0..steps {
let f = if steps == 1 { 1.0 } else { step as f64 / (steps - 1) as f64 };
let gamma = gmax * (gmin / gmax).powf(f);
let j_perp = if m == 1 {
0.0
} else {
let x = (p.beta * gamma / m as f64).tanh().max(1e-300);
-(m as f64 / (2.0 * p.beta)) * x.ln()
};
max_j_perp = max_j_perp.max(j_perp);
for _ in 0..p.sweeps_per_step.max(1) {
for k in 0..m {
for i in 0..n {
let cur = s[k * n + i];
let mut field = g.h[i];
for e in g.offset[i]..g.offset[i + 1] {
field += g.w[e] * s[k * n + g.nbr[e] as usize] as f64;
}
let mut d = 2.0 * cur as f64 * field / m as f64;
if m > 1 {
let up = s[((k + m - 1) % m) * n + i] as f64;
let down = s[((k + 1) % m) * n + i] as f64;
d += 2.0 * cur as f64 * j_perp * (up + down);
}
proposals += 1;
if d <= 0.0 || rng.f64() < (-p.beta * d).exp() {
s[k * n + i] = -cur;
accepted += 1;
}
}
}
if let Some(l) = ledger.as_deref_mut() {
l.samples += (m * n) as u64;
}
}
for k in 0..m {
let e = g.energy(&s[k * n..(k + 1) * n]);
if e < best_e {
best_e = e;
best.copy_from_slice(&s[k * n..(k + 1) * n]);
}
}
}
let energy = g.energy(&best);
Outcome { state: best, energy, proposals, accepted, max_j_perp }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
use crate::ising::lattice2d;
fn glass(l: usize, seed: u64) -> Graph {
let mut rng = Pcg::new(seed, 0x0005_9AC0);
let mut gb = GraphBuilder::new(l * l);
for y in 0..l {
for x in 0..l {
let i = y * l + x;
gb.couple(i, y * l + (x + 1) % l, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
gb.couple(i, ((y + 1) % l) * l + x, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
}
}
gb.build()
}
#[test]
fn the_energy_returned_is_the_energy_of_the_state_returned() {
for seed in 0..8u64 {
let g = glass(6, seed);
let out = run(&g, &Params::default(), seed);
assert_eq!(out.state.len(), g.n);
assert!(out.state.iter().all(|&v| v == 1 || v == -1));
assert!((out.energy - g.energy(&out.state)).abs() < 1e-9);
assert_eq!(out.proposals, (Params::default().trotter * g.n * Params::default().steps) as u64);
assert!(out.accepted <= out.proposals);
}
}
#[test]
fn it_reaches_a_known_ground_state() {
for l in [4usize, 6, 8] {
let g = lattice2d(l, 1.0);
let out = run(&g, &Params::default(), 3);
let bonds = 2.0 * (l * l) as f64;
assert!(
(out.energy + bonds).abs() < 1e-9,
"{l}x{l}: reached {} against a ground energy of {}",
out.energy,
-bonds
);
}
}
#[test]
fn more_trotter_slices_are_not_worse_on_a_frustrated_glass() {
let (mut wins, mut losses) = (0, 0);
for seed in 0..24u64 {
let g = glass(10, seed);
let classical = Params { trotter: 1, sweeps_per_step: 4, ..Params::default() };
let quantum = Params { trotter: 4, sweeps_per_step: 1, ..Params::default() };
let c = run(&g, &classical, seed);
let q = run(&g, &quantum, seed);
assert_eq!(c.proposals, q.proposals, "the arms must do equal work");
if q.energy < c.energy - 1e-9 {
wins += 1;
} else if q.energy > c.energy + 1e-9 {
losses += 1;
}
}
assert!(
wins >= losses,
"with four Trotter slices the anneal won {wins} and lost {losses} of 24 against the \
same code at one slice and equal work"
);
}
#[test]
fn the_transverse_field_stops_short_of_zero() {
let g = glass(5, 1);
let out = run(&g, &Params::default(), 1);
assert!(out.max_j_perp.is_finite(), "J_perp went to infinity: gamma reached zero");
assert!(out.max_j_perp > 0.0);
let zeroed = run(&g, &Params { gamma_min: 0.0, ..Params::default() }, 1);
assert!(zeroed.max_j_perp.is_finite() && zeroed.energy.is_finite());
let one = run(&g, &Params { trotter: 1, ..Params::default() }, 1);
assert_eq!(one.max_j_perp, 0.0);
}
#[test]
fn an_empty_graph_returns_rather_than_panicking() {
let out = run(&GraphBuilder::new(0).build(), &Params::default(), 1);
assert!(out.state.is_empty() && out.energy == 0.0 && out.proposals == 0);
}
}