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//! Algorithm mirrors [`super::ldpc`] for LDPC(174, 91); this module
4//! supplies the larger code's parity-check tables, generator sub-matrix
5//! and CRC polynomial. Algorithm-level improvements added to either
6//! module should be mirrored in the other — or, better, the shared
7//! BP / OSD bodies should be promoted to code-size-generic helpers
8//! (a natural follow-up now that a second concrete size exists).
9
10pub mod bp;
11pub mod osd;
12pub mod tables;
13
14pub use bp::{BpResult, bp_decode, check_crc24, crc24};
15pub use osd::{OsdResult, ldpc_encode, osd_decode, osd_decode_deep};
16
17use crate::core::{FecCodec, FecOpts, FecResult};
18
19pub const LDPC_N: usize = 240;
20pub const LDPC_K: usize = 101;
21pub const LDPC_M: usize = LDPC_N - LDPC_K; // 139
22
23/// Zero-sized LDPC(240, 101) codec.
24#[derive(Copy, Clone, Debug, Default)]
25pub struct Ldpc240_101;
26
27impl FecCodec for Ldpc240_101 {
28    const N: usize = LDPC_N;
29    const K: usize = LDPC_K;
30
31    fn encode(&self, info: &[u8], codeword: &mut [u8]) {
32        assert_eq!(info.len(), LDPC_K, "info must be {} bits", LDPC_K);
33        assert_eq!(codeword.len(), LDPC_N, "codeword must be {} bits", LDPC_N);
34        let mut arr = [0u8; LDPC_K];
35        arr.copy_from_slice(info);
36        let cw = ldpc_encode(&arr);
37        codeword.copy_from_slice(&cw);
38    }
39
40    fn decode_soft(&self, llr: &[f32], opts: &FecOpts<'_>) -> Option<FecResult> {
41        assert_eq!(llr.len(), LDPC_N, "llr must be {} values", LDPC_N);
42        let mut llr_arr = [0f32; LDPC_N];
43        llr_arr.copy_from_slice(llr);
44
45        // AP hint injection (same convention as Ldpc174_91).
46        let ap_storage;
47        let ap_mask: Option<&[bool; LDPC_N]> = match opts.ap_mask {
48            Some((mask, values)) => {
49                assert_eq!(mask.len(), LDPC_N, "ap mask must be {} bits", LDPC_N);
50                assert_eq!(values.len(), LDPC_N, "ap values must be {} bits", LDPC_N);
51                let apmag = llr_arr.iter().map(|x| x.abs()).fold(0.0f32, f32::max) * 1.01;
52                let mut a = [false; LDPC_N];
53                for i in 0..LDPC_N {
54                    if mask[i] != 0 {
55                        a[i] = true;
56                        llr_arr[i] = if values[i] != 0 { apmag } else { -apmag };
57                    }
58                }
59                ap_storage = a;
60                Some(&ap_storage)
61            }
62            None => None,
63        };
64
65        if let Some(r) = bp_decode(&llr_arr, ap_mask, opts.bp_max_iter) {
66            let mut info = vec![0u8; LDPC_K];
67            info[..77].copy_from_slice(&r.message77);
68            info[77..].copy_from_slice(&r.codeword[77..LDPC_K]);
69            return Some(FecResult {
70                info,
71                hard_errors: r.hard_errors,
72                iterations: r.iterations,
73            });
74        }
75
76        if opts.osd_depth == 0 {
77            return None;
78        }
79
80        let r = osd_decode_deep(&llr_arr, opts.osd_depth.min(3) as u8)?;
81        let mut info = vec![0u8; LDPC_K];
82        info[..77].copy_from_slice(&r.message77);
83        info[77..].copy_from_slice(&r.codeword[77..LDPC_K]);
84        Some(FecResult {
85            info,
86            hard_errors: r.hard_errors,
87            iterations: 0,
88        })
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    /// Round-trip: encode a 101-bit info word, feed perfect LLRs, decoder
97    /// should recover the original info. Exercises the full BP path plus
98    /// the generator sub-matrix.
99    #[test]
100    fn roundtrip_perfect_llr() {
101        // Build a 77-bit message + 24-bit CRC = 101-bit info word.
102        let mut info = [0u8; LDPC_K];
103        for i in 0..77 {
104            info[i] = ((i * 7 + 3) & 1) as u8;
105        }
106        let crc = crc24(&info); // upper 24 bits still zero
107        for i in 0..24 {
108            info[77 + i] = ((crc >> (23 - i)) & 1) as u8;
109        }
110
111        let cw = ldpc_encode(&info);
112        // Sanity: systematic encode keeps info bits in positions 0..K.
113        assert_eq!(&cw[..LDPC_K], &info[..]);
114
115        // Perfect LLR: ±8 per bit, sign follows the bit.
116        let mut llr = [0f32; LDPC_N];
117        for i in 0..LDPC_N {
118            llr[i] = if cw[i] == 1 { 8.0 } else { -8.0 };
119        }
120        let r = bp_decode(&llr, None, 30).expect("BP converges on perfect LLR");
121        assert_eq!(&r.message77[..], &info[..77]);
122    }
123}