Skip to main content

mfsk_core/fec/ldpc_128_90/
mod.rs

1//! LDPC(128, 90) codec with CRC-13 for MSK144.
2//!
3//! Shares the generic BP/OSD engine in [`crate::fec::ldpc`] via
4//! [`Ldpc128_90Params`] — only the parity-check tables (in [`tables`])
5//! and the CRC-13 helpers ([`crc13`], [`check_crc13`]) are specific to
6//! this code. Ported from WSJT-X `lib/encode_128_90.f90` +
7//! `lib/bpdecode128_90.f90` + `lib/ldpc_128_90_{generator,reordered_parity}.f90`.
8//!
9//! The 90 information bits are 77 packjt77 message bits + a 13-bit
10//! CRC (`crc13`, polynomial 0x15D7 — `boost::augmented_crc<13,
11//! 0x15D7>` in WSJT-X `lib/crc13.cpp`), the same `pack77`/`unpack77`
12//! payload FT8/FT4/FST4 use (`genmsk_128_90.f90:84`: `call
13//! pack77(message,i3,n3,c77)`).
14
15pub mod tables;
16
17use alloc::vec;
18
19use crate::core::{FecCodec, FecOpts, FecResult};
20use crate::fec::ldpc::bp::bp_decode_generic_kind;
21use crate::fec::ldpc::osd::{ldpc_encode_generic, osd_decode_generic};
22use crate::fec::ldpc::params::Ldpc128_90Params;
23
24pub const LDPC_N: usize = 128;
25pub const LDPC_K: usize = 90;
26pub const LDPC_M: usize = LDPC_N - LDPC_K; // 38
27
28// ────────────────────────────────────────────────────────────────────
29// CRC-13
30
31/// CRC-13 (polynomial 0x15D7) over `data` bytes, processed MSB-first.
32/// Matches `boost::augmented_crc<13, 0x15D7>` used in WSJT-X
33/// `crc13.cpp` — same bit-serial LFSR shape as
34/// [`crate::fec::ldpc::crc14`] / [`crate::fec::ldpc240_101::crc24`],
35/// just width 13 and a different polynomial.
36pub fn crc13(data: &[u8]) -> u16 {
37    let mut crc: u16 = 0;
38    for &byte in data {
39        for i in (0..8).rev() {
40            let bit = (byte >> i) & 1;
41            let msb = (crc >> 12) & 1;
42            crc = ((crc << 1) | bit as u16) & 0x1FFF;
43            if msb != 0 {
44                crc ^= 0x15D7;
45            }
46        }
47    }
48    crc
49}
50
51/// Verify CRC-13 for a 90-bit decoded word (77 msg + 13 CRC).
52/// Packs the 77 message bits into a 12-byte buffer (big-endian, MSB
53/// first), leaving the remaining 19 bits zero — matching WSJT-X
54/// `chkcrc13a.f90`'s masking of the trailing CRC field before
55/// recomputing — then compares against the stored CRC bits.
56///
57/// Accepts any `&[u8]` slice; lengths other than [`LDPC_K`] (= 90) are
58/// rejected so the function is suitable as a `MessageCodec::verify_info`
59/// implementation passed through `FecOpts::verify_info`.
60pub fn check_crc13(decoded: &[u8]) -> bool {
61    if decoded.len() != LDPC_K {
62        return false;
63    }
64    let mut bytes = [0u8; 12];
65    for (i, &bit) in decoded[..77].iter().enumerate() {
66        let byte_idx = i / 8;
67        let bit_pos = 7 - (i % 8);
68        bytes[byte_idx] |= (bit & 1) << bit_pos;
69    }
70
71    let computed = crc13(&bytes);
72
73    let mut received: u16 = 0;
74    for &bit in &decoded[77..90] {
75        received = (received << 1) | (bit as u16 & 1);
76    }
77
78    computed == received
79}
80
81// ────────────────────────────────────────────────────────────────────
82// FecCodec impl
83
84/// Zero-sized LDPC(128, 90) codec — a thin wrapper that pins the
85/// generic [`crate::fec::ldpc::params::LdpcParams`]-based
86/// implementation to [`Ldpc128_90Params`].
87#[derive(Copy, Clone, Debug, Default)]
88pub struct Ldpc128_90;
89
90impl FecCodec for Ldpc128_90 {
91    const N: usize = LDPC_N;
92    const K: usize = LDPC_K;
93
94    fn encode(&self, info: &[u8], codeword: &mut [u8]) {
95        assert_eq!(info.len(), LDPC_K, "info must be {} bits", LDPC_K);
96        assert_eq!(codeword.len(), LDPC_N, "codeword must be {} bits", LDPC_N);
97        ldpc_encode_generic::<Ldpc128_90Params>(info, codeword);
98    }
99
100    fn decode_soft(&self, llr: &[f32], opts: &FecOpts<'_>) -> Option<FecResult> {
101        assert_eq!(llr.len(), LDPC_N, "llr must be {} values", LDPC_N);
102        let mut llr_arr = vec![0f32; LDPC_N];
103        llr_arr.copy_from_slice(llr);
104
105        // AP hint injection (same convention as Ldpc174_91 / Ldpc240_101).
106        let ap_storage_holder;
107        let ap_slice: Option<&[bool]> = match opts.ap_mask {
108            Some((mask, values)) => {
109                assert_eq!(mask.len(), LDPC_N, "ap mask must be {} bits", LDPC_N);
110                assert_eq!(values.len(), LDPC_N, "ap values must be {} bits", LDPC_N);
111                let apmag = llr_arr.iter().map(|x| x.abs()).fold(0.0f32, f32::max) * 1.01;
112                let mut a = vec![false; LDPC_N];
113                for i in 0..LDPC_N {
114                    if mask[i] != 0 {
115                        a[i] = true;
116                        llr_arr[i] = if values[i] != 0 { apmag } else { -apmag };
117                    }
118                }
119                ap_storage_holder = a;
120                Some(ap_storage_holder.as_slice())
121            }
122            None => None,
123        };
124
125        if let Some(r) = bp_decode_generic_kind::<Ldpc128_90Params>(
126            &llr_arr,
127            ap_slice,
128            opts.bp_max_iter,
129            opts.verify_info,
130            opts.bp_kind,
131        ) {
132            return Some(FecResult {
133                info: r.info,
134                hard_errors: r.hard_errors,
135                iterations: r.iterations,
136            });
137        }
138
139        if opts.osd_depth == 0 {
140            return None;
141        }
142
143        if let Some(r) = osd_decode_generic::<Ldpc128_90Params>(
144            &llr_arr,
145            opts.osd_depth.min(3) as u8,
146            LDPC_K,
147            opts.verify_info,
148        ) {
149            return Some(FecResult {
150                info: r.info,
151                hard_errors: r.hard_errors,
152                iterations: 0,
153            });
154        }
155
156        None
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163    use crate::fec::ldpc::bp::bp_decode_generic;
164    use crate::fec::ldpc::osd::ldpc_encode_generic;
165
166    /// Known-vector cross-check against `boost::augmented_crc<13,
167    /// 0x15D7>` (WSJT-X `crc13.cpp`) — computed independently via a
168    /// small C++ harness linking the real Boost CRC implementation,
169    /// not just derived by inspection.
170    #[test]
171    fn crc13_known_vectors() {
172        assert_eq!(crc13(&[0u8; 12]), 0);
173
174        let mut all_ones_77 = [0u8; 12];
175        for i in 0..77 {
176            let byte_idx = i / 8;
177            let bit_pos = 7 - (i % 8);
178            all_ones_77[byte_idx] |= 1 << bit_pos;
179        }
180        assert_eq!(crc13(&all_ones_77), 5559);
181
182        let mut pattern = [0u8; 12];
183        for i in 0..77 {
184            if (i * 7 + 3) & 1 != 0 {
185                let byte_idx = i / 8;
186                let bit_pos = 7 - (i % 8);
187                pattern[byte_idx] |= 1 << bit_pos;
188            }
189        }
190        assert_eq!(crc13(&pattern), 4105);
191
192        let mut single_bit0 = [0u8; 12];
193        single_bit0[0] |= 1 << 7;
194        assert_eq!(crc13(&single_bit0), 4822);
195
196        let mut single_bit76 = [0u8; 12];
197        single_bit76[9] |= 1 << 3; // bit 76 (0-indexed) -> byte 9, bit_pos 7-(76%8)=3
198        assert_eq!(crc13(&single_bit76), 7029);
199    }
200
201    /// Round-trip: encode a 90-bit info word (77 msg + CRC13), feed
202    /// perfect LLRs, decoder should recover the original info.
203    /// Exercises the full BP path plus the generator sub-matrix via
204    /// the generic helpers.
205    #[test]
206    fn roundtrip_perfect_llr() {
207        let mut info = [0u8; LDPC_K];
208        for i in 0..77 {
209            info[i] = ((i * 7 + 3) & 1) as u8;
210        }
211        let crc = crc13(&{
212            let mut bytes = [0u8; 12];
213            for i in 0..77 {
214                let byte_idx = i / 8;
215                let bit_pos = 7 - (i % 8);
216                bytes[byte_idx] |= (info[i] & 1) << bit_pos;
217            }
218            bytes
219        });
220        for i in 0..13 {
221            info[77 + i] = ((crc >> (12 - i)) & 1) as u8;
222        }
223
224        let mut cw = [0u8; LDPC_N];
225        ldpc_encode_generic::<Ldpc128_90Params>(&info, &mut cw);
226        // Sanity: systematic encode keeps info bits in positions 0..K.
227        assert_eq!(&cw[..LDPC_K], &info[..]);
228        // Sanity: the CRC we just embedded verifies against itself.
229        assert!(check_crc13(&info));
230
231        let mut llr = vec![0f32; LDPC_N];
232        for i in 0..LDPC_N {
233            llr[i] = if cw[i] == 1 { 8.0 } else { -8.0 };
234        }
235        let r = bp_decode_generic::<Ldpc128_90Params>(&llr, None, 30, Some(check_crc13))
236            .expect("BP converges on perfect LLR");
237        assert_eq!(&r.info[..77], &info[..77]);
238    }
239
240    /// Noisy-LLR sweep: flip a handful of bits' confidence and confirm
241    /// BP still converges to the transmitted codeword.
242    #[test]
243    fn roundtrip_noisy_llr_recovers() {
244        let mut info = [0u8; LDPC_K];
245        for i in 0..77 {
246            info[i] = ((i * 3 + 1) & 1) as u8;
247        }
248        let crc = crc13(&{
249            let mut bytes = [0u8; 12];
250            for i in 0..77 {
251                let byte_idx = i / 8;
252                let bit_pos = 7 - (i % 8);
253                bytes[byte_idx] |= (info[i] & 1) << bit_pos;
254            }
255            bytes
256        });
257        for i in 0..13 {
258            info[77 + i] = ((crc >> (12 - i)) & 1) as u8;
259        }
260
261        let mut cw = [0u8; LDPC_N];
262        ldpc_encode_generic::<Ldpc128_90Params>(&info, &mut cw);
263
264        let mut llr = vec![0f32; LDPC_N];
265        for i in 0..LDPC_N {
266            // Weak, occasionally-wrong-sign LLR every 11th bit to
267            // simulate noise while staying within BP's correction range.
268            let mag = if i % 11 == 0 { 0.3 } else { 3.0 };
269            let sign = if cw[i] == 1 { 1.0 } else { -1.0 };
270            llr[i] = sign * mag;
271        }
272        let r = bp_decode_generic::<Ldpc128_90Params>(&llr, None, 30, Some(check_crc13))
273            .expect("BP converges on noisy LLR");
274        assert_eq!(&r.info[..77], &info[..77]);
275    }
276}