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_eq!(
717 lu.solve(Vector::<0>::zero())
718 .unwrap()
719 .into_array()
720 .as_slice(),
721 []
722 );
723 }
724
725 #[test]
726 fn solve_1x1() {
727 let a = Matrix::<1>::try_from_rows(black_box([[2.0]])).unwrap();
728 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
729
730 let b = Vector::<1>::new(black_box([6.0]));
731 let solve_fn: fn(&Lu<1>, Vector<1>) -> Result<Vector<1>, LaError> =
732 black_box(Lu::<1>::solve);
733 let x = solve_fn(&lu, b).unwrap().into_array();
734 assert_abs_diff_eq!(x[0], 3.0, epsilon = 1e-12);
735
736 let det_fn: fn(&Lu<1>) -> Result<f64, LaError> = black_box(Lu::<1>::det);
737 assert_abs_diff_eq!(det_fn(&lu).unwrap(), 2.0, epsilon = 0.0);
738 }
739
740 #[test]
741 fn solve_2x2_basic() {
742 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [3.0, 4.0]])).unwrap();
743 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
744 let b = Vector::<2>::new(black_box([5.0, 11.0]));
745
746 let solve_fn: fn(&Lu<2>, Vector<2>) -> Result<Vector<2>, LaError> =
747 black_box(Lu::<2>::solve);
748 let x = solve_fn(&lu, b).unwrap().into_array();
749
750 assert_abs_diff_eq!(x[0], 1.0, epsilon = 1e-12);
751 assert_abs_diff_eq!(x[1], 2.0, epsilon = 1e-12);
752 }
753
754 #[test]
755 fn det_2x2_basic() {
756 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [3.0, 4.0]])).unwrap();
757 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
758
759 let det_fn: fn(&Lu<2>) -> Result<f64, LaError> = black_box(Lu::<2>::det);
760 assert_abs_diff_eq!(det_fn(&lu).unwrap(), -2.0, epsilon = 1e-12);
761 }
762
763 #[test]
764 fn det_ordinary_factors_matches_direct_product_bits() {
765 let diagonal = [1.5, -2.0, 0.25, 8.0];
766 let mut rows = [[0.0; 4]; 4];
767 let mut expected = 1.0;
768 for (i, factor) in diagonal.into_iter().enumerate() {
769 rows[i][i] = factor;
770 expected *= factor;
771 }
772
773 let lu = Matrix::<4>::try_from_rows(rows)
774 .unwrap()
775 .lu(DEFAULT_SINGULAR_TOL)
776 .unwrap();
777 assert_eq!(lu.det().unwrap().to_bits(), expected.to_bits());
778 }
779
780 #[test]
781 fn singular_detected() {
782 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [2.0, 4.0]])).unwrap();
783 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
784 assert_eq!(
785 err,
786 LaError::singular_numerical(1, FactorizationKind::Lu, 0.0, DEFAULT_SINGULAR_TOL.get())
787 );
788 }
789
790 #[test]
791 fn singular_due_to_tolerance_at_first_pivot() {
792 let a = Matrix::<2>::try_from_rows(black_box([[1e-13, 0.0], [0.0, 1.0]])).unwrap();
794 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
795 assert_eq!(
796 err,
797 LaError::singular_numerical(
798 0,
799 FactorizationKind::Lu,
800 1e-13,
801 DEFAULT_SINGULAR_TOL.get()
802 )
803 );
804 }
805
806 #[test]
807 fn non_finite_detected_in_trailing_update() {
808 let a = Matrix::<3>::try_from_rows([
809 [1.0, f64::MAX, 0.0],
810 [-1.0, f64::MAX, 0.0],
811 [0.0, 0.0, 1.0],
812 ])
813 .unwrap();
814
815 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
816 assert_eq!(
817 err,
818 LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 1, 1)
819 );
820 }
821
822 #[test]
823 fn generated_non_finite_takes_precedence_over_later_singular_pivot() {
824 let a = Matrix::<4>::try_from_rows([
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 [-1.0, f64::MAX, 0.0, 0.0],
831 ])
832 .unwrap();
833
834 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
835 assert_eq!(
836 err,
837 LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 1, 1,)
838 );
839 }
840
841 #[test]
842 fn solve_non_finite_forward_substitution_overflow() {
843 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]])
845 .unwrap();
846 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
847
848 let b = Vector::<3>::new([1.0e308, 1.0e308, 0.0]);
849 let err = lu.solve(b).unwrap_err();
850 assert_eq!(
851 err,
852 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
853 );
854 }
855
856 #[test]
857 fn solve_non_finite_forward_substitution_overflow_fused_branch_5d() {
858 let a = Matrix::<5>::try_from_rows([
861 [1.0, 0.0, 0.0, 0.0, 0.0],
862 [-1.0, 1.0, 0.0, 0.0, 0.0],
863 [0.0, 0.0, 1.0, 0.0, 0.0],
864 [0.0, 0.0, 0.0, 1.0, 0.0],
865 [0.0, 0.0, 0.0, 0.0, 1.0],
866 ])
867 .unwrap();
868 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
869
870 let b = Vector::<5>::new([1.0e308, 1.0e308, 0.0, 0.0, 0.0]);
871 let err = lu.solve(b).unwrap_err();
872 assert_eq!(
873 err,
874 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
875 );
876 }
877
878 #[test]
879 fn solve_non_finite_back_substitution_overflow() {
880 let a = Matrix::<2>::try_from_rows([[1.0, 1.0], [0.0, 2.0e-12]]).unwrap();
882 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
883
884 let b = Vector::<2>::new([0.0, 1.0e300]);
885 let err = lu.solve(b).unwrap_err();
886 assert_eq!(
887 err,
888 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
889 );
890 }
891
892 #[test]
893 fn solve_non_finite_back_substitution_sum_overflow() {
894 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]])
901 .unwrap();
902 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
903
904 let b = Vector::<3>::new([0.0, 0.0, 1.0e200]);
905 let err = lu.solve(b).unwrap_err();
906 assert_eq!(
907 err,
908 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
909 );
910 }
911
912 #[test]
913 fn det_rejects_product_overflow() {
914 let a = Matrix::<5>::try_from_rows([
915 [1.0e100, 0.0, 0.0, 0.0, 0.0],
916 [0.0, 1.0e100, 0.0, 0.0, 0.0],
917 [0.0, 0.0, 1.0e100, 0.0, 0.0],
918 [0.0, 0.0, 0.0, 1.0e100, 0.0],
919 [0.0, 0.0, 0.0, 0.0, 1.0e100],
920 ])
921 .unwrap();
922 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
923 assert_eq!(
924 lu.det(),
925 Err(LaError::non_finite_computation_step(
926 ArithmeticOperation::Determinant,
927 4
928 ))
929 );
930 }
931
932 #[test]
933 fn det_balances_extreme_diagonals_independently_of_storage_order() {
934 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
935 for diagonal in [
936 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800],
937 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800],
938 ] {
939 let mut rows = [[0.0; 4]; 4];
940 for (i, value) in diagonal.into_iter().enumerate() {
941 rows[i][i] = value;
942 }
943
944 let lu = Matrix::<4>::try_from_rows(rows)
945 .unwrap()
946 .lu(zero_tolerance)
947 .unwrap();
948 assert_eq!(lu.det(), Ok(1.0));
949 }
950 }
951
952 #[test]
953 fn matrix_det_fallback_inherits_balanced_extreme_accumulation() {
954 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
955 for diagonal in [
956 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800, 1.0, 1.0],
957 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800, 1.0, 1.0],
958 ] {
959 let mut rows = [[0.0; 6]; 6];
960 for (i, value) in diagonal.into_iter().enumerate() {
961 rows[i][i] = value;
962 }
963
964 let matrix = Matrix::<6>::try_from_rows(rows).unwrap();
965 assert_eq!(matrix.det(), Ok(1.0));
966 assert_eq!(matrix.lu(zero_tolerance).unwrap().det(), Ok(1.0));
967 }
968 }
969
970 #[test]
971 fn det_rounds_final_tiny_magnitude_to_zero() {
972 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
973 let positive =
974 Matrix::<2>::try_from_rows([[TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
975 let positive_det = positive.lu(zero_tolerance).unwrap().det().unwrap();
976 assert_eq!(positive_det.to_bits(), 0.0f64.to_bits());
977
978 let negative =
979 Matrix::<2>::try_from_rows([[-TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
980 let negative_det = negative.lu(zero_tolerance).unwrap().det().unwrap();
981 assert_eq!(negative_det.to_bits(), (-0.0f64).to_bits());
982 }
983
984 #[test]
994 fn lu_det_const_eval_d2() {
995 const DET: Result<f64, LaError> = {
996 let Ok(factors) = LuFactors::try_from_computation([[2.0, 0.0], [0.0, 3.0]]) else {
998 panic!("LU test factors must be finite");
999 };
1000 let lu = Lu::<2> {
1001 factors,
1002 permutation: RowPermutation::identity(),
1003 };
1004 lu.det()
1005 };
1006 assert_eq!(DET, Ok(6.0));
1007 }
1008
1009 #[test]
1010 fn lu_det_const_eval_d3_row_swap() {
1011 const DET: Result<f64, LaError> = {
1012 let Ok(factors) = LuFactors::try_from_computation(Matrix::<3>::identity().into_rows())
1015 else {
1016 panic!("LU test factors must be usable");
1017 };
1018 let mut permutation = RowPermutation::identity();
1019 permutation.swap(0, 1);
1020 let lu = Lu::<3> {
1021 factors,
1022 permutation,
1023 };
1024 lu.det()
1025 };
1026 assert_eq!(DET, Ok(-1.0));
1027 }
1028
1029 #[test]
1030 fn lu_solve_const_eval_d2() {
1031 const X: Result<Vector<2>, LaError> = {
1033 let Ok(factors) = LuFactors::try_from_computation(Matrix::<2>::identity().into_rows())
1034 else {
1035 panic!("LU test factors must be usable");
1036 };
1037 let lu = Lu::<2> {
1038 factors,
1039 permutation: RowPermutation::identity(),
1040 };
1041 let b = Vector::<2>::new([1.0, 2.0]);
1042 lu.solve(b)
1043 };
1044 let x = X.unwrap().into_array();
1045 assert!((x[0] - 1.0).abs() <= 1e-12);
1046 assert!((x[1] - 2.0).abs() <= 1e-12);
1047 }
1048}