1#![forbid(unsafe_code)]
2
3use core::hint::cold_path;
11
12use crate::matrix::Matrix;
13use crate::scaled_product::{ScaledProduct, range_checked_product};
14use crate::vector::Vector;
15use crate::{ArithmeticOperation, FactorizationKind, LaError, Tolerance};
16
17#[must_use]
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct Lu<const D: usize> {
26 factors: LuFactors<D>,
27 permutation: RowPermutation<D>,
28}
29
30#[derive(Clone, Copy, Debug, PartialEq)]
35struct LuFactors<const D: usize> {
36 storage: [[f64; D]; D],
37}
38
39impl<const D: usize> LuFactors<D> {
40 #[inline]
42 const fn try_from_computation(storage: [[f64; D]; D]) -> Result<Self, LaError> {
43 let mut row = 0;
44 while row < D {
45 let mut col = 0;
46 while col < D {
47 if !storage[row][col].is_finite() {
48 return Err(LaError::non_finite_computation_matrix(
49 ArithmeticOperation::LuFactorization,
50 row,
51 col,
52 ));
53 }
54 col += 1;
55 }
56 row += 1;
57 }
58
59 Ok(Self { storage })
60 }
61
62 #[inline]
64 #[must_use]
65 const fn row(&self, index: usize) -> &[f64; D] {
66 &self.storage[index]
67 }
68
69 #[inline]
71 #[must_use]
72 const fn diag(&self, index: usize) -> f64 {
73 self.storage[index][index]
74 }
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq)]
83struct RowPermutation<const D: usize> {
84 source_rows: [usize; D],
85 odd: bool,
86}
87
88impl<const D: usize> RowPermutation<D> {
89 const fn identity() -> Self {
91 let mut source_rows = [0; D];
92 let mut row = 0;
93 while row < D {
94 source_rows[row] = row;
95 row += 1;
96 }
97 Self {
98 source_rows,
99 odd: false,
100 }
101 }
102
103 const fn swap(&mut self, left: usize, right: usize) {
105 if left != right {
106 let source_row = self.source_rows[left];
107 self.source_rows[left] = self.source_rows[right];
108 self.source_rows[right] = source_row;
109 self.odd = !self.odd;
110 }
111 }
112
113 const fn source_row(&self, row: usize) -> usize {
115 self.source_rows[row]
116 }
117
118 const fn is_odd(&self) -> bool {
120 self.odd
121 }
122}
123
124impl<const D: usize> Lu<D> {
125 #[inline]
134 pub(crate) fn factor_finite(a: Matrix<D>, tol: Tolerance) -> Result<Self, LaError> {
135 let mut rows = a.into_rows();
136 let tolerance = tol.get();
137 let mut permutation = RowPermutation::identity();
138
139 {
140 let rows = &mut rows;
141
142 for k in 0..D {
143 let mut pivot_row = k;
145 let mut pivot_abs = rows[k][k].abs();
146
147 #[expect(
148 clippy::needless_range_loop,
149 reason = "the row index identifies the pivot later used for synchronized matrix and permutation swaps"
150 )]
151 for r in (k + 1)..D {
152 let v = rows[r][k].abs();
153 if v > pivot_abs {
154 pivot_abs = v;
155 pivot_row = r;
156 }
157 }
158
159 if pivot_abs <= tolerance {
160 cold_path();
161
162 for (row, values) in rows.iter().enumerate() {
166 for (col, value) in values.iter().enumerate() {
167 if !value.is_finite() {
168 return Err(LaError::non_finite_computation_matrix(
169 ArithmeticOperation::LuFactorization,
170 row,
171 col,
172 ));
173 }
174 }
175 }
176
177 return Err(LaError::singular_numerical(
178 k,
179 FactorizationKind::Lu,
180 pivot_abs,
181 tolerance,
182 ));
183 }
184
185 if pivot_row != k {
186 rows.swap(k, pivot_row);
187 permutation.swap(k, pivot_row);
188 }
189
190 let pivot = rows[k][k];
191
192 for r in (k + 1)..D {
194 let mult = rows[r][k] / pivot;
195 rows[r][k] = mult;
196
197 #[expect(
198 clippy::needless_range_loop,
199 reason = "the column index pairs pivot-row reads with eliminated-row writes in the in-place update"
200 )]
201 for c in (k + 1)..D {
202 let updated = (-mult).mul_add(rows[k][c], rows[r][c]);
203 rows[r][c] = updated;
204 }
205 }
206 }
207 }
208
209 let factors = LuFactors::try_from_computation(rows)?;
210
211 Ok(Self {
212 factors,
213 permutation,
214 })
215 }
216
217 #[inline]
245 pub const fn solve(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
246 let mut x = [0.0; D];
247 let b = b.as_array();
248 let mut i = 0;
249
250 if D <= 4 {
251 while i < D {
252 x[i] = b[self.permutation.source_row(i)];
253 i += 1;
254 }
255
256 i = 0;
259 while i < D {
260 let mut sum = x[i];
261 let row = self.factors.row(i);
262 let mut j = 0;
263 while j < i {
264 sum = (-row[j]).mul_add(x[j], sum);
265 j += 1;
266 }
267 if !sum.is_finite() {
268 cold_path();
269 return Err(LaError::non_finite_computation_step(
270 ArithmeticOperation::LuSolve,
271 i,
272 ));
273 }
274 x[i] = sum;
275 i += 1;
276 }
277 } else {
278 while i < D {
281 let mut sum = b[self.permutation.source_row(i)];
282 let row = self.factors.row(i);
283 let mut j = 0;
284 while j < i {
285 sum = (-row[j]).mul_add(x[j], sum);
286 j += 1;
287 }
288 if !sum.is_finite() {
289 cold_path();
290 return Err(LaError::non_finite_computation_step(
291 ArithmeticOperation::LuSolve,
292 i,
293 ));
294 }
295 x[i] = sum;
296 i += 1;
297 }
298 }
299
300 let mut ii = 0;
302 while ii < D {
303 let i = D - 1 - ii;
304 let mut sum = x[i];
305 let row = self.factors.row(i);
306 let mut j = i + 1;
307 while j < D {
308 sum = (-row[j]).mul_add(x[j], sum);
309 j += 1;
310 }
311
312 let diag = row[i];
313 if !sum.is_finite() {
314 cold_path();
315 return Err(LaError::non_finite_computation_step(
316 ArithmeticOperation::LuSolve,
317 i,
318 ));
319 }
320
321 let quotient = sum / diag;
322 if !quotient.is_finite() {
323 cold_path();
324 return Err(LaError::non_finite_computation_step(
325 ArithmeticOperation::LuSolve,
326 i,
327 ));
328 }
329 x[i] = quotient;
330 ii += 1;
331 }
332
333 Vector::from_computation(x, ArithmeticOperation::LuSolve)
334 }
335
336 #[inline]
364 pub const fn det(&self) -> Result<f64, LaError> {
365 let mut det = if self.permutation.is_odd() { -1.0 } else { 1.0 };
366 let mut i = 0;
367
368 if D <= 4 {
369 while i < D {
372 let step = range_checked_product(det, self.factors.diag(i));
373 if !step.range_preserved() {
374 cold_path();
375 return self.scaled_det();
376 }
377 det = step.product();
378 i += 1;
379 }
380 return Ok(det);
381 }
382
383 let mut range_preserved = true;
384 while i < D {
385 let factor = self.factors.diag(i);
386 let step = range_checked_product(det, factor);
387 det = step.product();
388 range_preserved &= step.range_preserved();
389 i += 1;
390 }
391 if range_preserved {
392 Ok(det)
393 } else {
394 cold_path();
395 self.scaled_det()
396 }
397 }
398
399 #[cold]
401 const fn scaled_det(&self) -> Result<f64, LaError> {
402 let mut product = ScaledProduct::new(self.permutation.is_odd());
403 let mut i = 0;
404 while i < D {
405 product.multiply(self.factors.diag(i));
406 i += 1;
407 }
408
409 if let Some(det) = product.finish() {
410 Ok(det)
411 } else {
412 Err(LaError::non_finite_computation_step(
413 ArithmeticOperation::Determinant,
414 D.saturating_sub(1),
415 ))
416 }
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use core::array::from_fn;
423 use core::hint::black_box;
424
425 use approx::assert_abs_diff_eq;
426 use pastey::paste;
427
428 use super::*;
429 use crate::DEFAULT_SINGULAR_TOL;
430
431 const TWO_NEG_800: f64 = f64::from_bits(223_u64 << 52);
432
433 fn assert_triangular_solution<const D: usize>(
435 rows: [[f64; D]; D],
436 rhs: [f64; D],
437 expected: [u64; D],
438 label: &str,
439 ) {
440 for rotation in [0, 1, D - 1] {
441 let mut rows = rows;
442 let mut rhs = rhs;
443 rows.rotate_left(rotation);
444 rhs.rotate_left(rotation);
445 let actual = Matrix::try_from_rows(rows)
446 .unwrap()
447 .lu(DEFAULT_SINGULAR_TOL)
448 .unwrap()
449 .solve(Vector::try_new(rhs).unwrap());
450 assert_eq!(
451 actual.map(|solution| solution.into_array().map(f64::to_bits)),
452 Ok(expected),
453 "{label}, D={D}, rotation={rotation}",
454 );
455 }
456 }
457
458 fn assert_solve_arithmetic_order<const D: usize>() {
459 for (exponent, cancelled_bits) in [
465 (54_u64, 0x800f_ffff_ffff_ffff),
466 (1023, 0xbc9f_ffff_ffff_fffe),
467 (1993, 0xf93f_ffff_ffff_fffe),
468 ] {
469 let scale = f64::from_bits(exponent << 52);
470 let perturbed = f64::from_bits((exponent << 52) | 1);
471 for forward in [false, true] {
472 let (row, col) = if forward { (D - 1, 0) } else { (0, D - 1) };
473 let mut rows = Matrix::<D>::identity().into_rows();
474 rows[row][col] = f64::from_bits(0x3fef_ffff_ffff_ffff);
475 let mut rhs = [0.0; D];
476 rhs[row] = scale;
477 rhs[col] = perturbed;
478 let mut expected = [0; D];
479 expected[row] = cancelled_bits;
480 expected[col] = perturbed.to_bits();
481 assert_triangular_solution(
482 rows,
483 rhs,
484 expected,
485 &format!("fused cancellation, forward={forward}, exponent={exponent}"),
486 );
487 }
488 }
489
490 if D >= 3 {
491 for forward in [false, true] {
494 let (row, first, second) = if forward {
495 (D - 1, 0, 1)
496 } else {
497 (0, 1, D - 1)
498 };
499 let mut rows = Matrix::<D>::identity().into_rows();
500 rows[row][first] = 0.5;
501 rows[row][second] = 0.5;
502 let mut rhs = [0.0; D];
503 rhs[row] = 1.0;
504 rhs[first] = f64::from_bits(1077_u64 << 52); rhs[second] = -rhs[first];
506 assert_triangular_solution(
507 rows,
508 rhs,
509 rhs.map(f64::to_bits),
510 &format!("ascending columns, forward={forward}"),
511 );
512 }
513 }
514 }
515
516 macro_rules! gen_solve_order_tests {
517 ($d:literal) => {
518 paste! {
519 #[test]
520 fn [<solve_preserves_row_arithmetic_ $d d>]() {
521 assert_solve_arithmetic_order::<$d>();
522 }
523 }
524 };
525 }
526
527 gen_solve_order_tests!(2);
528 gen_solve_order_tests!(3);
529 gen_solve_order_tests!(4);
530 gen_solve_order_tests!(5);
531 gen_solve_order_tests!(8);
532 gen_solve_order_tests!(16);
533 gen_solve_order_tests!(32);
534 gen_solve_order_tests!(64);
535 const TWO_POS_800: f64 = f64::from_bits(1823_u64 << 52);
536
537 #[test]
538 fn row_permutation_keeps_mapping_and_parity_synchronized() {
539 let mut permutation = RowPermutation::<4>::identity();
540 assert_eq!(from_fn(|row| permutation.source_row(row)), [0, 1, 2, 3]);
541 assert!(!permutation.is_odd());
542
543 permutation.swap(0, 3);
544 assert_eq!(from_fn(|row| permutation.source_row(row)), [3, 1, 2, 0]);
545 assert!(permutation.is_odd());
546
547 permutation.swap(1, 2);
548 assert_eq!(from_fn(|row| permutation.source_row(row)), [3, 2, 1, 0]);
549 assert!(!permutation.is_odd());
550 }
551
552 macro_rules! gen_pivoting_solve_and_det_tests {
553 ($d:literal) => {
554 paste! {
555 #[test]
556 fn [<lu_solve_pivoting_ $d d>]() {
557 let mut rows = [[0.0f64; $d]; $d];
563 for i in 0..$d {
564 rows[i][i] = 1.0;
565 }
566 rows.swap(0, 1);
567
568 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
569 let lu_fn: fn(Matrix<$d>, Tolerance) -> Result<Lu<$d>, LaError> =
570 black_box(Matrix::<$d>::lu);
571 let lu = lu_fn(a, DEFAULT_SINGULAR_TOL).unwrap();
572
573 let b_arr = {
575 let mut arr = [0.0f64; $d];
576 let mut val = 1.0f64;
577 for dst in arr.iter_mut() {
578 *dst = val;
579 val += 1.0;
580 }
581 arr
582 };
583 let mut expected = b_arr;
584 expected.swap(0, 1);
585 let b = Vector::<$d>::new(black_box(b_arr));
586
587 let solve_fn: fn(&Lu<$d>, Vector<$d>) -> Result<Vector<$d>, LaError> =
588 black_box(Lu::<$d>::solve);
589 let x = solve_fn(&lu, b).unwrap().into_array();
590
591 for i in 0..$d {
592 assert_abs_diff_eq!(x[i], expected[i], epsilon = 1e-12);
593 }
594 }
595
596 #[test]
597 fn [<lu_det_pivoting_ $d d>]() {
598 let mut rows = [[0.0f64; $d]; $d];
603 for i in 0..$d {
604 rows[i][i] = 1.0;
605 }
606 rows.swap(0, 1);
607
608 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
609 let lu_fn: fn(Matrix<$d>, Tolerance) -> Result<Lu<$d>, LaError> =
610 black_box(Matrix::<$d>::lu);
611 let lu = lu_fn(a, DEFAULT_SINGULAR_TOL).unwrap();
612
613 let det_fn: fn(&Lu<$d>) -> Result<f64, LaError> =
615 black_box(Lu::<$d>::det);
616 assert_abs_diff_eq!(det_fn(&lu).unwrap(), -1.0, epsilon = 1e-12);
617 }
618 }
619 };
620 }
621
622 gen_pivoting_solve_and_det_tests!(2);
623 gen_pivoting_solve_and_det_tests!(3);
624 gen_pivoting_solve_and_det_tests!(4);
625 gen_pivoting_solve_and_det_tests!(5);
626
627 macro_rules! gen_tridiagonal_smoke_solve_and_det_tests {
628 ($d:literal $(, #[$stack_array_expectation:meta])?) => {
629 paste! {
630 #[test]
631 fn [<lu_solve_tridiagonal_smoke_ $d d>]() {
632 $(#[$stack_array_expectation])?
637 let mut rows = [[0.0f64; $d]; $d];
638 for i in 0..$d {
639 rows[i][i] = 2.0;
640 if i > 0 {
641 rows[i][i - 1] = -1.0;
642 }
643 if i + 1 < $d {
644 rows[i][i + 1] = -1.0;
645 }
646 }
647
648 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
649 let lu_fn: fn(Matrix<$d>, Tolerance) -> Result<Lu<$d>, LaError> =
650 black_box(Matrix::<$d>::lu);
651 let lu = lu_fn(a, DEFAULT_SINGULAR_TOL).unwrap();
652
653 let mut b_arr = [0.0f64; $d];
655 b_arr[0] = 1.0;
656 b_arr[$d - 1] = 1.0;
657 let b = Vector::<$d>::new(black_box(b_arr));
658
659 let solve_fn: fn(&Lu<$d>, Vector<$d>) -> Result<Vector<$d>, LaError> =
660 black_box(Lu::<$d>::solve);
661 let x = solve_fn(&lu, b).unwrap().into_array();
662
663 for &x_i in &x {
664 assert_abs_diff_eq!(x_i, 1.0, epsilon = 1e-9);
665 }
666 }
667
668 #[test]
669 fn [<lu_det_tridiagonal_smoke_ $d d>]() {
670 $(#[$stack_array_expectation])?
676 let mut rows = [[0.0f64; $d]; $d];
677 for i in 0..$d {
678 rows[i][i] = 2.0;
679 if i > 0 {
680 rows[i][i - 1] = -1.0;
681 }
682 if i + 1 < $d {
683 rows[i][i + 1] = -1.0;
684 }
685 }
686
687 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
688 let lu_fn: fn(Matrix<$d>, Tolerance) -> Result<Lu<$d>, LaError> =
689 black_box(Matrix::<$d>::lu);
690 let lu = lu_fn(a, DEFAULT_SINGULAR_TOL).unwrap();
691
692 let det_fn: fn(&Lu<$d>) -> Result<f64, LaError> =
693 black_box(Lu::<$d>::det);
694 assert_abs_diff_eq!(det_fn(&lu).unwrap(), f64::from($d) + 1.0, epsilon = 1e-8);
695 }
696 }
697 };
698 }
699
700 gen_tridiagonal_smoke_solve_and_det_tests!(16);
701 gen_tridiagonal_smoke_solve_and_det_tests!(32);
702 gen_tridiagonal_smoke_solve_and_det_tests!(
703 64,
704 #[expect(
705 clippy::large_stack_arrays,
706 reason = "the test deliberately exercises the crate's stack-allocated matrix storage"
707 )]
708 );
709
710 #[test]
711 fn solve_0x0_returns_empty_vector_and_unit_det() {
712 let a = Matrix::<0>::zero();
713 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
714
715 assert_eq!(lu.det(), Ok(1.0));
716 assert!(
717 lu.solve(Vector::<0>::zero())
718 .unwrap()
719 .into_array()
720 .is_empty()
721 );
722 }
723
724 #[test]
725 fn solve_1x1() {
726 let a = Matrix::<1>::try_from_rows(black_box([[2.0]])).unwrap();
727 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
728
729 let b = Vector::<1>::new(black_box([6.0]));
730 let solve_fn: fn(&Lu<1>, Vector<1>) -> Result<Vector<1>, LaError> =
731 black_box(Lu::<1>::solve);
732 let x = solve_fn(&lu, b).unwrap().into_array();
733 assert_abs_diff_eq!(x[0], 3.0, epsilon = 1e-12);
734
735 let det_fn: fn(&Lu<1>) -> Result<f64, LaError> = black_box(Lu::<1>::det);
736 assert_abs_diff_eq!(det_fn(&lu).unwrap(), 2.0, epsilon = 0.0);
737 }
738
739 #[test]
740 fn solve_2x2_basic() {
741 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [3.0, 4.0]])).unwrap();
742 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
743 let b = Vector::<2>::new(black_box([5.0, 11.0]));
744
745 let solve_fn: fn(&Lu<2>, Vector<2>) -> Result<Vector<2>, LaError> =
746 black_box(Lu::<2>::solve);
747 let x = solve_fn(&lu, b).unwrap().into_array();
748
749 assert_abs_diff_eq!(x[0], 1.0, epsilon = 1e-12);
750 assert_abs_diff_eq!(x[1], 2.0, epsilon = 1e-12);
751 }
752
753 #[test]
754 fn det_2x2_basic() {
755 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [3.0, 4.0]])).unwrap();
756 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
757
758 let det_fn: fn(&Lu<2>) -> Result<f64, LaError> = black_box(Lu::<2>::det);
759 assert_abs_diff_eq!(det_fn(&lu).unwrap(), -2.0, epsilon = 1e-12);
760 }
761
762 #[test]
763 fn det_ordinary_factors_matches_direct_product_bits() {
764 let diagonal = [1.5, -2.0, 0.25, 8.0];
765 let mut rows = [[0.0; 4]; 4];
766 let mut expected = 1.0;
767 for (i, factor) in diagonal.into_iter().enumerate() {
768 rows[i][i] = factor;
769 expected *= factor;
770 }
771
772 let lu = Matrix::<4>::try_from_rows(rows)
773 .unwrap()
774 .lu(DEFAULT_SINGULAR_TOL)
775 .unwrap();
776 assert_eq!(lu.det().unwrap().to_bits(), expected.to_bits());
777 }
778
779 #[test]
780 fn singular_detected() {
781 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [2.0, 4.0]])).unwrap();
782 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
783 assert_eq!(
784 err,
785 LaError::singular_numerical(1, FactorizationKind::Lu, 0.0, DEFAULT_SINGULAR_TOL.get())
786 );
787 }
788
789 #[test]
790 fn singular_due_to_tolerance_at_first_pivot() {
791 let a = Matrix::<2>::try_from_rows(black_box([[1e-13, 0.0], [0.0, 1.0]])).unwrap();
793 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
794 assert_eq!(
795 err,
796 LaError::singular_numerical(
797 0,
798 FactorizationKind::Lu,
799 1e-13,
800 DEFAULT_SINGULAR_TOL.get()
801 )
802 );
803 }
804
805 #[test]
806 fn non_finite_detected_in_trailing_update() {
807 let a = Matrix::<3>::try_from_rows([
808 [1.0, f64::MAX, 0.0],
809 [-1.0, f64::MAX, 0.0],
810 [0.0, 0.0, 1.0],
811 ])
812 .unwrap();
813
814 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
815 assert_eq!(
816 err,
817 LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 1, 1)
818 );
819 }
820
821 #[test]
822 fn generated_non_finite_takes_precedence_over_later_singular_pivot() {
823 let a = Matrix::<4>::try_from_rows([
826 [1.0, f64::MAX, 0.0, 0.0],
827 [1.0, f64::MAX, 0.0, 0.0],
828 [-1.0, f64::MAX, 0.0, 0.0],
829 [-1.0, f64::MAX, 0.0, 0.0],
830 ])
831 .unwrap();
832
833 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
834 assert_eq!(
835 err,
836 LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 1, 1,)
837 );
838 }
839
840 #[test]
841 fn solve_non_finite_forward_substitution_overflow() {
842 let a = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [-1.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
844 .unwrap();
845 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
846
847 let b = Vector::<3>::new([1.0e308, 1.0e308, 0.0]);
848 let err = lu.solve(b).unwrap_err();
849 assert_eq!(
850 err,
851 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
852 );
853 }
854
855 #[test]
856 fn solve_non_finite_forward_substitution_overflow_fused_branch_5d() {
857 let a = Matrix::<5>::try_from_rows([
860 [1.0, 0.0, 0.0, 0.0, 0.0],
861 [-1.0, 1.0, 0.0, 0.0, 0.0],
862 [0.0, 0.0, 1.0, 0.0, 0.0],
863 [0.0, 0.0, 0.0, 1.0, 0.0],
864 [0.0, 0.0, 0.0, 0.0, 1.0],
865 ])
866 .unwrap();
867 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
868
869 let b = Vector::<5>::new([1.0e308, 1.0e308, 0.0, 0.0, 0.0]);
870 let err = lu.solve(b).unwrap_err();
871 assert_eq!(
872 err,
873 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
874 );
875 }
876
877 #[test]
878 fn solve_non_finite_back_substitution_overflow() {
879 let a = Matrix::<2>::try_from_rows([[1.0, 1.0], [0.0, 2.0e-12]]).unwrap();
881 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
882
883 let b = Vector::<2>::new([0.0, 1.0e300]);
884 let err = lu.solve(b).unwrap_err();
885 assert_eq!(
886 err,
887 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
888 );
889 }
890
891 #[test]
892 fn solve_non_finite_back_substitution_sum_overflow() {
893 let a = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [0.0, 1.0, 1.0e200], [0.0, 0.0, 1.0]])
900 .unwrap();
901 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
902
903 let b = Vector::<3>::new([0.0, 0.0, 1.0e200]);
904 let err = lu.solve(b).unwrap_err();
905 assert_eq!(
906 err,
907 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
908 );
909 }
910
911 #[test]
912 fn det_rejects_product_overflow() {
913 let a = Matrix::<5>::try_from_rows([
914 [1.0e100, 0.0, 0.0, 0.0, 0.0],
915 [0.0, 1.0e100, 0.0, 0.0, 0.0],
916 [0.0, 0.0, 1.0e100, 0.0, 0.0],
917 [0.0, 0.0, 0.0, 1.0e100, 0.0],
918 [0.0, 0.0, 0.0, 0.0, 1.0e100],
919 ])
920 .unwrap();
921 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
922 assert_eq!(
923 lu.det(),
924 Err(LaError::non_finite_computation_step(
925 ArithmeticOperation::Determinant,
926 4
927 ))
928 );
929 }
930
931 #[test]
932 fn det_balances_extreme_diagonals_independently_of_storage_order() {
933 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
934 for diagonal in [
935 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800],
936 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800],
937 ] {
938 let mut rows = [[0.0; 4]; 4];
939 for (i, value) in diagonal.into_iter().enumerate() {
940 rows[i][i] = value;
941 }
942
943 let lu = Matrix::<4>::try_from_rows(rows)
944 .unwrap()
945 .lu(zero_tolerance)
946 .unwrap();
947 assert_eq!(lu.det(), Ok(1.0));
948 }
949 }
950
951 #[test]
952 fn matrix_det_fallback_inherits_balanced_extreme_accumulation() {
953 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
954 for diagonal in [
955 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800, 1.0, 1.0],
956 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800, 1.0, 1.0],
957 ] {
958 let mut rows = [[0.0; 6]; 6];
959 for (i, value) in diagonal.into_iter().enumerate() {
960 rows[i][i] = value;
961 }
962
963 let matrix = Matrix::<6>::try_from_rows(rows).unwrap();
964 assert_eq!(matrix.det(), Ok(1.0));
965 assert_eq!(matrix.lu(zero_tolerance).unwrap().det(), Ok(1.0));
966 }
967 }
968
969 #[test]
970 fn det_rounds_final_tiny_magnitude_to_zero() {
971 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
972 let positive =
973 Matrix::<2>::try_from_rows([[TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
974 let positive_det = positive.lu(zero_tolerance).unwrap().det().unwrap();
975 assert_eq!(positive_det.to_bits(), 0.0f64.to_bits());
976
977 let negative =
978 Matrix::<2>::try_from_rows([[-TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
979 let negative_det = negative.lu(zero_tolerance).unwrap().det().unwrap();
980 assert_eq!(negative_det.to_bits(), (-0.0f64).to_bits());
981 }
982
983 #[test]
993 fn lu_det_const_eval_d2() {
994 const DET: Result<f64, LaError> = {
995 let Ok(factors) = LuFactors::try_from_computation([[2.0, 0.0], [0.0, 3.0]]) else {
997 panic!("LU test factors must be finite");
998 };
999 let lu = Lu::<2> {
1000 factors,
1001 permutation: RowPermutation::identity(),
1002 };
1003 lu.det()
1004 };
1005 assert_eq!(DET, Ok(6.0));
1006 }
1007
1008 #[test]
1009 fn lu_det_const_eval_d3_row_swap() {
1010 const DET: Result<f64, LaError> = {
1011 let Ok(factors) = LuFactors::try_from_computation(Matrix::<3>::identity().into_rows())
1014 else {
1015 panic!("LU test factors must be usable");
1016 };
1017 let mut permutation = RowPermutation::identity();
1018 permutation.swap(0, 1);
1019 let lu = Lu::<3> {
1020 factors,
1021 permutation,
1022 };
1023 lu.det()
1024 };
1025 assert_eq!(DET, Ok(-1.0));
1026 }
1027
1028 #[test]
1029 fn lu_solve_const_eval_d2() {
1030 const X: Result<Vector<2>, LaError> = {
1032 let Ok(factors) = LuFactors::try_from_computation(Matrix::<2>::identity().into_rows())
1033 else {
1034 panic!("LU test factors must be usable");
1035 };
1036 let lu = Lu::<2> {
1037 factors,
1038 permutation: RowPermutation::identity(),
1039 };
1040 let b = Vector::<2>::new([1.0, 2.0]);
1041 lu.solve(b)
1042 };
1043 let x = X.unwrap().into_array();
1044 assert!((x[0] - 1.0).abs() <= 1e-12);
1045 assert!((x[1] - 2.0).abs() <= 1e-12);
1046 }
1047}