Skip to main content

comnum_fec/ccsds/
mod.rs

1//! The channel codes of CCSDS 131.0-B-5 (*TM Synchronization and Channel Coding*): the
2//! Reed–Solomon codes in the dual basis with their interleaving and virtual fill, the
3//! (8160, 7136) LDPC code, the AR4JA LDPC codes and the turbo codes.
4//!
5//! # Reed–Solomon codes (Sec. 4)
6//!
7//! [`CcsdsRs`] is the (255, 223) ($E = 16$) or (255, 239) ($E = 8$) code over
8//! $\mathrm{GF}(2^{8})$ of the field polynomial $F(x) = x^{8} + x^{7} + x^{2} + x + 1$ and the
9//! generator $g(x) = \prod_{j=128-E}^{127+E} (x - \alpha^{11 j})$ (Secs. 4.3.3, 4.3.4), with
10//! the symbols in the dual (Berlekamp) basis (Sec. 4.3.9): the octets of a frame are the
11//! dual-basis symbols, $z_{0}$ their most significant (first transmitted) bit, and
12//! [`to_dual_basis`], [`from_dual_basis`] are the matrices $T_{\alpha\ell}$ and
13//! $T_{\alpha\ell}^{-1}$ that take them to and from the conventional basis of the encoder
14//! (Annex F). $I$ codewords are interleaved symbol by symbol, $I \in \{1, 2, 3, 4, 5, 8\}$
15//! (Sec. 4.3.5, 4.4.1), each shortened by a virtual fill of $q$ leading zero symbols that
16//! are neither encoded nor sent (Sec. 4.3.7): a frame of $(255 - 2E - q) I$ octets, a
17//! codeblock of $(255 - q) I$ octets.
18//!
19//! # LDPC codes (Sec. 7)
20//!
21//! - [`CcsdsLdpc::c2`]: the (8160, 7136) code of rate 223/255 (Sec. 7.3), built on the
22//!   (8176, 7156) quasi-cyclic code whose parity-check matrix is a $2 \times 16$ array of
23//!   $511 \times 511$ circulants of weight 2 (Table 7-1); its encoder is the systematic
24//!   generator of the (8176, 7154) subcode, whose $28$ circulants are given by their first
25//!   rows (Annex C, Table C-1): 18 virtual zeros are prepended to the 7136 information bits,
26//!   the 1022 parity bits follow them, the 18 zeros are dropped and two zeros appended.
27//! - [`CcsdsLdpc::ar4ja`]: the nine AR4JA codes of rates 1/2, 2/3, 4/5 and $k = 1024, 4096,
28//!   16384$ (Sec. 7.4): $H$ is $3 \times (K + 3)$ blocks of $M \times M$ sums of identities and
29//!   permutations $\Pi_{k}$, $\pi_{k}(i) = \frac{M}{4}((\theta_{k} + \lfloor 4i/M \rfloor) \bmod 4) +
30//!   (\phi_{k}(\lfloor 4i/M \rfloor, M) + i) \bmod \frac{M}{4}$ (Tables 7-3, 7-4), $K = 2, 4, 8$;
31//!   the codeword $[u\ p]$ satisfies $H [u\ p]^{\mathrm{T}} = 0$ (the generator $[I\ W]$,
32//!   $W = (P^{-1} Q)^{\mathrm{T}}$, Sec. 7.4.3) and its last $M$ bits are punctured.
33//!   The encoder solves $P p = Q u$ by the block structure of $P$, with the inverse of one
34//!   $M \times M$ matrix.
35//!
36//! Both decode by the belief propagation of [`crate::ldpc`], the virtual zeros known
37//! ([`KNOWN_BIT_LLR`](comnum_core::signal::KNOWN_BIT_LLR)) and the punctured bits at a zero LLR.
38//!
39//! # Turbo codes (Sec. 6)
40//!
41//! [`CcsdsTurbo`]: two 16-state recursive codes of backward vector $G_{0} = 10011$
42//! ($w_{t} = u_{t} \oplus w_{t-3} \oplus w_{t-4}$) and forward vectors $G_{1} = 11011$,
43//! $G_{2} = 10101$, $G_{3} = 11111$, on $k = 1784, 3568, 7136, 8920$ bits, the second
44//! encoder fed by the permutation of Sec. 6.3 g) ([`turbo_permutation`]). Both encoders
45//! are terminated by 4 steps of feedback input; the outputs of every bit time leave in the
46//! order of Sec. 6.3 j): rate 1/3 $(0a, 1a, 1b)$, rate 1/4 $(0a, 2a, 3a, 1b)$, rate 1/6
47//! $(0a, 1a, 2a, 3a, 1b, 3b)$, rate 1/2 $(0a, 1a)$ and $(0a, 1b)$ alternately: $n = (k + 4)/r$.
48//! The decoder is the iterative BCJR decoder of [`crate::turbo`].
49//!
50//! # Examples
51//! ```
52//! use comnum_fec::block::BlockCode;
53//! use comnum_fec::ccsds::{CcsdsRs, from_dual_basis, to_dual_basis};
54//! // Annex F, example 2: [u] = 01011001 is [z] = 11101000 in the dual basis
55//! assert_eq!(to_dual_basis(0b0101_1001), 0b1110_1000);
56//! assert_eq!(from_dual_basis(0b1110_1000), 0b0101_1001);
57//! // (255, 223), interleaving depth 5: a frame of 1115 octets, 1280 check bits (Sec. 4.3.8)
58//! let rs = CcsdsRs::new(16, 5, 0)?;
59//! assert_eq!((rs.frame_len(), rs.codeblock_len()), (1115, 1275));
60//! assert_eq!(rs.n() - rs.k(), 1280);
61//! # Ok::<(), comnum_core::Error>(())
62//! ```
63//!
64//! # References
65//!
66//! 1. CCSDS 131.0-B-5, *TM Synchronization and Channel Coding*, Recommended Standard
67//!    (Blue Book), Issue 5, September 2023, Secs. 4, 6 and 7, Annexes C, F and G.
68
69use crate::block::{BlockCode, container_traits, expect_len};
70use crate::rs::ReedSolomon;
71use comnum_core::signal::hard_decisions;
72use comnum_core::{Result, ensure};
73
74mod ldpc;
75mod turbo;
76
77pub use ldpc::{Ar4jaRate, CcsdsLdpc};
78pub use turbo::{CcsdsTurbo, TURBO_LENGTHS, TurboRate, turbo_permutation};
79
80/// $T_{\alpha\ell}$ of Sec. 4.3.9: row $r$ is the dual-basis representation of
81/// $\alpha^{7-r}$, the bit $z_{0}$ the most significant.
82const DUAL_BASIS: [u8; 8] = [0x8D, 0xEF, 0xEC, 0x86, 0xFA, 0x99, 0xAF, 0x7B];
83/// $T_{\alpha\ell}^{-1}$ of Sec. 4.3.9: row $r$ multiplies $z_{r}$.
84const DUAL_BASIS_INV: [u8; 8] = [0xC5, 0x42, 0x2E, 0xFD, 0xF0, 0x79, 0xAC, 0xCC];
85/// The field polynomial $F(x) = x^{8} + x^{7} + x^{2} + x + 1$ (Sec. 4.3.3).
86const RS_FIELD: u32 = 0x187;
87/// The interleaving depths of Sec. 4.3.5.1.
88pub const RS_DEPTHS: [usize; 6] = [1, 2, 3, 4, 5, 8];
89
90/// The dual-basis symbol $[z_{0} \dots z_{7}] = [u_{7} \dots u_{0}]\, T_{\alpha\ell}$ of the
91/// conventional symbol `u` ($u_{7}$, the coefficient of $\alpha^{7}$, its most significant
92/// bit; $z_{0}$ the most significant bit of the result), Sec. 4.3.9.
93pub fn to_dual_basis(u: u8) -> u8 {
94    (0..8).filter(|r| (u >> (7 - r)) & 1 == 1).fold(0, |z, r| z ^ DUAL_BASIS[r])
95}
96
97/// The conventional symbol $[u_{7} \dots u_{0}] = [z_{0} \dots z_{7}]\, T_{\alpha\ell}^{-1}$ of
98/// the dual-basis symbol `z`, Sec. 4.3.9.
99pub fn from_dual_basis(z: u8) -> u8 {
100    (0..8).filter(|r| (z >> (7 - r)) & 1 == 1).fold(0, |u, r| u ^ DUAL_BASIS_INV[r])
101}
102
103/// An interleaved Reed–Solomon codeblock of CCSDS (see the module): the code of
104/// correction capability $E$, the interleaving depth $I$ and the virtual fill of $q$
105/// symbols per codeword.
106#[derive(Clone, Debug, PartialEq)]
107pub struct CcsdsRs {
108    rs: ReedSolomon,
109    depth: usize,
110    fill: usize,
111}
112
113impl CcsdsRs {
114    /// The codeblock of correction capability `e` (8 or 16 symbols per codeword), depth
115    /// `depth` (1, 2, 3, 4, 5 or 8) and virtual fill `fill` ($q$ symbols per codeword,
116    /// $Q = q I$ in all; 0 for the full length).
117    ///
118    /// # Errors
119    /// [`Error::Config`](comnum_core::Error::Config) for another $E$ or $I$, or a fill that leaves no information.
120    pub fn new(e: usize, depth: usize, fill: usize) -> Result<CcsdsRs> {
121        ensure(e == 8 || e == 16, || format!("the CCSDS Reed-Solomon codes correct E = 8 or 16 symbols, got {e}"))?;
122        ensure(RS_DEPTHS.contains(&depth), || {
123            format!("the interleaving depth must be 1, 2, 3, 4, 5 or 8, got {depth}")
124        })?;
125        ensure(fill < 255 - 2 * e, || format!("a virtual fill of {fill} symbols leaves no information"))?;
126        let rs = ReedSolomon::with_field(8, RS_FIELD, 128 - e, 11, e, 255 - fill)?;
127        Ok(CcsdsRs { rs, depth, fill })
128    }
129
130    /// The correction capability $E$ of a codeword.
131    pub fn e(&self) -> usize {
132        self.rs.t()
133    }
134
135    /// The interleaving depth $I$.
136    pub fn depth(&self) -> usize {
137        self.depth
138    }
139
140    /// The virtual fill $q$ of a codeword.
141    pub fn fill(&self) -> usize {
142        self.fill
143    }
144
145    /// The conventional code of a codeword (the field of $F(x)$, the roots
146    /// $\alpha^{11 j}$, shortened by the fill).
147    pub fn code(&self) -> &ReedSolomon {
148        &self.rs
149    }
150
151    /// Octets of a frame, $(255 - 2E - q) I$.
152    pub fn frame_len(&self) -> usize {
153        self.rs.k_symbols() * self.depth
154    }
155
156    /// Octets of a codeblock, $(255 - q) I$.
157    pub fn codeblock_len(&self) -> usize {
158        self.rs.n_symbols() * self.depth
159    }
160
161    /// The codeblock of a frame (dual-basis octets): the frame, then the $2 E I$ check
162    /// symbols, codeword $i$ holding the symbols $i, i + I, i + 2I, \dots$ (Sec. 4.4.1).
163    ///
164    /// # Errors
165    /// [`Error::Shape`](comnum_core::Error::Shape) unless the frame has [`CcsdsRs::frame_len`] octets.
166    pub fn encode_bytes(&self, frame: &[u8]) -> Result<Vec<u8>> {
167        expect_len(frame.len(), self.frame_len(), "octets")?;
168        let (k, i_depth) = (self.rs.k_symbols(), self.depth);
169        let parity = 2 * self.e();
170        let mut out = frame.to_vec();
171        out.resize(self.codeblock_len(), 0);
172        for i in 0..i_depth {
173            let msg: Vec<u16> = (0..k).map(|t| u16::from(from_dual_basis(frame[t * i_depth + i]))).collect();
174            let word = self.rs.encode_symbols(&msg)?;
175            for (t, &s) in word[k..].iter().enumerate() {
176                out[(k + t) * i_depth + i] = to_dual_basis(s as u8);
177            }
178            debug_assert_eq!(word.len(), k + parity);
179        }
180        Ok(out)
181    }
182
183    /// Decode a received codeblock: the frame and the number of corrected symbols
184    /// (`None` if a codeword could not be decoded; its frame symbols are then those
185    /// received).
186    ///
187    /// # Errors
188    /// [`Error::Shape`](comnum_core::Error::Shape) unless the codeblock has [`CcsdsRs::codeblock_len`] octets.
189    pub fn decode_bytes(&self, block: &[u8]) -> Result<(Vec<u8>, Option<usize>)> {
190        expect_len(block.len(), self.codeblock_len(), "octets")?;
191        let (n, k, i_depth) = (self.rs.n_symbols(), self.rs.k_symbols(), self.depth);
192        let mut frame = block[..self.frame_len()].to_vec();
193        let mut total = Some(0);
194        for i in 0..i_depth {
195            let r: Vec<u16> = (0..n).map(|t| u16::from(from_dual_basis(block[t * i_depth + i]))).collect();
196            let (msg, fixed) = self.rs.decode_symbols(&r)?;
197            match fixed {
198                Some(c) => {
199                    total = total.map(|t| t + c);
200                    for (t, &s) in msg.iter().enumerate().take(k) {
201                        frame[t * i_depth + i] = to_dual_basis(s as u8);
202                    }
203                }
204                None => total = None,
205            }
206        }
207        Ok((frame, total))
208    }
209}
210
211fn bits_to_bytes(bits: &[u8]) -> Vec<u8> {
212    bits.chunks(8).map(|b| b.iter().fold(0u8, |acc, &v| (acc << 1) | (v & 1))).collect()
213}
214
215fn bytes_to_bits(bytes: &[u8]) -> Vec<u8> {
216    bytes.iter().flat_map(|&b| (0..8).rev().map(move |k| (b >> k) & 1)).collect()
217}
218
219impl BlockCode for CcsdsRs {
220    fn k(&self) -> usize {
221        8 * self.frame_len()
222    }
223
224    fn n(&self) -> usize {
225        8 * self.codeblock_len()
226    }
227
228    fn encode_block(&self, info: &[u8]) -> Result<Vec<u8>> {
229        expect_len(info.len(), self.k(), "bits")?;
230        Ok(bytes_to_bits(&self.encode_bytes(&bits_to_bytes(info))?))
231    }
232
233    /// Hard decisions on the LLRs, then [`CcsdsRs::decode_bytes`].
234    fn decode_block(&self, llr: &[f64]) -> Result<Vec<u8>> {
235        expect_len(llr.len(), self.n(), "LLRs")?;
236        let (frame, _) = self.decode_bytes(&bits_to_bytes(&hard_decisions(llr)))?;
237        Ok(bytes_to_bits(&frame))
238    }
239}
240
241container_traits!(CcsdsRs);
242
243#[cfg(test)]
244mod tests {
245    use super::ldpc::ar4ja_pi;
246    use super::*;
247    use comnum_core::rng::Rng;
248    use comnum_math::gf2m::Field;
249
250    fn random_bits(n: usize, rng: &mut Rng) -> Vec<u8> {
251        (0..n).map(|_| rng.bit()).collect()
252    }
253
254    fn bpsk(c: &[u8], amplitude: f64) -> Vec<f64> {
255        c.iter().map(|&b| if b == 0 { amplitude } else { -amplitude }).collect()
256    }
257
258    #[test]
259    fn the_dual_basis_matrices_are_inverse_and_reproduce_annex_f() {
260        for u in 0..=255u8 {
261            assert_eq!(from_dual_basis(to_dual_basis(u)), u);
262        }
263        // Annex F, example 1: [z] = 10111001 gives [u] = 00101010 = alpha^213
264        assert_eq!(from_dual_basis(0b1011_1001), 0b0010_1010);
265        let f = Field::new(8, RS_FIELD).unwrap();
266        assert_eq!(f.pow(213), 0b0010_1010);
267        // example 2: [u] = 01011001 = alpha^152 gives [z] = 11101000
268        assert_eq!(f.pow(152), 0b0101_1001);
269        assert_eq!(to_dual_basis(0b0101_1001), 0b1110_1000);
270    }
271
272    #[test]
273    fn the_generators_are_those_of_annex_g() {
274        let f = Field::new(8, RS_FIELD).unwrap();
275        // the exponents of G_0 ... G_16 (E = 16) and G_0 ... G_8 (E = 8), symmetric
276        let g16 = [0, 249, 59, 66, 4, 43, 126, 251, 97, 30, 3, 213, 50, 66, 170, 5, 24];
277        let g8 = [0, 30, 230, 49, 235, 129, 81, 76, 173];
278        for (e, half) in [(16, &g16[..]), (8, &g8[..])] {
279            let rs = CcsdsRs::new(e, 1, 0).unwrap();
280            let g = rs.code().generator();
281            assert_eq!(g.len(), 2 * e + 1);
282            for (i, &x) in half.iter().enumerate() {
283                assert_eq!(u32::from(g[i]), f.pow(x), "E = {e}, G{i}");
284                assert_eq!(g[i], g[2 * e - i], "E = {e}, G{i} = G{}", 2 * e - i);
285            }
286        }
287    }
288
289    #[test]
290    fn a_codeblock_corrects_e_symbols_per_codeword() {
291        let mut rng = Rng::new(1);
292        for (e, depth, fill) in [(16, 5, 0), (8, 2, 11), (16, 8, 3)] {
293            let rs = CcsdsRs::new(e, depth, fill).unwrap();
294            assert_eq!(rs.codeblock_len() - rs.frame_len(), 2 * e * depth);
295            let frame: Vec<u8> = (0..rs.frame_len()).map(|_| rng.below(256) as u8).collect();
296            let mut block = rs.encode_bytes(&frame).unwrap();
297            assert_eq!(&block[..frame.len()], &frame[..], "systematic");
298            // E errors in every codeword: positions l = t I + i
299            for i in 0..depth {
300                for t in 0..e {
301                    block[(3 * t + 1) * depth + i] ^= 0x5A;
302                }
303            }
304            let (out, fixed) = rs.decode_bytes(&block).unwrap();
305            assert_eq!(out, frame, "E = {e}, I = {depth}, q = {fill}");
306            assert_eq!(fixed, Some(e * depth));
307        }
308        assert!(CcsdsRs::new(16, 6, 0).is_err() && CcsdsRs::new(12, 1, 0).is_err());
309    }
310
311    #[test]
312    fn the_rate_7_8_code_is_that_of_table_c1() {
313        let code = CcsdsLdpc::c2().unwrap();
314        assert_eq!((code.k(), code.n()), (7136, 8160));
315        let mut rng = Rng::new(2);
316        let info = random_bits(7136, &mut rng);
317        // G H^T = 0: the codeword of Table C-1 satisfies the checks of Table 7-1
318        assert!(code.ldpc().is_codeword(&code.full_codeword(&info).unwrap()));
319        let c = code.encode_block(&info).unwrap();
320        assert_eq!((c.len(), c[..7136] == info[..], &c[8158..]), (8160, true, &[0u8, 0][..]));
321        // decoding a noisy codeword: BPSK at Es/N0 = 5 dB (sigma = 0.4), exact LLRs
322        let llr: Vec<f64> = bpsk(&c, 1.0).iter().map(|v| 2.0 / 0.16 * (v + 0.4 * rng.gaussian())).collect();
323        assert_eq!(code.decode_block(&llr).unwrap(), info);
324    }
325
326    #[test]
327    fn the_ar4ja_codes_have_the_lengths_of_table_7_5_and_encode_codewords() {
328        let mut rng = Rng::new(3);
329        for (k, rate, n) in
330            [(1024, Ar4jaRate::R1_2, 2048), (1024, Ar4jaRate::R2_3, 1536), (1024, Ar4jaRate::R4_5, 1280)]
331        {
332            let code = CcsdsLdpc::ar4ja(k, rate).unwrap();
333            assert_eq!((code.k(), code.n()), (k, n), "rate {}", rate.name());
334            let info = random_bits(k, &mut rng);
335            assert!(code.ldpc().is_codeword(&code.full_codeword(&info).unwrap()), "rate {}", rate.name());
336            let c = code.encode_block(&info).unwrap();
337            assert_eq!(&c[..k], &info[..]);
338            let llr: Vec<f64> = bpsk(&c, 1.0).iter().map(|v| 2.0 / 0.16 * (v + 0.4 * rng.gaussian())).collect();
339            assert_eq!(code.decode_block(&llr).unwrap(), info, "rate {}", rate.name());
340        }
341        assert!(CcsdsLdpc::ar4ja(2048, Ar4jaRate::R1_2).is_err());
342    }
343
344    #[test]
345    fn the_permutations_of_the_ar4ja_codes_are_permutations() {
346        for m in [128, 256, 512, 1024, 2048, 4096, 8192] {
347            for k in 1..=26 {
348                let mut seen = vec![false; m];
349                for i in 0..m {
350                    seen[ar4ja_pi(k, i, m)] = true;
351                }
352                assert!(seen.iter().all(|&s| s), "Pi_{k}, M = {m}");
353            }
354        }
355    }
356
357    #[test]
358    fn the_turbo_codes_have_the_lengths_of_table_6_2() {
359        let table = [
360            (1784, [3576, 5364, 7152, 10728]),
361            (3568, [7144, 10716, 14288, 21432]),
362            (7136, [14280, 21420, 28560, 42840]),
363            (8920, [17848, 26772, 35696, 53544]),
364        ];
365        for (k, ns) in table {
366            for (r, n) in [TurboRate::R1_2, TurboRate::R1_3, TurboRate::R1_4, TurboRate::R1_6].into_iter().zip(ns) {
367                assert_eq!(CcsdsTurbo::new(k, r).unwrap().n(), n, "k = {k}, rate {}", r.name());
368            }
369        }
370        assert!(CcsdsTurbo::new(1000, TurboRate::R1_2).is_err());
371    }
372
373    #[test]
374    fn the_turbo_permutation_starts_as_computed_from_sec_6_3_g() {
375        // pi(1..10) for every k (computed from the algorithm, a regression vector)
376        for k in TURBO_LENGTHS {
377            let p = turbo_permutation(k).unwrap();
378            let first: Vec<usize> = p.perm()[..10].iter().map(|v| v + 1).collect();
379            assert_eq!(first, [4, 171, 300, 467, 596, 763, 892, 1059, 1188, 1355], "k = {k}");
380        }
381    }
382
383    #[test]
384    fn the_turbo_codes_terminate_and_decode() {
385        let mut rng = Rng::new(4);
386        for rate in [TurboRate::R1_2, TurboRate::R1_3, TurboRate::R1_4, TurboRate::R1_6] {
387            let code = CcsdsTurbo::new(1784, rate).unwrap();
388            let info = random_bits(1784, &mut rng);
389            let c = code.encode_block(&info).unwrap();
390            assert_eq!(c.len(), code.n());
391            // the systematic output 0a leads every bit time: the information, then the 4
392            // tail bits of encoder a
393            let per = c.len() / (1784 + 4);
394            if rate != TurboRate::R1_2 {
395                let sys: Vec<u8> = (0..1784).map(|t| c[t * per]).collect();
396                assert_eq!(sys, info);
397            }
398            let llr: Vec<f64> = bpsk(&c, 1.0).iter().map(|v| 2.0 * (v + 0.5 * rng.gaussian())).collect();
399            assert_eq!(code.decode_block(&llr).unwrap(), info, "rate {}", rate.name());
400        }
401    }
402}