Skip to main content

oxiz_math/
matrix.rs

1//! Dense matrix operations for linear programming and linear algebra.
2//!
3//! This module provides a dense matrix implementation with operations needed
4//! for linear programming solvers, including Gaussian elimination, LU decomposition,
5//! and linear system solving.
6
7#[allow(unused_imports)]
8use crate::prelude::*;
9use core::fmt;
10use num_rational::Rational64;
11use num_traits::Signed;
12
13/// A dense matrix stored in row-major order.
14#[derive(Debug, Clone, PartialEq)]
15pub struct Matrix {
16    rows: usize,
17    cols: usize,
18    data: Vec<Rational64>,
19}
20
21impl Matrix {
22    /// Creates a new matrix with the given dimensions, initialized to zero.
23    pub fn zeros(rows: usize, cols: usize) -> Self {
24        Self {
25            rows,
26            cols,
27            data: vec![Rational64::from(0); rows * cols],
28        }
29    }
30
31    /// Creates a new identity matrix of the given size.
32    pub fn identity(size: usize) -> Self {
33        let mut matrix = Self::zeros(size, size);
34        for i in 0..size {
35            matrix.set(i, i, Rational64::from(1));
36        }
37        matrix
38    }
39
40    /// Creates a new matrix from a Vec of row vectors.
41    pub fn from_rows(rows: Vec<Vec<Rational64>>) -> Self {
42        if rows.is_empty() {
43            return Self::zeros(0, 0);
44        }
45        let nrows = rows.len();
46        let ncols = rows[0].len();
47        let mut data = Vec::with_capacity(nrows * ncols);
48        for row in rows {
49            assert_eq!(row.len(), ncols, "All rows must have the same length");
50            data.extend(row);
51        }
52        Self {
53            rows: nrows,
54            cols: ncols,
55            data,
56        }
57    }
58
59    /// Creates a new matrix from a flat array in row-major order.
60    pub fn from_vec(rows: usize, cols: usize, data: Vec<Rational64>) -> Self {
61        assert_eq!(data.len(), rows * cols, "Data size must match dimensions");
62        Self { rows, cols, data }
63    }
64
65    /// Returns the number of rows.
66    pub fn nrows(&self) -> usize {
67        self.rows
68    }
69
70    /// Returns the number of columns.
71    pub fn ncols(&self) -> usize {
72        self.cols
73    }
74
75    /// Gets the element at position (row, col).
76    pub fn get(&self, row: usize, col: usize) -> Rational64 {
77        assert!(row < self.rows && col < self.cols);
78        self.data[row * self.cols + col]
79    }
80
81    /// Sets the element at position (row, col).
82    pub fn set(&mut self, row: usize, col: usize, value: Rational64) {
83        assert!(row < self.rows && col < self.cols);
84        self.data[row * self.cols + col] = value;
85    }
86
87    /// Gets a reference to a row.
88    pub fn row(&self, row: usize) -> &[Rational64] {
89        assert!(row < self.rows);
90        let start = row * self.cols;
91        &self.data[start..start + self.cols]
92    }
93
94    /// Swaps two rows.
95    pub fn swap_rows(&mut self, i: usize, j: usize) {
96        assert!(i < self.rows && j < self.rows);
97        if i == j {
98            return;
99        }
100        for k in 0..self.cols {
101            let idx_i = i * self.cols + k;
102            let idx_j = j * self.cols + k;
103            self.data.swap(idx_i, idx_j);
104        }
105    }
106
107    /// Adds a scalar multiple of one row to another: row_i += scalar * row_j
108    pub fn add_row_multiple(&mut self, i: usize, j: usize, scalar: Rational64) {
109        assert!(i < self.rows && j < self.rows);
110        if scalar == Rational64::from(0) {
111            return;
112        }
113        for k in 0..self.cols {
114            let idx_i = i * self.cols + k;
115            let idx_j = j * self.cols + k;
116            let val_j = self.data[idx_j];
117            self.data[idx_i] += scalar * val_j;
118        }
119    }
120
121    /// Multiplies a row by a scalar.
122    pub fn scale_row(&mut self, row: usize, scalar: Rational64) {
123        assert!(row < self.rows);
124        for k in 0..self.cols {
125            let idx = row * self.cols + k;
126            self.data[idx] *= scalar;
127        }
128    }
129
130    /// Matrix-vector multiplication: returns A * v
131    #[allow(clippy::needless_range_loop)]
132    pub fn mul_vec(&self, v: &[Rational64]) -> Vec<Rational64> {
133        assert_eq!(
134            v.len(),
135            self.cols,
136            "Vector dimension must match matrix columns"
137        );
138        let mut result = vec![Rational64::from(0); self.rows];
139        for i in 0..self.rows {
140            for j in 0..self.cols {
141                result[i] += self.get(i, j) * v[j];
142            }
143        }
144        result
145    }
146
147    /// Matrix-matrix multiplication: returns A * B
148    pub fn mul(&self, other: &Matrix) -> Matrix {
149        assert_eq!(
150            self.cols, other.rows,
151            "Matrix dimensions must be compatible for multiplication"
152        );
153        let mut result = Matrix::zeros(self.rows, other.cols);
154        for i in 0..self.rows {
155            for j in 0..other.cols {
156                let mut sum = Rational64::from(0);
157                for k in 0..self.cols {
158                    sum += self.get(i, k) * other.get(k, j);
159                }
160                result.set(i, j, sum);
161            }
162        }
163        result
164    }
165
166    /// Transposes the matrix.
167    pub fn transpose(&self) -> Matrix {
168        let mut result = Matrix::zeros(self.cols, self.rows);
169        for i in 0..self.rows {
170            for j in 0..self.cols {
171                result.set(j, i, self.get(i, j));
172            }
173        }
174        result
175    }
176
177    /// Performs Gaussian elimination with partial pivoting.
178    /// Returns the row-echelon form and the rank of the matrix.
179    pub fn gaussian_elimination(&self) -> (Matrix, usize) {
180        let mut result = self.clone();
181        let mut rank = 0;
182
183        for col in 0..self.cols.min(self.rows) {
184            // Find pivot
185            let mut pivot_row = None;
186            for row in rank..self.rows {
187                if result.get(row, col) != Rational64::from(0) {
188                    pivot_row = Some(row);
189                    break;
190                }
191            }
192
193            let pivot_row = match pivot_row {
194                Some(r) => r,
195                None => continue, // Column is all zeros below current rank
196            };
197
198            // Swap rows if needed
199            if pivot_row != rank {
200                result.swap_rows(rank, pivot_row);
201            }
202
203            // Eliminate below
204            let pivot = result.get(rank, col);
205            for row in rank + 1..self.rows {
206                let factor = result.get(row, col) / pivot;
207                result.add_row_multiple(row, rank, -factor);
208            }
209
210            rank += 1;
211        }
212
213        (result, rank)
214    }
215
216    /// Performs LU decomposition with partial pivoting.
217    /// Returns (L, U, P) where P is a permutation vector.
218    pub fn lu_decomposition(&self) -> Option<(Matrix, Matrix, Vec<usize>)> {
219        if self.rows != self.cols {
220            return None; // LU decomposition requires square matrix
221        }
222        let n = self.rows;
223
224        let mut l = Matrix::zeros(n, n);
225        let mut u = self.clone();
226        let mut p: Vec<usize> = (0..n).collect();
227
228        for i in 0..n {
229            // Partial pivoting: find row with largest element in column i
230            let mut max_row = i;
231            let mut max_val = u.get(i, i).abs();
232            for k in i + 1..n {
233                let val = u.get(k, i).abs();
234                if val > max_val {
235                    max_val = val;
236                    max_row = k;
237                }
238            }
239
240            // Swap rows in U and permutation vector
241            if max_row != i {
242                u.swap_rows(i, max_row);
243                p.swap(i, max_row);
244                // Also swap already computed parts of L
245                for k in 0..i {
246                    let tmp = l.get(i, k);
247                    l.set(i, k, l.get(max_row, k));
248                    l.set(max_row, k, tmp);
249                }
250            }
251
252            let pivot = u.get(i, i);
253            if pivot == Rational64::from(0) {
254                return None; // Matrix is singular
255            }
256
257            l.set(i, i, Rational64::from(1));
258
259            for k in i + 1..n {
260                let factor = u.get(k, i) / pivot;
261                l.set(k, i, factor);
262                for j in i..n {
263                    let val = u.get(k, j) - factor * u.get(i, j);
264                    u.set(k, j, val);
265                }
266            }
267        }
268
269        Some((l, u, p))
270    }
271
272    /// Solves a linear system Ax = b using Gaussian elimination with back substitution.
273    /// Returns None if the system has no unique solution.
274    #[allow(clippy::needless_range_loop)]
275    pub fn solve(&self, b: &[Rational64]) -> Option<Vec<Rational64>> {
276        assert_eq!(
277            b.len(),
278            self.rows,
279            "Right-hand side dimension must match matrix rows"
280        );
281
282        if self.rows != self.cols {
283            return None; // Only solve square systems for now
284        }
285
286        // Augment matrix with b
287        let mut aug = Matrix::zeros(self.rows, self.cols + 1);
288        for i in 0..self.rows {
289            for j in 0..self.cols {
290                aug.set(i, j, self.get(i, j));
291            }
292            aug.set(i, self.cols, b[i]);
293        }
294
295        // Forward elimination
296        for col in 0..self.cols {
297            // Find pivot
298            let mut pivot_row = None;
299            for row in col..self.rows {
300                if aug.get(row, col) != Rational64::from(0) {
301                    pivot_row = Some(row);
302                    break;
303                }
304            }
305
306            let pivot_row = pivot_row?; // No unique solution if None
307
308            if pivot_row != col {
309                aug.swap_rows(col, pivot_row);
310            }
311
312            let pivot = aug.get(col, col);
313            for row in col + 1..self.rows {
314                let factor = aug.get(row, col) / pivot;
315                aug.add_row_multiple(row, col, -factor);
316            }
317        }
318
319        // Back substitution
320        let mut x = vec![Rational64::from(0); self.cols];
321        for i in (0..self.rows).rev() {
322            let mut sum = aug.get(i, self.cols);
323            for j in i + 1..self.cols {
324                sum -= aug.get(i, j) * x[j];
325            }
326            let pivot = aug.get(i, i);
327            if pivot == Rational64::from(0) {
328                return None; // Singular matrix
329            }
330            x[i] = sum / pivot;
331        }
332
333        Some(x)
334    }
335
336    /// Computes the determinant using LU decomposition.
337    pub fn determinant(&self) -> Option<Rational64> {
338        if self.rows != self.cols {
339            return None;
340        }
341
342        let (_, u, p) = self.lu_decomposition()?;
343
344        // Determinant of U is the product of diagonal elements
345        let mut det = Rational64::from(1);
346        for i in 0..self.rows {
347            det *= u.get(i, i);
348        }
349
350        // Count swaps in permutation to get sign
351        let mut swaps = 0;
352        let mut visited = vec![false; p.len()];
353        for i in 0..p.len() {
354            if visited[i] {
355                continue;
356            }
357            let mut j = i;
358            let mut cycle_len = 0;
359            while !visited[j] {
360                visited[j] = true;
361                j = p[j];
362                cycle_len += 1;
363            }
364            if cycle_len > 1 {
365                swaps += cycle_len - 1;
366            }
367        }
368
369        if swaps % 2 == 1 {
370            det = -det;
371        }
372
373        Some(det)
374    }
375
376    /// Performs QR decomposition using the modified Gram-Schmidt process.
377    /// Returns (Q, R) where Q has orthogonal columns and R is upper triangular such that QR = A.
378    ///
379    /// # Note
380    /// Since we're working with exact rational arithmetic and can't compute square roots,
381    /// Q will not be orthonormal in the traditional sense. Instead, Q contains unnormalized
382    /// orthogonal vectors, and R is adjusted accordingly so that QR = A holds exactly.
383    ///
384    /// Specifically: `R[j,j] = 1` and the j-th column of Q has squared norm stored implicitly.
385    pub fn qr_decomposition(&self) -> Option<(Matrix, Matrix)> {
386        let m = self.rows;
387        let n = self.cols;
388
389        if m < n {
390            return None; // Need m >= n for standard QR
391        }
392
393        let mut q = Matrix::zeros(m, n);
394        let mut r = Matrix::zeros(n, n);
395
396        // Classical Gram-Schmidt: a_j = sum_{k=0}^j r[k,j] * q_k
397        for j in 0..n {
398            // Start with column j of A
399            for i in 0..m {
400                q.set(i, j, self.get(i, j));
401            }
402
403            // Subtract projections onto previous orthogonal vectors
404            for k in 0..j {
405                // Compute projection coefficient: <q_k, a_j> / <q_k, q_k>
406                let mut dot_qk_aj = Rational64::from(0);
407                let mut dot_qk_qk = Rational64::from(0);
408
409                for i in 0..m {
410                    dot_qk_aj += q.get(i, k) * self.get(i, j);
411                    dot_qk_qk += q.get(i, k) * q.get(i, k);
412                }
413
414                if dot_qk_qk == Rational64::from(0) {
415                    return None;
416                }
417
418                let proj_coeff = dot_qk_aj / dot_qk_qk;
419                r.set(k, j, proj_coeff);
420
421                // Subtract projection: q_j -= proj_coeff * q_k
422                for i in 0..m {
423                    let new_val = q.get(i, j) - proj_coeff * q.get(i, k);
424                    q.set(i, j, new_val);
425                }
426            }
427
428            // Compute norm squared of the orthogonalized q_j
429            let mut norm_sq = Rational64::from(0);
430            for i in 0..m {
431                let val = q.get(i, j);
432                norm_sq += val * val;
433            }
434
435            if norm_sq == Rational64::from(0) {
436                // Columns are linearly dependent
437                return None;
438            }
439
440            // Set r[j,j] = 1 (since we keep q_j unnormalized)
441            r.set(j, j, Rational64::from(1));
442        }
443
444        Some((q, r))
445    }
446
447    /// Performs Cholesky decomposition for symmetric positive definite matrices.
448    /// Returns L where A = L * L^T and L is lower triangular.
449    ///
450    /// # Returns
451    /// None if the matrix is not square, symmetric, or positive definite.
452    ///
453    /// # Note
454    /// This requires taking square roots, which may not be exact for rationals.
455    /// The function will fail (return None) if any diagonal element during
456    /// decomposition is not a perfect rational square.
457    pub fn cholesky_decomposition(&self) -> Option<Matrix> {
458        if self.rows != self.cols {
459            return None; // Must be square
460        }
461
462        let n = self.rows;
463
464        // Check symmetry
465        for i in 0..n {
466            for j in i + 1..n {
467                if self.get(i, j) != self.get(j, i) {
468                    return None; // Not symmetric
469                }
470            }
471        }
472
473        let mut l = Matrix::zeros(n, n);
474
475        for i in 0..n {
476            for j in 0..=i {
477                let mut sum = self.get(i, j);
478
479                for k in 0..j {
480                    sum -= l.get(i, k) * l.get(j, k);
481                }
482
483                if i == j {
484                    // Diagonal element
485                    if sum <= Rational64::from(0) {
486                        return None; // Not positive definite
487                    }
488
489                    // Need to take square root
490                    // Check if sum is a perfect rational square
491                    let sqrt_val = rational_sqrt(sum)?;
492                    l.set(i, i, sqrt_val);
493                } else {
494                    // Off-diagonal element
495                    let l_jj = l.get(j, j);
496                    if l_jj == Rational64::from(0) {
497                        return None;
498                    }
499                    l.set(i, j, sum / l_jj);
500                }
501            }
502        }
503
504        Some(l)
505    }
506
507    /// Computes the inverse of a square matrix using LU decomposition.
508    /// Returns None if the matrix is singular (non-invertible).
509    ///
510    /// # Algorithm
511    /// Uses LU decomposition with partial pivoting to solve AX = I,
512    /// where I is the identity matrix. Each column of the inverse is
513    /// computed by solving a system with the corresponding column of I.
514    pub fn inverse(&self) -> Option<Matrix> {
515        if self.rows != self.cols {
516            return None; // Only square matrices can be inverted
517        }
518
519        let n = self.rows;
520
521        // Use LU decomposition
522        let (l, u, p) = self.lu_decomposition()?;
523
524        // Create result matrix (will be the inverse)
525        let mut inv = Matrix::zeros(n, n);
526
527        // Solve for each column of the inverse
528        for col in 0..n {
529            // Create the col-th column of identity matrix
530            let mut b = vec![Rational64::from(0); n];
531            b[col] = Rational64::from(1);
532
533            // Apply permutation to b
534            let mut pb = vec![Rational64::from(0); n];
535            for i in 0..n {
536                pb[i] = b[p[i]];
537            }
538
539            // Solve L*y = pb (forward substitution)
540            let mut y = vec![Rational64::from(0); n];
541            for i in 0..n {
542                let mut sum = pb[i];
543                for (j, &y_j) in y.iter().enumerate().take(i) {
544                    sum -= l.get(i, j) * y_j;
545                }
546                y[i] = sum; // L has 1s on diagonal
547            }
548
549            // Solve U*x = y (backward substitution)
550            let mut x = vec![Rational64::from(0); n];
551            for i in (0..n).rev() {
552                let mut sum = y[i];
553                for (j, &x_j) in x.iter().enumerate().skip(i + 1).take(n - i - 1) {
554                    sum -= u.get(i, j) * x_j;
555                }
556                let u_ii = u.get(i, i);
557                if u_ii == Rational64::from(0) {
558                    return None; // Singular matrix
559                }
560                x[i] = sum / u_ii;
561            }
562
563            // Set the col-th column of inverse
564            for (i, &x_i) in x.iter().enumerate() {
565                inv.set(i, col, x_i);
566            }
567        }
568
569        Some(inv)
570    }
571
572    /// Checks if this matrix is the identity matrix.
573    pub fn is_identity(&self) -> bool {
574        if self.rows != self.cols {
575            return false;
576        }
577
578        for i in 0..self.rows {
579            for j in 0..self.cols {
580                let expected = if i == j {
581                    Rational64::from(1)
582                } else {
583                    Rational64::from(0)
584                };
585                if self.get(i, j) != expected {
586                    return false;
587                }
588            }
589        }
590        true
591    }
592}
593
594/// Computes the square root of a Rational64 if it's a perfect square.
595/// Returns None if the rational is not a perfect square.
596fn rational_sqrt(r: Rational64) -> Option<Rational64> {
597    use num_integer::Roots;
598    use num_traits::Zero;
599
600    if r < Rational64::zero() {
601        return None;
602    }
603    if r == Rational64::zero() {
604        return Some(Rational64::zero());
605    }
606
607    let numer = r.numer();
608    let denom = r.denom();
609
610    // Check if both numerator and denominator are perfect squares
611    let sqrt_numer = numer.sqrt();
612    let sqrt_denom = denom.sqrt();
613
614    if sqrt_numer * sqrt_numer == *numer && sqrt_denom * sqrt_denom == *denom {
615        Some(Rational64::new(sqrt_numer, sqrt_denom))
616    } else {
617        None
618    }
619}
620
621impl fmt::Display for Matrix {
622    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
623        writeln!(f, "[")?;
624        for i in 0..self.rows {
625            write!(f, "  [")?;
626            for j in 0..self.cols {
627                if j > 0 {
628                    write!(f, ", ")?;
629                }
630                write!(f, "{}", self.get(i, j))?;
631            }
632            writeln!(f, "]")?;
633        }
634        write!(f, "]")
635    }
636}
637
638/// A sparse matrix stored in Compressed Sparse Row (CSR) format.
639/// Only non-zero elements are stored, making it efficient for sparse matrices.
640#[derive(Debug, Clone, PartialEq)]
641pub struct SparseMatrix {
642    rows: usize,
643    cols: usize,
644    /// Row pointers: row_ptr[i] points to the start of row i in col_indices and values.
645    /// row_ptr has length rows + 1, with row_ptr[rows] = nnz (number of non-zeros).
646    row_ptr: Vec<usize>,
647    /// Column indices for each non-zero value.
648    col_indices: Vec<usize>,
649    /// Non-zero values.
650    values: Vec<Rational64>,
651}
652
653impl SparseMatrix {
654    /// Creates a new sparse matrix with the given dimensions.
655    pub fn new(rows: usize, cols: usize) -> Self {
656        Self {
657            rows,
658            cols,
659            row_ptr: vec![0; rows + 1],
660            col_indices: Vec::new(),
661            values: Vec::new(),
662        }
663    }
664
665    /// Creates a sparse identity matrix.
666    pub fn identity(size: usize) -> Self {
667        let mut matrix = Self::new(size, size);
668        matrix.row_ptr = (0..=size).collect();
669        matrix.col_indices = (0..size).collect();
670        matrix.values = vec![Rational64::from(1); size];
671        matrix
672    }
673
674    /// Creates a sparse matrix from a dense matrix.
675    pub fn from_dense(dense: &Matrix) -> Self {
676        let mut matrix = Self::new(dense.nrows(), dense.ncols());
677        let mut col_indices = Vec::new();
678        let mut values = Vec::new();
679
680        for i in 0..dense.nrows() {
681            matrix.row_ptr[i] = values.len();
682            for j in 0..dense.ncols() {
683                let val = dense.get(i, j);
684                if val != Rational64::from(0) {
685                    col_indices.push(j);
686                    values.push(val);
687                }
688            }
689        }
690        matrix.row_ptr[dense.nrows()] = values.len();
691        matrix.col_indices = col_indices;
692        matrix.values = values;
693        matrix
694    }
695
696    /// Returns the number of rows.
697    pub fn nrows(&self) -> usize {
698        self.rows
699    }
700
701    /// Returns the number of columns.
702    pub fn ncols(&self) -> usize {
703        self.cols
704    }
705
706    /// Returns the number of non-zero elements.
707    pub fn nnz(&self) -> usize {
708        self.values.len()
709    }
710
711    /// Gets the element at position (row, col).
712    pub fn get(&self, row: usize, col: usize) -> Rational64 {
713        assert!(row < self.rows && col < self.cols);
714        let start = self.row_ptr[row];
715        let end = self.row_ptr[row + 1];
716
717        // Binary search for the column index
718        for i in start..end {
719            if self.col_indices[i] == col {
720                return self.values[i];
721            } else if self.col_indices[i] > col {
722                break;
723            }
724        }
725        Rational64::from(0)
726    }
727
728    /// Sets the element at position (row, col).
729    /// Note: This is inefficient for CSR format. Consider building the matrix
730    /// incrementally using a builder pattern or from a dense matrix.
731    pub fn set(&mut self, row: usize, col: usize, value: Rational64) {
732        assert!(row < self.rows && col < self.cols);
733
734        if value == Rational64::from(0) {
735            // Remove the element if it exists
736            let start = self.row_ptr[row];
737            let end = self.row_ptr[row + 1];
738
739            for i in start..end {
740                if self.col_indices[i] == col {
741                    self.col_indices.remove(i);
742                    self.values.remove(i);
743                    // Update row pointers
744                    for r in row + 1..=self.rows {
745                        self.row_ptr[r] -= 1;
746                    }
747                    return;
748                }
749            }
750            return;
751        }
752
753        // Find or insert the element
754        let start = self.row_ptr[row];
755        let end = self.row_ptr[row + 1];
756
757        for i in start..end {
758            if self.col_indices[i] == col {
759                // Update existing element
760                self.values[i] = value;
761                return;
762            } else if self.col_indices[i] > col {
763                // Insert new element
764                self.col_indices.insert(i, col);
765                self.values.insert(i, value);
766                // Update row pointers
767                for r in row + 1..=self.rows {
768                    self.row_ptr[r] += 1;
769                }
770                return;
771            }
772        }
773
774        // Append to end of row
775        self.col_indices.insert(end, col);
776        self.values.insert(end, value);
777        for r in row + 1..=self.rows {
778            self.row_ptr[r] += 1;
779        }
780    }
781
782    /// Sparse matrix-vector multiplication: returns A * v
783    #[allow(clippy::needless_range_loop)]
784    pub fn mul_vec(&self, v: &[Rational64]) -> Vec<Rational64> {
785        assert_eq!(
786            v.len(),
787            self.cols,
788            "Vector dimension must match matrix columns"
789        );
790        let mut result = vec![Rational64::from(0); self.rows];
791
792        for i in 0..self.rows {
793            let start = self.row_ptr[i];
794            let end = self.row_ptr[i + 1];
795            let mut sum = Rational64::from(0);
796
797            for k in start..end {
798                sum += self.values[k] * v[self.col_indices[k]];
799            }
800            result[i] = sum;
801        }
802
803        result
804    }
805
806    /// Sparse matrix-matrix multiplication: returns A * B
807    pub fn mul(&self, other: &SparseMatrix) -> SparseMatrix {
808        assert_eq!(
809            self.cols, other.rows,
810            "Matrix dimensions must be compatible for multiplication"
811        );
812
813        let mut result = SparseMatrix::new(self.rows, other.cols);
814        let mut row_data: Vec<Rational64> = vec![Rational64::from(0); other.cols];
815        let mut col_indices = Vec::new();
816        let mut values = Vec::new();
817
818        for i in 0..self.rows {
819            result.row_ptr[i] = values.len();
820
821            // Compute row i of result
822            row_data.fill(Rational64::from(0));
823
824            let start = self.row_ptr[i];
825            let end = self.row_ptr[i + 1];
826
827            for k in start..end {
828                let a_val = &self.values[k];
829                let k_col = self.col_indices[k];
830
831                // Multiply with row k of B
832                let b_start = other.row_ptr[k_col];
833                let b_end = other.row_ptr[k_col + 1];
834
835                for j in b_start..b_end {
836                    let b_col = other.col_indices[j];
837                    let b_val = &other.values[j];
838                    row_data[b_col] += a_val * b_val;
839                }
840            }
841
842            // Store non-zero elements
843            for (j, val) in row_data.iter().enumerate() {
844                if val != &Rational64::from(0) {
845                    col_indices.push(j);
846                    values.push(*val);
847                }
848            }
849        }
850
851        result.row_ptr[self.rows] = values.len();
852        result.col_indices = col_indices;
853        result.values = values;
854        result
855    }
856
857    /// Transposes the sparse matrix.
858    pub fn transpose(&self) -> SparseMatrix {
859        let mut result = SparseMatrix::new(self.cols, self.rows);
860        let mut col_counts = vec![0; self.cols];
861
862        // Count elements per column (which become rows in transpose)
863        for &col in &self.col_indices {
864            col_counts[col] += 1;
865        }
866
867        // Build row pointers for transpose
868        result.row_ptr[0] = 0;
869        #[allow(clippy::needless_range_loop)]
870        for i in 0..self.cols {
871            result.row_ptr[i + 1] = result.row_ptr[i] + col_counts[i];
872        }
873
874        // Allocate space
875        result.col_indices = vec![0; self.nnz()];
876        result.values = vec![Rational64::from(0); self.nnz()];
877
878        // Fill in values
879        let mut current_pos = result.row_ptr.clone();
880        for i in 0..self.rows {
881            let start = self.row_ptr[i];
882            let end = self.row_ptr[i + 1];
883
884            for k in start..end {
885                let col = self.col_indices[k];
886                let pos = current_pos[col];
887                result.col_indices[pos] = i;
888                result.values[pos] = self.values[k];
889                current_pos[col] += 1;
890            }
891        }
892
893        result
894    }
895
896    /// Converts the sparse matrix to a dense matrix.
897    pub fn to_dense(&self) -> Matrix {
898        let mut dense = Matrix::zeros(self.rows, self.cols);
899        for i in 0..self.rows {
900            let start = self.row_ptr[i];
901            let end = self.row_ptr[i + 1];
902            for k in start..end {
903                dense.set(i, self.col_indices[k], self.values[k]);
904            }
905        }
906        dense
907    }
908}
909
910impl fmt::Display for SparseMatrix {
911    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
912        writeln!(
913            f,
914            "SparseMatrix({}x{}, {} non-zeros):",
915            self.rows,
916            self.cols,
917            self.nnz()
918        )?;
919        for i in 0..self.rows {
920            write!(f, "  Row {}: ", i)?;
921            let start = self.row_ptr[i];
922            let end = self.row_ptr[i + 1];
923            for k in start..end {
924                write!(f, "({}, {}) ", self.col_indices[k], self.values[k])?;
925            }
926            writeln!(f)?;
927        }
928        Ok(())
929    }
930}
931
932#[cfg(test)]
933mod tests {
934    use super::*;
935
936    #[test]
937    fn test_matrix_zeros() {
938        let m = Matrix::zeros(2, 3);
939        assert_eq!(m.nrows(), 2);
940        assert_eq!(m.ncols(), 3);
941        assert_eq!(m.get(0, 0), Rational64::from(0));
942        assert_eq!(m.get(1, 2), Rational64::from(0));
943    }
944
945    #[test]
946    fn test_matrix_identity() {
947        let m = Matrix::identity(3);
948        assert_eq!(m.get(0, 0), Rational64::from(1));
949        assert_eq!(m.get(1, 1), Rational64::from(1));
950        assert_eq!(m.get(2, 2), Rational64::from(1));
951        assert_eq!(m.get(0, 1), Rational64::from(0));
952        assert_eq!(m.get(1, 2), Rational64::from(0));
953    }
954
955    #[test]
956    fn test_matrix_from_rows() {
957        let m = Matrix::from_rows(vec![
958            vec![Rational64::from(1), Rational64::from(2)],
959            vec![Rational64::from(3), Rational64::from(4)],
960        ]);
961        assert_eq!(m.get(0, 0), Rational64::from(1));
962        assert_eq!(m.get(0, 1), Rational64::from(2));
963        assert_eq!(m.get(1, 0), Rational64::from(3));
964        assert_eq!(m.get(1, 1), Rational64::from(4));
965    }
966
967    #[test]
968    fn test_matrix_swap_rows() {
969        let mut m = Matrix::from_rows(vec![
970            vec![Rational64::from(1), Rational64::from(2)],
971            vec![Rational64::from(3), Rational64::from(4)],
972        ]);
973        m.swap_rows(0, 1);
974        assert_eq!(m.get(0, 0), Rational64::from(3));
975        assert_eq!(m.get(0, 1), Rational64::from(4));
976        assert_eq!(m.get(1, 0), Rational64::from(1));
977        assert_eq!(m.get(1, 1), Rational64::from(2));
978    }
979
980    #[test]
981    fn test_matrix_add_row_multiple() {
982        let mut m = Matrix::from_rows(vec![
983            vec![Rational64::from(1), Rational64::from(2)],
984            vec![Rational64::from(3), Rational64::from(4)],
985        ]);
986        m.add_row_multiple(1, 0, Rational64::from(2));
987        assert_eq!(m.get(0, 0), Rational64::from(1));
988        assert_eq!(m.get(0, 1), Rational64::from(2));
989        assert_eq!(m.get(1, 0), Rational64::from(5)); // 3 + 2*1
990        assert_eq!(m.get(1, 1), Rational64::from(8)); // 4 + 2*2
991    }
992
993    #[test]
994    fn test_matrix_mul_vec() {
995        let m = Matrix::from_rows(vec![
996            vec![Rational64::from(1), Rational64::from(2)],
997            vec![Rational64::from(3), Rational64::from(4)],
998        ]);
999        let v = vec![Rational64::from(5), Rational64::from(6)];
1000        let result = m.mul_vec(&v);
1001        assert_eq!(result[0], Rational64::from(17)); // 1*5 + 2*6
1002        assert_eq!(result[1], Rational64::from(39)); // 3*5 + 4*6
1003    }
1004
1005    #[test]
1006    fn test_matrix_mul() {
1007        let a = Matrix::from_rows(vec![
1008            vec![Rational64::from(1), Rational64::from(2)],
1009            vec![Rational64::from(3), Rational64::from(4)],
1010        ]);
1011        let b = Matrix::from_rows(vec![
1012            vec![Rational64::from(5), Rational64::from(6)],
1013            vec![Rational64::from(7), Rational64::from(8)],
1014        ]);
1015        let c = a.mul(&b);
1016        assert_eq!(c.get(0, 0), Rational64::from(19)); // 1*5 + 2*7
1017        assert_eq!(c.get(0, 1), Rational64::from(22)); // 1*6 + 2*8
1018        assert_eq!(c.get(1, 0), Rational64::from(43)); // 3*5 + 4*7
1019        assert_eq!(c.get(1, 1), Rational64::from(50)); // 3*6 + 4*8
1020    }
1021
1022    #[test]
1023    fn test_matrix_transpose() {
1024        let m = Matrix::from_rows(vec![
1025            vec![
1026                Rational64::from(1),
1027                Rational64::from(2),
1028                Rational64::from(3),
1029            ],
1030            vec![
1031                Rational64::from(4),
1032                Rational64::from(5),
1033                Rational64::from(6),
1034            ],
1035        ]);
1036        let t = m.transpose();
1037        assert_eq!(t.nrows(), 3);
1038        assert_eq!(t.ncols(), 2);
1039        assert_eq!(t.get(0, 0), Rational64::from(1));
1040        assert_eq!(t.get(1, 0), Rational64::from(2));
1041        assert_eq!(t.get(2, 0), Rational64::from(3));
1042        assert_eq!(t.get(0, 1), Rational64::from(4));
1043        assert_eq!(t.get(1, 1), Rational64::from(5));
1044        assert_eq!(t.get(2, 1), Rational64::from(6));
1045    }
1046
1047    #[test]
1048    fn test_gaussian_elimination() {
1049        let m = Matrix::from_rows(vec![
1050            vec![
1051                Rational64::from(2),
1052                Rational64::from(1),
1053                Rational64::from(-1),
1054            ],
1055            vec![
1056                Rational64::from(-3),
1057                Rational64::from(-1),
1058                Rational64::from(2),
1059            ],
1060            vec![
1061                Rational64::from(-2),
1062                Rational64::from(1),
1063                Rational64::from(2),
1064            ],
1065        ]);
1066        let (ref_form, rank) = m.gaussian_elimination();
1067        assert_eq!(rank, 3);
1068        // Check that it's in row echelon form (upper triangular)
1069        assert_ne!(ref_form.get(0, 0), Rational64::from(0));
1070        assert_eq!(ref_form.get(1, 0), Rational64::from(0));
1071        assert_eq!(ref_form.get(2, 0), Rational64::from(0));
1072    }
1073
1074    #[test]
1075    fn test_solve_linear_system() {
1076        // Solve: 2x + y - z = 8
1077        //       -3x - y + 2z = -11
1078        //       -2x + y + 2z = -3
1079        let a = Matrix::from_rows(vec![
1080            vec![
1081                Rational64::from(2),
1082                Rational64::from(1),
1083                Rational64::from(-1),
1084            ],
1085            vec![
1086                Rational64::from(-3),
1087                Rational64::from(-1),
1088                Rational64::from(2),
1089            ],
1090            vec![
1091                Rational64::from(-2),
1092                Rational64::from(1),
1093                Rational64::from(2),
1094            ],
1095        ]);
1096        let b = vec![
1097            Rational64::from(8),
1098            Rational64::from(-11),
1099            Rational64::from(-3),
1100        ];
1101        let x = a.solve(&b).expect("test operation should succeed");
1102        assert_eq!(x[0], Rational64::from(2));
1103        assert_eq!(x[1], Rational64::from(3));
1104        assert_eq!(x[2], Rational64::from(-1));
1105    }
1106
1107    #[test]
1108    fn test_lu_decomposition() {
1109        let m = Matrix::from_rows(vec![
1110            vec![Rational64::from(4), Rational64::from(3)],
1111            vec![Rational64::from(6), Rational64::from(3)],
1112        ]);
1113        let (l, u, _p) = m.lu_decomposition().expect("test operation should succeed");
1114
1115        // L should be lower triangular with ones on diagonal
1116        assert_eq!(l.get(0, 0), Rational64::from(1));
1117        assert_eq!(l.get(1, 1), Rational64::from(1));
1118        assert_eq!(l.get(0, 1), Rational64::from(0));
1119
1120        // U should be upper triangular
1121        assert_eq!(u.get(1, 0), Rational64::from(0));
1122    }
1123
1124    #[test]
1125    fn test_determinant() {
1126        let m = Matrix::from_rows(vec![
1127            vec![Rational64::from(1), Rational64::from(2)],
1128            vec![Rational64::from(3), Rational64::from(4)],
1129        ]);
1130        let det = m.determinant().expect("test operation should succeed");
1131        assert_eq!(det, Rational64::from(-2)); // 1*4 - 2*3 = -2
1132    }
1133
1134    #[test]
1135    fn test_determinant_3x3() {
1136        let m = Matrix::from_rows(vec![
1137            vec![
1138                Rational64::from(6),
1139                Rational64::from(1),
1140                Rational64::from(1),
1141            ],
1142            vec![
1143                Rational64::from(4),
1144                Rational64::from(-2),
1145                Rational64::from(5),
1146            ],
1147            vec![
1148                Rational64::from(2),
1149                Rational64::from(8),
1150                Rational64::from(7),
1151            ],
1152        ]);
1153        let det = m.determinant().expect("test operation should succeed");
1154        assert_eq!(det, Rational64::from(-306));
1155    }
1156
1157    #[test]
1158    fn test_sparse_matrix_identity() {
1159        let m = SparseMatrix::identity(3);
1160        assert_eq!(m.nrows(), 3);
1161        assert_eq!(m.ncols(), 3);
1162        assert_eq!(m.nnz(), 3);
1163        assert_eq!(m.get(0, 0), Rational64::from(1));
1164        assert_eq!(m.get(1, 1), Rational64::from(1));
1165        assert_eq!(m.get(2, 2), Rational64::from(1));
1166        assert_eq!(m.get(0, 1), Rational64::from(0));
1167        assert_eq!(m.get(1, 2), Rational64::from(0));
1168    }
1169
1170    #[test]
1171    fn test_sparse_from_dense() {
1172        let dense = Matrix::from_rows(vec![
1173            vec![
1174                Rational64::from(1),
1175                Rational64::from(0),
1176                Rational64::from(2),
1177            ],
1178            vec![
1179                Rational64::from(0),
1180                Rational64::from(3),
1181                Rational64::from(0),
1182            ],
1183            vec![
1184                Rational64::from(4),
1185                Rational64::from(0),
1186                Rational64::from(5),
1187            ],
1188        ]);
1189        let sparse = SparseMatrix::from_dense(&dense);
1190        assert_eq!(sparse.nnz(), 5); // Five non-zero elements
1191        assert_eq!(sparse.get(0, 0), Rational64::from(1));
1192        assert_eq!(sparse.get(0, 2), Rational64::from(2));
1193        assert_eq!(sparse.get(1, 1), Rational64::from(3));
1194        assert_eq!(sparse.get(2, 0), Rational64::from(4));
1195        assert_eq!(sparse.get(2, 2), Rational64::from(5));
1196        assert_eq!(sparse.get(0, 1), Rational64::from(0));
1197    }
1198
1199    #[test]
1200    fn test_sparse_to_dense() {
1201        let sparse = SparseMatrix::identity(3);
1202        let dense = sparse.to_dense();
1203        assert_eq!(dense.get(0, 0), Rational64::from(1));
1204        assert_eq!(dense.get(1, 1), Rational64::from(1));
1205        assert_eq!(dense.get(2, 2), Rational64::from(1));
1206        assert_eq!(dense.get(0, 1), Rational64::from(0));
1207    }
1208
1209    #[test]
1210    fn test_sparse_mul_vec() {
1211        let dense = Matrix::from_rows(vec![
1212            vec![
1213                Rational64::from(1),
1214                Rational64::from(0),
1215                Rational64::from(2),
1216            ],
1217            vec![
1218                Rational64::from(0),
1219                Rational64::from(3),
1220                Rational64::from(0),
1221            ],
1222        ]);
1223        let sparse = SparseMatrix::from_dense(&dense);
1224        let v = vec![
1225            Rational64::from(1),
1226            Rational64::from(2),
1227            Rational64::from(3),
1228        ];
1229        let result = sparse.mul_vec(&v);
1230        assert_eq!(result[0], Rational64::from(7)); // 1*1 + 0*2 + 2*3 = 7
1231        assert_eq!(result[1], Rational64::from(6)); // 0*1 + 3*2 + 0*3 = 6
1232    }
1233
1234    #[test]
1235    fn test_sparse_transpose() {
1236        let dense = Matrix::from_rows(vec![
1237            vec![Rational64::from(1), Rational64::from(2)],
1238            vec![Rational64::from(3), Rational64::from(4)],
1239            vec![Rational64::from(5), Rational64::from(6)],
1240        ]);
1241        let sparse = SparseMatrix::from_dense(&dense);
1242        let transposed = sparse.transpose();
1243        assert_eq!(transposed.nrows(), 2);
1244        assert_eq!(transposed.ncols(), 3);
1245        assert_eq!(transposed.get(0, 0), Rational64::from(1));
1246        assert_eq!(transposed.get(0, 1), Rational64::from(3));
1247        assert_eq!(transposed.get(0, 2), Rational64::from(5));
1248        assert_eq!(transposed.get(1, 0), Rational64::from(2));
1249        assert_eq!(transposed.get(1, 1), Rational64::from(4));
1250        assert_eq!(transposed.get(1, 2), Rational64::from(6));
1251    }
1252
1253    #[test]
1254    fn test_sparse_mul() {
1255        let a_dense = Matrix::from_rows(vec![
1256            vec![Rational64::from(1), Rational64::from(2)],
1257            vec![Rational64::from(3), Rational64::from(4)],
1258        ]);
1259        let b_dense = Matrix::from_rows(vec![
1260            vec![Rational64::from(5), Rational64::from(6)],
1261            vec![Rational64::from(7), Rational64::from(8)],
1262        ]);
1263        let a = SparseMatrix::from_dense(&a_dense);
1264        let b = SparseMatrix::from_dense(&b_dense);
1265        let c = a.mul(&b);
1266        let c_dense = c.to_dense();
1267        assert_eq!(c_dense.get(0, 0), Rational64::from(19)); // 1*5 + 2*7
1268        assert_eq!(c_dense.get(0, 1), Rational64::from(22)); // 1*6 + 2*8
1269        assert_eq!(c_dense.get(1, 0), Rational64::from(43)); // 3*5 + 4*7
1270        assert_eq!(c_dense.get(1, 1), Rational64::from(50)); // 3*6 + 4*8
1271    }
1272
1273    #[test]
1274    fn test_qr_decomposition_simple() {
1275        // Test QR decomposition with a simple 2x2 matrix
1276        let m = Matrix::from_rows(vec![
1277            vec![Rational64::from(1), Rational64::from(1)],
1278            vec![Rational64::from(0), Rational64::from(1)],
1279        ]);
1280
1281        let (q, r) = m.qr_decomposition().expect("test operation should succeed");
1282
1283        // Verify QR = A by multiplying Q and R
1284        let reconstructed = q.mul(&r);
1285
1286        // Check that reconstructed matrix equals original
1287        for i in 0..m.nrows() {
1288            for j in 0..m.ncols() {
1289                assert_eq!(
1290                    m.get(i, j),
1291                    reconstructed.get(i, j),
1292                    "QR reconstruction failed at ({}, {})",
1293                    i,
1294                    j
1295                );
1296            }
1297        }
1298    }
1299
1300    #[test]
1301    fn test_qr_decomposition_3x2() {
1302        // Test with a tall matrix (m > n)
1303        let m = Matrix::from_rows(vec![
1304            vec![Rational64::from(1), Rational64::from(0)],
1305            vec![Rational64::from(1), Rational64::from(1)],
1306            vec![Rational64::from(0), Rational64::from(1)],
1307        ]);
1308
1309        let (q, r) = m.qr_decomposition().expect("test operation should succeed");
1310
1311        // Verify dimensions
1312        assert_eq!(q.nrows(), 3);
1313        assert_eq!(q.ncols(), 2);
1314        assert_eq!(r.nrows(), 2);
1315        assert_eq!(r.ncols(), 2);
1316
1317        // Verify QR = A
1318        let reconstructed = q.mul(&r);
1319        for i in 0..m.nrows() {
1320            for j in 0..m.ncols() {
1321                assert_eq!(m.get(i, j), reconstructed.get(i, j));
1322            }
1323        }
1324    }
1325
1326    #[test]
1327    fn test_cholesky_decomposition_simple() {
1328        // Test with a simple 2x2 positive definite matrix
1329        // A = [[4, 2],
1330        //      [2, 3]]
1331        // L = [[2, 0],
1332        //      [1, sqrt(2)]]
1333        // But sqrt(2) is not rational, so this matrix won't decompose exactly
1334
1335        // Instead, use a matrix with rational square roots
1336        // A = [[4, 2],
1337        //      [2, 2]]
1338        // L = [[2, 0],
1339        //      [1, 1]]
1340        let m = Matrix::from_rows(vec![
1341            vec![Rational64::from(4), Rational64::from(2)],
1342            vec![Rational64::from(2), Rational64::from(2)],
1343        ]);
1344
1345        let l = m
1346            .cholesky_decomposition()
1347            .expect("test operation should succeed");
1348
1349        // Verify L * L^T = A
1350        let lt = l.transpose();
1351        let reconstructed = l.mul(&lt);
1352
1353        for i in 0..m.nrows() {
1354            for j in 0..m.ncols() {
1355                assert_eq!(m.get(i, j), reconstructed.get(i, j));
1356            }
1357        }
1358
1359        // Verify L is lower triangular
1360        assert_eq!(l.get(0, 1), Rational64::from(0));
1361    }
1362
1363    #[test]
1364    fn test_cholesky_decomposition_identity() {
1365        // Identity matrix should have Cholesky decomposition = identity
1366        let m = Matrix::identity(3);
1367        let l = m
1368            .cholesky_decomposition()
1369            .expect("test operation should succeed");
1370
1371        for i in 0..3 {
1372            for j in 0..3 {
1373                assert_eq!(l.get(i, j), m.get(i, j));
1374            }
1375        }
1376    }
1377
1378    #[test]
1379    fn test_cholesky_decomposition_3x3() {
1380        // Test with a 3x3 positive definite matrix
1381        // A = [[9, 3, 3],
1382        //      [3, 5, 1],
1383        //      [3, 1, 5]]
1384        // This has a rational Cholesky decomposition
1385        let m = Matrix::from_rows(vec![
1386            vec![
1387                Rational64::from(9),
1388                Rational64::from(3),
1389                Rational64::from(3),
1390            ],
1391            vec![
1392                Rational64::from(3),
1393                Rational64::from(5),
1394                Rational64::from(1),
1395            ],
1396            vec![
1397                Rational64::from(3),
1398                Rational64::from(1),
1399                Rational64::from(5),
1400            ],
1401        ]);
1402
1403        let l = m
1404            .cholesky_decomposition()
1405            .expect("test operation should succeed");
1406
1407        // Verify L * L^T = A
1408        let lt = l.transpose();
1409        let reconstructed = l.mul(&lt);
1410
1411        for i in 0..m.nrows() {
1412            for j in 0..m.ncols() {
1413                assert_eq!(
1414                    m.get(i, j),
1415                    reconstructed.get(i, j),
1416                    "Cholesky reconstruction failed at ({}, {})",
1417                    i,
1418                    j
1419                );
1420            }
1421        }
1422
1423        // Verify L is lower triangular
1424        for i in 0..3 {
1425            for j in i + 1..3 {
1426                assert_eq!(l.get(i, j), Rational64::from(0));
1427            }
1428        }
1429    }
1430
1431    #[test]
1432    fn test_cholesky_decomposition_not_symmetric() {
1433        // Non-symmetric matrix should fail
1434        let m = Matrix::from_rows(vec![
1435            vec![Rational64::from(4), Rational64::from(1)],
1436            vec![Rational64::from(2), Rational64::from(3)],
1437        ]);
1438
1439        assert!(m.cholesky_decomposition().is_none());
1440    }
1441
1442    #[test]
1443    fn test_cholesky_decomposition_not_positive_definite() {
1444        // Symmetric but not positive definite matrix should fail
1445        let m = Matrix::from_rows(vec![
1446            vec![Rational64::from(1), Rational64::from(2)],
1447            vec![Rational64::from(2), Rational64::from(1)],
1448        ]);
1449
1450        // This matrix has eigenvalues 3 and -1, so it's not positive definite
1451        assert!(m.cholesky_decomposition().is_none());
1452    }
1453
1454    #[test]
1455    fn test_rational_sqrt_perfect_squares() {
1456        assert_eq!(
1457            rational_sqrt(Rational64::from(0)),
1458            Some(Rational64::from(0))
1459        );
1460        assert_eq!(
1461            rational_sqrt(Rational64::from(1)),
1462            Some(Rational64::from(1))
1463        );
1464        assert_eq!(
1465            rational_sqrt(Rational64::from(4)),
1466            Some(Rational64::from(2))
1467        );
1468        assert_eq!(
1469            rational_sqrt(Rational64::from(9)),
1470            Some(Rational64::from(3))
1471        );
1472        assert_eq!(
1473            rational_sqrt(Rational64::from(16)),
1474            Some(Rational64::from(4))
1475        );
1476        assert_eq!(
1477            rational_sqrt(Rational64::from(25)),
1478            Some(Rational64::from(5))
1479        );
1480
1481        // Test rational perfect squares
1482        assert_eq!(
1483            rational_sqrt(Rational64::new(1, 4)),
1484            Some(Rational64::new(1, 2))
1485        );
1486        assert_eq!(
1487            rational_sqrt(Rational64::new(9, 16)),
1488            Some(Rational64::new(3, 4))
1489        );
1490    }
1491
1492    #[test]
1493    fn test_rational_sqrt_not_perfect_squares() {
1494        assert!(rational_sqrt(Rational64::from(2)).is_none());
1495        assert!(rational_sqrt(Rational64::from(3)).is_none());
1496        assert!(rational_sqrt(Rational64::from(5)).is_none());
1497        assert!(rational_sqrt(Rational64::new(1, 2)).is_none());
1498        assert!(rational_sqrt(Rational64::new(2, 3)).is_none());
1499        assert!(rational_sqrt(Rational64::from(-1)).is_none()); // Negative
1500    }
1501
1502    #[test]
1503    fn test_matrix_inverse_2x2() {
1504        // Test with a simple 2x2 matrix
1505        // A = [[1, 2],
1506        //      [3, 4]]
1507        // A^-1 = [[-2, 1],
1508        //         [3/2, -1/2]]
1509        let a = Matrix::from_rows(vec![
1510            vec![Rational64::from(1), Rational64::from(2)],
1511            vec![Rational64::from(3), Rational64::from(4)],
1512        ]);
1513
1514        let a_inv = a.inverse().expect("test operation should succeed");
1515
1516        // Verify A * A^-1 = I
1517        let product = a.mul(&a_inv);
1518        assert!(product.is_identity(), "A * A^-1 should be identity");
1519
1520        // Verify A^-1 * A = I
1521        let product2 = a_inv.mul(&a);
1522        assert!(product2.is_identity(), "A^-1 * A should be identity");
1523    }
1524
1525    #[test]
1526    fn test_matrix_inverse_identity() {
1527        // Identity matrix inverse should be itself
1528        let i = Matrix::identity(3);
1529        let i_inv = i.inverse().expect("test operation should succeed");
1530        assert!(i_inv.is_identity());
1531    }
1532
1533    #[test]
1534    fn test_matrix_inverse_3x3() {
1535        // Test with a 3x3 matrix
1536        let a = Matrix::from_rows(vec![
1537            vec![
1538                Rational64::from(2),
1539                Rational64::from(1),
1540                Rational64::from(1),
1541            ],
1542            vec![
1543                Rational64::from(1),
1544                Rational64::from(2),
1545                Rational64::from(1),
1546            ],
1547            vec![
1548                Rational64::from(1),
1549                Rational64::from(1),
1550                Rational64::from(2),
1551            ],
1552        ]);
1553
1554        let a_inv = a.inverse().expect("test operation should succeed");
1555
1556        // Verify A * A^-1 = I
1557        let product = a.mul(&a_inv);
1558        for i in 0..3 {
1559            for j in 0..3 {
1560                let expected = if i == j {
1561                    Rational64::from(1)
1562                } else {
1563                    Rational64::from(0)
1564                };
1565                assert_eq!(
1566                    product.get(i, j),
1567                    expected,
1568                    "A * A^-1 failed at ({}, {})",
1569                    i,
1570                    j
1571                );
1572            }
1573        }
1574    }
1575
1576    #[test]
1577    fn test_matrix_inverse_singular() {
1578        // Singular matrix (determinant = 0) should not have an inverse
1579        let a = Matrix::from_rows(vec![
1580            vec![Rational64::from(1), Rational64::from(2)],
1581            vec![Rational64::from(2), Rational64::from(4)], // Second row is 2x first row
1582        ]);
1583
1584        assert!(
1585            a.inverse().is_none(),
1586            "Singular matrix should not have inverse"
1587        );
1588    }
1589
1590    #[test]
1591    fn test_matrix_inverse_non_square() {
1592        // Non-square matrix should not have an inverse
1593        let a = Matrix::from_rows(vec![
1594            vec![
1595                Rational64::from(1),
1596                Rational64::from(2),
1597                Rational64::from(3),
1598            ],
1599            vec![
1600                Rational64::from(4),
1601                Rational64::from(5),
1602                Rational64::from(6),
1603            ],
1604        ]);
1605
1606        assert!(
1607            a.inverse().is_none(),
1608            "Non-square matrix should not have inverse"
1609        );
1610    }
1611
1612    #[test]
1613    fn test_is_identity() {
1614        let i = Matrix::identity(3);
1615        assert!(i.is_identity());
1616
1617        let not_i = Matrix::from_rows(vec![
1618            vec![Rational64::from(1), Rational64::from(0)],
1619            vec![Rational64::from(0), Rational64::from(2)],
1620        ]);
1621        assert!(!not_i.is_identity());
1622
1623        let also_not_i = Matrix::from_rows(vec![
1624            vec![Rational64::from(1), Rational64::from(1)],
1625            vec![Rational64::from(0), Rational64::from(1)],
1626        ]);
1627        assert!(!also_not_i.is_identity());
1628    }
1629}