1#![forbid(unsafe_code)]
2
3use core::hint::cold_path;
11
12use crate::matrix::Matrix;
13use crate::scaled_product::{RangeCheckedProduct, 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 while i < D {
368 let factor = self.factors.diag(i);
369 match range_checked_product(det, factor) {
370 RangeCheckedProduct::Safe(next) => det = next,
371 RangeCheckedProduct::NeedsScaling => {
372 cold_path();
373 return self.scaled_det();
374 }
375 }
376 i += 1;
377 }
378 Ok(det)
379 }
380
381 #[cold]
383 const fn scaled_det(&self) -> Result<f64, LaError> {
384 let mut product = ScaledProduct::new(self.permutation.is_odd());
385 let mut i = 0;
386 while i < D {
387 product.multiply(self.factors.diag(i));
388 i += 1;
389 }
390
391 if let Some(det) = product.finish() {
392 Ok(det)
393 } else {
394 Err(LaError::non_finite_computation_step(
395 ArithmeticOperation::Determinant,
396 D.saturating_sub(1),
397 ))
398 }
399 }
400}
401
402#[cfg(test)]
403mod tests {
404 use core::hint::black_box;
405
406 use approx::assert_abs_diff_eq;
407 use pastey::paste;
408
409 use super::*;
410 use crate::DEFAULT_SINGULAR_TOL;
411
412 const TWO_NEG_800: f64 = f64::from_bits(223_u64 << 52);
413 const TWO_POS_800: f64 = f64::from_bits(1823_u64 << 52);
414
415 #[test]
416 fn row_permutation_keeps_mapping_and_parity_synchronized() {
417 let mut permutation = RowPermutation::<4>::identity();
418 assert_eq!(
419 core::array::from_fn(|row| permutation.source_row(row)),
420 [0, 1, 2, 3]
421 );
422 assert!(!permutation.is_odd());
423
424 permutation.swap(0, 3);
425 assert_eq!(
426 core::array::from_fn(|row| permutation.source_row(row)),
427 [3, 1, 2, 0]
428 );
429 assert!(permutation.is_odd());
430
431 permutation.swap(1, 2);
432 assert_eq!(
433 core::array::from_fn(|row| permutation.source_row(row)),
434 [3, 2, 1, 0]
435 );
436 assert!(!permutation.is_odd());
437 }
438
439 macro_rules! gen_pivoting_solve_and_det_tests {
440 ($d:literal) => {
441 paste! {
442 #[test]
443 fn [<lu_solve_pivoting_ $d d>]() {
444 let mut rows = [[0.0f64; $d]; $d];
450 for i in 0..$d {
451 rows[i][i] = 1.0;
452 }
453 rows.swap(0, 1);
454
455 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
456 let lu_fn: fn(Matrix<$d>, Tolerance) -> Result<Lu<$d>, LaError> =
457 black_box(Matrix::<$d>::lu);
458 let lu = lu_fn(a, DEFAULT_SINGULAR_TOL).unwrap();
459
460 let b_arr = {
462 let mut arr = [0.0f64; $d];
463 let mut val = 1.0f64;
464 for dst in arr.iter_mut() {
465 *dst = val;
466 val += 1.0;
467 }
468 arr
469 };
470 let mut expected = b_arr;
471 expected.swap(0, 1);
472 let b = Vector::<$d>::new(black_box(b_arr));
473
474 let solve_fn: fn(&Lu<$d>, Vector<$d>) -> Result<Vector<$d>, LaError> =
475 black_box(Lu::<$d>::solve);
476 let x = solve_fn(&lu, b).unwrap().into_array();
477
478 for i in 0..$d {
479 assert_abs_diff_eq!(x[i], expected[i], epsilon = 1e-12);
480 }
481 }
482
483 #[test]
484 fn [<lu_det_pivoting_ $d d>]() {
485 let mut rows = [[0.0f64; $d]; $d];
490 for i in 0..$d {
491 rows[i][i] = 1.0;
492 }
493 rows.swap(0, 1);
494
495 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
496 let lu_fn: fn(Matrix<$d>, Tolerance) -> Result<Lu<$d>, LaError> =
497 black_box(Matrix::<$d>::lu);
498 let lu = lu_fn(a, DEFAULT_SINGULAR_TOL).unwrap();
499
500 let det_fn: fn(&Lu<$d>) -> Result<f64, LaError> =
502 black_box(Lu::<$d>::det);
503 assert_abs_diff_eq!(det_fn(&lu).unwrap(), -1.0, epsilon = 1e-12);
504 }
505 }
506 };
507 }
508
509 gen_pivoting_solve_and_det_tests!(2);
510 gen_pivoting_solve_and_det_tests!(3);
511 gen_pivoting_solve_and_det_tests!(4);
512 gen_pivoting_solve_and_det_tests!(5);
513
514 macro_rules! gen_tridiagonal_smoke_solve_and_det_tests {
515 ($d:literal $(, #[$stack_array_expectation:meta])?) => {
516 paste! {
517 #[test]
518 fn [<lu_solve_tridiagonal_smoke_ $d d>]() {
519 $(#[$stack_array_expectation])?
524 let mut rows = [[0.0f64; $d]; $d];
525 for i in 0..$d {
526 rows[i][i] = 2.0;
527 if i > 0 {
528 rows[i][i - 1] = -1.0;
529 }
530 if i + 1 < $d {
531 rows[i][i + 1] = -1.0;
532 }
533 }
534
535 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
536 let lu_fn: fn(Matrix<$d>, Tolerance) -> Result<Lu<$d>, LaError> =
537 black_box(Matrix::<$d>::lu);
538 let lu = lu_fn(a, DEFAULT_SINGULAR_TOL).unwrap();
539
540 let mut b_arr = [0.0f64; $d];
542 b_arr[0] = 1.0;
543 b_arr[$d - 1] = 1.0;
544 let b = Vector::<$d>::new(black_box(b_arr));
545
546 let solve_fn: fn(&Lu<$d>, Vector<$d>) -> Result<Vector<$d>, LaError> =
547 black_box(Lu::<$d>::solve);
548 let x = solve_fn(&lu, b).unwrap().into_array();
549
550 for &x_i in &x {
551 assert_abs_diff_eq!(x_i, 1.0, epsilon = 1e-9);
552 }
553 }
554
555 #[test]
556 fn [<lu_det_tridiagonal_smoke_ $d d>]() {
557 $(#[$stack_array_expectation])?
563 let mut rows = [[0.0f64; $d]; $d];
564 for i in 0..$d {
565 rows[i][i] = 2.0;
566 if i > 0 {
567 rows[i][i - 1] = -1.0;
568 }
569 if i + 1 < $d {
570 rows[i][i + 1] = -1.0;
571 }
572 }
573
574 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
575 let lu_fn: fn(Matrix<$d>, Tolerance) -> Result<Lu<$d>, LaError> =
576 black_box(Matrix::<$d>::lu);
577 let lu = lu_fn(a, DEFAULT_SINGULAR_TOL).unwrap();
578
579 let det_fn: fn(&Lu<$d>) -> Result<f64, LaError> =
580 black_box(Lu::<$d>::det);
581 assert_abs_diff_eq!(det_fn(&lu).unwrap(), f64::from($d) + 1.0, epsilon = 1e-8);
582 }
583 }
584 };
585 }
586
587 gen_tridiagonal_smoke_solve_and_det_tests!(16);
588 gen_tridiagonal_smoke_solve_and_det_tests!(32);
589 gen_tridiagonal_smoke_solve_and_det_tests!(
590 64,
591 #[expect(
592 clippy::large_stack_arrays,
593 reason = "the test deliberately exercises the crate's stack-allocated matrix storage"
594 )]
595 );
596
597 #[test]
598 fn solve_0x0_returns_empty_vector_and_unit_det() {
599 let a = Matrix::<0>::zero();
600 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
601
602 assert_eq!(lu.det(), Ok(1.0));
603 assert!(
604 lu.solve(Vector::<0>::zero())
605 .unwrap()
606 .into_array()
607 .is_empty()
608 );
609 }
610
611 #[test]
612 fn solve_1x1() {
613 let a = Matrix::<1>::try_from_rows(black_box([[2.0]])).unwrap();
614 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
615
616 let b = Vector::<1>::new(black_box([6.0]));
617 let solve_fn: fn(&Lu<1>, Vector<1>) -> Result<Vector<1>, LaError> =
618 black_box(Lu::<1>::solve);
619 let x = solve_fn(&lu, b).unwrap().into_array();
620 assert_abs_diff_eq!(x[0], 3.0, epsilon = 1e-12);
621
622 let det_fn: fn(&Lu<1>) -> Result<f64, LaError> = black_box(Lu::<1>::det);
623 assert_abs_diff_eq!(det_fn(&lu).unwrap(), 2.0, epsilon = 0.0);
624 }
625
626 #[test]
627 fn solve_2x2_basic() {
628 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [3.0, 4.0]])).unwrap();
629 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
630 let b = Vector::<2>::new(black_box([5.0, 11.0]));
631
632 let solve_fn: fn(&Lu<2>, Vector<2>) -> Result<Vector<2>, LaError> =
633 black_box(Lu::<2>::solve);
634 let x = solve_fn(&lu, b).unwrap().into_array();
635
636 assert_abs_diff_eq!(x[0], 1.0, epsilon = 1e-12);
637 assert_abs_diff_eq!(x[1], 2.0, epsilon = 1e-12);
638 }
639
640 #[test]
641 fn det_2x2_basic() {
642 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [3.0, 4.0]])).unwrap();
643 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
644
645 let det_fn: fn(&Lu<2>) -> Result<f64, LaError> = black_box(Lu::<2>::det);
646 assert_abs_diff_eq!(det_fn(&lu).unwrap(), -2.0, epsilon = 1e-12);
647 }
648
649 #[test]
650 fn det_ordinary_factors_matches_direct_product_bits() {
651 let diagonal = [1.5, -2.0, 0.25, 8.0];
652 let mut rows = [[0.0; 4]; 4];
653 let mut expected = 1.0;
654 for (i, factor) in diagonal.into_iter().enumerate() {
655 rows[i][i] = factor;
656 expected *= factor;
657 }
658
659 let lu = Matrix::<4>::try_from_rows(rows)
660 .unwrap()
661 .lu(DEFAULT_SINGULAR_TOL)
662 .unwrap();
663 assert_eq!(lu.det().unwrap().to_bits(), expected.to_bits());
664 }
665
666 #[test]
667 fn singular_detected() {
668 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [2.0, 4.0]])).unwrap();
669 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
670 assert_eq!(
671 err,
672 LaError::singular_numerical(1, FactorizationKind::Lu, 0.0, DEFAULT_SINGULAR_TOL.get())
673 );
674 }
675
676 #[test]
677 fn singular_due_to_tolerance_at_first_pivot() {
678 let a = Matrix::<2>::try_from_rows(black_box([[1e-13, 0.0], [0.0, 1.0]])).unwrap();
680 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
681 assert_eq!(
682 err,
683 LaError::singular_numerical(
684 0,
685 FactorizationKind::Lu,
686 1e-13,
687 DEFAULT_SINGULAR_TOL.get()
688 )
689 );
690 }
691
692 #[test]
693 fn non_finite_detected_in_trailing_update() {
694 let a = Matrix::<3>::try_from_rows([
695 [1.0, f64::MAX, 0.0],
696 [-1.0, f64::MAX, 0.0],
697 [0.0, 0.0, 1.0],
698 ])
699 .unwrap();
700
701 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
702 assert_eq!(
703 err,
704 LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 1, 1)
705 );
706 }
707
708 #[test]
709 fn generated_non_finite_takes_precedence_over_later_singular_pivot() {
710 let a = Matrix::<4>::try_from_rows([
713 [1.0, f64::MAX, 0.0, 0.0],
714 [1.0, f64::MAX, 0.0, 0.0],
715 [-1.0, f64::MAX, 0.0, 0.0],
716 [-1.0, f64::MAX, 0.0, 0.0],
717 ])
718 .unwrap();
719
720 let err = a.lu(DEFAULT_SINGULAR_TOL).unwrap_err();
721 assert_eq!(
722 err,
723 LaError::non_finite_computation_matrix(ArithmeticOperation::LuFactorization, 1, 1,)
724 );
725 }
726
727 #[test]
728 fn solve_non_finite_forward_substitution_overflow() {
729 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]])
731 .unwrap();
732 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
733
734 let b = Vector::<3>::new([1.0e308, 1.0e308, 0.0]);
735 let err = lu.solve(b).unwrap_err();
736 assert_eq!(
737 err,
738 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
739 );
740 }
741
742 #[test]
743 fn solve_non_finite_forward_substitution_overflow_fused_branch_5d() {
744 let a = Matrix::<5>::try_from_rows([
747 [1.0, 0.0, 0.0, 0.0, 0.0],
748 [-1.0, 1.0, 0.0, 0.0, 0.0],
749 [0.0, 0.0, 1.0, 0.0, 0.0],
750 [0.0, 0.0, 0.0, 1.0, 0.0],
751 [0.0, 0.0, 0.0, 0.0, 1.0],
752 ])
753 .unwrap();
754 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
755
756 let b = Vector::<5>::new([1.0e308, 1.0e308, 0.0, 0.0, 0.0]);
757 let err = lu.solve(b).unwrap_err();
758 assert_eq!(
759 err,
760 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
761 );
762 }
763
764 #[test]
765 fn solve_non_finite_back_substitution_overflow() {
766 let a = Matrix::<2>::try_from_rows([[1.0, 1.0], [0.0, 2.0e-12]]).unwrap();
768 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
769
770 let b = Vector::<2>::new([0.0, 1.0e300]);
771 let err = lu.solve(b).unwrap_err();
772 assert_eq!(
773 err,
774 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
775 );
776 }
777
778 #[test]
779 fn solve_non_finite_back_substitution_sum_overflow() {
780 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]])
787 .unwrap();
788 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
789
790 let b = Vector::<3>::new([0.0, 0.0, 1.0e200]);
791 let err = lu.solve(b).unwrap_err();
792 assert_eq!(
793 err,
794 LaError::non_finite_computation_step(ArithmeticOperation::LuSolve, 1)
795 );
796 }
797
798 #[test]
799 fn det_rejects_product_overflow() {
800 let a = Matrix::<5>::try_from_rows([
801 [1.0e100, 0.0, 0.0, 0.0, 0.0],
802 [0.0, 1.0e100, 0.0, 0.0, 0.0],
803 [0.0, 0.0, 1.0e100, 0.0, 0.0],
804 [0.0, 0.0, 0.0, 1.0e100, 0.0],
805 [0.0, 0.0, 0.0, 0.0, 1.0e100],
806 ])
807 .unwrap();
808 let lu = a.lu(DEFAULT_SINGULAR_TOL).unwrap();
809 assert_eq!(
810 lu.det(),
811 Err(LaError::non_finite_computation_step(
812 ArithmeticOperation::Determinant,
813 4
814 ))
815 );
816 }
817
818 #[test]
819 fn det_balances_extreme_diagonals_independently_of_storage_order() {
820 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
821 for diagonal in [
822 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800],
823 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800],
824 ] {
825 let mut rows = [[0.0; 4]; 4];
826 for (i, value) in diagonal.into_iter().enumerate() {
827 rows[i][i] = value;
828 }
829
830 let lu = Matrix::<4>::try_from_rows(rows)
831 .unwrap()
832 .lu(zero_tolerance)
833 .unwrap();
834 assert_eq!(lu.det(), Ok(1.0));
835 }
836 }
837
838 #[test]
839 fn matrix_det_fallback_inherits_balanced_extreme_accumulation() {
840 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
841 for diagonal in [
842 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800, 1.0, 1.0],
843 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800, 1.0, 1.0],
844 ] {
845 let mut rows = [[0.0; 6]; 6];
846 for (i, value) in diagonal.into_iter().enumerate() {
847 rows[i][i] = value;
848 }
849
850 let matrix = Matrix::<6>::try_from_rows(rows).unwrap();
851 assert_eq!(matrix.det(), Ok(1.0));
852 assert_eq!(matrix.lu(zero_tolerance).unwrap().det(), Ok(1.0));
853 }
854 }
855
856 #[test]
857 fn det_rounds_final_tiny_magnitude_to_zero() {
858 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
859 let positive =
860 Matrix::<2>::try_from_rows([[TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
861 let positive_det = positive.lu(zero_tolerance).unwrap().det().unwrap();
862 assert_eq!(positive_det.to_bits(), 0.0f64.to_bits());
863
864 let negative =
865 Matrix::<2>::try_from_rows([[-TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
866 let negative_det = negative.lu(zero_tolerance).unwrap().det().unwrap();
867 assert_eq!(negative_det.to_bits(), (-0.0f64).to_bits());
868 }
869
870 #[test]
880 fn lu_det_const_eval_d2() {
881 const DET: Result<f64, LaError> = {
882 let Ok(factors) = LuFactors::try_from_computation([[2.0, 0.0], [0.0, 3.0]]) else {
884 panic!("LU test factors must be finite");
885 };
886 let lu = Lu::<2> {
887 factors,
888 permutation: RowPermutation::identity(),
889 };
890 lu.det()
891 };
892 assert_eq!(DET, Ok(6.0));
893 }
894
895 #[test]
896 fn lu_det_const_eval_d3_row_swap() {
897 const DET: Result<f64, LaError> = {
898 let Ok(factors) = LuFactors::try_from_computation(Matrix::<3>::identity().into_rows())
901 else {
902 panic!("LU test factors must be usable");
903 };
904 let mut permutation = RowPermutation::identity();
905 permutation.swap(0, 1);
906 let lu = Lu::<3> {
907 factors,
908 permutation,
909 };
910 lu.det()
911 };
912 assert_eq!(DET, Ok(-1.0));
913 }
914
915 #[test]
916 fn lu_solve_const_eval_d2() {
917 const X: Result<Vector<2>, LaError> = {
919 let Ok(factors) = LuFactors::try_from_computation(Matrix::<2>::identity().into_rows())
920 else {
921 panic!("LU test factors must be usable");
922 };
923 let lu = Lu::<2> {
924 factors,
925 permutation: RowPermutation::identity(),
926 };
927 let b = Vector::<2>::new([1.0, 2.0]);
928 lu.solve(b)
929 };
930 let x = X.unwrap().into_array();
931 assert!((x[0] - 1.0).abs() <= 1e-12);
932 assert!((x[1] - 2.0).abs() <= 1e-12);
933 }
934}