Skip to main content

la_stack/
matrix.rs

1//! Fixed-size, stack-allocated square matrices.
2
3use crate::LaError;
4use crate::ldlt::Ldlt;
5use crate::lu::Lu;
6
7/// Fixed-size square matrix `D×D`, stored inline.
8#[must_use]
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct Matrix<const D: usize> {
11    pub(crate) rows: [[f64; D]; D],
12}
13
14impl<const D: usize> Matrix<D> {
15    /// Construct from row-major storage.
16    ///
17    /// # Examples
18    /// ```
19    /// use la_stack::prelude::*;
20    ///
21    /// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
22    /// assert_eq!(m.get(0, 1), Some(2.0));
23    /// ```
24    #[inline]
25    pub const fn from_rows(rows: [[f64; D]; D]) -> Self {
26        Self { rows }
27    }
28
29    /// All-zeros matrix.
30    ///
31    /// # Examples
32    /// ```
33    /// use la_stack::prelude::*;
34    ///
35    /// let z = Matrix::<2>::zero();
36    /// assert_eq!(z.get(1, 1), Some(0.0));
37    /// ```
38    #[inline]
39    pub const fn zero() -> Self {
40        Self {
41            rows: [[0.0; D]; D],
42        }
43    }
44
45    /// Identity matrix.
46    ///
47    /// # Examples
48    /// ```
49    /// use la_stack::prelude::*;
50    ///
51    /// let i = Matrix::<3>::identity();
52    /// assert_eq!(i.get(0, 0), Some(1.0));
53    /// assert_eq!(i.get(0, 1), Some(0.0));
54    /// assert_eq!(i.get(2, 2), Some(1.0));
55    /// ```
56    #[inline]
57    pub const fn identity() -> Self {
58        let mut m = Self::zero();
59
60        let mut i = 0;
61        while i < D {
62            m.rows[i][i] = 1.0;
63            i += 1;
64        }
65
66        m
67    }
68
69    /// Get an element with bounds checking.
70    ///
71    /// # Examples
72    /// ```
73    /// use la_stack::prelude::*;
74    ///
75    /// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
76    /// assert_eq!(m.get(1, 0), Some(3.0));
77    /// assert_eq!(m.get(2, 0), None);
78    /// ```
79    #[inline]
80    #[must_use]
81    pub const fn get(&self, r: usize, c: usize) -> Option<f64> {
82        if r < D && c < D {
83            Some(self.rows[r][c])
84        } else {
85            None
86        }
87    }
88
89    /// Set an element with bounds checking.
90    ///
91    /// Returns `true` if the index was in-bounds.
92    ///
93    /// # Examples
94    /// ```
95    /// use la_stack::prelude::*;
96    ///
97    /// let mut m = Matrix::<2>::zero();
98    /// assert!(m.set(0, 1, 2.5));
99    /// assert_eq!(m.get(0, 1), Some(2.5));
100    /// assert!(!m.set(10, 0, 1.0));
101    /// ```
102    #[inline]
103    pub const fn set(&mut self, r: usize, c: usize, value: f64) -> bool {
104        if r < D && c < D {
105            self.rows[r][c] = value;
106            true
107        } else {
108            false
109        }
110    }
111
112    /// Infinity norm (maximum absolute row sum).
113    ///
114    /// # Examples
115    /// ```
116    /// use la_stack::prelude::*;
117    ///
118    /// let m = Matrix::<2>::from_rows([[1.0, -2.0], [3.0, 4.0]]);
119    /// assert!((m.inf_norm() - 7.0).abs() <= 1e-12);
120    /// ```
121    #[inline]
122    #[must_use]
123    pub fn inf_norm(&self) -> f64 {
124        let mut max_row_sum: f64 = 0.0;
125
126        for row in &self.rows {
127            let row_sum: f64 = row.iter().map(|&x| x.abs()).sum();
128            if row_sum > max_row_sum {
129                max_row_sum = row_sum;
130            }
131        }
132
133        max_row_sum
134    }
135
136    /// Compute an LU decomposition with partial pivoting.
137    ///
138    /// # Examples
139    /// ```
140    /// use la_stack::prelude::*;
141    ///
142    /// # fn main() -> Result<(), LaError> {
143    /// let a = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
144    /// let lu = a.lu(DEFAULT_PIVOT_TOL)?;
145    ///
146    /// let b = Vector::<2>::new([5.0, 11.0]);
147    /// let x = lu.solve_vec(b)?.into_array();
148    ///
149    /// assert!((x[0] - 1.0).abs() <= 1e-12);
150    /// assert!((x[1] - 2.0).abs() <= 1e-12);
151    /// # Ok(())
152    /// # }
153    /// ```
154    ///
155    /// # Errors
156    /// Returns [`LaError::Singular`] if, for some column `k`, the largest-magnitude candidate pivot
157    /// in that column satisfies `|pivot| <= tol` (so no numerically usable pivot exists).
158    /// Returns [`LaError::NonFinite`] if NaN/∞ is detected during factorization.
159    #[inline]
160    pub fn lu(self, tol: f64) -> Result<Lu<D>, LaError> {
161        Lu::factor(self, tol)
162    }
163
164    /// Compute an LDLT factorization (`A = L D Lᵀ`) without pivoting.
165    ///
166    /// This is intended for symmetric positive definite (SPD) and positive semi-definite (PSD)
167    /// matrices such as Gram matrices.
168    ///
169    /// # Examples
170    /// ```
171    /// use la_stack::prelude::*;
172    ///
173    /// # fn main() -> Result<(), LaError> {
174    /// let a = Matrix::<2>::from_rows([[4.0, 2.0], [2.0, 3.0]]);
175    /// let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL)?;
176    ///
177    /// // det(A) = 8
178    /// assert!((ldlt.det() - 8.0).abs() <= 1e-12);
179    ///
180    /// // Solve A x = b
181    /// let b = Vector::<2>::new([1.0, 2.0]);
182    /// let x = ldlt.solve_vec(b)?.into_array();
183    /// assert!((x[0] - (-0.125)).abs() <= 1e-12);
184    /// assert!((x[1] - 0.75).abs() <= 1e-12);
185    /// # Ok(())
186    /// # }
187    /// ```
188    ///
189    /// # Errors
190    /// Returns [`LaError::Singular`] if, for some step `k`, the required diagonal entry `d = D[k,k]`
191    /// is `<= tol` (non-positive or too small). This treats PSD degeneracy (and indefinite inputs)
192    /// as singular/degenerate.
193    /// Returns [`LaError::NonFinite`] if NaN/∞ is detected during factorization.
194    #[inline]
195    pub fn ldlt(self, tol: f64) -> Result<Ldlt<D>, LaError> {
196        Ldlt::factor(self, tol)
197    }
198
199    /// Closed-form determinant for dimensions 0–4, bypassing LU factorization.
200    ///
201    /// Returns `Some(det)` for `D` ∈ {0, 1, 2, 3, 4}, `None` for D ≥ 5.
202    /// `D = 0` returns `Some(1.0)` (empty product).
203    /// This is a `const fn` (Rust 1.94+) and uses fused multiply-add (`mul_add`)
204    /// for improved accuracy and performance.
205    ///
206    /// For a determinant that works for any dimension (falling back to LU for D ≥ 5),
207    /// use [`det`](Self::det).
208    ///
209    /// # Examples
210    /// ```
211    /// use la_stack::prelude::*;
212    ///
213    /// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
214    /// assert!((m.det_direct().unwrap() - (-2.0)).abs() <= 1e-12);
215    ///
216    /// // D = 0 is the empty product.
217    /// assert_eq!(Matrix::<0>::zero().det_direct(), Some(1.0));
218    ///
219    /// // D ≥ 5 returns None.
220    /// assert!(Matrix::<5>::identity().det_direct().is_none());
221    /// ```
222    #[inline]
223    #[must_use]
224    pub const fn det_direct(&self) -> Option<f64> {
225        match D {
226            0 => Some(1.0),
227            1 => Some(self.rows[0][0]),
228            2 => {
229                // ad - bc
230                Some(self.rows[0][0].mul_add(self.rows[1][1], -(self.rows[0][1] * self.rows[1][0])))
231            }
232            3 => {
233                // Cofactor expansion on first row.
234                let m00 =
235                    self.rows[1][1].mul_add(self.rows[2][2], -(self.rows[1][2] * self.rows[2][1]));
236                let m01 =
237                    self.rows[1][0].mul_add(self.rows[2][2], -(self.rows[1][2] * self.rows[2][0]));
238                let m02 =
239                    self.rows[1][0].mul_add(self.rows[2][1], -(self.rows[1][1] * self.rows[2][0]));
240                Some(
241                    self.rows[0][0]
242                        .mul_add(m00, (-self.rows[0][1]).mul_add(m01, self.rows[0][2] * m02)),
243                )
244            }
245            4 => {
246                // Cofactor expansion on first row → four 3×3 sub-determinants.
247                // Hoist the 6 unique 2×2 minors from rows 2–3 (each used twice).
248                let r = &self.rows;
249
250                // 2×2 minors: s_ij = r[2][i]*r[3][j] - r[2][j]*r[3][i]
251                let s23 = r[2][2].mul_add(r[3][3], -(r[2][3] * r[3][2])); // cols 2,3
252                let s13 = r[2][1].mul_add(r[3][3], -(r[2][3] * r[3][1])); // cols 1,3
253                let s12 = r[2][1].mul_add(r[3][2], -(r[2][2] * r[3][1])); // cols 1,2
254                let s03 = r[2][0].mul_add(r[3][3], -(r[2][3] * r[3][0])); // cols 0,3
255                let s02 = r[2][0].mul_add(r[3][2], -(r[2][2] * r[3][0])); // cols 0,2
256                let s01 = r[2][0].mul_add(r[3][1], -(r[2][1] * r[3][0])); // cols 0,1
257
258                // 3×3 cofactors via row 1 expansion using hoisted minors.
259                let c00 = r[1][1].mul_add(s23, (-r[1][2]).mul_add(s13, r[1][3] * s12));
260                let c01 = r[1][0].mul_add(s23, (-r[1][2]).mul_add(s03, r[1][3] * s02));
261                let c02 = r[1][0].mul_add(s13, (-r[1][1]).mul_add(s03, r[1][3] * s01));
262                let c03 = r[1][0].mul_add(s12, (-r[1][1]).mul_add(s02, r[1][2] * s01));
263
264                Some(r[0][0].mul_add(
265                    c00,
266                    (-r[0][1]).mul_add(c01, r[0][2].mul_add(c02, -(r[0][3] * c03))),
267                ))
268            }
269            _ => None,
270        }
271    }
272
273    /// Determinant, using closed-form formulas for D ≤ 4 and LU decomposition for D ≥ 5.
274    ///
275    /// For D ∈ {1, 2, 3, 4}, this bypasses LU factorization entirely for a significant
276    /// speedup (see [`det_direct`](Self::det_direct)). The `tol` parameter is only used
277    /// by the LU fallback path for D ≥ 5.
278    ///
279    /// # Examples
280    /// ```
281    /// use la_stack::prelude::*;
282    ///
283    /// # fn main() -> Result<(), LaError> {
284    /// let det = Matrix::<3>::identity().det(DEFAULT_PIVOT_TOL)?;
285    /// assert!((det - 1.0).abs() <= 1e-12);
286    /// # Ok(())
287    /// # }
288    /// ```
289    ///
290    /// # Errors
291    /// Returns [`LaError::NonFinite`] if the result contains NaN or infinity.
292    /// For D ≥ 5, propagates LU factorization errors (e.g. [`LaError::Singular`]).
293    #[inline]
294    pub fn det(self, tol: f64) -> Result<f64, LaError> {
295        if let Some(d) = self.det_direct() {
296            return if d.is_finite() {
297                Ok(d)
298            } else {
299                Err(LaError::NonFinite { col: 0 })
300            };
301        }
302        self.lu(tol).map(|lu| lu.det())
303    }
304
305    /// Conservative absolute error bound for `det_direct()`.
306    ///
307    /// Returns `Some(bound)` such that `|det_direct() - det_exact| ≤ bound`,
308    /// or `None` for D ≥ 5 where no fast bound is available.
309    ///
310    /// For D ≤ 4, the bound is derived from the matrix permanent using
311    /// Shewchuk-style error analysis. For D = 0 or 1, returns `Some(0.0)`
312    /// since the determinant computation is exact (no arithmetic).
313    ///
314    /// This method does NOT require the `exact` feature — the bounds use
315    /// pure f64 arithmetic and are useful for custom adaptive-precision logic.
316    ///
317    /// # When to use
318    ///
319    /// Use this to build adaptive-precision logic: if `|det_direct()| > bound`,
320    /// the f64 sign is provably correct. Otherwise fall back to exact arithmetic.
321    ///
322    /// # Examples
323    /// ```
324    /// use la_stack::prelude::*;
325    ///
326    /// let m = Matrix::<3>::from_rows([
327    ///     [1.0, 2.0, 3.0],
328    ///     [4.0, 5.0, 6.0],
329    ///     [7.0, 8.0, 9.0],
330    /// ]);
331    /// let bound = m.det_errbound().unwrap();
332    /// let det_approx = m.det_direct().unwrap();
333    /// // If |det_approx| > bound, the sign is guaranteed correct.
334    /// ```
335    ///
336    /// # Adaptive precision pattern (requires `exact` feature)
337    /// ```ignore
338    /// use la_stack::prelude::*;
339    ///
340    /// let m = Matrix::<3>::identity();
341    /// if let Some(bound) = m.det_errbound() {
342    ///     let det = m.det_direct().unwrap();
343    ///     if det.abs() > bound {
344    ///         // f64 sign is guaranteed correct
345    ///         let sign = det.signum() as i8;
346    ///     } else {
347    ///         // Fall back to exact arithmetic (requires `exact` feature)
348    ///         let sign = m.det_sign_exact().unwrap();
349    ///     }
350    /// } else {
351    ///     // D ≥ 5: no fast filter, use exact directly
352    ///     let sign = m.det_sign_exact().unwrap();
353    /// }
354    /// ```
355    #[must_use]
356    #[inline]
357    pub fn det_errbound(&self) -> Option<f64> {
358        match D {
359            0 | 1 => Some(0.0), // No arithmetic — result is exact.
360            2 => {
361                let r = &self.rows;
362                let permanent = (r[0][0] * r[1][1]).abs() + (r[0][1] * r[1][0]).abs();
363                Some(crate::ERR_COEFF_2 * permanent)
364            }
365            3 => {
366                let r = &self.rows;
367                let pm00 = (r[1][1] * r[2][2]).abs() + (r[1][2] * r[2][1]).abs();
368                let pm01 = (r[1][0] * r[2][2]).abs() + (r[1][2] * r[2][0]).abs();
369                let pm02 = (r[1][0] * r[2][1]).abs() + (r[1][1] * r[2][0]).abs();
370                let permanent = r[0][2]
371                    .abs()
372                    .mul_add(pm02, r[0][1].abs().mul_add(pm01, r[0][0].abs() * pm00));
373                Some(crate::ERR_COEFF_3 * permanent)
374            }
375            4 => {
376                let r = &self.rows;
377                // 2×2 minor permanents from rows 2–3.
378                let sp23 = (r[2][2] * r[3][3]).abs() + (r[2][3] * r[3][2]).abs();
379                let sp13 = (r[2][1] * r[3][3]).abs() + (r[2][3] * r[3][1]).abs();
380                let sp12 = (r[2][1] * r[3][2]).abs() + (r[2][2] * r[3][1]).abs();
381                let sp03 = (r[2][0] * r[3][3]).abs() + (r[2][3] * r[3][0]).abs();
382                let sp02 = (r[2][0] * r[3][2]).abs() + (r[2][2] * r[3][0]).abs();
383                let sp01 = (r[2][0] * r[3][1]).abs() + (r[2][1] * r[3][0]).abs();
384                // 3×3 cofactor permanents from row 1.
385                let pc0 = r[1][3]
386                    .abs()
387                    .mul_add(sp12, r[1][2].abs().mul_add(sp13, r[1][1].abs() * sp23));
388                let pc1 = r[1][3]
389                    .abs()
390                    .mul_add(sp02, r[1][2].abs().mul_add(sp03, r[1][0].abs() * sp23));
391                let pc2 = r[1][3]
392                    .abs()
393                    .mul_add(sp01, r[1][1].abs().mul_add(sp03, r[1][0].abs() * sp13));
394                let pc3 = r[1][2]
395                    .abs()
396                    .mul_add(sp01, r[1][1].abs().mul_add(sp02, r[1][0].abs() * sp12));
397                let permanent = r[0][3].abs().mul_add(
398                    pc3,
399                    r[0][2]
400                        .abs()
401                        .mul_add(pc2, r[0][1].abs().mul_add(pc1, r[0][0].abs() * pc0)),
402                );
403                Some(crate::ERR_COEFF_4 * permanent)
404            }
405            _ => None,
406        }
407    }
408}
409
410impl<const D: usize> Default for Matrix<D> {
411    #[inline]
412    fn default() -> Self {
413        Self::zero()
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use super::*;
420    use crate::DEFAULT_PIVOT_TOL;
421
422    use approx::assert_abs_diff_eq;
423    use pastey::paste;
424    use std::hint::black_box;
425
426    macro_rules! gen_public_api_matrix_tests {
427        ($d:literal) => {
428            paste! {
429                #[test]
430                fn [<public_api_matrix_from_rows_get_set_bounds_checked_ $d d>]() {
431                    let mut rows = [[0.0f64; $d]; $d];
432                    rows[0][0] = 1.0;
433                    rows[$d - 1][$d - 1] = -2.0;
434
435                    let mut m = Matrix::<$d>::from_rows(rows);
436
437                    assert_eq!(m.get(0, 0), Some(1.0));
438                    assert_eq!(m.get($d - 1, $d - 1), Some(-2.0));
439
440                    // Out-of-bounds is None.
441                    assert_eq!(m.get($d, 0), None);
442
443                    // Out-of-bounds set fails.
444                    assert!(!m.set($d, 0, 3.0));
445
446                    // In-bounds set works.
447                    assert!(m.set(0, $d - 1, 3.0));
448                    assert_eq!(m.get(0, $d - 1), Some(3.0));
449                }
450
451                #[test]
452                fn [<public_api_matrix_zero_and_default_are_zero_ $d d>]() {
453                    let z = Matrix::<$d>::zero();
454                    assert_abs_diff_eq!(z.inf_norm(), 0.0, epsilon = 0.0);
455
456                    let d = Matrix::<$d>::default();
457                    assert_abs_diff_eq!(d.inf_norm(), 0.0, epsilon = 0.0);
458                }
459
460                #[test]
461                fn [<public_api_matrix_inf_norm_max_row_sum_ $d d>]() {
462                    let mut rows = [[0.0f64; $d]; $d];
463
464                    // Row 0 has absolute row sum = D.
465                    for c in 0..$d {
466                        rows[0][c] = -1.0;
467                    }
468
469                    // Row 1 has smaller absolute row sum.
470                    for c in 0..$d {
471                        rows[1][c] = 0.5;
472                    }
473
474                    let m = Matrix::<$d>::from_rows(rows);
475                    assert_abs_diff_eq!(m.inf_norm(), f64::from($d), epsilon = 0.0);
476                }
477
478                #[test]
479                fn [<public_api_matrix_identity_lu_det_solve_vec_ $d d>]() {
480                    let m = Matrix::<$d>::identity();
481
482                    // Identity has ones on diag and zeros off diag.
483                    for r in 0..$d {
484                        for c in 0..$d {
485                            let expected = if r == c { 1.0 } else { 0.0 };
486                            assert_abs_diff_eq!(m.get(r, c).unwrap(), expected, epsilon = 0.0);
487                        }
488                    }
489
490                    // Determinant is 1.
491                    let det = m.det(DEFAULT_PIVOT_TOL).unwrap();
492                    assert_abs_diff_eq!(det, 1.0, epsilon = 1e-12);
493
494                    // LU solve on identity returns the RHS.
495                    let lu = m.lu(DEFAULT_PIVOT_TOL).unwrap();
496
497                    let b_arr = {
498                        let mut arr = [0.0f64; $d];
499                        let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
500                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
501                            *dst = *src;
502                        }
503                        arr
504                    };
505
506                    let b = crate::Vector::<$d>::new(b_arr);
507                    let x = lu.solve_vec(b).unwrap().into_array();
508
509                    for (x_i, b_i) in x.iter().zip(b_arr.iter()) {
510                        assert_abs_diff_eq!(*x_i, *b_i, epsilon = 1e-12);
511                    }
512                }
513            }
514        };
515    }
516
517    // Mirror delaunay-style multi-dimension tests.
518    gen_public_api_matrix_tests!(2);
519    gen_public_api_matrix_tests!(3);
520    gen_public_api_matrix_tests!(4);
521    gen_public_api_matrix_tests!(5);
522
523    // === det_direct tests ===
524
525    #[test]
526    fn det_direct_d0_is_one() {
527        assert_eq!(Matrix::<0>::zero().det_direct(), Some(1.0));
528    }
529
530    #[test]
531    fn det_direct_d1_returns_element() {
532        let m = Matrix::<1>::from_rows([[42.0]]);
533        assert_eq!(m.det_direct(), Some(42.0));
534    }
535
536    #[test]
537    fn det_direct_d2_known_value() {
538        // [[1,2],[3,4]] → det = 1*4 - 2*3 = -2
539        // black_box prevents compile-time constant folding of the const fn.
540        let m = black_box(Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]));
541        assert_abs_diff_eq!(m.det_direct().unwrap(), -2.0, epsilon = 1e-15);
542    }
543
544    #[test]
545    fn det_direct_d3_known_value() {
546        // Classic 3×3: det = 0
547        let m = black_box(Matrix::<3>::from_rows([
548            [1.0, 2.0, 3.0],
549            [4.0, 5.0, 6.0],
550            [7.0, 8.0, 9.0],
551        ]));
552        assert_abs_diff_eq!(m.det_direct().unwrap(), 0.0, epsilon = 1e-12);
553    }
554
555    #[test]
556    fn det_direct_d3_nonsingular() {
557        // [[2,1,0],[0,3,1],[1,0,2]] → det = 2*(6-0) - 1*(0-1) + 0 = 13
558        let m = black_box(Matrix::<3>::from_rows([
559            [2.0, 1.0, 0.0],
560            [0.0, 3.0, 1.0],
561            [1.0, 0.0, 2.0],
562        ]));
563        assert_abs_diff_eq!(m.det_direct().unwrap(), 13.0, epsilon = 1e-12);
564    }
565
566    #[test]
567    fn det_direct_d4_identity() {
568        let m = black_box(Matrix::<4>::identity());
569        assert_abs_diff_eq!(m.det_direct().unwrap(), 1.0, epsilon = 1e-15);
570    }
571
572    #[test]
573    fn det_direct_d4_known_value() {
574        // Diagonal matrix: det = product of diagonal entries.
575        let mut rows = [[0.0f64; 4]; 4];
576        rows[0][0] = 2.0;
577        rows[1][1] = 3.0;
578        rows[2][2] = 5.0;
579        rows[3][3] = 7.0;
580        let m = black_box(Matrix::<4>::from_rows(rows));
581        assert_abs_diff_eq!(m.det_direct().unwrap(), 210.0, epsilon = 1e-12);
582    }
583
584    #[test]
585    fn det_direct_d5_returns_none() {
586        assert_eq!(Matrix::<5>::identity().det_direct(), None);
587    }
588
589    #[test]
590    fn det_direct_d8_returns_none() {
591        assert_eq!(Matrix::<8>::zero().det_direct(), None);
592    }
593
594    macro_rules! gen_det_direct_agrees_with_lu {
595        ($d:literal) => {
596            paste! {
597                #[test]
598                #[allow(clippy::cast_precision_loss)] // r, c, D are tiny integers
599                fn [<det_direct_agrees_with_lu_ $d d>]() {
600                    // Well-conditioned matrix: diagonally dominant.
601                    let mut rows = [[0.0f64; $d]; $d];
602                    for r in 0..$d {
603                        for c in 0..$d {
604                            rows[r][c] = if r == c {
605                                (r as f64) + f64::from($d) + 1.0
606                            } else {
607                                0.1 / ((r + c + 1) as f64)
608                            };
609                        }
610                    }
611                    let m = Matrix::<$d>::from_rows(rows);
612                    let direct = m.det_direct().unwrap();
613                    let lu_det = m.lu(DEFAULT_PIVOT_TOL).unwrap().det();
614                    let eps = lu_det.abs().mul_add(1e-12, 1e-12);
615                    assert_abs_diff_eq!(direct, lu_det, epsilon = eps);
616                }
617            }
618        };
619    }
620
621    gen_det_direct_agrees_with_lu!(1);
622    gen_det_direct_agrees_with_lu!(2);
623    gen_det_direct_agrees_with_lu!(3);
624    gen_det_direct_agrees_with_lu!(4);
625
626    #[test]
627    fn det_direct_identity_all_dims() {
628        assert_abs_diff_eq!(
629            Matrix::<1>::identity().det_direct().unwrap(),
630            1.0,
631            epsilon = 0.0
632        );
633        assert_abs_diff_eq!(
634            Matrix::<2>::identity().det_direct().unwrap(),
635            1.0,
636            epsilon = 0.0
637        );
638        assert_abs_diff_eq!(
639            Matrix::<3>::identity().det_direct().unwrap(),
640            1.0,
641            epsilon = 0.0
642        );
643        assert_abs_diff_eq!(
644            Matrix::<4>::identity().det_direct().unwrap(),
645            1.0,
646            epsilon = 0.0
647        );
648    }
649
650    #[test]
651    fn det_direct_zero_matrix() {
652        assert_abs_diff_eq!(
653            Matrix::<2>::zero().det_direct().unwrap(),
654            0.0,
655            epsilon = 0.0
656        );
657        assert_abs_diff_eq!(
658            Matrix::<3>::zero().det_direct().unwrap(),
659            0.0,
660            epsilon = 0.0
661        );
662        assert_abs_diff_eq!(
663            Matrix::<4>::zero().det_direct().unwrap(),
664            0.0,
665            epsilon = 0.0
666        );
667    }
668
669    #[test]
670    fn det_returns_nonfinite_error_for_nan_d2() {
671        let m = Matrix::<2>::from_rows([[f64::NAN, 1.0], [1.0, 1.0]]);
672        assert_eq!(m.det(DEFAULT_PIVOT_TOL), Err(LaError::NonFinite { col: 0 }));
673    }
674
675    #[test]
676    fn det_returns_nonfinite_error_for_inf_d3() {
677        let m =
678            Matrix::<3>::from_rows([[f64::INFINITY, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]);
679        assert_eq!(m.det(DEFAULT_PIVOT_TOL), Err(LaError::NonFinite { col: 0 }));
680    }
681
682    #[test]
683    fn det_direct_is_const_evaluable_d2() {
684        // Const evaluation proves the function is truly const fn.
685        const DET: Option<f64> = {
686            let m = Matrix::<2>::from_rows([[1.0, 0.0], [0.0, 1.0]]);
687            m.det_direct()
688        };
689        assert_eq!(DET, Some(1.0));
690    }
691
692    #[test]
693    fn det_direct_is_const_evaluable_d3() {
694        const DET: Option<f64> = {
695            let m = Matrix::<3>::from_rows([[2.0, 0.0, 0.0], [0.0, 3.0, 0.0], [0.0, 0.0, 5.0]]);
696            m.det_direct()
697        };
698        assert_eq!(DET, Some(30.0));
699    }
700
701    // === det_errbound tests (no `exact` feature required) ===
702
703    #[test]
704    fn det_errbound_available_without_exact_feature() {
705        // Verify det_errbound is accessible without exact feature
706        let m = Matrix::<3>::identity();
707        let bound = m.det_errbound();
708        assert!(bound.is_some());
709        assert!(bound.unwrap() > 0.0);
710    }
711
712    #[test]
713    fn det_errbound_d5_returns_none() {
714        // D=5 has no fast filter
715        assert_eq!(Matrix::<5>::identity().det_errbound(), None);
716    }
717}