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!(
565 ldlt.solve(Vector::<0>::zero())
566 .unwrap()
567 .into_array()
568 .is_empty()
569 );
570 }
571
572 #[test]
573 fn solve_2x2_known_spd() {
574 let a = Matrix::<2>::try_from_rows(black_box([[4.0, 2.0], [2.0, 3.0]])).unwrap();
575 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
576
577 let b = Vector::<2>::new(black_box([1.0, 2.0]));
578 let x = ldlt.solve(b).unwrap().into_array();
579
580 assert_abs_diff_eq!(x[0], -0.125, epsilon = 1e-12);
581 assert_abs_diff_eq!(x[1], 0.75, epsilon = 1e-12);
582 assert_abs_diff_eq!(ldlt.det().unwrap(), 8.0, epsilon = 1e-12);
583 }
584
585 #[test]
586 fn det_ordinary_factors_matches_direct_product_bits() {
587 let diagonal = [1.5, 2.0, 0.25, 8.0];
588 let mut rows = [[0.0; 4]; 4];
589 let mut expected = 1.0;
590 for (i, factor) in diagonal.into_iter().enumerate() {
591 rows[i][i] = factor;
592 expected *= factor;
593 }
594
595 let ldlt = Matrix::<4>::try_from_rows(rows)
596 .unwrap()
597 .ldlt(DEFAULT_SINGULAR_TOL)
598 .unwrap();
599 assert_eq!(ldlt.det().unwrap().to_bits(), expected.to_bits());
600 }
601
602 #[test]
603 fn solve_3x3_spd_tridiagonal_smoke() {
604 let a = Matrix::<3>::try_from_rows(black_box([
605 [2.0, -1.0, 0.0],
606 [-1.0, 2.0, -1.0],
607 [0.0, -1.0, 2.0],
608 ]))
609 .unwrap();
610 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
611
612 let b = Vector::<3>::new(black_box([1.0, 0.0, 1.0]));
614 let x = ldlt.solve(b).unwrap().into_array();
615
616 for &x_i in &x {
617 assert_abs_diff_eq!(x_i, 1.0, epsilon = 1e-9);
618 }
619 }
620
621 #[test]
622 fn singular_detected_for_degenerate_psd() {
623 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 1.0], [1.0, 1.0]])).unwrap();
625 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
626 assert_eq!(
627 err,
628 LaError::singular_numerical(
629 1,
630 FactorizationKind::Ldlt,
631 0.0,
632 DEFAULT_SINGULAR_TOL.get()
633 )
634 );
635 }
636
637 #[test]
638 fn zero_pivot_with_nonzero_coupling_is_not_reported_as_singular() {
639 let a = Matrix::<2>::try_from_rows(black_box([[0.0, 1.0], [1.0, 0.0]])).unwrap();
640 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
641 assert_eq!(
642 err,
643 LaError::not_positive_semidefinite_zero_coupling(0, 1, 1.0)
644 );
645 }
646
647 #[test]
648 fn zero_pivot_reports_non_finite_coupling_before_domain_violation() {
649 let a = Matrix::<3>::try_from_rows(black_box([
650 [1.0, 1.0, f64::MAX],
651 [1.0, 1.0, -f64::MAX],
652 [f64::MAX, -f64::MAX, 1.0],
653 ]))
654 .unwrap();
655 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
656 assert_eq!(
657 err,
658 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1,)
659 );
660 }
661
662 #[test]
663 fn small_positive_pivot_reports_numerical_singularity() {
664 let a = Matrix::<2>::try_from_rows(black_box([[1e-13, 0.0], [0.0, 1.0]])).unwrap();
665 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
666 assert_eq!(
667 err,
668 LaError::singular_numerical(
669 0,
670 FactorizationKind::Ldlt,
671 1e-13,
672 DEFAULT_SINGULAR_TOL.get()
673 )
674 );
675 }
676
677 #[test]
678 fn small_positive_pivot_does_not_mask_earlier_non_finite_update() {
679 let a = Matrix::<3>::try_from_rows(black_box([
680 [1.0, 1.0, f64::MAX],
681 [1.0, 1.0 + f64::EPSILON, -f64::MAX],
682 [f64::MAX, -f64::MAX, 1.0],
683 ]))
684 .unwrap();
685
686 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
687 assert_eq!(
688 err,
689 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1)
690 );
691 }
692
693 #[test]
694 fn negative_initial_diagonal_reports_not_positive_semidefinite() {
695 let a = Matrix::<2>::try_from_rows(black_box([[-1.0, 0.0], [0.0, 1.0]])).unwrap();
696 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
697 assert_eq!(err, LaError::not_positive_semidefinite_negative(0, -1.0));
698 }
699
700 #[test]
701 fn negative_updated_diagonal_reports_not_positive_semidefinite() {
702 let a = Matrix::<2>::try_from_rows(black_box([[1.0, 2.0], [2.0, 1.0]])).unwrap();
703 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
704 assert_eq!(err, LaError::not_positive_semidefinite_negative(1, -3.0));
705 }
706
707 #[test]
708 fn negative_pivot_does_not_mask_earlier_non_finite_update() {
709 let a = Matrix::<3>::try_from_rows(black_box([
710 [1.0, 2.0, f64::MAX],
711 [2.0, 1.0, 0.0],
712 [f64::MAX, 0.0, 1.0],
713 ]))
714 .unwrap();
715
716 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
717 assert_eq!(
718 err,
719 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 2, 1,)
720 );
721 }
722
723 #[test]
724 fn non_finite_l_multiplier_overflow() {
725 let a = Matrix::<2>::try_from_rows([[1e-11, 1e300], [1e300, 1.0]]).unwrap();
727 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
728 assert_eq!(
729 err,
730 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 1, 0)
731 );
732 }
733
734 #[test]
735 fn non_finite_l_multiplier_overflow_fused_branch_6d() {
736 let mut rows = [[0.0; 6]; 6];
739 for (i, row) in rows.iter_mut().enumerate() {
740 row[i] = 1.0;
741 }
742 rows[0][0] = 1e-11;
743 rows[0][5] = 1e300;
744 rows[5][0] = 1e300;
745
746 let a = Matrix::<6>::try_from_rows(rows).unwrap();
747 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
748 assert_eq!(
749 err,
750 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 5, 0)
751 );
752 }
753
754 #[test]
755 fn non_finite_trailing_submatrix_overflow() {
756 let a = Matrix::<2>::try_from_rows([[1.0, 1e200], [1e200, 1.0]]).unwrap();
759 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
760 assert_eq!(
761 err,
762 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 1, 1)
763 );
764 }
765
766 #[test]
767 fn non_finite_trailing_submatrix_overflow_fused_branch_6d() {
768 let mut rows = [[0.0; 6]; 6];
771 for (i, row) in rows.iter_mut().enumerate() {
772 row[i] = 1.0;
773 }
774 rows[0][5] = 1e200;
775 rows[5][0] = 1e200;
776
777 let a = Matrix::<6>::try_from_rows(rows).unwrap();
778 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
779 assert_eq!(
780 err,
781 LaError::non_finite_computation_matrix(ArithmeticOperation::LdltFactorization, 5, 5)
782 );
783 }
784
785 #[test]
786 fn non_finite_solve_forward_substitution_overflow() {
787 let a = Matrix::<3>::try_from_rows([
790 [1.0, 1e153, 0.0],
791 [1e153, 1e306 + 1.0, 0.0],
792 [0.0, 0.0, 1.0],
793 ])
794 .unwrap();
795 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
796
797 let b = Vector::<3>::new([1e156, 0.0, 0.0]);
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() {
807 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]])
812 .unwrap();
813 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
814
815 let b = Vector::<3>::new([0.0, 0.0, 1e308]);
816 let err = ldlt.solve(b).unwrap_err();
817 assert_eq!(
818 err,
819 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 1)
820 );
821 }
822
823 #[test]
824 fn non_finite_solve_back_substitution_overflow_scatter_branch_5d() {
825 let a = Matrix::<5>::try_from_rows([
828 [1.0, 0.0, 0.0, 0.0, 0.0],
829 [0.0, 1.0, 0.0, 0.0, 0.0],
830 [0.0, 0.0, 1.0, 0.0, 0.0],
831 [0.0, 0.0, 0.0, 1.0, 2.0],
832 [0.0, 0.0, 0.0, 2.0, 5.0],
833 ])
834 .unwrap();
835 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
836
837 let b = Vector::<5>::new([0.0, 0.0, 0.0, 0.0, 1e308]);
838 let err = ldlt.solve(b).unwrap_err();
839 assert_eq!(
840 err,
841 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 3)
842 );
843 }
844
845 #[test]
846 fn non_finite_solve_diagonal_solve_overflow() {
847 let a = Matrix::<2>::try_from_rows([[1.0, 0.0], [0.0, 1.0e-11]]).unwrap();
853 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
854
855 let b = Vector::<2>::new([0.0, 1.0e300]);
856 let err = ldlt.solve(b).unwrap_err();
857 assert_eq!(
858 err,
859 LaError::non_finite_computation_step(ArithmeticOperation::LdltSolve, 1)
860 );
861 }
862
863 #[test]
864 fn det_rejects_product_overflow() {
865 let a = Matrix::<5>::try_from_rows([
866 [1.0e100, 0.0, 0.0, 0.0, 0.0],
867 [0.0, 1.0e100, 0.0, 0.0, 0.0],
868 [0.0, 0.0, 1.0e100, 0.0, 0.0],
869 [0.0, 0.0, 0.0, 1.0e100, 0.0],
870 [0.0, 0.0, 0.0, 0.0, 1.0e100],
871 ])
872 .unwrap();
873 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
874 assert_eq!(
875 ldlt.det(),
876 Err(LaError::non_finite_computation_step(
877 ArithmeticOperation::Determinant,
878 4
879 ))
880 );
881 }
882
883 #[test]
884 fn det_balances_extreme_diagonals_independently_of_storage_order() {
885 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
886 for diagonal in [
887 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800],
888 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800],
889 ] {
890 let mut rows = [[0.0; 4]; 4];
891 for (i, value) in diagonal.into_iter().enumerate() {
892 rows[i][i] = value;
893 }
894
895 let ldlt = Matrix::<4>::try_from_rows(rows)
896 .unwrap()
897 .ldlt(zero_tolerance)
898 .unwrap();
899 assert_eq!(ldlt.det(), Ok(1.0));
900 }
901 }
902
903 #[test]
904 fn det_balances_extreme_diagonals_in_large_dimension() {
905 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
906 for diagonal in [
907 [TWO_NEG_800, TWO_NEG_800, TWO_POS_800, TWO_POS_800, 1.0, 1.0],
908 [TWO_POS_800, TWO_POS_800, TWO_NEG_800, TWO_NEG_800, 1.0, 1.0],
909 ] {
910 let mut rows = [[0.0; 6]; 6];
911 for (i, value) in diagonal.into_iter().enumerate() {
912 rows[i][i] = value;
913 }
914
915 let ldlt = Matrix::<6>::try_from_rows(rows)
916 .unwrap()
917 .ldlt(zero_tolerance)
918 .unwrap();
919 assert_eq!(ldlt.det(), Ok(1.0));
920 }
921 }
922
923 #[test]
924 fn det_rounds_final_tiny_magnitude_to_zero() {
925 let zero_tolerance = Tolerance::try_new(0.0).unwrap();
926 let matrix = Matrix::<2>::try_from_rows([[TWO_NEG_800, 0.0], [0.0, TWO_NEG_800]]).unwrap();
927 let det = matrix.ldlt(zero_tolerance).unwrap().det().unwrap();
928
929 assert_eq!(det.to_bits(), 0.0f64.to_bits());
930 }
931
932 #[test]
933 fn ldlt_d1_classifies_positive_zero_and_negative_inputs() {
934 let positive = Matrix::<1>::try_from_rows([[2.0]])
935 .unwrap()
936 .ldlt(DEFAULT_SINGULAR_TOL)
937 .unwrap();
938 assert_eq!(positive.det(), Ok(2.0));
939 assert_abs_diff_eq!(
940 positive
941 .solve(Vector::<1>::new([6.0]))
942 .unwrap()
943 .into_array()[0],
944 3.0,
945 epsilon = 0.0
946 );
947
948 let zero = Matrix::<1>::try_from_rows([[0.0]])
949 .unwrap()
950 .ldlt(DEFAULT_SINGULAR_TOL);
951 assert_eq!(
952 zero,
953 Err(LaError::singular_numerical(
954 0,
955 FactorizationKind::Ldlt,
956 0.0,
957 DEFAULT_SINGULAR_TOL.get()
958 ))
959 );
960
961 let negative = Matrix::<1>::try_from_rows([[-1.0]])
962 .unwrap()
963 .ldlt(DEFAULT_SINGULAR_TOL);
964 assert_eq!(
965 negative,
966 Err(LaError::not_positive_semidefinite_negative(0, -1.0))
967 );
968 }
969
970 fn nontrivial_spd_system<const D: usize>() -> (Matrix<D>, Vector<D>, [f64; D], f64) {
973 let mut rows = [[0.0_f64; D]; D];
974 let mut expected_det = 1.0_f64;
975 let mut diagonal = 1.0_f64;
976 let mut k = 0;
977 while k < D {
978 rows[k][k] += diagonal;
979 expected_det *= diagonal;
980 if k + 1 < D {
981 let off_diagonal = 0.5 * diagonal;
982 rows[k][k + 1] += off_diagonal;
983 rows[k + 1][k] += off_diagonal;
984 rows[k + 1][k + 1] = 0.25_f64.mul_add(diagonal, rows[k + 1][k + 1]);
985 }
986 diagonal += 1.0;
987 k += 1;
988 }
989
990 let mut expected_x = [0.0_f64; D];
991 let mut value = 1.0_f64;
992 for entry in &mut expected_x {
993 *entry = value;
994 value += 1.0;
995 }
996 let rhs = core::array::from_fn(|row| {
997 rows[row]
998 .iter()
999 .zip(expected_x.iter())
1000 .fold(0.0_f64, |sum, (&coefficient, &x)| {
1001 coefficient.mul_add(x, sum)
1002 })
1003 });
1004
1005 (
1006 Matrix::<D>::try_from_rows(rows).unwrap(),
1007 Vector::<D>::try_new(rhs).unwrap(),
1008 expected_x,
1009 expected_det,
1010 )
1011 }
1012
1013 macro_rules! gen_nontrivial_large_ldlt_tests {
1014 ($d:literal) => {
1015 paste! {
1016 #[test]
1017 fn [<ldlt_nontrivial_success_solve_and_det_agree_ $d d>]() {
1018 let (matrix, rhs, expected_x, expected_det) =
1019 nontrivial_spd_system::<$d>();
1020 let ldlt = matrix.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
1021 let solution = ldlt.solve(rhs).unwrap().into_array();
1022
1023 for (actual, expected) in solution.into_iter().zip(expected_x) {
1024 assert_abs_diff_eq!(actual, expected, epsilon = 1e-12);
1025 }
1026 assert_abs_diff_eq!(ldlt.det().unwrap(), expected_det, epsilon = 1e-12);
1027 assert_abs_diff_eq!(matrix.det().unwrap(), expected_det, epsilon = 1e-10);
1028 }
1029 }
1030 };
1031 }
1032
1033 gen_nontrivial_large_ldlt_tests!(6);
1034 gen_nontrivial_large_ldlt_tests!(8);
1035
1036 #[test]
1037 fn asymmetric_input_returns_typed_error() {
1038 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]])
1040 .unwrap();
1041 assert_eq!(
1042 a.ldlt(DEFAULT_SINGULAR_TOL),
1043 Err(LaError::asymmetric(0, 1, 3, 2.0, -2.0, 0.0))
1044 );
1045 }
1046
1047 #[test]
1048 fn approximately_symmetric_input_is_rejected_before_factoring_another_operator() {
1049 let matrix = Matrix::<2>::try_from_rows([[TWO_POS_43, 0.0], [4.0, TWO_NEG_38]]).unwrap();
1054 let diagnostic_tolerance = Tolerance::try_new(1e-12).unwrap();
1055
1056 assert_eq!(matrix.det(), Ok(32.0));
1057 assert_eq!(matrix.is_symmetric(diagnostic_tolerance), Ok(true));
1058 assert_eq!(
1059 matrix.ldlt(DEFAULT_SINGULAR_TOL),
1060 Err(LaError::asymmetric(0, 1, 2, 0.0, 4.0, 0.0))
1061 );
1062 }
1063
1064 macro_rules! gen_ldlt_const_eval_tests {
1075 ($d:literal) => {
1076 paste! {
1077 #[test]
1082 fn [<ldlt_det_const_eval_ $d d>]() {
1083 const DET: Result<f64, LaError> = {
1084 let mut rows = [[0.0f64; $d]; $d];
1085 let mut i = 0;
1086 while i < $d {
1087 rows[i][i] = 1.0;
1088 i += 1;
1089 }
1090 rows[0][0] = 2.0;
1091 let factors = LdltFactors::from_proven_rows(rows);
1092 let ldlt = Ldlt::<$d> { factors };
1093 ldlt.det()
1094 };
1095 assert_eq!(DET, Ok(2.0));
1096 }
1097
1098 #[test]
1103 fn [<ldlt_solve_const_eval_ $d d>]() {
1104 #[expect(
1105 clippy::cast_precision_loss,
1106 reason = "test indices are at most five and exactly representable as f64"
1107 )]
1108 const X: Result<Vector<$d>, LaError> = {
1109 let factors = LdltFactors::from_proven_rows(
1110 Matrix::<$d>::identity().into_rows()
1111 );
1112 let ldlt = Ldlt::<$d> { factors };
1113 let mut b_arr = [0.0f64; $d];
1114 let mut i = 0;
1115 while i < $d {
1116 b_arr[i] = i as f64 + 1.0;
1117 i += 1;
1118 }
1119 let b = Vector::<$d>::new(b_arr);
1120 ldlt.solve(b)
1121 };
1122 let x = X.unwrap().into_array();
1123 #[expect(
1124 clippy::cast_precision_loss,
1125 reason = "test indices are at most five and exactly representable as f64"
1126 )]
1127 for i in 0..$d {
1128 let expected = i as f64 + 1.0;
1129 assert!((x[i] - expected).abs() <= 1e-12);
1130 }
1131 }
1132 }
1133 };
1134 }
1135
1136 gen_ldlt_const_eval_tests!(2);
1137 gen_ldlt_const_eval_tests!(3);
1138 gen_ldlt_const_eval_tests!(4);
1139 gen_ldlt_const_eval_tests!(5);
1140}