Skip to main content

mfsk_core/fec/ldpc240_101/
mod.rs

1//! LDPC(240, 101) codec with CRC-24 for the WSJT FST4 / FST4W family.
2//!
3//! Phase 0c-B unified the implementation: this module no longer carries
4//! its own BP / OSD / encode bodies — those live in
5//! [`crate::fec::ldpc`] and are parameterised by [`Ldpc240_101Params`].
6//! All that remains here are the protocol-specific tables (in [`tables`])
7//! and the CRC-24 helpers ([`crc24`], [`check_crc24`]).
8//!
9//! The public surface is preserved as a type alias:
10//! `pub type Ldpc240_101 = LdpcCodec<Ldpc240_101Params>` lives here so
11//! callers continue to write `mfsk_core::fec::Ldpc240_101`.
12
13pub mod tables;
14
15use crate::core::{FecCodec, FecOpts, FecResult};
16use crate::fec::ldpc::bp::bp_decode_generic;
17use crate::fec::ldpc::osd::{ldpc_encode_generic, osd_decode_generic};
18use crate::fec::ldpc::params::Ldpc240_101Params;
19
20pub const LDPC_N: usize = 240;
21pub const LDPC_K: usize = 101;
22pub const LDPC_M: usize = LDPC_N - LDPC_K; // 139
23
24// ────────────────────────────────────────────────────────────────────
25// CRC-24
26
27/// CRC-24Q as used by WSJT-X FST4: polynomial 0x100065B, applied bit-
28/// serially over the message padded with 24 zeros.
29///
30/// Matches the `get_crc24` subroutine in WSJT-X `lib/fst4/get_crc24.f90`.
31pub fn crc24(bits: &[u8]) -> u32 {
32    let mut r = [0u8; 25];
33    for (i, slot) in r.iter_mut().enumerate() {
34        *slot = if i < bits.len() { bits[i] & 1 } else { 0 };
35    }
36    const POLY: [u8; 25] = [
37        1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 0, 1, 1,
38    ];
39    let n = bits.len().saturating_sub(25);
40    for i in 0..=n {
41        if i + 25 <= bits.len() {
42            r[24] = bits[i + 24] & 1;
43        } else {
44            r[24] = 0;
45        }
46        let top = r[0];
47        if top != 0 {
48            for (rv, pv) in r.iter_mut().zip(POLY.iter()) {
49                *rv ^= *pv;
50            }
51        }
52        let first = r[0];
53        for k in 0..24 {
54            r[k] = r[k + 1];
55        }
56        r[24] = first;
57    }
58    let mut v = 0u32;
59    for &b in &r[..24] {
60        v = (v << 1) | (b as u32);
61    }
62    v
63}
64
65/// Verify CRC-24 for a 101-bit decoded word (77 msg + 24 CRC).
66///
67/// Accepts any `&[u8]` slice; lengths other than [`LDPC_K`] (= 101) are
68/// rejected so the function is suitable as a `MessageCodec::verify_info`
69/// implementation passed through `FecOpts::verify_info`.
70pub fn check_crc24(decoded: &[u8]) -> bool {
71    if decoded.len() != LDPC_K {
72        return false;
73    }
74    let mut with_zero = [0u8; LDPC_K];
75    with_zero[..77].copy_from_slice(&decoded[..77]);
76    let expected = crc24(&with_zero);
77
78    let mut got = 0u32;
79    for &b in &decoded[77..101] {
80        got = (got << 1) | (b as u32 & 1);
81    }
82    expected == got
83}
84
85// ────────────────────────────────────────────────────────────────────
86// FecCodec impl
87
88/// Zero-sized LDPC(240, 101) codec — a thin wrapper that pins the
89/// generic [`crate::fec::ldpc::params::LdpcParams`]-based
90/// implementation to [`Ldpc240_101Params`].
91#[derive(Copy, Clone, Debug, Default)]
92pub struct Ldpc240_101;
93
94impl FecCodec for Ldpc240_101 {
95    const N: usize = LDPC_N;
96    const K: usize = LDPC_K;
97
98    fn encode(&self, info: &[u8], codeword: &mut [u8]) {
99        assert_eq!(info.len(), LDPC_K, "info must be {} bits", LDPC_K);
100        assert_eq!(codeword.len(), LDPC_N, "codeword must be {} bits", LDPC_N);
101        ldpc_encode_generic::<Ldpc240_101Params>(info, codeword);
102    }
103
104    fn decode_soft(&self, llr: &[f32], opts: &FecOpts<'_>) -> Option<FecResult> {
105        assert_eq!(llr.len(), LDPC_N, "llr must be {} values", LDPC_N);
106        let mut llr_arr = vec![0f32; LDPC_N];
107        llr_arr.copy_from_slice(llr);
108
109        // AP hint injection (same convention as Ldpc174_91): clamp the
110        // masked bits to ±apmag where apmag dominates any channel
111        // observation, then build a parallel bool mask the BP loop
112        // consults to skip variable-node updates on those bits.
113        let ap_storage_holder;
114        let ap_slice: Option<&[bool]> = match opts.ap_mask {
115            Some((mask, values)) => {
116                assert_eq!(mask.len(), LDPC_N, "ap mask must be {} bits", LDPC_N);
117                assert_eq!(values.len(), LDPC_N, "ap values must be {} bits", LDPC_N);
118                let apmag = llr_arr.iter().map(|x| x.abs()).fold(0.0f32, f32::max) * 1.01;
119                let mut a = vec![false; LDPC_N];
120                for i in 0..LDPC_N {
121                    if mask[i] != 0 {
122                        a[i] = true;
123                        llr_arr[i] = if values[i] != 0 { apmag } else { -apmag };
124                    }
125                }
126                ap_storage_holder = a;
127                Some(ap_storage_holder.as_slice())
128            }
129            None => None,
130        };
131
132        if let Some(r) = bp_decode_generic::<Ldpc240_101Params>(
133            &llr_arr,
134            ap_slice,
135            opts.bp_max_iter,
136            opts.verify_info,
137        ) {
138            return Some(FecResult {
139                info: r.info,
140                hard_errors: r.hard_errors,
141                iterations: r.iterations,
142            });
143        }
144
145        if opts.osd_depth == 0 {
146            return None;
147        }
148
149        let r = osd_decode_generic::<Ldpc240_101Params>(
150            &llr_arr,
151            opts.osd_depth.min(3) as u8,
152            LDPC_K,
153            opts.verify_info,
154        )?;
155        Some(FecResult {
156            info: r.info,
157            hard_errors: r.hard_errors,
158            iterations: 0,
159        })
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    use crate::fec::ldpc::bp::bp_decode_generic;
167    use crate::fec::ldpc::osd::ldpc_encode_generic;
168
169    /// Round-trip: encode a 101-bit info word, feed perfect LLRs,
170    /// decoder should recover the original info. Exercises the full
171    /// BP path plus the generator sub-matrix via the generic helpers.
172    #[test]
173    fn roundtrip_perfect_llr() {
174        let mut info = [0u8; LDPC_K];
175        for i in 0..77 {
176            info[i] = ((i * 7 + 3) & 1) as u8;
177        }
178        let crc = crc24(&info); // upper 24 bits still zero
179        for i in 0..24 {
180            info[77 + i] = ((crc >> (23 - i)) & 1) as u8;
181        }
182
183        let mut cw = [0u8; LDPC_N];
184        ldpc_encode_generic::<Ldpc240_101Params>(&info, &mut cw);
185        // Sanity: systematic encode keeps info bits in positions 0..K.
186        assert_eq!(&cw[..LDPC_K], &info[..]);
187
188        // Perfect LLR: ±8 per bit, sign follows the bit.
189        let mut llr = vec![0f32; LDPC_N];
190        for i in 0..LDPC_N {
191            llr[i] = if cw[i] == 1 { 8.0 } else { -8.0 };
192        }
193        let r = bp_decode_generic::<Ldpc240_101Params>(&llr, None, 30, Some(check_crc24))
194            .expect("BP converges on perfect LLR");
195        assert_eq!(&r.info[..77], &info[..77]);
196    }
197}