Skip to main content

par2_rs/
matrix.rs

1//! Matrix operations over GF(2^16) for PAR2 Reed-Solomon repair.
2//!
3//! Provides:
4//! - A row-major matrix type over GF(2^16)
5//! - Vandermonde matrix row construction
6//! - Gaussian elimination with partial pivoting
7//! - Decode matrix construction for repair
8
9use crate::error::{Par2Error, Result};
10use crate::gf;
11use crate::gf_pmul;
12use crate::gf_simd;
13use rayon::prelude::*;
14
15const SIMD_ELIMINATION_ROWS: usize = 16;
16const PARALLEL_ELIMINATION_ROWS: usize = 128;
17const PARALLEL_ELIMINATION_THRESHOLD: usize = 256;
18
19/// At or above this square size the repair-matrix solve routes to the rank-k
20/// tiled inverter ([`crate::matrix_tiled`]) instead of the rank-1 path. Held
21/// equal to [`PARALLEL_ELIMINATION_THRESHOLD`] because the tiled path only wins
22/// once the elimination is large enough for its batched apply to also run
23/// across rayon workers (below that both paths are serial and the rank-1 path's
24/// simpler per-column loop is competitive).
25const TILED_ELIMINATION_THRESHOLD: usize = PARALLEL_ELIMINATION_THRESHOLD;
26
27/// Whether the rank-k tiled inverter is enabled. Read per solve (solve
28/// granularity is not hot, so this avoids a `OnceLock` and lets tests/benches
29/// toggle it in-process). Enabled unless `WEAVER_MATRIX_TILED` is set to `0`.
30fn tiled_env_enabled() -> bool {
31    std::env::var_os("WEAVER_MATRIX_TILED").is_none_or(|v| v != "0")
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub(crate) struct DecodeMatrixError {
36    pub bad_row: Option<usize>,
37    pub reason: String,
38}
39
40impl DecodeMatrixError {
41    fn new(reason: String) -> Self {
42        Self {
43            bad_row: None,
44            reason,
45        }
46    }
47
48    fn singular(bad_row: usize) -> Self {
49        Self {
50            bad_row: Some(bad_row),
51            reason: "matrix is singular (no pivot found)".to_string(),
52        }
53    }
54
55    fn into_par2_error(self) -> Par2Error {
56        Par2Error::ReedSolomonError {
57            reason: self.reason,
58        }
59    }
60}
61
62/// A row-major matrix over GF(2^16).
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Matrix {
65    pub rows: usize,
66    pub cols: usize,
67    pub data: Vec<u16>,
68}
69
70impl Matrix {
71    /// Create a new matrix filled with zeros.
72    pub fn zeros(rows: usize, cols: usize) -> Self {
73        Self {
74            rows,
75            cols,
76            data: vec![0u16; rows.saturating_mul(cols)],
77        }
78    }
79
80    /// Create an identity matrix.
81    pub fn identity(n: usize) -> Self {
82        let mut m = Self::zeros(n, n);
83        for i in 0..n {
84            m.set(i, i, 1);
85        }
86        m
87    }
88
89    /// Compute a single row of the Vandermonde encoding matrix.
90    ///
91    /// For constants `[c0, c1, ..., cn-1]` and exponent `e`, produces
92    /// the row `[c0^e, c1^e, ..., cn-1^e]`.
93    pub fn vandermonde_row(constants: &[u16], exponent: u32) -> Vec<u16> {
94        constants.iter().map(|&c| gf::pow(c, exponent)).collect()
95    }
96
97    #[inline]
98    fn offset(&self, row: usize, col: usize) -> usize {
99        row * self.cols + col
100    }
101
102    #[inline]
103    pub(crate) fn get(&self, row: usize, col: usize) -> u16 {
104        self.data[self.offset(row, col)]
105    }
106
107    #[inline]
108    fn set(&mut self, row: usize, col: usize, value: u16) {
109        let offset = self.offset(row, col);
110        self.data[offset] = value;
111    }
112
113    #[inline]
114    pub(crate) fn row(&self, row: usize) -> &[u16] {
115        let start = row * self.cols;
116        &self.data[start..start + self.cols]
117    }
118
119    #[inline]
120    fn row_mut(&mut self, row: usize) -> &mut [u16] {
121        let start = row * self.cols;
122        &mut self.data[start..start + self.cols]
123    }
124
125    fn extract_columns(&self, start: usize, len: usize) -> Matrix {
126        let mut extracted = Matrix::zeros(self.rows, len);
127        for row_idx in 0..self.rows {
128            let src = &self.row(row_idx)[start..start + len];
129            extracted.row_mut(row_idx).copy_from_slice(src);
130        }
131        extracted
132    }
133
134    #[cfg(test)]
135    fn copy_row_from_slice(&mut self, row: usize, values: &[u16]) {
136        assert_eq!(
137            values.len(),
138            self.cols,
139            "row width must match matrix columns"
140        );
141        self.row_mut(row).copy_from_slice(values);
142    }
143
144    fn swap_rows(data: &mut [u16], cols: usize, a: usize, b: usize) {
145        if a == b {
146            return;
147        }
148
149        let a_start = a * cols;
150        let b_start = b * cols;
151        if a_start < b_start {
152            let (head, tail) = data.split_at_mut(b_start);
153            head[a_start..a_start + cols].swap_with_slice(&mut tail[..cols]);
154        } else {
155            let (head, tail) = data.split_at_mut(a_start);
156            tail[..cols].swap_with_slice(&mut head[b_start..b_start + cols]);
157        }
158    }
159
160    fn split_two_rows(
161        data: &mut [u16],
162        cols: usize,
163        a: usize,
164        b: usize,
165    ) -> (&mut [u16], &mut [u16]) {
166        assert_ne!(a, b, "row split requires distinct rows");
167
168        let a_start = a * cols;
169        let b_start = b * cols;
170        if a_start < b_start {
171            let (head, tail) = data.split_at_mut(b_start);
172            (&mut head[a_start..a_start + cols], &mut tail[..cols])
173        } else {
174            let (head, tail) = data.split_at_mut(a_start);
175            (&mut tail[..cols], &mut head[b_start..b_start + cols])
176        }
177    }
178
179    /// Perform in-place Gaussian elimination over GF(2^16).
180    ///
181    /// Transforms `self` into reduced row echelon form while applying the same
182    /// row operations to `rhs`. The matrix must be square.
183    ///
184    /// After elimination, `self` will be the identity matrix (if invertible)
185    /// and `rhs` will contain the solution/inverse.
186    pub fn gaussian_eliminate(&mut self, rhs: &mut Matrix) -> Result<()> {
187        let mut row_origins = (0..self.rows).collect::<Vec<_>>();
188        self.gaussian_eliminate_tracked(rhs, &mut row_origins)
189            .map_err(DecodeMatrixError::into_par2_error)
190    }
191
192    fn gaussian_eliminate_tracked(
193        &mut self,
194        rhs: &mut Matrix,
195        row_origins: &mut [usize],
196    ) -> std::result::Result<(), DecodeMatrixError> {
197        let n = self.rows;
198        if self.cols != n {
199            return Err(DecodeMatrixError::new(format!(
200                "matrix is not square: {}x{}",
201                self.rows, self.cols
202            )));
203        }
204        if rhs.rows != n {
205            return Err(DecodeMatrixError::new(format!(
206                "RHS row count {} does not match matrix rows {}",
207                rhs.rows, n
208            )));
209        }
210        if row_origins.len() != n {
211            return Err(DecodeMatrixError::new(format!(
212                "row origin count {} does not match matrix rows {}",
213                row_origins.len(),
214                n
215            )));
216        }
217
218        for col in 0..n {
219            // Partial pivoting: find a row with nonzero entry in this column.
220            let pivot_row = (col..n).find(|&r| self.get(r, col) != 0);
221            let pivot_row = match pivot_row {
222                Some(r) => r,
223                None => {
224                    return Err(DecodeMatrixError::singular(row_origins[col]));
225                }
226            };
227
228            // Swap pivot row into position.
229            if pivot_row != col {
230                Self::swap_rows(&mut self.data, self.cols, col, pivot_row);
231                Self::swap_rows(&mut rhs.data, rhs.cols, col, pivot_row);
232                row_origins.swap(col, pivot_row);
233            }
234
235            // Scale pivot row so that self[col][col] = 1.
236            let pivot_val = self.get(col, col);
237            if pivot_val != 1 {
238                let pivot_inv = gf::inv(pivot_val);
239                for value in &mut self.row_mut(col)[col..] {
240                    *value = gf::mul(*value, pivot_inv);
241                }
242                for value in rhs.row_mut(col).iter_mut() {
243                    *value = gf::mul(*value, pivot_inv);
244                }
245            }
246
247            // Eliminate this column in all other rows.
248            for row in 0..n {
249                if row == col {
250                    continue;
251                }
252                let factor = self.get(row, col);
253                if factor == 0 {
254                    continue;
255                }
256
257                let (target_row, pivot_row) =
258                    Self::split_two_rows(&mut self.data, self.cols, row, col);
259                if factor == 1 {
260                    for (target, pivot) in target_row[col..].iter_mut().zip(&pivot_row[col..]) {
261                        *target ^= *pivot;
262                    }
263                } else {
264                    for (target, pivot) in target_row[col..].iter_mut().zip(&pivot_row[col..]) {
265                        *target ^= gf::mul(factor, *pivot);
266                    }
267                }
268
269                let (target_rhs, pivot_rhs) =
270                    Self::split_two_rows(&mut rhs.data, rhs.cols, row, col);
271                if factor == 1 {
272                    for (target, pivot) in target_rhs.iter_mut().zip(pivot_rhs.iter()) {
273                        *target ^= *pivot;
274                    }
275                } else {
276                    for (target, pivot) in target_rhs.iter_mut().zip(pivot_rhs.iter()) {
277                        *target ^= gf::mul(factor, *pivot);
278                    }
279                }
280            }
281        }
282
283        Ok(())
284    }
285
286    /// Invert this square matrix in-place, returning the inverse.
287    pub fn invert(&self) -> Result<Matrix> {
288        let n = self.rows;
289        let mut m = self.clone();
290        let mut inv = Matrix::identity(n);
291        m.gaussian_eliminate(&mut inv)?;
292        Ok(inv)
293    }
294
295    fn gaussian_eliminate_vandermonde(
296        &mut self,
297        rhs: &mut Matrix,
298    ) -> std::result::Result<(), DecodeMatrixError> {
299        let use_tiled = self.rows >= TILED_ELIMINATION_THRESHOLD && tiled_env_enabled();
300        self.gaussian_eliminate_vandermonde_inner(rhs, use_tiled)
301    }
302
303    /// Reduced-row-echelon solve over the Vandermonde submatrix, applying the
304    /// same operations to `rhs`. `use_tiled` selects the rank-k tiled inverter
305    /// ([`crate::matrix_tiled`]) over the rank-1 per-column path; the two are
306    /// byte-identical (unique GF(2^16) reduced form) including the singular
307    /// `bad_row`. Split from the env/threshold decision so tests and benches
308    /// drive either path with an explicit flag, never a racy `set_var`.
309    fn gaussian_eliminate_vandermonde_inner(
310        &mut self,
311        rhs: &mut Matrix,
312        use_tiled: bool,
313    ) -> std::result::Result<(), DecodeMatrixError> {
314        let n = self.rows;
315        if self.cols != n {
316            return Err(DecodeMatrixError::new(format!(
317                "matrix is not square: {}x{}",
318                self.rows, self.cols
319            )));
320        }
321        if rhs.rows != n {
322            return Err(DecodeMatrixError::new(format!(
323                "RHS row count {} does not match matrix rows {}",
324                rhs.rows, n
325            )));
326        }
327
328        if use_tiled {
329            return crate::matrix_tiled::invert_augmented_tiled(
330                &mut self.data,
331                &mut rhs.data,
332                n,
333                rhs.cols,
334            )
335            .map_err(DecodeMatrixError::singular);
336        }
337
338        for col in 0..n {
339            let pivot_val = self.get(col, col);
340            if pivot_val == 0 {
341                return Err(DecodeMatrixError::singular(col));
342            }
343
344            if pivot_val != 1 {
345                let pivot_inv = gf::inv(pivot_val);
346                for value in &mut self.row_mut(col)[col..] {
347                    *value = gf::mul(*value, pivot_inv);
348                }
349                for value in rhs.row_mut(col).iter_mut() {
350                    *value = gf::mul(*value, pivot_inv);
351                }
352            }
353
354            let pivot_matrix_tail = self.row(col)[col..].to_vec();
355            let pivot_rhs_row = rhs.row(col).to_vec();
356            let pivot_matrix_bytes = words_as_bytes(&pivot_matrix_tail);
357            let pivot_rhs_bytes = words_as_bytes(&pivot_rhs_row);
358            let matrix_ptr = self.data.as_mut_ptr() as usize;
359            let rhs_ptr = rhs.data.as_mut_ptr() as usize;
360            let matrix_cols = self.cols;
361            let rhs_cols = rhs.cols;
362            // `parallel_enabled()` const-folds to `true` on native (the guard is
363            // the original expression, byte-identical codegen). On wasm it is a
364            // cached runtime probe: `false` on single-threaded `wasm32-wasip1`,
365            // so the row batches take the serial `SIMD_ELIMINATION_ROWS` branch
366            // and `rayon::current_num_threads` is never evaluated; `true` on
367            // `wasm32-wasip1-threads`, where rayon has a real worker pool.
368            let row_group = if reedsolomon_rs::threading::parallel_enabled()
369                && n >= PARALLEL_ELIMINATION_THRESHOLD
370                && rayon::current_num_threads() > 1
371            {
372                PARALLEL_ELIMINATION_ROWS
373            } else {
374                SIMD_ELIMINATION_ROWS
375            };
376            let eliminate_batch = |batch_start: usize, batch_end: usize| unsafe {
377                let mut matrix_pairs = Vec::with_capacity(batch_end - batch_start);
378                let mut rhs_pairs = Vec::with_capacity(batch_end - batch_start);
379
380                for row in batch_start..batch_end {
381                    if row == col {
382                        continue;
383                    }
384
385                    let factor = *((matrix_ptr as *const u16).add(row * matrix_cols + col));
386                    if factor == 0 {
387                        continue;
388                    }
389
390                    let row_start = row * matrix_cols + col;
391                    let row_len = matrix_cols - col;
392                    let rhs_start = row * rhs_cols;
393
394                    let matrix_row = std::slice::from_raw_parts_mut(
395                        (matrix_ptr as *mut u16).add(row_start),
396                        row_len,
397                    );
398                    matrix_pairs.push(gf_simd::FactorDst {
399                        factor,
400                        dst: words_as_bytes_mut(matrix_row),
401                    });
402
403                    let rhs_row = std::slice::from_raw_parts_mut(
404                        (rhs_ptr as *mut u16).add(rhs_start),
405                        rhs_cols,
406                    );
407                    rhs_pairs.push(gf_simd::FactorDst {
408                        factor,
409                        dst: words_as_bytes_mut(rhs_row),
410                    });
411                }
412
413                if !matrix_pairs.is_empty() {
414                    gf_simd::mul_acc_multi_region(&mut matrix_pairs, pivot_matrix_bytes);
415                    gf_simd::mul_acc_multi_region(&mut rhs_pairs, pivot_rhs_bytes);
416                }
417            };
418
419            if row_group == SIMD_ELIMINATION_ROWS {
420                for batch_start in (0..n).step_by(row_group) {
421                    eliminate_batch(batch_start, (batch_start + row_group).min(n));
422                }
423            } else {
424                let batch_starts: Vec<_> = (0..n).step_by(row_group).collect();
425                batch_starts.into_par_iter().for_each(|batch_start| {
426                    eliminate_batch(batch_start, (batch_start + row_group).min(n));
427                });
428            }
429        }
430
431        Ok(())
432    }
433}
434
435/// Fill the Vandermonde rows of `submatrix` (missing columns) and
436/// `repair_matrix` (`[avail | I]`). Rows whose exponent continues a run
437/// (`exp == prev_exp + 1`) are built by element-wise multiplication of the
438/// previous row with the gathered exponent-1 base row. Gathering that base
439/// from `constants` permits a per-row multiply-versus-power decision; both
440/// paths are byte-identical (`c^e == c * c^(e-1)` in the field).
441fn fill_vandermonde_rows(
442    submatrix: &mut Matrix,
443    repair_matrix: &mut Matrix,
444    available_indices: &[usize],
445    missing_indices: &[usize],
446    recovery_exponents: &[u32],
447    constants: &[u16],
448) {
449    let avail_len = available_indices.len();
450    let sub_base: Vec<u16> = missing_indices.iter().map(|&idx| constants[idx]).collect();
451    let rep_base: Vec<u16> = available_indices
452        .iter()
453        .map(|&idx| constants[idx])
454        .collect();
455
456    for (i, &exp) in recovery_exponents.iter().enumerate() {
457        // checked_add also rules out exp == 0 (it can only match exp >= 1).
458        let sequential = i > 0 && recovery_exponents[i - 1].checked_add(1) == Some(exp);
459        if sequential {
460            let (cur, prev) = Matrix::split_two_rows(&mut submatrix.data, submatrix.cols, i, i - 1);
461            gf_pmul::pmul_region(
462                words_as_bytes_mut(cur),
463                words_as_bytes(prev),
464                words_as_bytes(&sub_base),
465            );
466
467            let (cur, prev) =
468                Matrix::split_two_rows(&mut repair_matrix.data, repair_matrix.cols, i, i - 1);
469            gf_pmul::pmul_region(
470                words_as_bytes_mut(&mut cur[..avail_len]),
471                words_as_bytes(&prev[..avail_len]),
472                words_as_bytes(&rep_base),
473            );
474            cur[avail_len + i] = 1;
475        } else {
476            let row = submatrix.row_mut(i);
477            for (slot, &idx) in row.iter_mut().zip(missing_indices.iter()) {
478                *slot = gf::pow(constants[idx], exp);
479            }
480
481            let row = repair_matrix.row_mut(i);
482            for (slot, &idx) in row.iter_mut().take(avail_len).zip(available_indices.iter()) {
483                *slot = gf::pow(constants[idx], exp);
484            }
485            row[avail_len + i] = 1;
486        }
487    }
488}
489
490#[inline]
491fn words_as_bytes(words: &[u16]) -> &[u8] {
492    unsafe { std::slice::from_raw_parts(words.as_ptr().cast::<u8>(), words.len() * 2) }
493}
494
495#[inline]
496fn words_as_bytes_mut(words: &mut [u16]) -> &mut [u8] {
497    unsafe { std::slice::from_raw_parts_mut(words.as_mut_ptr().cast::<u8>(), words.len() * 2) }
498}
499
500/// Build the decode matrix needed for repair.
501///
502/// Given:
503/// - `missing_indices`: global indices of missing input slices
504/// - `recovery_exponents`: exponents of available recovery blocks to use
505/// - `constants`: the PAR2 constant assignment for all input slices
506///
507/// Constructs the submatrix of the Vandermonde encoding matrix corresponding
508/// to the selected recovery exponents and missing slice positions, then inverts it.
509///
510/// The number of recovery exponents must equal the number of missing indices.
511pub fn build_decode_matrix(
512    missing_indices: &[usize],
513    recovery_exponents: &[u32],
514    constants: &[u16],
515) -> Result<Matrix> {
516    build_decode_matrix_with_bad_row(missing_indices, recovery_exponents, constants)
517        .map_err(DecodeMatrixError::into_par2_error)
518}
519
520pub(crate) fn build_decode_matrix_with_bad_row(
521    missing_indices: &[usize],
522    recovery_exponents: &[u32],
523    constants: &[u16],
524) -> std::result::Result<Matrix, DecodeMatrixError> {
525    build_repair_matrix_with_bad_row(&[], missing_indices, recovery_exponents, constants)
526        .map(|(_, decode)| decode)
527}
528
529pub(crate) fn build_repair_matrix_with_bad_row(
530    available_indices: &[usize],
531    missing_indices: &[usize],
532    recovery_exponents: &[u32],
533    constants: &[u16],
534) -> std::result::Result<(Matrix, Matrix), DecodeMatrixError> {
535    // `None`: the elimination picks rank-1 vs tiled from env + threshold.
536    build_repair_matrix_core(
537        available_indices,
538        missing_indices,
539        recovery_exponents,
540        constants,
541        None,
542    )
543}
544
545/// Build the repair matrix forcing a specific elimination strategy
546/// (`use_tiled`), bypassing the env/threshold gate. For A/B tests and benches
547/// that must exercise both paths deterministically in-process.
548pub(crate) fn build_repair_matrix_with_bad_row_using(
549    available_indices: &[usize],
550    missing_indices: &[usize],
551    recovery_exponents: &[u32],
552    constants: &[u16],
553    use_tiled: bool,
554) -> std::result::Result<(Matrix, Matrix), DecodeMatrixError> {
555    build_repair_matrix_core(
556        available_indices,
557        missing_indices,
558        recovery_exponents,
559        constants,
560        Some(use_tiled),
561    )
562}
563
564/// A/B hook: build the repair matrix with an explicit elimination strategy.
565/// `#[doc(hidden)]` public purely so the `par2_repair` bench can honestly
566/// compare the rank-1 and tiled paths; not part of the stable API.
567#[doc(hidden)]
568pub fn build_repair_matrix_ab(
569    available_indices: &[usize],
570    missing_indices: &[usize],
571    recovery_exponents: &[u32],
572    constants: &[u16],
573    use_tiled: bool,
574) -> Result<(Matrix, Matrix)> {
575    build_repair_matrix_with_bad_row_using(
576        available_indices,
577        missing_indices,
578        recovery_exponents,
579        constants,
580        use_tiled,
581    )
582    .map_err(DecodeMatrixError::into_par2_error)
583}
584
585fn build_repair_matrix_core(
586    available_indices: &[usize],
587    missing_indices: &[usize],
588    recovery_exponents: &[u32],
589    constants: &[u16],
590    force_tiled: Option<bool>,
591) -> std::result::Result<(Matrix, Matrix), DecodeMatrixError> {
592    let n = missing_indices.len();
593    if recovery_exponents.len() != n {
594        return Err(DecodeMatrixError::new(format!(
595            "recovery exponent count ({}) does not match missing slice count ({n})",
596            recovery_exponents.len()
597        )));
598    }
599    if n == 0 {
600        return Ok((
601            Matrix::zeros(0, available_indices.len()),
602            Matrix::zeros(0, 0),
603        ));
604    }
605
606    let mut submatrix = Matrix::zeros(n, n);
607    let mut repair_matrix = Matrix::zeros(n, available_indices.len() + n);
608    fill_vandermonde_rows(
609        &mut submatrix,
610        &mut repair_matrix,
611        available_indices,
612        missing_indices,
613        recovery_exponents,
614        constants,
615    );
616    match force_tiled {
617        Some(use_tiled) => {
618            submatrix.gaussian_eliminate_vandermonde_inner(&mut repair_matrix, use_tiled)?
619        }
620        None => submatrix.gaussian_eliminate_vandermonde(&mut repair_matrix)?,
621    }
622    let decode = repair_matrix.extract_columns(available_indices.len(), n);
623    Ok((repair_matrix, decode))
624}
625
626#[cfg(test)]
627mod tests {
628    use super::*;
629
630    #[test]
631    fn identity_inversion() {
632        let id = Matrix::identity(4);
633        let inv = id.invert().unwrap();
634        assert_eq!(inv, Matrix::identity(4));
635    }
636
637    #[test]
638    fn vandermonde_row_basic() {
639        let constants = vec![2u16, 4, 16];
640        let row = Matrix::vandermonde_row(&constants, 0);
641        // c^0 = 1 for all nonzero c
642        assert_eq!(row, vec![1, 1, 1]);
643
644        let row1 = Matrix::vandermonde_row(&constants, 1);
645        // c^1 = c
646        assert_eq!(row1, vec![2, 4, 16]);
647    }
648
649    #[test]
650    fn small_matrix_inversion() {
651        // Build a 2x2 Vandermonde-like matrix and verify M * M^-1 = I
652        let constants = crate::gf::input_slice_constants(2);
653        let mut m = Matrix::zeros(2, 2);
654        for (i, exp) in [0u32, 1].iter().enumerate() {
655            let row = Matrix::vandermonde_row(&constants, *exp);
656            m.copy_row_from_slice(i, &row);
657        }
658
659        let inv = m.invert().unwrap();
660
661        // Verify M * inv = I
662        let n = 2;
663        for i in 0..n {
664            for j in 0..n {
665                let mut sum = 0u16;
666                for k in 0..n {
667                    sum = gf::add(sum, gf::mul(m.get(i, k), inv.get(k, j)));
668                }
669                let expected = if i == j { 1 } else { 0 };
670                assert_eq!(sum, expected, "M*M^-1 [{i}][{j}] should be {expected}");
671            }
672        }
673    }
674
675    #[test]
676    fn larger_matrix_inversion() {
677        // 5x5 Vandermonde matrix
678        let constants = crate::gf::input_slice_constants(5);
679        let exponents = [0u32, 1, 2, 4, 7]; // valid PAR2 exponents
680        let mut m = Matrix::zeros(5, 5);
681        for (i, &exp) in exponents.iter().enumerate() {
682            let row = Matrix::vandermonde_row(&constants, exp);
683            m.copy_row_from_slice(i, &row);
684        }
685
686        let orig = m.clone();
687        let inv = m.invert().unwrap();
688
689        // Verify orig * inv = I
690        let n = 5;
691        for i in 0..n {
692            for j in 0..n {
693                let mut sum = 0u16;
694                for k in 0..n {
695                    sum = gf::add(sum, gf::mul(orig.get(i, k), inv.get(k, j)));
696                }
697                let expected = if i == j { 1 } else { 0 };
698                assert_eq!(
699                    sum, expected,
700                    "M*M^-1 [{i}][{j}] should be {expected}, got {sum}"
701                );
702            }
703        }
704    }
705
706    #[test]
707    fn singular_matrix_fails() {
708        // Two identical rows
709        let mut m = Matrix::zeros(2, 2);
710        m.copy_row_from_slice(0, &[1, 2]);
711        m.copy_row_from_slice(1, &[1, 2]);
712        let err = m.invert().unwrap_err();
713        assert!(matches!(err, Par2Error::ReedSolomonError { .. }));
714    }
715
716    #[test]
717    fn singular_decode_matrix_reports_bad_recovery_row() {
718        let constants = crate::gf::input_slice_constants(2);
719        let missing = vec![0usize, 1];
720        let err = build_decode_matrix_with_bad_row(&missing, &[0, 0], &constants).unwrap_err();
721        assert_eq!(err.bad_row, Some(1));
722    }
723
724    #[test]
725    fn build_decode_matrix_basic() {
726        let constants = crate::gf::input_slice_constants(4);
727        let missing = vec![1usize, 3];
728        let exponents = vec![0u32, 1];
729
730        let decode = build_decode_matrix(&missing, &exponents, &constants).unwrap();
731        assert_eq!(decode.rows, 2);
732        assert_eq!(decode.cols, 2);
733
734        // Verify: submatrix * decode = I
735        let mut sub = Matrix::zeros(2, 2);
736        for (i, &exp) in exponents.iter().enumerate() {
737            for (j, &idx) in missing.iter().enumerate() {
738                sub.set(i, j, gf::pow(constants[idx], exp));
739            }
740        }
741
742        for i in 0..2 {
743            for j in 0..2 {
744                let mut sum = 0u16;
745                for k in 0..2 {
746                    sum = gf::add(sum, gf::mul(sub.get(i, k), decode.get(k, j)));
747                }
748                let expected = if i == j { 1 } else { 0 };
749                assert_eq!(sum, expected);
750            }
751        }
752    }
753
754    #[test]
755    fn build_decode_matrix_mismatched_counts() {
756        let constants = crate::gf::input_slice_constants(4);
757        let err = build_decode_matrix(&[0, 1], &[0u32], &constants).unwrap_err();
758        assert!(matches!(err, Par2Error::ReedSolomonError { .. }));
759    }
760
761    #[test]
762    fn build_decode_matrix_empty() {
763        let constants = crate::gf::input_slice_constants(4);
764        let decode = build_decode_matrix(&[], &[], &constants).unwrap();
765        assert_eq!(decode.rows, 0);
766        assert_eq!(decode.cols, 0);
767    }
768
769    /// The rank-k tiled path must be byte-identical to the rank-1 path for the
770    /// full repair matrix and the extracted decode matrix, across
771    /// above-threshold sizes and one below-threshold control.
772    #[test]
773    fn tiled_equals_serial() {
774        for n in [100usize, 300, 512, 1000] {
775            let total = 2 * n;
776            let constants = crate::gf::input_slice_constants(total);
777            let missing: Vec<usize> = (0..n).collect();
778            let available: Vec<usize> = (n..total).collect();
779            let exponents: Vec<u32> = (0..n as u32).collect();
780
781            let (rank1_repair, rank1_decode) = build_repair_matrix_with_bad_row_using(
782                &available, &missing, &exponents, &constants, false,
783            )
784            .expect("rank-1 solve");
785            let (tiled_repair, tiled_decode) = build_repair_matrix_with_bad_row_using(
786                &available, &missing, &exponents, &constants, true,
787            )
788            .expect("tiled solve");
789
790            assert_eq!(
791                rank1_repair.data, tiled_repair.data,
792                "n={n}: repair matrix must be byte-identical"
793            );
794            assert_eq!(
795                rank1_decode.data, tiled_decode.data,
796                "n={n}: decode matrix must be byte-identical"
797            );
798        }
799    }
800
801    /// Pow-only reference for [`fill_vandermonde_rows`]: the original
802    /// per-element `gf::pow` fill, no pmul fast path.
803    fn fill_vandermonde_rows_pow_only(
804        submatrix: &mut Matrix,
805        repair_matrix: &mut Matrix,
806        available_indices: &[usize],
807        missing_indices: &[usize],
808        recovery_exponents: &[u32],
809        constants: &[u16],
810    ) {
811        for (i, &exp) in recovery_exponents.iter().enumerate() {
812            let row = submatrix.row_mut(i);
813            for (slot, &idx) in row.iter_mut().zip(missing_indices.iter()) {
814                *slot = gf::pow(constants[idx], exp);
815            }
816            let row = repair_matrix.row_mut(i);
817            for (slot, &idx) in row
818                .iter_mut()
819                .take(available_indices.len())
820                .zip(available_indices.iter())
821            {
822                *slot = gf::pow(constants[idx], exp);
823            }
824            row[available_indices.len() + i] = 1;
825        }
826    }
827
828    /// The pmul fast-fill must be byte-identical to the pow fill for every
829    /// exponent pattern: full runs, runs with gaps, non-monotonic, duplicate
830    /// exponents, and exponent 0 starts.
831    #[test]
832    fn fast_fill_matches_pow_fill() {
833        let exponent_patterns: Vec<Vec<u32>> = vec![
834            (0..24).collect(),                               // full run from 0
835            (1..25).collect(),                               // full run from 1
836            vec![0, 1, 2, 5, 6, 7, 100, 101, 102, 4, 9, 10], // gappy runs
837            vec![7, 3, 9, 2, 60000, 60001],                  // mostly non-sequential
838            vec![65534, 65535, 65536, 65537],                // multiplicative-group wrap
839            vec![4, 4, 5, 6],                                // duplicate (singular later)
840            vec![0],                                         // single row
841        ];
842        for exponents in &exponent_patterns {
843            let n = exponents.len();
844            let total = n + 48;
845            let constants = crate::gf::input_slice_constants(total);
846            let missing: Vec<usize> = (0..n).collect();
847            let available: Vec<usize> = (n..total).collect();
848
849            let mut sub_fast = Matrix::zeros(n, n);
850            let mut rep_fast = Matrix::zeros(n, available.len() + n);
851            fill_vandermonde_rows(
852                &mut sub_fast,
853                &mut rep_fast,
854                &available,
855                &missing,
856                exponents,
857                &constants,
858            );
859
860            let mut sub_ref = Matrix::zeros(n, n);
861            let mut rep_ref = Matrix::zeros(n, available.len() + n);
862            fill_vandermonde_rows_pow_only(
863                &mut sub_ref,
864                &mut rep_ref,
865                &available,
866                &missing,
867                exponents,
868                &constants,
869            );
870
871            assert_eq!(sub_fast.data, sub_ref.data, "submatrix {exponents:?}");
872            assert_eq!(rep_fast.data, rep_ref.data, "repair {exponents:?}");
873        }
874    }
875
876    /// A singular selection (duplicate exponent -> identical rows) at
877    /// n >= threshold must report the same `bad_row` from both paths.
878    #[test]
879    fn tiled_singular_reports_same_bad_row() {
880        let n = 300usize;
881        let bad = 150usize;
882        let total = 2 * n;
883        let constants = crate::gf::input_slice_constants(total);
884        let missing: Vec<usize> = (0..n).collect();
885        let available: Vec<usize> = (n..total).collect();
886        let mut exponents: Vec<u32> = (0..n as u32).collect();
887        exponents[bad] = exponents[bad - 1]; // duplicate -> rows bad-1 and bad identical
888
889        let rank1_err = build_repair_matrix_with_bad_row_using(
890            &available, &missing, &exponents, &constants, false,
891        )
892        .unwrap_err();
893        let tiled_err = build_repair_matrix_with_bad_row_using(
894            &available, &missing, &exponents, &constants, true,
895        )
896        .unwrap_err();
897
898        assert_eq!(rank1_err.bad_row, Some(bad));
899        assert_eq!(rank1_err.bad_row, tiled_err.bad_row);
900    }
901}