1#[derive(Debug, Clone, PartialEq)]
31pub enum MatrixError {
32 DimensionMismatch {
34 expected: (usize, usize),
35 got: (usize, usize),
36 },
37 NotSquare { rows: usize, cols: usize },
39 Singular,
41 NotSymmetric,
43 NotPositiveDefinite,
45 InvalidData { expected: usize, got: usize },
47}
48
49impl std::fmt::Display for MatrixError {
50 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
51 match self {
52 MatrixError::DimensionMismatch { expected, got } => {
53 write!(
54 f,
55 "dimension mismatch: expected {}×{}, got {}×{}",
56 expected.0, expected.1, got.0, got.1
57 )
58 }
59 MatrixError::NotSquare { rows, cols } => {
60 write!(f, "matrix must be square, got {rows}×{cols}")
61 }
62 MatrixError::Singular => write!(f, "matrix is singular"),
63 MatrixError::NotSymmetric => write!(f, "matrix is not symmetric"),
64 MatrixError::NotPositiveDefinite => write!(f, "matrix is not positive-definite"),
65 MatrixError::InvalidData { expected, got } => {
66 write!(f, "data length mismatch: expected {expected}, got {got}")
67 }
68 }
69 }
70}
71
72impl std::error::Error for MatrixError {}
73
74#[derive(Debug, Clone, PartialEq)]
80pub struct Matrix {
81 data: Vec<f64>,
82 rows: usize,
83 cols: usize,
84}
85
86impl Matrix {
87 pub fn new(rows: usize, cols: usize, data: Vec<f64>) -> Result<Self, MatrixError> {
100 if data.len() != rows * cols {
101 return Err(MatrixError::InvalidData {
102 expected: rows * cols,
103 got: data.len(),
104 });
105 }
106 Ok(Self { data, rows, cols })
107 }
108
109 pub fn from_rows(rows: &[&[f64]]) -> Self {
121 assert!(!rows.is_empty(), "must have at least one row");
122 let ncols = rows[0].len();
123 assert!(ncols > 0, "must have at least one column");
124 let nrows = rows.len();
125 let mut data = Vec::with_capacity(nrows * ncols);
126 for (i, row) in rows.iter().enumerate() {
127 assert_eq!(
128 row.len(),
129 ncols,
130 "row {i} has {} columns, expected {ncols}",
131 row.len()
132 );
133 data.extend_from_slice(row);
134 }
135 Self {
136 data,
137 rows: nrows,
138 cols: ncols,
139 }
140 }
141
142 pub fn zeros(rows: usize, cols: usize) -> Self {
144 Self {
145 data: vec![0.0; rows * cols],
146 rows,
147 cols,
148 }
149 }
150
151 pub fn identity(n: usize) -> Self {
162 let mut m = Self::zeros(n, n);
163 for i in 0..n {
164 m.data[i * n + i] = 1.0;
165 }
166 m
167 }
168
169 pub fn from_col(data: &[f64]) -> Self {
171 Self {
172 data: data.to_vec(),
173 rows: data.len(),
174 cols: 1,
175 }
176 }
177
178 #[inline]
180 pub fn rows(&self) -> usize {
181 self.rows
182 }
183
184 #[inline]
186 pub fn cols(&self) -> usize {
187 self.cols
188 }
189
190 #[inline]
195 pub fn get(&self, row: usize, col: usize) -> f64 {
196 self.data[row * self.cols + col]
197 }
198
199 #[inline]
204 pub fn set(&mut self, row: usize, col: usize, value: f64) {
205 self.data[row * self.cols + col] = value;
206 }
207
208 pub fn data(&self) -> &[f64] {
210 &self.data
211 }
212
213 #[inline]
215 pub fn row(&self, row: usize) -> &[f64] {
216 let start = row * self.cols;
217 &self.data[start..start + self.cols]
218 }
219
220 pub fn diag(&self) -> Vec<f64> {
222 let n = self.rows.min(self.cols);
223 (0..n).map(|i| self.get(i, i)).collect()
224 }
225
226 pub fn is_square(&self) -> bool {
228 self.rows == self.cols
229 }
230
231 pub fn transpose(&self) -> Self {
247 let mut result = Self::zeros(self.cols, self.rows);
248 for i in 0..self.rows {
249 for j in 0..self.cols {
250 result.data[j * self.rows + i] = self.data[i * self.cols + j];
251 }
252 }
253 result
254 }
255
256 pub fn add(&self, other: &Self) -> Result<Self, MatrixError> {
261 if self.rows != other.rows || self.cols != other.cols {
262 return Err(MatrixError::DimensionMismatch {
263 expected: (self.rows, self.cols),
264 got: (other.rows, other.cols),
265 });
266 }
267 let data: Vec<f64> = self
268 .data
269 .iter()
270 .zip(&other.data)
271 .map(|(a, b)| a + b)
272 .collect();
273 Ok(Self {
274 data,
275 rows: self.rows,
276 cols: self.cols,
277 })
278 }
279
280 pub fn sub(&self, other: &Self) -> Result<Self, MatrixError> {
285 if self.rows != other.rows || self.cols != other.cols {
286 return Err(MatrixError::DimensionMismatch {
287 expected: (self.rows, self.cols),
288 got: (other.rows, other.cols),
289 });
290 }
291 let data: Vec<f64> = self
292 .data
293 .iter()
294 .zip(&other.data)
295 .map(|(a, b)| a - b)
296 .collect();
297 Ok(Self {
298 data,
299 rows: self.rows,
300 cols: self.cols,
301 })
302 }
303
304 pub fn scale(&self, c: f64) -> Self {
306 let data: Vec<f64> = self.data.iter().map(|x| c * x).collect();
307 Self {
308 data,
309 rows: self.rows,
310 cols: self.cols,
311 }
312 }
313
314 pub fn mul_mat(&self, other: &Self) -> Result<Self, MatrixError> {
333 if self.cols != other.rows {
334 return Err(MatrixError::DimensionMismatch {
335 expected: (self.rows, self.cols),
336 got: (other.rows, other.cols),
337 });
338 }
339 let mut result = Self::zeros(self.rows, other.cols);
340 for i in 0..self.rows {
342 for k in 0..self.cols {
343 let a_ik = self.data[i * self.cols + k];
344 let row_start = i * other.cols;
345 let other_row_start = k * other.cols;
346 for j in 0..other.cols {
347 result.data[row_start + j] += a_ik * other.data[other_row_start + j];
348 }
349 }
350 }
351 Ok(result)
352 }
353
354 pub fn mul_vec(&self, v: &[f64]) -> Result<Vec<f64>, MatrixError> {
359 if self.cols != v.len() {
360 return Err(MatrixError::DimensionMismatch {
361 expected: (self.rows, self.cols),
362 got: (v.len(), 1),
363 });
364 }
365 let mut result = vec![0.0; self.rows];
366 for (i, res) in result.iter_mut().enumerate() {
367 let row_start = i * self.cols;
368 *res = self.data[row_start..row_start + self.cols]
369 .iter()
370 .zip(v.iter())
371 .map(|(&a, &b)| a * b)
372 .sum();
373 }
374 Ok(result)
375 }
376
377 pub fn frobenius_norm(&self) -> f64 {
379 self.data.iter().map(|x| x * x).sum::<f64>().sqrt()
380 }
381
382 pub fn is_symmetric(&self, tol: f64) -> bool {
384 if self.rows != self.cols {
385 return false;
386 }
387 for i in 0..self.rows {
388 for j in (i + 1)..self.cols {
389 if (self.get(i, j) - self.get(j, i)).abs() > tol {
390 return false;
391 }
392 }
393 }
394 true
395 }
396
397 fn swap_rows(&mut self, a: usize, b: usize) {
398 if a == b {
399 return;
400 }
401 let cols = self.cols;
402 for j in 0..cols {
403 self.data.swap(a * cols + j, b * cols + j);
404 }
405 }
406
407 pub fn determinant(&self) -> Result<f64, MatrixError> {
427 if !self.is_square() {
428 return Err(MatrixError::NotSquare {
429 rows: self.rows,
430 cols: self.cols,
431 });
432 }
433 let n = self.rows;
434 if n == 0 {
435 return Ok(1.0);
436 }
437 if n == 1 {
438 return Ok(self.data[0]);
439 }
440
441 let mut work = self.clone();
442 let mut sign = 1.0_f64;
443 let pivot_tol = 1e-15 * self.frobenius_norm().max(1e-300);
444
445 for k in 0..n {
446 let mut max_val = work.get(k, k).abs();
448 let mut max_row = k;
449 for i in (k + 1)..n {
450 let v = work.get(i, k).abs();
451 if v > max_val {
452 max_val = v;
453 max_row = i;
454 }
455 }
456 if max_val <= pivot_tol {
457 return Ok(0.0); }
459 if max_row != k {
460 work.swap_rows(k, max_row);
461 sign = -sign;
462 }
463
464 let pivot = work.get(k, k);
465 for i in (k + 1)..n {
466 let factor = work.get(i, k) / pivot;
467 for j in (k + 1)..n {
468 let val = work.get(i, j) - factor * work.get(k, j);
469 work.set(i, j, val);
470 }
471 }
472 }
473
474 let mut det = sign;
475 for i in 0..n {
476 det *= work.get(i, i);
477 }
478 Ok(det)
479 }
480
481 pub fn inverse(&self) -> Result<Self, MatrixError> {
501 if !self.is_square() {
502 return Err(MatrixError::NotSquare {
503 rows: self.rows,
504 cols: self.cols,
505 });
506 }
507 let n = self.rows;
508 if n == 0 {
509 return Ok(Self::zeros(0, 0));
510 }
511
512 let n2 = 2 * n;
514 let mut aug = Self::zeros(n, n2);
515 for i in 0..n {
516 for j in 0..n {
517 aug.set(i, j, self.get(i, j));
518 }
519 aug.set(i, n + i, 1.0);
520 }
521
522 let pivot_tol = 1e-14 * self.frobenius_norm().max(1e-300);
523
524 for k in 0..n {
525 let mut max_val = aug.get(k, k).abs();
527 let mut max_row = k;
528 for i in (k + 1)..n {
529 let v = aug.get(i, k).abs();
530 if v > max_val {
531 max_val = v;
532 max_row = i;
533 }
534 }
535 if max_val <= pivot_tol {
536 return Err(MatrixError::Singular);
537 }
538 if max_row != k {
539 aug.swap_rows(k, max_row);
540 }
541
542 let pivot = aug.get(k, k);
544 for j in 0..n2 {
545 aug.set(k, j, aug.get(k, j) / pivot);
546 }
547
548 for i in 0..n {
550 if i != k {
551 let factor = aug.get(i, k);
552 for j in 0..n2 {
553 let val = aug.get(i, j) - factor * aug.get(k, j);
554 aug.set(i, j, val);
555 }
556 }
557 }
558 }
559
560 let mut inv = Self::zeros(n, n);
562 for i in 0..n {
563 for j in 0..n {
564 inv.set(i, j, aug.get(i, n + j));
565 }
566 }
567 Ok(inv)
568 }
569
570 pub fn cholesky(&self) -> Result<Self, MatrixError> {
596 if !self.is_square() {
597 return Err(MatrixError::NotSquare {
598 rows: self.rows,
599 cols: self.cols,
600 });
601 }
602 let n = self.rows;
603 let sym_tol = 1e-10 * self.frobenius_norm().max(1e-300);
604 if !self.is_symmetric(sym_tol) {
605 return Err(MatrixError::NotSymmetric);
606 }
607
608 let mut l = Self::zeros(n, n);
609
610 for j in 0..n {
611 let mut sum = 0.0;
613 for k in 0..j {
614 let ljk = l.get(j, k);
615 sum += ljk * ljk;
616 }
617 let diag = self.get(j, j) - sum;
618 if diag <= 0.0 {
619 return Err(MatrixError::NotPositiveDefinite);
620 }
621 l.set(j, j, diag.sqrt());
622
623 let ljj = l.get(j, j);
625 for i in (j + 1)..n {
626 let mut sum = 0.0;
627 for k in 0..j {
628 sum += l.get(i, k) * l.get(j, k);
629 }
630 l.set(i, j, (self.get(i, j) - sum) / ljj);
631 }
632 }
633
634 Ok(l)
635 }
636
637 pub fn cholesky_solve(&self, b: &[f64]) -> Result<Vec<f64>, MatrixError> {
649 if b.len() != self.rows {
650 return Err(MatrixError::DimensionMismatch {
651 expected: (self.rows, 1),
652 got: (b.len(), 1),
653 });
654 }
655 let l = self.cholesky()?;
656 let y = solve_lower_triangular(&l, b)?;
657 let lt = l.transpose();
658 solve_upper_triangular(<, &y)
659 }
660
661 pub fn eigen_symmetric(&self) -> Result<(Vec<f64>, Matrix), MatrixError> {
704 let n = self.rows;
705 if !self.is_square() {
706 return Err(MatrixError::NotSquare {
707 rows: self.rows,
708 cols: self.cols,
709 });
710 }
711 let sym_tol = 1e-10 * self.frobenius_norm();
713 if !self.is_symmetric(sym_tol) {
714 return Err(MatrixError::NotSymmetric);
715 }
716
717 let mut a = self.data.clone();
719 let mut v = vec![0.0; n * n];
721 for i in 0..n {
722 v[i * n + i] = 1.0;
723 }
724
725 let max_sweeps = 100;
726 let scale = self.frobenius_norm();
730 if scale == 0.0 {
731 return Ok((vec![0.0; n], Matrix::identity(n)));
732 }
733 let tol = 1e-15 * scale;
734
735 for _ in 0..max_sweeps {
736 let mut off_norm = 0.0;
738 for i in 0..n {
739 for j in (i + 1)..n {
740 off_norm += 2.0 * a[i * n + j] * a[i * n + j];
741 }
742 }
743 off_norm = off_norm.sqrt();
744
745 if off_norm < tol {
746 break;
747 }
748
749 for p in 0..n {
751 for q in (p + 1)..n {
752 let apq = a[p * n + q];
753 if apq.abs() < tol * 0.01 {
754 continue;
755 }
756
757 let app = a[p * n + p];
758 let aqq = a[q * n + q];
759 let diff = aqq - app;
760
761 let (cos, sin) = if diff.abs() < 1e-300 {
763 let s = std::f64::consts::FRAC_1_SQRT_2;
765 (s, if apq > 0.0 { s } else { -s })
766 } else {
767 let tau = diff / (2.0 * apq);
768 let t = if tau >= 0.0 {
770 1.0 / (tau + (1.0 + tau * tau).sqrt())
771 } else {
772 -1.0 / (-tau + (1.0 + tau * tau).sqrt())
773 };
774 let c = 1.0 / (1.0 + t * t).sqrt();
775 let s = t * c;
776 (c, s)
777 };
778
779 a[p * n + p] -=
781 2.0 * sin * cos * apq + sin * sin * (a[q * n + q] - a[p * n + p]);
782 a[q * n + q] += 2.0 * sin * cos * apq + sin * sin * (aqq - app); a[p * n + q] = 0.0;
784 a[q * n + p] = 0.0;
785
786 a[p * n + p] = app;
790 a[q * n + q] = aqq;
791 a[p * n + q] = apq;
792 a[q * n + p] = apq;
793
794 for r in 0..n {
797 if r == p || r == q {
798 continue;
799 }
800 let arp = a[r * n + p];
801 let arq = a[r * n + q];
802 a[r * n + p] = cos * arp - sin * arq;
803 a[r * n + q] = sin * arp + cos * arq;
804 a[p * n + r] = a[r * n + p]; a[q * n + r] = a[r * n + q]; }
807
808 let new_pp = cos * cos * app - 2.0 * sin * cos * apq + sin * sin * aqq;
810 let new_qq = sin * sin * app + 2.0 * sin * cos * apq + cos * cos * aqq;
811 a[p * n + p] = new_pp;
812 a[q * n + q] = new_qq;
813 a[p * n + q] = 0.0;
814 a[q * n + p] = 0.0;
815
816 for r in 0..n {
818 let vp = v[r * n + p];
819 let vq = v[r * n + q];
820 v[r * n + p] = cos * vp - sin * vq;
821 v[r * n + q] = sin * vp + cos * vq;
822 }
823 }
824 }
825 }
826
827 let mut eigen_pairs: Vec<(f64, Vec<f64>)> = (0..n)
829 .map(|i| {
830 let eigenvalue = a[i * n + i];
831 let eigenvector: Vec<f64> = (0..n).map(|r| v[r * n + i]).collect();
832 (eigenvalue, eigenvector)
833 })
834 .collect();
835
836 eigen_pairs.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
838
839 let eigenvalues: Vec<f64> = eigen_pairs.iter().map(|(val, _)| *val).collect();
840 let mut eigvec_data = vec![0.0; n * n];
841 for (col, (_, vec)) in eigen_pairs.iter().enumerate() {
842 for (row, &val) in vec.iter().enumerate() {
843 eigvec_data[row * n + col] = val;
844 }
845 }
846 let eigenvectors = Matrix {
847 data: eigvec_data,
848 rows: n,
849 cols: n,
850 };
851
852 Ok((eigenvalues, eigenvectors))
853 }
854}
855
856fn solve_lower_triangular(l: &Matrix, b: &[f64]) -> Result<Vec<f64>, MatrixError> {
858 let n = l.rows();
859 let mut x = vec![0.0; n];
860 for i in 0..n {
861 let mut sum = 0.0;
862 for (j, &xj) in x[..i].iter().enumerate() {
863 sum += l.get(i, j) * xj;
864 }
865 let diag = l.get(i, i);
866 if diag.abs() < 1e-300 {
867 return Err(MatrixError::Singular);
868 }
869 x[i] = (b[i] - sum) / diag;
870 }
871 Ok(x)
872}
873
874fn solve_upper_triangular(u: &Matrix, b: &[f64]) -> Result<Vec<f64>, MatrixError> {
876 let n = u.rows();
877 let mut x = vec![0.0; n];
878 for i in (0..n).rev() {
879 let mut sum = 0.0;
880 for (off, &xj) in x[i + 1..].iter().enumerate() {
881 sum += u.get(i, i + 1 + off) * xj;
882 }
883 let diag = u.get(i, i);
884 if diag.abs() < 1e-300 {
885 return Err(MatrixError::Singular);
886 }
887 x[i] = (b[i] - sum) / diag;
888 }
889 Ok(x)
890}
891
892impl std::fmt::Display for Matrix {
897 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
898 for i in 0..self.rows {
899 write!(f, "[")?;
900 for j in 0..self.cols {
901 if j > 0 {
902 write!(f, ", ")?;
903 }
904 write!(f, "{:>10.4}", self.get(i, j))?;
905 }
906 writeln!(f, "]")?;
907 }
908 Ok(())
909 }
910}
911
912#[cfg(test)]
917mod tests {
918 use super::*;
919
920 #[test]
923 fn test_new_valid() {
924 let m = Matrix::new(2, 3, vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]).unwrap();
925 assert_eq!(m.rows(), 2);
926 assert_eq!(m.cols(), 3);
927 assert_eq!(m.get(0, 0), 1.0);
928 assert_eq!(m.get(1, 2), 6.0);
929 }
930
931 #[test]
932 fn test_new_invalid_length() {
933 assert!(Matrix::new(2, 3, vec![1.0, 2.0]).is_err());
934 }
935
936 #[test]
937 fn test_from_rows() {
938 let m = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
939 assert_eq!(m.get(0, 0), 1.0);
940 assert_eq!(m.get(1, 1), 4.0);
941 }
942
943 #[test]
944 fn test_zeros() {
945 let m = Matrix::zeros(3, 4);
946 assert_eq!(m.rows(), 3);
947 assert_eq!(m.cols(), 4);
948 assert_eq!(m.get(2, 3), 0.0);
949 }
950
951 #[test]
952 fn test_identity() {
953 let eye = Matrix::identity(3);
954 assert_eq!(eye.get(0, 0), 1.0);
955 assert_eq!(eye.get(1, 1), 1.0);
956 assert_eq!(eye.get(2, 2), 1.0);
957 assert_eq!(eye.get(0, 1), 0.0);
958 assert_eq!(eye.get(1, 2), 0.0);
959 }
960
961 #[test]
962 fn test_diag() {
963 let m = Matrix::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0]]);
964 assert_eq!(m.diag(), vec![1.0, 5.0, 9.0]);
965 }
966
967 #[test]
970 fn test_transpose() {
971 let m = Matrix::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
972 let t = m.transpose();
973 assert_eq!(t.rows(), 3);
974 assert_eq!(t.cols(), 2);
975 assert_eq!(t.get(0, 0), 1.0);
976 assert_eq!(t.get(2, 1), 6.0);
977 }
978
979 #[test]
980 fn test_transpose_twice() {
981 let m = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0], &[5.0, 6.0]]);
982 let tt = m.transpose().transpose();
983 assert_eq!(m, tt);
984 }
985
986 #[test]
987 fn test_add() {
988 let a = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
989 let b = Matrix::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
990 let c = a.add(&b).unwrap();
991 assert_eq!(c.get(0, 0), 6.0);
992 assert_eq!(c.get(1, 1), 12.0);
993 }
994
995 #[test]
996 fn test_add_dimension_mismatch() {
997 let a = Matrix::zeros(2, 3);
998 let b = Matrix::zeros(3, 2);
999 assert!(a.add(&b).is_err());
1000 }
1001
1002 #[test]
1003 fn test_sub() {
1004 let a = Matrix::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
1005 let b = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
1006 let c = a.sub(&b).unwrap();
1007 assert_eq!(c.get(0, 0), 4.0);
1008 assert_eq!(c.get(1, 1), 4.0);
1009 }
1010
1011 #[test]
1012 fn test_scale() {
1013 let m = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
1014 let s = m.scale(2.0);
1015 assert_eq!(s.get(0, 0), 2.0);
1016 assert_eq!(s.get(1, 1), 8.0);
1017 }
1018
1019 #[test]
1022 fn test_mul_identity() {
1023 let a = Matrix::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0], &[7.0, 8.0, 9.0]]);
1024 let eye = Matrix::identity(3);
1025 let result = a.mul_mat(&eye).unwrap();
1026 assert_eq!(a, result);
1027 }
1028
1029 #[test]
1030 fn test_mul_2x2() {
1031 let a = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
1032 let b = Matrix::from_rows(&[&[5.0, 6.0], &[7.0, 8.0]]);
1033 let c = a.mul_mat(&b).unwrap();
1034 assert_eq!(c.get(0, 0), 19.0);
1037 assert_eq!(c.get(0, 1), 22.0);
1038 assert_eq!(c.get(1, 0), 43.0);
1039 assert_eq!(c.get(1, 1), 50.0);
1040 }
1041
1042 #[test]
1043 fn test_mul_nonsquare() {
1044 let a = Matrix::from_rows(&[&[1.0, 2.0, 3.0], &[4.0, 5.0, 6.0]]);
1045 let b = Matrix::from_rows(&[&[7.0, 8.0], &[9.0, 10.0], &[11.0, 12.0]]);
1046 let c = a.mul_mat(&b).unwrap();
1047 assert_eq!(c.rows(), 2);
1048 assert_eq!(c.cols(), 2);
1049 assert_eq!(c.get(0, 0), 58.0);
1051 assert_eq!(c.get(0, 1), 64.0);
1052 }
1053
1054 #[test]
1055 fn test_mul_vec() {
1056 let a = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
1057 let v = vec![5.0, 6.0];
1058 let result = a.mul_vec(&v).unwrap();
1059 assert_eq!(result, vec![17.0, 39.0]);
1060 }
1061
1062 #[test]
1063 fn test_mul_dimension_mismatch() {
1064 let a = Matrix::zeros(2, 3);
1065 let b = Matrix::zeros(2, 3);
1066 assert!(a.mul_mat(&b).is_err());
1067 }
1068
1069 #[test]
1072 fn test_det_2x2() {
1073 let m = Matrix::from_rows(&[&[2.0, 3.0], &[1.0, 4.0]]);
1074 assert!((m.determinant().unwrap() - 5.0).abs() < 1e-10);
1075 }
1076
1077 #[test]
1078 fn test_det_3x3() {
1079 let m = Matrix::from_rows(&[&[6.0, 1.0, 1.0], &[4.0, -2.0, 5.0], &[2.0, 8.0, 7.0]]);
1080 assert!((m.determinant().unwrap() - (-306.0)).abs() < 1e-8);
1085 }
1086
1087 #[test]
1088 fn test_det_identity() {
1089 let eye = Matrix::identity(4);
1090 assert!((eye.determinant().unwrap() - 1.0).abs() < 1e-10);
1091 }
1092
1093 #[test]
1094 fn test_det_singular() {
1095 let m = Matrix::from_rows(&[&[1.0, 2.0], &[2.0, 4.0]]);
1096 assert!(m.determinant().unwrap().abs() < 1e-10);
1097 }
1098
1099 #[test]
1100 fn test_det_not_square() {
1101 let m = Matrix::zeros(2, 3);
1102 assert!(m.determinant().is_err());
1103 }
1104
1105 #[test]
1108 fn test_inverse_2x2() {
1109 let a = Matrix::from_rows(&[&[4.0, 7.0], &[2.0, 6.0]]);
1110 let inv = a.inverse().unwrap();
1111 let eye = a.mul_mat(&inv).unwrap();
1112 for i in 0..2 {
1113 for j in 0..2 {
1114 let expected = if i == j { 1.0 } else { 0.0 };
1115 assert!(
1116 (eye.get(i, j) - expected).abs() < 1e-10,
1117 "A·A⁻¹[{i},{j}] = {}, expected {expected}",
1118 eye.get(i, j)
1119 );
1120 }
1121 }
1122 }
1123
1124 #[test]
1125 fn test_inverse_3x3() {
1126 let a = Matrix::from_rows(&[&[1.0, 2.0, 3.0], &[0.0, 1.0, 4.0], &[5.0, 6.0, 0.0]]);
1127 let inv = a.inverse().unwrap();
1128 let eye = a.mul_mat(&inv).unwrap();
1129 for i in 0..3 {
1130 for j in 0..3 {
1131 let expected = if i == j { 1.0 } else { 0.0 };
1132 assert!(
1133 (eye.get(i, j) - expected).abs() < 1e-10,
1134 "A·A⁻¹[{i},{j}] = {}",
1135 eye.get(i, j)
1136 );
1137 }
1138 }
1139 }
1140
1141 #[test]
1142 fn test_inverse_identity() {
1143 let eye = Matrix::identity(4);
1144 let inv = eye.inverse().unwrap();
1145 assert_eq!(eye, inv);
1146 }
1147
1148 #[test]
1149 fn test_inverse_singular() {
1150 let m = Matrix::from_rows(&[&[1.0, 2.0], &[2.0, 4.0]]);
1151 assert!(m.inverse().is_err());
1152 }
1153
1154 #[test]
1157 fn test_cholesky_2x2() {
1158 let a = Matrix::from_rows(&[&[4.0, 2.0], &[2.0, 3.0]]);
1159 let l = a.cholesky().unwrap();
1160 assert!(l.get(0, 1).abs() < 1e-15);
1162 let llt = l.mul_mat(&l.transpose()).unwrap();
1164 for i in 0..2 {
1165 for j in 0..2 {
1166 assert!(
1167 (llt.get(i, j) - a.get(i, j)).abs() < 1e-10,
1168 "LLᵀ[{i},{j}] = {}, expected {}",
1169 llt.get(i, j),
1170 a.get(i, j)
1171 );
1172 }
1173 }
1174 }
1175
1176 #[test]
1177 fn test_cholesky_3x3() {
1178 let a = Matrix::from_rows(&[&[25.0, 15.0, -5.0], &[15.0, 18.0, 0.0], &[-5.0, 0.0, 11.0]]);
1179 let l = a.cholesky().unwrap();
1180 let llt = l.mul_mat(&l.transpose()).unwrap();
1181 for i in 0..3 {
1182 for j in 0..3 {
1183 assert!(
1184 (llt.get(i, j) - a.get(i, j)).abs() < 1e-10,
1185 "LLᵀ[{i},{j}] = {}, A[{i},{j}] = {}",
1186 llt.get(i, j),
1187 a.get(i, j)
1188 );
1189 }
1190 }
1191 }
1192
1193 #[test]
1194 fn test_cholesky_identity() {
1195 let eye = Matrix::identity(3);
1196 let l = eye.cholesky().unwrap();
1197 assert_eq!(l, eye);
1198 }
1199
1200 #[test]
1201 fn test_cholesky_not_positive_definite() {
1202 let a = Matrix::from_rows(&[&[1.0, 2.0], &[2.0, 1.0]]);
1203 assert!(matches!(
1204 a.cholesky(),
1205 Err(MatrixError::NotPositiveDefinite)
1206 ));
1207 }
1208
1209 #[test]
1210 fn test_cholesky_not_symmetric() {
1211 let a = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
1212 assert!(matches!(a.cholesky(), Err(MatrixError::NotSymmetric)));
1213 }
1214
1215 #[test]
1218 fn test_cholesky_solve() {
1219 let a = Matrix::from_rows(&[&[4.0, 2.0], &[2.0, 3.0]]);
1222 let b = vec![1.0, 2.0];
1223 let x = a.cholesky_solve(&b).unwrap();
1224 let ax = a.mul_vec(&x).unwrap();
1226 for i in 0..2 {
1227 assert!(
1228 (ax[i] - b[i]).abs() < 1e-10,
1229 "Ax[{i}] = {}, b[{i}] = {}",
1230 ax[i],
1231 b[i]
1232 );
1233 }
1234 }
1235
1236 #[test]
1237 fn test_cholesky_solve_3x3() {
1238 let a = Matrix::from_rows(&[&[25.0, 15.0, -5.0], &[15.0, 18.0, 0.0], &[-5.0, 0.0, 11.0]]);
1239 let b = vec![35.0, 33.0, 6.0];
1240 let x = a.cholesky_solve(&b).unwrap();
1241 let ax = a.mul_vec(&x).unwrap();
1242 for i in 0..3 {
1243 assert!((ax[i] - b[i]).abs() < 1e-10);
1244 }
1245 }
1246
1247 #[test]
1250 fn test_frobenius_norm() {
1251 let m = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
1252 assert!((m.frobenius_norm() - 30.0_f64.sqrt()).abs() < 1e-10);
1254 }
1255
1256 #[test]
1259 fn test_is_symmetric() {
1260 let sym = Matrix::from_rows(&[&[1.0, 2.0, 3.0], &[2.0, 5.0, 6.0], &[3.0, 6.0, 9.0]]);
1261 assert!(sym.is_symmetric(1e-10));
1262
1263 let asym = Matrix::from_rows(&[&[1.0, 2.0], &[3.0, 4.0]]);
1264 assert!(!asym.is_symmetric(1e-10));
1265 }
1266}
1267
1268#[cfg(test)]
1269mod proptests {
1270 use super::*;
1271 use proptest::prelude::*;
1272
1273 fn square_matrix(n: usize) -> impl Strategy<Value = Matrix> {
1274 proptest::collection::vec(-10.0_f64..10.0, n * n)
1275 .prop_map(move |data| Matrix::new(n, n, data).expect("valid dimensions"))
1276 }
1277
1278 fn spd_matrix(n: usize) -> impl Strategy<Value = Matrix> {
1279 proptest::collection::vec(-5.0_f64..5.0, n * n).prop_map(move |data| {
1281 let a = Matrix::new(n, n, data).expect("valid dimensions");
1282 let ata = a.transpose().mul_mat(&a).expect("compatible");
1283 let eye_scaled = Matrix::identity(n).scale(n as f64);
1284 ata.add(&eye_scaled).expect("compatible")
1285 })
1286 }
1287
1288 proptest! {
1289 #![proptest_config(ProptestConfig::with_cases(200))]
1290
1291 #[test]
1292 fn transpose_involution(m in square_matrix(3)) {
1293 let m_tt = m.transpose().transpose();
1294 for i in 0..3 {
1295 for j in 0..3 {
1296 prop_assert!((m.get(i, j) - m_tt.get(i, j)).abs() < 1e-14);
1297 }
1298 }
1299 }
1300
1301 #[test]
1302 fn mul_identity_is_identity(m in square_matrix(3)) {
1303 let eye = Matrix::identity(3);
1304 let me = m.mul_mat(&eye).unwrap();
1305 let em = eye.mul_mat(&m).unwrap();
1306 for i in 0..3 {
1307 for j in 0..3 {
1308 prop_assert!((me.get(i, j) - m.get(i, j)).abs() < 1e-10);
1309 prop_assert!((em.get(i, j) - m.get(i, j)).abs() < 1e-10);
1310 }
1311 }
1312 }
1313
1314 #[test]
1315 fn det_of_product(a in square_matrix(3), b in square_matrix(3)) {
1316 let det_a = a.determinant().unwrap();
1318 let det_b = b.determinant().unwrap();
1319 let ab = a.mul_mat(&b).unwrap();
1320 let det_ab = ab.determinant().unwrap();
1321 let expected = det_a * det_b;
1322 let tol = 1e-6 * expected.abs().max(det_ab.abs()).max(1.0);
1324 prop_assert!(
1325 (det_ab - expected).abs() < tol,
1326 "det(AB)={det_ab}, det(A)*det(B)={expected}"
1327 );
1328 }
1329
1330 #[test]
1331 fn cholesky_roundtrip(a in spd_matrix(3)) {
1332 let l = a.cholesky().expect("SPD should decompose");
1333 let llt = l.mul_mat(&l.transpose()).expect("compatible");
1334 for i in 0..3 {
1335 for j in 0..3 {
1336 let diff = (llt.get(i, j) - a.get(i, j)).abs();
1337 let tol = 1e-8 * a.get(i, j).abs().max(1.0);
1338 prop_assert!(
1339 diff < tol,
1340 "LLᵀ[{i},{j}]={}, A[{i},{j}]={}",
1341 llt.get(i, j), a.get(i, j)
1342 );
1343 }
1344 }
1345 }
1346
1347 #[test]
1348 fn cholesky_solve_roundtrip(a in spd_matrix(3), b in proptest::collection::vec(-10.0_f64..10.0, 3)) {
1349 let x = a.cholesky_solve(&b).expect("SPD solve should work");
1350 let ax = a.mul_vec(&x).expect("compatible");
1351 for i in 0..3 {
1352 let tol = 1e-8 * b[i].abs().max(1.0);
1353 prop_assert!(
1354 (ax[i] - b[i]).abs() < tol,
1355 "Ax[{i}]={}, b[{i}]={}",
1356 ax[i], b[i]
1357 );
1358 }
1359 }
1360
1361 #[test]
1362 fn inverse_roundtrip(a in spd_matrix(3)) {
1363 let inv = a.inverse().expect("SPD invertible");
1365 let eye = a.mul_mat(&inv).expect("compatible");
1366 for i in 0..3 {
1367 for j in 0..3 {
1368 let expected = if i == j { 1.0 } else { 0.0 };
1369 let diff = (eye.get(i, j) - expected).abs();
1370 prop_assert!(
1371 diff < 1e-6,
1372 "A·A⁻¹[{i},{j}]={}, expected {expected}",
1373 eye.get(i, j)
1374 );
1375 }
1376 }
1377 }
1378 }
1379
1380 #[test]
1381 fn eigen_symmetric_does_not_depend_on_the_scale_of_the_matrix() {
1382 let base = Matrix::from_rows(&[&[4.0, 1.0, 0.5], &[1.0, 3.0, 0.25], &[0.5, 0.25, 2.0]]);
1383 let (want, _) = base.eigen_symmetric().expect("symmetric");
1384 for scale in [1e-3, 1e-12, 1e-18, 1e-24, 1e12] {
1385 let scaled = Matrix::from_rows(&[
1386 &[4.0 * scale, 1.0 * scale, 0.5 * scale],
1387 &[1.0 * scale, 3.0 * scale, 0.25 * scale],
1388 &[0.5 * scale, 0.25 * scale, 2.0 * scale],
1389 ]);
1390 let (got, vectors) = scaled.eigen_symmetric().expect("symmetric");
1391 for (g, w) in got.iter().zip(&want) {
1392 assert!((g / scale - w).abs() < 1e-12, "scale {scale}: {g} vs {w}");
1393 }
1394 let dot: f64 = (0..3).map(|r| vectors.get(r, 0) * vectors.get(r, 1)).sum();
1396 assert!(dot.abs() < 1e-10, "scale {scale}: dot {dot}");
1397 }
1398 let (zeros, vectors) = Matrix::new(2, 2, vec![0.0; 4])
1400 .expect("2x2")
1401 .eigen_symmetric()
1402 .expect("symmetric");
1403 assert_eq!(zeros, vec![0.0, 0.0]);
1404 assert_eq!(vectors.get(0, 0), 1.0);
1405 }
1406}