use crate::graph::{Graph, GraphBuilder};
use std::cmp::Reverse;
use std::collections::{BTreeSet, BinaryHeap, VecDeque};
#[derive(Clone, Debug, PartialEq)]
pub struct Embedding {
pub chains: Vec<Vec<usize>>,
pub sites: usize,
}
impl Embedding {
pub fn longest_chain(&self) -> usize {
self.chains.iter().map(|c| c.len()).max().unwrap_or(0)
}
pub fn used(&self) -> usize {
self.chains.iter().map(|c| c.len()).sum()
}
pub fn verify(&self, logical: &Graph, hardware: &Graph) -> Result<(), String> {
let mut seen = vec![usize::MAX; hardware.n];
for (v, chain) in self.chains.iter().enumerate() {
if chain.is_empty() {
return Err(format!("variable {v} has no sites"));
}
for &s in chain {
if s >= hardware.n {
return Err(format!("variable {v} uses site {s}, past the {} the machine has", hardware.n));
}
if seen[s] != usize::MAX {
return Err(format!("site {s} is used by both {} and {v}", seen[s]));
}
seen[s] = v;
}
if !connected(chain, hardware) {
return Err(format!("variable {v}'s chain {chain:?} is not connected in the hardware"));
}
}
for i in 0..logical.n {
for k in logical.offset[i]..logical.offset[i + 1] {
let j = logical.nbr[k] as usize;
if j <= i {
continue;
}
if !touching(&self.chains[i], &self.chains[j], hardware) {
return Err(format!(
"variables {i} and {j} interact, and no site of one is adjacent to a site \
of the other"
));
}
}
}
Ok(())
}
}
fn connected(chain: &[usize], h: &Graph) -> bool {
if chain.len() <= 1 {
return true;
}
let set: BTreeSet<usize> = chain.iter().copied().collect();
let mut seen = BTreeSet::new();
let mut q = VecDeque::from([chain[0]]);
seen.insert(chain[0]);
while let Some(u) = q.pop_front() {
for k in h.offset[u]..h.offset[u + 1] {
let v = h.nbr[k] as usize;
if set.contains(&v) && seen.insert(v) {
q.push_back(v);
}
}
}
seen.len() == chain.len()
}
fn touching(a: &[usize], b: &[usize], h: &Graph) -> bool {
let bs: BTreeSet<usize> = b.iter().copied().collect();
a.iter().any(|&u| {
(h.offset[u]..h.offset[u + 1]).any(|k| bs.contains(&(h.nbr[k] as usize)))
})
}
pub fn embed(logical: &Graph, hardware: &Graph, seed: u64) -> Option<Embedding> {
embed_with(logical, hardware, seed, 20)
}
pub fn site_lower_bound(logical: &Graph, hardware: &Graph) -> usize {
let d = (0..hardware.n)
.map(|s| hardware.offset[s + 1] - hardware.offset[s])
.max()
.unwrap_or(0);
if d <= 2 {
return logical.n;
}
(0..logical.n)
.map(|v| {
let k = degree(logical, v);
if k <= d {
1
} else {
(k - 2).div_ceil(d - 2)
}
})
.sum()
}
pub const DEFAULT_SEARCH_BUDGET: u64 = 200_000;
pub fn embed_bounded(
logical: &Graph,
hardware: &Graph,
seed: u64,
rounds: usize,
budget: u64,
) -> Option<Embedding> {
embed_inner(logical, hardware, seed, rounds, budget)
}
pub fn embed_with(logical: &Graph, hardware: &Graph, seed: u64, rounds: usize) -> Option<Embedding> {
embed_inner(logical, hardware, seed, rounds, DEFAULT_SEARCH_BUDGET)
}
fn embed_inner(
logical: &Graph,
hardware: &Graph,
seed: u64,
rounds: usize,
budget: u64,
) -> Option<Embedding> {
let mut spent: u64 = 0;
if logical.n == 0 {
return Some(Embedding { chains: Vec::new(), sites: hardware.n });
}
if logical.n > hardware.n {
return None; }
if site_lower_bound(logical, hardware) > hardware.n {
return None;
}
let identity = Embedding {
chains: (0..logical.n).map(|i| vec![i]).collect(),
sites: hardware.n,
};
if identity.verify(logical, hardware).is_ok() {
return Some(identity);
}
let mut rng = Pcg::new(seed);
let mut order: Vec<usize> = (0..logical.n).collect();
order.sort_by_key(|&v| core::cmp::Reverse(degree(logical, v)));
let mut chains: Vec<Vec<usize>> = vec![Vec::new(); logical.n];
let mut best = (usize::MAX, usize::MAX);
let mut stale = 0usize;
for round in 0..=rounds {
if spent >= budget {
return None;
}
for idx in 0..order.len() {
let v = order[idx];
chains[v].clear();
let mut load = vec![0usize; hardware.n];
for (u, c) in chains.iter().enumerate() {
if u != v {
for &s in c {
load[s] += 1;
}
}
}
let neighbours: Vec<usize> = (logical.offset[v]..logical.offset[v + 1])
.map(|k| logical.nbr[k] as usize)
.filter(|&u| !chains[u].is_empty())
.collect();
let mut chain = if neighbours.is_empty() {
match seed_site(hardware, &load, &mut rng) {
Some(s) => vec![s],
None => continue,
}
} else {
match vertex_model(hardware, &neighbours, &chains, &load, &mut rng, &mut spent) {
Some(c) => c,
None => continue,
}
};
grow_to_fit(hardware, &mut chain, &load, degree(logical, v));
chains[v] = chain;
}
for v in 0..logical.n {
if chains[v].is_empty() {
continue;
}
let nbrs: Vec<usize> = (logical.offset[v]..logical.offset[v + 1])
.map(|k| logical.nbr[k] as usize)
.filter(|&u| !chains[u].is_empty())
.collect();
let mut c = core::mem::take(&mut chains[v]);
prune(hardware, &mut c, &nbrs, &chains);
chains[v] = c;
}
let mut load = vec![0usize; hardware.n];
for c in &chains {
for &s in c {
load[s] += 1;
}
}
let excess: usize = load.iter().map(|&n| n.saturating_sub(1)).sum();
let all_placed = chains.iter().all(|c| !c.is_empty());
if excess == 0 && all_placed {
let e = Embedding { chains: chains.clone(), sites: hardware.n };
if e.verify(logical, hardware).is_ok() {
return Some(e);
}
}
if round == rounds {
break;
}
let here = (excess, chains.iter().map(|c| c.len()).sum::<usize>());
if here < best {
best = here;
stale = 0;
} else {
stale += 1;
}
if stale >= STALL {
for c in chains.iter_mut() {
c.clear();
}
order.sort_by_key(|&v| core::cmp::Reverse(degree(logical, v)));
best = (usize::MAX, usize::MAX);
stale = 0;
} else {
shuffle(&mut order, &mut rng);
}
}
None
}
const STALL: usize = 6;
const OCCUPIED_BASE: u64 = 8;
fn degree(g: &Graph, v: usize) -> usize {
g.offset[v + 1] - g.offset[v]
}
fn site_cost(load: &[usize], s: usize) -> u64 {
1 + load[s] as u64 * OCCUPIED_BASE
}
fn seed_site(h: &Graph, load: &[usize], rng: &mut Pcg) -> Option<usize> {
(0..h.n).min_by_key(|&s| {
(load[s], core::cmp::Reverse(h.offset[s + 1] - h.offset[s]), rng.next() % 1024)
})
}
fn shuffle(order: &mut [usize], rng: &mut Pcg) {
for i in (1..order.len()).rev() {
let j = (rng.next() % (i as u64 + 1)) as usize;
order.swap(i, j);
}
}
fn vertex_model(
h: &Graph,
neighbours: &[usize],
chains: &[Vec<usize>],
load: &[usize],
rng: &mut Pcg,
spent: &mut u64,
) -> Option<Vec<usize>> {
let mut total = vec![0u64; h.n];
let mut parents: Vec<Vec<usize>> = Vec::with_capacity(neighbours.len());
for &u in neighbours {
*spent += 1;
let (dist, parent) = dijkstra(h, &chains[u], load);
for s in 0..h.n {
total[s] = total[s].saturating_add(dist[s]);
}
for &s in &chains[u] {
total[s] = total[s].saturating_add(site_cost(load, s));
}
parents.push(parent);
}
let jitter: Vec<u64> = (0..h.n).map(|_| rng.next() % 8).collect();
let mut ranked: Vec<usize> = (0..h.n).filter(|&s| total[s] < u64::MAX / 4).collect();
ranked.sort_unstable_by_key(|&s| (total[s], jitter[s]));
ranked.truncate(ROOT_CANDIDATES);
let mut best: Option<(u64, Vec<usize>)> = None;
for &root in &ranked {
let mut set = BTreeSet::new();
set.insert(root);
let mut reachable = true;
for parent in &parents {
if parent[root] == usize::MAX {
reachable = false; break;
}
let mut at = root;
while parent[at] != at {
let p = parent[at];
if parent[p] == p {
break; }
set.insert(p);
at = p;
}
}
if !reachable {
continue;
}
let mut chain: Vec<usize> = set.into_iter().collect();
prune(h, &mut chain, neighbours, chains);
let cost: u64 = chain.iter().map(|&s| site_cost(load, s)).sum();
if best.as_ref().is_none_or(|(c, _)| cost < *c) {
best = Some((cost, chain));
}
}
best.map(|(_, chain)| chain)
}
const ROOT_CANDIDATES: usize = 8;
fn prune(h: &Graph, chain: &mut Vec<usize>, neighbours: &[usize], chains: &[Vec<usize>]) {
let reaches = |c: &[usize], u: usize| -> bool {
touching(c, &chains[u], h) || c.iter().any(|s| chains[u].contains(s))
};
let before: Vec<bool> = neighbours.iter().map(|&u| reaches(chain, u)).collect();
let mut i = 0;
while i < chain.len() {
if chain.len() == 1 {
break;
}
let trial: Vec<usize> =
chain.iter().enumerate().filter(|&(k, _)| k != i).map(|(_, &s)| s).collect();
let keeps = connected(&trial, h)
&& neighbours
.iter()
.zip(&before)
.all(|(&u, &had)| !had || reaches(&trial, u));
if keeps {
*chain = trial;
i = 0; } else {
i += 1;
}
}
}
fn grow_to_fit(h: &Graph, chain: &mut Vec<usize>, load: &[usize], want: usize) {
let mut inside: BTreeSet<usize> = chain.iter().copied().collect();
loop {
let mut free: Vec<usize> = Vec::new();
for &s in &inside {
for k in h.offset[s]..h.offset[s + 1] {
let v = h.nbr[k] as usize;
if load[v] == 0 && !inside.contains(&v) && !free.contains(&v) {
free.push(v);
}
}
}
if free.len() >= want || free.is_empty() {
break;
}
let pick = free
.iter()
.copied()
.max_by_key(|&v| {
(h.offset[v]..h.offset[v + 1])
.filter(|&k| {
let w = h.nbr[k] as usize;
load[w] == 0 && !inside.contains(&w) && !free.contains(&w)
})
.count()
})
.expect("free is not empty");
inside.insert(pick);
}
*chain = inside.into_iter().collect();
}
fn dijkstra(h: &Graph, sources: &[usize], load: &[usize]) -> (Vec<u64>, Vec<usize>) {
let mut dist = vec![u64::MAX; h.n];
let mut parent = vec![usize::MAX; h.n];
let mut heap: BinaryHeap<Reverse<(u64, usize)>> = BinaryHeap::new();
for &s in sources {
dist[s] = 0;
parent[s] = s;
heap.push(Reverse((0, s)));
}
while let Some(Reverse((d, u))) = heap.pop() {
if d > dist[u] {
continue;
}
for k in h.offset[u]..h.offset[u + 1] {
let v = h.nbr[k] as usize;
let nd = d.saturating_add(site_cost(load, v));
if nd < dist[v] {
dist[v] = nd;
parent[v] = u;
heap.push(Reverse((nd, v)));
}
}
}
(dist, parent)
}
pub struct Embedded {
pub graph: Graph,
pub embedding: Embedding,
pub chain_strength: f64,
}
pub fn apply(logical: &Graph, hardware: &Graph, e: &Embedding) -> Embedded {
apply_with(logical, hardware, e, DEFAULT_CHAIN_MULTIPLE * worst_coefficient(logical))
}
pub const DEFAULT_CHAIN_MULTIPLE: f64 = 4.0;
pub fn worst_coefficient(logical: &Graph) -> f64 {
let worst = (0..logical.n)
.flat_map(|i| (logical.offset[i]..logical.offset[i + 1]).map(move |k| logical.w[k].abs()))
.chain(logical.h.iter().map(|x| x.abs()))
.fold(0.0f64, f64::max);
if worst > 0.0 {
worst
} else {
0.5
}
}
pub fn apply_with(
logical: &Graph,
hardware: &Graph,
e: &Embedding,
chain_strength: f64,
) -> Embedded {
let chain_strength = if chain_strength.is_finite() && chain_strength > 0.0 {
chain_strength
} else {
DEFAULT_CHAIN_MULTIPLE * worst_coefficient(logical)
};
let mut b = GraphBuilder::new(hardware.n);
for chain in &e.chains {
for a in 0..chain.len() {
for c in (a + 1)..chain.len() {
let (u, v) = (chain[a], chain[c]);
if (hardware.offset[u]..hardware.offset[u + 1])
.any(|k| hardware.nbr[k] as usize == v)
{
b.couple(u, v, chain_strength);
}
}
}
}
for i in 0..logical.n {
for k in logical.offset[i]..logical.offset[i + 1] {
let j = logical.nbr[k] as usize;
if j <= i {
continue;
}
let mut links = Vec::new();
for &u in &e.chains[i] {
for kk in hardware.offset[u]..hardware.offset[u + 1] {
let v = hardware.nbr[kk] as usize;
if e.chains[j].contains(&v) {
links.push((u, v));
}
}
}
if links.is_empty() {
continue; }
let share = logical.w[k] / links.len() as f64;
for (u, v) in links {
b.couple(u, v, share);
}
}
}
for i in 0..logical.n {
if logical.h[i] == 0.0 {
continue;
}
let share = logical.h[i] / e.chains[i].len() as f64;
for &s in &e.chains[i] {
b.bias(s, share);
}
}
Embedded { graph: b.build(), embedding: e.clone(), chain_strength }
}
pub fn unembed(e: &Embedding, state: &[i8]) -> (Vec<i8>, Vec<usize>) {
let mut out = vec![0i8; e.chains.len()];
let mut broken = Vec::new();
for (v, chain) in e.chains.iter().enumerate() {
let up = chain.iter().filter(|&&s| state.get(s).copied().unwrap_or(0) > 0).count();
let down = chain.len() - up;
if up != 0 && down != 0 {
broken.push(v);
}
out[v] = if up >= down { 1 } else { -1 };
}
(out, broken)
}
struct Pcg(u64);
impl Pcg {
fn new(seed: u64) -> Pcg {
Pcg(seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407))
}
fn next(&mut self) -> u64 {
self.0 = self.0.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
let x = self.0;
(x >> 33) ^ x
}
}
pub mod topology {
use super::*;
pub fn king(l: usize) -> Graph {
let mut b = GraphBuilder::new(l * l);
let at = |x: usize, y: usize| y * l + x;
for y in 0..l {
for x in 0..l {
for (dx, dy) in [(1i64, 0i64), (0, 1), (1, 1), (1, -1)] {
let (nx, ny) = (x as i64 + dx, y as i64 + dy);
if nx >= 0 && ny >= 0 && (nx as usize) < l && (ny as usize) < l {
b.couple(at(x, y), at(nx as usize, ny as usize), 1.0);
}
}
}
}
b.build()
}
pub fn grid(l: usize) -> Graph {
let mut b = GraphBuilder::new(l * l);
for y in 0..l {
for x in 0..l {
if x + 1 < l {
b.couple(y * l + x, y * l + x + 1, 1.0);
}
if y + 1 < l {
b.couple(y * l + x, (y + 1) * l + x, 1.0);
}
}
}
b.build()
}
pub fn complete(n: usize) -> Graph {
let mut b = GraphBuilder::new(n);
for i in 0..n {
for j in (i + 1)..n {
b.couple(i, j, 1.0);
}
}
b.build()
}
}
#[cfg(test)]
mod tests {
use super::*;
use topology::{complete, grid, king};
fn triangle() -> Graph {
let mut b = GraphBuilder::new(3);
b.couple(0, 1, 1.0);
b.couple(1, 2, 1.0);
b.couple(0, 2, 1.0);
b.build()
}
fn clique(n: usize, w: f64) -> Graph {
let mut b = GraphBuilder::new(n);
for i in 0..n {
for j in (i + 1)..n {
b.couple(i, j, w);
}
}
b.build()
}
#[test]
fn a_graph_embeds_into_itself_with_no_chains() {
let g = king(4);
let e = embed(&g, &king(4), 1).expect("a graph is its own minor");
assert_eq!(e.longest_chain(), 1, "nothing needs a chain");
e.verify(&g, &king(4)).unwrap();
}
#[test]
fn a_triangle_needs_no_chain_on_a_kings_graph_and_does_on_a_grid() {
let k = king(4);
let ek = embed(&triangle(), &k, 7).expect("a King's graph has triangles");
ek.verify(&triangle(), &k).unwrap();
assert_eq!(ek.longest_chain(), 1, "no chain needed: {:?}", ek.chains);
let g = grid(4);
let eg = embed(&triangle(), &g, 7).expect("a triangle is a minor of a 4x4 grid");
eg.verify(&triangle(), &g).unwrap();
assert!(eg.longest_chain() > 1, "a square grid has no triangle: {:?}", eg.chains);
}
#[test]
fn verify_rejects_an_embedding_that_is_not_one() {
let g = grid(4);
let broken = Embedding { chains: vec![vec![0, 15], vec![1], vec![2]], sites: g.n };
let e = broken.verify(&triangle(), &g).unwrap_err();
assert!(e.contains("not connected"), "{e}");
let shared = Embedding { chains: vec![vec![0], vec![0], vec![2]], sites: g.n };
let e = shared.verify(&triangle(), &g).unwrap_err();
assert!(e.contains("used by both"), "{e}");
let apart = Embedding { chains: vec![vec![0], vec![3], vec![12]], sites: g.n };
let e = apart.verify(&triangle(), &g).unwrap_err();
assert!(e.contains("interact"), "{e}");
}
#[test]
fn a_clique_embeds_into_a_kings_graph_with_chains() {
let k = king(8);
let c = clique(6, 1.0);
let e = embed(&c, &k, 3).expect("K6 is a minor of an 8x8 King's graph");
e.verify(&c, &k).unwrap();
assert!(e.used() >= 6);
}
#[test]
fn more_variables_than_sites_is_refused_immediately() {
assert_eq!(embed(&clique(10, 1.0), &grid(3), 1), None, "9 sites cannot hold 10 variables");
}
#[test]
fn an_embedded_program_has_the_same_ground_state_as_the_one_it_came_from() {
let mut b = GraphBuilder::new(4);
b.couple(0, 1, -1.0);
b.couple(1, 2, -1.0);
b.couple(2, 3, -1.0);
b.couple(0, 3, -1.0);
b.couple(0, 2, 1.0);
b.set_bias(0, 0.3);
b.set_bias(3, -0.2);
let logical = b.build();
let hw = grid(5);
let e = embed(&logical, &hw, 11).expect("a small graph fits a 5x5 grid");
e.verify(&logical, &hw).unwrap();
let emb = apply(&logical, &hw, &e);
let best = crate::exact::Elimination::default()
.ground_state(&emb.graph)
.expect("elimination")
.ground_state
.expect("a ground state");
let (values, broken) = unembed(&e, &best);
assert!(broken.is_empty(), "the chain coupling should hold at the optimum: {broken:?}");
let want = (0u32..(1 << logical.n))
.map(|m| {
let s: Vec<i8> =
(0..logical.n).map(|i| if m & (1 << i) != 0 { 1 } else { -1 }).collect();
(logical.energy(&s) * 1e9) as i64
})
.min()
.unwrap();
assert_eq!(
(logical.energy(&values) * 1e9) as i64,
want,
"unembedded {values:?} must minimise the logical model"
);
}
#[test]
fn a_broken_chain_is_reported_rather_than_resolved_silently() {
let e = Embedding { chains: vec![vec![0, 1, 2], vec![3]], sites: 4 };
let (v, broken) = unembed(&e, &[1, 1, -1, 1]);
assert_eq!(v, vec![1, 1], "majority is up");
assert_eq!(broken, vec![0], "and variable 0 is the one that disagreed");
let (_, none) = unembed(&e, &[1, 1, 1, -1]);
assert!(none.is_empty(), "an agreeing chain is not reported");
}
#[test]
fn a_star_that_needs_one_chain_is_placed() {
let hardware = crate::ising::chimera(8, 8, 4, 1.0);
let mut b = GraphBuilder::new(9);
for i in 1..9 {
b.couple(0, i, 1.0);
}
let star = b.build();
assert!(hardware.n > 50 * star.n, "the machine is nowhere near full");
for seed in 0..8 {
let e = embed(&star, &hardware, seed)
.unwrap_or_else(|| panic!("the star must embed, seed {seed}"));
e.verify(&star, &hardware).expect("and the embedding must be one");
assert!(e.longest_chain() >= 2, "the centre must be a chain: {:?}", e.chains);
assert!(e.used() <= 20, "about ten sites, not a sprawl: {:?}", e.chains);
}
let e = embed_with(&star, &hardware, 0, 0).expect("the first round already places it");
e.verify(&star, &hardware).unwrap();
}
#[test]
fn cliques_past_the_first_one_needing_a_chain_are_placed() {
let hardware = crate::ising::chimera(8, 8, 4, 1.0);
for n in [6usize, 8, 12] {
let c = clique(n, 1.0);
for seed in 0..4 {
let e = embed(&c, &hardware, seed)
.unwrap_or_else(|| panic!("K_{n} must embed, seed {seed}"));
e.verify(&c, &hardware).expect("and the embedding must be one");
assert!(e.longest_chain() > 1, "K_{n} cannot fit without a chain: {:?}", e.chains);
}
}
let c = clique(16, 1.0);
let ok = (0..8u64)
.filter(|&s| {
embed(&c, &hardware, s).map(|e| e.verify(&c, &hardware).is_ok()).unwrap_or(false)
})
.count();
assert!(ok >= 6, "K_16 embedded on only {ok} of 8 seeds");
}
#[test]
fn the_site_lower_bound_never_exceeds_an_embedding_it_admits() {
let machines = [
crate::ising::chimera(8, 8, 4, 1.0),
crate::ising::lattice2d(12, 1.0),
crate::ising::grid2d(10, 8, 1.0),
];
let clique = |n: usize| {
let mut b = GraphBuilder::new(n);
for i in 0..n {
for j in (i + 1)..n {
b.couple(i, j, 1.0);
}
}
b.build()
};
let star = |leaves: usize| {
let mut b = GraphBuilder::new(leaves + 1);
for i in 1..=leaves {
b.couple(0, i, 1.0);
}
b.build()
};
let path = |n: usize| {
let mut b = GraphBuilder::new(n);
for i in 0..n - 1 {
b.couple(i, i + 1, 1.0);
}
b.build()
};
let mut checked = 0;
for hw in &machines {
for g in [clique(4), clique(6), clique(8), clique(12), star(3), star(8), star(20),
path(5), path(40)] {
let lb = site_lower_bound(&g, hw);
for seed in 0..4u64 {
if let Some(e) = embed(&g, hw, seed) {
e.verify(&g, hw).expect("embed only returns verified embeddings");
assert!(
lb <= e.used(),
"the bound claims {lb} sites are needed and an embedding used {} \
-- the bound is UNSOUND and embed_with refuses on it",
e.used()
);
checked += 1;
}
}
}
}
assert!(checked > 30, "only {checked} embeddings to check the bound against");
}
#[test]
fn the_site_lower_bound_refuses_what_cannot_fit() {
let hw = crate::ising::chimera(8, 8, 4, 1.0);
let clique = |n: usize| {
let mut b = GraphBuilder::new(n);
for i in 0..n {
for j in (i + 1)..n {
b.couple(i, j, 1.0);
}
}
b.build()
};
assert_eq!(site_lower_bound(&clique(60), &hw), 900);
assert!(embed(&clique(60), &hw, 0).is_none());
assert!(site_lower_bound(&clique(100), &hw) > hw.n);
assert!(site_lower_bound(&clique(24), &hw) <= hw.n);
assert!(site_lower_bound(&clique(33), &hw) <= hw.n);
assert!(embed(&clique(24), &hw, 0).is_some());
let ring = crate::ising::ring(16, 1.0, 0.0);
assert_eq!(site_lower_bound(&clique(5), &ring), 5);
}
#[test]
fn chain_strength_outweighs_the_model_it_holds_together() {
let mut b = GraphBuilder::new(3);
b.couple(0, 1, 5.0);
b.couple(1, 2, 5.0);
b.couple(0, 2, 5.0);
let logical = b.build();
let hw = grid(4);
let e = embed(&logical, &hw, 5).unwrap();
let emb = apply(&logical, &hw, &e);
assert!(emb.chain_strength > 5.0, "a chain must outrank the couplings it carries: {}", emb.chain_strength);
}
#[test]
fn embedding_is_reproducible_from_its_seed() {
let c = clique(5, 1.0);
let k = king(6);
let a = embed(&c, &k, 42).unwrap();
let b = embed(&c, &k, 42).unwrap();
assert_eq!(a, b, "same seed, same placement");
}
#[test]
fn a_generous_machine_makes_it_easy() {
let c = clique(8, 1.0);
let e = embed(&c, &complete(8), 1).expect("K8 into K8");
assert_eq!(e.longest_chain(), 1);
e.verify(&c, &complete(8)).unwrap();
}
}