use crate::graph::{Graph, GraphBuilder};
pub fn ring(n: usize, j: f64, h: f64) -> Graph {
let mut gb = GraphBuilder::new(n);
for i in 0..n {
gb.couple(i, (i + 1) % n, j);
if h != 0.0 {
gb.bias(i, h);
}
}
gb.build()
}
pub fn lattice2d(l: usize, j: f64) -> Graph {
if l < 2 {
return GraphBuilder::new(l * l).build();
}
let mut gb = GraphBuilder::new(l * l);
for y in 0..l {
for x in 0..l {
let i = y * l + x;
gb.couple(i, y * l + (x + 1) % l, j);
gb.couple(i, ((y + 1) % l) * l + x, j);
}
}
gb.build()
}
pub fn chimera(m: usize, n: usize, t: usize, j: f64) -> Graph {
let cells = m * n;
if cells == 0 || t == 0 {
return GraphBuilder::new(0).build();
}
let idx = |i: usize, jj: usize, u: usize, k: usize| ((i * n) + jj) * 2 * t + u * t + k;
let mut gb = GraphBuilder::new(2 * t * cells);
for i in 0..m {
for jj in 0..n {
for a in 0..t {
for b in 0..t {
gb.couple(idx(i, jj, 0, a), idx(i, jj, 1, b), j);
}
}
if i + 1 < m {
for k in 0..t {
gb.couple(idx(i, jj, 0, k), idx(i + 1, jj, 0, k), j);
}
}
if jj + 1 < n {
for k in 0..t {
gb.couple(idx(i, jj, 1, k), idx(i, jj + 1, 1, k), j);
}
}
}
}
gb.build()
}
pub fn chimera_glass(m: usize, n: usize, t: usize, seed: u64) -> Graph {
let g = chimera(m, n, t, 1.0);
let mut rng = crate::rng::Pcg::new(seed, 0x00C1_1E5A);
let mut gb = GraphBuilder::new(g.n);
for i in 0..g.n {
for k in g.offset[i]..g.offset[i + 1] {
let jj = g.nbr[k] as usize;
if jj > i {
gb.couple(i, jj, if rng.f64() < 0.5 { 1.0 } else { -1.0 });
}
}
}
gb.build()
}
pub fn chimera_shore(m: usize, n: usize, t: usize, u: usize) -> Vec<usize> {
if m * n == 0 || t == 0 || u > 1 {
return Vec::new();
}
let mut out = Vec::with_capacity(t * m * n);
for i in 0..m {
for jj in 0..n {
for k in 0..t {
out.push(((i * n) + jj) * 2 * t + u * t + k);
}
}
}
out
}
pub fn grid2d(w: usize, h: usize, j: f64) -> Graph {
let mut gb = GraphBuilder::new(w * h);
for y in 0..h {
for x in 0..w {
let i = y * w + x;
if x + 1 < w {
gb.couple(i, i + 1, j);
}
if y + 1 < h {
gb.couple(i, i + w, j);
}
}
}
gb.build()
}
pub fn exact_boltzmann(g: &Graph, beta: f64) -> Vec<f64> {
assert!(g.n <= 24, "exact enumeration limited to 24 spins");
let m = 1usize << g.n;
let mut p = vec![0.0f64; m];
let mut s = vec![-1i8; g.n];
let mut mx = f64::NEG_INFINITY;
for mask in 0..m {
for b in 0..g.n {
s[b] = if mask >> b & 1 == 1 { 1 } else { -1 };
}
let l = -beta * g.energy(&s);
p[mask] = l;
if l > mx {
mx = l;
}
}
let mut z = 0.0;
for v in p.iter_mut() {
*v = (*v - mx).exp();
z += *v;
}
for v in p.iter_mut() {
*v /= z;
}
p
}
pub fn onsager_m(beta: f64) -> f64 {
let s = (2.0 * beta).sinh();
let x = 1.0 - s.powi(-4);
if x <= 0.0 {
0.0
} else {
x.powf(1.0 / 8.0)
}
}
pub fn tv(p: &[f64], q: &[f64]) -> f64 {
assert_eq!(
p.len(),
q.len(),
"total variation needs two distributions over the SAME state space"
);
p.iter().zip(q).map(|(a, b)| (a - b).abs()).sum::<f64>() / 2.0
}
#[cfg(test)]
mod tests {
#[test]
fn chimera_has_the_vertices_and_edges_the_formula_says() {
for (m, n, tt) in [(1usize, 1usize, 4usize), (2, 3, 4), (4, 4, 4), (16, 16, 4), (3, 3, 2)] {
let g = chimera(m, n, tt, 1.0);
assert_eq!(g.n, 2 * tt * m * n, "C_{{{m},{n},{tt}}} vertices");
let want = m * n * tt * tt + (m - 1) * n * tt + m * (n - 1) * tt;
assert_eq!(g.n_edges, want, "C_{{{m},{n},{tt}}} edges");
}
let dw = chimera(16, 16, 4, 1.0);
assert_eq!(dw.n, 2048, "C_16,16,4 is 2048 qubits");
assert_eq!(dw.n_edges, 6016);
}
#[test]
fn chimera_degrees_are_t_plus_the_directions_a_qubit_is_not_on_the_edge_of() {
let (m, n, tt) = (4usize, 5usize, 4usize);
let g = chimera(m, n, tt, 1.0);
for i in 0..m {
for jj in 0..n {
for u in 0..2 {
for k in 0..tt {
let q = ((i * n) + jj) * 2 * tt + u * tt + k;
let deg = g.offset[q + 1] - g.offset[q];
let inter = if u == 0 {
usize::from(i > 0) + usize::from(i + 1 < m)
} else {
usize::from(jj > 0) + usize::from(jj + 1 < n)
};
assert_eq!(
deg,
tt + inter,
"({i},{jj},{u},{k}) index {q}: degree {deg}, expected {} + {inter}",
tt
);
}
}
}
}
}
#[test]
fn each_chimera_shore_induces_a_forest_and_the_two_cover_everything() {
let (m, n, tt) = (4usize, 5usize, 4usize);
let g = chimera(m, n, tt, 1.0);
let mut seen = vec![false; g.n];
for u in 0..2 {
let shore = chimera_shore(m, n, tt, u);
assert_eq!(shore.len(), tt * m * n, "a shore is half the graph");
let inside: std::collections::BTreeSet<usize> = shore.iter().copied().collect();
assert_eq!(inside.len(), shore.len(), "a shore is a set");
let mut edges = 0usize;
for &q in &shore {
seen[q] = true;
for k in g.offset[q]..g.offset[q + 1] {
let r = g.nbr[k] as usize;
if r > q && inside.contains(&r) {
edges += 1;
}
}
}
let components = if u == 0 { n * tt } else { m * tt };
assert_eq!(
edges,
shore.len() - components,
"shore {u}: {} vertices, {edges} edges, {components} paths is not a forest",
shore.len()
);
let mut map = vec![usize::MAX; g.n];
for (a, &q) in shore.iter().enumerate() {
map[q] = a;
}
let mut gb = GraphBuilder::new(shore.len());
for (a, &q) in shore.iter().enumerate() {
for k in g.offset[q]..g.offset[q + 1] {
let r = g.nbr[k] as usize;
if map[r] != usize::MAX && r > q {
gb.couple(a, map[r], g.w[k]);
}
}
}
assert_eq!(crate::exact::Elimination::default().width(&gb.build()), 1);
}
assert!(seen.iter().all(|&x| x), "the two shores must cover every vertex");
}
#[test]
fn a_chimera_glass_is_the_same_graph_with_signs() {
let plain = chimera(3, 3, 4, 1.0);
let g = chimera_glass(3, 3, 4, 11);
assert_eq!(g.n, plain.n);
assert_eq!(g.n_edges, plain.n_edges);
assert!(g.w.iter().all(|w| w.abs() == 1.0), "couplings are +/-1");
let pos = g.w.iter().filter(|w| **w > 0.0).count();
assert!(pos > 0 && pos < g.w.len(), "a glass is frustrated, not a ferromagnet: {pos}");
assert_eq!(g.w, chimera_glass(3, 3, 4, 11).w);
assert_ne!(g.w, chimera_glass(3, 3, 4, 12).w);
}
#[test]
fn a_degenerate_chimera_is_empty_rather_than_a_panic() {
assert_eq!(chimera(0, 4, 4, 1.0).n, 0);
assert_eq!(chimera(4, 0, 4, 1.0).n, 0);
assert_eq!(chimera(4, 4, 0, 1.0).n, 0);
assert!(chimera_shore(0, 4, 4, 0).is_empty());
assert!(chimera_shore(4, 4, 4, 2).is_empty(), "there are two shores");
let one = chimera(1, 1, 4, 1.0);
assert_eq!(one.n, 8);
assert_eq!(one.n_edges, 16);
}
use super::*;
#[test]
fn the_exact_reference_survives_the_betas_its_own_schedules_use() {
let g = ring(8, 1.0, 0.0);
for beta in [1.0, 3.0, 6.0, 8.0, 24.0, 200.0] {
let p = exact_boltzmann(&g, beta);
assert!(p.iter().all(|v| v.is_finite()), "beta={beta} produced a non-finite entry");
let sum: f64 = p.iter().sum();
assert!((sum - 1.0).abs() < 1e-12, "beta={beta} sums to {sum}, not 1");
}
let p = exact_boltzmann(&g, 20.0);
assert!((p[0] - 0.5).abs() < 1e-9, "all-down should carry half the mass, got {}", p[0]);
assert!((p[255] - 0.5).abs() < 1e-9, "all-up should carry half the mass, got {}", p[255]);
}
#[test]
#[should_panic(expected = "SAME state space")]
fn a_total_variation_between_different_sized_distributions_is_refused() {
let _ = tv(&[0.25, 0.25, 0.25, 0.25], &[0.5, 0.5]);
}
}