Skip to main content

ocas_poly/
matrix.rs

1//! Matrix types and linear algebra over algebraic domains.
2//!
3//! Provides a generic [`Matrix`] type parameterized by any [`EuclideanDomain`].
4//! Supports Gaussian elimination, back-substitution, and solving linear
5//! systems Ax = b with fraction-free arithmetic to avoid coefficient blow-up.
6
7use std::fmt;
8use std::ops::{Index, IndexMut};
9
10use ocas_domain::EuclideanDomain;
11
12/// Errors that can occur during matrix operations.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum MatrixError {
15    /// Matrix shapes are incompatible for the requested operation.
16    ShapeMismatch,
17    /// The right-hand side is not a column vector.
18    RightHandSideIsNotVector,
19    /// The linear system is inconsistent (no solution).
20    Inconsistent,
21    /// The system is underdetermined (infinitely many solutions).
22    Underdetermined {
23        /// Rank of the coefficient matrix.
24        rank: usize,
25    },
26    /// The solution does not lie in the expected domain.
27    ResultNotInDomain,
28}
29
30impl fmt::Display for MatrixError {
31    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
32        match self {
33            MatrixError::ShapeMismatch => f.write_str("matrix shape mismatch"),
34            MatrixError::RightHandSideIsNotVector => {
35                f.write_str("right-hand side must be a column vector")
36            }
37            MatrixError::Inconsistent => f.write_str("inconsistent linear system"),
38            MatrixError::Underdetermined { rank } => {
39                write!(f, "underdetermined system (rank {})", rank)
40            }
41            MatrixError::ResultNotInDomain => {
42                f.write_str("solution does not lie in the expected domain")
43            }
44        }
45    }
46}
47
48impl std::error::Error for MatrixError {}
49
50/// A dense matrix with elements from a [`EuclideanDomain`].
51///
52/// Elements are stored in row-major order.
53///
54/// # Example
55///
56/// ```
57/// use ocas_domain::{EuclideanDomain, IntegerDomain, Integer};
58/// use ocas_poly::matrix::Matrix;
59///
60/// let d = IntegerDomain;
61/// let a = Matrix::from_rows(vec![
62///     vec![Integer::from(1), Integer::from(1)],
63///     vec![Integer::from(1), Integer::from(-1)],
64/// ], d);
65/// let b = vec![Integer::from(3), Integer::from(-1)];
66/// let x = a.solve(&b).unwrap();
67/// assert_eq!(x, vec![Integer::from(1), Integer::from(2)]);
68/// ```
69#[derive(Debug, Clone, PartialEq, Eq)]
70pub struct Matrix<D: EuclideanDomain> {
71    data: Vec<D::Element>,
72    nrows: usize,
73    ncols: usize,
74    domain: D,
75}
76
77impl<D: EuclideanDomain> Matrix<D> {
78    /// Create a matrix from row-major data.
79    ///
80    /// Panics if `data.len() != nrows * ncols`.
81    pub fn new(nrows: usize, ncols: usize, data: Vec<D::Element>, domain: D) -> Self {
82        assert_eq!(
83            data.len(),
84            nrows * ncols,
85            "data length {} != {} * {}",
86            data.len(),
87            nrows,
88            ncols
89        );
90        Self {
91            data,
92            nrows,
93            ncols,
94            domain,
95        }
96    }
97
98    /// Create a zero matrix of the given shape.
99    pub fn zeros(nrows: usize, ncols: usize, domain: D) -> Self {
100        let data = vec![domain.zero(); nrows * ncols];
101        Self {
102            data,
103            nrows,
104            ncols,
105            domain,
106        }
107    }
108
109    /// Create an identity matrix of size `n`.
110    pub fn identity(n: usize, domain: D) -> Self {
111        let mut m = Self::zeros(n, n, domain);
112        for i in 0..n {
113            m[(i, i)] = m.domain.one();
114        }
115        m
116    }
117
118    /// Create a matrix from nested row vectors.
119    ///
120    /// Returns `None` if rows have inconsistent lengths.
121    pub fn from_rows(rows: Vec<Vec<D::Element>>, domain: D) -> Self {
122        let nrows = rows.len();
123        let ncols = rows.first().map_or(0, |r| r.len());
124        let mut data = Vec::with_capacity(nrows * ncols);
125        for row in &rows {
126            assert_eq!(
127                row.len(),
128                ncols,
129                "inconsistent row lengths: expected {}, got {}",
130                ncols,
131                row.len()
132            );
133            data.extend(row.iter().cloned());
134        }
135        Self {
136            data,
137            nrows,
138            ncols,
139            domain,
140        }
141    }
142
143    /// Return the number of rows.
144    pub fn nrows(&self) -> usize {
145        self.nrows
146    }
147
148    /// Return the number of columns.
149    pub fn ncols(&self) -> usize {
150        self.ncols
151    }
152
153    /// Return a reference to the domain.
154    pub fn domain(&self) -> &D {
155        &self.domain
156    }
157
158    /// Swap two rows, starting from column `start_col`.
159    pub fn swap_rows(&mut self, i: usize, j: usize, start_col: usize) {
160        if i == j {
161            return;
162        }
163        for col in start_col..self.ncols {
164            self.data.swap(i * self.ncols + col, j * self.ncols + col);
165        }
166    }
167
168    /// Horizontally concatenate `self` with `other`.
169    ///
170    /// Both must have the same number of rows.
171    pub fn augment(&self, other: &Matrix<D>) -> Result<Matrix<D>, MatrixError> {
172        if self.nrows != other.nrows {
173            return Err(MatrixError::ShapeMismatch);
174        }
175        let new_ncols = self.ncols + other.ncols;
176        let mut data = Vec::with_capacity(self.nrows * new_ncols);
177        for row in 0..self.nrows {
178            let start = row * self.ncols;
179            data.extend_from_slice(&self.data[start..start + self.ncols]);
180            let ostart = row * other.ncols;
181            data.extend_from_slice(&other.data[ostart..ostart + other.ncols]);
182        }
183        Ok(Matrix {
184            data,
185            nrows: self.nrows,
186            ncols: new_ncols,
187            domain: self.domain.clone(),
188        })
189    }
190
191    /// Perform fraction-free Gaussian elimination on the first `max_col`
192    /// columns, putting the matrix in row echelon form. Returns the rank.
193    ///
194    /// This mirrors Symbolica's `partial_row_reduce_fraction_free`.
195    pub fn row_echelon(&mut self, max_col: usize) -> usize {
196        let max_col = max_col.min(self.ncols);
197        let mut i = 0;
198
199        for j in 0..max_col {
200            if i >= self.nrows {
201                break;
202            }
203
204            if self.domain.is_zero(&self[(i, j)]) {
205                // Select a non-zero pivot.
206                let mut found = false;
207                for k in i + 1..self.nrows {
208                    if !self.domain.is_zero(&self[(k, j)]) {
209                        self.swap_rows(i, k, j);
210                        found = true;
211                        break;
212                    }
213                }
214                if !found {
215                    continue; // zero column
216                }
217            }
218
219            // Strip content from pivot row to prevent coefficient growth.
220            let mut g = self[(i, j)].clone();
221            for l in j + 1..self.ncols {
222                if self.domain.is_one(&g) {
223                    break;
224                }
225                g = self.domain.gcd(&g, &self[(i, l)]);
226            }
227            if !self.domain.is_one(&g) {
228                for l in j..self.ncols {
229                    self[(i, l)] = self.domain.div(&self[(i, l)], &g).unwrap();
230                }
231            }
232
233            // Eliminate below.
234            let pivot = self[(i, j)].clone();
235            for k in i + 1..self.nrows {
236                if !self.domain.is_zero(&self[(k, j)]) {
237                    let g = self.domain.gcd(&pivot, &self[(k, j)]);
238                    let scale_pivot = self.domain.div(&self[(k, j)], &g).unwrap();
239                    let scale_row = self.domain.div(&pivot, &g).unwrap();
240
241                    self[(k, j)] = self.domain.zero();
242                    for l in j + 1..self.ncols {
243                        let term1 = self.domain.mul(&self[(k, l)], &scale_row);
244                        let term2 = self.domain.mul(&self[(i, l)], &scale_pivot);
245                        self[(k, l)] = self.domain.sub(&term1, &term2);
246                    }
247                }
248            }
249
250            i += 1;
251        }
252
253        i
254    }
255
256    /// Perform fraction-free back substitution on a matrix already in
257    /// row echelon form (mutating the first `max_col` columns).
258    pub fn back_substitution(&mut self, max_col: usize) {
259        let max_col = max_col.min(self.ncols);
260        for i in (0..self.nrows).rev() {
261            if let Some(j) = (0..max_col).find(|&j| !self.domain.is_zero(&self[(i, j)])) {
262                // Strip content from pivot row.
263                let mut g = self[(i, j)].clone();
264                for l in j + 1..self.ncols {
265                    if self.domain.is_one(&g) {
266                        break;
267                    }
268                    g = self.domain.gcd(&g, &self[(i, l)]);
269                }
270                if !self.domain.is_one(&g) {
271                    for l in j..self.ncols {
272                        self[(i, l)] = self.domain.div(&self[(i, l)], &g).unwrap();
273                    }
274                }
275
276                // Eliminate above.
277                for k in 0..i {
278                    if !self.domain.is_zero(&self[(k, j)]) {
279                        let g = self.domain.gcd(&self[(i, j)], &self[(k, j)]);
280                        let scale_pivot = self.domain.div(&self[(k, j)], &g).unwrap();
281                        let scale_row = self.domain.div(&self[(i, j)], &g).unwrap();
282
283                        if !self.domain.is_one(&scale_row) {
284                            for l in 0..self.ncols {
285                                if !self.domain.is_zero(&self[(k, l)]) {
286                                    self[(k, l)] = self.domain.mul(&self[(k, l)], &scale_row);
287                                }
288                            }
289                        }
290
291                        self[(k, j)] = self.domain.zero();
292                        for l in j + 1..self.ncols {
293                            let term1 = self[(k, l)].clone();
294                            let term2 = self.domain.mul(&self[(i, l)], &scale_pivot);
295                            self[(k, l)] = self.domain.sub(&term1, &term2);
296                        }
297                    }
298                }
299            }
300        }
301    }
302
303    /// Solve the linear system `Ax = b` where `A` is `self` and `b` is a
304    /// column vector represented as a slice.
305    ///
306    /// Returns the solution vector `x` on success.
307    pub fn solve(&self, b: &[D::Element]) -> Result<Vec<D::Element>, MatrixError> {
308        if self.nrows != b.len() {
309            return Err(MatrixError::ShapeMismatch);
310        }
311
312        let b_matrix = Matrix::new(b.len(), 1, b.to_vec(), self.domain.clone());
313
314        let mut augmented = self.augment(&b_matrix)?;
315        let nvars = self.ncols;
316
317        let rank = augmented.row_echelon(nvars);
318
319        // Check consistency.
320        for k in rank..self.nrows {
321            if !self.domain.is_zero(&augmented[(k, nvars)]) {
322                return Err(MatrixError::Inconsistent);
323            }
324        }
325
326        augmented.back_substitution(nvars);
327
328        if rank < nvars {
329            return Err(MatrixError::Underdetermined { rank });
330        }
331
332        // Divide by pivot to get the final solution.
333        let mut solution = Vec::with_capacity(nvars);
334        for i in 0..nvars {
335            match self.domain.div(&augmented[(i, nvars)], &augmented[(i, i)]) {
336                Some(val) => solution.push(val),
337                None => return Err(MatrixError::ResultNotInDomain),
338            }
339        }
340
341        Ok(solution)
342    }
343
344    /// Convert the matrix into a nested vector of rows.
345    pub fn into_rows(self) -> Vec<Vec<D::Element>> {
346        let mut rows = Vec::with_capacity(self.nrows);
347        for i in 0..self.nrows {
348            let start = i * self.ncols;
349            rows.push(self.data[start..start + self.ncols].to_vec());
350        }
351        rows
352    }
353
354    /// Return a reference to the underlying row-major data.
355    pub fn data(&self) -> &[D::Element] {
356        &self.data
357    }
358
359    /// Return a copy of a single row as a vector.
360    pub fn row(&self, i: usize) -> Vec<D::Element> {
361        let start = i * self.ncols;
362        self.data[start..start + self.ncols].to_vec()
363    }
364
365    /// Return a copy of a single column as a vector.
366    pub fn column(&self, j: usize) -> Vec<D::Element> {
367        (0..self.nrows).map(|i| self[(i, j)].clone()).collect()
368    }
369
370    /// Return the transpose of the matrix.
371    ///
372    /// # Example
373    ///
374    /// ```
375    /// use ocas_domain::{Integer, IntegerDomain};
376    /// use ocas_poly::matrix::Matrix;
377    ///
378    /// let d = IntegerDomain;
379    /// let a = Matrix::from_rows(vec![vec![Integer::from(1), Integer::from(2)]], d);
380    /// let t = a.transpose();
381    /// assert_eq!(t.nrows(), 2);
382    /// assert_eq!(t.ncols(), 1);
383    /// assert_eq!(t[(0, 0)], Integer::from(1));
384    /// assert_eq!(t[(1, 0)], Integer::from(2));
385    /// ```
386    pub fn transpose(&self) -> Matrix<D> {
387        let mut data = Vec::with_capacity(self.nrows * self.ncols);
388        for j in 0..self.ncols {
389            for i in 0..self.nrows {
390                data.push(self[(i, j)].clone());
391            }
392        }
393        Matrix {
394            data,
395            nrows: self.ncols,
396            ncols: self.nrows,
397            domain: self.domain.clone(),
398        }
399    }
400
401    /// Return the trace (sum of the diagonal) of a square matrix.
402    ///
403    /// Returns an error if the matrix is not square.
404    ///
405    /// # Example
406    ///
407    /// ```
408    /// use ocas_domain::{Integer, IntegerDomain};
409    /// use ocas_poly::matrix::Matrix;
410    ///
411    /// let d = IntegerDomain;
412    /// let a = Matrix::from_rows(
413    ///     vec![vec![Integer::from(1), Integer::from(2)], vec![Integer::from(3), Integer::from(4)]],
414    ///     d,
415    /// );
416    /// assert_eq!(a.trace().unwrap(), Integer::from(5));
417    /// ```
418    pub fn trace(&self) -> Result<D::Element, MatrixError> {
419        if self.nrows != self.ncols {
420            return Err(MatrixError::ShapeMismatch);
421        }
422        let mut sum = self.domain.zero();
423        for i in 0..self.nrows {
424            sum = self.domain.add(&sum, &self[(i, i)]);
425        }
426        Ok(sum)
427    }
428
429    /// Compute the matrix product `self * other`.
430    ///
431    /// Returns an error if `self.ncols != other.nrows`.
432    ///
433    /// # Example
434    ///
435    /// ```
436    /// use ocas_domain::{Integer, IntegerDomain};
437    /// use ocas_poly::matrix::Matrix;
438    ///
439    /// let d = IntegerDomain;
440    /// let a = Matrix::from_rows(vec![vec![Integer::from(1), Integer::from(2)]], d);
441    /// let b = Matrix::from_rows(vec![vec![Integer::from(3)], vec![Integer::from(4)]], d);
442    /// let c = a.matmul(&b).unwrap();
443    /// assert_eq!(c[(0, 0)], Integer::from(11));
444    /// ```
445    pub fn matmul(&self, other: &Matrix<D>) -> Result<Matrix<D>, MatrixError> {
446        if self.ncols != other.nrows {
447            return Err(MatrixError::ShapeMismatch);
448        }
449        let mut data = Vec::with_capacity(self.nrows * other.ncols);
450        for i in 0..self.nrows {
451            for j in 0..other.ncols {
452                let mut acc = self.domain.zero();
453                for k in 0..self.ncols {
454                    let term = self.domain.mul(&self[(i, k)], &other[(k, j)]);
455                    acc = self.domain.add(&acc, &term);
456                }
457                data.push(acc);
458            }
459        }
460        Ok(Matrix {
461            data,
462            nrows: self.nrows,
463            ncols: other.ncols,
464            domain: self.domain.clone(),
465        })
466    }
467
468    /// Compute the rank of the matrix via fraction-free Gaussian elimination.
469    ///
470    /// # Example
471    ///
472    /// ```
473    /// use ocas_domain::{Integer, IntegerDomain};
474    /// use ocas_poly::matrix::Matrix;
475    ///
476    /// let d = IntegerDomain;
477    /// let a = Matrix::from_rows(
478    ///     vec![vec![Integer::from(1), Integer::from(2)], vec![Integer::from(2), Integer::from(4)]],
479    ///     d,
480    /// );
481    /// assert_eq!(a.rank(), 1);
482    /// ```
483    pub fn rank(&self) -> usize {
484        let mut copy = self.clone();
485        copy.row_echelon(self.ncols)
486    }
487
488    /// Compute the determinant of a square matrix using the Bareiss
489    /// fraction-free algorithm with partial pivoting.
490    ///
491    /// Returns an error if the matrix is not square.
492    ///
493    /// # Example
494    ///
495    /// ```
496    /// use ocas_domain::{Integer, IntegerDomain};
497    /// use ocas_poly::matrix::Matrix;
498    ///
499    /// let d = IntegerDomain;
500    /// let a = Matrix::from_rows(
501    ///     vec![vec![Integer::from(1), Integer::from(2)], vec![Integer::from(3), Integer::from(4)]],
502    ///     d,
503    /// );
504    /// assert_eq!(a.determinant().unwrap(), Integer::from(-2));
505    /// ```
506    pub fn determinant(&self) -> Result<D::Element, MatrixError> {
507        if self.nrows != self.ncols {
508            return Err(MatrixError::ShapeMismatch);
509        }
510        let n = self.nrows;
511        if n == 0 {
512            return Ok(self.domain.one());
513        }
514        if n == 1 {
515            return Ok(self.data[0].clone());
516        }
517        // Bareiss fraction-free elimination with partial pivoting.
518        let mut m = self.data.clone();
519        let mut sign_pos = true;
520        let mut prev = self.domain.one();
521        for k in 0..n - 1 {
522            let pivot = m[k * n + k].clone();
523            if self.domain.is_zero(&pivot) {
524                // Find a row below with a nonzero entry in column k.
525                let mut swap_row = None;
526                for i in k + 1..n {
527                    if !self.domain.is_zero(&m[i * n + k]) {
528                        swap_row = Some(i);
529                        break;
530                    }
531                }
532                match swap_row {
533                    Some(i) => {
534                        for j in 0..n {
535                            m.swap(k * n + j, i * n + j);
536                        }
537                        sign_pos = !sign_pos;
538                    }
539                    None => return Ok(self.domain.zero()),
540                }
541            }
542            let pivot = m[k * n + k].clone();
543            for i in k + 1..n {
544                for j in k + 1..n {
545                    let term1 = self.domain.mul(&m[i * n + j], &pivot);
546                    let term2 = self.domain.mul(&m[i * n + k], &m[k * n + j]);
547                    let diff = self.domain.sub(&term1, &term2);
548                    // Bareiss guarantees exact divisibility by prev.
549                    m[i * n + j] = self.domain.div(&diff, &prev).unwrap_or(diff);
550                }
551            }
552            prev = pivot;
553        }
554        let det = m[(n - 1) * n + (n - 1)].clone();
555        if sign_pos {
556            Ok(det)
557        } else {
558            Ok(self.domain.neg(&det))
559        }
560    }
561
562    /// Compute the inverse of a square non-singular matrix.
563    ///
564    /// Returns an error if the matrix is not square or is singular over the
565    /// coefficient domain.
566    ///
567    /// # Example
568    ///
569    /// ```
570    /// use ocas_domain::{Integer, IntegerDomain};
571    /// use ocas_poly::matrix::Matrix;
572    ///
573    /// let d = IntegerDomain;
574    /// // Unimodular matrix: determinant 1, integer inverse exists.
575    /// let a = Matrix::from_rows(
576    ///     vec![vec![Integer::from(1), Integer::from(2)], vec![Integer::from(0), Integer::from(1)]],
577    ///     d,
578    /// );
579    /// let inv = a.inverse().unwrap();
580    /// assert_eq!(inv[(0, 0)], Integer::from(1));
581    /// assert_eq!(inv[(0, 1)], Integer::from(-2));
582    /// assert_eq!(inv[(1, 0)], Integer::from(0));
583    /// assert_eq!(inv[(1, 1)], Integer::from(1));
584    /// ```
585    pub fn inverse(&self) -> Result<Matrix<D>, MatrixError> {
586        if self.nrows != self.ncols {
587            return Err(MatrixError::ShapeMismatch);
588        }
589        let n = self.nrows;
590        let identity = Matrix::identity(n, self.domain.clone());
591        // Solve A * x_j = e_j for each column j of the identity, giving
592        // column j of A^{-1}. Stored in row-major order.
593        let mut inv_data = vec![self.domain.zero(); n * n];
594        for j in 0..n {
595            let b = identity.column(j);
596            let col = self.solve(&b)?;
597            if col.len() != n {
598                return Err(MatrixError::Underdetermined { rank: col.len() });
599            }
600            for i in 0..n {
601                inv_data[i * n + j] = col[i].clone();
602            }
603        }
604        Ok(Matrix {
605            data: inv_data,
606            nrows: n,
607            ncols: n,
608            domain: self.domain.clone(),
609        })
610    }
611}
612
613impl<D: EuclideanDomain> Index<(usize, usize)> for Matrix<D> {
614    type Output = D::Element;
615
616    fn index(&self, (row, col): (usize, usize)) -> &Self::Output {
617        &self.data[row * self.ncols + col]
618    }
619}
620
621impl<D: EuclideanDomain> IndexMut<(usize, usize)> for Matrix<D> {
622    fn index_mut(&mut self, (row, col): (usize, usize)) -> &mut Self::Output {
623        &mut self.data[row * self.ncols + col]
624    }
625}
626
627impl<D: EuclideanDomain + fmt::Display> fmt::Display for Matrix<D>
628where
629    D::Element: fmt::Display,
630{
631    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
632        for i in 0..self.nrows {
633            if i > 0 {
634                writeln!(f)?;
635            }
636            for j in 0..self.ncols {
637                if j > 0 {
638                    write!(f, " ")?;
639                }
640                write!(f, "{}", self[(i, j)])?;
641            }
642        }
643        Ok(())
644    }
645}
646
647#[cfg(test)]
648mod tests {
649    use super::*;
650    use ocas_domain::{Integer, IntegerDomain};
651
652    fn i(n: i64) -> Integer {
653        Integer::from(n)
654    }
655
656    #[test]
657    fn identity_solve() {
658        let d = IntegerDomain;
659        let a = Matrix::identity(3, d);
660        let b = vec![i(1), i(2), i(3)];
661        let x = a.solve(&b).unwrap();
662        assert_eq!(x, b);
663    }
664
665    #[test]
666    fn solve_2x2() {
667        let d = IntegerDomain;
668        // 2x + y = 5
669        // x + 3y = 6  => x=1, y=3... wait no: 2*1+3=5 ✓, 1+3*3=10 ≠ 6
670        // Let's check: 2x+y=5, x+3y=6
671        // From 1: y=5-2x. Into 2: x+3(5-2x)=6 → x+15-6x=6 → -5x=-9 → x=9/5 (not integer)
672        // Let me use a solvable system: 2x+y=4, x+y=3 → x=1, y=2
673        let a = Matrix::from_rows(vec![vec![i(2), i(1)], vec![i(1), i(1)]], d);
674        let b = vec![i(4), i(3)];
675        let x = a.solve(&b).unwrap();
676        assert_eq!(x, vec![i(1), i(2)]);
677    }
678
679    #[test]
680    fn solve_3x3() {
681        let d = IntegerDomain;
682        // x + y + z = 6
683        // 2x - y + z = 3
684        // x + 2y - z = 2  → x=1, y=2, z=3
685        let a = Matrix::from_rows(
686            vec![
687                vec![i(1), i(1), i(1)],
688                vec![i(2), i(-1), i(1)],
689                vec![i(1), i(2), i(-1)],
690            ],
691            d,
692        );
693        let b = vec![i(6), i(3), i(2)];
694        let x = a.solve(&b).unwrap();
695        assert_eq!(x, vec![i(1), i(2), i(3)]);
696    }
697
698    #[test]
699    fn inconsistent_system() {
700        let d = IntegerDomain;
701        let a = Matrix::from_rows(vec![vec![i(1), i(1)], vec![i(1), i(1)]], d);
702        let b = vec![i(1), i(2)];
703        assert_eq!(a.solve(&b), Err(MatrixError::Inconsistent));
704    }
705
706    #[test]
707    fn underdetermined_system() {
708        let d = IntegerDomain;
709        let a = Matrix::from_rows(vec![vec![i(1), i(1), i(1)]], d);
710        let b = vec![i(3)];
711        assert!(matches!(
712            a.solve(&b),
713            Err(MatrixError::Underdetermined { .. })
714        ));
715    }
716
717    #[test]
718    fn row_echelon_rank() {
719        let d = IntegerDomain;
720        let mut a = Matrix::from_rows(
721            vec![
722                vec![i(1), i(2), i(3)],
723                vec![i(2), i(4), i(6)],
724                vec![i(0), i(1), i(1)],
725            ],
726            d,
727        );
728        let rank = a.row_echelon(3);
729        assert_eq!(rank, 2);
730    }
731
732    #[test]
733    fn augment_and_solve() {
734        let d = IntegerDomain;
735        let a = Matrix::from_rows(vec![vec![i(3), i(2)], vec![i(1), i(1)]], d);
736        let b = Matrix::new(2, 1, vec![i(5), i(2)], d);
737        let aug = a.augment(&b).unwrap();
738        assert_eq!(aug.nrows, 2);
739        assert_eq!(aug.ncols, 3);
740        assert_eq!(aug[(0, 0)], i(3));
741        assert_eq!(aug[(0, 2)], i(5));
742        assert_eq!(aug[(1, 0)], i(1));
743        assert_eq!(aug[(1, 2)], i(2));
744    }
745
746    #[test]
747    fn transpose_rect() {
748        let d = IntegerDomain;
749        let a = Matrix::from_rows(vec![vec![i(1), i(2), i(3)], vec![i(4), i(5), i(6)]], d);
750        let t = a.transpose();
751        assert_eq!(t.nrows(), 3);
752        assert_eq!(t.ncols(), 2);
753        assert_eq!(t[(0, 0)], i(1));
754        assert_eq!(t[(0, 1)], i(4));
755        assert_eq!(t[(2, 1)], i(6));
756    }
757
758    #[test]
759    fn transpose_square() {
760        let d = IntegerDomain;
761        let a = Matrix::from_rows(vec![vec![i(1), i(2)], vec![i(3), i(4)]], d);
762        let t = a.transpose();
763        assert_eq!(t[(0, 1)], i(3));
764        assert_eq!(t[(1, 0)], i(2));
765    }
766
767    #[test]
768    fn trace_square() {
769        let d = IntegerDomain;
770        let a = Matrix::from_rows(
771            vec![
772                vec![i(1), i(2), i(3)],
773                vec![i(4), i(5), i(6)],
774                vec![i(7), i(8), i(9)],
775            ],
776            d,
777        );
778        assert_eq!(a.trace().unwrap(), i(15));
779    }
780
781    #[test]
782    fn trace_nonsquare_errors() {
783        let d = IntegerDomain;
784        let a = Matrix::from_rows(vec![vec![i(1), i(2)]], d);
785        assert_eq!(a.trace(), Err(MatrixError::ShapeMismatch));
786    }
787
788    #[test]
789    fn matmul_basic() {
790        let d = IntegerDomain;
791        let a = Matrix::from_rows(vec![vec![i(1), i(2)], vec![i(3), i(4)]], d);
792        let b = Matrix::from_rows(vec![vec![i(5), i(6)], vec![i(7), i(8)]], d);
793        let c = a.matmul(&b).unwrap();
794        assert_eq!(c[(0, 0)], i(19));
795        assert_eq!(c[(0, 1)], i(22));
796        assert_eq!(c[(1, 0)], i(43));
797        assert_eq!(c[(1, 1)], i(50));
798    }
799
800    #[test]
801    fn matmul_identity() {
802        let d = IntegerDomain;
803        let a = Matrix::from_rows(vec![vec![i(1), i(2)], vec![i(3), i(4)]], d);
804        let id = Matrix::identity(2, d);
805        let c = a.matmul(&id).unwrap();
806        assert_eq!(c, a);
807    }
808
809    #[test]
810    fn matmul_shape_mismatch() {
811        let d = IntegerDomain;
812        let a = Matrix::from_rows(vec![vec![i(1), i(2)]], d);
813        let b = Matrix::from_rows(vec![vec![i(3), i(4)]], d);
814        assert_eq!(a.matmul(&b), Err(MatrixError::ShapeMismatch));
815    }
816
817    #[test]
818    fn rank_full() {
819        let d = IntegerDomain;
820        let a = Matrix::from_rows(vec![vec![i(1), i(0)], vec![i(0), i(1)]], d);
821        assert_eq!(a.rank(), 2);
822    }
823
824    #[test]
825    fn rank_deficient() {
826        let d = IntegerDomain;
827        let a = Matrix::from_rows(
828            vec![
829                vec![i(1), i(2), i(3)],
830                vec![i(2), i(4), i(6)],
831                vec![i(1), i(1), i(1)],
832            ],
833            d,
834        );
835        assert_eq!(a.rank(), 2);
836    }
837
838    #[test]
839    fn determinant_2x2() {
840        let d = IntegerDomain;
841        let a = Matrix::from_rows(vec![vec![i(1), i(2)], vec![i(3), i(4)]], d);
842        assert_eq!(a.determinant().unwrap(), i(-2));
843    }
844
845    #[test]
846    fn determinant_3x3() {
847        let d = IntegerDomain;
848        // det = 1*(0*7 - 6*6) - 2*(2*7 - 6*5) + 3*(2*6 - 0*5)
849        //     = 1*(-36) - 2*(14-30) + 3*(12)
850        //     = -36 - 2*(-16) + 36 = -36 + 32 + 36 = 32
851        let a = Matrix::from_rows(
852            vec![
853                vec![i(1), i(2), i(3)],
854                vec![i(4), i(5), i(6)],
855                vec![i(5), i(6), i(7)],
856            ],
857            d,
858        );
859        // Recompute: 1*(5*7-6*6) - 2*(4*7-6*5) + 3*(4*6-5*5)
860        //          = 1*(35-36) - 2*(28-30) + 3*(24-25)
861        //          = -1 -2*(-2) + 3*(-1) = -1 +4 -3 = 0
862        assert_eq!(a.determinant().unwrap(), i(0));
863    }
864
865    #[test]
866    fn determinant_singular() {
867        let d = IntegerDomain;
868        // Singular: second row is 2x first.
869        let a = Matrix::from_rows(vec![vec![i(1), i(2)], vec![i(2), i(4)]], d);
870        assert_eq!(a.determinant().unwrap(), i(0));
871    }
872
873    #[test]
874    fn determinant_nonsquare_errors() {
875        let d = IntegerDomain;
876        let a = Matrix::from_rows(vec![vec![i(1), i(2)]], d);
877        assert_eq!(a.determinant(), Err(MatrixError::ShapeMismatch));
878    }
879
880    #[test]
881    fn inverse_unimodular() {
882        let d = IntegerDomain;
883        let a = Matrix::from_rows(vec![vec![i(1), i(2)], vec![i(3), i(5)]], d);
884        // det = 5 - 6 = -1, so integer inverse exists.
885        let inv = a.inverse().unwrap();
886        // A^{-1} = (1/det) * [[5,-2],[-3,1]] = (-1) * [[5,-2],[-3,1]] = [[-5,2],[3,-1]]
887        assert_eq!(inv[(0, 0)], i(-5));
888        assert_eq!(inv[(0, 1)], i(2));
889        assert_eq!(inv[(1, 0)], i(3));
890        assert_eq!(inv[(1, 1)], i(-1));
891        // Verify A * A^{-1} = I.
892        let prod = a.matmul(&inv).unwrap();
893        assert_eq!(prod, Matrix::identity(2, IntegerDomain));
894    }
895
896    #[test]
897    fn inverse_singular_errors() {
898        let d = IntegerDomain;
899        let a = Matrix::from_rows(vec![vec![i(1), i(2)], vec![i(2), i(4)]], d);
900        assert!(a.inverse().is_err());
901    }
902}