Skip to main content

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}