use comnum_core::{Error, Result};
use comnum_math::fft::{fft, ifft};
use comnum_math::{C64, bisect, entropy, qfunc};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RegularEnsemble {
pub dv: usize,
pub dc: usize,
}
impl RegularEnsemble {
pub fn new(dv: usize, dc: usize) -> Result<RegularEnsemble> {
if dv < 2 || dc <= dv {
return Err(Error::Config(format!("a regular ensemble needs 2 <= dv < dc, got ({dv}, {dc})")));
}
Ok(RegularEnsemble { dv, dc })
}
pub fn rate(&self) -> f64 {
1.0 - self.dv as f64 / self.dc as f64
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HardDecoder {
GallagerA,
GallagerB,
AlgorithmE,
}
fn binomial(n: usize, k: usize) -> f64 {
(0..k).fold(1.0, |c, i| c * (n - i) as f64 / (i + 1) as f64)
}
pub fn hard_decision_error(
ensemble: &RegularEnsemble,
decoder: HardDecoder,
eps: f64,
iterations: usize,
) -> Result<f64> {
if !(0.0..=0.5).contains(&eps) {
return Err(Error::Config(format!("the crossover probability must be in [0, 1/2], got {eps}")));
}
let (dv, dc) = (ensemble.dv, ensemble.dc);
match decoder {
HardDecoder::GallagerA | HardDecoder::GallagerB => {
let mut p = eps;
for _ in 0..iterations {
let x = (1.0 - 2.0 * p).powi(dc as i32 - 1);
let (agree, disagree) = ((1.0 + x) / 2.0, (1.0 - x) / 2.0);
let b = match decoder {
HardDecoder::GallagerA => dv - 1,
_ => (1..dv)
.find(|&b| {
let e = 2 * b as i32 - dv as i32 + 1;
e > 0 && (1.0 - eps) / eps <= (agree / disagree).powi(e)
})
.unwrap_or(dv - 1),
};
let tail = |u: f64, v: f64| {
(b..dv).map(|k| binomial(dv - 1, k) * u.powi(k as i32) * v.powi((dv - 1 - k) as i32)).sum::<f64>()
};
p = eps - eps * tail(agree, disagree) + (1.0 - eps) * tail(disagree, agree);
if p < 1e-300 {
return Ok(0.0);
}
}
Ok(p)
}
HardDecoder::AlgorithmE => Ok(algorithm_e(dv, dc, eps, iterations)),
}
}
fn algorithm_e(dv: usize, dc: usize, eps: f64, iterations: usize) -> f64 {
let n = dv - 1;
let (mut p1, mut pm, mut p0): (f64, f64, f64) = (1.0 - eps, eps, 0.0);
for _ in 0..iterations {
let (s, d) = ((p1 + pm).powi(dc as i32 - 1), (p1 - pm).powi(dc as i32 - 1));
let _ = p0;
let (q1, qm) = (((s + d) / 2.0).max(0.0), ((s - d) / 2.0).max(0.0));
let q0 = (1.0 - q1 - qm).max(0.0);
let mut best: Option<(f64, f64, f64, f64)> = None;
for w in 1..=dv as i64 {
let (mut n1, mut nm, mut n0) = (0.0, 0.0, 0.0);
for i in 0..=n {
for j in 0..=n - i {
let pr = binomial(n, i)
* binomial(n - i, j)
* q1.powi(i as i32)
* qm.powi(j as i32)
* q0.powi((n - i - j) as i32);
let diff = i as i64 - j as i64;
for (m0, pm0) in [(1i64, 1.0 - eps), (-1, eps)] {
let v = w * m0 + diff;
let mass = pr * pm0;
match v.signum() {
1 => n1 += mass,
-1 => nm += mass,
_ => n0 += mass,
}
}
}
}
let (n1, nm, n0): (f64, f64, f64) = (n1.max(0.0), nm.max(0.0), n0.max(0.0));
let cap = 1.0 - n0 + entropy(&[n0, (1.0 - n0).max(0.0)]) - entropy(&[n0, nm, n1]);
if best.is_none_or(|b| cap > b.0) {
best = Some((cap, n1, nm, n0));
}
}
let Some((_, a, b, c)) = best else { break };
let total = a + b + c;
(p1, pm, p0) = (a / total, b / total, c / total);
if pm + p0 / 2.0 < 1e-300 {
return 0.0;
}
}
pm + p0 / 2.0
}
pub fn hard_decision_threshold(ensemble: &RegularEnsemble, decoder: HardDecoder) -> Result<f64> {
let converges = |eps: f64| hard_decision_error(ensemble, decoder, eps, 100_000).is_ok_and(|p| p < 1e-12);
Ok(bisect(converges, 0.0, 0.5, 20))
}
pub fn correction_effect_threshold(ensemble: &RegularEnsemble) -> f64 {
let (t, tr) = (ensemble.dv as i32, ensemble.dc as i32);
let corrects = |f: f64| {
let pz = 0.5 - 0.5 * (1.0 - 2.0 * f).powi(tr - 1);
let (a, b) = (pz.powi(t), (1.0 - pz).powi(t));
a * (1.0 - f) - b * f < 0.0
};
bisect(corrects, 1e-12, 0.5, 60)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum BinaryInputChannel {
Bsc {
crossover: f64,
},
Biawgn {
sigma: f64,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct DensityEvolutionRun {
pub error: f64,
pub iterations: usize,
pub converged: bool,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BpDensityEvolution {
pub llr_step: f64,
pub llr_max: f64,
pub max_iterations: usize,
pub target: f64,
}
impl Default for BpDensityEvolution {
fn default() -> Self {
BpDensityEvolution { llr_step: 0.05, llr_max: 25.0, max_iterations: 3000, target: 1e-7 }
}
}
struct Grid {
half: usize,
table: Vec<u32>,
}
impl Grid {
fn new(step: f64, half: usize) -> Grid {
let n = 2 * half + 1;
let th: Vec<f64> = (0..n).map(|i| ((i as f64 - half as f64) * step / 2.0).tanh()).collect();
let mut table = vec![0u32; n * n];
for i in 0..n {
for j in 0..n {
let t = (th[i] * th[j]).clamp(-1.0, 1.0);
let m = 2.0 * t.atanh();
let k = if m.is_finite() { (m / step).round() } else { m.signum() * half as f64 };
table[i * n + j] = (k.clamp(-(half as f64), half as f64) + half as f64) as u32;
}
}
Grid { half, table }
}
fn len(&self) -> usize {
2 * self.half + 1
}
fn boxplus(&self, a: &[f64], b: &[f64]) -> Vec<f64> {
let n = self.len();
let mut out = vec![0.0; n];
for (i, &pa) in a.iter().enumerate() {
if pa == 0.0 {
continue;
}
let row = &self.table[i * n..(i + 1) * n];
for (&k, &pb) in row.iter().zip(b) {
out[k as usize] += pa * pb;
}
}
out
}
fn convolve(&self, parts: &[&[f64]]) -> Vec<f64> {
let n = self.len();
let full = parts.len() * (n - 1) + 1;
let size = full.next_power_of_two();
let mut acc = vec![C64::new(1.0, 0.0); size];
for p in parts {
let mut x = vec![C64::new(0.0, 0.0); size];
for (xi, &pi) in x.iter_mut().zip(p.iter()) {
*xi = C64::new(pi, 0.0);
}
for (a, f) in acc.iter_mut().zip(fft(&x)) {
*a *= f;
}
}
let y = ifft(&acc);
let offset = parts.len() * self.half;
let mut out = vec![0.0; n];
for (t, v) in y.iter().take(full).enumerate() {
let k = (t as i64 - offset as i64).clamp(-(self.half as i64), self.half as i64);
out[(k + self.half as i64) as usize] += v.re.max(0.0);
}
let total: f64 = out.iter().sum();
for v in &mut out {
*v /= total;
}
out
}
fn error(&self, p: &[f64]) -> f64 {
p[..self.half].iter().sum::<f64>() + 0.5 * p[self.half]
}
}
impl BpDensityEvolution {
fn channel(&self, channel: BinaryInputChannel) -> Result<(f64, usize, Vec<f64>)> {
if !(self.llr_step > 0.0 && self.llr_max > self.llr_step) {
return Err(Error::Config(format!(
"density evolution needs 0 < llr_step < llr_max, got {} and {}",
self.llr_step, self.llr_max
)));
}
match channel {
BinaryInputChannel::Bsc { crossover: eps } => {
if !(eps > 0.0 && eps < 0.5) {
return Err(Error::Config(format!("the crossover probability must be in (0, 1/2), got {eps}")));
}
let l0 = ((1.0 - eps) / eps).ln();
let bins = (l0 / self.llr_step).round().max(1.0);
let step = l0 / bins;
let half = (self.llr_max / step).round() as usize;
let half = half.max(bins as usize);
let mut p = vec![0.0; 2 * half + 1];
p[half + bins as usize] = 1.0 - eps;
p[half - bins as usize] = eps;
Ok((step, half, p))
}
BinaryInputChannel::Biawgn { sigma } => {
if !(sigma > 0.0 && sigma.is_finite()) {
return Err(Error::Config(format!("the noise standard deviation must be positive, got {sigma}")));
}
let (mu, sd) = (2.0 / (sigma * sigma), 2.0 / sigma);
let step = self.llr_step;
let half = (self.llr_max / step).round() as usize;
let cdf = |x: f64| 1.0 - qfunc((x - mu) / sd);
let p = (0..=2 * half)
.map(|i| {
let k = i as f64 - half as f64;
let lo = if i == 0 { f64::NEG_INFINITY } else { (k - 0.5) * step };
let hi = if i == 2 * half { f64::INFINITY } else { (k + 0.5) * step };
let c = |x: f64| if x.is_infinite() { if x > 0.0 { 1.0 } else { 0.0 } } else { cdf(x) };
(c(hi) - c(lo)).max(0.0)
})
.collect();
Ok((step, half, p))
}
}
}
pub fn run(&self, ensemble: &RegularEnsemble, channel: BinaryInputChannel) -> Result<DensityEvolutionRun> {
let (step, half, p0) = self.channel(channel)?;
let grid = Grid::new(step, half);
let mut p = p0.clone();
let mut last = grid.error(&p);
for it in 1..=self.max_iterations {
let mut q = p.clone();
for _ in 1..ensemble.dc - 1 {
q = grid.boxplus(&q, &p);
}
let mut parts: Vec<&[f64]> = vec![&p0];
parts.extend(std::iter::repeat_n(q.as_slice(), ensemble.dv - 1));
p = grid.convolve(&parts);
let e = grid.error(&p);
if e < self.target {
return Ok(DensityEvolutionRun { error: e, iterations: it, converged: true });
}
if e >= last - 1e-15 * last.max(1e-300) && it > 1 {
return Ok(DensityEvolutionRun { error: e, iterations: it, converged: false });
}
last = e;
}
Ok(DensityEvolutionRun { error: last, iterations: self.max_iterations, converged: false })
}
pub fn threshold(
&self,
ensemble: &RegularEnsemble,
channel: impl Fn(f64) -> BinaryInputChannel,
lo: f64,
hi: f64,
steps: usize,
) -> Result<f64> {
let mut err = None;
let t = bisect(
|x| match self.run(ensemble, channel(x)) {
Ok(r) => r.converged,
Err(e) => {
err = Some(e);
false
}
},
lo,
hi,
steps,
);
match err {
Some(e) => Err(e),
None => Ok(t),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_ensemble_needs_two_le_dv_lt_dc() {
assert!(RegularEnsemble::new(1, 6).is_err() && RegularEnsemble::new(6, 6).is_err());
assert!((RegularEnsemble::new(3, 6).unwrap().rate() - 0.5).abs() < 1e-15);
let e = RegularEnsemble::new(3, 6).unwrap();
assert!(hard_decision_error(&e, HardDecoder::GallagerA, 0.6, 10).is_err());
let bp = BpDensityEvolution::default();
assert!(bp.run(&e, BinaryInputChannel::Bsc { crossover: 0.0 }).is_err());
assert!(bp.run(&e, BinaryInputChannel::Biawgn { sigma: -1.0 }).is_err());
}
#[test]
fn gallager_a_and_b_coincide_for_dv_3_and_have_fixed_points_above_threshold() {
let e = RegularEnsemble::new(3, 6).unwrap();
for eps in [0.03, 0.05] {
let a = hard_decision_error(&e, HardDecoder::GallagerA, eps, 2000).unwrap();
let b = hard_decision_error(&e, HardDecoder::GallagerB, eps, 2000).unwrap();
assert_eq!(a, b);
}
assert_eq!(hard_decision_error(&e, HardDecoder::GallagerA, 0.0, 10).unwrap(), 0.0);
assert!(hard_decision_error(&e, HardDecoder::GallagerA, 0.05, 2000).unwrap() > 0.01);
}
#[test]
fn the_grid_operations_keep_the_mass_and_the_density_symmetry() {
let bp = BpDensityEvolution::default();
let (step, half, p0) = bp.channel(BinaryInputChannel::Biawgn { sigma: 0.9 }).unwrap();
let g = Grid::new(step, half);
let q = g.boxplus(&p0, &p0);
let v = g.convolve(&[&p0, &q, &q]);
for d in [&p0, &q, &v] {
assert!((d.iter().sum::<f64>() - 1.0).abs() < 1e-9);
}
for k in [5usize, 20, 40] {
let (pos, neg) = (q[half + k], q[half - k]);
let m = k as f64 * step;
assert!((neg - (-m).exp() * pos).abs() < 0.05 * pos, "bin {k}: {neg} against {}", (-m).exp() * pos);
}
}
#[test]
fn bp_density_evolution_converges_well_below_the_threshold_only() {
let e = RegularEnsemble::new(3, 6).unwrap();
let bp = BpDensityEvolution { llr_step: 0.1, ..BpDensityEvolution::default() };
let good = bp.run(&e, BinaryInputChannel::Biawgn { sigma: 0.7 }).unwrap();
assert!(good.converged && good.iterations < 30, "{good:?}");
let bad = bp.run(&e, BinaryInputChannel::Biawgn { sigma: 1.0 }).unwrap();
assert!(!bad.converged && bad.error > 0.05, "{bad:?}");
}
#[test]
fn algorithm_b_and_e_improve_on_a_as_in_table_i() {
let e48 = RegularEnsemble::new(4, 8).unwrap();
let a = hard_decision_threshold(&e48, HardDecoder::GallagerA).unwrap();
let b = hard_decision_threshold(&e48, HardDecoder::GallagerB).unwrap();
let ee = hard_decision_threshold(&e48, HardDecoder::AlgorithmE).unwrap();
assert!((a - 0.0476).abs() < 2e-4 && (b - 0.0517).abs() < 2e-4 && (ee - 0.0595).abs() < 2e-4, "{a} {b} {ee}");
let e36 = RegularEnsemble::new(3, 6).unwrap();
let ee = hard_decision_threshold(&e36, HardDecoder::AlgorithmE).unwrap();
assert!((0.070..0.0715).contains(&ee), "{ee}");
assert!(ee < 0.11);
}
}