mfsk_core/fec/ldpc/mod.rs
1//! LDPC (174, 91) codec with CRC-14 (polynomial 0x2757).
2//!
3//! This is the Forward Error Correction layer shared by FT8, FT4, FT2 and
4//! FST4. The code itself is identical across protocols; different message
5//! payloads (always 77 information bits) plus a 14-bit CRC are systematically
6//! encoded to 174 codeword bits.
7//!
8//! ## Organisation
9//!
10//! | Module | Role |
11//! |---------------|---------------------------------------------------|
12//! | [`tables`] | Parity-check matrix (MN / NM / NRW) — static data |
13//! | [`bp`] | Belief-propagation soft-decision decoder |
14//! | [`osd`] | Ordered-statistics decoder (order 0..4) fallback |
15//!
16//! ## Public surface
17//!
18//! - [`Ldpc174_91`] — zero-sized type implementing [`crate::core::FecCodec`].
19//! - [`bp::bp_decode`] / [`osd::osd_decode_deep`] / [`osd::ldpc_encode`] — raw
20//! functions kept stable for the existing ft8-core callers that integrate
21//! CRC checks and AP hints directly.
22
23pub mod bp;
24pub mod osd;
25pub mod params;
26pub mod tables;
27
28pub use bp::{BpResult, bp_decode, check_crc14, crc14};
29pub use osd::{OsdResult, ldpc_encode, osd_decode, osd_decode_deep, osd_decode_deep4};
30pub use params::{Ldpc174_91Params, Ldpc240_101Params, LdpcParams};
31
32use crate::core::{FecCodec, FecOpts, FecResult};
33
34/// Codeword length of the WSJT LDPC code.
35pub const LDPC_N: usize = 174;
36/// Information-bit length (77 message bits + 14 CRC).
37pub const LDPC_K: usize = 91;
38/// Parity-bit count.
39pub const LDPC_M: usize = LDPC_N - LDPC_K; // 83
40
41/// Zero-sized codec implementing [`FecCodec`] for the WSJT LDPC(174, 91) code.
42///
43/// All tables are `const` / `static` so the type carries no data — any
44/// concrete protocol (FT8/FT4/FT2/FST4) may share a single instance.
45#[derive(Copy, Clone, Debug, Default)]
46pub struct Ldpc174_91;
47
48impl FecCodec for Ldpc174_91 {
49 const N: usize = LDPC_N;
50 const K: usize = LDPC_K;
51
52 fn encode(&self, info: &[u8], codeword: &mut [u8]) {
53 assert_eq!(info.len(), LDPC_K, "info must be {} bits", LDPC_K);
54 assert_eq!(codeword.len(), LDPC_N, "codeword must be {} bits", LDPC_N);
55 let mut arr = [0u8; LDPC_K];
56 arr.copy_from_slice(info);
57 let cw = ldpc_encode(&arr);
58 codeword.copy_from_slice(&cw);
59 }
60
61 fn decode_soft(&self, llr: &[f32], opts: &FecOpts<'_>) -> Option<FecResult> {
62 assert_eq!(llr.len(), LDPC_N, "llr must be {} values", LDPC_N);
63 let mut llr_arr = [0f32; LDPC_N];
64 llr_arr.copy_from_slice(llr);
65
66 // Apply AP hint: for every `mask[i] == 1`, clamp LLR to ±apmag
67 // according to `values[i]` (1 → +apmag, 0 → −apmag).
68 // `apmag = max(|llr|) · 1.01` gives the AP bits a stronger vote than
69 // any channel observation, matching WSJT-X convention.
70 let ap_storage;
71 let ap_mask: Option<&[bool; LDPC_N]> = match opts.ap_mask {
72 Some((mask, values)) => {
73 assert_eq!(mask.len(), LDPC_N, "ap mask must be {} bits", LDPC_N);
74 assert_eq!(values.len(), LDPC_N, "ap values must be {} bits", LDPC_N);
75 let apmag = llr_arr.iter().map(|x| x.abs()).fold(0.0f32, f32::max) * 1.01;
76 let mut a = [false; LDPC_N];
77 for i in 0..LDPC_N {
78 if mask[i] != 0 {
79 a[i] = true;
80 llr_arr[i] = if values[i] != 0 { apmag } else { -apmag };
81 }
82 }
83 ap_storage = a;
84 Some(&ap_storage)
85 }
86 None => None,
87 };
88
89 if let Some(r) = bp_decode(&llr_arr, ap_mask, opts.bp_max_iter, opts.verify_info) {
90 // Phase 0c-B: BpResult.info is a Vec<u8> of length P::K
91 // already, so no copy/reconstruction needed.
92 return Some(FecResult {
93 info: r.info,
94 hard_errors: r.hard_errors,
95 iterations: r.iterations,
96 });
97 }
98
99 if opts.osd_depth == 0 {
100 return None;
101 }
102
103 let r = if opts.osd_depth >= 4 {
104 osd_decode_deep4(&llr_arr, 30, opts.verify_info)?
105 } else {
106 osd_decode_deep(&llr_arr, opts.osd_depth.min(3) as u8, opts.verify_info)?
107 };
108 Some(FecResult {
109 info: r.info,
110 hard_errors: r.hard_errors,
111 iterations: 0,
112 })
113 }
114}