Skip to main content

comnum_fec/
ldpc.rs

1//! Low-density parity-check (LDPC) codes.
2//!
3//! A code is the null space of a sparse parity-check matrix $H$ ($m \times n$):
4//! $H c = 0$. Its Tanner graph links every variable node (coded bit) to the check
5//! nodes (parity equations) it takes part in.
6//!
7//! # Construction
8//!
9//! [`LdpcCode::peg`] builds $H$ by progressive edge growth (Hu, Eleftheriou & Arnold
10//! 2005): every new edge of a variable node goes to the check node farthest from it in
11//! the current graph (lowest degree among the farthest), which maximises the local girth.
12//! [`LdpcCode::from_rows`] takes any $H$ (for instance the lifted base graph of a
13//! standard), and [`LdpcCode::from_alist`] reads it from MacKay's `alist` format (the
14//! format of the matrices of MacKay's encyclopedia and of AFF3CT). The encoder brings $H$ to the systematic form $[A \mid I]$ by Gaussian
15//! elimination over GF(2) (with a column permutation), so that the parity bits are
16//! $p = A u$.
17//!
18//! # Decoding
19//!
20//! Message passing on the Tanner graph, with the LLR convention of the library
21//! (positive for a 0). A variable node sends the sum of its channel LLR and of the
22//! other incoming messages; a check node sends
23//!
24//! $$
25//! L_{c \to v} = 2 \operatorname{atanh} \prod_{v' \ne v} \tanh\frac{L_{v' \to c}}{2}
26//! \quad \text{(sum-product)}, \qquad
27//! L_{c \to v} \approx \alpha \prod_{v' \ne v} \operatorname{sgn} L_{v' \to c}
28//! \min_{v' \ne v} |L_{v' \to c}| \quad \text{(min-sum)},
29//! $$
30//!
31//! with the normalisation $\alpha$ (Chen & Fossorier 2002) or an offset. The schedule is
32//! flooding, or layered (row by row with immediate updates, Hocevar 2004), which
33//! converges in about half the iterations; decoding stops as soon as $H \hat c = 0$.
34//!
35//! The decoder settings of a new code ([`LdpcDecoder`], the field `decoder`) are the
36//! defaults of the crate, which the specifications (`comnum_spec`) and the Python bindings
37//! also take: [`DEFAULT_RULE`] (sum-product), [`DEFAULT_LAYERED`] (layered) and
38//! [`DEFAULT_MAX_ITERATIONS`] (50);
39//! [`DEFAULT_ALPHA`] is the scale or the offset of a min-sum rule named without it,
40//! [`DEFAULT_VAR_DEGREE`] and [`DEFAULT_PEG_SEED`] those of a PEG construction.
41//!
42//! # Examples
43//! ```
44//! use comnum_fec::block::BlockCode;
45//! use comnum_fec::ldpc::LdpcCode;
46//! let code = LdpcCode::peg(256, 128, 3, 1)?;                 // (256, 128), degree 3
47//! assert_eq!(code.k(), 128);
48//! let info: Vec<u8> = (0..128).map(|i| (i % 5 == 2) as u8).collect();
49//! let c = code.encode_block(&info)?;
50//! assert!(code.is_codeword(&c));
51//! let llr: Vec<f64> = c.iter().map(|&b| if b == 0 { 4.0 } else { -4.0 }).collect(); // noiseless
52//! assert_eq!(code.decode_block(&llr)?, info);
53//! # Ok::<(), comnum_core::Error>(())
54//! ```
55//!
56//! # References
57//!
58//! 1. R. G. Gallager, "Low-density parity-check codes," *IRE Trans. Inf. Theory*, 8(1),
59//!    21–28, 1962.
60//! 2. X.-Y. Hu, E. Eleftheriou and D. M. Arnold, "Regular and irregular progressive
61//!    edge-growth Tanner graphs," *IEEE Trans. Inf. Theory*, 51(1), 386–398, 2005.
62//! 3. J. Chen and M. P. C. Fossorier, "Near optimum universal belief propagation based
63//!    decoding of low-density parity check codes," *IEEE Trans. Commun.*, 50(3), 406–414,
64//!    2002.
65//! 4. D. E. Hocevar, "A reduced complexity decoder architecture via layered decoding of
66//!    LDPC codes," *Proc. IEEE SiPS*, 107–112, 2004.
67//! 5. T. Richardson and R. Urbanke, *Modern Coding Theory*, Cambridge University Press,
68//!    2008.
69
70use crate::block::{BlockCode, SoftDecode, container_traits, expect_len};
71use comnum_core::{Error, Result, ensure};
72use comnum_math::gf2;
73use std::collections::VecDeque;
74
75/// The default check-node rule: sum-product, the exact belief propagation (the
76/// reference of the performance tables of the standards).
77pub const DEFAULT_RULE: CheckRule = CheckRule::SumProduct;
78/// The default scale $\alpha$ of the normalised min-sum, or offset $\beta$ of the offset
79/// min-sum, when a rule is named without it.
80pub const DEFAULT_ALPHA: f64 = 0.8;
81/// The default schedule: layered.
82pub const DEFAULT_LAYERED: bool = true;
83/// The default maximum number of iterations.
84pub const DEFAULT_MAX_ITERATIONS: usize = 50;
85/// The default degree of the variable nodes of a PEG construction.
86pub const DEFAULT_VAR_DEGREE: usize = 3;
87/// The default seed of a PEG construction.
88pub const DEFAULT_PEG_SEED: u64 = 1;
89
90/// The bound of $|\prod \tanh|$ in the check node of the sum-product rule, so that its
91/// $\operatorname{atanh}$ stays finite (a message of at most $2 \operatorname{atanh}(1 -
92/// 10^{-12}) \approx 28.3$).
93const TANH_BOUND: f64 = 0.999_999_999_999;
94
95/// The check-node rule of the decoder.
96#[derive(Clone, Copy, Debug, PartialEq)]
97#[non_exhaustive]
98pub enum CheckRule {
99    /// Sum-product (belief propagation, exact).
100    SumProduct,
101    /// Min-sum scaled by $\alpha$ (1 for plain min-sum, 0.75 to 0.8 typically).
102    NormalizedMinSum(f64),
103    /// Min-sum with the offset $\beta$ subtracted from the magnitudes.
104    OffsetMinSum(f64),
105}
106
107impl CheckRule {
108    /// The rule of a name: `sum_product`, `min_sum` (plain), `normalized_min_sum`
109    /// (scaled by `alpha`) or `offset_min_sum` (offset `alpha`), case-insensitive.
110    ///
111    /// # Errors
112    /// [`Error::Config`] for another name.
113    pub fn from_name(rule: &str, alpha: f64) -> Result<CheckRule> {
114        match rule.to_ascii_lowercase().as_str() {
115            "sum_product" => Ok(CheckRule::SumProduct),
116            "min_sum" => Ok(CheckRule::NormalizedMinSum(1.0)),
117            "normalized_min_sum" => Ok(CheckRule::NormalizedMinSum(alpha)),
118            "offset_min_sum" => Ok(CheckRule::OffsetMinSum(alpha)),
119            other => Err(Error::Config(format!(
120                "rule must be sum_product, min_sum, normalized_min_sum or offset_min_sum, got {other:?}"
121            ))),
122        }
123    }
124
125    /// The name of the rule, as [`CheckRule::from_name`] reads it (`min_sum` is the
126    /// normalised min-sum of scale 1).
127    ///
128    /// # Examples
129    /// ```
130    /// use comnum_fec::ldpc::{CheckRule, DEFAULT_RULE};
131    /// assert_eq!(DEFAULT_RULE.name(), "sum_product");
132    /// assert_eq!(CheckRule::from_name("min_sum", 0.5)?.name(), "normalized_min_sum");
133    /// # Ok::<(), comnum_core::Error>(())
134    /// ```
135    pub fn name(self) -> &'static str {
136        match self {
137            CheckRule::SumProduct => "sum_product",
138            CheckRule::NormalizedMinSum(_) => "normalized_min_sum",
139            CheckRule::OffsetMinSum(_) => "offset_min_sum",
140        }
141    }
142}
143
144/// The systematic encoder of a code.
145#[derive(Clone, Debug, PartialEq)]
146enum Encoder {
147    /// The positions of the parity bits, and the information bits each one sums
148    /// (from the Gaussian elimination of $H$).
149    Sums(Vec<(usize, Vec<usize>)>),
150    /// Irregular repeat-accumulate: the information bits are the first $k$, parity bit
151    /// $p_{r}$ (position $k + r$) closes check $r$ with $p_{r-1}$ (the dual-diagonal part
152    /// of $H$): $p_{r} = p_{r-1} \oplus \bigoplus_{v \in r,\, v < k} c_{v}$.
153    Accumulate,
154    /// A core and an extension (the structure of the codes of 5G NR): the information
155    /// bits are the first $k$; the first `core` checks involve the bits below
156    /// $k + \mathrm{core}$ only, and core parity bit $j$ is the sum of the syndromes of
157    /// the information bits over the checks `solve[j]` (the inverse of the core); check
158    /// $r \ge \mathrm{core}$ then closes on its own parity bit $k + r$, the last of its
159    /// row.
160    Core { core: usize, solve: Vec<Vec<usize>> },
161    /// No encoder of its own: the codewords come from the encoder of the standard that
162    /// defines the code ([`LdpcCode::without_encoder`]).
163    External,
164}
165
166/// The settings of the belief-propagation decoder of an [`LdpcCode`].
167#[derive(Clone, Copy, Debug, PartialEq)]
168pub struct LdpcDecoder {
169    /// The check-node rule.
170    pub rule: CheckRule,
171    /// Layered (true) or flooding schedule.
172    pub layered: bool,
173    /// Maximum iterations (0: the channel LLRs are returned as they are).
174    pub max_iterations: usize,
175}
176
177impl Default for LdpcDecoder {
178    /// [`DEFAULT_RULE`], [`DEFAULT_LAYERED`], [`DEFAULT_MAX_ITERATIONS`].
179    fn default() -> LdpcDecoder {
180        LdpcDecoder { rule: DEFAULT_RULE, layered: DEFAULT_LAYERED, max_iterations: DEFAULT_MAX_ITERATIONS }
181    }
182}
183
184/// An LDPC code with its systematic encoder and its decoder settings.
185#[derive(Clone, Debug, PartialEq)]
186pub struct LdpcCode {
187    n: usize,
188    /// The variable nodes of every check node (the rows of $H$).
189    rows: Vec<Vec<usize>>,
190    /// The messages of check `c` sit at `start[c]..start[c + 1]` of the edges (row
191    /// after row), computed once with the rows.
192    start: Vec<usize>,
193    /// Positions of the information bits in the codeword.
194    info: Vec<usize>,
195    /// How the parity bits follow from the information bits.
196    encoder: Encoder,
197    /// The decoder settings.
198    pub decoder: LdpcDecoder,
199}
200
201impl LdpcCode {
202    /// The code of checked rows, its information positions and its encoder, with the
203    /// edge offsets of the decoder and the default settings.
204    fn assemble(n: usize, rows: Vec<Vec<usize>>, info: Vec<usize>, encoder: Encoder) -> LdpcCode {
205        let mut start = Vec::with_capacity(rows.len() + 1);
206        start.push(0);
207        for row in &rows {
208            start.push(start[start.len() - 1] + row.len());
209        }
210        LdpcCode { n, rows, start, info, encoder, decoder: LdpcDecoder::default() }
211    }
212
213    /// The code of the parity-check matrix whose rows list the variable nodes of every
214    /// check; $k = n - \operatorname{rank} H$.
215    ///
216    /// # Errors
217    /// [`Error::Config`] for a variable index out of range or a code without
218    /// information bits.
219    pub fn from_rows(n: usize, rows: Vec<Vec<usize>>) -> Result<LdpcCode> {
220        ensure(rows.iter().flatten().all(|v| *v < n), || "a check refers to a missing variable".into())?;
221        // the reduced parity-check matrix: every pivot (parity) bit is a sum of
222        // information bits
223        let mut m = gf2::Matrix::from_rows(n, &rows);
224        let pivots = m.reduce();
225        let is_pivot: Vec<bool> = (0..n).map(|c| pivots.contains(&c)).collect();
226        let info: Vec<usize> = (0..n).filter(|c| !is_pivot[*c]).collect();
227        ensure(!info.is_empty(), || "the code has no information bits".into())?;
228        let parity = pivots
229            .iter()
230            .enumerate()
231            .map(|(row, &c)| (c, info.iter().copied().filter(|&v| m.get(row, v)).collect()))
232            .collect();
233        Ok(LdpcCode::assemble(n, rows, info, Encoder::Sums(parity)))
234    }
235
236    /// The code of the parity-check matrix `rows` whose information bits are at the
237    /// positions `info`, without an encoder of its own: its decoder only
238    /// ([`LdpcCode::decode_llr`], [`BlockCode::decode_block`]), for a code whose
239    /// standard gives the encoder (the LDPC codes of [`crate::ccsds`], whose systematic
240    /// generators [`LdpcCode::from_rows`] would store densely). [`BlockCode::encode_block`]
241    /// returns an error.
242    ///
243    /// # Errors
244    /// [`Error::Config`] for a variable index out of range, or information positions
245    /// out of range, repeated or absent.
246    pub fn without_encoder(n: usize, rows: Vec<Vec<usize>>, info: Vec<usize>) -> Result<LdpcCode> {
247        ensure(rows.iter().flatten().all(|v| *v < n), || "a check refers to a missing variable".into())?;
248        ensure(!info.is_empty() && info.iter().all(|&v| v < n), || {
249            "the information positions are out of range".into()
250        })?;
251        let mut sorted = info.clone();
252        sorted.sort_unstable();
253        sorted.dedup();
254        ensure(sorted.len() == info.len(), || "an information position is repeated".into())?;
255        Ok(LdpcCode::assemble(n, rows, info, Encoder::External))
256    }
257
258    /// An irregular repeat-accumulate code: check $r$ of `rows` holds information bits
259    /// (positions below $k = n - m$, $m$ the number of checks) and the parity bits
260    /// $k + r$ and, for $r > 0$, $k + r - 1$ only (the dual-diagonal structure of the
261    /// DVB-S2, DVB-T2 and IEEE 802.11n codes). The encoder accumulates the checks, in
262    /// $O(n)$, without the elimination of [`LdpcCode::from_rows`].
263    ///
264    /// # Errors
265    /// [`Error::Config`] if the checks do not have this structure.
266    pub fn accumulate(n: usize, rows: Vec<Vec<usize>>) -> Result<LdpcCode> {
267        let m = rows.len();
268        ensure(m > 0 && m < n, || format!("{m} checks for {n} bits"))?;
269        let k = n - m;
270        for (r, row) in rows.iter().enumerate() {
271            let parity: Vec<usize> = row.iter().copied().filter(|&v| v >= k).collect();
272            let expected: Vec<usize> = if r == 0 { vec![k] } else { vec![k + r - 1, k + r] };
273            let mut sorted = parity.clone();
274            sorted.sort_unstable();
275            ensure(sorted == expected, || {
276                format!("check {r} is not dual-diagonal: parity bits {parity:?}, expected {expected:?}")
277            })?;
278            ensure(row.iter().all(|&v| v < n), || "a check refers to a missing variable".into())?;
279        }
280        Ok(LdpcCode::assemble(n, rows, (0..k).collect(), Encoder::Accumulate))
281    }
282
283    /// A code whose $m$ checks have a core and an extension, the structure of the LDPC
284    /// codes of 5G NR (TS 38.212, Sec. 5.3.2): the information bits are the first $k$;
285    /// the first `core` checks involve the bits below $k + \mathrm{core}$ only, with an
286    /// invertible square block on the core parity bits $k, \dots, k + \mathrm{core} - 1$;
287    /// every other check $r$ contains the parity bit $k + r$ and no bit above it. The
288    /// encoder inverts the core block once, by Gauss–Jordan elimination over GF(2), then
289    /// computes the extension parity bits one after the other, without the elimination of
290    /// the whole matrix of [`LdpcCode::from_rows`].
291    ///
292    /// # Errors
293    /// [`Error::Config`] if the checks do not have this structure or the core block is
294    /// singular.
295    pub fn from_core_rows(n: usize, rows: Vec<Vec<usize>>, k: usize, core: usize) -> Result<LdpcCode> {
296        let m = rows.len();
297        ensure(k > 0 && k + m == n && core <= m, || format!("{m} checks, {core} in the core, for k = {k}, n = {n}"))?;
298        ensure(rows.iter().flatten().all(|v| *v < n), || "a check refers to a missing variable".into())?;
299        for (r, row) in rows.iter().enumerate() {
300            let top = row.iter().copied().max().unwrap_or(0);
301            if r < core {
302                ensure(top < k + core, || format!("core check {r} reaches bit {top} beyond the core"))?;
303            } else {
304                ensure(top == k + r && row.iter().filter(|&&v| v == k + r).count() == 1, || {
305                    format!("extension check {r} does not close on its parity bit {}", k + r)
306                })?;
307            }
308        }
309        // parity bit j = (B^-1 s)_j: the checks of row j of the inverse of the core block
310        let solve = core_inverse(&rows, k, core)?;
311        Ok(LdpcCode::assemble(n, rows, (0..k).collect(), Encoder::Core { core, solve }))
312    }
313
314    /// A code of length `n` with `n - k` checks built by progressive edge growth, every
315    /// variable node of degree `var_degree` (seeded tie breaks).
316    ///
317    /// # Errors
318    /// [`Error::Config`] for $k \ge n$ or a degree larger than the number of checks.
319    pub fn peg(n: usize, k: usize, var_degree: usize, seed: u64) -> Result<LdpcCode> {
320        ensure(k < n && k > 0, || format!("PEG needs 0 < k < n, got k = {k}, n = {n}"))?;
321        let m = n - k;
322        ensure(var_degree >= 1 && var_degree <= m, || format!("variable degree {var_degree} for {m} checks"))?;
323        let mut rng = comnum_core::rng::Rng::new(seed);
324        let mut check_vars: Vec<Vec<usize>> = vec![Vec::new(); m];
325        let mut var_checks: Vec<Vec<usize>> = vec![Vec::new(); n];
326        let all: Vec<usize> = (0..m).collect();
327        for v in 0..n {
328            for e in 0..var_degree {
329                let c = if e == 0 {
330                    lowest_degree(&all, &check_vars, &mut rng)
331                } else {
332                    lowest_degree(&farthest_checks(v, &check_vars, &var_checks), &check_vars, &mut rng)
333                };
334                check_vars[c].push(v);
335                var_checks[v].push(c);
336            }
337        }
338        LdpcCode::from_rows(n, check_vars)
339    }
340
341    /// The code of a parity-check matrix written in MacKay's `alist` format (the format
342    /// of MacKay's *Encyclopedia of Sparse Graph Codes* and of the matrices shipped with
343    /// AFF3CT), with the systematic encoder of [`LdpcCode::from_rows`]. The rows of $H$
344    /// keep the order of the file, which is the order of a layered schedule.
345    ///
346    /// The format is a list of integers: $n$ and $m$ (columns and rows of $H$), the
347    /// largest column and row degrees, the $n$ column degrees, the $m$ row degrees, then
348    /// for every column the (1-based) rows of its ones, and for every row the columns of
349    /// its ones. The lists may be padded with zeros up to the largest degree or not;
350    /// the text after a `#` is a comment. The two descriptions of the matrix must agree.
351    ///
352    /// # Errors
353    /// [`Error::Config`] for a malformed file: a missing or extra number, a degree or an
354    /// index out of range, a repeated one, or column and row lists that disagree; or
355    /// for a matrix that [`LdpcCode::from_rows`] rejects.
356    ///
357    /// # Examples
358    /// ```
359    /// use comnum_fec::block::BlockCode;
360    /// use comnum_fec::ldpc::LdpcCode;
361    /// // the (7, 4) Hamming code: n = 7 columns, m = 3 rows
362    /// let alist = "7 3\n3 4\n3 2 2 2 1 1 1\n4 4 4\n\
363    ///              1 2 3\n1 2 0\n1 3 0\n2 3 0\n1 0 0\n2 0 0\n3 0 0\n\
364    ///              1 2 3 5\n1 2 4 6\n1 3 4 7\n";
365    /// let code = LdpcCode::from_alist(alist)?;
366    /// assert_eq!((code.n(), code.k()), (7, 4));
367    /// assert_eq!(code.rows()[1], [0, 1, 3, 5]);
368    /// assert_eq!(LdpcCode::from_alist(&code.to_alist())?, code);
369    /// # Ok::<(), comnum_core::Error>(())
370    /// ```
371    ///
372    /// # References
373    /// 1. D. J. C. MacKay, *Encyclopedia of Sparse Graph Codes*, "alist" format,
374    ///    <http://www.inference.org.uk/mackay/codes/alist.html>.
375    pub fn from_alist(text: &str) -> Result<LdpcCode> {
376        let (n, rows) = parse_alist(text).map_err(|e| Error::Config(format!("alist: {e}")))?;
377        LdpcCode::from_rows(n, rows)
378    }
379
380    /// [`LdpcCode::from_alist`] on the content of the file `path`.
381    ///
382    /// # Errors
383    /// [`Error::Io`] if the file cannot be read, otherwise those of
384    /// [`LdpcCode::from_alist`].
385    pub fn from_alist_file(path: impl AsRef<std::path::Path>) -> Result<LdpcCode> {
386        LdpcCode::from_alist(&std::fs::read_to_string(path)?)
387    }
388
389    /// The parity-check matrix in the `alist` format of [`LdpcCode::from_alist`], with
390    /// the lists padded with zeros (the format MacKay's files use).
391    pub fn to_alist(&self) -> String {
392        let m = self.rows.len();
393        let mut cols: Vec<Vec<usize>> = vec![Vec::new(); self.n];
394        for (r, row) in self.rows.iter().enumerate() {
395            for &v in row {
396                cols[v].push(r);
397            }
398        }
399        let max_col = cols.iter().map(Vec::len).max().unwrap_or(0);
400        let max_row = self.rows.iter().map(Vec::len).max().unwrap_or(0);
401        let line = |values: &mut dyn Iterator<Item = usize>| {
402            let mut s = values.map(|v| v.to_string()).collect::<Vec<_>>().join(" ");
403            s.push('\n');
404            s
405        };
406        let padded = |list: &[usize], len: usize| -> String {
407            line(&mut list.iter().map(|v| v + 1).chain(std::iter::repeat_n(0, len - list.len())))
408        };
409        let mut out = format!("{} {m}\n{max_col} {max_row}\n", self.n);
410        out.push_str(&line(&mut cols.iter().map(Vec::len)));
411        out.push_str(&line(&mut self.rows.iter().map(Vec::len)));
412        for c in &cols {
413            out.push_str(&padded(c, max_col));
414        }
415        for r in &self.rows {
416            out.push_str(&padded(r, max_row));
417        }
418        out
419    }
420
421    /// The positions of the information bits in the codeword, in the order of the
422    /// information bits of [`BlockCode::encode_block`] (the code is systematic).
423    pub fn info_positions(&self) -> &[usize] {
424        &self.info
425    }
426
427    /// The parity checks: the variable nodes of every row of $H$.
428    pub fn rows(&self) -> &[Vec<usize>] {
429        &self.rows
430    }
431
432    /// Whether $H c = 0$.
433    pub fn is_codeword(&self, c: &[u8]) -> bool {
434        self.rows.iter().all(|r| r.iter().fold(0u8, |a, &v| a ^ c[v]) == 0)
435    }
436
437    /// The length of the shortest cycle through every variable node of the Tanner
438    /// graph, minimised over the graph (the girth; 0 without cycle).
439    pub fn girth(&self) -> usize {
440        let mut var_checks: Vec<Vec<usize>> = vec![Vec::new(); self.n];
441        for (c, r) in self.rows.iter().enumerate() {
442            for &v in r {
443                var_checks[v].push(c);
444            }
445        }
446        let mut best = usize::MAX;
447        // BFS on the bipartite graph from every variable node
448        let nodes = self.n + self.rows.len();
449        for s in 0..self.n {
450            let mut dist = vec![usize::MAX; nodes];
451            let mut parent = vec![usize::MAX; nodes];
452            dist[s] = 0;
453            let mut q = VecDeque::from([s]);
454            while let Some(x) = q.pop_front() {
455                let nbrs: Vec<usize> = if x < self.n {
456                    var_checks[x].iter().map(|c| self.n + c).collect()
457                } else {
458                    self.rows[x - self.n].clone()
459                };
460                for y in nbrs {
461                    if dist[y] == usize::MAX {
462                        dist[y] = dist[x] + 1;
463                        parent[y] = x;
464                        q.push_back(y);
465                    } else if parent[x] != y {
466                        best = best.min(dist[x] + dist[y] + 1);
467                    }
468                }
469            }
470        }
471        if best == usize::MAX { 0 } else { best }
472    }
473
474    fn check_message(&self, incoming: &[f64], out: &mut [f64], scratch: &mut Vec<f64>) {
475        match self.decoder.rule {
476            CheckRule::SumProduct => {
477                // the product of the other tanh(L/2) by prefix and suffix products, O(d)
478                let d = incoming.len();
479                scratch.clear();
480                scratch.extend(incoming.iter().map(|l| (l / 2.0).tanh()));
481                let mut prefix = 1.0;
482                for (o, t) in out.iter_mut().zip(scratch.iter()) {
483                    *o = prefix;
484                    prefix *= t;
485                }
486                let mut suffix = 1.0;
487                for i in (0..d).rev() {
488                    let p = out[i] * suffix;
489                    suffix *= scratch[i];
490                    out[i] = 2.0 * p.clamp(-TANH_BOUND, TANH_BOUND).atanh();
491                }
492            }
493            CheckRule::NormalizedMinSum(_) | CheckRule::OffsetMinSum(_) => {
494                let sign: f64 = incoming.iter().map(|l| if *l < 0.0 { -1.0 } else { 1.0 }).product();
495                let (mut m1, mut m2, mut i1) = (f64::INFINITY, f64::INFINITY, 0);
496                for (i, l) in incoming.iter().enumerate() {
497                    let a = l.abs();
498                    if a < m1 {
499                        m2 = m1;
500                        m1 = a;
501                        i1 = i;
502                    } else if a < m2 {
503                        m2 = a;
504                    }
505                }
506                for (i, o) in out.iter_mut().enumerate() {
507                    let mag = if i == i1 { m2 } else { m1 };
508                    let s = sign * if incoming[i] < 0.0 { -1.0 } else { 1.0 };
509                    let mag = match self.decoder.rule {
510                        CheckRule::NormalizedMinSum(a) => a * mag,
511                        CheckRule::OffsetMinSum(b) => (mag - b).max(0.0),
512                        CheckRule::SumProduct => mag,
513                    };
514                    *o = s * mag;
515                }
516            }
517        }
518    }
519
520    /// Decode a block of $n$ channel LLRs: the a posteriori LLRs of the coded bits and
521    /// the number of iterations used.
522    ///
523    /// # Errors
524    /// [`Error::Shape`] if `llr` does not have $n$ values.
525    pub fn decode_llr(&self, llr: &[f64]) -> Result<(Vec<f64>, usize)> {
526        expect_len(llr.len(), self.n, "LLRs")?;
527        let mut post = llr.to_vec();
528        let it = self.propagate(llr, &mut post);
529        Ok((post, it))
530    }
531
532    /// Belief propagation from the channel LLRs `channel` ($n$ values), `post` holding
533    /// them on entry and the a posteriori LLRs on exit: the iterations used.
534    fn propagate(&self, channel: &[f64], post: &mut [f64]) -> usize {
535        if self.hard_decisions_are_a_codeword(post) {
536            return 0;
537        }
538        // the check-to-variable messages, row after row: those of check c at
539        // start[c]..start[c + 1]; the flooding schedule writes the next ones apart
540        let LdpcDecoder { layered, max_iterations, .. } = self.decoder;
541        let start = &self.start;
542        let edges = start[self.rows.len()];
543        let mut c2v = vec![0.0; edges];
544        let mut new_c2v = if layered { Vec::new() } else { vec![0.0; edges] };
545        let (mut incoming, mut scratch) = (Vec::new(), Vec::new());
546        for it in 1..=max_iterations {
547            if layered {
548                for (c, row) in self.rows.iter().enumerate() {
549                    let msgs = &mut c2v[start[c]..start[c + 1]];
550                    incoming.clear();
551                    incoming.extend(row.iter().zip(msgs.iter()).map(|(&v, m)| post[v] - m));
552                    self.check_message(&incoming, msgs, &mut scratch);
553                    for ((&v, o), i) in row.iter().zip(msgs.iter()).zip(&incoming) {
554                        post[v] = i + o;
555                    }
556                }
557            } else {
558                for (c, row) in self.rows.iter().enumerate() {
559                    incoming.clear();
560                    incoming.extend(row.iter().zip(&c2v[start[c]..start[c + 1]]).map(|(&v, m)| post[v] - m));
561                    self.check_message(&incoming, &mut new_c2v[start[c]..start[c + 1]], &mut scratch);
562                }
563                post.copy_from_slice(channel);
564                for (c, row) in self.rows.iter().enumerate() {
565                    for (&v, m) in row.iter().zip(&new_c2v[start[c]..start[c + 1]]) {
566                        post[v] += m;
567                    }
568                }
569                std::mem::swap(&mut c2v, &mut new_c2v);
570            }
571            if self.hard_decisions_are_a_codeword(post) {
572                return it;
573            }
574        }
575        max_iterations
576    }
577
578    /// Whether the hard decisions of the LLRs `post` ($1$ for a negative LLR) satisfy
579    /// every check, without forming them.
580    fn hard_decisions_are_a_codeword(&self, post: &[f64]) -> bool {
581        self.rows.iter().all(|r| r.iter().fold(0u8, |a, &v| a ^ u8::from(post[v] < 0.0)) == 0)
582    }
583}
584
585/// The inverse of the core block $B$ (core × core: the core checks on the core parity bits
586/// $k, \dots, k + \mathrm{core} - 1$) of [`LdpcCode::from_core_rows`], row by row: row $j$
587/// lists the checks whose syndromes sum to the parity bit $k + j$.
588///
589/// It reduces $[I \mid B]$ over GF(2) ([`gf2::Matrix::reduce`], pivots searched from the
590/// last column): when $B$ is invertible every pivot falls in its columns, the reduced
591/// matrix is $[P B^{-1} \mid P]$ for a permutation $P$, and the row whose pivot is
592/// column $j$ of $B$ holds row $j$ of $B^{-1}$.
593fn core_inverse(rows: &[Vec<usize>], k: usize, core: usize) -> Result<Vec<Vec<usize>>> {
594    let augmented: Vec<Vec<usize>> = rows
595        .iter()
596        .take(core)
597        .enumerate()
598        .map(|(r, row)| std::iter::once(r).chain(row.iter().filter(|&&v| v >= k).map(|&v| core + v - k)).collect())
599        .collect();
600    let mut a = gf2::Matrix::from_rows(2 * core, &augmented);
601    let pivots = a.reduce();
602    let rank = pivots.iter().filter(|&&c| c >= core).count();
603    ensure(rank == core, || format!("the core block is singular (rank {rank} of {core})"))?;
604    let mut solve = vec![Vec::new(); core];
605    for (t, &c) in pivots.iter().enumerate() {
606        solve[c - core] = (0..core).filter(|&j| a.get(t, j)).collect();
607    }
608    Ok(solve)
609}
610
611/// The check of lowest degree among `cands` (in the graph `check_vars`), the ties
612/// broken by `rng`.
613fn lowest_degree(cands: &[usize], check_vars: &[Vec<usize>], rng: &mut comnum_core::rng::Rng) -> usize {
614    let d = cands.iter().map(|&c| check_vars[c].len()).min().unwrap_or(0);
615    let best: Vec<usize> = cands.iter().copied().filter(|&c| check_vars[c].len() == d).collect();
616    best[rng.below(best.len())]
617}
618
619/// The candidate checks of a new edge of the variable node `v` in progressive edge
620/// growth (Hu, Eleftheriou & Arnold 2005): the checks the breadth-first expansion from
621/// `v` never reaches, or, when it reaches them all, those of its last level not yet
622/// linked to `v` (any check not linked to `v` if none).
623fn farthest_checks(v: usize, check_vars: &[Vec<usize>], var_checks: &[Vec<usize>]) -> Vec<usize> {
624    let m = check_vars.len();
625    let mut reached = vec![false; m];
626    let mut frontier: VecDeque<usize> = VecDeque::new();
627    for &c in &var_checks[v] {
628        reached[c] = true;
629        frontier.push_back(c);
630    }
631    let mut last_new: Vec<usize> = var_checks[v].clone();
632    loop {
633        // the checks of the next level
634        let mut new = Vec::new();
635        let mut next = VecDeque::new();
636        while let Some(c) = frontier.pop_front() {
637            for &u in &check_vars[c] {
638                for &c2 in &var_checks[u] {
639                    if !reached[c2] {
640                        reached[c2] = true;
641                        new.push(c2);
642                        next.push_back(c2);
643                    }
644                }
645            }
646        }
647        if new.is_empty() || reached.iter().all(|r| *r) {
648            let unreached: Vec<usize> = (0..m).filter(|&c| !reached[c]).collect();
649            if !unreached.is_empty() {
650                return unreached;
651            }
652            let last = if new.is_empty() { last_new } else { new };
653            let cands: Vec<usize> = last.into_iter().filter(|c| !var_checks[v].contains(c)).collect();
654            return if cands.is_empty() { (0..m).filter(|c| !var_checks[v].contains(c)).collect() } else { cands };
655        }
656        last_new = new;
657        frontier = next;
658    }
659}
660
661/// The number of columns and the rows (0-based column indices, in the order of the
662/// file) of an `alist` text; the message of the error names what is wrong.
663fn parse_alist(text: &str) -> std::result::Result<(usize, Vec<Vec<usize>>), String> {
664    let mut it = alist_numbers(text)?.into_iter();
665    let mut next = |what: &str| it.next().ok_or_else(|| format!("the file ends before {what}"));
666    let (n, m) = (next("the number of columns")?, next("the number of rows")?);
667    if n == 0 || m == 0 {
668        return Err(format!("a matrix of {m} × {n}"));
669    }
670    let (max_col, max_row) = (next("the largest column degree")?, next("the largest row degree")?);
671    let mut col_deg = Vec::with_capacity(n);
672    for _ in 0..n {
673        col_deg.push(next("the column degrees")?);
674    }
675    let mut row_deg = Vec::with_capacity(m);
676    for _ in 0..m {
677        row_deg.push(next("the row degrees")?);
678    }
679    check_degrees(&col_deg, max_col, "column")?;
680    check_degrees(&row_deg, max_row, "row")?;
681    let edges: usize = col_deg.iter().sum();
682    if row_deg.iter().sum::<usize>() != edges {
683        return Err(format!(
684            "the column degrees count {edges} ones, the row degrees {}",
685            row_deg.iter().sum::<usize>()
686        ));
687    }
688    let rest: Vec<usize> = it.collect();
689    // the lists are padded with zeros up to the largest degree, or not
690    let padded = if rest.len() == n * max_col + m * max_row {
691        true
692    } else if rest.len() == 2 * edges {
693        false
694    } else {
695        return Err(format!(
696            "{} numbers after the degrees: {} with zero padding or {} without",
697            rest.len(),
698            n * max_col + m * max_row,
699            2 * edges
700        ));
701    };
702    let mut pos = 0;
703    let cols = alist_lists(&rest, &mut pos, &col_deg, padded.then_some(max_col), m, "column")?;
704    let rows = alist_lists(&rest, &mut pos, &row_deg, padded.then_some(max_row), n, "row")?;
705    for (c, col) in cols.iter().enumerate() {
706        for &r in col {
707            if !rows[r].contains(&c) {
708                return Err(format!("column {} lists row {} but row {} does not list it", c + 1, r + 1, r + 1));
709            }
710        }
711    }
712    Ok((n, rows))
713}
714
715/// The numbers of an `alist` text, the comments (after `#`) left out.
716fn alist_numbers(text: &str) -> std::result::Result<Vec<usize>, String> {
717    let mut numbers = Vec::new();
718    for (l, line) in text.lines().enumerate() {
719        let content = line.split('#').next().unwrap_or("");
720        for token in content.split_whitespace() {
721            numbers.push(token.parse::<usize>().map_err(|_| format!("line {}: {token:?} is not a number", l + 1))?);
722        }
723    }
724    Ok(numbers)
725}
726
727/// The degrees of the columns (or rows) are at most, and reach, the declared maximum.
728fn check_degrees(degrees: &[usize], max: usize, what: &str) -> std::result::Result<(), String> {
729    if degrees.iter().any(|&d| d > max) || degrees.iter().max() != Some(&max) {
730        return Err(format!("the {what} degrees do not have the maximum {max}"));
731    }
732    Ok(())
733}
734
735/// The lists of the columns (or rows) of degrees `degrees` read from `rest` at `pos`
736/// (0-based indices below `bound`), each padded to `width` with zeros when given.
737fn alist_lists(
738    rest: &[usize],
739    pos: &mut usize,
740    degrees: &[usize],
741    width: Option<usize>,
742    bound: usize,
743    what: &str,
744) -> std::result::Result<Vec<Vec<usize>>, String> {
745    let mut out = Vec::with_capacity(degrees.len());
746    for (i, &d) in degrees.iter().enumerate() {
747        let len = width.unwrap_or(d);
748        let entries = &rest[*pos..*pos + len];
749        *pos += len;
750        let (ones, zeros) = entries.split_at(d);
751        if zeros.iter().any(|&z| z != 0) {
752            return Err(format!("{what} {}: more entries than its degree {d}", i + 1));
753        }
754        let mut list = Vec::with_capacity(d);
755        for &e in ones {
756            if e == 0 || e > bound {
757                return Err(format!("{what} {}: the index {e} is outside 1 to {bound}", i + 1));
758            }
759            if list.contains(&(e - 1)) {
760                return Err(format!("{what} {}: the index {e} is repeated", i + 1));
761            }
762            list.push(e - 1);
763        }
764        out.push(list);
765    }
766    Ok(out)
767}
768
769impl BlockCode for LdpcCode {
770    fn k(&self) -> usize {
771        self.info.len()
772    }
773
774    fn n(&self) -> usize {
775        self.n
776    }
777
778    /// # Errors
779    /// [`Error::Shape`] unless `info` has $k$ bits; [`Error::Config`] for a code built
780    /// by [`LdpcCode::without_encoder`].
781    fn encode_block(&self, info: &[u8]) -> Result<Vec<u8>> {
782        expect_len(info.len(), self.k(), "bits")?;
783        if self.encoder == Encoder::External {
784            return Err(Error::Config(
785                "this LDPC code has no encoder of its own: use the encoder of its standard".into(),
786            ));
787        }
788        let mut c = vec![0u8; self.n];
789        for (&p, &b) in self.info.iter().zip(info) {
790            c[p] = b & 1;
791        }
792        match &self.encoder {
793            Encoder::Sums(parity) => {
794                for (p, deps) in parity {
795                    c[*p] = deps.iter().fold(0, |a, &v| a ^ c[v]);
796                }
797            }
798            Encoder::Accumulate => {
799                let k = self.info.len();
800                let mut prev = 0u8;
801                for (r, row) in self.rows.iter().enumerate() {
802                    let s = row.iter().filter(|&&v| v < k).fold(0, |a, &v| a ^ c[v]);
803                    prev ^= s;
804                    c[k + r] = prev;
805                }
806            }
807            Encoder::Core { core, solve } => {
808                let k = self.info.len();
809                let syndrome: Vec<u8> = self.rows[..*core]
810                    .iter()
811                    .map(|row| row.iter().filter(|&&v| v < k).fold(0, |a, &v| a ^ c[v]))
812                    .collect();
813                for (j, checks) in solve.iter().enumerate() {
814                    c[k + j] = checks.iter().fold(0, |a, &r| a ^ syndrome[r]);
815                }
816                for (r, row) in self.rows.iter().enumerate().skip(*core) {
817                    c[k + r] = row.iter().filter(|&&v| v != k + r).fold(0, |a, &v| a ^ c[v]);
818                }
819            }
820            Encoder::External => unreachable!("refused above"),
821        }
822        // the encoders solve H c = 0 by construction (the tests check every family)
823        debug_assert!(self.is_codeword(&c), "the encoder produced a non-codeword");
824        Ok(c)
825    }
826
827    fn decode_block(&self, llr: &[f64]) -> Result<Vec<u8>> {
828        let (post, _) = self.decode_llr(llr)?;
829        Ok(self.info.iter().map(|&p| u8::from(post[p] < 0.0)).collect())
830    }
831}
832
833impl SoftDecode for LdpcCode {
834    /// The a posteriori LLRs of the information bits of [`LdpcCode::decode_llr`]; the a
835    /// priori LLRs add to the channel LLRs of the information positions.
836    fn app_into(&self, llr: &[f64], apriori: Option<&[f64]>, out: &mut [f64]) -> Result<usize> {
837        expect_len(llr.len(), self.n, "LLRs")?;
838        expect_len(out.len(), self.k(), "a posteriori LLRs")?;
839        let mut channel = llr.to_vec();
840        if let Some(a) = apriori {
841            expect_len(a.len(), self.k(), "a priori LLRs")?;
842            for (&p, a) in self.info.iter().zip(a) {
843                channel[p] += a;
844            }
845        }
846        let mut post = channel.clone();
847        let it = self.propagate(&channel, &mut post);
848        for (o, &p) in out.iter_mut().zip(&self.info) {
849            *o = post[p];
850        }
851        Ok(it)
852    }
853}
854
855container_traits!(LdpcCode);
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860    use crate::block::testing::awgn_llr;
861
862    /// The progressive edge growth as it was written in one function, eight levels deep.
863    fn former_peg_rows(n: usize, k: usize, var_degree: usize, seed: u64) -> Vec<Vec<usize>> {
864        let m = n - k;
865        let mut rng = comnum_core::rng::Rng::new(seed);
866        let mut check_vars: Vec<Vec<usize>> = vec![Vec::new(); m];
867        let mut var_checks: Vec<Vec<usize>> = vec![Vec::new(); n];
868        let lowest = |cands: &[usize], cv: &[Vec<usize>], rng: &mut comnum_core::rng::Rng| -> usize {
869            let d = cands.iter().map(|&c| cv[c].len()).min().unwrap_or(0);
870            let best: Vec<usize> = cands.iter().copied().filter(|&c| cv[c].len() == d).collect();
871            best[rng.below(best.len())]
872        };
873        for v in 0..n {
874            for e in 0..var_degree {
875                let c = if e == 0 {
876                    let all: Vec<usize> = (0..m).collect();
877                    lowest(&all, &check_vars, &mut rng)
878                } else {
879                    // breadth-first expansion from v until no new check is reached
880                    let mut reached = vec![false; m];
881                    let mut frontier: VecDeque<usize> = VecDeque::new();
882                    for &c in &var_checks[v] {
883                        reached[c] = true;
884                        frontier.push_back(c);
885                    }
886                    let mut last_new: Vec<usize> = var_checks[v].clone();
887                    loop {
888                        let mut new = Vec::new();
889                        let mut next = VecDeque::new();
890                        while let Some(c) = frontier.pop_front() {
891                            for &u in &check_vars[c] {
892                                for &c2 in &var_checks[u] {
893                                    if !reached[c2] {
894                                        reached[c2] = true;
895                                        new.push(c2);
896                                        next.push_back(c2);
897                                    }
898                                }
899                            }
900                        }
901                        if new.is_empty() || reached.iter().all(|r| *r) {
902                            let unreached: Vec<usize> = (0..m).filter(|&c| !reached[c]).collect();
903                            break if unreached.is_empty() {
904                                let cands: Vec<usize> = if new.is_empty() { last_new.clone() } else { new };
905                                let cands: Vec<usize> =
906                                    cands.into_iter().filter(|c| !var_checks[v].contains(c)).collect();
907                                let cands = if cands.is_empty() {
908                                    (0..m).filter(|c| !var_checks[v].contains(c)).collect()
909                                } else {
910                                    cands
911                                };
912                                lowest(&cands, &check_vars, &mut rng)
913                            } else {
914                                lowest(&unreached, &check_vars, &mut rng)
915                            };
916                        }
917                        last_new = new;
918                        frontier = next;
919                    }
920                };
921                check_vars[c].push(v);
922                var_checks[v].push(c);
923            }
924        }
925        check_vars
926    }
927
928    #[test]
929    fn the_peg_graphs_are_those_of_the_former_construction() {
930        for (n, k, d, seed) in [(64, 32, 3, 1), (256, 128, 3, 7), (96, 48, 2, 3), (40, 10, 5, 2), (20, 18, 2, 1)] {
931            let code = LdpcCode::peg(n, k, d, seed).unwrap();
932            assert_eq!(code.rows(), former_peg_rows(n, k, d, seed), "({n}, {k}), degree {d}");
933        }
934    }
935
936    /// The Gauss–Jordan elimination of `[B | I]` that `from_core_rows` wrote itself
937    /// before it used `gf2::Matrix::reduce`.
938    fn former_core_inverse(rows: &[Vec<usize>], k: usize, core: usize) -> Option<Vec<Vec<usize>>> {
939        let words = core.div_ceil(64);
940        let mut a: Vec<Vec<u64>> = vec![vec![0; 2 * words]; core];
941        for (r, row) in rows.iter().take(core).enumerate() {
942            for &v in row.iter().filter(|&&v| v >= k) {
943                a[r][(v - k) / 64] ^= 1 << ((v - k) % 64);
944            }
945            a[r][words + r / 64] |= 1 << (r % 64);
946        }
947        for col in 0..core {
948            let (w, b) = (col / 64, 1u64 << (col % 64));
949            let pivot = (col..core).find(|&r| a[r][w] & b != 0)?;
950            a.swap(col, pivot);
951            let prow = a[col].clone();
952            for (r, row) in a.iter_mut().enumerate() {
953                if r != col && row[w] & b != 0 {
954                    for (x, y) in row.iter_mut().zip(&prow) {
955                        *x ^= y;
956                    }
957                }
958            }
959        }
960        Some(a.iter().map(|row| (0..core).filter(|&c| row[words + c / 64] & (1 << (c % 64)) != 0).collect()).collect())
961    }
962
963    /// The inverse of the core block of both base graphs of 5G NR, at lifting sizes of
964    /// every set, is the one of the former elimination; a singular block is an error.
965    #[test]
966    fn the_core_inverse_is_the_former_elimination() {
967        use crate::nr::tables::{BG1, BG2, LIFTING_SETS};
968        for (table, cols_k) in [(&BG1[..], 22), (&BG2[..], 10)] {
969            for (ils, z) in [(0, 2), (1, 384), (2, 10), (3, 7), (4, 36), (5, 352), (6, 208), (7, 15)] {
970                assert!(LIFTING_SETS[ils].contains(&z));
971                let rows_n = table.iter().map(|e| e.0 as usize).max().unwrap() + 1;
972                let mut rows: Vec<Vec<usize>> = vec![Vec::new(); rows_n * z];
973                for &(i, j, v) in table {
974                    let shift = v[ils] as usize % z;
975                    for r in 0..z {
976                        rows[i as usize * z + r].push(j as usize * z + (r + shift) % z);
977                    }
978                }
979                let (k, core) = (cols_k * z, 4 * z);
980                let new = core_inverse(&rows, k, core).unwrap();
981                assert_eq!(Some(new), former_core_inverse(&rows, k, core), "z = {z}");
982            }
983        }
984        // two equal core checks: singular for both
985        let rows = vec![vec![0, 2], vec![0, 2], vec![1, 2, 3, 4]];
986        assert!(core_inverse(&rows, 2, 2).is_err() && former_core_inverse(&rows, 2, 2).is_none());
987        assert!(LdpcCode::from_core_rows(5, rows, 2, 2).is_err());
988    }
989
990    #[test]
991    fn the_container_traits_fill_the_output_in_place() {
992        use comnum_core::signal::{Bits, Llr};
993        use comnum_core::traits::{Decode, Encode};
994        let mut code = LdpcCode::peg(96, 48, 3, 1).unwrap();
995        let rows: Vec<Vec<u8>> = (0..2).map(|s| (0..96).map(|i| u8::from((s + i) % 3 == 0)).collect()).collect();
996        let info = Bits::from_streams(&rows).unwrap();
997        let mut coded = Bits::zeros(2, 192);
998        let p = coded.as_slice().as_ptr();
999        code.encode_into(&info, &mut coded).unwrap();
1000        assert_eq!(coded.as_slice().as_ptr(), p);
1001        assert_eq!(coded.stream(1), code.encode_stream(info.stream(1)).unwrap());
1002        let llr = Llr::new(coded.as_array().mapv(|b| 4.0 * (1.0 - 2.0 * f64::from(b))));
1003        let mut decoded = Bits::zeros(2, 96);
1004        let p = decoded.as_slice().as_ptr();
1005        code.decode_into(&llr, &mut decoded).unwrap();
1006        assert_eq!((decoded.as_slice().as_ptr(), decoded.as_slice()), (p, info.as_slice()));
1007        assert!(code.encode_into(&Bits::zeros(1, 95), &mut coded).is_err());
1008    }
1009    use comnum_core::rng::Rng;
1010    use comnum_core::signal::hard_decisions;
1011
1012    #[test]
1013    fn peg_builds_a_regular_code_without_four_cycles() {
1014        let code = LdpcCode::peg(504, 252, 3, 2).unwrap();
1015        assert_eq!(code.k(), 252);
1016        assert!(code.girth() >= 6, "girth {}", code.girth());
1017        let mut deg = vec![0; 504];
1018        for r in code.rows() {
1019            for &v in r {
1020                deg[v] += 1;
1021            }
1022        }
1023        assert!(deg.iter().all(|d| *d == 3));
1024        let mut r = Rng::new(3);
1025        for _ in 0..20 {
1026            let info: Vec<u8> = (0..252).map(|_| r.bit()).collect();
1027            assert!(code.is_codeword(&code.encode_block(&info).unwrap()));
1028        }
1029    }
1030
1031    #[test]
1032    fn every_schedule_and_rule_decodes_and_layered_converges_faster() {
1033        let mut code = LdpcCode::peg(504, 252, 3, 4).unwrap();
1034        let mut r = Rng::new(5);
1035        let info: Vec<u8> = (0..252).map(|_| r.bit()).collect();
1036        let c = code.encode_block(&info).unwrap();
1037        // Eb/N0 = 2.5 dB at rate 1/2: σ² = 1/(2 R Eb/N0)
1038        let s2 = 1.0 / 10f64.powf(0.25);
1039        let y = awgn_llr(&c, s2, &mut r);
1040        let mut its = Vec::new();
1041        for layered in [false, true] {
1042            for rule in [CheckRule::SumProduct, CheckRule::NormalizedMinSum(0.8), CheckRule::OffsetMinSum(0.15)] {
1043                code.decoder.layered = layered;
1044                code.decoder.rule = rule;
1045                let (post, it) = code.decode_llr(&y).unwrap();
1046                assert!(code.is_codeword(&hard_decisions(&post)), "{rule:?} layered={layered}");
1047                assert_eq!(code.decode_block(&y).unwrap(), info);
1048                if rule == CheckRule::SumProduct {
1049                    its.push(it);
1050                }
1051            }
1052        }
1053        assert!(its[1] <= its[0], "flooding {} vs layered {} iterations", its[0], its[1]);
1054    }
1055
1056    #[test]
1057    fn alist_files_round_trip_with_or_without_padding_and_comments() {
1058        let code = LdpcCode::peg(96, 48, 3, 7).unwrap();
1059        let text = code.to_alist();
1060        assert_eq!(LdpcCode::from_alist(&text).unwrap(), code);
1061        // the same matrix without the zero padding, with comments and blank lines
1062        let hamming_padded = "7 3\n3 4\n3 2 2 2 1 1 1\n4 4 4\n1 2 3\n1 2 0\n1 3 0\n2 3 0\n1 0 0\n2 0 0\n\
1063                              3 0 0\n1 2 3 5\n1 2 4 6\n1 3 4 7\n";
1064        let hamming_bare = "# the (7, 4) Hamming code\n7 3\n\n3 4 # degrees\n3 2 2 2 1 1 1\n4 4 4\n\
1065                            1 2 3\n1 2\n1 3\n2 3\n1\n2\n3\n1 2 3 5\n1 2 4 6\n1 3 4 7\n";
1066        let a = LdpcCode::from_alist(hamming_padded).unwrap();
1067        assert_eq!(LdpcCode::from_alist(hamming_bare).unwrap(), a);
1068        assert_eq!(a.rows(), &[vec![0, 1, 2, 4], vec![0, 1, 3, 5], vec![0, 2, 3, 6]]);
1069        let dir = std::env::temp_dir().join(format!("comnum-fec-alist-{}", std::process::id()));
1070        std::fs::create_dir_all(&dir).unwrap();
1071        let path = dir.join("hamming.alist");
1072        std::fs::write(&path, hamming_bare).unwrap();
1073        assert_eq!(LdpcCode::from_alist_file(&path).unwrap(), a);
1074        std::fs::remove_dir_all(&dir).unwrap();
1075        assert!(matches!(LdpcCode::from_alist_file(dir.join("missing.alist")), Err(Error::Io(_))));
1076    }
1077
1078    #[test]
1079    fn malformed_alist_files_are_rejected() {
1080        let good = "7 3\n3 4\n3 2 2 2 1 1 1\n4 4 4\n1 2 3\n1 2\n1 3\n2 3\n1\n2\n3\n1 2 3 5\n1 2 4 6\n1 3 4 7\n";
1081        assert!(LdpcCode::from_alist(good).is_ok());
1082        let cases = [
1083            ("", "ends before"),
1084            ("7 3\n3 4\n3 2 2 2 1 1 1\n", "ends before"),
1085            (&good.replace("1 3 4 7", "1 3 4 x"), "not a number"),
1086            (&good.replace("1 3 4 7", "1 3 4"), "numbers after the degrees"),
1087            (&good.replace("1 3 4 7", "1 3 4 9"), "outside"),
1088            (&good.replace("1 3 4 7", "1 3 3 7"), "repeated"),
1089            (&good.replace("1 3 4 7", "1 3 5 7"), "does not list"),
1090            (&good.replace("3 4\n3 2", "3 5\n3 2"), "maximum"),
1091        ];
1092        for (text, message) in cases {
1093            match LdpcCode::from_alist(text) {
1094                Err(Error::Config(e)) => assert!(e.contains(message), "{e:?} should name {message:?}"),
1095                other => panic!("{text:?} gave {other:?}"),
1096            }
1097        }
1098    }
1099
1100    #[test]
1101    fn from_rows_handles_a_hamming_code() {
1102        // the (7, 4) Hamming code
1103        let code = LdpcCode::from_rows(7, vec![vec![0, 1, 2, 4], vec![0, 1, 3, 5], vec![0, 2, 3, 6]]).unwrap();
1104        assert_eq!(code.k(), 4);
1105        assert!(LdpcCode::from_rows(3, vec![vec![0, 5]]).is_err());
1106    }
1107}
1108
1109/// Checks against the published constructions (`references/`).
1110#[cfg(test)]
1111mod literature {
1112    use super::*;
1113
1114    /// X.-Y. Hu, E. Eleftheriou and D. M. Arnold, "Regular and irregular progressive
1115    /// edge-growth Tanner graphs," IEEE Trans. Inf. Theory 51(1), 2005, Sec. VI
1116    /// (pp. 393-394): the regular (504, 252) PEG code with symbol degree 3 has girth 8
1117    /// (a few nodes 10), and its check degrees are 6 except for a few nodes of degrees 5
1118    /// and 7 (the paper: 8 of each).
1119    #[test]
1120    fn hu_2005_peg_504_252_has_girth_8() {
1121        for seed in [0, 1, 2] {
1122            let code = LdpcCode::peg(504, 252, 3, seed).unwrap();
1123            assert_eq!(code.girth(), 8, "seed {seed}");
1124            let mut degrees = std::collections::BTreeMap::new();
1125            for r in code.rows() {
1126                *degrees.entry(r.len()).or_insert(0usize) += 1;
1127            }
1128            assert_eq!(degrees.keys().copied().collect::<Vec<_>>(), [5, 6, 7], "seed {seed}: {degrees:?}");
1129            assert_eq!(degrees[&5], degrees[&7]);
1130            assert!(degrees[&5] <= 10 && degrees[&6] >= 230, "seed {seed}: {degrees:?}");
1131        }
1132    }
1133    #[test]
1134    fn a_code_without_encoder_decodes_and_refuses_to_encode() {
1135        // the Hamming (7, 4) code, its information bits at 0..4 (H c = 0 for 1101 010)
1136        let rows = vec![vec![0, 1, 2, 4], vec![0, 1, 3, 5], vec![0, 2, 3, 6]];
1137        let code = LdpcCode::without_encoder(7, rows, vec![0, 1, 2, 3]).unwrap();
1138        assert!(code.encode_block(&[1, 0, 1, 1]).is_err());
1139        let c = [1u8, 1, 0, 1, 0, 1, 0];
1140        assert!(code.is_codeword(&c));
1141        let llr: Vec<f64> = c.iter().map(|&b| if b == 0 { 3.0 } else { -3.0 }).collect();
1142        assert_eq!(code.decode_block(&llr).unwrap(), [1, 1, 0, 1]);
1143        assert!(LdpcCode::without_encoder(7, vec![vec![0, 9]], vec![0]).is_err());
1144        assert!(LdpcCode::without_encoder(7, vec![vec![0, 1]], vec![0, 0]).is_err());
1145    }
1146}