thread_local! {
static BUILDS: core::cell::Cell<u64> = const { core::cell::Cell::new(0) };
}
pub fn graph_builds() -> u64 {
BUILDS.with(|b| b.get())
}
pub struct GraphBuilder {
n: usize,
edges: Vec<(u32, u32, f64)>,
bias: Vec<f64>,
}
impl GraphBuilder {
pub fn new(n: usize) -> Self {
GraphBuilder { n, edges: Vec::new(), bias: vec![0.0; n] }
}
pub fn n(&self) -> usize {
self.n
}
pub fn couple(&mut self, i: usize, j: usize, jij: f64) {
assert!(i < self.n && j < self.n && i != j, "bad edge ({i},{j}) n={}", self.n);
self.edges.push((i as u32, j as u32, jij));
}
pub fn bias(&mut self, i: usize, h: f64) {
self.bias[i] += h;
}
pub fn set_bias(&mut self, i: usize, h: f64) {
self.bias[i] = h;
}
pub fn build(self) -> Graph {
BUILDS.with(|b| b.set(b.get() + 1));
let n = self.n;
let mut merged: std::collections::BTreeMap<(u32, u32), f64> = std::collections::BTreeMap::new();
for (a, b, j) in self.edges {
let key = if a < b { (a, b) } else { (b, a) };
*merged.entry(key).or_insert(0.0) += j;
}
let mut deg = vec![0usize; n];
for &(a, b) in merged.keys() {
deg[a as usize] += 1;
deg[b as usize] += 1;
}
let mut offset = vec![0usize; n + 1];
for i in 0..n {
offset[i + 1] = offset[i] + deg[i];
}
let m2 = offset[n];
let mut nbr = vec![0u32; m2];
let mut w = vec![0.0f64; m2];
let mut cursor = offset.clone();
for (&(a, b), &j) in merged.iter() {
nbr[cursor[a as usize]] = b;
w[cursor[a as usize]] = j;
cursor[a as usize] += 1;
nbr[cursor[b as usize]] = a;
w[cursor[b as usize]] = j;
cursor[b as usize] += 1;
}
let colors = color_greedy(n, &offset, &nbr);
let n_colors = colors.iter().copied().max().map_or(1, |c| c as usize + 1);
let mut classes: Vec<Vec<u32>> = vec![Vec::new(); n_colors];
for i in 0..n {
classes[colors[i] as usize].push(i as u32);
}
Graph { n, offset, nbr, w, h: self.bias, colors, classes, n_edges: merged.len() }
}
}
pub struct Graph {
pub n: usize,
pub offset: Vec<usize>,
pub nbr: Vec<u32>,
pub w: Vec<f64>,
pub h: Vec<f64>,
pub colors: Vec<u16>,
pub classes: Vec<Vec<u32>>,
pub n_edges: usize,
}
impl Graph {
#[inline]
pub fn field(&self, i: usize, s: &[i8]) -> f64 {
let mut f = self.h[i];
for k in self.offset[i]..self.offset[i + 1] {
f += self.w[k] * s[self.nbr[k] as usize] as f64;
}
f
}
pub fn energy(&self, s: &[i8]) -> f64 {
let mut e = 0.0;
for i in 0..self.n {
let si = s[i] as f64;
e -= self.h[i] * si;
for k in self.offset[i]..self.offset[i + 1] {
let j = self.nbr[k] as usize;
if j > i {
e -= self.w[k] * si * s[j] as f64;
}
}
}
e
}
pub fn max_degree(&self) -> usize {
(0..self.n).map(|i| self.offset[i + 1] - self.offset[i]).max().unwrap_or(0)
}
}
fn color_greedy(n: usize, offset: &[usize], nbr: &[u32]) -> Vec<u16> {
let mut colors = vec![u16::MAX; n];
let mut used: Vec<bool> = Vec::new();
for i in 0..n {
used.clear();
used.resize(64, false);
for k in offset[i]..offset[i + 1] {
let c = colors[nbr[k] as usize];
if c != u16::MAX {
if (c as usize) >= used.len() {
used.resize(c as usize + 1, false);
}
used[c as usize] = true;
}
}
let c = used.iter().position(|&u| !u).unwrap_or(used.len());
colors[i] = c as u16;
}
colors
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coloring_is_proper() {
let mut gb = GraphBuilder::new(100);
let mut x = 1u64;
for _ in 0..300 {
x = x.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
let a = (x >> 33) as usize % 100;
let b = (x >> 13) as usize % 100;
if a != b {
gb.couple(a, b, 0.5);
}
}
let g = gb.build();
for i in 0..g.n {
for k in g.offset[i]..g.offset[i + 1] {
assert_ne!(g.colors[i], g.colors[g.nbr[k] as usize], "adjacent same color");
}
}
let total: usize = g.classes.iter().map(|c| c.len()).sum();
assert_eq!(total, g.n);
}
#[test]
fn a_graph_builds_bit_identically_every_time() {
use crate::gibbs::Sampler;
use crate::planted::wishart;
let mut orders = Vec::new();
let mut states = Vec::new();
let mut bits = Vec::new();
for _ in 0..8 {
let g = wishart(40, 1.0, 7).graph;
orders.push(g.nbr.clone());
let mut s = Sampler::new(&g, 1.2, 42);
s.sweeps(200, None);
bits.push(g.energy(&s.s).to_bits());
states.push(s.s.clone());
}
assert!(orders.windows(2).all(|w| w[0] == w[1]), "CSR neighbour order must not vary");
assert!(states.windows(2).all(|w| w[0] == w[1]), "the sampled state must not vary");
assert!(
bits.windows(2).all(|w| w[0] == w[1]),
"energies must be BIT-identical WITHIN a platform, not merely equal to the digits that \
get printed"
);
}
#[test]
fn the_compiled_program_is_byte_reproducible() {
use crate::model::{Expr, Lit, Model, Sense};
let build = || {
let mut m = Model::new();
let a = m.categorical("a", 3);
let b = m.categorical("b", 3);
m.not_equal(a, b);
m.at_most(vec![Lit::Is(a, 0), Lit::Is(b, 0)], 1);
m.objective(Sense::Maximize, Expr::product(3.0, &[Lit::Is(a, 1)]));
m.compile().unwrap().program.to_ftp()
};
let first = build();
for _ in 0..4 {
assert_eq!(build(), first, "the same model must compile to the same bytes");
}
}
}