Skip to main content

comnum_math/
gf2.rs

1//! Arithmetic over GF(2): binary polynomials and dense binary matrices on bitsets.
2//!
3//! The channel codes build their generators and parity structures from these; nothing
4//! here knows what a code is.
5//!
6//! # Examples
7//! ```
8//! use comnum_math::gf2::{Matrix, poly_mul};
9//! // (1 + x)(1 + x) = 1 + x² over GF(2)
10//! assert_eq!(poly_mul(&[1, 1], &[1, 1]), vec![1, 0, 1]);
11//! // the parity-check matrix of the (3, 1) repetition code has rank 2
12//! let mut h = Matrix::from_rows(3, &[vec![0, 1], vec![1, 2]]);
13//! assert_eq!(h.reduce().len(), 2);
14//! ```
15
16/// Product of binary polynomials, lowest degree first (bits 0 or 1); the empty
17/// polynomial (no coefficient) is zero, and so is its product with any other.
18pub fn poly_mul(a: &[u8], b: &[u8]) -> Vec<u8> {
19    if a.is_empty() || b.is_empty() {
20        return Vec::new();
21    }
22    let mut out = vec![0u8; a.len() + b.len() - 1];
23    for (i, &x) in a.iter().enumerate() {
24        if x == 1 {
25            for (j, &y) in b.iter().enumerate() {
26                out[i + j] ^= y;
27            }
28        }
29    }
30    out
31}
32
33/// The remainder $r(x) = m(x)\, x^{r} \bmod g(x)$ of the message `msg` by the generator
34/// `generator` of degree $r$: the $r$ parity bits of a systematic cyclic code or a CRC.
35///
36/// The message is in transmission order (its first bit the coefficient of the highest
37/// degree), the generator lowest degree first with $g_{r} = 1$; the remainder is
38/// returned highest degree first. A shift register of $r$ bits on 64-bit words, any
39/// degree (the CRCs of 5G NR, the BCH codes of DVB-S2 of degree 192).
40///
41/// # Examples
42/// ```
43/// use comnum_math::gf2::poly_rem;
44/// // the (7, 4) Hamming code of g(x) = 1 + x + x³: x³ · x³ = x⁶ ≡ x² + 1
45/// assert_eq!(poly_rem(&[1, 0, 0, 0], &[1, 1, 0, 1]), [1, 0, 1]);
46/// assert!(poly_rem(&[1, 0, 1], &[1]).is_empty());
47/// ```
48pub fn poly_rem(msg: &[u8], generator: &[u8]) -> Vec<u8> {
49    let r = generator.len().saturating_sub(1);
50    if r == 0 {
51        return Vec::new();
52    }
53    // the register of r bits in 64-bit words, bit d the coefficient of x^d
54    let words = r.div_ceil(64);
55    let mut g = vec![0u64; words];
56    for (d, &b) in generator[..r].iter().enumerate() {
57        if b & 1 == 1 {
58            g[d / 64] |= 1 << (d % 64);
59        }
60    }
61    let mut reg = vec![0u64; words];
62    let top = r - 1;
63    for &b in msg {
64        let fb = ((reg[top / 64] >> (top % 64)) & 1) as u8 ^ (b & 1);
65        // shift left by one
66        for w in (0..words).rev() {
67            let carry = if w > 0 { reg[w - 1] >> 63 } else { 0 };
68            reg[w] = (reg[w] << 1) | carry;
69        }
70        if r % 64 != 0 {
71            reg[words - 1] &= (1u64 << (r % 64)) - 1;
72        }
73        if fb == 1 {
74            for (x, y) in reg.iter_mut().zip(&g) {
75                *x ^= y;
76            }
77        }
78    }
79    (0..r).rev().map(|d| ((reg[d / 64] >> (d % 64)) & 1) as u8).collect()
80}
81
82/// A dense binary matrix, every row a bitset of `n_cols` bits.
83#[derive(Clone, Debug, PartialEq, Eq)]
84pub struct Matrix {
85    n_cols: usize,
86    rows: Vec<Vec<u64>>,
87}
88
89impl Matrix {
90    /// The matrix of `n_cols` columns whose rows list the columns that hold a 1.
91    pub fn from_rows(n_cols: usize, rows: &[Vec<usize>]) -> Matrix {
92        let words = n_cols.div_ceil(64);
93        let rows = rows
94            .iter()
95            .map(|r| {
96                let mut w = vec![0u64; words];
97                for &c in r {
98                    w[c / 64] ^= 1 << (c % 64);
99                }
100                w
101            })
102            .collect();
103        Matrix { n_cols, rows }
104    }
105
106    /// Number of rows.
107    pub fn n_rows(&self) -> usize {
108        self.rows.len()
109    }
110
111    /// Number of columns.
112    pub fn n_cols(&self) -> usize {
113        self.n_cols
114    }
115
116    /// The entry at row `r`, column `c`.
117    pub fn get(&self, r: usize, c: usize) -> bool {
118        (self.rows[r][c / 64] >> (c % 64)) & 1 == 1
119    }
120
121    /// Reduced row echelon form in place, the pivots searched from the last column
122    /// to the first: returns the pivot column of every leading row, in that order.
123    /// The remaining rows are zero.
124    pub fn reduce(&mut self) -> Vec<usize> {
125        let mut pivots = Vec::new();
126        let mut r = 0;
127        for c in (0..self.n_cols).rev() {
128            if r == self.rows.len() {
129                break;
130            }
131            let Some(p) = (r..self.rows.len()).find(|&i| self.get(i, c)) else { continue };
132            self.rows.swap(r, p);
133            for i in 0..self.rows.len() {
134                if i != r && self.get(i, c) {
135                    let (a, b) = if i < r {
136                        let (x, y) = self.rows.split_at_mut(r);
137                        (&mut x[i], &y[0])
138                    } else {
139                        let (x, y) = self.rows.split_at_mut(i);
140                        (&mut y[0], &x[r])
141                    };
142                    for (u, v) in a.iter_mut().zip(b.iter()) {
143                        *u ^= v;
144                    }
145                }
146            }
147            pivots.push(c);
148            r += 1;
149        }
150        pivots
151    }
152
153    /// The inverse of a square matrix by Gauss–Jordan elimination, `None` if it is not
154    /// square or is singular.
155    ///
156    /// # Examples
157    /// ```
158    /// use comnum_math::gf2::Matrix;
159    /// // [[1, 1], [0, 1]] is its own inverse over GF(2)
160    /// let a = Matrix::from_rows(2, &[vec![0, 1], vec![1]]);
161    /// assert_eq!(a.inverse(), Some(a.clone()));
162    /// assert!(Matrix::from_rows(2, &[vec![0, 1], vec![0, 1]]).inverse().is_none());
163    /// ```
164    pub fn inverse(&self) -> Option<Matrix> {
165        let n = self.rows.len();
166        if n != self.n_cols {
167            return None;
168        }
169        let words = n.div_ceil(64);
170        let mut a = self.rows.clone();
171        let mut inv: Vec<Vec<u64>> = (0..n)
172            .map(|i| {
173                let mut w = vec![0u64; words];
174                w[i / 64] |= 1 << (i % 64);
175                w
176            })
177            .collect();
178        for c in 0..n {
179            let (word, bit) = (c / 64, c % 64);
180            let p = (c..n).find(|&i| (a[i][word] >> bit) & 1 == 1)?;
181            a.swap(c, p);
182            inv.swap(c, p);
183            let (pivot_a, pivot_inv) = (a[c].clone(), inv[c].clone());
184            for i in 0..n {
185                if i != c && (a[i][word] >> bit) & 1 == 1 {
186                    // the columns below c are already zero in the pivot row
187                    for (u, v) in a[i][word..].iter_mut().zip(&pivot_a[word..]) {
188                        *u ^= v;
189                    }
190                    for (u, v) in inv[i].iter_mut().zip(&pivot_inv) {
191                        *u ^= v;
192                    }
193                }
194            }
195        }
196        Some(Matrix { n_cols: n, rows: inv })
197    }
198
199    /// The product $A v$ over GF(2) of the vector `v` of `n_cols` bits (0 or 1, extra
200    /// bits ignored).
201    pub fn mul_vec(&self, v: &[u8]) -> Vec<u8> {
202        let mut packed = vec![0u64; self.n_cols.div_ceil(64)];
203        for (c, &b) in v.iter().enumerate().take(self.n_cols) {
204            if b & 1 == 1 {
205                packed[c / 64] |= 1 << (c % 64);
206            }
207        }
208        self.rows
209            .iter()
210            .map(|row| (row.iter().zip(&packed).map(|(a, b)| (a & b).count_ones()).sum::<u32>() & 1) as u8)
211            .collect()
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    /// The remainder by long division of m(x) x^r, written directly.
220    fn long_division(msg: &[u8], g: &[u8]) -> Vec<u8> {
221        let r = g.len() - 1;
222        // highest degree first: the message then r zeros
223        let mut a: Vec<u8> = msg.iter().copied().chain(std::iter::repeat_n(0, r)).collect();
224        for i in 0..msg.len() {
225            if a[i] == 1 {
226                for d in 0..=r {
227                    a[i + d] ^= g[r - d];
228                }
229            }
230        }
231        a[msg.len()..].to_vec()
232    }
233
234    #[test]
235    fn the_remainder_is_that_of_the_long_division_at_any_degree() {
236        let mut state = 0x1234_5678_9abc_def1u64;
237        let mut bit = || {
238            state ^= state << 13;
239            state ^= state >> 7;
240            state ^= state << 17;
241            (state & 1) as u8
242        };
243        for r in [1usize, 5, 24, 63, 64, 65, 128, 192] {
244            let mut g: Vec<u8> = (0..r).map(|_| bit()).collect();
245            g.push(1);
246            for len in [0usize, 1, 7, 100] {
247                let msg: Vec<u8> = (0..len).map(|_| bit()).collect();
248                assert_eq!(poly_rem(&msg, &g), long_division(&msg, &g), "r = {r}, {len} bits");
249            }
250        }
251    }
252
253    #[test]
254    fn the_product_with_the_empty_polynomial_is_empty_instead_of_an_underflow() {
255        let none: Vec<u8> = Vec::new();
256        assert_eq!(poly_mul(&[], &[]), none);
257        assert_eq!((poly_mul(&[1, 1], &[]), poly_mul(&[], &[1])), (none.clone(), none));
258    }
259
260    #[test]
261    fn polynomial_products_and_rank() {
262        assert_eq!(poly_mul(&[1, 1, 0, 1], &[1]), vec![1, 1, 0, 1]);
263        assert_eq!(poly_mul(&[1, 1], &[1, 1, 1]), vec![1, 0, 0, 1]);
264        // two equal rows have rank 1; the reduction leaves the second one zero
265        let mut m = Matrix::from_rows(70, &[vec![3, 69], vec![3, 69], vec![0]]);
266        let pivots = m.reduce();
267        assert_eq!(pivots, vec![69, 0]);
268        assert!(m.get(0, 69) && m.get(0, 3) && !m.get(2, 3) && !m.get(2, 69));
269        assert_eq!((m.n_rows(), m.n_cols()), (3, 70));
270    }
271    #[test]
272    fn the_inverse_of_a_random_invertible_matrix_gives_the_identity() {
273        // a unit lower-triangular matrix times a unit upper-triangular one is invertible
274        let n = 150;
275        let mut state = 12_345u64;
276        let mut bit = || {
277            state = state.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
278            (state >> 33) & 1 == 1
279        };
280        let lower: Vec<Vec<usize>> = (0..n).map(|i| (0..i).filter(|_| bit()).chain([i]).collect()).collect();
281        let upper: Vec<Vec<usize>> =
282            (0..n).map(|i| [i].into_iter().chain((i + 1..n).filter(|_| bit())).collect()).collect();
283        let l = Matrix::from_rows(n, &lower);
284        let u = Matrix::from_rows(n, &upper);
285        // the rows of L U: row i is the sum of the rows of U selected by row i of L
286        let lu_rows: Vec<Vec<usize>> = (0..n)
287            .map(|i| {
288                let mut acc = vec![0u8; n];
289                for j in (0..n).filter(|&j| l.get(i, j)) {
290                    for (c, a) in acc.iter_mut().enumerate() {
291                        *a ^= u8::from(u.get(j, c));
292                    }
293                }
294                (0..n).filter(|&c| acc[c] == 1).collect()
295            })
296            .collect();
297        let a = Matrix::from_rows(n, &lu_rows);
298        let inv = a.inverse().unwrap();
299        // A (A^-1 e_c) = e_c for every column c
300        for c in 0..n {
301            let col: Vec<u8> = (0..n).map(|r| u8::from(inv.get(r, c))).collect();
302            let back = a.mul_vec(&col);
303            assert!(back.iter().enumerate().all(|(r, &b)| b == u8::from(r == c)), "column {c}");
304        }
305    }
306}