la_stack/ldlt.rs
1//! LDLT factorization and solves.
2//!
3//! This module provides a stack-allocated LDLT factorization (`A = L D Lᵀ`) intended for
4//! symmetric positive definite (SPD) and positive semi-definite (PSD) matrices (e.g. Gram
5//! matrices) without pivoting.
6//!
7//! # Preconditions
8//! The input matrix must be **symmetric**. This is a correctness contract, not a hint:
9//! the factorization algorithm reads only the lower triangle and implicitly assumes the
10//! upper triangle mirrors it. Symmetry is verified by a `debug_assert!` in debug builds
11//! only; in release builds an asymmetric input will silently produce a meaningless
12//! factorization. Callers who cannot statically guarantee symmetry should pre-validate
13//! with [`Matrix::is_symmetric`](crate::Matrix::is_symmetric) (or locate the offending
14//! pair with [`Matrix::first_asymmetry`](crate::Matrix::first_asymmetry)), or fall back
15//! to [`crate::Lu`] if their matrices are not guaranteed to be symmetric at all.
16
17use core::hint::cold_path;
18
19use crate::LaError;
20use crate::matrix::Matrix;
21use crate::vector::Vector;
22
23/// LDLT factorization (`A = L D Lᵀ`) for symmetric positive (semi)definite matrices.
24///
25/// This factorization is **not** a general-purpose symmetric-indefinite LDLT (no pivoting).
26/// It assumes the input matrix is symmetric and (numerically) SPD/PSD.
27///
28/// # Preconditions
29/// The source matrix passed to [`Matrix::ldlt`](crate::Matrix::ldlt) must be symmetric
30/// (`A[i][j] == A[j][i]` within rounding). Asymmetric inputs panic in debug builds via
31/// `debug_assert!` and are silently accepted in release builds — producing a
32/// mathematically meaningless factorization whose [`Self::det`] and [`Self::solve_vec`]
33/// results are wrong without any error being reported. Pre-validate with
34/// [`Matrix::is_symmetric`](crate::Matrix::is_symmetric) when the input cannot be
35/// statically guaranteed symmetric; see [`Matrix::ldlt`](crate::Matrix::ldlt) for further
36/// details and alternatives.
37///
38/// # Storage
39/// The factors are stored in a single [`Matrix`]:
40/// - `D` is stored on the diagonal.
41/// - The strict lower triangle stores the multipliers of `L`.
42/// - The diagonal of `L` is implicit ones.
43#[must_use]
44#[derive(Clone, Copy, Debug, PartialEq)]
45pub struct Ldlt<const D: usize> {
46 factors: Matrix<D>,
47 tol: f64,
48}
49
50impl<const D: usize> Ldlt<D> {
51 #[inline]
52 pub(crate) fn factor(a: Matrix<D>, tol: f64) -> Result<Self, LaError> {
53 debug_assert!(tol >= 0.0, "tol must be non-negative");
54
55 #[cfg(debug_assertions)]
56 debug_assert_symmetric(&a);
57
58 let mut f = a;
59
60 // LDLT via symmetric rank-1 updates, using only the lower triangle.
61 for j in 0..D {
62 let d = f.rows[j][j];
63 if !d.is_finite() {
64 cold_path();
65 return Err(LaError::non_finite_cell(j, j));
66 }
67 if d <= tol {
68 cold_path();
69 return Err(LaError::Singular { pivot_col: j });
70 }
71
72 // Compute L multipliers below the diagonal in column j.
73 for i in (j + 1)..D {
74 let l = f.rows[i][j] / d;
75 if !l.is_finite() {
76 cold_path();
77 return Err(LaError::non_finite_cell(i, j));
78 }
79 f.rows[i][j] = l;
80 }
81
82 // Update the trailing submatrix (lower triangle): A := A - (L_col * d) * L_col^T.
83 for i in (j + 1)..D {
84 let l_i = f.rows[i][j];
85 let l_i_d = l_i * d;
86
87 for k in (j + 1)..=i {
88 let l_k = f.rows[k][j];
89 let new_val = (-l_i_d).mul_add(l_k, f.rows[i][k]);
90 if !new_val.is_finite() {
91 cold_path();
92 return Err(LaError::non_finite_cell(i, k));
93 }
94 f.rows[i][k] = new_val;
95 }
96 }
97 }
98
99 Ok(Self { factors: f, tol })
100 }
101
102 /// Determinant of the original matrix.
103 ///
104 /// For SPD/PSD matrices, this is the product of the diagonal terms of `D`.
105 ///
106 /// # Examples
107 /// ```
108 /// use la_stack::prelude::*;
109 ///
110 /// // Symmetric SPD matrix.
111 /// let a = Matrix::<2>::from_rows([[4.0, 2.0], [2.0, 3.0]]);
112 /// let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
113 ///
114 /// assert!((ldlt.det() - 8.0).abs() <= 1e-12);
115 /// ```
116 #[inline]
117 #[must_use]
118 pub const fn det(&self) -> f64 {
119 let mut det = 1.0;
120 let mut i = 0;
121 while i < D {
122 det *= self.factors.rows[i][i];
123 i += 1;
124 }
125 det
126 }
127
128 /// Solve `A x = b` using this LDLT factorization.
129 ///
130 /// # Examples
131 /// ```
132 /// use la_stack::prelude::*;
133 ///
134 /// # fn main() -> Result<(), LaError> {
135 /// let a = Matrix::<2>::from_rows([[4.0, 2.0], [2.0, 3.0]]);
136 /// let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL)?;
137 ///
138 /// let b = Vector::<2>::new([1.0, 2.0]);
139 /// let x = ldlt.solve_vec(b)?.into_array();
140 ///
141 /// assert!((x[0] - (-0.125)).abs() <= 1e-12);
142 /// assert!((x[1] - 0.75).abs() <= 1e-12);
143 /// # Ok(())
144 /// # }
145 /// ```
146 ///
147 /// # Errors
148 /// Returns [`LaError::Singular`] if a diagonal entry `d = D[i,i]` satisfies `d <= tol`
149 /// (non-positive or too small), where `tol` is the tolerance that was used during factorization.
150 ///
151 /// Returns [`LaError::NonFinite`] if NaN/∞ is detected. The `row`/`col` coordinates
152 /// follow the convention documented on [`LaError::NonFinite`]:
153 ///
154 /// - `row: Some(i), col: i` — the stored `D` diagonal at `(i, i)` is non-finite
155 /// (only reachable via direct `Ldlt` construction; [`Matrix::ldlt`](crate::Matrix::ldlt)
156 /// rejects such factorizations).
157 /// - `row: None, col: i` — a computed intermediate (forward/back-substitution
158 /// accumulator or the quotient `x[i] / diag`) overflowed to NaN/∞ at step `i`.
159 #[inline]
160 pub const fn solve_vec(&self, b: Vector<D>) -> Result<Vector<D>, LaError> {
161 let mut x = b.data;
162
163 // Forward substitution: L y = b (L has unit diagonal).
164 let mut i = 0;
165 while i < D {
166 let mut sum = x[i];
167 let row = self.factors.rows[i];
168 let mut j = 0;
169 while j < i {
170 sum = (-row[j]).mul_add(x[j], sum);
171 j += 1;
172 }
173 if !sum.is_finite() {
174 cold_path();
175 return Err(LaError::non_finite_at(i));
176 }
177 x[i] = sum;
178 i += 1;
179 }
180
181 // Diagonal solve: D z = y.
182 let mut i = 0;
183 while i < D {
184 let diag = self.factors.rows[i][i];
185 // A corrupt stored diagonal is a specific matrix cell (i, i),
186 // distinct from a computed overflow — report it with
187 // `row: Some(i)` per the `LaError::NonFinite` convention used by
188 // `Matrix::det`, `Lu::factor`, and `Ldlt::factor`.
189 if !diag.is_finite() {
190 cold_path();
191 return Err(LaError::non_finite_cell(i, i));
192 }
193 if diag <= self.tol {
194 cold_path();
195 return Err(LaError::Singular { pivot_col: i });
196 }
197
198 let quotient = x[i] / diag;
199 if !quotient.is_finite() {
200 cold_path();
201 return Err(LaError::non_finite_at(i));
202 }
203 x[i] = quotient;
204 i += 1;
205 }
206
207 // Back substitution: Lᵀ x = z.
208 let mut ii = 0;
209 while ii < D {
210 let i = D - 1 - ii;
211 let mut sum = x[i];
212 let mut j = i + 1;
213 while j < D {
214 sum = (-self.factors.rows[j][i]).mul_add(x[j], sum);
215 j += 1;
216 }
217 if !sum.is_finite() {
218 cold_path();
219 return Err(LaError::non_finite_at(i));
220 }
221 x[i] = sum;
222 ii += 1;
223 }
224
225 Ok(Vector::new(x))
226 }
227}
228
229#[cfg(debug_assertions)]
230fn debug_assert_symmetric<const D: usize>(a: &Matrix<D>) {
231 // Delegate to the public predicate so the runtime check and the documented
232 // contract on `Matrix::ldlt` cannot drift apart. `first_asymmetry` is used
233 // (rather than `is_symmetric`) so the panic message can name the offending
234 // pair — which is invaluable for debugging.
235 if let Some((r, c)) = a.first_asymmetry(1e-12) {
236 let diff = (a.rows[r][c] - a.rows[c][r]).abs();
237 let eps = 1e-12 * a.inf_norm().max(1.0);
238 debug_assert!(
239 false,
240 "matrix must be symmetric (diff={diff}, eps={eps}) at ({r}, {c}); \
241 pre-validate with Matrix::is_symmetric before calling ldlt"
242 );
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249
250 use crate::DEFAULT_SINGULAR_TOL;
251
252 use core::hint::black_box;
253
254 use approx::assert_abs_diff_eq;
255 use pastey::paste;
256
257 macro_rules! gen_public_api_ldlt_identity_tests {
258 ($d:literal) => {
259 paste! {
260 #[test]
261 fn [<public_api_ldlt_det_and_solve_identity_ $d d>]() {
262 let a = Matrix::<$d>::identity();
263 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
264
265 assert_abs_diff_eq!(ldlt.det(), 1.0, epsilon = 1e-12);
266
267 let b_arr = {
268 let mut arr = [0.0f64; $d];
269 let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
270 for (dst, src) in arr.iter_mut().zip(values.iter()) {
271 *dst = *src;
272 }
273 arr
274 };
275 let b = Vector::<$d>::new(black_box(b_arr));
276 let x = ldlt.solve_vec(b).unwrap().into_array();
277
278 for i in 0..$d {
279 assert_abs_diff_eq!(x[i], b_arr[i], epsilon = 1e-12);
280 }
281 }
282 }
283 };
284 }
285
286 gen_public_api_ldlt_identity_tests!(2);
287 gen_public_api_ldlt_identity_tests!(3);
288 gen_public_api_ldlt_identity_tests!(4);
289 gen_public_api_ldlt_identity_tests!(5);
290
291 macro_rules! gen_public_api_ldlt_diagonal_tests {
292 ($d:literal) => {
293 paste! {
294 #[test]
295 fn [<public_api_ldlt_det_and_solve_diagonal_spd_ $d d>]() {
296 let diag = {
297 let mut arr = [0.0f64; $d];
298 let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
299 for (dst, src) in arr.iter_mut().zip(values.iter()) {
300 *dst = *src;
301 }
302 arr
303 };
304
305 let mut rows = [[0.0f64; $d]; $d];
306 for i in 0..$d {
307 rows[i][i] = diag[i];
308 }
309
310 let a = Matrix::<$d>::from_rows(black_box(rows));
311 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
312
313 let expected_det = {
314 let mut acc = 1.0;
315 for i in 0..$d {
316 acc *= diag[i];
317 }
318 acc
319 };
320 assert_abs_diff_eq!(ldlt.det(), expected_det, epsilon = 1e-12);
321
322 let b_arr = {
323 let mut arr = [0.0f64; $d];
324 let values = [5.0f64, 4.0, 3.0, 2.0, 1.0];
325 for (dst, src) in arr.iter_mut().zip(values.iter()) {
326 *dst = *src;
327 }
328 arr
329 };
330
331 let b = Vector::<$d>::new(black_box(b_arr));
332 let x = ldlt.solve_vec(b).unwrap().into_array();
333
334 for i in 0..$d {
335 assert_abs_diff_eq!(x[i], b_arr[i] / diag[i], epsilon = 1e-12);
336 }
337 }
338 }
339 };
340 }
341
342 gen_public_api_ldlt_diagonal_tests!(2);
343 gen_public_api_ldlt_diagonal_tests!(3);
344 gen_public_api_ldlt_diagonal_tests!(4);
345 gen_public_api_ldlt_diagonal_tests!(5);
346
347 #[test]
348 fn solve_2x2_known_spd() {
349 let a = Matrix::<2>::from_rows(black_box([[4.0, 2.0], [2.0, 3.0]]));
350 let ldlt = (black_box(Ldlt::<2>::factor))(a, DEFAULT_SINGULAR_TOL).unwrap();
351
352 let b = Vector::<2>::new(black_box([1.0, 2.0]));
353 let x = ldlt.solve_vec(b).unwrap().into_array();
354
355 assert_abs_diff_eq!(x[0], -0.125, epsilon = 1e-12);
356 assert_abs_diff_eq!(x[1], 0.75, epsilon = 1e-12);
357 assert_abs_diff_eq!(ldlt.det(), 8.0, epsilon = 1e-12);
358 }
359
360 #[test]
361 fn solve_3x3_spd_tridiagonal_smoke() {
362 let a = Matrix::<3>::from_rows(black_box([
363 [2.0, -1.0, 0.0],
364 [-1.0, 2.0, -1.0],
365 [0.0, -1.0, 2.0],
366 ]));
367 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
368
369 // Choose x = 1 so b = A x is simple: [1, 0, 1].
370 let b = Vector::<3>::new(black_box([1.0, 0.0, 1.0]));
371 let x = ldlt.solve_vec(b).unwrap().into_array();
372
373 for &x_i in &x {
374 assert_abs_diff_eq!(x_i, 1.0, epsilon = 1e-9);
375 }
376 }
377
378 #[test]
379 fn singular_detected_for_degenerate_psd() {
380 // Rank-1 Gram-like matrix.
381 let a = Matrix::<2>::from_rows(black_box([[1.0, 1.0], [1.0, 1.0]]));
382 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
383 assert_eq!(err, LaError::Singular { pivot_col: 1 });
384 }
385
386 #[test]
387 fn nonfinite_detected() {
388 let a = Matrix::<2>::from_rows([[f64::NAN, 0.0], [0.0, 1.0]]);
389 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
390 assert_eq!(
391 err,
392 LaError::NonFinite {
393 row: Some(0),
394 col: 0
395 }
396 );
397 }
398
399 #[test]
400 fn nonfinite_l_multiplier_overflow() {
401 // d = 1e-11 > tol, but l = 1e300 / 1e-11 = 1e311 overflows f64.
402 let a = Matrix::<2>::from_rows([[1e-11, 1e300], [1e300, 1.0]]);
403 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
404 assert_eq!(
405 err,
406 LaError::NonFinite {
407 row: Some(1),
408 col: 0
409 }
410 );
411 }
412
413 #[test]
414 fn nonfinite_trailing_submatrix_overflow() {
415 // L multiplier is finite (1e200), but the rank-1 update
416 // (-1e200 * 1.0) * 1e200 + 1.0 overflows.
417 let a = Matrix::<2>::from_rows([[1.0, 1e200], [1e200, 1.0]]);
418 let err = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap_err();
419 assert_eq!(
420 err,
421 LaError::NonFinite {
422 row: Some(1),
423 col: 1
424 }
425 );
426 }
427
428 #[test]
429 fn nonfinite_solve_vec_forward_substitution_overflow() {
430 // SPD matrix with large L multiplier: L[1,0] = 1e153.
431 // Forward substitution overflows: y[1] = 0 - 1e153 * 1e156 = -inf.
432 let a = Matrix::<3>::from_rows([
433 [1.0, 1e153, 0.0],
434 [1e153, 1e306 + 1.0, 0.0],
435 [0.0, 0.0, 1.0],
436 ]);
437 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
438
439 let b = Vector::<3>::new([1e156, 0.0, 0.0]);
440 let err = ldlt.solve_vec(b).unwrap_err();
441 assert_eq!(err, LaError::NonFinite { row: None, col: 1 });
442 }
443
444 #[test]
445 fn nonfinite_solve_vec_back_substitution_overflow() {
446 // SPD matrix: [[1,0,0],[0,1,2],[0,2,5]] has LDLT factors
447 // D=[1,1,1], L[2,1]=2. Forward sub and diagonal solve produce
448 // z=[0,0,1e308]. Back-substitution: x[2]=1e308 then
449 // x[1] = 0 - 2*1e308 = -inf (overflows f64).
450 let a = Matrix::<3>::from_rows([[1.0, 0.0, 0.0], [0.0, 1.0, 2.0], [0.0, 2.0, 5.0]]);
451 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
452
453 let b = Vector::<3>::new([0.0, 0.0, 1e308]);
454 let err = ldlt.solve_vec(b).unwrap_err();
455 assert_eq!(err, LaError::NonFinite { row: None, col: 1 });
456 }
457
458 #[test]
459 fn nonfinite_solve_vec_diagonal_solve_overflow() {
460 // Diagonal SPD matrix with a tiny diagonal entry just above the
461 // singularity tolerance. Forward substitution passes through the
462 // large RHS unchanged, then the diagonal solve z[1] = y[1] / D[1]
463 // = 1e300 / 1e-11 = 1e311 overflows f64, exercising the
464 // `!v.is_finite()` branch of the diagonal solve.
465 let a = Matrix::<2>::from_rows([[1.0, 0.0], [0.0, 1.0e-11]]);
466 let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
467
468 let b = Vector::<2>::new([0.0, 1.0e300]);
469 let err = ldlt.solve_vec(b).unwrap_err();
470 assert_eq!(err, LaError::NonFinite { row: None, col: 1 });
471 }
472
473 /// Verifies the symmetry precondition documented on [`Matrix::ldlt`] is
474 /// enforced by `debug_assert_symmetric` in debug builds. The test is
475 /// gated on `debug_assertions` so `cargo test --release` simply skips it
476 /// (the assertion is compiled out in release builds — see the
477 /// Preconditions section of `Matrix::ldlt`).
478 #[cfg(debug_assertions)]
479 #[test]
480 #[should_panic(expected = "matrix must be symmetric")]
481 fn debug_asymmetric_input_panics() {
482 // a[0][1] = 2.0 but a[1][0] = -2.0 → clearly asymmetric.
483 let a = Matrix::<3>::from_rows([[4.0, 2.0, 0.0], [-2.0, 5.0, 1.0], [0.0, 1.0, 3.0]]);
484 let _ = a.ldlt(DEFAULT_SINGULAR_TOL);
485 }
486
487 // -----------------------------------------------------------------------
488 // Defensive-path coverage for `solve_vec`.
489 //
490 // `Ldlt::factor` guarantees that every stored diagonal is finite and
491 // strictly greater than the recorded `tol`. `solve_vec` still re-checks
492 // both invariants as a safety net (see the `!diag.is_finite()` and
493 // `diag <= self.tol` guards in the diagonal solve). Those branches are
494 // unreachable through the public API, so the only way to exercise them
495 // is to construct `Ldlt` directly with corrupt factors. The tests below
496 // document and verify that the safety nets return the documented error
497 // variants.
498 // -----------------------------------------------------------------------
499
500 macro_rules! gen_solve_vec_defensive_tests {
501 ($d:literal) => {
502 paste! {
503 /// `solve_vec` must surface `NonFinite` when a stored
504 /// diagonal is NaN, even though `factor` cannot produce
505 /// such a factorization. The error must pinpoint the
506 /// corrupt cell at `(D-1, D-1)` per the
507 /// [`LaError::NonFinite`] convention.
508 #[test]
509 fn [<solve_vec_defensive_non_finite_diagonal_ $d d>]() {
510 let mut factors = Matrix::<$d>::identity();
511 factors.rows[$d - 1][$d - 1] = f64::NAN;
512 let ldlt = Ldlt::<$d> {
513 factors,
514 tol: DEFAULT_SINGULAR_TOL,
515 };
516 let b = Vector::<$d>::new([1.0; $d]);
517 let err = ldlt.solve_vec(b).unwrap_err();
518 assert_eq!(
519 err,
520 LaError::NonFinite {
521 row: Some($d - 1),
522 col: $d - 1,
523 }
524 );
525 }
526
527 /// `solve_vec` must surface `Singular` when a stored
528 /// diagonal is at or below the recorded tolerance, even
529 /// though `factor` cannot produce such a factorization.
530 #[test]
531 fn [<solve_vec_defensive_sub_tolerance_diagonal_ $d d>]() {
532 let mut factors = Matrix::<$d>::identity();
533 factors.rows[$d - 1][$d - 1] = 0.0;
534 let ldlt = Ldlt::<$d> {
535 factors,
536 tol: DEFAULT_SINGULAR_TOL,
537 };
538 let b = Vector::<$d>::new([1.0; $d]);
539 let err = ldlt.solve_vec(b).unwrap_err();
540 assert_eq!(err, LaError::Singular { pivot_col: $d - 1 });
541 }
542 }
543 };
544 }
545
546 gen_solve_vec_defensive_tests!(2);
547 gen_solve_vec_defensive_tests!(3);
548 gen_solve_vec_defensive_tests!(4);
549 gen_solve_vec_defensive_tests!(5);
550
551 // -----------------------------------------------------------------------
552 // Const-evaluability tests.
553 //
554 // These prove that `Ldlt::det` and `Ldlt::solve_vec` are truly `const fn`
555 // by forcing the compiler to evaluate them inside a `const` initializer.
556 // `Ldlt::factor` is not (yet) `const fn` because the rank-1 update loop
557 // uses array indexing patterns that still require non-const helpers on
558 // some toolchains; we therefore construct `Ldlt<D>` directly.
559 // -----------------------------------------------------------------------
560
561 macro_rules! gen_ldlt_const_eval_tests {
562 ($d:literal) => {
563 paste! {
564 /// `Ldlt::det` must be fully const-evaluable. Setting
565 /// `factors[0][0] = 2.0` and leaving the remaining identity
566 /// diagonals at `1.0` gives `det = 2.0` for every `D ≥ 1`,
567 /// exercising the multiply-accumulate loop at each dimension.
568 #[test]
569 fn [<ldlt_det_const_eval_ $d d>]() {
570 const DET: f64 = {
571 let mut factors = Matrix::<$d>::identity();
572 factors.rows[0][0] = 2.0;
573 let ldlt = Ldlt::<$d> {
574 factors,
575 tol: DEFAULT_SINGULAR_TOL,
576 };
577 ldlt.det()
578 };
579 assert!((DET - 2.0).abs() <= 1e-12);
580 }
581
582 /// `Ldlt::solve_vec` must be fully const-evaluable. Identity
583 /// factors with RHS `b = [1.0, 2.0, …, D]` round-trips `b`
584 /// unchanged, exercising the full forward sub / diagonal solve
585 /// / back sub pipeline inside a `const { … }` initializer.
586 #[test]
587 fn [<ldlt_solve_vec_const_eval_ $d d>]() {
588 #[allow(clippy::cast_precision_loss)]
589 const X: [f64; $d] = {
590 let ldlt = Ldlt::<$d> {
591 factors: Matrix::<$d>::identity(),
592 tol: DEFAULT_SINGULAR_TOL,
593 };
594 let mut b_arr = [0.0f64; $d];
595 let mut i = 0;
596 while i < $d {
597 b_arr[i] = i as f64 + 1.0;
598 i += 1;
599 }
600 let b = Vector::<$d>::new(b_arr);
601 match ldlt.solve_vec(b) {
602 Ok(v) => v.into_array(),
603 Err(_) => [0.0f64; $d],
604 }
605 };
606 #[allow(clippy::cast_precision_loss)]
607 for i in 0..$d {
608 let expected = i as f64 + 1.0;
609 assert!((X[i] - expected).abs() <= 1e-12);
610 }
611 }
612 }
613 };
614 }
615
616 gen_ldlt_const_eval_tests!(2);
617 gen_ldlt_const_eval_tests!(3);
618 gen_ldlt_const_eval_tests!(4);
619 gen_ldlt_const_eval_tests!(5);
620}