use crate::exact::{Elimination, TooWide};
use crate::graph::{Graph, GraphBuilder};
use crate::rng::Pcg;
pub fn step(g: &Graph, s: &mut [i8], block: &[usize], el: &Elimination) -> Result<f64, TooWide> {
let mut map = vec![usize::MAX; g.n];
let mut vars: Vec<usize> = Vec::with_capacity(block.len());
for &i in block {
if i < g.n && map[i] == usize::MAX {
map[i] = vars.len();
vars.push(i);
}
}
if vars.is_empty() {
return Ok(0.0);
}
let mut gb = GraphBuilder::new(vars.len());
for (a, &i) in vars.iter().enumerate() {
let mut field = g.h[i];
for k in g.offset[i]..g.offset[i + 1] {
let j = g.nbr[k] as usize;
if map[j] == usize::MAX {
field += g.w[k] * s[j] as f64;
} else if j > i {
gb.couple(a, map[j], g.w[k]);
}
}
if field != 0.0 {
gb.bias(a, field);
}
}
let residual = gb.build();
let solved = el.ground_state(&residual)?;
let best = solved.ground_state.expect("min-sum was run");
let before = g.energy(s);
for (a, &i) in vars.iter().enumerate() {
s[i] = best[a];
}
Ok(g.energy(s) - before)
}
pub fn tree_block(g: &Graph, seed: usize, target: usize, rng: &mut Pcg) -> Vec<usize> {
if g.n == 0 || target == 0 {
return Vec::new();
}
let seed = seed % g.n;
let mut inside = vec![false; g.n];
let mut block = vec![seed];
inside[seed] = true;
let mut frontier: Vec<usize> = Vec::new();
let push_nbrs = |i: usize, frontier: &mut Vec<usize>| {
for k in g.offset[i]..g.offset[i + 1] {
frontier.push(g.nbr[k] as usize);
}
};
push_nbrs(seed, &mut frontier);
while block.len() < target && !frontier.is_empty() {
let at = (rng.f64() * frontier.len() as f64) as usize % frontier.len();
let cand = frontier.swap_remove(at);
if inside[cand] {
continue;
}
let touching = (g.offset[cand]..g.offset[cand + 1])
.filter(|&k| inside[g.nbr[k] as usize])
.count();
if touching != 1 {
continue;
}
inside[cand] = true;
block.push(cand);
push_nbrs(cand, &mut frontier);
}
block
}
pub fn forest_block(g: &Graph, seed: usize, target: usize, rng: &mut Pcg) -> Vec<usize> {
if g.n == 0 || target == 0 {
return Vec::new();
}
let start = seed % g.n;
let mut inside = vec![false; g.n];
let mut block = Vec::with_capacity(target.min(g.n));
let mut touching = vec![0usize; g.n];
let mut order: Vec<usize> = (0..g.n).collect();
order.swap(0, start);
for i in 1..g.n {
let at = i + (rng.f64() * (g.n - i) as f64) as usize % (g.n - i);
order.swap(i, at);
}
for &cand in &order {
if block.len() >= target {
break;
}
if touching[cand] > 1 {
continue;
}
inside[cand] = true;
block.push(cand);
for k in g.offset[cand]..g.offset[cand + 1] {
touching[g.nbr[k] as usize] += 1;
}
}
let _ = inside;
block
}
pub fn grown_block(
g: &Graph,
seed: usize,
target: usize,
max_width: usize,
rng: &mut Pcg,
) -> Option<Vec<usize>> {
if g.n == 0 || target == 0 {
return None;
}
let seed = seed % g.n;
let mut inside = vec![false; g.n];
let mut block = vec![seed];
inside[seed] = true;
let mut frontier: Vec<usize> = (g.offset[seed]..g.offset[seed + 1])
.map(|k| g.nbr[k] as usize)
.collect();
while block.len() < target && !frontier.is_empty() {
let at = (rng.f64() * frontier.len() as f64) as usize % frontier.len();
let cand = frontier.swap_remove(at);
if inside[cand] {
continue;
}
inside[cand] = true;
block.push(cand);
for k in g.offset[cand]..g.offset[cand + 1] {
frontier.push(g.nbr[k] as usize);
}
}
let mut map = vec![usize::MAX; g.n];
for (a, &i) in block.iter().enumerate() {
map[i] = a;
}
let mut gb = GraphBuilder::new(block.len());
for (a, &i) in block.iter().enumerate() {
for k in g.offset[i]..g.offset[i + 1] {
let j = g.nbr[k] as usize;
if map[j] != usize::MAX && j > i {
gb.couple(a, map[j], g.w[k]);
}
}
}
let induced = gb.build();
if Elimination::default().width(&induced) > max_width {
return None;
}
Some(block)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Blocks {
Tree,
Forest,
Grown,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Params {
pub steps: usize,
pub block: usize,
pub max_width: usize,
pub blocks: Blocks,
}
impl Default for Params {
fn default() -> Self {
Params { steps: 400, block: 64, max_width: 12, blocks: Blocks::Tree }
}
}
#[derive(Clone, Debug)]
pub struct Outcome {
pub state: Vec<i8>,
pub energy: f64,
pub moves: usize,
pub improving: usize,
pub refused: usize,
}
pub fn run(g: &Graph, p: &Params, seed: u64) -> Outcome {
let mut rng = Pcg::new(seed, 0x004F_5300);
let s: Vec<i8> = (0..g.n).map(|_| rng.spin(0.5)).collect();
run_from(g, s, p, seed)
}
pub fn run_from(g: &Graph, start: Vec<i8>, p: &Params, seed: u64) -> Outcome {
let el = Elimination { max_width: p.max_width.max(1) };
let mut rng = Pcg::new(seed, 0x0048_4653);
let mut s = start;
let (mut moves, mut improving, mut refused) = (0usize, 0usize, 0usize);
for _ in 0..p.steps {
if g.n == 0 {
break;
}
let seed_node = (rng.f64() * g.n as f64) as usize % g.n;
let block = match p.blocks {
Blocks::Tree => tree_block(g, seed_node, p.block, &mut rng),
Blocks::Forest => forest_block(g, seed_node, p.block, &mut rng),
Blocks::Grown => match grown_block(g, seed_node, p.block, p.max_width, &mut rng) {
Some(b) => b,
None => {
refused += 1;
continue;
}
},
};
match step(g, &mut s, &block, &el) {
Ok(d) => {
moves += 1;
if d < -1e-12 {
improving += 1;
}
}
Err(_) => refused += 1,
}
}
Outcome { energy: g.energy(&s), state: s, moves, improving, refused }
}
#[cfg(test)]
mod tests {
#[test]
fn a_forest_block_is_a_forest_and_is_about_the_size_of_a_tree() {
let g = crate::ising::chimera(4, 4, 4, 1.0);
let mut rng = Pcg::new(11, 1);
for seed in 0..8usize {
let f = forest_block(&g, seed, g.n, &mut rng);
let inside: std::collections::BTreeSet<usize> = f.iter().copied().collect();
assert_eq!(inside.len(), f.len(), "a block is a set");
let mut edges = 0usize;
for &i in &f {
for k in g.offset[i]..g.offset[i + 1] {
let jj = g.nbr[k] as usize;
if jj > i && inside.contains(&jj) {
edges += 1;
}
}
}
assert!(edges < f.len(), "{} vertices and {edges} edges is not acyclic", f.len());
let mut map = vec![usize::MAX; g.n];
for (a, &i) in f.iter().enumerate() {
map[i] = a;
}
let mut gb = GraphBuilder::new(f.len());
for (a, &i) in f.iter().enumerate() {
for k in g.offset[i]..g.offset[i + 1] {
let jj = g.nbr[k] as usize;
if map[jj] != usize::MAX && jj > i {
gb.couple(a, map[jj], g.w[k]);
}
}
}
assert!(Elimination::default().width(&gb.build()) <= 1);
}
let mut r1 = Pcg::new(4, 1);
let mut r2 = Pcg::new(4, 1);
let tree = tree_block(&g, 0, g.n, &mut r1).len();
let forest = forest_block(&g, 0, g.n, &mut r2).len();
assert!(tree >= g.n / 4 && forest >= g.n / 4, "tree {tree}, forest {forest} of {}", g.n);
assert!(
(tree as i64 - forest as i64).abs() < g.n as i64 / 4,
"neither should dwarf the other: tree {tree}, forest {forest}"
);
}
#[test]
fn the_block_strategies_differ() {
let g = crate::ising::chimera_glass(4, 4, 4, 7);
let base = Params { steps: 60, block: 96, ..Params::default() };
let tree = run(&g, &Params { blocks: Blocks::Tree, ..base }, 3);
let forest = run(&g, &Params { blocks: Blocks::Forest, ..base }, 3);
assert!(
tree.energy != forest.energy || tree.state != forest.state,
"tree and forest blocks must be different searches"
);
}
use super::*;
use crate::graph::GraphBuilder;
fn glass(l: usize, seed: u64) -> Graph {
let mut rng = Pcg::new(seed, 0x0F5A);
let mut b = GraphBuilder::new(l * l);
for y in 0..l {
for x in 0..l {
let i = y * l + x;
b.couple(i, y * l + (x + 1) % l, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
b.couple(i, ((y + 1) % l) * l + x, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
}
}
b.build()
}
#[test]
fn a_block_move_finds_exactly_what_enumerating_the_block_finds() {
let el = Elimination::default();
for seed in [1u64, 42, 777] {
let g = glass(5, seed);
let mut rng = Pcg::new(seed ^ 0xB10C, 5);
let mut s: Vec<i8> = (0..g.n).map(|_| rng.spin(0.5)).collect();
let block: Vec<usize> = vec![0, 1, 2, 5, 6, 7, 10, 11];
let start = s.clone();
let mut best_e = f64::INFINITY;
let mut best = start.clone();
for mask in 0..(1u32 << block.len()) {
let mut cand = start.clone();
for (bit, &i) in block.iter().enumerate() {
cand[i] = if mask >> bit & 1 == 1 { 1 } else { -1 };
}
let e = g.energy(&cand);
if e < best_e {
best_e = e;
best = cand;
}
}
let d = step(&g, &mut s, &block, &el).expect("a width-8 block is narrow");
assert!(
(g.energy(&s) - best_e).abs() < 1e-9,
"seed {seed}: block move got {} where enumeration gets {best_e}",
g.energy(&s)
);
assert!(d <= 1e-12, "a block move can never raise the energy: {d}");
assert!((g.energy(&best) - g.energy(&s)).abs() < 1e-9);
}
}
#[test]
fn a_tree_block_is_a_tree() {
let g = glass(8, 3);
let mut rng = Pcg::new(9, 1);
for target in [4usize, 16, 40] {
for seed in 0..12usize {
let block = tree_block(&g, seed, target, &mut rng);
let inside: std::collections::BTreeSet<usize> = block.iter().copied().collect();
assert_eq!(inside.len(), block.len(), "a block is a set");
let mut edges = 0usize;
for &i in &block {
for k in g.offset[i]..g.offset[i + 1] {
let j = g.nbr[k] as usize;
if j > i && inside.contains(&j) {
edges += 1;
}
}
}
assert_eq!(
edges,
block.len() - 1,
"target {target}, seed {seed}: {} nodes and {edges} induced edges is not a tree",
block.len()
);
let mut map = vec![usize::MAX; g.n];
for (a, &i) in block.iter().enumerate() {
map[i] = a;
}
let mut gb = GraphBuilder::new(block.len());
for (a, &i) in block.iter().enumerate() {
for k in g.offset[i]..g.offset[i + 1] {
let j = g.nbr[k] as usize;
if map[j] != usize::MAX && j > i {
gb.couple(a, map[j], g.w[k]);
}
}
}
if block.len() > 1 {
assert!(Elimination::default().width(&gb.build()) <= 1);
}
}
}
}
#[test]
fn the_descent_never_raises_the_energy() {
let g = glass(10, 11);
let el = Elimination::default();
let mut rng = Pcg::new(4, 1);
let mut s: Vec<i8> = (0..g.n).map(|_| rng.spin(0.5)).collect();
let mut e = g.energy(&s);
for k in 0..60usize {
let block = tree_block(&g, k * 7, 24, &mut rng);
step(&g, &mut s, &block, &el).unwrap();
let now = g.energy(&s);
assert!(now <= e + 1e-9, "step {k}: {e} -> {now}");
e = now;
}
}
#[test]
fn block_moves_beat_single_flip_descent_from_the_same_starts() {
let g = glass(12, 0xC0DE);
let el = Elimination::default();
let (mut hfs_wins, mut ties, mut losses) = (0, 0, 0);
for seed in 0..12u64 {
let mut rng = Pcg::new(seed, 0xA1);
let start: Vec<i8> = (0..g.n).map(|_| rng.spin(0.5)).collect();
let mut a = start.clone();
let mut r = Pcg::new(seed, 0xB2);
for k in 0..40usize {
let block = tree_block(&g, k * 13, 48, &mut r);
step(&g, &mut a, &block, &el).unwrap();
}
let mut b = start.clone();
loop {
let mut best = (0.0f64, usize::MAX);
for i in 0..g.n {
let mut f = g.h[i];
for k in g.offset[i]..g.offset[i + 1] {
f += g.w[k] * b[g.nbr[k] as usize] as f64;
}
let d = 2.0 * f * b[i] as f64;
if d < best.0 - 1e-12 {
best = (d, i);
}
}
if best.1 == usize::MAX {
break;
}
b[best.1] = -b[best.1];
}
let (ea, eb) = (g.energy(&a), g.energy(&b));
if ea < eb - 1e-9 {
hfs_wins += 1;
} else if ea > eb + 1e-9 {
losses += 1;
} else {
ties += 1;
}
}
assert!(
hfs_wins > losses,
"block moves must beat single flips on a frustrated glass: {hfs_wins} wins, \
{losses} losses, {ties} ties"
);
}
#[test]
fn a_grown_block_is_refused_when_it_measures_too_wide() {
let mut b = GraphBuilder::new(24);
for i in 0..24 {
for j in (i + 1)..24 {
b.couple(i, j, 0.5);
}
}
let g = b.build();
let mut rng = Pcg::new(1, 1);
assert!(grown_block(&g, 0, 16, 3, &mut rng).is_none(), "16 dense nodes are not width 3");
assert!(grown_block(&g, 0, 2, 3, &mut rng).is_some());
}
#[test]
fn a_run_reports_what_it_did_and_reproduces_itself() {
let g = glass(8, 5);
let p = Params { steps: 30, block: 20, ..Params::default() };
let a = run(&g, &p, 7);
let b = run(&g, &p, 7);
assert_eq!(a.state, b.state, "same seed, same run");
assert_eq!(a.energy, b.energy);
assert!(a.moves > 0 && a.refused == 0, "{a:?}");
assert!((g.energy(&a.state) - a.energy).abs() < 1e-12);
assert!(run(&g, &p, 8).state != a.state);
}
#[test]
fn an_empty_block_and_an_empty_graph_are_no_ops_rather_than_panics() {
let g = glass(4, 1);
let el = Elimination::default();
let mut s = vec![1i8; g.n];
assert_eq!(step(&g, &mut s, &[], &el).unwrap(), 0.0);
assert_eq!(step(&g, &mut s, &[9999], &el).unwrap(), 0.0);
let empty = GraphBuilder::new(0).build();
assert_eq!(run(&empty, &Params::default(), 1).moves, 0);
}
}