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);
-(1.0 / (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 {
#[test]
fn the_trotter_mapping_converges_to_the_quantum_model_it_claims_to_represent() {
fn ring_mean(h: f64, beta: f64, m: usize, j_perp: f64) -> f64 {
let b = h / m as f64;
let sp = [1.0f64, -1.0];
let mut tm = [[0.0f64; 2]; 2];
for i in 0..2 {
for j in 0..2 {
tm[i][j] = (beta * (j_perp * sp[i] * sp[j] + b * (sp[i] + sp[j]) / 2.0)).exp();
}
}
let mut acc = [[1.0f64, 0.0], [0.0, 1.0]];
let mut base = tm;
let mut k = m;
while k > 0 {
if k & 1 == 1 {
acc = mul_norm(acc, base);
}
base = mul_norm(base, base);
k >>= 1;
}
let z = acc[0][0] + acc[1][1];
(sp[0] * acc[0][0] + sp[1] * acc[1][1]) / z
}
fn mul_norm(a: [[f64; 2]; 2], b: [[f64; 2]; 2]) -> [[f64; 2]; 2] {
let mut c = [[0.0f64; 2]; 2];
for i in 0..2 {
for j in 0..2 {
c[i][j] = a[i][0] * b[0][j] + a[i][1] * b[1][j];
}
}
let s = c.iter().flatten().fold(0.0f64, |m, &x| m.max(x.abs())).max(1e-300);
for row in c.iter_mut() {
for v in row.iter_mut() {
*v /= s;
}
}
c
}
fn j_perp(beta: f64, gamma: f64, m: usize) -> f64 {
let x = (beta * gamma / m as f64).tanh().max(1e-300);
-(1.0 / (2.0 * beta)) * x.ln()
}
for &(h, gamma, beta) in &[(0.5f64, 1.0f64, 1.0f64), (1.0, 2.0, 1.0), (0.5, 0.5, 2.0)] {
let e = (h * h + gamma * gamma).sqrt();
let quantum = (h / e) * (beta * e).tanh();
let classical = (beta * h).tanh();
assert!(
(quantum - classical).abs() > 0.05,
"this case cannot tell the two apart: quantum {quantum:.4} classical {classical:.4}"
);
let mut prev = f64::INFINITY;
for &m in &[8usize, 32, 128, 512] {
let got = ring_mean(h, beta, m, j_perp(beta, gamma, m));
let err = (got - quantum).abs();
assert!(
err < prev + 1e-12,
"error must not grow with M: M={m} gave {got:.6} (err {err:.2e}), \
previous error {prev:.2e}"
);
prev = err;
}
assert!(
prev < 2e-4,
"M=512 must reach the quantum answer {quantum:.6}, and the error was {prev:.2e}"
);
}
}
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);
}
}