Skip to main content

rust_par2/
matrix.rs

1//! Matrix operations over GF(2^16) for Reed-Solomon decoding.
2//!
3//! PAR2 repair requires:
4//! 1. Building a Vandermonde-like matrix from recovery block exponents
5//! 2. Selecting rows corresponding to available (non-damaged) data + recovery blocks
6//! 3. Inverting the submatrix via Gaussian elimination
7//! 4. Multiplying the inverse by recovery data to recover original blocks
8
9use crate::gf;
10
11/// A matrix over GF(2^16), stored in row-major order.
12#[derive(Debug, Clone)]
13pub struct GfMatrix {
14    pub rows: usize,
15    pub cols: usize,
16    /// Row-major data: element at (r, c) is data[r * cols + c].
17    pub data: Vec<u16>,
18}
19
20impl GfMatrix {
21    /// Create a zero matrix.
22    pub fn zeros(rows: usize, cols: usize) -> Self {
23        Self {
24            rows,
25            cols,
26            data: vec![0u16; rows * cols],
27        }
28    }
29
30    /// Create an identity matrix.
31    pub fn identity(n: usize) -> Self {
32        let mut m = Self::zeros(n, n);
33        for i in 0..n {
34            m.set(i, i, 1);
35        }
36        m
37    }
38
39    /// Get element at (row, col).
40    #[inline]
41    pub fn get(&self, row: usize, col: usize) -> u16 {
42        self.data[row * self.cols + col]
43    }
44
45    /// Set element at (row, col).
46    #[inline]
47    pub fn set(&mut self, row: usize, col: usize, val: u16) {
48        self.data[row * self.cols + col] = val;
49    }
50
51    /// Build the PAR2 encoding matrix.
52    ///
53    /// PAR2 recovery blocks use: `recovery[e] = Σ (input[i] * c[i]^e)`
54    /// where `c[i]` are per-input-slice constants assigned by the PAR2 spec.
55    ///
56    /// The encoding matrix has `input_count` columns (one per data block).
57    /// Row `i` for i < input_count is the identity (data blocks are passed through).
58    /// Row `input_count + r` is the recovery row for exponent `recovery_exponents[r]`:
59    ///   `[c[0]^exp, c[1]^exp, c[2]^exp, ..., c[k-1]^exp]`
60    pub fn par2_encoding_matrix(input_count: usize, recovery_exponents: &[u32]) -> Self {
61        let total_rows = input_count + recovery_exponents.len();
62        let mut m = Self::zeros(total_rows, input_count);
63
64        // Compute input slice constants per PAR2 spec
65        let constants = par2_input_constants(input_count);
66
67        // Identity rows for data blocks
68        for i in 0..input_count {
69            m.set(i, i, 1);
70        }
71
72        // Recovery rows: row[input_count + r][c] = constants[c] ^ exponent[r]
73        for (r, &exp) in recovery_exponents.iter().enumerate() {
74            for c in 0..input_count {
75                let val = gf::pow(constants[c], exp);
76                m.set(input_count + r, c, val);
77            }
78        }
79
80        m
81    }
82
83    /// Select specific rows from this matrix, returning a new matrix.
84    pub fn select_rows(&self, row_indices: &[usize]) -> Self {
85        let mut result = Self::zeros(row_indices.len(), self.cols);
86        for (new_row, &old_row) in row_indices.iter().enumerate() {
87            let src_start = old_row * self.cols;
88            let dst_start = new_row * self.cols;
89            result.data[dst_start..dst_start + self.cols]
90                .copy_from_slice(&self.data[src_start..src_start + self.cols]);
91        }
92        result
93    }
94
95    /// Invert this square matrix using Gaussian elimination over GF(2^16).
96    /// Returns None if the matrix is singular.
97    pub fn invert(&self) -> Option<Self> {
98        assert_eq!(self.rows, self.cols, "Can only invert square matrices");
99        let n = self.rows;
100
101        // Augmented matrix [A | I]
102        let mut aug = Self::zeros(n, 2 * n);
103        for r in 0..n {
104            for c in 0..n {
105                aug.set(r, c, self.get(r, c));
106            }
107            aug.set(r, n + r, 1); // Identity on the right
108        }
109
110        // Forward elimination (row echelon form)
111        for col in 0..n {
112            // Find pivot row
113            let mut pivot_row = None;
114            for r in col..n {
115                if aug.get(r, col) != 0 {
116                    pivot_row = Some(r);
117                    break;
118                }
119            }
120            let pivot_row = pivot_row?; // Singular if no pivot found
121
122            // Swap pivot row into position
123            if pivot_row != col {
124                for c in 0..2 * n {
125                    let tmp = aug.get(col, c);
126                    aug.set(col, c, aug.get(pivot_row, c));
127                    aug.set(pivot_row, c, tmp);
128                }
129            }
130
131            // Scale pivot row so pivot element = 1
132            let pivot_val = aug.get(col, col);
133            let pivot_inv = gf::inv(pivot_val);
134            for c in 0..2 * n {
135                aug.set(col, c, gf::mul(aug.get(col, c), pivot_inv));
136            }
137
138            // Eliminate column in all other rows
139            for r in 0..n {
140                if r == col {
141                    continue;
142                }
143                let factor = aug.get(r, col);
144                if factor == 0 {
145                    continue;
146                }
147                for c in 0..2 * n {
148                    let val = gf::add(aug.get(r, c), gf::mul(factor, aug.get(col, c)));
149                    aug.set(r, c, val);
150                }
151            }
152        }
153
154        // Extract the inverse (right half of augmented matrix)
155        let mut result = Self::zeros(n, n);
156        for r in 0..n {
157            for c in 0..n {
158                result.set(r, c, aug.get(r, n + c));
159            }
160        }
161
162        Some(result)
163    }
164}
165
166/// Compute the PAR2 input slice constants.
167///
168/// Per the PAR2 spec, each input slice is assigned a constant `c[i] = 2^n[i]`
169/// where `n[i]` is the i-th valid exponent. Valid exponents satisfy:
170///   `n % 3 != 0 && n % 5 != 0 && n % 17 != 0 && n % 257 != 0`
171///
172/// This ensures all constants have order 65535 (the full multiplicative group),
173/// which guarantees the Vandermonde matrix is non-singular.
174pub fn par2_input_constants(count: usize) -> Vec<u16> {
175    let mut constants = Vec::with_capacity(count);
176    let mut n: u32 = 0;
177    while constants.len() < count {
178        n += 1;
179        if n % 3 != 0 && n % 5 != 0 && n % 17 != 0 && n % 257 != 0 {
180            constants.push(gf::exp2(n));
181        }
182    }
183    constants
184}
185
186// ---------------------------------------------------------------------------
187// Tests
188// ---------------------------------------------------------------------------
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193
194    #[test]
195    fn test_identity_inverse() {
196        let id = GfMatrix::identity(4);
197        let inv = id.invert().unwrap();
198        for r in 0..4 {
199            for c in 0..4 {
200                let expected = if r == c { 1 } else { 0 };
201                assert_eq!(inv.get(r, c), expected);
202            }
203        }
204    }
205
206    #[test]
207    fn test_inverse_roundtrip() {
208        // Create a known non-singular matrix and verify A * A^-1 = I
209        let mut m = GfMatrix::zeros(3, 3);
210        m.set(0, 0, 1); m.set(0, 1, 2); m.set(0, 2, 3);
211        m.set(1, 0, 4); m.set(1, 1, 5); m.set(1, 2, 6);
212        m.set(2, 0, 7); m.set(2, 1, 8); m.set(2, 2, 10);
213
214        let inv = m.invert().unwrap();
215
216        // Verify M * M^-1 = I
217        for r in 0..3 {
218            for c in 0..3 {
219                let mut sum = 0u16;
220                for k in 0..3 {
221                    sum = gf::add(sum, gf::mul(m.get(r, k), inv.get(k, c)));
222                }
223                let expected = if r == c { 1 } else { 0 };
224                assert_eq!(sum, expected, "M*M^-1 [{r},{c}] should be {expected}");
225            }
226        }
227    }
228
229    #[test]
230    fn test_vandermonde_invertible() {
231        // A Vandermonde matrix with distinct generators is always invertible
232        let exponents = vec![0, 1, 2];
233        let m = GfMatrix::par2_encoding_matrix(3, &exponents);
234        // The recovery submatrix (rows 3..6) should be invertible
235        let recovery = m.select_rows(&[3, 4, 5]);
236        assert!(recovery.invert().is_some(), "Vandermonde submatrix should be invertible");
237    }
238
239    #[test]
240    fn test_select_rows() {
241        let mut m = GfMatrix::zeros(4, 3);
242        for r in 0..4 {
243            for c in 0..3 {
244                m.set(r, c, (r * 10 + c) as u16);
245            }
246        }
247        let sub = m.select_rows(&[1, 3]);
248        assert_eq!(sub.rows, 2);
249        assert_eq!(sub.cols, 3);
250        assert_eq!(sub.get(0, 0), 10);
251        assert_eq!(sub.get(1, 2), 32);
252    }
253
254    #[test]
255    fn test_singular_matrix() {
256        // All-zero matrix is singular
257        let m = GfMatrix::zeros(3, 3);
258        assert!(m.invert().is_none());
259
260        // Matrix with duplicate rows is singular
261        let mut m = GfMatrix::zeros(2, 2);
262        m.set(0, 0, 1); m.set(0, 1, 2);
263        m.set(1, 0, 1); m.set(1, 1, 2);
264        assert!(m.invert().is_none());
265    }
266}