use crate::chacha::{StdRng, Uniform};
use crate::relay_num::RelayNum;
#[derive(Clone, Debug, PartialEq)]
pub struct RelayConfig {
pub pre_iter: usize,
pub legs: usize,
pub leg_iter: usize,
pub solutions: Option<usize>,
pub stop_after_first: bool,
pub gamma0: Option<f64>,
pub gamma_range: (f64, f64),
pub gammas: Option<Vec<Vec<f64>>>,
pub alpha: Option<f64>,
pub alpha_scaling: f64,
pub data_scale: Option<f64>,
pub max_data: Option<f64>,
pub logging: bool,
pub seed: u64,
}
impl Default for RelayConfig {
fn default() -> Self {
RelayConfig {
pre_iter: 80,
legs: 300,
leg_iter: 60,
solutions: Some(1),
stop_after_first: false,
gamma0: Some(0.1),
gamma_range: (-0.24, 0.66),
gammas: None,
alpha: None,
alpha_scaling: 1.0,
data_scale: None,
max_data: None,
logging: false,
seed: 0,
}
}
}
#[derive(Clone)]
pub struct RelayCore<N: RelayNum> {
pub num_checks: usize,
pub num_vars: usize,
var_start: Vec<u32>,
edge_var: Vec<u32>,
chk_start: Vec<u32>,
chk_edges: Vec<u32>,
parity: Vec<u8>,
prior: Vec<N>,
weight: Vec<f64>,
data_scale: Option<N>,
max_data: Option<N>,
pub config: RelayConfig,
uniform: Uniform,
}
pub type Relay = RelayCore<f64>;
pub type RelayWork = RelayCoreWork<f64>;
#[derive(Clone)]
pub struct RelayCoreWork<N: RelayNum> {
v2c: Vec<N>,
c2v: Vec<N>,
posterior: Vec<N>,
gamma: Vec<N>,
hard: Vec<u8>,
decoded: Vec<u8>,
rng: StdRng,
pub correction: Vec<u8>,
pub decoded_detectors: Vec<u8>,
pub posterior_ratios: Vec<f64>,
log_iter: Vec<usize>,
log_conv: Vec<bool>,
log_best: Vec<bool>,
log_legs: usize,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RelayOutcome {
pub converged: bool,
pub iterations: usize,
pub legs: usize,
pub weight: f64,
pub max_iter: usize,
}
impl<N: RelayNum> RelayCore<N> {
pub fn new(num_checks: usize, columns: &[Vec<u32>], priors: &[f64], config: RelayConfig) -> Result<RelayCore<N>, String> {
Self::with_values(num_checks, columns, None, priors, config)
}
pub fn with_values(num_checks: usize, columns: &[Vec<u32>], values: Option<&[Vec<u8>]>, priors: &[f64], config: RelayConfig) -> Result<RelayCore<N>, String> {
if columns.len() != priors.len() {
return Err(format!("{} columns but {} priors", columns.len(), priors.len()));
}
if let Some(vals) = values {
if vals.len() != columns.len() || vals.iter().zip(columns).any(|(a, b)| a.len() != b.len()) {
return Err("one stored value per check-matrix entry".to_string());
}
}
let (lo, hi) = config.gamma_range;
if !(lo < hi && lo.is_finite() && hi.is_finite()) {
return Err(format!("the memory strengths' range must be finite and increasing, not ({lo}, {hi})"));
}
if let Some(g) = &config.gammas {
if g.is_empty() || g.iter().any(|row| row.len() != columns.len()) {
return Err(format!("explicit memory strengths need rows of {} (one per fault)", columns.len()));
}
}
let mut var_start = vec![0u32];
let mut edge_check = Vec::new();
let mut edge_var = Vec::new();
let mut degree = vec![0u32; num_checks];
let mut parity = Vec::new();
for (v, col) in columns.iter().enumerate() {
let mut entries: Vec<(u32, u8)> = match values {
Some(vals) => col.iter().copied().zip(vals[v].iter().map(|&x| x & 1)).collect(),
None => col.iter().map(|&c| (c, 1)).collect(),
};
entries.sort_unstable_by_key(|&(c, _)| c);
let mut merged: Vec<(u32, u8)> = Vec::with_capacity(entries.len());
for (c, p) in entries {
match merged.last_mut() {
Some(last) if last.0 == c => {
if values.is_some() {
last.1 ^= p;
}
}
_ => merged.push((c, p)),
}
}
for &(c, p) in &merged {
if c as usize >= num_checks {
return Err(format!("fault {v} sets off check {c}, beyond {num_checks}"));
}
edge_check.push(c);
edge_var.push(v as u32);
if values.is_some() {
parity.push(p);
}
degree[c as usize] += 1;
}
var_start.push(edge_check.len() as u32);
}
if parity.iter().all(|&p| p == 1) {
parity.clear();
}
let mut chk_start = vec![0u32; num_checks + 1];
for c in 0..num_checks {
chk_start[c + 1] = chk_start[c] + degree[c];
}
let mut fill: Vec<u32> = chk_start[..num_checks].to_vec();
let mut chk_edges = vec![0u32; edge_check.len()];
for (e, &c) in edge_check.iter().enumerate() {
chk_edges[fill[c as usize] as usize] = e as u32;
fill[c as usize] += 1;
}
let convert = |x: f64, what: &str| N::from_f64(x).ok_or_else(|| format!("{what} {x} does not fit this decoder's number type"));
let max_data = config.max_data.and_then(N::from_f64);
let data_scale = config.data_scale.and_then(N::from_f64);
let llr: Vec<f64> = priors.iter().map(|&p| ((1.0 - p) / p).ln()).collect();
let mut prior = Vec::with_capacity(llr.len());
for &x in &llr {
let v = if x == f64::INFINITY { N::max_value() } else { convert(config.data_scale.map_or(x, |s| s * x), "the log prior ratio")? };
prior.push(max_data.map_or(v, |m| bound(v, m)));
}
let g0 = config.gamma0.unwrap_or(0.0);
convert(config.data_scale.map_or(g0, |s| s * g0), "the memory strength")?;
let weight = llr.iter().map(|&x| if x.is_finite() { x } else { 0.0 }).collect();
let uniform = Uniform::new(lo, hi);
Ok(RelayCore { num_checks, num_vars: columns.len(), var_start, edge_var, chk_start, chk_edges, parity, prior, weight, data_scale, max_data, config, uniform })
}
pub fn priors(&self) -> Vec<f64> {
self.prior.iter().zip(&self.weight).map(|(p, &w)| if *p == N::max_value() { 0.0 } else { 1.0 / (1.0 + w.exp()) }).collect()
}
pub fn set_prior(&mut self, v: usize, p: f64) {
let x = ((1.0 - p) / p).ln();
let scaled = if x == f64::INFINITY { N::max_value() } else { N::from_f64(self.config.data_scale.map_or(x, |s| s * x)).unwrap_or_else(N::max_value) };
self.prior[v] = self.max_data.map_or(scaled, |m| bound(scaled, m));
self.weight[v] = if x.is_finite() { x } else { 0.0 };
}
pub fn work(&self) -> RelayCoreWork<N> {
self.work_seeded(self.config.seed)
}
pub(crate) fn work_seeded(&self, seed: u64) -> RelayCoreWork<N> {
let m = self.edge_var.len();
let sets = if self.config.logging { self.config.legs + 1 } else { 0 };
RelayCoreWork {
v2c: vec![N::zero(); m],
c2v: vec![N::zero(); m],
posterior: if self.config.gamma0.is_some() { self.prior.clone() } else { vec![N::zero(); self.num_vars] },
gamma: vec![N::zero(); self.num_vars],
hard: vec![0; self.num_vars],
decoded: vec![0; self.num_checks],
rng: StdRng::seed_from_u64(seed),
correction: vec![0; self.num_vars],
decoded_detectors: vec![0; self.num_checks],
posterior_ratios: vec![0.0; self.num_vars],
log_iter: vec![self.config.leg_iter; sets],
log_conv: vec![false; sets],
log_best: vec![false; sets],
log_legs: 0,
}
}
pub(crate) fn reseed(&self, w: &mut RelayCoreWork<N>, seed: u64) {
w.rng = StdRng::seed_from_u64(seed);
}
fn num(&self, x: f64) -> N {
N::from_f64(x).unwrap_or_else(|| panic!("{x} does not fit this decoder's number type"))
}
fn scaled(&self, x: f64) -> N {
self.num(self.config.data_scale.map_or(x, |s| s * x))
}
fn alpha(&self, iteration: usize) -> N {
let mut a = match self.config.alpha {
Some(0.0) => 1.0 - 2f64.powf(-((iteration + 1) as f64 / self.config.alpha_scaling)),
Some(v) => v,
None => 1.0,
};
if a < 0.0 {
a = 1.0;
}
self.scaled(a)
}
fn reset_messages(&self, w: &mut RelayCoreWork<N>) {
w.c2v.fill(N::zero());
for v in 0..self.num_vars {
for e in self.var_start[v] as usize..self.var_start[v + 1] as usize {
w.v2c[e] = self.prior[v];
}
}
}
fn iterate(&self, syndrome: &[u8], iteration: usize, w: &mut RelayCoreWork<N>) {
let alpha = self.alpha(iteration);
for c in 0..self.num_checks {
let row = &self.chk_edges[self.chk_start[c] as usize..self.chk_start[c + 1] as usize];
let mut sign = syndrome[c] == 1;
let (mut min_var, mut min, mut second) = (0u32, N::max_value(), N::max_value());
for &e in row {
let m = w.v2c[e as usize];
sign ^= m.is_negative();
let a = m.abs();
if a <= min {
second = min;
min = a;
min_var = self.edge_var[e as usize];
} else if a <= second {
second = a;
}
}
for &e in row {
let e = e as usize;
let s = sign ^ w.v2c[e].is_negative();
let m = if self.edge_var[e] != min_var { min } else { second };
let msg = alpha.mul(m);
w.c2v[e] = if s { msg.neg() } else { msg };
}
}
if let Some(scale) = self.data_scale {
for x in w.c2v.iter_mut() {
*x = x.div(scale);
}
}
let memory = self.config.gamma0.is_some();
let one = self.data_scale.unwrap_or(N::one());
for v in 0..self.num_vars {
let mut sum = if !memory || self.prior[v] == N::max_value() {
self.prior[v]
} else {
let g = w.gamma[v];
self.prior[v].div(one).mul(one.add(g.neg())).add(w.posterior[v].div(one).mul(g))
};
let edges = self.var_start[v] as usize..self.var_start[v + 1] as usize;
for e in edges.clone() {
w.v2c[e] = sum;
sum = sum.add(w.c2v[e]);
}
w.posterior[v] = sum;
let mut sum = N::zero();
for e in edges.rev() {
w.v2c[e] = w.v2c[e].add(sum);
sum = sum.add(w.c2v[e]);
}
}
if let Some(m) = self.max_data {
for x in w.v2c.iter_mut() {
*x = bound(*x, m);
}
for x in w.posterior.iter_mut() {
*x = bound(*x, m);
}
}
for v in 0..self.num_vars {
w.hard[v] = u8::from(w.posterior[v] <= N::zero());
}
}
fn explains(&self, syndrome: &[u8], w: &mut RelayCoreWork<N>) -> bool {
for c in 0..self.num_checks {
let mut x = 0u8;
let row = &self.chk_edges[self.chk_start[c] as usize..self.chk_start[c + 1] as usize];
if self.parity.is_empty() {
for &e in row {
x ^= w.hard[self.edge_var[e as usize] as usize];
}
} else {
for &e in row {
x ^= w.hard[self.edge_var[e as usize] as usize] & self.parity[e as usize];
}
}
w.decoded[c] = x;
}
w.decoded.iter().zip(syndrome).all(|(&a, &b)| a == b)
}
fn leg(&self, syndrome: &[u8], max_iter: usize, w: &mut RelayCoreWork<N>) -> (bool, usize) {
if max_iter == 0 {
w.decoded.fill(0);
return (false, 0);
}
for it in 0..max_iter {
self.iterate(syndrome, it, w);
if self.explains(syndrome, w) {
return (true, it + 1);
}
}
(false, max_iter)
}
fn weight_of(&self, hard: &[u8]) -> f64 {
hard.iter().zip(&self.weight).filter(|(&h, _)| h == 1).map(|(_, &x)| x).sum()
}
pub fn quality(&self, error: &[u8]) -> f64 {
self.weight_of(error)
}
fn keep(&self, w: &mut RelayCoreWork<N>) {
w.correction.copy_from_slice(&w.hard);
w.decoded_detectors.copy_from_slice(&w.decoded);
let unscale = self.config.data_scale;
for (out, &p) in w.posterior_ratios.iter_mut().zip(&w.posterior) {
let x = p.to_f64();
*out = unscale.map_or(x, |s| x / s);
}
}
fn start(&self, w: &mut RelayCoreWork<N>) {
let gamma0 = self.scaled(self.config.gamma0.unwrap_or(0.0));
w.gamma.fill(gamma0);
self.reset_messages(w);
if self.config.gamma0.is_some() {
w.posterior.copy_from_slice(&self.prior);
}
}
pub fn decode_min_sum(&self, syndrome: &[u8], w: &mut RelayCoreWork<N>) -> RelayOutcome {
assert_eq!(syndrome.len(), self.num_checks, "one syndrome bit per check");
self.start(w);
let (ok, its) = self.leg(syndrome, self.config.pre_iter, w);
self.keep(w);
let weight = if ok { self.weight_of(&w.hard) } else { f64::MAX };
RelayOutcome { converged: ok, iterations: its, legs: 1, weight, max_iter: self.config.pre_iter }
}
pub fn decode(&self, syndrome: &[u8], w: &mut RelayCoreWork<N>) -> RelayOutcome {
assert_eq!(syndrome.len(), self.num_checks, "one syndrome bit per check");
let logging = self.config.logging;
self.start(w);
let (ok, its) = self.leg(syndrome, self.config.pre_iter, w);
self.keep(w);
w.log_legs = 1;
if logging {
w.log_iter[0] = its;
w.log_conv[0] = ok;
}
let mut best = if ok { self.weight_of(&w.hard) } else { f64::MAX };
let mut converged = usize::from(ok);
let mut best_count = usize::from(ok);
let mut best_leg = 0;
let mut out = RelayOutcome { converged: ok, iterations: its, legs: 1, weight: best, max_iter: self.config.pre_iter };
if ok && (self.config.stop_after_first || self.config.solutions.is_some_and(|k| converged >= k)) {
if logging {
w.log_best[0] = true;
}
return out;
}
let stop = if self.config.stop_after_first { None } else { self.config.solutions };
let mut total = its;
for leg in 1..=self.config.legs {
match &self.config.gammas {
Some(rows) => {
let row = &rows[leg % rows.len()];
for (g, &x) in w.gamma.iter_mut().zip(row) {
*g = self.scaled(x);
}
}
None => {
for i in 0..self.num_vars {
let x = self.uniform.sample(&mut w.rng);
w.gamma[i] = self.scaled(x);
}
}
}
self.reset_messages(w);
let (ok, its) = self.leg(syndrome, self.config.leg_iter, w);
total += its;
out.legs += 1;
w.log_legs += 1;
if ok {
converged += 1;
let q = self.weight_of(&w.hard);
if logging {
w.log_conv[leg] = true;
w.log_iter[leg] = its;
}
if q == best {
best_count += 1;
}
if q < best {
best_count = 1;
best_leg = leg;
best = q;
self.keep(w);
out.converged = true;
out.weight = q;
out.max_iter = self.config.leg_iter;
}
if stop.is_some_and(|k| converged >= k) {
break;
}
}
}
if logging && best_count == 1 {
w.log_best[best_leg] = true;
}
out.iterations = total;
out
}
}
impl<N: RelayNum> RelayCoreWork<N> {
pub fn copy_from(&mut self, other: &RelayCoreWork<N>) {
let RelayCoreWork { v2c, c2v, posterior, gamma, hard, decoded, rng, correction, decoded_detectors, posterior_ratios, log_iter, log_conv, log_best, log_legs } = other;
self.v2c.clone_from(v2c);
self.c2v.clone_from(c2v);
self.posterior.clone_from(posterior);
self.gamma.clone_from(gamma);
self.hard.clone_from(hard);
self.decoded.clone_from(decoded);
self.rng.clone_from(rng);
self.correction.clone_from(correction);
self.decoded_detectors.clone_from(decoded_detectors);
self.posterior_ratios.clone_from(posterior_ratios);
self.log_iter.clone_from(log_iter);
self.log_conv.clone_from(log_conv);
self.log_best.clone_from(log_best);
self.log_legs = *log_legs;
}
pub fn log_lines(&self) -> Vec<String> {
(0..self.log_legs.min(self.log_iter.len())).map(|s| format!("{}, {}, {}, {}", s as i64 - 1, self.log_iter[s], self.log_conv[s] as u8, self.log_best[s] as u8)).collect()
}
}
fn bound<N: RelayNum>(value: N, max: N) -> N {
if value < max.neg() {
max.neg()
} else if value > max {
max
} else {
value
}
}