use crate::graph::{Graph, GraphBuilder};
use crate::rng::Pcg;
pub struct Planted {
pub graph: Graph,
pub ground_state: Vec<i8>,
pub ground_energy: f64,
pub loops: usize,
}
impl Planted {
pub fn excess(&self, s: &[i8]) -> f64 {
let e = self.graph.energy(s);
if self.ground_energy.abs() < 1e-12 {
return (e - self.ground_energy).abs();
}
(e - self.ground_energy) / self.ground_energy.abs()
}
pub fn solved(&self, s: &[i8]) -> bool {
self.graph.energy(s) <= self.ground_energy + 1e-9
}
}
pub fn frustrated_loops(l: usize, loops: usize, seed: u64) -> Planted {
assert!(l >= 3, "a periodic lattice smaller than 3x3 has degenerate plaquettes");
assert!(loops >= 1, "an instance with no frustration is a ferromagnet");
let n = l * l;
let mut rng = Pcg::new(seed, 0);
let sigma: Vec<i8> = (0..n).map(|_| if rng.f64() < 0.5 { 1 } else { -1 }).collect();
let at = |x: usize, y: usize| (y % l) * l + (x % l);
let mut b = GraphBuilder::new(n);
for _ in 0..loops {
let (x, y) = ((rng.f64() * l as f64) as usize % l, (rng.f64() * l as f64) as usize % l);
let c = [at(x, y), at(x + 1, y), at(x + 1, y + 1), at(x, y + 1)];
let broken = (rng.f64() * 4.0) as usize % 4;
for k in 0..4 {
let (i, j) = (c[k], c[(k + 1) % 4]);
let want = sigma[i] as f64 * sigma[j] as f64;
b.couple(i, j, if k == broken { -want } else { want });
}
}
Planted {
graph: b.build(),
ground_state: sigma,
ground_energy: -2.0 * loops as f64,
loops,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::oracle::{Exhaustive, RandomGuess, Solver, SteepestDescent};
#[test]
fn the_planted_state_really_is_the_ground_state() {
for l in 3..=4 {
for loops in [1, 3, 8, 20] {
for seed in 1..=3 {
let p = frustrated_loops(l, loops, seed);
let (_s, exact) = Exhaustive.solve(&p.graph);
let planted = p.graph.energy(&p.ground_state);
assert!(
(planted - exact).abs() < 1e-9,
"l={l} loops={loops} seed={seed}: planted {planted} but true optimum {exact}"
);
assert!(
(planted - p.ground_energy).abs() < 1e-9,
"l={l} loops={loops}: predicted {} but measured {planted}",
p.ground_energy
);
}
}
}
}
#[test]
fn the_predicted_ground_energy_is_exactly_minus_two_per_loop() {
for loops in [1, 5, 50, 500] {
let p = frustrated_loops(8, loops, 11);
assert_eq!(p.ground_energy, -2.0 * loops as f64);
assert!((p.graph.energy(&p.ground_state) - p.ground_energy).abs() < 1e-9);
}
}
#[test]
fn it_is_frustrated_rather_than_merely_disguised() {
let p = frustrated_loops(6, 60, 5);
let g = &p.graph;
let mut total = 0.0;
for i in 0..g.n {
for k in g.offset[i]..g.offset[i + 1] {
if g.nbr[k] as usize > i {
total += g.w[k].abs();
}
}
}
assert!(
p.ground_energy > -total + 1e-9,
"ground energy {} vs unfrustrated bound {}",
p.ground_energy,
-total
);
}
#[test]
fn instances_are_reproducible_and_seeds_differ() {
let a = frustrated_loops(6, 30, 42);
let b = frustrated_loops(6, 30, 42);
let c = frustrated_loops(6, 30, 43);
assert_eq!(a.ground_state, b.ground_state, "same seed must reproduce");
assert_ne!(a.ground_state, c.ground_state, "different seeds must differ");
}
#[test]
fn the_noise_oracle_never_solves_one() {
for l in [6, 10] {
let p = frustrated_loops(l, l * l * 2, 9);
let (s, _) = RandomGuess { tries: 20_000, seed: 2 }.solve(&p.graph);
assert!(!p.solved(&s), "random guessing solved a planted instance at l={l}");
assert!(p.excess(&s) > 0.2, "noise should be far off, was {}", p.excess(&s));
}
}
#[test]
fn a_real_method_gets_much_closer_than_noise() {
let p = frustrated_loops(8, 128, 4);
let noise = p.excess(&RandomGuess { tries: 20_000, seed: 2 }.solve(&p.graph).0);
let greedy = p.excess(&SteepestDescent { restarts: 200, seed: 2 }.solve(&p.graph).0);
let annealed = p.excess(
&crate::oracle::Annealer {
schedule: crate::schedule::Schedule::geometric(0.05, 6.0, 120, 40),
seed: 2,
}
.solve(&p.graph)
.0,
);
assert!(greedy < noise, "greedy {greedy} vs noise {noise}");
assert!(annealed < noise);
assert!(greedy < 0.20, "greedy should stay within 20% even near the hard peak, was {greedy}");
assert!(annealed < 0.10, "annealing should land within 10% of the optimum, got {annealed}");
}
#[test]
fn difficulty_peaks_in_the_middle_of_the_density_range() {
let rate = |loops: usize| {
let (mut solved, mut total) = (0, 0);
for iseed in 1..=4u64 {
let p = frustrated_loops(8, loops, iseed);
for sseed in 1..=4u64 {
let (s, _) = SteepestDescent { restarts: 50, seed: sseed }.solve(&p.graph);
total += 1;
if p.solved(&s) {
solved += 1;
}
}
}
solved as f64 / total as f64
};
let sparse = rate(16);
let peak = rate(128);
let dense = rate(512);
assert_eq!(sparse, 1.0, "a sparsely frustrated instance should always be solved");
assert_eq!(dense, 1.0, "a saturated instance relaxes to a gauged ferromagnet");
assert!(peak < 0.5, "the peak should defeat greedy more often than not, got {peak}");
assert!(peak < sparse && peak < dense, "difficulty must peak in the middle");
}
}
fn gauss(rng: &mut Pcg) -> f64 {
let u = rng.f64().max(1e-15);
let v = rng.f64();
(-2.0 * u.ln()).sqrt() * (core::f64::consts::TAU * v).cos()
}
pub fn wishart(n: usize, alpha: f64, seed: u64) -> Planted {
assert!(n >= 3, "a Wishart instance needs at least 3 spins");
assert!(alpha > 0.0, "alpha must be positive");
let m = ((alpha * n as f64).round() as usize).max(1);
let mut rng = Pcg::new(seed, 0);
let t: Vec<i8> = (0..n).map(|_| if rng.f64() < 0.5 { 1 } else { -1 }).collect();
let mut w = vec![0.0f64; n * m];
for c in 0..m {
let mut col: Vec<f64> = (0..n).map(|_| gauss(&mut rng)).collect();
let dot: f64 = col.iter().zip(&t).map(|(x, &s)| x * s as f64).sum();
for (i, x) in col.iter_mut().enumerate() {
*x -= dot * t[i] as f64 / n as f64; }
for i in 0..n {
w[i * m + c] = col[i];
}
}
let mut b = GraphBuilder::new(n);
let mut trace = 0.0;
for i in 0..n {
for j in i..n {
let mut dot = 0.0;
for c in 0..m {
dot += w[i * m + c] * w[j * m + c];
}
if i == j {
trace += dot;
} else {
b.couple(i, j, -dot / n as f64);
}
}
}
Planted {
graph: b.build(),
ground_state: t,
ground_energy: -trace / (2.0 * n as f64),
loops: m,
}
}
#[cfg(test)]
mod wishart_tests {
use super::*;
use crate::oracle::{Exhaustive, RandomGuess, Solver, SteepestDescent};
#[test]
fn the_planted_state_is_the_ground_state() {
for n in [8, 12, 16] {
for alpha in [0.5, 1.0, 2.0] {
for seed in 1..=3u64 {
let p = wishart(n, alpha, seed);
let (_s, exact) = Exhaustive.solve(&p.graph);
let planted = p.graph.energy(&p.ground_state);
assert!(
(planted - exact).abs() < 1e-7,
"n={n} alpha={alpha} seed={seed}: planted {planted} vs true {exact}"
);
}
}
}
}
#[test]
fn the_closed_form_ground_energy_is_right() {
for n in [10, 30, 80] {
for alpha in [0.3, 1.5] {
let p = wishart(n, alpha, 7);
let measured = p.graph.energy(&p.ground_state);
assert!(
(measured - p.ground_energy).abs() / p.ground_energy.abs() < 1e-9,
"n={n} alpha={alpha}: predicted {} measured {measured}",
p.ground_energy
);
}
}
}
#[test]
fn it_is_dense_where_the_lattice_family_is_sparse() {
let p = wishart(40, 1.0, 1);
assert_eq!(p.graph.max_degree(), 39, "every spin should couple to every other");
}
#[test]
fn low_alpha_really_is_harder() {
let rate = |alpha: f64| {
let (mut solved, mut total) = (0, 0);
for iseed in 1..=4u64 {
let p = wishart(24, alpha, iseed);
for sseed in 1..=4u64 {
let (s, _) = SteepestDescent { restarts: 100, seed: sseed }.solve(&p.graph);
total += 1;
if p.solved(&s) {
solved += 1;
}
}
}
solved as f64 / total as f64
};
let hard = rate(0.3);
let easy = rate(3.0);
assert!(
hard < easy,
"alpha 0.3 should defeat greedy more often than alpha 3.0: {hard} vs {easy}"
);
}
#[test]
fn noise_never_solves_one() {
let p = wishart(40, 0.5, 3);
let (s, _) = RandomGuess { tries: 50_000, seed: 1 }.solve(&p.graph);
assert!(!p.solved(&s));
assert!(p.excess(&s) > 0.1, "noise was only {} off", p.excess(&s));
}
}