1use crate::block::{BlockCode, SoftDecode, container_traits, expect_len};
71use comnum_core::{Error, Result, ensure};
72use comnum_math::gf2;
73use std::collections::VecDeque;
74
75pub const DEFAULT_RULE: CheckRule = CheckRule::SumProduct;
78pub const DEFAULT_ALPHA: f64 = 0.8;
81pub const DEFAULT_LAYERED: bool = true;
83pub const DEFAULT_MAX_ITERATIONS: usize = 50;
85pub const DEFAULT_VAR_DEGREE: usize = 3;
87pub const DEFAULT_PEG_SEED: u64 = 1;
89
90const TANH_BOUND: f64 = 0.999_999_999_999;
94
95#[derive(Clone, Copy, Debug, PartialEq)]
97#[non_exhaustive]
98pub enum CheckRule {
99 SumProduct,
101 NormalizedMinSum(f64),
103 OffsetMinSum(f64),
105}
106
107impl CheckRule {
108 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 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#[derive(Clone, Debug, PartialEq)]
146enum Encoder {
147 Sums(Vec<(usize, Vec<usize>)>),
150 Accumulate,
154 Core { core: usize, solve: Vec<Vec<usize>> },
161 External,
164}
165
166#[derive(Clone, Copy, Debug, PartialEq)]
168pub struct LdpcDecoder {
169 pub rule: CheckRule,
171 pub layered: bool,
173 pub max_iterations: usize,
175}
176
177impl Default for LdpcDecoder {
178 fn default() -> LdpcDecoder {
180 LdpcDecoder { rule: DEFAULT_RULE, layered: DEFAULT_LAYERED, max_iterations: DEFAULT_MAX_ITERATIONS }
181 }
182}
183
184#[derive(Clone, Debug, PartialEq)]
186pub struct LdpcCode {
187 n: usize,
188 rows: Vec<Vec<usize>>,
190 start: Vec<usize>,
193 info: Vec<usize>,
195 encoder: Encoder,
197 pub decoder: LdpcDecoder,
199}
200
201impl LdpcCode {
202 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 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 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 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 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 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 let solve = core_inverse(&rows, k, core)?;
311 Ok(LdpcCode::assemble(n, rows, (0..k).collect(), Encoder::Core { core, solve }))
312 }
313
314 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 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 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 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 pub fn info_positions(&self) -> &[usize] {
424 &self.info
425 }
426
427 pub fn rows(&self) -> &[Vec<usize>] {
429 &self.rows
430 }
431
432 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 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 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 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 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 fn propagate(&self, channel: &[f64], post: &mut [f64]) -> usize {
535 if self.hard_decisions_are_a_codeword(post) {
536 return 0;
537 }
538 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 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
585fn 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
611fn 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
619fn 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 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
661fn 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 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
715fn 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
727fn 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
735fn 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 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 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 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 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 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 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 #[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 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 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 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 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#[cfg(test)]
1111mod literature {
1112 use super::*;
1113
1114 #[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 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}