1#![forbid(unsafe_code)]
2
3use core::hint::cold_path;
21
22use crate::matrix::SymmetricMatrix;
23use crate::scaled_product::{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
280 if D <= 4 {
281 while i < D {
284 let step = range_checked_product(det, self.factors.diag(i));
285 if !step.range_preserved() {
286 cold_path();
287 return self.scaled_det();
288 }
289 det = step.product();
290 i += 1;
291 }
292 return Ok(det);
293 }
294
295 let mut range_preserved = true;
296 while i < D {
297 let factor = self.factors.diag(i);
298 let step = range_checked_product(det, factor);
299 det = step.product();
300 range_preserved &= step.range_preserved();
301 i += 1;
302 }
303 if range_preserved {
304 Ok(det)
305 } else {
306 cold_path();
307 self.scaled_det()
308 }
309 }
310
311 #[cold]
313 const fn scaled_det(&self) -> Result<f64, LaError> {
314 let mut product = ScaledProduct::new(false);
315 let mut i = 0;
316 while i < D {
317 product.multiply(self.factors.diag(i));
318 i += 1;
319 }
320
321 if let Some(det) = product.finish() {
322 Ok(det)
323 } else {
324 Err(LaError::non_finite_computation_step(
325 ArithmeticOperation::Determinant,
326 D.saturating_sub(1),
327 ))
328 }
329 }
330
331 #[inline]
359 pub const fn solve(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
360 let mut x = b.into_array();
361
362 let mut i = 0;
364 while i < D {
365 let mut sum = x[i];
366 let row = self.factors.row(i);
367 let mut j = 0;
368 while j < i {
369 sum = (-row[j]).mul_add(x[j], sum);
370 j += 1;
371 }
372 if !sum.is_finite() {
373 cold_path();
374 return Err(LaError::non_finite_computation_step(
375 ArithmeticOperation::LdltSolve,
376 i,
377 ));
378 }
379 x[i] = sum;
380 i += 1;
381 }
382
383 let mut i = 0;
385 while i < D {
386 let diag = self.factors.diag(i);
387
388 let quotient = x[i] / diag;
389 if !quotient.is_finite() {
390 cold_path();
391 return Err(LaError::non_finite_computation_step(
392 ArithmeticOperation::LdltSolve,
393 i,
394 ));
395 }
396 x[i] = quotient;
397 i += 1;
398 }
399
400 if D <= 4 {
401 let mut ii = 0;
404 while ii < D {
405 let i = D - 1 - ii;
406 let mut sum = x[i];
407 let mut j = i + 1;
408 while j < D {
409 sum = (-self.factors.row(j)[i]).mul_add(x[j], sum);
410 j += 1;
411 }
412 if !sum.is_finite() {
413 cold_path();
414 return Err(LaError::non_finite_computation_step(
415 ArithmeticOperation::LdltSolve,
416 i,
417 ));
418 }
419 x[i] = sum;
420 ii += 1;
421 }
422 } else {
423 let mut jj = D;
427 while jj > 0 {
428 jj -= 1;
429
430 let x_j = x[jj];
431 if !x_j.is_finite() {
432 cold_path();
433 return Err(LaError::non_finite_computation_step(
434 ArithmeticOperation::LdltSolve,
435 jj,
436 ));
437 }
438
439 let row = self.factors.row(jj);
440 let mut i = 0;
441 while i < jj {
442 x[i] = (-row[i]).mul_add(x_j, x[i]);
443 i += 1;
444 }
445 }
446 }
447
448 Vector::from_computation(x, ArithmeticOperation::LdltSolve)
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use core::hint::black_box;
455
456 use approx::assert_abs_diff_eq;
457 use pastey::paste;
458
459 use super::*;
460 use crate::DEFAULT_SINGULAR_TOL;
461 use crate::matrix::Matrix;
462
463 const TWO_NEG_800: f64 = f64::from_bits(223_u64 << 52);
464 const TWO_NEG_38: f64 = f64::from_bits(985_u64 << 52);
465 const TWO_POS_43: f64 = f64::from_bits(1066_u64 << 52);
466 const TWO_POS_800: f64 = f64::from_bits(1823_u64 << 52);
467
468 macro_rules! gen_ldlt_identity_tests {
469 ($d:literal) => {
470 paste! {
471 #[test]
472 fn [<ldlt_det_and_solve_identity_ $d d>]() {
473 let a = Matrix::<$d>::identity();
474 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
475
476 assert_abs_diff_eq!(ldlt.det().unwrap(), 1.0, epsilon = 1e-12);
477
478 let b_arr = {
479 let mut arr = [0.0f64; $d];
480 let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
481 for (dst, src) in arr.iter_mut().zip(values.iter()) {
482 *dst = *src;
483 }
484 arr
485 };
486 let b = Vector::<$d>::new(black_box(b_arr));
487 let x = ldlt.solve(b).unwrap().into_array();
488
489 for i in 0..$d {
490 assert_abs_diff_eq!(x[i], b_arr[i], epsilon = 1e-12);
491 }
492 }
493 }
494 };
495 }
496
497 gen_ldlt_identity_tests!(2);
498 gen_ldlt_identity_tests!(3);
499 gen_ldlt_identity_tests!(4);
500 gen_ldlt_identity_tests!(5);
501
502 macro_rules! gen_ldlt_diagonal_tests {
503 ($d:literal) => {
504 paste! {
505 #[test]
506 fn [<ldlt_det_and_solve_diagonal_spd_ $d d>]() {
507 let diag = {
508 let mut arr = [0.0f64; $d];
509 let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
510 for (dst, src) in arr.iter_mut().zip(values.iter()) {
511 *dst = *src;
512 }
513 arr
514 };
515
516 let mut rows = [[0.0f64; $d]; $d];
517 for i in 0..$d {
518 rows[i][i] = diag[i];
519 }
520
521 let a = Matrix::<$d>::try_from_rows(black_box(rows)).unwrap();
522 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
523
524 let expected_det = {
525 let mut acc = 1.0;
526 for i in 0..$d {
527 acc *= diag[i];
528 }
529 acc
530 };
531 assert_abs_diff_eq!(ldlt.det().unwrap(), expected_det, epsilon = 1e-12);
532
533 let b_arr = {
534 let mut arr = [0.0f64; $d];
535 let values = [5.0f64, 4.0, 3.0, 2.0, 1.0];
536 for (dst, src) in arr.iter_mut().zip(values.iter()) {
537 *dst = *src;
538 }
539 arr
540 };
541
542 let b = Vector::<$d>::new(black_box(b_arr));
543 let x = ldlt.solve(b).unwrap().into_array();
544
545 for i in 0..$d {
546 assert_abs_diff_eq!(x[i], b_arr[i] / diag[i], epsilon = 1e-12);
547 }
548 }
549 }
550 };
551 }
552
553 gen_ldlt_diagonal_tests!(2);
554 gen_ldlt_diagonal_tests!(3);
555 gen_ldlt_diagonal_tests!(4);
556 gen_ldlt_diagonal_tests!(5);
557
558 #[test]
559 fn solve_0x0_returns_empty_vector_and_unit_det() {
560 let a = Matrix::<0>::zero();
561 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
562
563 assert_eq!(ldlt.det(), Ok(1.0));
564 assert_eq!(
565 ldlt.solve(Vector::<0>::zero())
566 .unwrap()
567 .into_array()
568 .as_slice(),
569 []
570 );
571 }
572
573 #[test]
574 fn solve_2x2_known_spd() {
575 let a = Matrix::<2>::try_from_rows(black_box([[4.0, 2.0], [2.0, 3.0]])).unwrap();
576 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
577
578 let b = Vector::<2>::new(black_box([1.0, 2.0]));
579 let x = ldlt.solve(b).unwrap().into_array();
580
581 assert_abs_diff_eq!(x[0], -0.125, epsilon = 1e-12);
582 assert_abs_diff_eq!(x[1], 0.75, epsilon = 1e-12);
583 assert_abs_diff_eq!(ldlt.det().unwrap(), 8.0, epsilon = 1e-12);
584 }
585
586 #[test]
587 fn det_ordinary_factors_matches_direct_product_bits() {
588 let diagonal = [1.5, 2.0, 0.25, 8.0];
589 let mut rows = [[0.0; 4]; 4];
590 let mut expected = 1.0;
591 for (i, factor) in diagonal.into_iter().enumerate() {
592 rows[i][i] = factor;
593 expected *= factor;
594 }
595
596 let ldlt = Matrix::<4>::try_from_rows(rows)
597 .unwrap()
598 .ldlt(DEFAULT_SINGULAR_TOL)
599 .unwrap();
600 assert_eq!(ldlt.det().unwrap().to_bits(), expected.to_bits());
601 }
602
603 #[test]
604 fn solve_3x3_spd_tridiagonal_smoke() {
605 let a = Matrix::<3>::try_from_rows(black_box([
606 [2.0, -1.0, 0.0],
607 [-1.0, 2.0, -1.0],
608 [0.0, -1.0, 2.0],
609 ]))
610 .unwrap();
611 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
612
613 let b = Vector::<3>::new(black_box([1.0, 0.0, 1.0]));
615 let x = ldlt.solve(b).unwrap().into_array();
616
617 for &x_i in &x {
618 assert_abs_diff_eq!(x_i, 1.0, epsilon = 1e-9);
619 }
620 }
621
622 #[test]
623 fn singular_detected_for_degenerate_psd() {
624 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 1.0], [1.0, 1.0]])).unwrap();
626 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
627 assert_eq!(
628 err,
629 LaError::singular_numerical(
630 1,
631 FactorizationKind::Ldlt,
632 0.0,
633 DEFAULT_SINGULAR_TOL.get()
634 )
635 );
636 }
637
638 #[test]
639 fn zero_pivot_with_nonzero_coupling_is_not_reported_as_singular() {
640 let a = Matrix::<2>::try_from_rows(black_box([[0.0, 1.0], [1.0, 0.0]])).unwrap();
641 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
642 assert_eq!(
643 err,
644 LaError::not_positive_semidefinite_zero_coupling(0, 1, 1.0)
645 );
646 }
647
648 #[test]
649 fn zero_pivot_reports_non_finite_coupling_before_domain_violation() {
650 let a = Matrix::<3>::try_from_rows(black_box([
651 [1.0, 1.0, f64::MAX],
652 [1.0, 1.0, -f64::MAX],
653 [f64::MAX, -f64::MAX, 1.0],
654 ]))
655 .unwrap();
656 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
657 assert_eq!(
658 err,
659 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1,)
660 );
661 }
662
663 #[test]
664 fn small_positive_pivot_reports_numerical_singularity() {
665 let a = Matrix::<2>::try_from_rows(black_box([[1e-13, 0.0], [0.0, 1.0]])).unwrap();
666 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
667 assert_eq!(
668 err,
669 LaError::singular_numerical(
670 0,
671 FactorizationKind::Ldlt,
672 1e-13,
673 DEFAULT_SINGULAR_TOL.get()
674 )
675 );
676 }
677
678 #[test]
679 fn small_positive_pivot_does_not_mask_earlier_non_finite_update() {
680 let a = Matrix::<3>::try_from_rows(black_box([
681 [1.0, 1.0, f64::MAX],
682 [1.0, 1.0 + f64::EPSILON, -f64::MAX],
683 [f64::MAX, -f64::MAX, 1.0],
684 ]))
685 .unwrap();
686
687 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
688 assert_eq!(
689 err,
690 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1)
691 );
692 }
693
694 #[test]
695 fn negative_initial_diagonal_reports_not_positive_semidefinite() {
696 let a = Matrix::<2>::try_from_rows(black_box([[-1.0, 0.0], [0.0, 1.0]])).unwrap();
697 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
698 assert_eq!(err, LaError::not_positive_semidefinite_negative(0, -1.0));
699 }
700
701 #[test]
702 fn negative_updated_diagonal_reports_not_positive_semidefinite() {
703 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [2.0, 1.0]])).unwrap();
704 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
705 assert_eq!(err, LaError::not_positive_semidefinite_negative(1, -3.0));
706 }
707
708 #[test]
709 fn negative_pivot_does_not_mask_earlier_non_finite_update() {
710 let a = Matrix::<3>::try_from_rows(black_box([
711 [1.0, 2.0, f64::MAX],
712 [2.0, 1.0, 0.0],
713 [f64::MAX, 0.0, 1.0],
714 ]))
715 .unwrap();
716
717 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
718 assert_eq!(
719 err,
720 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1,)
721 );
722 }
723
724 #[test]
725 fn non_finite_l_multiplier_overflow() {
726 let a = Matrix::<2>::try_from_rows([[1e-11, 1e300], [1e300, 1.0]]).unwrap();
728 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
729 assert_eq!(
730 err,
731 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 1, 0)
732 );
733 }
734
735 #[test]
736 fn non_finite_l_multiplier_overflow_fused_branch_6d() {
737 let mut rows = [[0.0; 6]; 6];
740 for (i, row) in rows.iter_mut().enumerate() {
741 row[i] = 1.0;
742 }
743 rows[0][0] = 1e-11;
744 rows[0][5] = 1e300;
745 rows[5][0] = 1e300;
746
747 let a = Matrix::<6>::try_from_rows(rows).unwrap();
748 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
749 assert_eq!(
750 err,
751 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 5, 0)
752 );
753 }
754
755 #[test]
756 fn non_finite_trailing_submatrix_overflow() {
757 let a = Matrix::<2>::try_from_rows([[1.0, 1e200], [1e200, 1.0]]).unwrap();
760 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
761 assert_eq!(
762 err,
763 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 1, 1)
764 );
765 }
766
767 #[test]
768 fn non_finite_trailing_submatrix_overflow_fused_branch_6d() {
769 let mut rows = [[0.0; 6]; 6];
772 for (i, row) in rows.iter_mut().enumerate() {
773 row[i] = 1.0;
774 }
775 rows[0][5] = 1e200;
776 rows[5][0] = 1e200;
777
778 let a = Matrix::<6>::try_from_rows(rows).unwrap();
779 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
780 assert_eq!(
781 err,
782 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 5, 5)
783 );
784 }
785
786 #[test]
787 fn non_finite_solve_forward_substitution_overflow() {
788 let a = Matrix::<3>::try_from_rows([
791 [1.0, 1e153, 0.0],
792 [1e153, 1e306 + 1.0, 0.0],
793 [0.0, 0.0, 1.0],
794 ])
795 .unwrap();
796 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
797
798 let b = Vector::<3>::new([1e156, 0.0, 0.0]);
799 let err = ldlt.solve(b).unwrap_err();
800 assert_eq!(
801 err,
802 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 1)
803 );
804 }
805
806 #[test]
807 fn non_finite_solve_back_substitution_overflow() {
808 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]])
813 .unwrap();
814 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
815
816 let b = Vector::<3>::new([0.0, 0.0, 1e308]);
817 let err = ldlt.solve(b).unwrap_err();
818 assert_eq!(
819 err,
820 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 1)
821 );
822 }
823
824 #[test]
825 fn non_finite_solve_back_substitution_overflow_scatter_branch_5d() {
826 let a = Matrix::<5>::try_from_rows([
829 [1.0, 0.0, 0.0, 0.0, 0.0],
830 [0.0, 1.0, 0.0, 0.0, 0.0],
831 [0.0, 0.0, 1.0, 0.0, 0.0],
832 [0.0, 0.0, 0.0, 1.0, 2.0],
833 [0.0, 0.0, 0.0, 2.0, 5.0],
834 ])
835 .unwrap();
836 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
837
838 let b = Vector::<5>::new([0.0, 0.0, 0.0, 0.0, 1e308]);
839 let err = ldlt.solve(b).unwrap_err();
840 assert_eq!(
841 err,
842 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 3)
843 );
844 }
845
846 #[test]
847 fn non_finite_solve_diagonal_solve_overflow() {
848 let a = Matrix::<2>::try_from_rows([[1.0, 0.0], [0.0, 1.0e-11]]).unwrap();
854 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
855
856 let b = Vector::<2>::new([0.0, 1.0e300]);
857 let err = ldlt.solve(b).unwrap_err();
858 assert_eq!(
859 err,
860 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 1)
861 );
862 }
863
864 #[test]
865 fn det_rejects_product_overflow() {
866 let a = Matrix::<5>::try_from_rows([
867 [1.0e100, 0.0, 0.0, 0.0, 0.0],
868 [0.0, 1.0e100, 0.0, 0.0, 0.0],
869 [0.0, 0.0, 1.0e100, 0.0, 0.0],
870 [0.0, 0.0, 0.0, 1.0e100, 0.0],
871 [0.0, 0.0, 0.0, 0.0, 1.0e100],
872 ])
873 .unwrap();
874 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
875 assert_eq!(
876 ldlt.det(),
877 Err(LaError::non_finite_computation_step(
878 ArithmeticOperation::Determinant,
879 4
880 ))
881 );
882 }
883
884 #[test]
885 fn det_balances_extreme_diagonals_independently_of_storage_order() {
886 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
887 for diagonal in [
888 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800],
889 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800],
890 ] {
891 let mut rows = [[0.0; 4]; 4];
892 for (i, value) in diagonal.into_iter().enumerate() {
893 rows[i][i] = value;
894 }
895
896 let ldlt = Matrix::<4>::try_from_rows(rows)
897 .unwrap()
898 .ldlt(zero_tolerance)
899 .unwrap();
900 assert_eq!(ldlt.det(), Ok(1.0));
901 }
902 }
903
904 #[test]
905 fn det_balances_extreme_diagonals_in_large_dimension() {
906 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
907 for diagonal in [
908 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800, 1.0, 1.0],
909 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800, 1.0, 1.0],
910 ] {
911 let mut rows = [[0.0; 6]; 6];
912 for (i, value) in diagonal.into_iter().enumerate() {
913 rows[i][i] = value;
914 }
915
916 let ldlt = Matrix::<6>::try_from_rows(rows)
917 .unwrap()
918 .ldlt(zero_tolerance)
919 .unwrap();
920 assert_eq!(ldlt.det(), Ok(1.0));
921 }
922 }
923
924 #[test]
925 fn det_rounds_final_tiny_magnitude_to_zero() {
926 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
927 let matrix = Matrix::<2>::try_from_rows([[TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
928 let det = matrix.ldlt(zero_tolerance).unwrap().det().unwrap();
929
930 assert_eq!(det.to_bits(), 0.0f64.to_bits());
931 }
932
933 #[test]
934 fn ldlt_d1_classifies_positive_zero_and_negative_inputs() {
935 let positive = Matrix::<1>::try_from_rows([[2.0]])
936 .unwrap()
937 .ldlt(DEFAULT_SINGULAR_TOL)
938 .unwrap();
939 assert_eq!(positive.det(), Ok(2.0));
940 assert_abs_diff_eq!(
941 positive
942 .solve(Vector::<1>::new([6.0]))
943 .unwrap()
944 .into_array()[0],
945 3.0,
946 epsilon = 0.0
947 );
948
949 let zero = Matrix::<1>::try_from_rows([[0.0]])
950 .unwrap()
951 .ldlt(DEFAULT_SINGULAR_TOL);
952 assert_eq!(
953 zero,
954 Err(LaError::singular_numerical(
955 0,
956 FactorizationKind::Ldlt,
957 0.0,
958 DEFAULT_SINGULAR_TOL.get()
959 ))
960 );
961
962 let negative = Matrix::<1>::try_from_rows([[-1.0]])
963 .unwrap()
964 .ldlt(DEFAULT_SINGULAR_TOL);
965 assert_eq!(
966 negative,
967 Err(LaError::not_positive_semidefinite_negative(0, -1.0))
968 );
969 }
970
971 fn nontrivial_spd_system<const D: usize>() -> (Matrix<D>, Vector<D>, [f64; D], f64) {
974 let mut rows = [[0.0_f64; D]; D];
975 let mut expected_det = 1.0_f64;
976 let mut diagonal = 1.0_f64;
977 let mut k = 0;
978 while k < D {
979 rows[k][k] += diagonal;
980 expected_det *= diagonal;
981 if k + 1 < D {
982 let off_diagonal = 0.5 * diagonal;
983 rows[k][k + 1] += off_diagonal;
984 rows[k + 1][k] += off_diagonal;
985 rows[k + 1][k + 1] = 0.25_f64.mul_add(diagonal, rows[k + 1][k + 1]);
986 }
987 diagonal += 1.0;
988 k += 1;
989 }
990
991 let mut expected_x = [0.0_f64; D];
992 let mut value = 1.0_f64;
993 for entry in &mut expected_x {
994 *entry = value;
995 value += 1.0;
996 }
997 let rhs = core::array::from_fn(|row| {
998 rows[row]
999 .iter()
1000 .zip(expected_x.iter())
1001 .fold(0.0_f64, |sum, (&coefficient, &x)| {
1002 coefficient.mul_add(x, sum)
1003 })
1004 });
1005
1006 (
1007 Matrix::<D>::try_from_rows(rows).unwrap(),
1008 Vector::<D>::try_new(rhs).unwrap(),
1009 expected_x,
1010 expected_det,
1011 )
1012 }
1013
1014 macro_rules! gen_nontrivial_large_ldlt_tests {
1015 ($d:literal) => {
1016 paste! {
1017 #[test]
1018 fn [<ldlt_nontrivial_success_solve_and_det_agree_ $d d>]() {
1019 let (matrix, rhs, expected_x, expected_det) =
1020 nontrivial_spd_system::<$d>();
1021 let ldlt = matrix.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
1022 let solution = ldlt.solve(rhs).unwrap().into_array();
1023
1024 for (actual, expected) in solution.into_iter().zip(expected_x) {
1025 assert_abs_diff_eq!(actual, expected, epsilon = 1e-12);
1026 }
1027 assert_abs_diff_eq!(ldlt.det().unwrap(), expected_det, epsilon = 1e-12);
1028 assert_abs_diff_eq!(matrix.det().unwrap(), expected_det, epsilon = 1e-10);
1029 }
1030 }
1031 };
1032 }
1033
1034 gen_nontrivial_large_ldlt_tests!(6);
1035 gen_nontrivial_large_ldlt_tests!(8);
1036
1037 #[test]
1038 fn asymmetric_input_returns_typed_error() {
1039 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]])
1041 .unwrap();
1042 assert_eq!(
1043 a.ldlt(DEFAULT_SINGULAR_TOL),
1044 Err(LaError::asymmetric(0, 1, 3, 2.0, -2.0, 0.0))
1045 );
1046 }
1047
1048 #[test]
1049 fn approximately_symmetric_input_is_rejected_before_factoring_another_operator() {
1050 let matrix = Matrix::<2>::try_from_rows([[TWO_POS_43, 0.0], [4.0, TWO_NEG_38]]).unwrap();
1055 let diagnostic_tolerance = Tolerance::try_new(1e-12).unwrap();
1056
1057 assert_eq!(matrix.det(), Ok(32.0));
1058 assert_eq!(matrix.is_symmetric(diagnostic_tolerance), Ok(true));
1059 assert_eq!(
1060 matrix.ldlt(DEFAULT_SINGULAR_TOL),
1061 Err(LaError::asymmetric(0, 1, 2, 0.0, 4.0, 0.0))
1062 );
1063 }
1064
1065 macro_rules! gen_ldlt_const_eval_tests {
1076 ($d:literal) => {
1077 paste! {
1078 #[test]
1083 fn [<ldlt_det_const_eval_ $d d>]() {
1084 const DET: Result<f64, LaError> = {
1085 let mut rows = [[0.0f64; $d]; $d];
1086 let mut i = 0;
1087 while i < $d {
1088 rows[i][i] = 1.0;
1089 i += 1;
1090 }
1091 rows[0][0] = 2.0;
1092 let factors = LdltFactors::from_proven_rows(rows);
1093 let ldlt = Ldlt::<$d> { factors };
1094 ldlt.det()
1095 };
1096 assert_eq!(DET, Ok(2.0));
1097 }
1098
1099 #[test]
1104 fn [<ldlt_solve_const_eval_ $d d>]() {
1105 #[expect(
1106 clippy::cast_precision_loss,
1107 reason = "test indices are at most five and exactly representable as f64"
1108 )]
1109 const X: Result<Vector<$d>, LaError> = {
1110 let factors = LdltFactors::from_proven_rows(
1111 Matrix::<$d>::identity().into_rows()
1112 );
1113 let ldlt = Ldlt::<$d> { factors };
1114 let mut b_arr = [0.0f64; $d];
1115 let mut i = 0;
1116 while i < $d {
1117 b_arr[i] = i as f64 + 1.0;
1118 i += 1;
1119 }
1120 let b = Vector::<$d>::new(b_arr);
1121 ldlt.solve(b)
1122 };
1123 let x = X.unwrap().into_array();
1124 #[expect(
1125 clippy::cast_precision_loss,
1126 reason = "test indices are at most five and exactly representable as f64"
1127 )]
1128 for i in 0..$d {
1129 let expected = i as f64 + 1.0;
1130 assert!((x[i] - expected).abs() <= 1e-12);
1131 }
1132 }
1133 }
1134 };
1135 }
1136
1137 gen_ldlt_const_eval_tests!(2);
1138 gen_ldlt_const_eval_tests!(3);
1139 gen_ldlt_const_eval_tests!(4);
1140 gen_ldlt_const_eval_tests!(5);
1141}