Skip to main content

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 tables;
26
27pub use bp::{BpResult, bp_decode, check_crc14, crc14};
28pub use osd::{OsdResult, ldpc_encode, osd_decode, osd_decode_deep, osd_decode_deep4};
29
30use crate::core::{FecCodec, FecOpts, FecResult};
31
32/// Codeword length of the WSJT LDPC code.
33pub const LDPC_N: usize = 174;
34/// Information-bit length (77 message bits + 14 CRC).
35pub const LDPC_K: usize = 91;
36/// Parity-bit count.
37pub const LDPC_M: usize = LDPC_N - LDPC_K; // 83
38
39/// Zero-sized codec implementing [`FecCodec`] for the WSJT LDPC(174, 91) code.
40///
41/// All tables are `const` / `static` so the type carries no data — any
42/// concrete protocol (FT8/FT4/FT2/FST4) may share a single instance.
43#[derive(Copy, Clone, Debug, Default)]
44pub struct Ldpc174_91;
45
46impl FecCodec for Ldpc174_91 {
47    const N: usize = LDPC_N;
48    const K: usize = LDPC_K;
49
50    fn encode(&self, info: &[u8], codeword: &mut [u8]) {
51        assert_eq!(info.len(), LDPC_K, "info must be {} bits", LDPC_K);
52        assert_eq!(codeword.len(), LDPC_N, "codeword must be {} bits", LDPC_N);
53        let mut arr = [0u8; LDPC_K];
54        arr.copy_from_slice(info);
55        let cw = ldpc_encode(&arr);
56        codeword.copy_from_slice(&cw);
57    }
58
59    fn decode_soft(&self, llr: &[f32], opts: &FecOpts<'_>) -> Option<FecResult> {
60        assert_eq!(llr.len(), LDPC_N, "llr must be {} values", LDPC_N);
61        let mut llr_arr = [0f32; LDPC_N];
62        llr_arr.copy_from_slice(llr);
63
64        // Apply AP hint: for every `mask[i] == 1`, clamp LLR to ±apmag
65        // according to `values[i]` (1 → +apmag, 0 → −apmag).
66        // `apmag = max(|llr|) · 1.01` gives the AP bits a stronger vote than
67        // any channel observation, matching WSJT-X convention.
68        let ap_storage;
69        let ap_mask: Option<&[bool; LDPC_N]> = match opts.ap_mask {
70            Some((mask, values)) => {
71                assert_eq!(mask.len(), LDPC_N, "ap mask must be {} bits", LDPC_N);
72                assert_eq!(values.len(), LDPC_N, "ap values must be {} bits", LDPC_N);
73                let apmag = llr_arr.iter().map(|x| x.abs()).fold(0.0f32, f32::max) * 1.01;
74                let mut a = [false; LDPC_N];
75                for i in 0..LDPC_N {
76                    if mask[i] != 0 {
77                        a[i] = true;
78                        llr_arr[i] = if values[i] != 0 { apmag } else { -apmag };
79                    }
80                }
81                ap_storage = a;
82                Some(&ap_storage)
83            }
84            None => None,
85        };
86
87        if let Some(r) = bp_decode(&llr_arr, ap_mask, opts.bp_max_iter) {
88            let mut info = vec![0u8; LDPC_K];
89            info[..77].copy_from_slice(&r.message77);
90            info[77..].copy_from_slice(&r.codeword[77..LDPC_K]);
91            return Some(FecResult {
92                info,
93                hard_errors: r.hard_errors,
94                iterations: r.iterations,
95            });
96        }
97
98        if opts.osd_depth == 0 {
99            return None;
100        }
101
102        let r = if opts.osd_depth >= 4 {
103            osd_decode_deep4(&llr_arr, 30)?
104        } else {
105            osd_decode_deep(&llr_arr, opts.osd_depth.min(3) as u8)?
106        };
107        let mut info = vec![0u8; LDPC_K];
108        info[..77].copy_from_slice(&r.message77);
109        info[77..].copy_from_slice(&r.codeword[77..LDPC_K]);
110        Some(FecResult {
111            info,
112            hard_errors: r.hard_errors,
113            iterations: 0,
114        })
115    }
116}