use crate::factor::FactorError;
use crate::graph::Graph;
use crate::ledger::Ledger;
use crate::rng::Pcg;
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Hubo {
n: usize,
terms: Vec<(Vec<u32>, f64)>,
incident: Vec<Vec<usize>>,
}
impl Hubo {
pub fn new(n: usize) -> Hubo {
Hubo { n, terms: Vec::new(), incident: vec![Vec::new(); n] }
}
pub fn len(&self) -> usize {
self.n
}
pub fn is_empty(&self) -> bool {
self.n == 0
}
pub fn terms(&self) -> usize {
self.terms.len()
}
pub fn max_arity(&self) -> usize {
self.terms.iter().map(|(v, _)| v.len()).max().unwrap_or(0)
}
pub fn ancillas_avoided(&self) -> usize {
self.terms.iter().map(|(v, _)| v.len().saturating_sub(2)).sum()
}
pub fn add(&mut self, vars: &[usize], weight: f64) -> Result<(), FactorError> {
let f = crate::factor::Factor::new(vars, weight, self.n)?;
let mut vs: Vec<u32> = f.vars().map(|v| v as u32).collect();
vs.sort_unstable();
let idx = self.terms.len();
for &v in &vs {
self.incident[v as usize].push(idx);
}
self.terms.push((vs, weight));
Ok(())
}
pub fn from_graph(g: &Graph) -> Hubo {
let mut h = Hubo::new(g.n);
for i in 0..g.n {
if g.h[i] != 0.0 {
h.add(&[i], g.h[i]).expect("a single in-range variable");
}
for k in g.offset[i]..g.offset[i + 1] {
let j = g.nbr[k] as usize;
if j > i {
h.add(&[i, j], g.w[k]).expect("two distinct in-range variables");
}
}
}
h
}
pub fn energy(&self, s: &[i8]) -> f64 {
self.terms
.iter()
.map(|(vs, w)| {
let prod: i32 = vs.iter().map(|&v| s[v as usize] as i32).product();
-w * prod as f64
})
.sum()
}
pub fn delta(&self, s: &[i8], i: usize) -> f64 {
2.0 * self.incident[i]
.iter()
.map(|&t| {
let (vs, w) = &self.terms[t];
let prod: i32 = vs.iter().map(|&v| s[v as usize] as i32).product();
w * prod as f64
})
.sum::<f64>()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Params {
pub beta_min: f64,
pub beta_max: f64,
pub stages: usize,
pub sweeps_per_stage: usize,
}
impl Default for Params {
fn default() -> Self {
Params { beta_min: 0.05, beta_max: 8.0, stages: 200, sweeps_per_stage: 8 }
}
}
#[derive(Clone, Debug)]
pub struct Outcome {
pub state: Vec<i8>,
pub energy: f64,
pub proposals: u64,
pub accepted: u64,
pub ancillas_avoided: usize,
}
pub fn anneal(h: &Hubo, p: &Params, seed: u64) -> Outcome {
anneal_metered(h, p, seed, None)
}
pub fn anneal_metered(h: &Hubo, p: &Params, seed: u64, mut ledger: Option<&mut Ledger>) -> Outcome {
let n = h.n;
if n == 0 {
return Outcome {
state: Vec::new(),
energy: 0.0,
proposals: 0,
accepted: 0,
ancillas_avoided: 0,
};
}
let mut rng = Pcg::new(seed, 0x0000_40B0);
let mut s: Vec<i8> = (0..n).map(|_| rng.spin(0.5)).collect();
let mut best = s.clone();
let mut best_e = h.energy(&s);
let (mut proposals, mut accepted) = (0u64, 0u64);
let stages = p.stages.max(1);
let (b0, b1) = (p.beta_min.max(1e-12), p.beta_max.max(p.beta_min.max(1e-12)));
for stage in 0..stages {
let f = if stages == 1 { 1.0 } else { stage as f64 / (stages - 1) as f64 };
let beta = b0 * (b1 / b0).powf(f);
for _ in 0..p.sweeps_per_stage.max(1) {
for i in 0..n {
let d = h.delta(&s, i);
proposals += 1;
if d <= 0.0 || rng.f64() < (-beta * d).exp() {
s[i] = -s[i];
accepted += 1;
}
}
if let Some(l) = ledger.as_deref_mut() {
l.samples += n as u64;
}
let e = h.energy(&s);
if e < best_e {
best_e = e;
best.copy_from_slice(&s);
}
}
}
let energy = h.energy(&best);
Outcome { state: best, energy, proposals, accepted, ancillas_avoided: h.ancillas_avoided() }
}
#[cfg(test)]
mod tests {
use super::*;
use crate::graph::GraphBuilder;
use crate::ising::lattice2d;
#[test]
fn a_lifted_graph_has_exactly_the_graph_energy() {
for seed in 0..8u64 {
let mut rng = Pcg::new(seed, 0x11);
let n = 20;
let mut gb = GraphBuilder::new(n);
for i in 0..n {
gb.bias(i, rng.f64() - 0.5);
for j in (i + 1)..n {
if rng.f64() < 0.3 {
gb.couple(i, j, rng.f64() * 2.0 - 1.0);
}
}
}
let g = gb.build();
let h = Hubo::from_graph(&g);
assert_eq!(h.max_arity(), 2);
assert_eq!(h.ancillas_avoided(), 0, "a pairwise model needs no ancillas");
for t in 0..40 {
let s: Vec<i8> = (0..n).map(|_| rng.spin(0.5)).collect();
assert!(
(h.energy(&s) - g.energy(&s)).abs() < 1e-9,
"seed {seed} trial {t}: hubo {} vs graph {}",
h.energy(&s),
g.energy(&s)
);
}
}
}
#[test]
fn the_flip_update_agrees_with_recomputing_from_scratch() {
let mut rng = Pcg::new(5, 0x22);
let n = 12;
let mut h = Hubo::new(n);
for _ in 0..40 {
let k = 1 + (rng.next_u32() as usize) % 4;
let mut vars: Vec<usize> = Vec::new();
while vars.len() < k {
let v = (rng.next_u32() as usize) % n;
if !vars.contains(&v) {
vars.push(v);
}
}
h.add(&vars, rng.f64() * 2.0 - 1.0).unwrap();
}
assert!(h.max_arity() >= 3, "the test needs a genuinely higher-order model");
for _ in 0..50 {
let mut s: Vec<i8> = (0..n).map(|_| rng.spin(0.5)).collect();
for i in 0..n {
let before = h.energy(&s);
let d = h.delta(&s, i);
s[i] = -s[i];
let after = h.energy(&s);
assert!(
(after - before - d).abs() < 1e-9,
"site {i}: delta said {d}, the model moved by {}",
after - before
);
s[i] = -s[i];
}
}
}
#[test]
fn it_solves_a_higher_order_model_that_the_reduction_would_pay_ancillas_for() {
let mut solved = 0;
for seed in 0..12u64 {
let mut rng = Pcg::new(seed, 0x33);
let n = 14;
let mut h = Hubo::new(n);
for _ in 0..24 {
let k = 3 + (rng.next_u32() as usize) % 2; let mut vars: Vec<usize> = Vec::new();
while vars.len() < k {
let v = (rng.next_u32() as usize) % n;
if !vars.contains(&v) {
vars.push(v);
}
}
h.add(&vars, rng.f64() * 2.0 - 1.0).unwrap();
}
assert!(h.ancillas_avoided() >= 24, "arity 3 and 4 terms cost at least one each");
let out = anneal(&h, &Params::default(), seed);
assert_eq!(out.ancillas_avoided, h.ancillas_avoided());
assert!((out.energy - h.energy(&out.state)).abs() < 1e-9);
let mut truth = f64::INFINITY;
let mut s = vec![1i8; n];
for mask in 0..(1u32 << n) {
for i in 0..n {
s[i] = if mask >> i & 1 == 1 { 1 } else { -1 };
}
truth = truth.min(h.energy(&s));
}
assert!(
out.energy >= truth - 1e-9,
"seed {seed}: reached {}, BELOW the exhaustive minimum {truth} -- the model's \
energy and the enumeration's disagree",
out.energy
);
if (out.energy - truth).abs() < 1e-9 {
solved += 1;
}
}
assert!(
solved >= 10,
"the anneal reached the true minimum on only {solved} of 12 higher-order instances"
);
}
#[test]
fn on_a_pairwise_model_it_matches_the_pairwise_path() {
for l in [4usize, 5, 6] {
let g = lattice2d(l, 1.0);
let h = Hubo::from_graph(&g);
let out = anneal(&h, &Params::default(), 3);
let bonds = 2.0 * (l * l) as f64;
assert!((out.energy + bonds).abs() < 1e-9, "{l}x{l}: got {}", out.energy);
}
}
#[test]
fn a_malformed_term_is_refused_and_an_empty_model_returns() {
let mut h = Hubo::new(4);
assert!(h.add(&[], 1.0).is_err(), "an empty term");
assert!(h.add(&[0, 9], 1.0).is_err(), "a variable off the end");
assert!(h.add(&[1, 1], 1.0).is_err(), "a repeated variable");
assert!(h.add(&[0, 1], f64::NAN).is_err(), "a non-finite weight");
assert_eq!(h.terms(), 0, "nothing malformed was recorded");
let out = anneal(&Hubo::new(0), &Params::default(), 1);
assert!(out.state.is_empty() && out.energy == 0.0);
}
}