use crate::repro::exp;
#[derive(Clone, Debug, PartialEq)]
pub struct Ising {
n: usize,
start: Vec<usize>,
col: Vec<u32>,
val: Vec<f64>,
h: Vec<f64>,
}
#[derive(Clone, Debug, PartialEq)]
pub enum QioError {
BadCoupling { i: usize, j: usize },
Parse { line: usize, message: String },
TooLarge(usize),
}
impl core::fmt::Display for QioError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
QioError::BadCoupling { i, j } => write!(f, "coupling ({i}, {j}) is out of range or on the diagonal"),
QioError::Parse { line, message } => write!(f, "line {line}: {message}"),
QioError::TooLarge(n) => write!(f, "{n} spins is too many to enumerate (at most 34)"),
}
}
}
impl std::error::Error for QioError {}
impl Ising {
pub fn new(n: usize, couplings: &[(usize, usize, f64)], h: &[f64]) -> Result<Ising, QioError> {
let mut rows: Vec<Vec<(u32, f64)>> = vec![Vec::new(); n];
for &(i, j, v) in couplings {
if i >= n || j >= n || i == j {
return Err(QioError::BadCoupling { i, j });
}
rows[i].push((j as u32, v));
rows[j].push((i as u32, v));
}
let mut start = vec![0];
let (mut col, mut val) = (Vec::new(), Vec::new());
for r in rows.iter_mut() {
r.sort_by_key(|e| e.0);
let mut k = 0;
while k < r.len() {
let (c, mut v) = r[k];
k += 1;
while k < r.len() && r[k].0 == c {
v += r[k].1;
k += 1;
}
col.push(c);
val.push(v);
}
start.push(col.len());
}
let mut hv = vec![0.0; n];
hv[..h.len().min(n)].copy_from_slice(&h[..h.len().min(n)]);
Ok(Ising { n, start, col, val, h: hv })
}
pub fn sk(n: usize, seed: u64) -> Ising {
let mut rng = Rng(seed);
let mut c = Vec::with_capacity(n * n.saturating_sub(1) / 2);
for i in 0..n {
for j in i + 1..n {
c.push((i, j, if rng.next() >> 63 == 0 { 1.0 } else { -1.0 }));
}
}
Ising::new(n, &c, &[]).expect("in-range couplings")
}
pub fn n(&self) -> usize {
self.n
}
pub fn row(&self, i: usize) -> impl Iterator<Item = (usize, f64)> + '_ {
(self.start[i]..self.start[i + 1]).map(|k| (self.col[k] as usize, self.val[k]))
}
fn has_fields(&self) -> bool {
self.h.iter().any(|&x| x != 0.0)
}
pub fn energy(&self, s: &[i8]) -> f64 {
let mut e = 0.0;
for i in 0..self.n {
let mut field = 0.0;
for (j, v) in self.row(i) {
field += v * f64::from(s[j]);
}
e -= f64::from(s[i]) * (0.5 * field + self.h[i]);
}
e
}
pub fn rms_coupling(&self) -> f64 {
if self.n < 2 {
return 0.0;
}
let sum: f64 = self.val.iter().map(|v| v * v).sum();
(sum / (self.n as f64 * (self.n - 1) as f64)).sqrt()
}
fn without_fields(&self) -> Ising {
let mut c = Vec::new();
for i in 0..self.n {
for (j, v) in self.row(i) {
if i < j {
c.push((i, j, v));
}
}
if self.h[i] != 0.0 {
c.push((i, self.n, self.h[i]));
}
}
Ising::new(self.n + 1, &c, &[]).expect("in-range couplings")
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MaxCut {
pub n: usize,
pub edges: Vec<(usize, usize, f64)>,
}
impl MaxCut {
pub fn ising(&self) -> Ising {
let c: Vec<(usize, usize, f64)> = self.edges.iter().map(|&(i, j, w)| (i, j, -w)).collect();
Ising::new(self.n, &c, &[]).expect("a graph's edges are in range")
}
pub fn cut(&self, s: &[i8]) -> f64 {
self.edges.iter().filter(|&&(i, j, _)| s[i] != s[j]).map(|e| e.2).sum()
}
}
pub fn read_gset(text: &str) -> Result<MaxCut, QioError> {
let mut lines = text.lines().enumerate().map(|(k, l)| (k + 1, l.trim())).filter(|(_, l)| !l.is_empty());
let err = |line: usize, m: &str| QioError::Parse { line, message: m.into() };
let (hl, header) = lines.next().ok_or_else(|| err(1, "empty"))?;
let mut h = header.split_whitespace().map(|x| x.parse::<usize>());
let (n, m) = match (h.next(), h.next()) {
(Some(Ok(n)), Some(Ok(m))) => (n, m),
_ => return Err(err(hl, "expected 'n m'")),
};
let mut edges = Vec::with_capacity(m);
for (line, l) in lines {
let f: Vec<&str> = l.split_whitespace().collect();
if f.len() < 3 {
return Err(err(line, "expected 'i j w'"));
}
let i: usize = f[0].parse().map_err(|_| err(line, "bad vertex"))?;
let j: usize = f[1].parse().map_err(|_| err(line, "bad vertex"))?;
let w: f64 = f[2].parse().map_err(|_| err(line, "bad weight"))?;
if i == 0 || j == 0 || i > n || j > n || i == j {
return Err(QioError::BadCoupling { i, j });
}
edges.push((i - 1, j - 1, w));
}
if edges.len() != m {
return Err(err(hl, &format!("header says {m} edges, found {}", edges.len())));
}
Ok(MaxCut { n, edges })
}
#[derive(Clone)]
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
fn unit(&mut self) -> f64 {
(self.next() >> 11) as f64 * (1.0 / 9_007_199_254_740_992.0)
}
fn symmetric(&mut self) -> f64 {
2.0 * self.unit() - 1.0
}
#[cfg(test)]
fn below(&mut self, n: u64) -> u64 {
self.next() % n
}
}
fn trial_rng(seed: u64, t: usize) -> Rng {
let mut r = Rng(seed ^ (t as u64).wrapping_mul(0xd1b5_4a32_d192_ed03));
r.next();
r
}
#[cfg(test)]
fn spins(x: &[f64]) -> Vec<i8> {
x.iter().map(|&v| if v >= 0.0 { 1 } else { -1 }).collect()
}
fn run_trials<T: Send>(trials: usize, threads: usize, f: impl Fn(usize) -> T + Sync) -> Vec<T> {
let threads = if cfg!(target_arch = "wasm32") { 1 } else { threads.max(1).min(trials.max(1)) };
if threads == 1 {
return (0..trials).map(f).collect();
}
let next = std::sync::atomic::AtomicUsize::new(0);
let slots: Vec<std::sync::Mutex<Option<T>>> = (0..trials).map(|_| std::sync::Mutex::new(None)).collect();
std::thread::scope(|scope| {
for _ in 0..threads {
scope.spawn(|| loop {
let t = next.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if t >= trials {
break;
}
let r = f(t);
*slots[t].lock().unwrap() = Some(r);
});
}
});
slots.into_iter().map(|m| m.into_inner().unwrap().expect("every trial ran")).collect()
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Variant {
Ballistic,
Discrete,
HeatedBallistic(f64),
HeatedDiscrete(f64),
}
impl Variant {
fn discrete(&self) -> bool {
matches!(self, Variant::Discrete | Variant::HeatedDiscrete(_))
}
fn gamma(&self) -> f64 {
match *self {
Variant::HeatedBallistic(g) | Variant::HeatedDiscrete(g) => g,
_ => 0.0,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SbConfig {
pub variant: Variant,
pub steps: u32,
pub dt: f64,
pub c1: f64,
pub a0: f64,
pub sample_every: u32,
pub trials: usize,
pub seed: u64,
}
impl SbConfig {
pub fn published(variant: Variant, steps: u32, trials: usize, seed: u64) -> SbConfig {
let (variant, dt, c1) = match variant {
Variant::Ballistic => (variant, 0.7, 0.6),
Variant::Discrete => (variant, 1.1, 0.6),
Variant::HeatedBallistic(_) => (Variant::HeatedBallistic(0.5), 1.1, 0.9),
Variant::HeatedDiscrete(_) => (Variant::HeatedDiscrete(0.06), 1.1, 0.7),
};
SbConfig { variant, steps, dt, c1, a0: 1.0, sample_every: 100, trials, seed }
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Trial {
pub energy: f64,
pub spins: Vec<i8>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Run {
pub trials: Vec<Trial>,
}
impl Run {
pub fn best(&self) -> &Trial {
self.trials.iter().fold(&self.trials[0], |b, t| if t.energy < b.energy { t } else { b })
}
pub fn success(&self, energy: f64) -> f64 {
self.trials.iter().filter(|t| t.energy <= energy + 1e-9).count() as f64 / self.trials.len() as f64
}
}
const LANES: usize = 8;
pub fn simulated_bifurcation(problem: &Ising, cfg: &SbConfig, threads: usize) -> Run {
sb_with_state(problem, cfg, threads).0
}
fn sb_with_state(problem: &Ising, cfg: &SbConfig, threads: usize) -> (Run, Vec<Vec<f64>>) {
let fields = problem.has_fields();
let p = if fields { problem.without_fields() } else { problem.clone() };
let n = p.n;
let sigma = p.rms_coupling();
let c0 = if sigma > 0.0 { cfg.c1 / (sigma * (n as f64).sqrt()) } else { 0.0 };
let (dt, a0, gamma, discrete) = (cfg.dt, cfg.a0, cfg.variant.gamma(), cfg.variant.discrete());
const L: usize = LANES;
let blocks = run_trials(cfg.trials.div_ceil(L), threads, |b| {
let mut x = vec![0.0; n * L];
let mut y = vec![0.0; n * L];
for l in 0..L {
let mut rng = trial_rng(cfg.seed, b * L + l);
for i in 0..n {
x[i * L + l] = rng.symmetric();
}
for i in 0..n {
y[i * L + l] = rng.symmetric();
}
}
let mut f = vec![0.0; n * L];
let mut src = vec![0.0; n * L];
let mut best: Vec<Trial> = (0..L).map(|_| Trial { energy: f64::INFINITY, spins: Vec::new() }).collect();
let consider = |x: &[f64], best: &mut Vec<Trial>| {
for (l, b) in best.iter_mut().enumerate() {
let s: Vec<i8> = (0..n).map(|i| if x[i * L + l] >= 0.0 { 1 } else { -1 }).collect();
let e = p.energy(&s);
if e < b.energy {
*b = Trial { energy: e, spins: s };
}
}
};
for k in 0..cfg.steps {
let a = a0 * f64::from(k) / f64::from(cfg.steps);
for (s, &v) in src.iter_mut().zip(&x) {
*s = if discrete { if v >= 0.0 { 1.0 } else { -1.0 } } else { v };
}
for i in 0..n {
let mut acc = [0.0f64; L];
for kk in p.start[i]..p.start[i + 1] {
let (v, c) = (p.val[kk], p.col[kk] as usize * L);
let row = &src[c..c + L];
for l in 0..L {
acc[l] += v * row[l];
}
}
f[i * L..i * L + L].copy_from_slice(&acc);
}
for ((xi, yi), &fi) in x.iter_mut().zip(y.iter_mut()).zip(&f) {
let y0 = *yi;
let mut yn = y0 + (-(a0 - a) * *xi + c0 * fi) * dt;
let mut xn = *xi + a0 * yn * dt;
if xn.abs() > 1.0 {
xn = if xn > 0.0 { 1.0 } else { -1.0 };
yn = 0.0;
}
*xi = xn;
*yi = yn + gamma * y0 * dt;
}
if cfg.sample_every > 0 && (k + 1) % cfg.sample_every == 0 {
consider(&x, &mut best);
}
}
consider(&x, &mut best);
let finals: Vec<Vec<f64>> = (0..L).map(|l| (0..n).map(|i| x[i * L + l]).collect()).collect();
best.into_iter().zip(finals).collect::<Vec<_>>()
});
let (trials, finals): (Vec<Trial>, Vec<Vec<f64>>) = blocks.into_iter().flatten().take(cfg.trials).unzip();
let trials = trials
.into_iter()
.map(|mut t| {
if fields {
let g = t.spins[n - 1];
t.spins.truncate(n - 1);
if g < 0 {
t.spins.iter_mut().for_each(|s| *s = -*s);
}
t.energy = problem.energy(&t.spins);
}
t
})
.collect();
(Run { trials }, finals)
}
pub fn descend(problem: &Ising, spins: &[i8]) -> Trial {
let n = problem.n;
let mut s = spins.to_vec();
let mut field: Vec<f64> = (0..n).map(|i| problem.row(i).map(|(j, v)| v * f64::from(s[j])).sum::<f64>() + problem.h[i]).collect();
loop {
let (mut best_i, mut best_de) = (usize::MAX, -1e-12);
for i in 0..n {
let de = 2.0 * f64::from(s[i]) * field[i];
if de < best_de {
best_de = de;
best_i = i;
}
}
if best_i == usize::MAX {
break;
}
let si = f64::from(s[best_i]);
s[best_i] = -s[best_i];
for (j, v) in problem.row(best_i) {
field[j] -= 2.0 * v * si;
}
}
Trial { energy: problem.energy(&s), spins: s }
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SaConfig {
pub sweeps: u32,
pub beta0: f64,
pub beta1: f64,
pub trials: usize,
pub seed: u64,
}
pub fn simulated_annealing(problem: &Ising, cfg: &SaConfig, threads: usize) -> Run {
let n = problem.n;
let trials = run_trials(cfg.trials, threads, |t| {
let mut rng = trial_rng(cfg.seed, t);
let mut s: Vec<i8> = (0..n).map(|_| if rng.next() >> 63 == 0 { 1 } else { -1 }).collect();
let mut field: Vec<f64> = (0..n).map(|i| problem.row(i).map(|(j, v)| v * f64::from(s[j])).sum::<f64>() + problem.h[i]).collect();
let mut e = problem.energy(&s);
let mut best = Trial { energy: e, spins: s.clone() };
for sweep in 0..cfg.sweeps {
let beta = if cfg.sweeps > 1 { cfg.beta0 + (cfg.beta1 - cfg.beta0) * f64::from(sweep) / f64::from(cfg.sweeps - 1) } else { cfg.beta1 };
for i in 0..n {
let de = 2.0 * f64::from(s[i]) * field[i];
let accept = de <= 0.0 || rng.unit() < exp(-beta * de);
if accept {
let si = f64::from(s[i]);
s[i] = -s[i];
e += de;
for (j, v) in problem.row(i) {
field[j] -= 2.0 * v * si;
}
}
}
if e < best.energy - 1e-9 {
let exact = problem.energy(&s);
e = exact;
if exact < best.energy {
best = Trial { energy: exact, spins: s.clone() };
}
}
}
best
});
Run { trials }
}
pub fn ground_state(problem: &Ising, threads: usize) -> Result<Trial, QioError> {
let n = problem.n;
if n > 34 {
return Err(QioError::TooLarge(n));
}
if n == 0 {
return Ok(Trial { energy: 0.0, spins: Vec::new() });
}
let free = if problem.has_fields() { n } else { n - 1 };
let offset = n - free;
let top = free.min(8).min(free.saturating_sub(4));
let low = free - top;
let chunks = 1usize << top;
let results = run_trials(chunks, threads, |c| {
let mut s = vec![1i8; n];
for b in 0..top {
if (c >> b) & 1 == 1 {
s[offset + low + b] = -1;
}
}
let mut field: Vec<f64> = (0..n).map(|i| problem.row(i).map(|(j, v)| v * f64::from(s[j])).sum::<f64>() + problem.h[i]).collect();
let mut e = problem.energy(&s);
let mut best = Trial { energy: e, spins: s.clone() };
for k in 1u64..(1u64 << low) {
let i = offset + k.trailing_zeros() as usize;
let si = f64::from(s[i]);
e += 2.0 * si * field[i];
s[i] = -s[i];
for (j, v) in problem.row(i) {
field[j] -= 2.0 * v * si;
}
if e < best.energy - 1e-9 {
best = Trial { energy: problem.energy(&s), spins: s.clone() };
e = best.energy;
}
}
best
});
Ok(results.into_iter().fold(Trial { energy: f64::INFINITY, spins: Vec::new() }, |b, t| if t.energy < b.energy { t } else { b }))
}
pub fn step_to_solution(p: f64, steps: f64) -> Option<f64> {
if p <= 0.0 {
return None;
}
if p >= 0.99 {
return Some(steps);
}
Some(steps * crate::repro::ln(0.01) / crate::repro::ln(1.0 - p))
}
#[cfg(test)]
mod tests {
use super::*;
fn random_sparse(n: usize, edges: usize, seed: u64) -> MaxCut {
let mut rng = Rng(seed);
let mut e = Vec::new();
while e.len() < edges {
let (i, j) = (rng.below(n as u64) as usize, rng.below(n as u64) as usize);
if i != j && !e.iter().any(|&(a, b, _)| (a, b) == (i.min(j), i.max(j))) {
e.push((i.min(j), i.max(j), if rng.next() >> 63 == 0 { 1.0 } else { -1.0 }));
}
}
MaxCut { n, edges: e }
}
#[test]
fn energy_and_cut_agree() {
let g = random_sparse(12, 30, 3);
let p = g.ising();
let total: f64 = g.edges.iter().map(|e| e.2).sum();
let mut rng = Rng(9);
for _ in 0..20 {
let s: Vec<i8> = (0..12).map(|_| if rng.next() >> 63 == 0 { 1 } else { -1 }).collect();
assert!((g.cut(&s) - (-p.energy(&s) / 2.0 + total / 2.0)).abs() < 1e-12);
}
}
#[test]
fn the_referee_enumerates_exhaustively() {
let p = Ising::sk(10, 4);
let mut best = f64::INFINITY;
for m in 0..(1u32 << 10) {
let s: Vec<i8> = (0..10).map(|b| if (m >> b) & 1 == 1 { -1 } else { 1 }).collect();
best = best.min(p.energy(&s));
}
assert_eq!(ground_state(&p, 3).unwrap().energy, best);
let q = Ising::new(6, &[(0, 1, 1.0), (1, 2, -2.0), (3, 4, 0.5)], &[0.3, -0.2, 0.0, 1.0, 0.0, -0.7]).unwrap();
let mut best = f64::INFINITY;
for m in 0..64u32 {
let s: Vec<i8> = (0..6).map(|b| if (m >> b) & 1 == 1 { -1 } else { 1 }).collect();
best = best.min(q.energy(&s));
}
assert!((ground_state(&q, 1).unwrap().energy - best).abs() < 1e-12);
}
#[test]
fn every_heuristic_finds_small_ground_states() {
for seed in 0..4 {
let p = Ising::sk(20, 100 + seed);
let exact = ground_state(&p, 4).unwrap().energy;
for v in [Variant::Ballistic, Variant::Discrete, Variant::HeatedBallistic(0.0), Variant::HeatedDiscrete(0.0)] {
let run = simulated_bifurcation(&p, &SbConfig::published(v, 1000, 32, seed), 4);
assert_eq!(run.best().energy, exact, "{v:?} seed {seed}");
assert!((p.energy(&run.best().spins) - run.best().energy).abs() < 1e-12);
}
let sa = simulated_annealing(&p, &SaConfig { sweeps: 500, beta0: 0.1, beta1: 3.0, trials: 16, seed }, 4);
assert_eq!(sa.best().energy, exact, "SA seed {seed}");
}
}
#[test]
fn fields_ride_on_an_extra_spin() {
let mut rng = Rng(5);
let mut c = Vec::new();
for i in 0..14 {
for j in i + 1..14 {
if rng.below(3) == 0 {
c.push((i, j, rng.symmetric()));
}
}
}
let h: Vec<f64> = (0..14).map(|_| rng.symmetric()).collect();
let p = Ising::new(14, &c, &h).unwrap();
let exact = ground_state(&p, 2).unwrap();
let run = simulated_bifurcation(&p, &SbConfig::published(Variant::Discrete, 2000, 32, 1), 4);
assert!((run.best().energy - exact.energy).abs() < 1e-9, "{} vs {}", run.best().energy, exact.energy);
assert_eq!(run.best().spins.len(), 14);
}
fn scalar_reference(problem: &Ising, cfg: &SbConfig, threads: usize) -> (Run, Vec<Vec<f64>>) {
let fields = problem.has_fields();
let p = if fields { problem.without_fields() } else { problem.clone() };
let n = p.n;
let sigma = p.rms_coupling();
let c0 = if sigma > 0.0 { cfg.c1 / (sigma * (n as f64).sqrt()) } else { 0.0 };
let (dt, a0, gamma, discrete) = (cfg.dt, cfg.a0, cfg.variant.gamma(), cfg.variant.discrete());
let trials = run_trials(cfg.trials, threads, |t| {
let mut rng = trial_rng(cfg.seed, t);
let mut x: Vec<f64> = (0..n).map(|_| rng.symmetric()).collect();
let mut y: Vec<f64> = (0..n).map(|_| rng.symmetric()).collect();
let mut f = vec![0.0; n];
let mut src = vec![0.0; n];
let mut best = Trial { energy: f64::INFINITY, spins: Vec::new() };
let mut consider = |x: &[f64]| {
let s = spins(x);
let e = p.energy(&s);
if e < best.energy {
best = Trial { energy: e, spins: s };
}
};
for k in 0..cfg.steps {
let a = a0 * f64::from(k) / f64::from(cfg.steps);
for i in 0..n {
src[i] = if discrete { if x[i] >= 0.0 { 1.0 } else { -1.0 } } else { x[i] };
}
for (i, fi) in f.iter_mut().enumerate() {
let mut acc = 0.0;
for kk in p.start[i]..p.start[i + 1] {
acc += p.val[kk] * src[p.col[kk] as usize];
}
*fi = acc;
}
for i in 0..n {
let y0 = y[i];
let mut yi = y0 + (-(a0 - a) * x[i] + c0 * f[i]) * dt;
let mut xi = x[i] + a0 * yi * dt;
if xi.abs() > 1.0 {
xi = if xi > 0.0 { 1.0 } else { -1.0 };
yi = 0.0;
}
x[i] = xi;
y[i] = yi + gamma * y0 * dt;
}
if cfg.sample_every > 0 && (k + 1) % cfg.sample_every == 0 {
consider(&x);
}
}
consider(&x);
(best, x)
});
let (trials, finals): (Vec<Trial>, Vec<Vec<f64>>) = trials.into_iter().unzip();
let trials = trials
.into_iter()
.map(|mut t| {
if fields {
let g = t.spins[n - 1];
t.spins.truncate(n - 1);
if g < 0 {
t.spins.iter_mut().for_each(|s| *s = -*s);
}
t.energy = problem.energy(&t.spins);
}
t
})
.collect();
(Run { trials }, finals)
}
#[test]
fn batching_changes_no_bit() {
let g = random_sparse(60, 300, 21);
let q = Ising::new(15, &[(0, 3, 0.5), (3, 7, -1.25), (7, 14, 2.0), (2, 9, 1.0)], &[0.1, 0.0, -0.4, 0.0, 0.2, 0.0, 0.0, 0.3, 0.0, 0.0, 0.0, -0.5, 0.0, 0.0, 0.0]).unwrap();
for p in [g.ising(), Ising::sk(33, 2), q] {
for v in [Variant::Ballistic, Variant::Discrete, Variant::HeatedBallistic(0.0), Variant::HeatedDiscrete(0.0)] {
let cfg = SbConfig::published(v, 250, 13, 5);
let (run, finals) = sb_with_state(&p, &cfg, 3);
let (r_run, r_finals) = scalar_reference(&p, &cfg, 2);
assert_eq!(run, r_run, "{v:?}");
let bits = |f: &Vec<Vec<f64>>| f.iter().flatten().map(|v| v.to_bits()).collect::<Vec<u64>>();
assert_eq!(bits(&finals), bits(&r_finals), "{v:?}");
}
}
}
#[test]
fn runs_do_not_depend_on_threads() {
let p = Ising::sk(40, 8);
for v in [Variant::Discrete, Variant::HeatedBallistic(0.5)] {
let cfg = SbConfig::published(v, 300, 12, 77);
assert_eq!(simulated_bifurcation(&p, &cfg, 1), simulated_bifurcation(&p, &cfg, 5));
}
let sa = SaConfig { sweeps: 100, beta0: 0.1, beta1: 2.0, trials: 9, seed: 3 };
assert_eq!(simulated_annealing(&p, &sa, 1), simulated_annealing(&p, &sa, 4));
}
#[test]
fn gset_text_reads_and_refuses() {
let g = read_gset("4 3\n1 2 1\n2 3 1\n3 4 -1\n").unwrap();
assert_eq!(g.n, 4);
assert_eq!(g.cut(&[1, -1, 1, -1]), 1.0);
assert!(read_gset("4 2\n1 2 1\n").is_err());
assert!(read_gset("4 1\n1 1 1\n").is_err());
assert!(read_gset("4 1\n1 5 1\n").is_err());
}
#[test]
fn descent_ends_at_a_local_minimum_never_higher() {
let p = Ising::sk(30, 6);
let mut rng = Rng(2);
for _ in 0..10 {
let s: Vec<i8> = (0..30).map(|_| if rng.next() >> 63 == 0 { 1 } else { -1 }).collect();
let d = descend(&p, &s);
assert!(d.energy <= p.energy(&s));
for i in 0..30 {
let mut t = d.spins.clone();
t[i] = -t[i];
assert!(p.energy(&t) >= d.energy - 1e-9);
}
}
}
#[test]
fn step_to_solution_matches_its_formula() {
assert_eq!(step_to_solution(0.0, 1000.0), None);
let s = step_to_solution(0.5, 1000.0).unwrap();
assert!((s - 1000.0 * (0.01f64).ln() / (0.5f64).ln()).abs() < 1e-9);
}
}