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 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}