1#![forbid(unsafe_code)]
2
3use core::hint::cold_path;
21
22use crate::matrix::SymmetricMatrix;
23use crate::scaled_product::{RangeCheckedProduct, ScaledProduct, range_checked_product};
24use crate::vector::Vector;
25use crate::{ArithmeticOperation, FactorizationKind, LaError, Tolerance};
26
27#[must_use]
53#[derive(Clone, Copy, Debug, PartialEq)]
54pub struct Ldlt<const D: usize> {
55 factors: LdltFactors<D>,
56}
57
58#[derive(Clone, Copy, Debug, PartialEq)]
63struct LdltFactors<const D: usize> {
64 storage: [[f64; D]; D],
65}
66
67impl<const D: usize> LdltFactors<D> {
68 #[inline]
70 const fn from_proven_rows(storage: [[f64; D]; D]) -> Self {
71 Self { storage }
72 }
73
74 #[inline]
76 #[must_use]
77 const fn row(&self, index: usize) -> &[f64; D] {
78 &self.storage[index]
79 }
80
81 #[inline]
83 #[must_use]
84 const fn diag(&self, index: usize) -> f64 {
85 self.storage[index][index]
86 }
87}
88
89impl<const D: usize> Ldlt<D> {
90 #[inline]
103 pub(crate) fn factor_symmetric(a: SymmetricMatrix<D>, tol: Tolerance) -> Result<Self, LaError> {
104 let mut rows = a.into_matrix().into_rows();
105 let tolerance = tol.get();
106
107 {
108 let rows = &mut rows;
109
110 for j in 0..D {
112 let d = rows[j][j];
113 if !(d.is_finite() && d > tolerance) {
114 cold_path();
115 return Err(Self::pivot_failure(rows, j, d, tolerance));
116 }
117 if D <= 5 {
118 #[expect(
121 clippy::needless_range_loop,
122 reason = "the row index identifies the lower-triangle entry and any reported non-finite coordinate"
123 )]
124 for i in (j + 1)..D {
125 let l = rows[i][j] / d;
126 if !l.is_finite() {
127 cold_path();
128 return Err(LaError::non_finite_computation_matrix(
129 ArithmeticOperation::LdltFactorization,
130 i,
131 j,
132 ));
133 }
134 rows[i][j] = l;
135 }
136
137 for i in (j + 1)..D {
138 let l_i = rows[i][j];
139 let l_i_d = l_i * d;
140
141 #[expect(
142 clippy::needless_range_loop,
143 reason = "the triangular column index coordinates multiplier reads with in-place trailing-row writes"
144 )]
145 for k in (j + 1)..=i {
146 let l_k = rows[k][j];
147 let new_val = (-l_i_d).mul_add(l_k, rows[i][k]);
148 rows[i][k] = new_val;
149 }
150 }
151 } else {
152 for i in (j + 1)..D {
155 let l_i = rows[i][j] / d;
156 if !l_i.is_finite() {
157 cold_path();
158 return Err(LaError::non_finite_computation_matrix(
159 ArithmeticOperation::LdltFactorization,
160 i,
161 j,
162 ));
163 }
164 rows[i][j] = l_i;
165
166 let l_i_d = l_i * d;
167
168 #[expect(
169 clippy::needless_range_loop,
170 reason = "the triangular column index coordinates normalized-column reads with the fused in-place update"
171 )]
172 for k in (j + 1)..=i {
173 let l_k = rows[k][j];
174 let new_val = (-l_i_d).mul_add(l_k, rows[i][k]);
175 rows[i][k] = new_val;
176 }
177 }
178 }
179 }
180 }
181
182 Ok(Self {
185 factors: LdltFactors::from_proven_rows(rows),
186 })
187 }
188
189 fn non_finite_factor_error(rows: &[[f64; D]; D]) -> Option<LaError> {
191 for (row, values) in rows.iter().enumerate() {
192 for (col, value) in values.iter().enumerate() {
193 if !value.is_finite() {
194 return Some(LaError::non_finite_computation_matrix(
195 ArithmeticOperation::LdltFactorization,
196 row,
197 col,
198 ));
199 }
200 }
201 }
202 None
203 }
204
205 fn pivot_failure(
207 rows: &[[f64; D]; D],
208 pivot_col: usize,
209 pivot: f64,
210 tolerance: f64,
211 ) -> LaError {
212 if !pivot.is_finite() {
213 return LaError::non_finite_computation_matrix(
214 ArithmeticOperation::LdltFactorization,
215 pivot_col,
216 pivot_col,
217 );
218 }
219 if pivot < 0.0 {
220 return Self::non_finite_factor_error(rows)
221 .unwrap_or_else(|| LaError::not_positive_semidefinite_negative(pivot_col, pivot));
222 }
223 if pivot == 0.0 {
224 return Self::zero_pivot_failure(rows, pivot_col, tolerance);
225 }
226 Self::non_finite_factor_error(rows).unwrap_or_else(|| {
227 LaError::singular_numerical(pivot_col, FactorizationKind::Ldlt, pivot, tolerance)
228 })
229 }
230
231 fn zero_pivot_failure(rows: &[[f64; D]; D], pivot_col: usize, tolerance: f64) -> LaError {
233 if let Some(error) = Self::non_finite_factor_error(rows) {
234 return error;
235 }
236 for (row, values) in rows.iter().enumerate().skip(pivot_col + 1) {
237 let coupling = values[pivot_col];
238 if coupling != 0.0 {
239 return LaError::not_positive_semidefinite_zero_coupling(pivot_col, row, coupling);
240 }
241 }
242 LaError::singular_numerical(pivot_col, FactorizationKind::Ldlt, 0.0, tolerance)
243 }
244
245 #[inline]
276 pub const fn det(&self) -> Result<f64, LaError> {
277 let mut det = 1.0;
278 let mut i = 0;
279 while i < D {
280 let factor = self.factors.diag(i);
281 match range_checked_product(det, factor) {
282 RangeCheckedProduct::Safe(next) => det = next,
283 RangeCheckedProduct::NeedsScaling => {
284 cold_path();
285 return self.scaled_det();
286 }
287 }
288 i += 1;
289 }
290 Ok(det)
291 }
292
293 #[cold]
295 const fn scaled_det(&self) -> Result<f64, LaError> {
296 let mut product = ScaledProduct::new(false);
297 let mut i = 0;
298 while i < D {
299 product.multiply(self.factors.diag(i));
300 i += 1;
301 }
302
303 if let Some(det) = product.finish() {
304 Ok(det)
305 } else {
306 Err(LaError::non_finite_computation_step(
307 ArithmeticOperation::Determinant,
308 D.saturating_sub(1),
309 ))
310 }
311 }
312
313 #[inline]
341 pub const fn solve(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
342 let mut x = b.into_array();
343
344 let mut i = 0;
346 while i < D {
347 let mut sum = x[i];
348 let row = self.factors.row(i);
349 let mut j = 0;
350 while j < i {
351 sum = (-row[j]).mul_add(x[j], sum);
352 j += 1;
353 }
354 if !sum.is_finite() {
355 cold_path();
356 return Err(LaError::non_finite_computation_step(
357 ArithmeticOperation::LdltSolve,
358 i,
359 ));
360 }
361 x[i] = sum;
362 i += 1;
363 }
364
365 let mut i = 0;
367 while i < D {
368 let diag = self.factors.diag(i);
369
370 let quotient = x[i] / diag;
371 if !quotient.is_finite() {
372 cold_path();
373 return Err(LaError::non_finite_computation_step(
374 ArithmeticOperation::LdltSolve,
375 i,
376 ));
377 }
378 x[i] = quotient;
379 i += 1;
380 }
381
382 if D <= 4 {
383 let mut ii = 0;
386 while ii < D {
387 let i = D - 1 - ii;
388 let mut sum = x[i];
389 let mut j = i + 1;
390 while j < D {
391 sum = (-self.factors.row(j)[i]).mul_add(x[j], sum);
392 j += 1;
393 }
394 if !sum.is_finite() {
395 cold_path();
396 return Err(LaError::non_finite_computation_step(
397 ArithmeticOperation::LdltSolve,
398 i,
399 ));
400 }
401 x[i] = sum;
402 ii += 1;
403 }
404 } else {
405 let mut jj = D;
409 while jj > 0 {
410 jj -= 1;
411
412 let x_j = x[jj];
413 if !x_j.is_finite() {
414 cold_path();
415 return Err(LaError::non_finite_computation_step(
416 ArithmeticOperation::LdltSolve,
417 jj,
418 ));
419 }
420
421 let row = self.factors.row(jj);
422 let mut i = 0;
423 while i < jj {
424 x[i] = (-row[i]).mul_add(x_j, x[i]);
425 i += 1;
426 }
427 }
428 }
429
430 Vector::from_computation(x, ArithmeticOperation::LdltSolve)
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use core::hint::black_box;
437
438 use approx::assert_abs_diff_eq;
439 use pastey::paste;
440
441 use super::*;
442 use crate::DEFAULT_SINGULAR_TOL;
443 use crate::matrix::Matrix;
444
445 const TWO_NEG_800: f64 = f64::from_bits(223_u64 << 52);
446 const TWO_NEG_38: f64 = f64::from_bits(985_u64 << 52);
447 const TWO_POS_43: f64 = f64::from_bits(1066_u64 << 52);
448 const TWO_POS_800: f64 = f64::from_bits(1823_u64 << 52);
449
450 macro_rules! gen_ldlt_identity_tests {
451 ($d:literal) => {
452 paste! {
453 #[test]
454 fn [<ldlt_det_and_solve_identity_ $d d>]() {
455 let a = Matrix::<$d>::identity();
456 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
457
458 assert_abs_diff_eq!(ldlt.det().unwrap(), 1.0, epsilon = 1e-12);
459
460 let b_arr = {
461 let mut arr = [0.0f64; $d];
462 let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
463 for (dst, src) in arr.iter_mut().zip(values.iter()) {
464 *dst = *src;
465 }
466 arr
467 };
468 let b = Vector::<$d>::new(black_box(b_arr));
469 let x = ldlt.solve(b).unwrap().into_array();
470
471 for i in 0..$d {
472 assert_abs_diff_eq!(x[i], b_arr[i], epsilon = 1e-12);
473 }
474 }
475 }
476 };
477 }
478
479 gen_ldlt_identity_tests!(2);
480 gen_ldlt_identity_tests!(3);
481 gen_ldlt_identity_tests!(4);
482 gen_ldlt_identity_tests!(5);
483
484 macro_rules! gen_ldlt_diagonal_tests {
485 ($d:literal) => {
486 paste! {
487 #[test]
488 fn [<ldlt_det_and_solve_diagonal_spd_ $d d>]() {
489 let diag = {
490 let mut arr = [0.0f64; $d];
491 let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
492 for (dst, src) in arr.iter_mut().zip(values.iter()) {
493 *dst = *src;
494 }
495 arr
496 };
497
498 let mut rows = [[0.0f64; $d]; $d];
499 for i in 0..$d {
500 rows[i][i] = diag[i];
501 }
502
503 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
504 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
505
506 let expected_det = {
507 let mut acc = 1.0;
508 for i in 0..$d {
509 acc *= diag[i];
510 }
511 acc
512 };
513 assert_abs_diff_eq!(ldlt.det().unwrap(), expected_det, epsilon = 1e-12);
514
515 let b_arr = {
516 let mut arr = [0.0f64; $d];
517 let values = [5.0f64, 4.0, 3.0, 2.0, 1.0];
518 for (dst, src) in arr.iter_mut().zip(values.iter()) {
519 *dst = *src;
520 }
521 arr
522 };
523
524 let b = Vector::<$d>::new(black_box(b_arr));
525 let x = ldlt.solve(b).unwrap().into_array();
526
527 for i in 0..$d {
528 assert_abs_diff_eq!(x[i], b_arr[i] / diag[i], epsilon = 1e-12);
529 }
530 }
531 }
532 };
533 }
534
535 gen_ldlt_diagonal_tests!(2);
536 gen_ldlt_diagonal_tests!(3);
537 gen_ldlt_diagonal_tests!(4);
538 gen_ldlt_diagonal_tests!(5);
539
540 #[test]
541 fn solve_0x0_returns_empty_vector_and_unit_det() {
542 let a = Matrix::<0>::zero();
543 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
544
545 assert_eq!(ldlt.det(), Ok(1.0));
546 assert!(
547 ldlt.solve(Vector::<0>::zero())
548 .unwrap()
549 .into_array()
550 .is_empty()
551 );
552 }
553
554 #[test]
555 fn solve_2x2_known_spd() {
556 let a = Matrix::<2>::try_from_rows(black_box([[4.0, 2.0], [2.0, 3.0]])).unwrap();
557 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
558
559 let b = Vector::<2>::new(black_box([1.0, 2.0]));
560 let x = ldlt.solve(b).unwrap().into_array();
561
562 assert_abs_diff_eq!(x[0], -0.125, epsilon = 1e-12);
563 assert_abs_diff_eq!(x[1], 0.75, epsilon = 1e-12);
564 assert_abs_diff_eq!(ldlt.det().unwrap(), 8.0, epsilon = 1e-12);
565 }
566
567 #[test]
568 fn det_ordinary_factors_matches_direct_product_bits() {
569 let diagonal = [1.5, 2.0, 0.25, 8.0];
570 let mut rows = [[0.0; 4]; 4];
571 let mut expected = 1.0;
572 for (i, factor) in diagonal.into_iter().enumerate() {
573 rows[i][i] = factor;
574 expected *= factor;
575 }
576
577 let ldlt = Matrix::<4>::try_from_rows(rows)
578 .unwrap()
579 .ldlt(DEFAULT_SINGULAR_TOL)
580 .unwrap();
581 assert_eq!(ldlt.det().unwrap().to_bits(), expected.to_bits());
582 }
583
584 #[test]
585 fn solve_3x3_spd_tridiagonal_smoke() {
586 let a = Matrix::<3>::try_from_rows(black_box([
587 [2.0, -1.0, 0.0],
588 [-1.0, 2.0, -1.0],
589 [0.0, -1.0, 2.0],
590 ]))
591 .unwrap();
592 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
593
594 let b = Vector::<3>::new(black_box([1.0, 0.0, 1.0]));
596 let x = ldlt.solve(b).unwrap().into_array();
597
598 for &x_i in &x {
599 assert_abs_diff_eq!(x_i, 1.0, epsilon = 1e-9);
600 }
601 }
602
603 #[test]
604 fn singular_detected_for_degenerate_psd() {
605 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 1.0], [1.0, 1.0]])).unwrap();
607 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
608 assert_eq!(
609 err,
610 LaError::singular_numerical(
611 1,
612 FactorizationKind::Ldlt,
613 0.0,
614 DEFAULT_SINGULAR_TOL.get()
615 )
616 );
617 }
618
619 #[test]
620 fn zero_pivot_with_nonzero_coupling_is_not_reported_as_singular() {
621 let a = Matrix::<2>::try_from_rows(black_box([[0.0, 1.0], [1.0, 0.0]])).unwrap();
622 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
623 assert_eq!(
624 err,
625 LaError::not_positive_semidefinite_zero_coupling(0, 1, 1.0)
626 );
627 }
628
629 #[test]
630 fn zero_pivot_reports_non_finite_coupling_before_domain_violation() {
631 let a = Matrix::<3>::try_from_rows(black_box([
632 [1.0, 1.0, f64::MAX],
633 [1.0, 1.0, -f64::MAX],
634 [f64::MAX, -f64::MAX, 1.0],
635 ]))
636 .unwrap();
637 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
638 assert_eq!(
639 err,
640 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1,)
641 );
642 }
643
644 #[test]
645 fn small_positive_pivot_reports_numerical_singularity() {
646 let a = Matrix::<2>::try_from_rows(black_box([[1e-13, 0.0], [0.0, 1.0]])).unwrap();
647 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
648 assert_eq!(
649 err,
650 LaError::singular_numerical(
651 0,
652 FactorizationKind::Ldlt,
653 1e-13,
654 DEFAULT_SINGULAR_TOL.get()
655 )
656 );
657 }
658
659 #[test]
660 fn small_positive_pivot_does_not_mask_earlier_non_finite_update() {
661 let a = Matrix::<3>::try_from_rows(black_box([
662 [1.0, 1.0, f64::MAX],
663 [1.0, 1.0 + f64::EPSILON, -f64::MAX],
664 [f64::MAX, -f64::MAX, 1.0],
665 ]))
666 .unwrap();
667
668 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
669 assert_eq!(
670 err,
671 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1)
672 );
673 }
674
675 #[test]
676 fn negative_initial_diagonal_reports_not_positive_semidefinite() {
677 let a = Matrix::<2>::try_from_rows(black_box([[-1.0, 0.0], [0.0, 1.0]])).unwrap();
678 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
679 assert_eq!(err, LaError::not_positive_semidefinite_negative(0, -1.0));
680 }
681
682 #[test]
683 fn negative_updated_diagonal_reports_not_positive_semidefinite() {
684 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [2.0, 1.0]])).unwrap();
685 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
686 assert_eq!(err, LaError::not_positive_semidefinite_negative(1, -3.0));
687 }
688
689 #[test]
690 fn negative_pivot_does_not_mask_earlier_non_finite_update() {
691 let a = Matrix::<3>::try_from_rows(black_box([
692 [1.0, 2.0, f64::MAX],
693 [2.0, 1.0, 0.0],
694 [f64::MAX, 0.0, 1.0],
695 ]))
696 .unwrap();
697
698 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
699 assert_eq!(
700 err,
701 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1,)
702 );
703 }
704
705 #[test]
706 fn non_finite_l_multiplier_overflow() {
707 let a = Matrix::<2>::try_from_rows([[1e-11, 1e300], [1e300, 1.0]]).unwrap();
709 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
710 assert_eq!(
711 err,
712 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 1, 0)
713 );
714 }
715
716 #[test]
717 fn non_finite_l_multiplier_overflow_fused_branch_6d() {
718 let mut rows = [[0.0; 6]; 6];
721 for (i, row) in rows.iter_mut().enumerate() {
722 row[i] = 1.0;
723 }
724 rows[0][0] = 1e-11;
725 rows[0][5] = 1e300;
726 rows[5][0] = 1e300;
727
728 let a = Matrix::<6>::try_from_rows(rows).unwrap();
729 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
730 assert_eq!(
731 err,
732 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 5, 0)
733 );
734 }
735
736 #[test]
737 fn non_finite_trailing_submatrix_overflow() {
738 let a = Matrix::<2>::try_from_rows([[1.0, 1e200], [1e200, 1.0]]).unwrap();
741 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
742 assert_eq!(
743 err,
744 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 1, 1)
745 );
746 }
747
748 #[test]
749 fn non_finite_trailing_submatrix_overflow_fused_branch_6d() {
750 let mut rows = [[0.0; 6]; 6];
753 for (i, row) in rows.iter_mut().enumerate() {
754 row[i] = 1.0;
755 }
756 rows[0][5] = 1e200;
757 rows[5][0] = 1e200;
758
759 let a = Matrix::<6>::try_from_rows(rows).unwrap();
760 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
761 assert_eq!(
762 err,
763 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 5, 5)
764 );
765 }
766
767 #[test]
768 fn non_finite_solve_forward_substitution_overflow() {
769 let a = Matrix::<3>::try_from_rows([
772 [1.0, 1e153, 0.0],
773 [1e153, 1e306 + 1.0, 0.0],
774 [0.0, 0.0, 1.0],
775 ])
776 .unwrap();
777 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
778
779 let b = Vector::<3>::new([1e156, 0.0, 0.0]);
780 let err = ldlt.solve(b).unwrap_err();
781 assert_eq!(
782 err,
783 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 1)
784 );
785 }
786
787 #[test]
788 fn non_finite_solve_back_substitution_overflow() {
789 let a = Matrix::<3>::try_from_rows([[1.0, 0.0, 0.0], [0.0, 1.0, 2.0], [0.0, 2.0, 5.0]])
794 .unwrap();
795 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
796
797 let b = Vector::<3>::new([0.0, 0.0, 1e308]);
798 let err = ldlt.solve(b).unwrap_err();
799 assert_eq!(
800 err,
801 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 1)
802 );
803 }
804
805 #[test]
806 fn non_finite_solve_back_substitution_overflow_scatter_branch_5d() {
807 let a = Matrix::<5>::try_from_rows([
810 [1.0, 0.0, 0.0, 0.0, 0.0],
811 [0.0, 1.0, 0.0, 0.0, 0.0],
812 [0.0, 0.0, 1.0, 0.0, 0.0],
813 [0.0, 0.0, 0.0, 1.0, 2.0],
814 [0.0, 0.0, 0.0, 2.0, 5.0],
815 ])
816 .unwrap();
817 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
818
819 let b = Vector::<5>::new([0.0, 0.0, 0.0, 0.0, 1e308]);
820 let err = ldlt.solve(b).unwrap_err();
821 assert_eq!(
822 err,
823 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 3)
824 );
825 }
826
827 #[test]
828 fn non_finite_solve_diagonal_solve_overflow() {
829 let a = Matrix::<2>::try_from_rows([[1.0, 0.0], [0.0, 1.0e-11]]).unwrap();
835 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
836
837 let b = Vector::<2>::new([0.0, 1.0e300]);
838 let err = ldlt.solve(b).unwrap_err();
839 assert_eq!(
840 err,
841 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 1)
842 );
843 }
844
845 #[test]
846 fn det_rejects_product_overflow() {
847 let a = Matrix::<5>::try_from_rows([
848 [1.0e100, 0.0, 0.0, 0.0, 0.0],
849 [0.0, 1.0e100, 0.0, 0.0, 0.0],
850 [0.0, 0.0, 1.0e100, 0.0, 0.0],
851 [0.0, 0.0, 0.0, 1.0e100, 0.0],
852 [0.0, 0.0, 0.0, 0.0, 1.0e100],
853 ])
854 .unwrap();
855 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
856 assert_eq!(
857 ldlt.det(),
858 Err(LaError::non_finite_computation_step(
859 ArithmeticOperation::Determinant,
860 4
861 ))
862 );
863 }
864
865 #[test]
866 fn det_balances_extreme_diagonals_independently_of_storage_order() {
867 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
868 for diagonal in [
869 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800],
870 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800],
871 ] {
872 let mut rows = [[0.0; 4]; 4];
873 for (i, value) in diagonal.into_iter().enumerate() {
874 rows[i][i] = value;
875 }
876
877 let ldlt = Matrix::<4>::try_from_rows(rows)
878 .unwrap()
879 .ldlt(zero_tolerance)
880 .unwrap();
881 assert_eq!(ldlt.det(), Ok(1.0));
882 }
883 }
884
885 #[test]
886 fn det_balances_extreme_diagonals_in_large_dimension() {
887 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
888 for diagonal in [
889 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800, 1.0, 1.0],
890 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800, 1.0, 1.0],
891 ] {
892 let mut rows = [[0.0; 6]; 6];
893 for (i, value) in diagonal.into_iter().enumerate() {
894 rows[i][i] = value;
895 }
896
897 let ldlt = Matrix::<6>::try_from_rows(rows)
898 .unwrap()
899 .ldlt(zero_tolerance)
900 .unwrap();
901 assert_eq!(ldlt.det(), Ok(1.0));
902 }
903 }
904
905 #[test]
906 fn det_rounds_final_tiny_magnitude_to_zero() {
907 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
908 let matrix = Matrix::<2>::try_from_rows([[TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
909 let det = matrix.ldlt(zero_tolerance).unwrap().det().unwrap();
910
911 assert_eq!(det.to_bits(), 0.0f64.to_bits());
912 }
913
914 #[test]
915 fn ldlt_d1_classifies_positive_zero_and_negative_inputs() {
916 let positive = Matrix::<1>::try_from_rows([[2.0]])
917 .unwrap()
918 .ldlt(DEFAULT_SINGULAR_TOL)
919 .unwrap();
920 assert_eq!(positive.det(), Ok(2.0));
921 assert_abs_diff_eq!(
922 positive
923 .solve(Vector::<1>::new([6.0]))
924 .unwrap()
925 .into_array()[0],
926 3.0,
927 epsilon = 0.0
928 );
929
930 let zero = Matrix::<1>::try_from_rows([[0.0]])
931 .unwrap()
932 .ldlt(DEFAULT_SINGULAR_TOL);
933 assert_eq!(
934 zero,
935 Err(LaError::singular_numerical(
936 0,
937 FactorizationKind::Ldlt,
938 0.0,
939 DEFAULT_SINGULAR_TOL.get()
940 ))
941 );
942
943 let negative = Matrix::<1>::try_from_rows([[-1.0]])
944 .unwrap()
945 .ldlt(DEFAULT_SINGULAR_TOL);
946 assert_eq!(
947 negative,
948 Err(LaError::not_positive_semidefinite_negative(0, -1.0))
949 );
950 }
951
952 fn nontrivial_spd_system<const D: usize>() -> (Matrix<D>, Vector<D>, [f64; D], f64) {
955 let mut rows = [[0.0_f64; D]; D];
956 let mut expected_det = 1.0_f64;
957 let mut diagonal = 1.0_f64;
958 let mut k = 0;
959 while k < D {
960 rows[k][k] += diagonal;
961 expected_det *= diagonal;
962 if k + 1 < D {
963 let off_diagonal = 0.5 * diagonal;
964 rows[k][k + 1] += off_diagonal;
965 rows[k + 1][k] += off_diagonal;
966 rows[k + 1][k + 1] = 0.25_f64.mul_add(diagonal, rows[k + 1][k + 1]);
967 }
968 diagonal += 1.0;
969 k += 1;
970 }
971
972 let mut expected_x = [0.0_f64; D];
973 let mut value = 1.0_f64;
974 for entry in &mut expected_x {
975 *entry = value;
976 value += 1.0;
977 }
978 let rhs = core::array::from_fn(|row| {
979 rows[row]
980 .iter()
981 .zip(expected_x.iter())
982 .fold(0.0_f64, |sum, (&coefficient, &x)| {
983 coefficient.mul_add(x, sum)
984 })
985 });
986
987 (
988 Matrix::<D>::try_from_rows(rows).unwrap(),
989 Vector::<D>::try_new(rhs).unwrap(),
990 expected_x,
991 expected_det,
992 )
993 }
994
995 macro_rules! gen_nontrivial_large_ldlt_tests {
996 ($d:literal) => {
997 paste! {
998 #[test]
999 fn [<ldlt_nontrivial_success_solve_and_det_agree_ $d d>]() {
1000 let (matrix, rhs, expected_x, expected_det) =
1001 nontrivial_spd_system::<$d>();
1002 let ldlt = matrix.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
1003 let solution = ldlt.solve(rhs).unwrap().into_array();
1004
1005 for (actual, expected) in solution.into_iter().zip(expected_x) {
1006 assert_abs_diff_eq!(actual, expected, epsilon = 1e-12);
1007 }
1008 assert_abs_diff_eq!(ldlt.det().unwrap(), expected_det, epsilon = 1e-12);
1009 assert_abs_diff_eq!(matrix.det().unwrap(), expected_det, epsilon = 1e-10);
1010 }
1011 }
1012 };
1013 }
1014
1015 gen_nontrivial_large_ldlt_tests!(6);
1016 gen_nontrivial_large_ldlt_tests!(8);
1017
1018 #[test]
1019 fn asymmetric_input_returns_typed_error() {
1020 let a = Matrix::<3>::try_from_rows([[4.0, 2.0, 0.0], [-2.0, 5.0, 1.0], [0.0, 1.0, 3.0]])
1022 .unwrap();
1023 assert_eq!(
1024 a.ldlt(DEFAULT_SINGULAR_TOL),
1025 Err(LaError::asymmetric(0, 1, 3, 2.0, -2.0, 0.0))
1026 );
1027 }
1028
1029 #[test]
1030 fn approximately_symmetric_input_is_rejected_before_factoring_another_operator() {
1031 let matrix = Matrix::<2>::try_from_rows([[TWO_POS_43, 0.0], [4.0, TWO_NEG_38]]).unwrap();
1036 let diagnostic_tolerance = Tolerance::try_new(1e-12).unwrap();
1037
1038 assert_eq!(matrix.det(), Ok(32.0));
1039 assert_eq!(matrix.is_symmetric(diagnostic_tolerance), Ok(true));
1040 assert_eq!(
1041 matrix.ldlt(DEFAULT_SINGULAR_TOL),
1042 Err(LaError::asymmetric(0, 1, 2, 0.0, 4.0, 0.0))
1043 );
1044 }
1045
1046 macro_rules! gen_ldlt_const_eval_tests {
1057 ($d:literal) => {
1058 paste! {
1059 #[test]
1064 fn [<ldlt_det_const_eval_ $d d>]() {
1065 const DET: Result<f64, LaError> = {
1066 let mut rows = [[0.0f64; $d]; $d];
1067 let mut i = 0;
1068 while i < $d {
1069 rows[i][i] = 1.0;
1070 i += 1;
1071 }
1072 rows[0][0] = 2.0;
1073 let factors = LdltFactors::from_proven_rows(rows);
1074 let ldlt = Ldlt::<$d> { factors };
1075 ldlt.det()
1076 };
1077 assert_eq!(DET, Ok(2.0));
1078 }
1079
1080 #[test]
1085 fn [<ldlt_solve_const_eval_ $d d>]() {
1086 #[expect(
1087 clippy::cast_precision_loss,
1088 reason = "test indices are at most five and exactly representable as f64"
1089 )]
1090 const X: Result<Vector<$d>, LaError> = {
1091 let factors = LdltFactors::from_proven_rows(
1092 Matrix::<$d>::identity().into_rows()
1093 );
1094 let ldlt = Ldlt::<$d> { factors };
1095 let mut b_arr = [0.0f64; $d];
1096 let mut i = 0;
1097 while i < $d {
1098 b_arr[i] = i as f64 + 1.0;
1099 i += 1;
1100 }
1101 let b = Vector::<$d>::new(b_arr);
1102 ldlt.solve(b)
1103 };
1104 let x = X.unwrap().into_array();
1105 #[expect(
1106 clippy::cast_precision_loss,
1107 reason = "test indices are at most five and exactly representable as f64"
1108 )]
1109 for i in 0..$d {
1110 let expected = i as f64 + 1.0;
1111 assert!((x[i] - expected).abs() <= 1e-12);
1112 }
1113 }
1114 }
1115 };
1116 }
1117
1118 gen_ldlt_const_eval_tests!(2);
1119 gen_ldlt_const_eval_tests!(3);
1120 gen_ldlt_const_eval_tests!(4);
1121 gen_ldlt_const_eval_tests!(5);
1122}