comnum_fec/density_evolution.rs
1//! Density evolution of regular LDPC ensembles under message passing (Richardson &
2//! Urbanke 2001): the expected fraction of incorrect messages on a cycle-free Tanner
3//! graph, iteration after iteration, and the threshold of the channel parameter below
4//! which it tends to zero.
5//!
6//! For the $(d_{v}, d_{c})$-regular ensemble, an output-symmetric binary-input channel
7//! and a symmetric decoder, the all-one codeword may be assumed (Lemma 1 of the paper)
8//! and the densities of the messages evolve deterministically:
9//!
10//! - **hard decisions on the BSC** ([`hard_decision_error`], [`hard_decision_threshold`]):
11//! Gallager's algorithm A, eq. (6),
12//! $$p^{(\ell)} = \epsilon - \epsilon \left[\frac{1 + (1 - 2p^{(\ell-1)})^{d_{c}-1}}{2}\right]^{d_{v}-1} + (1 - \epsilon) \left[\frac{1 - (1 - 2p^{(\ell-1)})^{d_{c}-1}}{2}\right]^{d_{v}-1},$$
13//! algorithm B (Example 4: the received bit is flipped when at least $b_{\ell}$ of the
14//! $d_{v} - 1$ incoming check messages disagree, $b_{\ell}$ chosen by Gallager's rule),
15//! and algorithm E (Example 5: ternary messages $\{-1, 0, 1\}$, the received bit weighted
16//! by $w^{(\ell)}$ chosen to maximise the capacity (7) of the induced binary symmetric
17//! erasure channel);
18//! - **belief propagation** ([`BpDensityEvolution`]): the densities of the
19//! log-likelihood ratios on a uniform grid; at a variable node the channel density
20//! convolved with $d_{v} - 1$ check densities (eq. (9), by FFT), at a check node the
21//! $d_{c} - 1$ variable densities combined by $m = 2 \operatorname{atanh}\left(\tanh\frac{a}{2} \tanh\frac{b}{2}\right)$
22//! pair by pair on the grid (the discretized density evolution of Chung, Forney,
23//! Richardson & Urbanke, *IEEE Commun. Lett.* 5(2), 2001; the paper computes the same
24//! density with the transform of eq. (8)). An LLR of zero counts as half an error.
25//!
26//! The threshold is found by bisection ([`comnum_math::bisect`]) of the predicate "the
27//! fraction of incorrect messages falls below the target". With a finite grid and a
28//! finite number of iterations the computed threshold is an estimate; Richardson &
29//! Urbanke give theirs rounded down (Tables I and II).
30//!
31//! # Examples
32//! ```
33//! use comnum_fec::density_evolution::{hard_decision_threshold, HardDecoder, RegularEnsemble};
34//! // Gallager A on the (3, 6)-regular ensemble: 0.0395 (Table I prints 0.04)
35//! let e = RegularEnsemble::new(3, 6)?;
36//! let t = hard_decision_threshold(&e, HardDecoder::GallagerA)?;
37//! assert!((t - 0.0395).abs() < 5e-4, "{t}");
38//! # Ok::<(), comnum_core::Error>(())
39//! ```
40//!
41//! # References
42//! 1. T. J. Richardson, R. L. Urbanke, "The capacity of low-density parity-check codes
43//! under message-passing decoding," *IEEE Trans. Inf. Theory* 47(2), 599–618, 2001:
44//! eq. (6), Examples 3–5, 10–11, Tables I–II.
45//! 2. S.-Y. Chung, G. D. Forney, T. J. Richardson, R. Urbanke, "On the design of
46//! low-density parity-check codes within 0.0045 dB of the Shannon limit," *IEEE Commun.
47//! Lett.* 5(2), 58–60, 2001: the discretized density evolution of the check node.
48//! 3. D. J. C. MacKay, "Good error-correcting codes based on very sparse matrices,"
49//! *IEEE Trans. Inf. Theory* 45(2), 399–431, 1999: eqs. (44), (54)–(57).
50
51use comnum_core::{Error, Result};
52use comnum_math::fft::{fft, ifft};
53use comnum_math::{C64, bisect, entropy, qfunc};
54
55/// The $(d_{v}, d_{c})$-regular LDPC ensemble: every variable node of degree $d_{v}$,
56/// every check node of degree $d_{c}$, design rate $1 - d_{v}/d_{c}$.
57#[derive(Clone, Copy, Debug, PartialEq, Eq)]
58pub struct RegularEnsemble {
59 /// Variable-node degree $d_{v}$.
60 pub dv: usize,
61 /// Check-node degree $d_{c}$.
62 pub dc: usize,
63}
64
65impl RegularEnsemble {
66 /// The ensemble of degrees `dv` and `dc`.
67 ///
68 /// # Errors
69 /// [`Error::Config`] unless $2 \le d_{v} < d_{c}$.
70 pub fn new(dv: usize, dc: usize) -> Result<RegularEnsemble> {
71 if dv < 2 || dc <= dv {
72 return Err(Error::Config(format!("a regular ensemble needs 2 <= dv < dc, got ({dv}, {dc})")));
73 }
74 Ok(RegularEnsemble { dv, dc })
75 }
76
77 /// The design rate $1 - d_{v}/d_{c}$.
78 pub fn rate(&self) -> f64 {
79 1.0 - self.dv as f64 / self.dc as f64
80 }
81}
82
83/// A hard-decision message-passing decoder on the binary symmetric channel.
84#[derive(Clone, Copy, Debug, PartialEq, Eq)]
85pub enum HardDecoder {
86 /// Gallager's algorithm A (Example 3, eq. (6)).
87 GallagerA,
88 /// Gallager's algorithm B (Example 4).
89 GallagerB,
90 /// The ternary decoder with erasures in the messages (Example 5).
91 AlgorithmE,
92}
93
94/// The binomial coefficient as a float.
95fn binomial(n: usize, k: usize) -> f64 {
96 (0..k).fold(1.0, |c, i| c * (n - i) as f64 / (i + 1) as f64)
97}
98
99/// The fraction of incorrect variable-to-check messages after `iterations` iterations of
100/// `decoder` on the BSC of crossover probability `eps` (an erasure counts as half an
101/// error), on a cycle-free graph of the ensemble.
102///
103/// # Errors
104/// [`Error::Config`] unless $0 \le \epsilon \le 1/2$.
105pub fn hard_decision_error(
106 ensemble: &RegularEnsemble,
107 decoder: HardDecoder,
108 eps: f64,
109 iterations: usize,
110) -> Result<f64> {
111 if !(0.0..=0.5).contains(&eps) {
112 return Err(Error::Config(format!("the crossover probability must be in [0, 1/2], got {eps}")));
113 }
114 let (dv, dc) = (ensemble.dv, ensemble.dc);
115 match decoder {
116 HardDecoder::GallagerA | HardDecoder::GallagerB => {
117 let mut p = eps;
118 for _ in 0..iterations {
119 let x = (1.0 - 2.0 * p).powi(dc as i32 - 1);
120 let (agree, disagree) = ((1.0 + x) / 2.0, (1.0 - x) / 2.0);
121 // b: the number of disagreeing checks that flips the received bit; A: all
122 // of them; B: the smallest b of Gallager's rule (Example 4)
123 let b = match decoder {
124 HardDecoder::GallagerA => dv - 1,
125 _ => (1..dv)
126 .find(|&b| {
127 let e = 2 * b as i32 - dv as i32 + 1;
128 e > 0 && (1.0 - eps) / eps <= (agree / disagree).powi(e)
129 })
130 .unwrap_or(dv - 1),
131 };
132 let tail = |u: f64, v: f64| {
133 (b..dv).map(|k| binomial(dv - 1, k) * u.powi(k as i32) * v.powi((dv - 1 - k) as i32)).sum::<f64>()
134 };
135 p = eps - eps * tail(agree, disagree) + (1.0 - eps) * tail(disagree, agree);
136 if p < 1e-300 {
137 return Ok(0.0);
138 }
139 }
140 Ok(p)
141 }
142 HardDecoder::AlgorithmE => Ok(algorithm_e(dv, dc, eps, iterations)),
143 }
144}
145
146/// Algorithm E (Example 5): the probabilities $(p_{1}, p_{-1}, p_{0})$ of a correct, an
147/// incorrect and an erased variable-to-check message, iterated; returns $p_{-1} + p_{0}/2$.
148fn algorithm_e(dv: usize, dc: usize, eps: f64, iterations: usize) -> f64 {
149 let n = dv - 1;
150 let (mut p1, mut pm, mut p0): (f64, f64, f64) = (1.0 - eps, eps, 0.0);
151 for _ in 0..iterations {
152 // check node
153 let (s, d) = ((p1 + pm).powi(dc as i32 - 1), (p1 - pm).powi(dc as i32 - 1));
154 // q0 = 1 - (1 - p0)^(dc - 1) taken as the complement, so that the three sum to one:
155 // a total off by rounding grows by (dc - 1)(dv - 1) per iteration
156 let _ = p0;
157 let (q1, qm) = (((s + d) / 2.0).max(0.0), ((s - d) / 2.0).max(0.0));
158 let q0 = (1.0 - q1 - qm).max(0.0);
159 // variable node: for every weight, the trinomial distribution of i - j over the
160 // n incoming messages (i correct, j incorrect), then the sign of w m0 + i - j
161 let mut best: Option<(f64, f64, f64, f64)> = None;
162 for w in 1..=dv as i64 {
163 let (mut n1, mut nm, mut n0) = (0.0, 0.0, 0.0);
164 for i in 0..=n {
165 for j in 0..=n - i {
166 let pr = binomial(n, i)
167 * binomial(n - i, j)
168 * q1.powi(i as i32)
169 * qm.powi(j as i32)
170 * q0.powi((n - i - j) as i32);
171 let diff = i as i64 - j as i64;
172 for (m0, pm0) in [(1i64, 1.0 - eps), (-1, eps)] {
173 let v = w * m0 + diff;
174 let mass = pr * pm0;
175 match v.signum() {
176 1 => n1 += mass,
177 -1 => nm += mass,
178 _ => n0 += mass,
179 }
180 }
181 }
182 }
183 // eq. (7): 1 - p0 + h(p0) - h(p0, p_-1), the capacity of the binary symmetric
184 // erasure channel of these probabilities
185 let (n1, nm, n0): (f64, f64, f64) = (n1.max(0.0), nm.max(0.0), n0.max(0.0));
186 let cap = 1.0 - n0 + entropy(&[n0, (1.0 - n0).max(0.0)]) - entropy(&[n0, nm, n1]);
187 if best.is_none_or(|b| cap > b.0) {
188 best = Some((cap, n1, nm, n0));
189 }
190 }
191 let Some((_, a, b, c)) = best else { break };
192 let total = a + b + c;
193 (p1, pm, p0) = (a / total, b / total, c / total);
194 if pm + p0 / 2.0 < 1e-300 {
195 return 0.0;
196 }
197 }
198 pm + p0 / 2.0
199}
200
201/// The largest crossover probability for which `decoder` drives the fraction of
202/// incorrect messages to zero on the ensemble (Table I of Richardson & Urbanke): a
203/// bisection of $[0, 1/2]$ to $10^{-6}$, each point iterated until the fraction falls
204/// below $10^{-12}$ (at most $10^{5}$ iterations).
205///
206/// # Errors
207/// As [`hard_decision_error`] (never for this range).
208pub fn hard_decision_threshold(ensemble: &RegularEnsemble, decoder: HardDecoder) -> Result<f64> {
209 let converges = |eps: f64| hard_decision_error(ensemble, decoder, eps, 100_000).is_ok_and(|p| p < 1e-12);
210 Ok(bisect(converges, 0.0, 0.5, 20))
211}
212
213/// The crossover probability below which a bit of the BSC that every one of its $t$ checks
214/// votes against is more likely in error than correct, the "correction effect" bound of
215/// MacKay's simple decoder (MacKay 1999, eqs. (44), (54)–(57)): with
216/// $p_{z}(\tau) = \frac12 - \frac12(1 - 2f)^{\tau}$, $a = p_{z}(t_{r} - 1)^{t}$ and
217/// $b = (1 - p_{z}(t_{r} - 1))^{t}$, the root in $f$ of $a(1 - f) - b f$ ($t = d_{v}$,
218/// $t_{r} = d_{c}$; bisection to $10^{-12}$). It bounds the first iteration only.
219///
220/// # Examples
221/// ```
222/// use comnum_fec::density_evolution::{correction_effect_threshold, RegularEnsemble};
223/// // (4, 8): about 0.08 (MacKay 1999, Fig. 6(b))
224/// let f = correction_effect_threshold(&RegularEnsemble::new(4, 8)?);
225/// assert!((f - 0.0796).abs() < 1e-4, "{f}");
226/// # Ok::<(), comnum_core::Error>(())
227/// ```
228pub fn correction_effect_threshold(ensemble: &RegularEnsemble) -> f64 {
229 let (t, tr) = (ensemble.dv as i32, ensemble.dc as i32);
230 let corrects = |f: f64| {
231 let pz = 0.5 - 0.5 * (1.0 - 2.0 * f).powi(tr - 1);
232 let (a, b) = (pz.powi(t), (1.0 - pz).powi(t));
233 a * (1.0 - f) - b * f < 0.0
234 };
235 bisect(corrects, 1e-12, 0.5, 60)
236}
237
238/// A binary-input memoryless output-symmetric channel, the input of
239/// [`BpDensityEvolution`].
240#[derive(Clone, Copy, Debug, PartialEq)]
241pub enum BinaryInputChannel {
242 /// The binary symmetric channel of crossover probability `crossover` (Example 1).
243 Bsc {
244 /// Crossover probability $\epsilon$.
245 crossover: f64,
246 },
247 /// The binary-input AWGN channel $y = x + z$, $x \in \{\pm 1\}$,
248 /// $z \sim \mathcal{N}(0, \sigma^{2})$ (Example 2).
249 Biawgn {
250 /// Noise standard deviation $\sigma$.
251 sigma: f64,
252 },
253}
254
255/// The outcome of a run of density evolution.
256#[derive(Clone, Copy, Debug, PartialEq)]
257pub struct DensityEvolutionRun {
258 /// Fraction of incorrect variable-to-check messages at the last iteration (an LLR of
259 /// zero counts as half).
260 pub error: f64,
261 /// Iterations run.
262 pub iterations: usize,
263 /// Whether the fraction fell below the target.
264 pub converged: bool,
265}
266
267/// Discretized density evolution of belief propagation (see the module).
268#[derive(Clone, Copy, Debug, PartialEq)]
269pub struct BpDensityEvolution {
270 /// Step of the LLR grid (nominal: the step is adjusted so that the LLR of the BSC
271 /// falls on the grid).
272 pub llr_step: f64,
273 /// Largest LLR of the grid; larger magnitudes are clipped to it.
274 pub llr_max: f64,
275 /// Largest number of iterations.
276 pub max_iterations: usize,
277 /// The fraction of incorrect messages below which the run has converged.
278 pub target: f64,
279}
280
281impl Default for BpDensityEvolution {
282 /// A step of 0.05, LLRs up to 25, 3000 iterations, a target of $10^{-7}$.
283 fn default() -> Self {
284 BpDensityEvolution { llr_step: 0.05, llr_max: 25.0, max_iterations: 3000, target: 1e-7 }
285 }
286}
287
288/// The grid of the LLRs and the table of the check-node operation on it.
289struct Grid {
290 /// Half-width $K$: the LLRs are $k \Delta$, $-K \le k \le K$.
291 half: usize,
292 /// For every pair of bins, the bin of $2 \operatorname{atanh}(\tanh(a/2) \tanh(b/2))$.
293 table: Vec<u32>,
294}
295
296impl Grid {
297 fn new(step: f64, half: usize) -> Grid {
298 let n = 2 * half + 1;
299 let th: Vec<f64> = (0..n).map(|i| ((i as f64 - half as f64) * step / 2.0).tanh()).collect();
300 let mut table = vec![0u32; n * n];
301 for i in 0..n {
302 for j in 0..n {
303 let t = (th[i] * th[j]).clamp(-1.0, 1.0);
304 let m = 2.0 * t.atanh();
305 let k = if m.is_finite() { (m / step).round() } else { m.signum() * half as f64 };
306 table[i * n + j] = (k.clamp(-(half as f64), half as f64) + half as f64) as u32;
307 }
308 }
309 Grid { half, table }
310 }
311
312 fn len(&self) -> usize {
313 2 * self.half + 1
314 }
315
316 /// The density of the check-node combination of two independent messages.
317 fn boxplus(&self, a: &[f64], b: &[f64]) -> Vec<f64> {
318 let n = self.len();
319 let mut out = vec![0.0; n];
320 for (i, &pa) in a.iter().enumerate() {
321 if pa == 0.0 {
322 continue;
323 }
324 let row = &self.table[i * n..(i + 1) * n];
325 for (&k, &pb) in row.iter().zip(b) {
326 out[k as usize] += pa * pb;
327 }
328 }
329 out
330 }
331
332 /// The density of the sum of independent messages of densities `parts` (indices add),
333 /// clipped to the grid.
334 fn convolve(&self, parts: &[&[f64]]) -> Vec<f64> {
335 let n = self.len();
336 let full = parts.len() * (n - 1) + 1;
337 let size = full.next_power_of_two();
338 let mut acc = vec![C64::new(1.0, 0.0); size];
339 for p in parts {
340 let mut x = vec![C64::new(0.0, 0.0); size];
341 for (xi, &pi) in x.iter_mut().zip(p.iter()) {
342 *xi = C64::new(pi, 0.0);
343 }
344 for (a, f) in acc.iter_mut().zip(fft(&x)) {
345 *a *= f;
346 }
347 }
348 let y = ifft(&acc);
349 // index t of the full convolution is the LLR (t - parts.len() * half) step
350 let offset = parts.len() * self.half;
351 let mut out = vec![0.0; n];
352 for (t, v) in y.iter().take(full).enumerate() {
353 let k = (t as i64 - offset as i64).clamp(-(self.half as i64), self.half as i64);
354 out[(k + self.half as i64) as usize] += v.re.max(0.0);
355 }
356 let total: f64 = out.iter().sum();
357 for v in &mut out {
358 *v /= total;
359 }
360 out
361 }
362
363 /// Fraction of incorrect messages: the negative LLRs and half the zero bin.
364 fn error(&self, p: &[f64]) -> f64 {
365 p[..self.half].iter().sum::<f64>() + 0.5 * p[self.half]
366 }
367}
368
369impl BpDensityEvolution {
370 /// The grid step and the density of the channel LLRs on it.
371 fn channel(&self, channel: BinaryInputChannel) -> Result<(f64, usize, Vec<f64>)> {
372 if !(self.llr_step > 0.0 && self.llr_max > self.llr_step) {
373 return Err(Error::Config(format!(
374 "density evolution needs 0 < llr_step < llr_max, got {} and {}",
375 self.llr_step, self.llr_max
376 )));
377 }
378 match channel {
379 BinaryInputChannel::Bsc { crossover: eps } => {
380 if !(eps > 0.0 && eps < 0.5) {
381 return Err(Error::Config(format!("the crossover probability must be in (0, 1/2), got {eps}")));
382 }
383 // the step that puts the LLR log((1 - eps)/eps) on a bin (Example 10)
384 let l0 = ((1.0 - eps) / eps).ln();
385 let bins = (l0 / self.llr_step).round().max(1.0);
386 let step = l0 / bins;
387 let half = (self.llr_max / step).round() as usize;
388 let half = half.max(bins as usize);
389 let mut p = vec![0.0; 2 * half + 1];
390 p[half + bins as usize] = 1.0 - eps;
391 p[half - bins as usize] = eps;
392 Ok((step, half, p))
393 }
394 BinaryInputChannel::Biawgn { sigma } => {
395 if !(sigma > 0.0 && sigma.is_finite()) {
396 return Err(Error::Config(format!("the noise standard deviation must be positive, got {sigma}")));
397 }
398 // LLR 2y/sigma^2 ~ N(2/sigma^2, 4/sigma^2) for the input +1 (Example 11)
399 let (mu, sd) = (2.0 / (sigma * sigma), 2.0 / sigma);
400 let step = self.llr_step;
401 let half = (self.llr_max / step).round() as usize;
402 let cdf = |x: f64| 1.0 - qfunc((x - mu) / sd);
403 let p = (0..=2 * half)
404 .map(|i| {
405 let k = i as f64 - half as f64;
406 let lo = if i == 0 { f64::NEG_INFINITY } else { (k - 0.5) * step };
407 let hi = if i == 2 * half { f64::INFINITY } else { (k + 0.5) * step };
408 let c = |x: f64| if x.is_infinite() { if x > 0.0 { 1.0 } else { 0.0 } } else { cdf(x) };
409 (c(hi) - c(lo)).max(0.0)
410 })
411 .collect();
412 Ok((step, half, p))
413 }
414 }
415 }
416
417 /// Run density evolution of belief propagation on the ensemble and the channel until
418 /// the fraction of incorrect messages falls below [`target`](Self::target), stops
419 /// decreasing, or [`max_iterations`](Self::max_iterations) is reached.
420 ///
421 /// # Errors
422 /// [`Error::Config`] for an invalid grid or channel parameter.
423 pub fn run(&self, ensemble: &RegularEnsemble, channel: BinaryInputChannel) -> Result<DensityEvolutionRun> {
424 let (step, half, p0) = self.channel(channel)?;
425 let grid = Grid::new(step, half);
426 let mut p = p0.clone();
427 let mut last = grid.error(&p);
428 for it in 1..=self.max_iterations {
429 // check node: d_c - 1 variable messages
430 let mut q = p.clone();
431 for _ in 1..ensemble.dc - 1 {
432 q = grid.boxplus(&q, &p);
433 }
434 // variable node: the channel and d_v - 1 check messages, eq. (9)
435 let mut parts: Vec<&[f64]> = vec![&p0];
436 parts.extend(std::iter::repeat_n(q.as_slice(), ensemble.dv - 1));
437 p = grid.convolve(&parts);
438 let e = grid.error(&p);
439 if e < self.target {
440 return Ok(DensityEvolutionRun { error: e, iterations: it, converged: true });
441 }
442 // a fixed point: the fraction no longer decreases
443 if e >= last - 1e-15 * last.max(1e-300) && it > 1 {
444 return Ok(DensityEvolutionRun { error: e, iterations: it, converged: false });
445 }
446 last = e;
447 }
448 Ok(DensityEvolutionRun { error: last, iterations: self.max_iterations, converged: false })
449 }
450
451 /// The threshold of a family of channels `channel(t)` that degrades as `t` grows,
452 /// between `lo` (converges) and `hi` (does not), by `steps` bisection steps.
453 ///
454 /// # Errors
455 /// As [`run`](Self::run) for a parameter of the bracket.
456 pub fn threshold(
457 &self,
458 ensemble: &RegularEnsemble,
459 channel: impl Fn(f64) -> BinaryInputChannel,
460 lo: f64,
461 hi: f64,
462 steps: usize,
463 ) -> Result<f64> {
464 let mut err = None;
465 let t = bisect(
466 |x| match self.run(ensemble, channel(x)) {
467 Ok(r) => r.converged,
468 Err(e) => {
469 err = Some(e);
470 false
471 }
472 },
473 lo,
474 hi,
475 steps,
476 );
477 match err {
478 Some(e) => Err(e),
479 None => Ok(t),
480 }
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487
488 #[test]
489 fn an_ensemble_needs_two_le_dv_lt_dc() {
490 assert!(RegularEnsemble::new(1, 6).is_err() && RegularEnsemble::new(6, 6).is_err());
491 assert!((RegularEnsemble::new(3, 6).unwrap().rate() - 0.5).abs() < 1e-15);
492 let e = RegularEnsemble::new(3, 6).unwrap();
493 assert!(hard_decision_error(&e, HardDecoder::GallagerA, 0.6, 10).is_err());
494 let bp = BpDensityEvolution::default();
495 assert!(bp.run(&e, BinaryInputChannel::Bsc { crossover: 0.0 }).is_err());
496 assert!(bp.run(&e, BinaryInputChannel::Biawgn { sigma: -1.0 }).is_err());
497 }
498
499 /// For $d_{v} = 3$ algorithms A and B coincide (Table I caption); the noiseless channel
500 /// has no error; above the threshold the fraction stays at a fixed point.
501 #[test]
502 fn gallager_a_and_b_coincide_for_dv_3_and_have_fixed_points_above_threshold() {
503 let e = RegularEnsemble::new(3, 6).unwrap();
504 for eps in [0.03, 0.05] {
505 let a = hard_decision_error(&e, HardDecoder::GallagerA, eps, 2000).unwrap();
506 let b = hard_decision_error(&e, HardDecoder::GallagerB, eps, 2000).unwrap();
507 assert_eq!(a, b);
508 }
509 assert_eq!(hard_decision_error(&e, HardDecoder::GallagerA, 0.0, 10).unwrap(), 0.0);
510 assert!(hard_decision_error(&e, HardDecoder::GallagerA, 0.05, 2000).unwrap() > 0.01);
511 }
512
513 /// The check and variable maps preserve the total probability and the symmetry
514 /// $P(-m) = e^{-m} P(m)$ of LLR densities, approximately on the grid.
515 #[test]
516 fn the_grid_operations_keep_the_mass_and_the_density_symmetry() {
517 let bp = BpDensityEvolution::default();
518 let (step, half, p0) = bp.channel(BinaryInputChannel::Biawgn { sigma: 0.9 }).unwrap();
519 let g = Grid::new(step, half);
520 let q = g.boxplus(&p0, &p0);
521 let v = g.convolve(&[&p0, &q, &q]);
522 for d in [&p0, &q, &v] {
523 assert!((d.iter().sum::<f64>() - 1.0).abs() < 1e-9);
524 }
525 // symmetry at a few bins of the check output
526 for k in [5usize, 20, 40] {
527 let (pos, neg) = (q[half + k], q[half - k]);
528 let m = k as f64 * step;
529 assert!((neg - (-m).exp() * pos).abs() < 0.05 * pos, "bin {k}: {neg} against {}", (-m).exp() * pos);
530 }
531 }
532
533 /// On a cycle-free graph a channel far better than the threshold converges in a few
534 /// iterations, a channel far worse never does.
535 #[test]
536 fn bp_density_evolution_converges_well_below_the_threshold_only() {
537 let e = RegularEnsemble::new(3, 6).unwrap();
538 let bp = BpDensityEvolution { llr_step: 0.1, ..BpDensityEvolution::default() };
539 let good = bp.run(&e, BinaryInputChannel::Biawgn { sigma: 0.7 }).unwrap();
540 assert!(good.converged && good.iterations < 30, "{good:?}");
541 let bad = bp.run(&e, BinaryInputChannel::Biawgn { sigma: 1.0 }).unwrap();
542 assert!(!bad.converged && bad.error > 0.05, "{bad:?}");
543 }
544
545 /// Gallager's algorithm B is A with the flipping rule of Example 4: it only helps for
546 /// $d_{v} > 3$; algorithm E with the weights that maximise (7) beats both (Table I).
547 #[test]
548 fn algorithm_b_and_e_improve_on_a_as_in_table_i() {
549 let e48 = RegularEnsemble::new(4, 8).unwrap();
550 let a = hard_decision_threshold(&e48, HardDecoder::GallagerA).unwrap();
551 let b = hard_decision_threshold(&e48, HardDecoder::GallagerB).unwrap();
552 let ee = hard_decision_threshold(&e48, HardDecoder::AlgorithmE).unwrap();
553 assert!((a - 0.0476).abs() < 2e-4 && (b - 0.0517).abs() < 2e-4 && (ee - 0.0595).abs() < 2e-4, "{a} {b} {ee}");
554 // (3, 6): Example 5 finds the weights 2 then 1 and a threshold near 0.07
555 let e36 = RegularEnsemble::new(3, 6).unwrap();
556 let ee = hard_decision_threshold(&e36, HardDecoder::AlgorithmE).unwrap();
557 assert!((0.070..0.0715).contains(&ee), "{ee}");
558 // below capacity: eps_opt = 0.11 at rate 1/2
559 assert!(ee < 0.11);
560 }
561}