Skip to main content

comnum_fec/
bch.rs

1//! Binary BCH codes, shortened: the outer code of DVB-S2 (`comnum_systems::dvbs2`).
2//!
3//! # Model
4//!
5//! In the Galois field $\mathrm{GF}(2^{m})$ generated by a primitive element $\alpha$
6//! (root of a primitive polynomial $p(x)$), the narrow-sense BCH code of designed
7//! distance $2t + 1$ has the generator polynomial
8//!
9//! $$
10//! g(x) = \operatorname{lcm}\left(m_{1}(x), m_{3}(x), \dots, m_{2t-1}(x)\right),
11//! $$
12//!
13//! with $m_{i}$ the minimal polynomial of $\alpha^{i}$ (Lin & Costello, ch. 6). The code
14//! of length $2^{m} - 1$ is shortened to $n$ bits by fixing its first information bits
15//! to zero. The encoder is systematic: the $k$ message bits $m_{k-1}, \dots, m_{0}$ (the
16//! highest degree first, in transmission order) followed by the $n - k$ coefficients of
17//! $x^{n-k} m(x) \bmod g(x)$. The decoder computes the syndromes $S_{j} = r(\alpha^{j})$,
18//! the error-locator polynomial by the Berlekamp–Massey algorithm and its roots by a
19//! Chien search over the $n$ positions; it corrects up to $t$ errors and reports a
20//! failure beyond (when the locator has fewer roots than its degree). The field
21//! arithmetic is that of [`comnum_math::gf2m`].
22//!
23//! # Examples
24//! ```
25//! use comnum_fec::bch::Bch;
26//! // the (15, 7) BCH code, t = 2, of GF(16) with p(x) = 1 + x + x⁴
27//! let code = Bch::new(4, 0b1_0011, 2, 15)?;
28//! assert_eq!(code.k(), 7);
29//! assert_eq!(code.generator(), &[1, 0, 0, 0, 1, 0, 1, 1, 1]);   // 1 + x⁴ + x⁶ + x⁷ + x⁸
30//! let msg = [1, 0, 1, 1, 0, 0, 1];
31//! let mut c = code.encode(&msg)?;
32//! c[2] ^= 1;
33//! c[11] ^= 1;
34//! let (decoded, corrected) = code.decode(&c)?;
35//! assert_eq!((decoded, corrected), (msg.to_vec(), Some(2)));
36//! # Ok::<(), comnum_core::Error>(())
37//! ```
38//!
39//! # References
40//!
41//! 1. S. Lin and D. J. Costello, *Error Control Coding*, 2nd ed., Pearson, 2004, ch. 6.
42//! 2. ETSI EN 302 307-1 V1.4.1, *Digital Video Broadcasting (DVB); Second generation
43//!    framing structure, channel coding and modulation systems (DVB-S2)*, 2014,
44//!    § 5.3.1.
45
46use crate::algebraic::{berlekamp_massey, chien_search};
47use crate::block::{BlockCode, container_traits, expect_len};
48use comnum_core::signal::hard_decisions;
49use comnum_core::{Result, ensure};
50use comnum_math::gf2::{poly_mul, poly_rem};
51use comnum_math::gf2m::Field;
52
53/// A shortened binary BCH code (see the module).
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct Bch {
56    field: Field,
57    t: usize,
58    n: usize,
59    /// $g(x)$, lowest degree first.
60    generator: Vec<u8>,
61}
62
63impl Bch {
64    /// The code of $\mathrm{GF}(2^{m})$ with the primitive polynomial `poly` (bit $i$ the
65    /// coefficient of $x^{i}$), correcting `t` errors, shortened to `n` bits.
66    ///
67    /// # Errors
68    /// [`comnum_core::Error::Config`] for a polynomial that is not primitive, or a length
69    /// that leaves no message bit or exceeds $2^{m} - 1$.
70    pub fn new(m: u32, poly: u32, t: usize, n: usize) -> Result<Bch> {
71        let field = Field::new(m, poly)?;
72        ensure(t >= 1, || "BCH: t must be at least 1".into())?;
73        let mut generator = vec![1u8];
74        let mut used: Vec<Vec<u8>> = Vec::new();
75        for i in (1..2 * t).step_by(2) {
76            let mi = field.minimal_polynomial(i);
77            if !used.contains(&mi) {
78                generator = poly_mul(&generator, &mi);
79                used.push(mi);
80            }
81        }
82        let r = generator.len() - 1;
83        ensure(n <= field.order() && n > r, || {
84            format!("BCH: the length {n} must exceed the {r} parity bits and not exceed {}", field.order())
85        })?;
86        Ok(Bch { field, t, n, generator })
87    }
88
89    /// Codeword length $n$.
90    pub fn n(&self) -> usize {
91        self.n
92    }
93
94    /// Message length $k = n - \deg g$.
95    pub fn k(&self) -> usize {
96        self.n + 1 - self.generator.len()
97    }
98
99    /// Errors corrected $t$.
100    pub fn t(&self) -> usize {
101        self.t
102    }
103
104    /// The generator polynomial $g(x)$, lowest degree first.
105    pub fn generator(&self) -> &[u8] {
106        &self.generator
107    }
108
109    /// The codeword of $k$ message bits: the message, then the parity bits (highest
110    /// degree first).
111    ///
112    /// # Errors
113    /// [`comnum_core::Error::Shape`] for another number of bits.
114    pub fn encode(&self, msg: &[u8]) -> Result<Vec<u8>> {
115        expect_len(msg.len(), self.k(), "bits")?;
116        // the parity bits: the remainder of m(x) x^r by g(x)
117        let mut c: Vec<u8> = msg.iter().map(|b| b & 1).collect();
118        c.extend(poly_rem(msg, &self.generator));
119        Ok(c)
120    }
121
122    /// Decode $n$ hard bits: the message and the number of corrected errors (`None` if
123    /// the word has more errors than the code corrects and they were detected; the
124    /// message bits are then returned as received).
125    ///
126    /// # Errors
127    /// [`comnum_core::Error::Shape`] for another number of bits.
128    pub fn decode(&self, r: &[u8]) -> Result<(Vec<u8>, Option<usize>)> {
129        expect_len(r.len(), self.n, "bits")?;
130        let f = &self.field;
131        let n = self.n;
132        let two_t = 2 * self.t;
133        // syndromes S_j = r(α^j), position i carrying x^{n-1-i}
134        let mut s = vec![0u32; two_t + 1];
135        for (i, &b) in r.iter().enumerate() {
136            if b & 1 == 1 {
137                let e = n - 1 - i;
138                for (j, sj) in s.iter_mut().enumerate().skip(1) {
139                    *sj ^= f.pow(j * e);
140                }
141            }
142        }
143        let k = self.k();
144        let msg = r[..k].iter().map(|b| b & 1).collect::<Vec<u8>>();
145        if s.iter().all(|&v| v == 0) {
146            return Ok((msg, Some(0)));
147        }
148        // Berlekamp–Massey on S_1 … S_{2t}; at most t errors
149        let (lambda, _) = berlekamp_massey(f, &s[1..]);
150        let degree = lambda.len() - 1;
151        if degree > self.t {
152            return Ok((msg, None));
153        }
154        // Chien search: position i is in error if Λ(α^{-(n-1-i)}) = 0; as many roots as
155        // the degree of Λ
156        let order = f.order();
157        let roots = chien_search(f, &lambda, n, |i| (n - 1 - i) % order);
158        if roots.len() != degree {
159            return Ok((msg, None));
160        }
161        let mut out: Vec<u8> = r.iter().map(|b| b & 1).collect();
162        for &i in &roots {
163            out[i] ^= 1;
164        }
165        let found = roots.len();
166        out.truncate(k);
167        Ok((out, Some(found)))
168    }
169}
170
171impl BlockCode for Bch {
172    fn k(&self) -> usize {
173        Bch::k(self)
174    }
175
176    fn n(&self) -> usize {
177        Bch::n(self)
178    }
179
180    fn encode_block(&self, info: &[u8]) -> Result<Vec<u8>> {
181        self.encode(info)
182    }
183
184    /// Hard decisions of the LLRs, then [`Bch::decode`]; a decoding failure returns the
185    /// message bits as received (as [`crate::rs::ReedSolomon`]).
186    fn decode_block(&self, llr: &[f64]) -> Result<Vec<u8>> {
187        expect_len(llr.len(), Bch::n(self), "LLRs")?;
188        Ok(self.decode(&hard_decisions(llr))?.0)
189    }
190}
191
192container_traits!(Bch);
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197    use comnum_core::rng::Rng;
198
199    /// The encoder of the BCH code before `poly_rem`: its own register of 64-bit words.
200    fn former_encode(code: &Bch, msg: &[u8]) -> Vec<u8> {
201        let r = code.generator.len() - 1;
202        let words = r.div_ceil(64);
203        let mut g = vec![0u64; words];
204        for (d, &b) in code.generator[..r].iter().enumerate() {
205            if b == 1 {
206                g[d / 64] |= 1 << (d % 64);
207            }
208        }
209        let mut reg = vec![0u64; words];
210        let top = r - 1;
211        for &b in msg {
212            let fb = ((reg[top / 64] >> (top % 64)) & 1) as u8 ^ (b & 1);
213            for w in (0..words).rev() {
214                let carry = if w > 0 { reg[w - 1] >> 63 } else { 0 };
215                reg[w] = (reg[w] << 1) | carry;
216            }
217            if r % 64 != 0 {
218                reg[words - 1] &= (1u64 << (r % 64)) - 1;
219            }
220            if fb == 1 {
221                for (x, y) in reg.iter_mut().zip(&g) {
222                    *x ^= y;
223                }
224            }
225        }
226        let mut c: Vec<u8> = msg.iter().map(|b| b & 1).collect();
227        c.extend((0..r).rev().map(|d| ((reg[d / 64] >> (d % 64)) & 1) as u8));
228        c
229    }
230
231    #[test]
232    fn the_codewords_are_those_of_the_former_encoder() {
233        let mut rng = Rng::new(17);
234        // degrees 10 to 192: one word, several words, a multiple of 64
235        for (m, poly, t, n) in
236            [(5, 0b10_0101, 2, 31), (16, 0x1_002D, 12, 16_200), (16, 0x1_002D, 4, 1000), (8, 0x11D, 8, 255)]
237        {
238            let code = Bch::new(m, poly, t, n).unwrap();
239            for _ in 0..3 {
240                let msg: Vec<u8> = (0..code.k()).map(|_| rng.bit()).collect();
241                assert_eq!(code.encode(&msg).unwrap(), former_encode(&code, &msg), "m {m}, t {t}, n {n}");
242            }
243        }
244    }
245
246    #[test]
247    fn the_generators_of_dvb_s2_are_the_products_of_the_published_polynomials() {
248        // ETSI EN 302 307-1, Table 6a: g1 … g12 of the normal FECFRAME, GF(2^16)
249        let table6a: [&[usize]; 12] = [
250            &[0, 2, 3, 5, 16],
251            &[0, 1, 4, 5, 6, 8, 16],
252            &[0, 2, 3, 4, 5, 7, 8, 9, 10, 11, 16],
253            &[0, 2, 4, 6, 9, 11, 12, 14, 16],
254            &[0, 1, 2, 3, 5, 8, 9, 10, 11, 12, 16],
255            &[0, 2, 4, 5, 7, 8, 9, 10, 12, 13, 14, 15, 16],
256            &[0, 2, 5, 6, 8, 9, 10, 11, 13, 15, 16],
257            &[0, 1, 2, 5, 6, 8, 9, 12, 13, 14, 16],
258            &[0, 5, 7, 9, 10, 11, 16],
259            &[0, 1, 2, 5, 7, 8, 10, 12, 13, 14, 16],
260            &[0, 2, 3, 5, 9, 11, 12, 13, 16],
261            &[0, 1, 5, 6, 7, 9, 11, 12, 16],
262        ];
263        let f = Field::new(16, 0b1_0000_0000_0010_1101).unwrap();
264        for (i, g) in table6a.iter().enumerate() {
265            let mut p = vec![0u8; 17];
266            for &d in *g {
267                p[d] = 1;
268            }
269            assert_eq!(f.minimal_polynomial(2 * i + 1), p, "g{}", i + 1);
270        }
271    }
272
273    #[test]
274    fn up_to_t_errors_are_corrected_and_more_are_mostly_detected() {
275        // the short DVB-S2 code of rate 1/2: GF(2^14), t = 12, (7200, 7032)
276        let code = Bch::new(14, 0b100_0000_0010_1011, 12, 7200).unwrap();
277        assert_eq!(code.k(), 7032);
278        let mut rng = Rng::new(4);
279        // `errors` distinct positions drawn at random
280        let mut positions: Vec<usize> = (0..code.n()).collect();
281        let (mut detected, mut wrong) = (0, 0);
282        for errors in (0..=12).chain((13..=24).flat_map(|e| [e; 4])) {
283            let msg: Vec<u8> = (0..code.k()).map(|_| rng.bit()).collect();
284            let mut c = code.encode(&msg).unwrap();
285            rng.shuffle(&mut positions);
286            for &p in &positions[..errors] {
287                c[p] ^= 1;
288            }
289            let (m, n) = code.decode(&c).unwrap();
290            if errors <= 12 {
291                // every pattern of at most t errors is corrected and counted
292                assert_eq!((&m, n), (&msg, Some(errors)), "{errors} errors");
293            } else if n.is_none() {
294                // detected: the message bits come back as received
295                assert_eq!(m, c[..code.k()], "{errors} errors");
296                detected += 1;
297            } else {
298                // a decoded word within t of the received one cannot be the sent one
299                assert_ne!(m, msg, "{errors} errors");
300                wrong += 1;
301            }
302        }
303        // a miscorrection needs the t roots of a wrong locator among the 7200 positions of
304        // the shortened code out of 2^14 - 1: rare, none in these 48 patterns
305        assert_eq!((detected, wrong), (48, 0), "{detected} detected, {wrong} miscorrected");
306        // exactly t distinct errors
307        let msg: Vec<u8> = (0..code.k()).map(|_| rng.bit()).collect();
308        let mut c = code.encode(&msg).unwrap();
309        for p in [3, 100, 2000, 5000, 7031, 7032, 7100, 7199, 1, 2, 4000, 6000] {
310            c[p] ^= 1;
311        }
312        assert_eq!(code.decode(&c).unwrap(), (msg, Some(12)));
313    }
314}