use crate::encode::{Encoding, Slot};
use crate::gibbs::Sampler;
use crate::graph::GraphBuilder;
use crate::schedule::Schedule;
pub struct Categorical {
pub slots: Vec<Slot>,
pub graph: crate::graph::Graph,
pub encoding: Encoding,
pub k: usize,
}
impl Categorical {
pub fn new(n: usize, k: usize, encoding: Encoding, p: f64) -> Categorical {
assert!(n >= 1 && k >= 2);
let width = encoding.spins(k);
let mut b = GraphBuilder::new(n * width);
let mut slots = Vec::with_capacity(n);
for v in 0..n {
let s = Slot::new(v * width, k, encoding);
s.add_penalty(&mut b, p);
slots.push(s);
}
Categorical { slots, graph: b.build(), encoding, k }
}
pub fn spins(&self) -> usize {
self.graph.n
}
pub fn feasible_fraction(&self, state: &[i8]) -> f64 {
let ok = self.slots.iter().filter(|s| s.decode(state).is_some()).count();
ok as f64 / self.slots.len() as f64
}
pub fn anneal_feasibility(&self, schedule: &Schedule, seed: u64) -> f64 {
let (best, _) = crate::tempering::anneal_scheduled(&self.graph, schedule, seed, None);
self.feasible_fraction(&best)
}
pub fn sample_feasibility(&self, beta: f64, sweeps: usize, seed: u64) -> f64 {
let mut smp = Sampler::new(&self.graph, beta, seed);
smp.sweeps(sweeps, None);
self.feasible_fraction(&smp.s)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ladder() -> Schedule {
Schedule::geometric(0.05, 8.0, 80, 30)
}
fn rate(k: usize, enc: Encoding, p: f64) -> f64 {
let n = 40;
(1..=8u64)
.map(|seed| Categorical::new(n, k, enc, p).anneal_feasibility(&ladder(), seed))
.sum::<f64>()
/ 8.0
}
#[test]
fn the_counting_argument_holds_in_the_layout() {
for k in 3..=12 {
let dw = Categorical::new(10, k, Encoding::DomainWall, 1.0);
let oh = Categorical::new(10, k, Encoding::OneHot, 1.0);
assert!(dw.spins() < oh.spins(), "k={k}: {} vs {}", dw.spins(), oh.spins());
let edges = |c: &Categorical| {
(0..c.graph.n).map(|i| c.graph.offset[i + 1] - c.graph.offset[i]).sum::<usize>() / 2
};
assert!(edges(&dw) < edges(&oh), "k={k}: {} vs {} couplings", edges(&dw), edges(&oh));
}
}
#[test]
fn both_encodings_can_be_satisfied_at_all() {
for enc in [Encoding::DomainWall, Encoding::OneHot] {
let f = rate(4, enc, 2.0);
assert!(f > 0.9, "{enc:?} only reached {f:.3} feasible");
}
}
#[test]
fn at_an_adequate_penalty_there_is_no_gap_at_all() {
for k in [8usize, 16, 32] {
assert_eq!(rate(k, Encoding::DomainWall, 2.0), 1.0, "k={k}");
assert_eq!(rate(k, Encoding::OneHot, 2.0), 1.0, "k={k}");
}
}
#[test]
fn domain_wall_tolerates_a_much_weaker_penalty() {
let smallest = |k: usize, e: Encoding| {
let mut p = 0.02f64;
while p < 4.0 {
if rate(k, e, p) >= 0.99 {
return p;
}
p *= 1.3;
}
f64::INFINITY
};
for k in [8usize, 32] {
let (dw, oh) = (smallest(k, Encoding::DomainWall), smallest(k, Encoding::OneHot));
assert!(dw.is_finite() && oh.is_finite(), "k={k}: neither encoding reached 0.99");
assert!(dw < oh / 2.0, "k={k}: domain wall {dw:.3} vs one-hot {oh:.3}");
}
}
#[test]
fn a_weak_penalty_fails_and_says_so_by_failing() {
let weak = rate(8, Encoding::OneHot, 0.01);
let strong = rate(8, Encoding::OneHot, 2.0);
assert!(weak < strong, "weak penalty {weak:.3} should lose to strong {strong:.3}");
}
#[test]
fn hot_sampling_is_infeasible_and_cold_sampling_is_not() {
let c = Categorical::new(40, 6, Encoding::DomainWall, 2.0);
let hot = c.sample_feasibility(0.02, 400, 1);
let cold = c.sample_feasibility(6.0, 400, 1);
assert!(cold > hot, "cold {cold:.3} should beat hot {hot:.3}");
assert!(hot < 0.9, "an essentially free penalty should let invalid states through");
}
#[test]
fn binary_is_exact_only_on_a_power_of_two() {
let eight = Categorical::new(60, 8, Encoding::Binary, 2.0);
assert_eq!(eight.feasible_fraction(&vec![1i8; eight.spins()]), 1.0);
let six = Categorical::new(60, 6, Encoding::Binary, 2.0);
let f = six.sample_feasibility(1.0, 300, 3);
assert!(f < 1.0, "k=6 binary must let surplus codes through, got {f:.3}");
}
}