Skip to main content

la_stack/
matrix.rs

1#![forbid(unsafe_code)]
2
3//! Fixed-size, stack-allocated square matrices.
4
5use core::hint::cold_path;
6
7use crate::ldlt::Ldlt;
8use crate::lu::Lu;
9use crate::{ArithmeticOperation, ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4, LaError, Tolerance};
10
11/// A closed-form determinant and its certified absolute error bound.
12///
13/// Values of this type are produced by
14/// [`Matrix::det_direct_with_errbound`]. The paired result guarantees that the
15/// determinant was evaluated once and that its matching bound was computed for
16/// the same matrix in one call. The guarantee is unavailable when gradual
17/// underflow could invalidate the relative-error analysis or when the matrix
18/// dimension exceeds the closed-form D ≤ 4 scope.
19#[must_use]
20#[non_exhaustive]
21#[derive(Clone, Copy, Debug, PartialEq)]
22pub struct DeterminantWithErrorBound {
23    determinant: f64,
24    absolute_error_bound: f64,
25}
26
27impl DeterminantWithErrorBound {
28    /// Return the closed-form determinant approximation.
29    #[inline]
30    #[must_use]
31    pub const fn determinant(self) -> f64 {
32        self.determinant
33    }
34
35    /// Return the certified absolute error bound.
36    ///
37    /// The exact determinant lies in
38    /// `[determinant - bound, determinant + bound]`.
39    #[inline]
40    #[must_use]
41    pub const fn absolute_error_bound(self) -> f64 {
42        self.absolute_error_bound
43    }
44}
45
46/// Finite fixed-size square matrix `D×D`, stored inline.
47///
48/// `Matrix` is designed for small, robustness-sensitive systems where stack
49/// allocation and const-generic dimensions are useful. For large, dynamic, sparse,
50/// or parallel workloads, prefer a broader linear-algebra crate such as
51/// [`nalgebra`](https://crates.io/crates/nalgebra) or
52/// [`faer`](https://crates.io/crates/faer).
53///
54/// Public construction and mutation reject NaN and infinity through
55/// [`try_from_rows`](Self::try_from_rows) and [`set`](Self::set). The storage
56/// field is private, so a
57/// `Matrix` value carries the invariant that every stored entry is finite.
58/// Algorithms therefore do not re-scan stored entries at every use; user-visible
59/// non-finite errors come from construction/mutation boundaries or from values
60/// computed during arithmetic, such as overflowed elimination or determinant
61/// intermediates.
62///
63/// Direct field construction is intentionally unavailable to downstream callers:
64///
65/// ```compile_fail
66/// use la_stack::Matrix;
67///
68/// let _ = Matrix::<2> {
69///     rows: [[1.0, f64::NAN], [0.0, 1.0]],
70/// };
71/// ```
72#[must_use]
73#[derive(Clone, Copy, Debug, PartialEq)]
74pub struct Matrix<const D: usize> {
75    rows: [[f64; D]; D],
76}
77
78/// A finite [`Matrix`] proven exactly symmetric for LDLT factorization.
79///
80/// Mirrored entries have equal numeric values; IEEE-754 signed zeros may have
81/// different bit patterns because `+0.0 == -0.0`.
82#[must_use]
83#[derive(Clone, Copy, Debug, PartialEq)]
84pub(crate) struct SymmetricMatrix<const D: usize> {
85    matrix: Matrix<D>,
86}
87
88/// Rounded arithmetic result together with proof that gradual underflow could
89/// not have changed that operation's result.
90///
91/// The determinant filter may only use its relative-error coefficients while
92/// every rounded operation in both the determinant and absolute-Leibniz trees
93/// stays in the normal range. Exact structural zeros are safe; cancellation to
94/// zero is conservatively treated as inconclusive.
95#[derive(Clone, Copy, Debug, PartialEq)]
96struct FilterArithmetic<const TRACK_UNDERFLOW: bool> {
97    value: f64,
98    underflow_safe: bool,
99}
100
101impl<const TRACK_UNDERFLOW: bool> FilterArithmetic<TRACK_UNDERFLOW> {
102    /// Return whether a rounded result is normal or non-finite.
103    ///
104    /// A single exponent-field test keeps the overwhelmingly common normal
105    /// path cheap. Callers inspect operands only when the result is zero or
106    /// subnormal so they can distinguish structural zero from range loss.
107    #[expect(
108        clippy::inline_always,
109        reason = "determinant hot-path specialization must eliminate unused safety state"
110    )]
111    #[inline(always)]
112    const fn has_nonzero_exponent(value: f64) -> bool {
113        value.to_bits() & 0x7ff0_0000_0000_0000 != 0
114    }
115
116    /// Ordinary floating-point multiplication.
117    #[expect(
118        clippy::inline_always,
119        reason = "determinant hot-path specialization must eliminate unused safety state"
120    )]
121    #[inline(always)]
122    const fn multiply(lhs: f64, rhs: f64) -> Self {
123        let value = lhs * rhs;
124        Self {
125            value,
126            underflow_safe: !TRACK_UNDERFLOW
127                || Self::has_nonzero_exponent(value)
128                || lhs == 0.0
129                || rhs == 0.0,
130        }
131    }
132
133    /// Ordinary addition of the non-negative terms used by the error-bound tree.
134    #[expect(
135        clippy::inline_always,
136        reason = "determinant hot-path specialization must eliminate unused safety state"
137    )]
138    #[inline(always)]
139    const fn add_non_negative(lhs: f64, rhs: f64) -> Self {
140        let value = lhs + rhs;
141        Self {
142            value,
143            underflow_safe: !TRACK_UNDERFLOW
144                || Self::has_nonzero_exponent(value)
145                || (lhs == 0.0 && rhs == 0.0),
146        }
147    }
148
149    /// Fused multiply-add.
150    #[expect(
151        clippy::inline_always,
152        reason = "determinant hot-path specialization must eliminate unused safety state"
153    )]
154    #[inline(always)]
155    const fn mul_add(lhs: f64, rhs: f64, addend: f64) -> Self {
156        let value = lhs.mul_add(rhs, addend);
157        Self {
158            value,
159            underflow_safe: !TRACK_UNDERFLOW
160                || Self::has_nonzero_exponent(value)
161                || ((lhs == 0.0 || rhs == 0.0) && addend == 0.0),
162        }
163    }
164}
165
166/// A finite D=4 matrix proven safe for shared-minor determinant and permanent
167/// evaluation.
168///
169/// Construction proves both the fixed dimension and that every coefficient in
170/// the first two rows is non-zero. The latter makes every shared 2×2 minor part
171/// of an active Leibniz term, so the dense kernel cannot evaluate an overflowing
172/// minor solely for a mathematically absent term.
173#[repr(transparent)]
174#[derive(Clone, Copy)]
175struct Det4SharedMinorInput<'a, const D: usize> {
176    matrix: &'a Matrix<D>,
177}
178
179impl<'a, const D: usize> Det4SharedMinorInput<'a, D> {
180    /// Parse a matrix into the shared-minor D=4 domain.
181    ///
182    /// `None` selects the guarded determinant path; it does not represent an
183    /// invalid public matrix.
184    #[expect(
185        clippy::inline_always,
186        reason = "the D=4 determinant hot path must eliminate its proof wrapper"
187    )]
188    #[inline(always)]
189    const fn try_new(matrix: &'a Matrix<D>) -> Option<Self> {
190        if D != 4 {
191            return None;
192        }
193
194        let r = &matrix.rows;
195        let shared_minors_are_active = (r[0][0] != 0.0)
196            && (r[0][1] != 0.0)
197            && (r[0][2] != 0.0)
198            && (r[0][3] != 0.0)
199            && (r[1][0] != 0.0)
200            && (r[1][1] != 0.0)
201            && (r[1][2] != 0.0)
202            && (r[1][3] != 0.0);
203
204        if shared_minors_are_active {
205            Some(Self { matrix })
206        } else {
207            None
208        }
209    }
210}
211
212impl<const D: usize> SymmetricMatrix<D> {
213    /// Consume the wrapper and return the underlying matrix.
214    #[inline]
215    pub(crate) const fn into_matrix(self) -> Matrix<D> {
216        self.matrix
217    }
218
219    /// Construct a symmetric matrix proof without checking the invariant.
220    ///
221    /// This constructor is only for paths that have already validated exact
222    /// mirrored-entry equality with the same predicate as
223    /// [`try_new`](Self::try_new). Finiteness is carried by [`Matrix`].
224    #[inline]
225    const fn new_unchecked(matrix: Matrix<D>) -> Self {
226        Self { matrix }
227    }
228
229    /// Validate that every mirrored pair has exactly the same finite value.
230    ///
231    /// IEEE-754 signed zeros compare equal, so `+0.0` and `-0.0` satisfy this
232    /// mathematical-symmetry proof even though their bit patterns differ.
233    ///
234    /// # Errors
235    /// Returns [`LaError::Asymmetric`] with `allowed_abs_diff == 0.0` when the
236    /// first off-diagonal pair is not exactly equal.
237    #[inline]
238    #[expect(
239        clippy::float_cmp,
240        reason = "LDLT requires exact mirrored-entry equality to factor the supplied operator"
241    )]
242    fn try_new(matrix: Matrix<D>) -> Result<Self, LaError> {
243        for row in 0..D {
244            for col in (row + 1)..D {
245                let upper = matrix.rows[row][col];
246                let lower = matrix.rows[col][row];
247                if upper != lower {
248                    cold_path();
249                    return Err(LaError::asymmetric(row, col, D, upper, lower, 0.0));
250                }
251            }
252        }
253
254        Ok(Self::new_unchecked(matrix))
255    }
256}
257
258impl<const D: usize> Matrix<D> {
259    /// Try to create a finite matrix from row-major storage.
260    ///
261    /// This is the public raw-storage boundary for matrices. Successful
262    /// construction makes the returned [`Matrix`] a finite-storage proof.
263    ///
264    /// # Examples
265    /// ```
266    /// use la_stack::prelude::*;
267    ///
268    /// # fn main() -> Result<(), LaError> {
269    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
270    /// assert_eq!(m.get(0, 1), Some(2.0));
271    /// # Ok(())
272    /// # }
273    /// ```
274    ///
275    /// # Errors
276    /// Returns [`LaError::NonFinite`] with matrix coordinates for the first
277    /// offending entry in row-major order when `rows` contains NaN or infinity.
278    #[inline]
279    pub const fn try_from_rows(rows: [[f64; D]; D]) -> Result<Self, LaError> {
280        if let Some((row, col)) = Self::first_non_finite_cell(&rows) {
281            Err(LaError::non_finite_input_matrix(row, col))
282        } else {
283            Ok(Self::from_rows_unchecked(rows))
284        }
285    }
286
287    /// Construct a matrix without checking that entries are finite.
288    ///
289    /// This module-private escape hatch is reserved for finite literals and
290    /// algorithm outputs whose finite invariant is visible at the call site.
291    /// Computed outputs must be validated before becoming observable API values.
292    #[inline]
293    const fn from_rows_unchecked(rows: [[f64; D]; D]) -> Self {
294        Self { rows }
295    }
296
297    /// Borrow the finite row-major backing array.
298    ///
299    /// The returned view is tied to this [`Matrix`], so callers can inspect the
300    /// canonical storage without copying it or bypassing the finite-value
301    /// invariant.
302    ///
303    /// # Examples
304    /// ```
305    /// use la_stack::prelude::*;
306    ///
307    /// # fn main() -> Result<(), LaError> {
308    /// let matrix = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
309    /// assert_eq!(matrix.as_rows(), &[[1.0, 2.0], [3.0, 4.0]]);
310    /// # Ok(())
311    /// # }
312    /// ```
313    ///
314    /// A live view keeps the matrix immutably borrowed, so validated mutation
315    /// cannot occur until the view is no longer used:
316    ///
317    /// ```compile_fail
318    /// use la_stack::Matrix;
319    ///
320    /// let mut matrix = Matrix::<2>::identity();
321    /// let rows = matrix.as_rows();
322    /// assert!(matrix.set(0, 0, 5.0).is_ok());
323    /// assert_eq!(rows[0][0], 1.0);
324    /// ```
325    #[inline]
326    #[must_use]
327    pub const fn as_rows(&self) -> &[[f64; D]; D] {
328        &self.rows
329    }
330
331    /// Consume this matrix and return its finite row-major backing array.
332    ///
333    /// # Examples
334    /// ```
335    /// use la_stack::prelude::*;
336    ///
337    /// # fn main() -> Result<(), LaError> {
338    /// let matrix = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
339    /// assert_eq!(matrix.into_rows(), [[1.0, 2.0], [3.0, 4.0]]);
340    /// # Ok(())
341    /// # }
342    /// ```
343    #[inline]
344    #[must_use]
345    pub const fn into_rows(self) -> [[f64; D]; D] {
346        self.rows
347    }
348
349    /// All-zeros finite matrix.
350    ///
351    /// # Examples
352    /// ```
353    /// use la_stack::prelude::*;
354    ///
355    /// let z = Matrix::<2>::zero();
356    /// assert_eq!(z.get(1, 1), Some(0.0));
357    /// ```
358    #[inline]
359    pub const fn zero() -> Self {
360        Self::from_rows_unchecked([[0.0; D]; D])
361    }
362
363    /// Finite identity matrix.
364    ///
365    /// # Examples
366    /// ```
367    /// use la_stack::prelude::*;
368    ///
369    /// let i = Matrix::<3>::identity();
370    /// assert_eq!(i.get(0, 0), Some(1.0));
371    /// assert_eq!(i.get(0, 1), Some(0.0));
372    /// assert_eq!(i.get(2, 2), Some(1.0));
373    /// ```
374    #[inline]
375    pub const fn identity() -> Self {
376        let mut m = Self::zero();
377
378        let mut i = 0;
379        while i < D {
380            m.rows[i][i] = 1.0;
381            i += 1;
382        }
383
384        m
385    }
386
387    /// Get a finite element with bounds checking.
388    ///
389    /// # Examples
390    /// ```
391    /// use la_stack::prelude::*;
392    ///
393    /// # fn main() -> Result<(), LaError> {
394    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
395    /// assert_eq!(m.get(1, 0), Some(3.0));
396    /// assert_eq!(m.get(2, 0), None);
397    /// # Ok(())
398    /// # }
399    /// ```
400    #[inline]
401    #[must_use]
402    pub const fn get(&self, row: usize, col: usize) -> Option<f64> {
403        if row < D && col < D {
404            Some(self.rows[row][col])
405        } else {
406            None
407        }
408    }
409
410    /// Get a finite element, preserving index context on failure.
411    ///
412    /// Prefer [`get`](Self::get) for const or hot paths that only need
413    /// `Option`-style absence.  Use this method at public runtime boundaries
414    /// where row, column, and dimension context should survive in a typed error.
415    ///
416    /// # Examples
417    /// ```
418    /// use core::assert_matches;
419    /// use la_stack::prelude::*;
420    ///
421    /// # fn main() -> Result<(), LaError> {
422    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
423    /// assert_eq!(m.try_get(1, 0)?, 3.0);
424    /// assert_matches!(
425    ///     m.try_get(2, 0),
426    ///     Err(LaError::IndexOutOfBounds {
427    ///         row: 2,
428    ///         col: 0,
429    ///         dim: 2,
430    ///         ..
431    ///     })
432    /// );
433    /// # Ok(())
434    /// # }
435    /// ```
436    ///
437    /// # Errors
438    /// Returns [`LaError::IndexOutOfBounds`] when either index is not `< D`.
439    #[inline]
440    pub const fn try_get(&self, row: usize, col: usize) -> Result<f64, LaError> {
441        if row < D && col < D {
442            Ok(self.rows[row][col])
443        } else {
444            Err(LaError::index_out_of_bounds(row, col, D))
445        }
446    }
447
448    /// Set a finite element with bounds checking.
449    ///
450    /// # Examples
451    /// ```
452    /// use core::assert_matches;
453    /// use la_stack::prelude::*;
454    ///
455    /// # fn main() -> Result<(), LaError> {
456    /// let mut m = Matrix::<2>::zero();
457    /// assert_eq!(m.set(0, 1, 2.5), Ok(()));
458    /// assert_eq!(m.get(0, 1), Some(2.5));
459    /// assert_matches!(
460    ///     m.set(10, 0, 1.0),
461    ///     Err(LaError::IndexOutOfBounds {
462    ///         row: 10,
463    ///         col: 0,
464    ///         dim: 2,
465    ///         ..
466    ///     })
467    /// );
468    /// # Ok(())
469    /// # }
470    /// ```
471    ///
472    /// # Errors
473    /// Returns [`LaError::IndexOutOfBounds`] when either index is not `< D`.
474    /// Returns [`LaError::NonFinite`] when `value` is NaN or infinity.
475    #[inline]
476    pub const fn set(&mut self, row: usize, col: usize, value: f64) -> Result<(), LaError> {
477        if row >= D || col >= D {
478            return Err(LaError::index_out_of_bounds(row, col, D));
479        }
480        if !value.is_finite() {
481            return Err(LaError::non_finite_input_matrix(row, col));
482        }
483        self.rows[row][col] = value;
484        Ok(())
485    }
486
487    /// Infinity norm (maximum absolute row sum).
488    ///
489    /// # Non-finite handling
490    /// Public constructors and setters reject raw non-finite entries, but
491    /// `Matrix` values are finite by construction. `inf_norm` returns
492    /// [`LaError::NonFinite`] with the matrix cell whose addition first makes a
493    /// row sum non-finite.
494    ///
495    /// Row sums are accumulated in `f64` with ordinary addition.  This method
496    /// checks for overflowed accumulators, but it does not provide a certified
497    /// absolute rounding bound for the returned norm.
498    ///
499    /// # Examples
500    /// ```
501    /// use core::assert_matches;
502    /// use la_stack::prelude::*;
503    ///
504    /// # fn main() -> Result<(), LaError> {
505    /// let m = Matrix::<2>::try_from_rows([[1.0, -2.0], [3.0, 4.0]])?;
506    /// assert!((m.inf_norm()? - 7.0).abs() <= 1e-12);
507    ///
508    /// // Raw NaN entries are rejected with coordinates.
509    /// assert_matches!(
510    ///     Matrix::<2>::try_from_rows([[f64::NAN, 1.0], [2.0, 3.0]]),
511    ///     Err(LaError::NonFinite {
512    ///         location: NonFiniteLocation::MatrixCell { row: 0, col: 0, .. },
513    ///         origin: NonFiniteOrigin::Input,
514    ///         ..
515    ///     })
516    /// );
517    /// # Ok(())
518    /// # }
519    /// ```
520    ///
521    /// # Errors
522    /// Returns [`LaError::NonFinite`] with matrix coordinates when a row sum
523    /// overflows to NaN or infinity.
524    #[inline]
525    pub const fn inf_norm(&self) -> Result<f64, LaError> {
526        let mut max_row_sum: f64 = 0.0;
527
528        let mut r = 0;
529        while r < D {
530            let row = &self.rows[r];
531            let mut row_sum: f64 = 0.0;
532            let mut c = 0;
533            while c < D {
534                row_sum += row[c].abs();
535                c += 1;
536            }
537            if !row_sum.is_finite() {
538                cold_path();
539                return Err(Self::inf_norm_overflow_error(row, r));
540            }
541            if row_sum > max_row_sum {
542                max_row_sum = row_sum;
543            }
544            r += 1;
545        }
546
547        Ok(max_row_sum)
548    }
549
550    /// Replay an overflowed infinity-norm row to locate the first non-finite sum.
551    ///
552    /// This runs only after the success-path traversal has found a non-finite
553    /// completed row sum. Because stored entries are finite and their absolute
554    /// values are non-negative, replaying the same additions must find the
555    /// first column whose addition overflowed; if every earlier prefix is
556    /// finite, the final column is that first failure.
557    #[cold]
558    const fn inf_norm_overflow_error(row: &[f64; D], row_index: usize) -> LaError {
559        let mut row_sum = 0.0;
560        let mut col = 0;
561        let last_col = D.saturating_sub(1);
562        while col < last_col {
563            row_sum += row[col].abs();
564            if !row_sum.is_finite() {
565                return LaError::non_finite_computation_matrix(
566                    ArithmeticOperation::MatrixInfinityNorm,
567                    row_index,
568                    col,
569                );
570            }
571            col += 1;
572        }
573
574        LaError::non_finite_computation_matrix(
575            ArithmeticOperation::MatrixInfinityNorm,
576            row_index,
577            last_col,
578        )
579    }
580
581    /// Returns `true` if the matrix is approximately symmetric within a relative tolerance.
582    ///
583    /// Two entries `self[r][c]` and `self[c][r]` are considered equal (for the
584    /// purposes of symmetry) when
585    /// `|self[r][c] - self[c][r]| <= rel_tol * max(1.0, inf_norm(self))`.
586    /// This is a diagnostic predicate for applications that have an
587    /// approximation-specific symmetry threshold. It is not the precondition
588    /// used by [`ldlt`](Self::ldlt), which requires exact mirrored-entry
589    /// equality so the returned factors represent the original matrix.
590    ///
591    /// Use [`first_asymmetry`](Self::first_asymmetry) to locate the first
592    /// offending pair when this returns `Ok(false)`.
593    ///
594    /// The `rel_tol` argument is a [`Tolerance`], so raw caller input must be
595    /// finite and non-negative before it can reach this predicate. Use
596    /// [`Tolerance::try_new`] when accepting a raw `f64`; negative, NaN, and
597    /// infinite tolerances return
598    /// [`LaError::InvalidTolerance`].
599    ///
600    /// # Overflow handling
601    /// A finite matrix can return [`LaError::NonFinite`] with matrix coordinates
602    /// if computing the scaled symmetry tolerance overflows to NaN or infinity.
603    /// If both stored entries are finite but their difference overflows to ±∞,
604    /// the pair is reported as asymmetric.
605    ///
606    /// # Examples
607    /// ```
608    /// use la_stack::prelude::*;
609    ///
610    /// # fn main() -> Result<(), LaError> {
611    /// let a = Matrix::<2>::try_from_rows([[4.0, 2.0], [2.0, 3.0]])?;
612    /// let tol = Tolerance::try_new(1e-12)?;
613    /// assert!(a.is_symmetric(tol)?);
614    ///
615    /// let b = Matrix::<2>::try_from_rows([[4.0, 2.0], [3.0, 3.0]])?;
616    /// assert!(!b.is_symmetric(tol)?);
617    /// # Ok(())
618    /// # }
619    /// ```
620    ///
621    /// # Errors
622    /// Returns [`LaError::NonFinite`] with matrix coordinates when computing the
623    /// scaled symmetry tolerance overflows to NaN or infinity.
624    #[inline]
625    pub fn is_symmetric(&self, rel_tol: Tolerance) -> Result<bool, LaError> {
626        Ok(self.first_asymmetry(rel_tol)?.is_none())
627    }
628
629    /// Returns the indices `(r, c)` (with `r < c`) of the first off-diagonal
630    /// pair that violates approximate symmetry, or `None` if the matrix is
631    /// symmetric within `rel_tol`.
632    ///
633    /// Iteration order is row-major over the strict upper triangle, so the
634    /// returned indices are the lexicographically smallest such pair.  The
635    /// predicate is the same as [`is_symmetric`](Self::is_symmetric):
636    /// `|self[r][c] - self[c][r]| <= rel_tol * max(1.0, inf_norm(self))`.
637    /// It is intentionally distinct from the exact equality required by
638    /// [`ldlt`](Self::ldlt).
639    ///
640    /// A finite matrix can return [`LaError::NonFinite`] with matrix coordinates
641    /// if computing the scaled symmetry tolerance overflows to NaN or infinity.
642    /// If both stored entries are finite but their difference overflows to ±∞,
643    /// the pair is reported as asymmetric.
644    ///
645    /// The `rel_tol` argument is a [`Tolerance`], so raw caller input must be
646    /// finite and non-negative before it can reach this predicate. Use
647    /// [`Tolerance::try_new`] when accepting a raw `f64`; negative, NaN, and
648    /// infinite tolerances return
649    /// [`LaError::InvalidTolerance`].
650    ///
651    /// # Examples
652    /// ```
653    /// use la_stack::prelude::*;
654    ///
655    /// # fn main() -> Result<(), LaError> {
656    /// let a = Matrix::<3>::try_from_rows([
657    ///     [1.0, 2.0, 0.0],
658    ///     [2.0, 4.0, 5.0],
659    ///     [0.0, 6.0, 9.0], // 6.0 breaks symmetry with a[1][2] = 5.0
660    /// ])?;
661    /// let tol = Tolerance::try_new(1e-12)?;
662    /// assert_eq!(a.first_asymmetry(tol)?, Some((1, 2)));
663    /// assert_eq!(Matrix::<3>::identity().first_asymmetry(tol)?, None);
664    /// # Ok(())
665    /// # }
666    /// ```
667    ///
668    /// # Errors
669    /// Returns [`LaError::NonFinite`] with matrix coordinates when computing the
670    /// scaled symmetry tolerance overflows to NaN or infinity.
671    #[inline]
672    pub fn first_asymmetry(&self, rel_tol: Tolerance) -> Result<Option<(usize, usize)>, LaError> {
673        let eps = self.symmetry_epsilon(rel_tol)?;
674        for r in 0..D {
675            for c in (r + 1)..D {
676                let upper = self.rows[r][c];
677                let lower = self.rows[c][r];
678
679                let diff = (upper - lower).abs();
680                if !diff.is_finite() || diff > eps {
681                    cold_path();
682                    return Ok(Some((r, c)));
683                }
684            }
685        }
686        Ok(None)
687    }
688
689    /// Compute an LU decomposition with partial pivoting.
690    ///
691    /// `D = 0` follows the empty-matrix convention: factorization succeeds,
692    /// [`Lu::det`](crate::Lu::det) returns `1.0`, and solving a length-zero
693    /// right-hand side returns a length-zero [`Vector`](crate::Vector).
694    /// Partial pivoting is a practical finite-precision strategy, not a
695    /// certified accuracy guarantee; see `REFERENCES.md` \[1-3, 11-12\].
696    ///
697    /// # Examples
698    /// ```
699    /// use la_stack::prelude::*;
700    ///
701    /// # fn main() -> Result<(), LaError> {
702    /// let a = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
703    /// let lu = a.lu(DEFAULT_SINGULAR_TOL)?;
704    ///
705    /// let b = Vector::<2>::try_new([5.0, 11.0])?;
706    /// let x = lu.solve(b)?.into_array();
707    ///
708    /// assert!((x[0] - 1.0).abs() <= 1e-12);
709    /// assert!((x[1] - 2.0).abs() <= 1e-12);
710    /// # Ok(())
711    /// # }
712    /// ```
713    ///
714    /// Empty matrices use the standard empty-product convention:
715    ///
716    /// ```
717    /// use la_stack::prelude::*;
718    ///
719    /// # fn main() -> Result<(), LaError> {
720    /// let lu = Matrix::<0>::zero().lu(DEFAULT_SINGULAR_TOL)?;
721    ///
722    /// assert_eq!(lu.det()?, 1.0);
723    /// assert!(lu.solve(Vector::<0>::zero())?.into_array().is_empty());
724    /// # Ok(())
725    /// # }
726    /// ```
727    ///
728    /// The `tol` argument is a [`Tolerance`], so raw caller input must be
729    /// finite and non-negative before it can reach factorization. Use
730    /// [`Tolerance::try_new`] when accepting a raw `f64`; negative, NaN, and
731    /// infinite tolerances return
732    /// [`LaError::InvalidTolerance`].
733    ///
734    /// # Errors
735    /// Returns [`LaError::Singular`] if, for some column `k`, the largest-magnitude candidate pivot
736    /// in that column satisfies `|pivot| <= tol` (so no numerically usable pivot exists).
737    /// Returns [`LaError::NonFinite`] if an elimination intermediate overflows
738    /// to NaN/∞ before it can be stored in the returned [`Lu`].
739    #[inline]
740    pub fn lu(self, tol: Tolerance) -> Result<Lu<D>, LaError> {
741        Lu::factor_finite(self, tol)
742    }
743
744    /// Compute an LDLT factorization (`A = L D Lᵀ`) without pivoting.
745    ///
746    /// `D = 0` follows the empty-matrix convention: factorization succeeds,
747    /// [`Ldlt::det`](crate::Ldlt::det) returns `1.0`, and solving a length-zero
748    /// right-hand side returns a length-zero [`Vector`](crate::Vector).
749    ///
750    /// This is intended for exactly symmetric positive-definite matrices such
751    /// as nonsingular Gram matrices. Computed zero and tolerance-small positive
752    /// pivots are diagnosed as singular rather than returned in a usable
753    /// factorization. Because pivots are computed in binary64, success is not
754    /// an exact proof that the stored matrix is positive definite.
755    /// See `REFERENCES.md` \[4-6, 11-12\] for Cholesky/LDLT background and the
756    /// pivoted symmetric-indefinite alternative.
757    ///
758    /// # Symmetry validation
759    /// The input matrix `self` must be exactly symmetric: every mirrored pair
760    /// must satisfy `self[i][j] == self[j][i]`. IEEE-754 signed zeros compare
761    /// equal and are therefore accepted. Exact equality is a correctness
762    /// invariant, not merely a performance hint: LDLT reads only the lower
763    /// triangle, so accepting an approximate mismatch would factor a different
764    /// operator than the matrix supplied by the caller. Asymmetric inputs return
765    /// [`LaError::Asymmetric`] with an allowed absolute difference of `0.0`
766    /// before factorization starts.
767    ///
768    /// [`is_symmetric`](Self::is_symmetric) remains available as a
769    /// tolerance-based diagnostic, but `Ok(true)` from that method does not
770    /// establish this exact LDLT precondition. If you need a general-purpose
771    /// factorization for a non-symmetric matrix, use [`lu`](Self::lu) instead.
772    ///
773    /// The `tol` argument is a [`Tolerance`], so raw caller input must be
774    /// finite and non-negative before it can reach factorization. Use
775    /// [`Tolerance::try_new`] when accepting a raw `f64`; negative, NaN, and
776    /// infinite tolerances return
777    /// [`LaError::InvalidTolerance`].
778    ///
779    /// # Examples
780    /// ```
781    /// use la_stack::prelude::*;
782    ///
783    /// # fn main() -> Result<(), LaError> {
784    /// // Note the symmetric layout: a[0][1] == a[1][0] == 2.0.
785    /// let a = Matrix::<2>::try_from_rows([[4.0, 2.0], [2.0, 3.0]])?;
786    /// let ldlt = a.ldlt(DEFAULT_SINGULAR_TOL)?;
787    ///
788    /// // det(A) = 8
789    /// assert!((ldlt.det()? - 8.0).abs() <= 1e-12);
790    ///
791    /// // Solve A x = b
792    /// let b = Vector::<2>::try_new([1.0, 2.0])?;
793    /// let x = ldlt.solve(b)?.into_array();
794    /// assert!((x[0] - (-0.125)).abs() <= 1e-12);
795    /// assert!((x[1] - 0.75).abs() <= 1e-12);
796    /// # Ok(())
797    /// # }
798    /// ```
799    ///
800    /// Empty matrices use the standard empty-product convention:
801    ///
802    /// ```
803    /// use la_stack::prelude::*;
804    ///
805    /// # fn main() -> Result<(), LaError> {
806    /// let ldlt = Matrix::<0>::zero().ldlt(DEFAULT_SINGULAR_TOL)?;
807    ///
808    /// assert_eq!(ldlt.det()?, 1.0);
809    /// assert!(ldlt.solve(Vector::<0>::zero())?.into_array().is_empty());
810    /// # Ok(())
811    /// # }
812    /// ```
813    ///
814    /// # Errors
815    /// Returns [`LaError::NotPositiveSemidefinite`] if a pivot is negative or a
816    /// zero pivot retains a non-zero coupling below it.
817    /// Returns [`LaError::Singular`] if a zero pivot has no remaining coupling,
818    /// or if a positive pivot satisfies `d <= tol`, treating PSD degeneracy as
819    /// singular.
820    /// Returns [`LaError::NonFinite`] if factorization computes a non-finite
821    /// intermediate.
822    /// Returns [`LaError::Asymmetric`] if the input matrix is not symmetric.
823    #[inline]
824    pub fn ldlt(self, tol: Tolerance) -> Result<Ldlt<D>, LaError> {
825        Ldlt::factor_symmetric(SymmetricMatrix::try_new(self)?, tol)
826    }
827
828    /// Return the first non-finite stored cell in row-major order.
829    const fn first_non_finite_cell(rows: &[[f64; D]; D]) -> Option<(usize, usize)> {
830        let mut r = 0;
831        while r < D {
832            let mut c = 0;
833            while c < D {
834                if !rows[r][c].is_finite() {
835                    return Some((r, c));
836                }
837                c += 1;
838            }
839            r += 1;
840        }
841        None
842    }
843
844    /// Compute the approximate-symmetry tolerance scale for a finite matrix.
845    ///
846    /// This helper protects the public [`is_symmetric`](Self::is_symmetric) and
847    /// [`first_asymmetry`](Self::first_asymmetry) diagnostic contracts: the
848    /// documented norm-first formula is used whenever its intermediate is
849    /// representable, while an overflow-safe termwise fallback reports the
850    /// matrix cell that makes the scaled tolerance non-finite.
851    fn symmetry_epsilon(&self, rel_tol: Tolerance) -> Result<f64, LaError> {
852        let rel_tol = rel_tol.get();
853
854        if rel_tol == 0.0 {
855            return Ok(rel_tol);
856        }
857
858        if let Ok(norm) = self.inf_norm() {
859            let scale = if norm > 1.0 { norm } else { 1.0 };
860            let eps = rel_tol * scale;
861            if eps.is_finite() {
862                return Ok(eps);
863            }
864        }
865
866        // If the unscaled row sum or the final multiplication overflows, apply
867        // the tolerance to each non-negative contribution before summing. A row
868        // can overflow only at magnitudes where multiplication by the smallest
869        // positive tolerance is normal, so this fallback cannot introduce the
870        // gradual-underflow discrepancy avoided by the direct path above.
871        let mut eps = rel_tol;
872
873        for r in 0..D {
874            let mut row_eps = 0.0;
875            for c in 0..D {
876                row_eps = rel_tol.mul_add(self.rows[r][c].abs(), row_eps);
877                if !row_eps.is_finite() {
878                    cold_path();
879                    return Err(LaError::non_finite_computation_matrix(
880                        ArithmeticOperation::SymmetryCheck,
881                        r,
882                        c,
883                    ));
884                }
885            }
886            if row_eps > eps {
887                eps = row_eps;
888            }
889        }
890
891        Ok(eps)
892    }
893
894    /// Closed-form determinant for dimensions 0–4, bypassing LU factorization.
895    ///
896    /// Returns `Ok(Some(det))` for `D` ∈ {0, 1, 2, 3, 4}, `Ok(None)` for D ≥ 5.
897    /// `D = 0` returns `Ok(Some(1.0))` (empty product).
898    /// This is a `const fn` (Rust 1.94+) and uses fused multiply-add (`mul_add`)
899    /// for improved accuracy and performance.
900    ///
901    /// For a determinant that works for any dimension (falling back to LU for D ≥ 5),
902    /// use [`det`](Self::det).
903    ///
904    /// # Examples
905    /// ```
906    /// use la_stack::prelude::*;
907    ///
908    /// # fn main() -> Result<(), LaError> {
909    /// let m = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
910    /// assert_eq!(m.det_direct()?, Some(-2.0));
911    ///
912    /// // D = 0 is the empty product.
913    /// assert_eq!(Matrix::<0>::zero().det_direct()?, Some(1.0));
914    ///
915    /// // D ≥ 5 returns None.
916    /// assert!(Matrix::<5>::identity().det_direct()?.is_none());
917    /// # Ok(())
918    /// # }
919    /// ```
920    ///
921    /// # Errors
922    /// Returns [`LaError::NonFinite`] when the closed-form determinant overflows
923    /// to NaN or infinity.
924    #[inline]
925    pub const fn det_direct(&self) -> Result<Option<f64>, LaError> {
926        let Some(det) = self.det_direct_arithmetic::<false>() else {
927            cold_path();
928            return Ok(None);
929        };
930
931        Self::computed_scalar_result(ArithmeticOperation::Determinant, det.value)
932    }
933
934    /// Evaluate the closed-form determinant while certifying every rounded
935    /// operation against gradual underflow.
936    #[expect(
937        clippy::inline_always,
938        reason = "det_direct callers must eliminate unused filter-safety bookkeeping"
939    )]
940    #[inline(always)]
941    const fn det_direct_arithmetic<const TRACK_UNDERFLOW: bool>(
942        &self,
943    ) -> Option<FilterArithmetic<TRACK_UNDERFLOW>> {
944        match D {
945            0 => Some(FilterArithmetic {
946                value: 1.0,
947                underflow_safe: true,
948            }),
949            1 => Some(FilterArithmetic {
950                value: self.rows[0][0],
951                underflow_safe: true,
952            }),
953            2 => {
954                let a = self.rows[0][0];
955                let b = self.rows[0][1];
956                let c = self.rows[1][0];
957                let d = self.rows[1][1];
958                let subtrahend = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(b, c);
959                let mut det = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(a, d, -subtrahend.value);
960                det.underflow_safe &= subtrahend.underflow_safe;
961                Some(det)
962            }
963            3 => Some(Self::det3_elements::<TRACK_UNDERFLOW>(
964                [self.rows[0][0], self.rows[0][1], self.rows[0][2]],
965                [self.rows[1][0], self.rows[1][1], self.rows[1][2]],
966                [self.rows[2][0], self.rows[2][1], self.rows[2][2]],
967            )),
968            4 => {
969                if !TRACK_UNDERFLOW && let Some(input) = Det4SharedMinorInput::try_new(self) {
970                    return Some(FilterArithmetic {
971                        value: Self::det4_dense_elements(input),
972                        underflow_safe: true,
973                    });
974                }
975
976                let r = &self.rows;
977                let mut det = if r[0][3] == 0.0 {
978                    FilterArithmetic {
979                        value: 0.0,
980                        underflow_safe: true,
981                    }
982                } else {
983                    let c03 = Self::det3_elements::<TRACK_UNDERFLOW>(
984                        [r[1][0], r[1][1], r[1][2]],
985                        [r[2][0], r[2][1], r[2][2]],
986                        [r[3][0], r[3][1], r[3][2]],
987                    );
988                    let mut term =
989                        FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r[0][3], c03.value);
990                    term.value = -term.value;
991                    term.underflow_safe &= c03.underflow_safe;
992                    term
993                };
994                if r[0][2] != 0.0 {
995                    let c02 = Self::det3_elements::<TRACK_UNDERFLOW>(
996                        [r[1][0], r[1][1], r[1][3]],
997                        [r[2][0], r[2][1], r[2][3]],
998                        [r[3][0], r[3][1], r[3][3]],
999                    );
1000                    let prior_safe = det.underflow_safe && c02.underflow_safe;
1001                    det =
1002                        FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r[0][2], c02.value, det.value);
1003                    det.underflow_safe &= prior_safe;
1004                }
1005                if r[0][1] != 0.0 {
1006                    let c01 = Self::det3_elements::<TRACK_UNDERFLOW>(
1007                        [r[1][0], r[1][2], r[1][3]],
1008                        [r[2][0], r[2][2], r[2][3]],
1009                        [r[3][0], r[3][2], r[3][3]],
1010                    );
1011                    let prior_safe = det.underflow_safe && c01.underflow_safe;
1012                    det = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1013                        -r[0][1], c01.value, det.value,
1014                    );
1015                    det.underflow_safe &= prior_safe;
1016                }
1017                if r[0][0] != 0.0 {
1018                    let c00 = Self::det3_elements::<TRACK_UNDERFLOW>(
1019                        [r[1][1], r[1][2], r[1][3]],
1020                        [r[2][1], r[2][2], r[2][3]],
1021                        [r[3][1], r[3][2], r[3][3]],
1022                    );
1023                    let prior_safe = det.underflow_safe && c00.underflow_safe;
1024                    det =
1025                        FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r[0][0], c00.value, det.value);
1026                    det.underflow_safe &= prior_safe;
1027                }
1028
1029                Some(det)
1030            }
1031            _ => None,
1032        }
1033    }
1034
1035    /// Evaluate the proof-bearing 4×4 cofactor expansion with shared 2×2 minors.
1036    ///
1037    /// When no intermediate undergoes gradual underflow, the rounding error is
1038    /// bounded by `ERR_COEFF_4 · p(|A|)`, where `p(|A|)` is the absolute Leibniz
1039    /// sum. This helper returns only the determinant; use
1040    /// [`Self::det_errbound`] or [`Self::det_direct_with_errbound`] to obtain the
1041    /// certified bound.
1042    #[expect(
1043        clippy::inline_always,
1044        reason = "the D=4 determinant hot path must inline its shared-minor expansion"
1045    )]
1046    #[inline(always)]
1047    const fn det4_dense_elements(input: Det4SharedMinorInput<'_, D>) -> f64 {
1048        let r = &input.matrix.rows;
1049        let s23 = r[2][2].mul_add(r[3][3], -(r[2][3] * r[3][2]));
1050        let s13 = r[2][1].mul_add(r[3][3], -(r[2][3] * r[3][1]));
1051        let s12 = r[2][1].mul_add(r[3][2], -(r[2][2] * r[3][1]));
1052        let s03 = r[2][0].mul_add(r[3][3], -(r[2][3] * r[3][0]));
1053        let s02 = r[2][0].mul_add(r[3][2], -(r[2][2] * r[3][0]));
1054        let s01 = r[2][0].mul_add(r[3][1], -(r[2][1] * r[3][0]));
1055
1056        let c00 = r[1][1].mul_add(s23, (-r[1][2]).mul_add(s13, r[1][3] * s12));
1057        let c01 = r[1][0].mul_add(s23, (-r[1][2]).mul_add(s03, r[1][3] * s02));
1058        let c02 = r[1][0].mul_add(s13, (-r[1][1]).mul_add(s03, r[1][3] * s01));
1059        let c03 = r[1][0].mul_add(s12, (-r[1][1]).mul_add(s02, r[1][2] * s01));
1060
1061        r[0][0].mul_add(
1062            c00,
1063            (-r[0][1]).mul_add(c01, r[0][2].mul_add(c02, -(r[0][3] * c03))),
1064        )
1065    }
1066
1067    /// Evaluate the dense 4×4 absolute permanent with shared 2×2 minors.
1068    ///
1069    /// The proof carried by `input` makes every shared minor part of an active
1070    /// Leibniz term. The caller separately establishes a wide exponent margin,
1071    /// so this branch-free kernel cannot hide gradual underflow or evaluate an
1072    /// overflowing minor for a mathematically absent term.
1073    #[expect(
1074        clippy::inline_always,
1075        reason = "the D=4 determinant filter must inline its shared-minor permanent"
1076    )]
1077    #[inline(always)]
1078    const fn det4_dense_abs_permanent_elements(input: Det4SharedMinorInput<'_, D>) -> f64 {
1079        let r = &input.matrix.rows;
1080        let sp23 = (r[2][2] * r[3][3]).abs() + (r[2][3] * r[3][2]).abs();
1081        let sp13 = (r[2][1] * r[3][3]).abs() + (r[2][3] * r[3][1]).abs();
1082        let sp12 = (r[2][1] * r[3][2]).abs() + (r[2][2] * r[3][1]).abs();
1083        let sp03 = (r[2][0] * r[3][3]).abs() + (r[2][3] * r[3][0]).abs();
1084        let sp02 = (r[2][0] * r[3][2]).abs() + (r[2][2] * r[3][0]).abs();
1085        let sp01 = (r[2][0] * r[3][1]).abs() + (r[2][1] * r[3][0]).abs();
1086
1087        let pc0 = r[1][3]
1088            .abs()
1089            .mul_add(sp12, r[1][2].abs().mul_add(sp13, r[1][1].abs() * sp23));
1090        let pc1 = r[1][3]
1091            .abs()
1092            .mul_add(sp02, r[1][2].abs().mul_add(sp03, r[1][0].abs() * sp23));
1093        let pc2 = r[1][3]
1094            .abs()
1095            .mul_add(sp01, r[1][1].abs().mul_add(sp03, r[1][0].abs() * sp13));
1096        let pc3 = r[1][2]
1097            .abs()
1098            .mul_add(sp01, r[1][1].abs().mul_add(sp02, r[1][0].abs() * sp12));
1099
1100        r[0][3].abs().mul_add(
1101            pc3,
1102            r[0][2]
1103                .abs()
1104                .mul_add(pc2, r[0][1].abs().mul_add(pc1, r[0][0].abs() * pc0)),
1105        )
1106    }
1107
1108    /// Floating-point determinant, using closed-form formulas for D ≤ 4 and
1109    /// LU decomposition for D ≥ 5.
1110    ///
1111    /// For D ∈ {1, 2, 3, 4}, this bypasses LU factorization entirely for a significant
1112    /// speedup (see [`det_direct`](Self::det_direct)).
1113    ///
1114    /// Because this method mixes closed-form paths from
1115    /// [`det_direct`](Self::det_direct) with an LU fallback, the returned value has
1116    /// no certified absolute error bound. Use
1117    /// [`det_errbound`](Self::det_errbound) for D ≤ 4 bounds, or the exact
1118    /// determinant APIs when exact singularity classification or certified values
1119    /// matter. For D ≥ 5, the zero-tolerance LU fallback surfaces
1120    /// [`LaError::Singular`] when elimination cannot produce a non-zero pivot.
1121    /// Floating-point elimination cannot in general distinguish an exactly
1122    /// singular matrix from a non-singular matrix whose intermediate pivot
1123    /// rounded to zero, so this method never converts that numerical failure into
1124    /// an exact `0.0` result.
1125    ///
1126    /// # Examples
1127    /// ```
1128    /// use la_stack::prelude::*;
1129    ///
1130    /// # fn main() -> Result<(), LaError> {
1131    /// let det = Matrix::<3>::identity().det()?;
1132    /// assert!((det - 1.0).abs() <= 1e-12);
1133    /// # Ok(())
1134    /// # }
1135    /// ```
1136    ///
1137    /// The LU fallback accumulates its diagonal product with power-of-two
1138    /// scaling, so factor order cannot cause premature overflow or underflow in
1139    /// the final product. Elimination intermediates remain subject to binary64
1140    /// rounding and range limits.
1141    ///
1142    /// # Errors
1143    /// Returns [`LaError::Singular`] if the D ≥ 5 LU fallback cannot produce a
1144    /// non-zero pivot, including when a non-zero mathematical intermediate rounds
1145    /// to zero during elimination. Returns [`LaError::NonFinite`] if a D ≤ 4
1146    /// closed-form result is non-finite, if the LU fallback computes a
1147    /// non-finite factorization cell, or if its final scaled determinant cannot
1148    /// be represented as a finite `f64`.
1149    #[inline]
1150    pub fn det(self) -> Result<f64, LaError> {
1151        if let Some(d) = self.det_direct()? {
1152            return Ok(d);
1153        }
1154        self.lu(Tolerance::ZERO)?.det()
1155    }
1156
1157    /// Evaluate `det_direct()` and its absolute error bound together.
1158    ///
1159    /// Returns `Ok(Some(result))` for D ≤ 4 when the relative-error analysis
1160    /// is valid. The result contains the closed-form determinant and a bound
1161    /// such that `|result.determinant() - det_exact| ≤
1162    /// result.absolute_error_bound()`. Returns `Ok(None)` when gradual
1163    /// underflow could invalidate that analysis or for D ≥ 5, where no
1164    /// closed-form bound is available.
1165    ///
1166    /// This is the preferred API when both values are needed: it evaluates the
1167    /// determinant arithmetic tree once, then computes the matching bound for
1168    /// the same matrix within that call.
1169    ///
1170    /// # Examples
1171    /// ```
1172    /// use la_stack::prelude::*;
1173    ///
1174    /// # fn main() -> Result<(), LaError> {
1175    /// let matrix = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]])?;
1176    /// if let Some(estimate) = matrix.det_direct_with_errbound()? {
1177    ///     assert_eq!(estimate.determinant(), -2.0);
1178    ///     assert!(estimate.absolute_error_bound() >= 0.0);
1179    /// }
1180    /// # Ok(())
1181    /// # }
1182    /// ```
1183    ///
1184    /// # Errors
1185    /// Returns [`LaError::NonFinite`] when the determinant or bound computation
1186    /// overflows to NaN or infinity. Underflow-sensitive finite computations
1187    /// return `Ok(None)` because they remain valid inputs for an exact fallback.
1188    #[inline]
1189    pub const fn det_direct_with_errbound(
1190        &self,
1191    ) -> Result<Option<DeterminantWithErrorBound>, LaError> {
1192        if self.det_bound_inputs_have_wide_exponent_margin() {
1193            let Some(det) = self.det_direct_arithmetic::<false>() else {
1194                cold_path();
1195                return Ok(None);
1196            };
1197            return self.det_direct_with_errbound_from_arithmetic(det);
1198        }
1199
1200        let Some(det) = self.det_direct_arithmetic::<true>() else {
1201            cold_path();
1202            return Ok(None);
1203        };
1204        self.det_direct_with_errbound_from_arithmetic(det)
1205    }
1206
1207    /// Conservative absolute error bound for `det_direct()`.
1208    ///
1209    /// Returns `Ok(Some(bound))` such that `|det_direct() - det_exact| ≤ bound`
1210    /// when every rounded intermediate used by the closed-form determinant and
1211    /// bound is normal (or an exact structural zero). Returns `Ok(None)` when
1212    /// gradual underflow could invalidate the relative-error analysis, or for
1213    /// D ≥ 5 where no fast bound is available.
1214    ///
1215    /// For D ≤ 4, the bound is derived from the absolute Leibniz sum using
1216    /// Shewchuk-style error analysis (see `REFERENCES.md` \[8\] and the
1217    /// per-constant docs on [`ERR_COEFF_2`], [`ERR_COEFF_3`], and
1218    /// [`ERR_COEFF_4`]). For D = 0 or 1, returns
1219    /// `Some(0.0)` since the determinant computation is exact (no
1220    /// arithmetic).
1221    ///
1222    /// This method does NOT require the `exact` feature — the bounds use
1223    /// pure f64 arithmetic and are useful for custom adaptive-precision logic.
1224    ///
1225    /// # When to use
1226    ///
1227    /// Use [`det_direct_with_errbound`](Self::det_direct_with_errbound) when the
1228    /// determinant and bound are both needed. This accessor is convenient when
1229    /// only the bound is needed.
1230    ///
1231    /// # Examples
1232    /// ```
1233    /// use la_stack::prelude::*;
1234    ///
1235    /// # fn main() -> Result<(), LaError> {
1236    /// let m = Matrix::<3>::try_from_rows([
1237    ///     [1.0, 2.0, 3.0],
1238    ///     [4.0, 5.0, 6.0],
1239    ///     [7.0, 8.0, 9.0],
1240    /// ])?;
1241    /// if let Some(bound) = m.det_errbound()? {
1242    ///     assert!(bound >= 0.0);
1243    /// }
1244    /// # Ok(())
1245    /// # }
1246    /// ```
1247    ///
1248    /// # Adaptive precision pattern (requires `exact` feature)
1249    /// ```ignore
1250    /// use la_stack::prelude::*;
1251    ///
1252    /// fn adaptive_det_sign<const D: usize>(
1253    ///     matrix: &Matrix<D>,
1254    /// ) -> DeterminantSign {
1255    ///     if let Ok(Some(estimate)) = matrix.det_direct_with_errbound() {
1256    ///         if estimate.determinant().abs() > estimate.absolute_error_bound() {
1257    ///             return if estimate.determinant() > 0.0 {
1258    ///                 DeterminantSign::Positive
1259    ///             } else {
1260    ///                 DeterminantSign::Negative
1261    ///             };
1262    ///         }
1263    ///     }
1264    ///
1265    ///     matrix.det_sign_exact()
1266    /// }
1267    ///
1268    /// fn main() -> Result<(), LaError> {
1269    ///     assert_eq!(
1270    ///         adaptive_det_sign(&Matrix::<3>::identity()),
1271    ///         DeterminantSign::Positive
1272    ///     );
1273    ///
1274    ///     let big = f64::MAX / 2.0;
1275    ///     let overflowing = Matrix::<3>::try_from_rows([
1276    ///         [0.0, 0.0, 1.0],
1277    ///         [big, 0.0, 1.0],
1278    ///         [0.0, big, 1.0],
1279    ///     ])?;
1280    ///     assert_eq!(
1281    ///         adaptive_det_sign(&overflowing),
1282    ///         DeterminantSign::Positive
1283    ///     );
1284    ///     Ok(())
1285    /// }
1286    /// ```
1287    ///
1288    /// # Errors
1289    /// Returns [`LaError::NonFinite`] when the bound computation overflows to
1290    /// NaN or infinity. Underflow-sensitive finite computations return
1291    /// `Ok(None)` instead because they are valid inputs for an exact fallback.
1292    #[inline]
1293    pub const fn det_errbound(&self) -> Result<Option<f64>, LaError> {
1294        match self.det_direct_with_errbound() {
1295            Ok(Some(result)) => Ok(Some(result.absolute_error_bound)),
1296            Ok(None) => Ok(None),
1297            Err(error) => Err(error),
1298        }
1299    }
1300
1301    /// Return whether every non-zero entry is large enough that the complete
1302    /// D≤4 determinant and permanent trees cannot gradually underflow.
1303    ///
1304    /// The `2^-16` threshold leaves hundreds of binary exponent bits of margin
1305    /// even after the D=4 tree's products, FMAs, and binary64 rounding steps.
1306    /// Overflow remains possible and is classified after evaluation. Inputs
1307    /// below this conservative threshold use per-operation tracking instead.
1308    const fn det_bound_inputs_have_wide_exponent_margin(&self) -> bool {
1309        const MIN_MAGNITUDE_BITS: u64 = 1007_u64 << 52; // 2^-16
1310        const MAGNITUDE_MASK: u64 = !(1_u64 << 63);
1311
1312        if D > 4 {
1313            return false;
1314        }
1315
1316        let mut row = 0;
1317        while row < D {
1318            let mut col = 0;
1319            while col < D {
1320                let magnitude_bits = self.rows[row][col].to_bits() & MAGNITUDE_MASK;
1321                if magnitude_bits != 0 && magnitude_bits < MIN_MAGNITUDE_BITS {
1322                    return false;
1323                }
1324                col += 1;
1325            }
1326            row += 1;
1327        }
1328        true
1329    }
1330
1331    /// Classify a completed determinant tree and construct its matching bound.
1332    const fn det_direct_with_errbound_from_arithmetic<const TRACK_UNDERFLOW: bool>(
1333        &self,
1334        det: FilterArithmetic<TRACK_UNDERFLOW>,
1335    ) -> Result<Option<DeterminantWithErrorBound>, LaError> {
1336        let bound = match self.det_errbound_from_arithmetic(det) {
1337            Ok(Some(bound)) => bound,
1338            Ok(None) => {
1339                if !det.value.is_finite() {
1340                    cold_path();
1341                    return Err(LaError::non_finite_computation_scalar(
1342                        ArithmeticOperation::Determinant,
1343                    ));
1344                }
1345                return Ok(None);
1346            }
1347            Err(error) => return Err(error),
1348        };
1349        if !det.value.is_finite() {
1350            cold_path();
1351            return Err(LaError::non_finite_computation_scalar(
1352                ArithmeticOperation::Determinant,
1353            ));
1354        }
1355        Ok(Some(DeterminantWithErrorBound {
1356            determinant: det.value,
1357            absolute_error_bound: bound,
1358        }))
1359    }
1360
1361    /// Compute a bound after the matching determinant tree has been evaluated.
1362    const fn det_errbound_from_arithmetic<const TRACK_UNDERFLOW: bool>(
1363        &self,
1364        det: FilterArithmetic<TRACK_UNDERFLOW>,
1365    ) -> Result<Option<f64>, LaError> {
1366        if !det.underflow_safe {
1367            cold_path();
1368            return Ok(None);
1369        }
1370
1371        match D {
1372            0 | 1 => Self::computed_scalar_result(ArithmeticOperation::DeterminantErrorBound, 0.0),
1373            2 => {
1374                let r = &self.rows;
1375                let product_0 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r[0][0], r[1][1]);
1376                let product_1 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r[0][1], r[1][0]);
1377                let mut permanent = FilterArithmetic::<TRACK_UNDERFLOW>::add_non_negative(
1378                    product_0.value.abs(),
1379                    product_1.value.abs(),
1380                );
1381                permanent.underflow_safe &= product_0.underflow_safe && product_1.underflow_safe;
1382                Self::certified_error_bound(ERR_COEFF_2, permanent)
1383            }
1384            3 => {
1385                let r = &self.rows;
1386                let permanent = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1387                    [r[0][0], r[0][1], r[0][2]],
1388                    [r[1][0], r[1][1], r[1][2]],
1389                    [r[2][0], r[2][1], r[2][2]],
1390                );
1391                Self::certified_error_bound(ERR_COEFF_3, permanent)
1392            }
1393            4 => self.det4_errbound::<TRACK_UNDERFLOW>(),
1394            _ => {
1395                cold_path();
1396                Ok(None)
1397            }
1398        }
1399    }
1400
1401    /// Compute the D=4 determinant error bound after the dimension dispatch.
1402    const fn det4_errbound<const TRACK_UNDERFLOW: bool>(&self) -> Result<Option<f64>, LaError> {
1403        if !TRACK_UNDERFLOW && let Some(input) = Det4SharedMinorInput::try_new(self) {
1404            return Self::certified_error_bound(
1405                ERR_COEFF_4,
1406                FilterArithmetic::<TRACK_UNDERFLOW> {
1407                    value: Self::det4_dense_abs_permanent_elements(input),
1408                    underflow_safe: true,
1409                },
1410            );
1411        }
1412
1413        let r = &self.rows;
1414        let mut permanent = if r[0][3] == 0.0 {
1415            FilterArithmetic {
1416                value: 0.0,
1417                underflow_safe: true,
1418            }
1419        } else {
1420            let pc3 = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1421                [r[1][0], r[1][1], r[1][2]],
1422                [r[2][0], r[2][1], r[2][2]],
1423                [r[3][0], r[3][1], r[3][2]],
1424            );
1425            let mut term = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r[0][3].abs(), pc3.value);
1426            term.underflow_safe &= pc3.underflow_safe;
1427            term
1428        };
1429        if r[0][2] != 0.0 {
1430            let pc2 = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1431                [r[1][0], r[1][1], r[1][3]],
1432                [r[2][0], r[2][1], r[2][3]],
1433                [r[3][0], r[3][1], r[3][3]],
1434            );
1435            let prior_safe = permanent.underflow_safe && pc2.underflow_safe;
1436            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1437                r[0][2].abs(),
1438                pc2.value,
1439                permanent.value,
1440            );
1441            permanent.underflow_safe &= prior_safe;
1442        }
1443        if r[0][1] != 0.0 {
1444            let pc1 = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1445                [r[1][0], r[1][2], r[1][3]],
1446                [r[2][0], r[2][2], r[2][3]],
1447                [r[3][0], r[3][2], r[3][3]],
1448            );
1449            let prior_safe = permanent.underflow_safe && pc1.underflow_safe;
1450            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1451                r[0][1].abs(),
1452                pc1.value,
1453                permanent.value,
1454            );
1455            permanent.underflow_safe &= prior_safe;
1456        }
1457        if r[0][0] != 0.0 {
1458            let pc0 = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1459                [r[1][1], r[1][2], r[1][3]],
1460                [r[2][1], r[2][2], r[2][3]],
1461                [r[3][1], r[3][2], r[3][3]],
1462            );
1463            let prior_safe = permanent.underflow_safe && pc0.underflow_safe;
1464            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1465                r[0][0].abs(),
1466                pc0.value,
1467                permanent.value,
1468            );
1469            permanent.underflow_safe &= prior_safe;
1470        }
1471        Self::certified_error_bound(ERR_COEFF_4, permanent)
1472    }
1473
1474    /// Evaluate a 3×3 determinant expansion with a guarded sparse fallback.
1475    ///
1476    /// When all three first-row coefficients are non-zero, one branch-free
1477    /// closed form is used. The sparse fallback protects the public
1478    /// [`det_direct`](Self::det_direct) contract: a mathematically absent term
1479    /// must not compute an overflowing minor and poison the determinant with
1480    /// `0.0 * inf == NaN`. Nonzero terms keep the same fused multiply-add
1481    /// ordering as the closed-form expansion.
1482    #[expect(
1483        clippy::inline_always,
1484        reason = "det_direct callers must eliminate unused filter-safety bookkeeping"
1485    )]
1486    #[inline(always)]
1487    const fn det3_elements<const TRACK_UNDERFLOW: bool>(
1488        r0: [f64; 3],
1489        r1: [f64; 3],
1490        r2: [f64; 3],
1491    ) -> FilterArithmetic<TRACK_UNDERFLOW> {
1492        let dense = (r0[0] != 0.0) && (r0[1] != 0.0) && (r0[2] != 0.0);
1493        if !TRACK_UNDERFLOW && dense {
1494            let m00 = r1[1].mul_add(r2[2], -(r1[2] * r2[1]));
1495            let m01 = r1[0].mul_add(r2[2], -(r1[2] * r2[0]));
1496            let m02 = r1[0].mul_add(r2[1], -(r1[1] * r2[0]));
1497            return FilterArithmetic {
1498                value: r0[0].mul_add(m00, (-r0[1]).mul_add(m01, r0[2] * m02)),
1499                underflow_safe: true,
1500            };
1501        }
1502
1503        let mut det = if r0[2] == 0.0 {
1504            FilterArithmetic {
1505                value: 0.0,
1506                underflow_safe: true,
1507            }
1508        } else {
1509            let subtrahend = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[1], r2[0]);
1510            let mut m02 =
1511                FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r1[0], r2[1], -subtrahend.value);
1512            m02.underflow_safe &= subtrahend.underflow_safe;
1513            let mut term = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r0[2], m02.value);
1514            term.underflow_safe &= m02.underflow_safe;
1515            term
1516        };
1517        if r0[1] != 0.0 {
1518            let subtrahend = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[2], r2[0]);
1519            let mut m01 =
1520                FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r1[0], r2[2], -subtrahend.value);
1521            m01.underflow_safe &= subtrahend.underflow_safe;
1522            let prior_safe = det.underflow_safe && m01.underflow_safe;
1523            det = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(-r0[1], m01.value, det.value);
1524            det.underflow_safe &= prior_safe;
1525        }
1526        if r0[0] != 0.0 {
1527            let subtrahend = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[2], r2[1]);
1528            let mut m00 =
1529                FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r1[1], r2[2], -subtrahend.value);
1530            m00.underflow_safe &= subtrahend.underflow_safe;
1531            let prior_safe = det.underflow_safe && m00.underflow_safe;
1532            det = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r0[0], m00.value, det.value);
1533            det.underflow_safe &= prior_safe;
1534        }
1535        det
1536    }
1537
1538    /// Evaluate a 3×3 absolute permanent while skipping zero coefficients.
1539    ///
1540    /// This mirrors [`det3_elements`](Self::det3_elements) for error-bound
1541    /// computation: absent determinant terms should not force evaluation of an
1542    /// overflowing absolute minor.
1543    #[expect(
1544        clippy::inline_always,
1545        reason = "error-bound call-site specialization avoids tracked-helper overhead"
1546    )]
1547    #[inline(always)]
1548    const fn det3_abs_permanent_elements<const TRACK_UNDERFLOW: bool>(
1549        r0: [f64; 3],
1550        r1: [f64; 3],
1551        r2: [f64; 3],
1552    ) -> FilterArithmetic<TRACK_UNDERFLOW> {
1553        let dense = (r0[0] != 0.0) && (r0[1] != 0.0) && (r0[2] != 0.0);
1554        if !TRACK_UNDERFLOW && dense {
1555            let pm00 = (r1[1] * r2[2]).abs() + (r1[2] * r2[1]).abs();
1556            let pm01 = (r1[0] * r2[2]).abs() + (r1[2] * r2[0]).abs();
1557            let pm02 = (r1[0] * r2[1]).abs() + (r1[1] * r2[0]).abs();
1558            return FilterArithmetic {
1559                value: r0[2]
1560                    .abs()
1561                    .mul_add(pm02, r0[1].abs().mul_add(pm01, r0[0].abs() * pm00)),
1562                underflow_safe: true,
1563            };
1564        }
1565
1566        let mut permanent = if r0[2] == 0.0 {
1567            FilterArithmetic {
1568                value: 0.0,
1569                underflow_safe: true,
1570            }
1571        } else {
1572            let product_0 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[0], r2[1]);
1573            let product_1 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[1], r2[0]);
1574            let mut pm02 = FilterArithmetic::<TRACK_UNDERFLOW>::add_non_negative(
1575                product_0.value.abs(),
1576                product_1.value.abs(),
1577            );
1578            pm02.underflow_safe &= product_0.underflow_safe && product_1.underflow_safe;
1579            let mut term = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r0[2].abs(), pm02.value);
1580            term.underflow_safe &= pm02.underflow_safe;
1581            term
1582        };
1583        if r0[1] != 0.0 {
1584            let product_0 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[0], r2[2]);
1585            let product_1 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[2], r2[0]);
1586            let mut pm01 = FilterArithmetic::<TRACK_UNDERFLOW>::add_non_negative(
1587                product_0.value.abs(),
1588                product_1.value.abs(),
1589            );
1590            pm01.underflow_safe &= product_0.underflow_safe && product_1.underflow_safe;
1591            let prior_safe = permanent.underflow_safe && pm01.underflow_safe;
1592            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1593                r0[1].abs(),
1594                pm01.value,
1595                permanent.value,
1596            );
1597            permanent.underflow_safe &= prior_safe;
1598        }
1599        if r0[0] != 0.0 {
1600            let product_0 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[1], r2[2]);
1601            let product_1 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[2], r2[1]);
1602            let mut pm00 = FilterArithmetic::<TRACK_UNDERFLOW>::add_non_negative(
1603                product_0.value.abs(),
1604                product_1.value.abs(),
1605            );
1606            pm00.underflow_safe &= product_0.underflow_safe && product_1.underflow_safe;
1607            let prior_safe = permanent.underflow_safe && pm00.underflow_safe;
1608            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1609                r0[0].abs(),
1610                pm00.value,
1611                permanent.value,
1612            );
1613            permanent.underflow_safe &= prior_safe;
1614        }
1615        permanent
1616    }
1617
1618    /// Finish a determinant error bound only when its full arithmetic tree is
1619    /// outside the gradual-underflow regime.
1620    const fn certified_error_bound<const TRACK_UNDERFLOW: bool>(
1621        coefficient: f64,
1622        permanent: FilterArithmetic<TRACK_UNDERFLOW>,
1623    ) -> Result<Option<f64>, LaError> {
1624        let mut bound = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(coefficient, permanent.value);
1625        bound.underflow_safe &= permanent.underflow_safe;
1626        if bound.underflow_safe {
1627            Self::computed_scalar_result(ArithmeticOperation::DeterminantErrorBound, bound.value)
1628        } else {
1629            cold_path();
1630            Ok(None)
1631        }
1632    }
1633
1634    /// Return a computed scalar result for a matrix with finite stored entries.
1635    const fn computed_scalar_result(
1636        operation: ArithmeticOperation,
1637        value: f64,
1638    ) -> Result<Option<f64>, LaError> {
1639        if value.is_finite() {
1640            Ok(Some(value))
1641        } else {
1642            Err(LaError::non_finite_computation_scalar(operation))
1643        }
1644    }
1645}
1646
1647impl<const D: usize> Default for Matrix<D> {
1648    #[inline]
1649    fn default() -> Self {
1650        Self::zero()
1651    }
1652}
1653
1654#[cfg(all(doc, feature = "exact"))]
1655mod det_errbound_doctests {
1656    /// ```rust
1657    /// use la_stack::prelude::*;
1658    ///
1659    /// fn adaptive_det_sign<const D: usize>(
1660    ///     matrix: &Matrix<D>,
1661    /// ) -> DeterminantSign {
1662    ///     if let Ok(Some(estimate)) = matrix.det_direct_with_errbound() {
1663    ///         if estimate.determinant().abs() > estimate.absolute_error_bound() {
1664    ///             return if estimate.determinant() > 0.0 {
1665    ///                 DeterminantSign::Positive
1666    ///             } else {
1667    ///                 DeterminantSign::Negative
1668    ///             };
1669    ///         }
1670    ///     }
1671    ///
1672    ///     matrix.det_sign_exact()
1673    /// }
1674    ///
1675    /// # fn main() -> Result<(), LaError> {
1676    /// let identity = Matrix::<3>::identity();
1677    /// assert_eq!(
1678    ///     adaptive_det_sign(&identity),
1679    ///     DeterminantSign::Positive
1680    /// );
1681    ///
1682    /// let singular = Matrix::<3>::try_from_rows([
1683    ///     [1.0, 2.0, 3.0],
1684    ///     [4.0, 5.0, 6.0],
1685    ///     [7.0, 8.0, 9.0],
1686    /// ])?;
1687    /// assert_eq!(adaptive_det_sign(&singular), DeterminantSign::Zero);
1688    ///
1689    /// let big = f64::MAX / 2.0;
1690    /// let overflowing = Matrix::<3>::try_from_rows([
1691    ///     [0.0, 0.0, 1.0],
1692    ///     [big, 0.0, 1.0],
1693    ///     [0.0, big, 1.0],
1694    /// ])?;
1695    /// assert_eq!(
1696    ///     adaptive_det_sign(&overflowing),
1697    ///     DeterminantSign::Positive
1698    /// );
1699    /// # Ok(())
1700    /// # }
1701    /// ```
1702    fn adaptive_precision_pattern() {}
1703}
1704
1705#[cfg(test)]
1706mod tests {
1707    use core::hint::black_box;
1708
1709    use approx::assert_abs_diff_eq;
1710    use pastey::paste;
1711
1712    use super::*;
1713    use crate::{DEFAULT_SINGULAR_TOL, FactorizationKind, Vector};
1714
1715    macro_rules! gen_matrix_tests {
1716        ($d:literal) => {
1717            paste! {
1718                #[test]
1719                fn [<matrix_try_from_rows_get_set_bounds_checked_ $d d>]() {
1720                    let mut rows = [[0.0f64; $d]; $d];
1721                    rows[0][0] = 1.0;
1722                    rows[$d - 1][$d - 1] = -2.0;
1723
1724                    let mut m = Matrix::<$d>::try_from_rows(rows).unwrap();
1725
1726                    assert_eq!(m.get(0, 0), Some(1.0));
1727                    assert_eq!(m.get($d - 1, $d - 1), Some(-2.0));
1728                    assert_eq!(m.try_get(0, 0), Ok(1.0));
1729                    assert_eq!(m.try_get($d - 1, $d - 1), Ok(-2.0));
1730
1731                    // Out-of-bounds is None.
1732                    assert_eq!(m.get($d, 0), None);
1733                    assert_eq!(
1734                        m.try_get($d, 0),
1735                        Err(LaError::IndexOutOfBounds {
1736                            row: $d,
1737                            col: 0,
1738                            dim: $d,
1739                        })
1740                    );
1741
1742                    // Out-of-bounds set fails.
1743                    let before_failed_set = m;
1744                    assert_eq!(
1745                        m.set($d, 0, 3.0),
1746                        Err(LaError::IndexOutOfBounds {
1747                            row: $d,
1748                            col: 0,
1749                            dim: $d,
1750                        })
1751                    );
1752                    assert_eq!(m, before_failed_set);
1753                    assert_eq!(
1754                        m.set(0, $d, 3.0),
1755                        Err(LaError::IndexOutOfBounds {
1756                            row: 0,
1757                            col: $d,
1758                            dim: $d,
1759                        })
1760                    );
1761                    assert_eq!(m, before_failed_set);
1762                    assert_eq!(m.get(0, 0), Some(1.0));
1763
1764                    // In-bounds set works.
1765                    assert_eq!(m.set(0, $d - 1, 3.0), Ok(()));
1766                    assert_eq!(m.get(0, $d - 1), Some(3.0));
1767                    assert_eq!(m.set($d - 1, 0, 4.0), Ok(()));
1768                    assert_eq!(m.try_get($d - 1, 0), Ok(4.0));
1769                }
1770
1771                #[test]
1772                fn [<matrix_set_rejects_non_finite_and_preserves_storage_ $d d>]() {
1773                    for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1774                        let mut m = Matrix::<$d>::identity();
1775                        let before = m;
1776                        assert_eq!(
1777                            m.set($d - 1, 0, value),
1778                            Err(LaError::non_finite_input_matrix($d - 1, 0))
1779                        );
1780                        assert_eq!(m, before);
1781                    }
1782                }
1783
1784                #[test]
1785                fn [<matrix_try_from_rows_rejects_non_finite_ $d d>]() {
1786                    for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1787                        let mut rows = [[0.0f64; $d]; $d];
1788                        rows[$d - 1][$d - 1] = value;
1789                        assert_eq!(
1790                            Matrix::<$d>::try_from_rows(rows),
1791                            Err(LaError::non_finite_input_matrix($d - 1, $d - 1))
1792                        );
1793                    }
1794
1795                    let mut rows = [[0.0f64; $d]; $d];
1796                    rows[0][$d - 1] = f64::INFINITY;
1797                    rows[$d - 1][0] = f64::NAN;
1798                    assert_eq!(
1799                        Matrix::<$d>::try_from_rows(rows),
1800                        Err(LaError::non_finite_input_matrix(0, $d - 1))
1801                    );
1802                }
1803
1804                #[test]
1805                fn [<matrix_zero_and_default_are_zero_ $d d>]() {
1806                    let z = Matrix::<$d>::zero();
1807                    assert_abs_diff_eq!(z.inf_norm().unwrap(), 0.0, epsilon = 0.0);
1808
1809                    let d = Matrix::<$d>::default();
1810                    assert_abs_diff_eq!(d.inf_norm().unwrap(), 0.0, epsilon = 0.0);
1811                }
1812
1813                #[test]
1814                fn [<matrix_inf_norm_max_row_sum_ $d d>]() {
1815                    let mut rows = [[0.0f64; $d]; $d];
1816
1817                    // Row 0 has a smaller absolute row sum.
1818                    for c in 0..$d {
1819                        rows[0][c] = 0.5;
1820                    }
1821
1822                    // The last row has absolute row sum = D.
1823                    for c in 0..$d {
1824                        rows[$d - 1][c] = -1.0;
1825                    }
1826
1827                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1828                    assert_abs_diff_eq!(m.inf_norm().unwrap(), f64::from($d), epsilon = 0.0);
1829                }
1830
1831                #[test]
1832                fn [<matrix_inf_norm_reports_first_overflowing_column_ $d d>]() {
1833                    let mut rows = [[0.0f64; $d]; $d];
1834                    rows[$d - 1][0] = f64::MAX;
1835                    rows[$d - 1][1] = f64::MAX;
1836
1837                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1838                    assert_eq!(
1839                        m.inf_norm(),
1840                        Err(LaError::non_finite_computation_matrix(
1841                            ArithmeticOperation::MatrixInfinityNorm,
1842                            $d - 1,
1843                            1,
1844                        ))
1845                    );
1846                }
1847
1848                #[test]
1849                fn [<matrix_inf_norm_reports_first_overflowing_row_ $d d>]() {
1850                    let mut rows = [[0.0f64; $d]; $d];
1851                    rows[0][0] = f64::MAX;
1852                    rows[0][$d - 1] = f64::MAX;
1853                    rows[$d - 1][0] = f64::MAX;
1854                    rows[$d - 1][1] = f64::MAX;
1855
1856                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1857                    assert_eq!(
1858                        m.inf_norm(),
1859                        Err(LaError::non_finite_computation_matrix(
1860                            ArithmeticOperation::MatrixInfinityNorm,
1861                            0,
1862                            $d - 1,
1863                        ))
1864                    );
1865                }
1866
1867                #[test]
1868                fn [<matrix_identity_lu_det_solve_ $d d>]() {
1869                    let m = Matrix::<$d>::identity();
1870
1871                    // Identity has ones on diag and zeros off diag.
1872                    for r in 0..$d {
1873                        for c in 0..$d {
1874                            let expected = if r == c { 1.0 } else { 0.0 };
1875                            assert_abs_diff_eq!(m.get(r, c).unwrap(), expected, epsilon = 0.0);
1876                        }
1877                    }
1878
1879                    // Determinant is 1.
1880                    let det = m.det().unwrap();
1881                    assert_abs_diff_eq!(det, 1.0, epsilon = 1e-12);
1882
1883                    // LU solve on identity returns the RHS.
1884                    let lu = m.lu(DEFAULT_SINGULAR_TOL).unwrap();
1885
1886                    let b_arr = {
1887                        let mut arr = [0.0f64; $d];
1888                        let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
1889                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
1890                            *dst = *src;
1891                        }
1892                        arr
1893                    };
1894
1895                    let b = Vector::<$d>::new(b_arr);
1896                    let x = lu.solve(b).unwrap().into_array();
1897
1898                    for (x_i, b_i) in x.iter().zip(b_arr.iter()) {
1899                        assert_abs_diff_eq!(*x_i, *b_i, epsilon = 1e-12);
1900                    }
1901                }
1902
1903            }
1904        };
1905    }
1906
1907    // Mirror delaunay-style multi-dimension tests.
1908    gen_matrix_tests!(2);
1909    gen_matrix_tests!(3);
1910    gen_matrix_tests!(4);
1911    gen_matrix_tests!(5);
1912
1913    #[test]
1914    fn matrix_inf_norm_preserves_left_to_right_row_sum_order() {
1915        let large = 9_007_199_254_740_992.0;
1916        let matrix =
1917            Matrix::<4>::try_from_rows([[large, 1.0, 1.0, 1.0], [0.0; 4], [0.0; 4], [0.0; 4]])
1918                .unwrap();
1919
1920        assert_eq!(matrix.inf_norm(), Ok(large));
1921    }
1922
1923    // === det_direct tests ===
1924
1925    #[test]
1926    fn det_direct_d0_is_one() {
1927        assert_eq!(Matrix::<0>::zero().det_direct(), Ok(Some(1.0)));
1928    }
1929
1930    #[test]
1931    fn det_direct_d1_returns_element() {
1932        let m = Matrix::<1>::try_from_rows([[42.0]]).unwrap();
1933        assert_eq!(m.det_direct(), Ok(Some(42.0)));
1934    }
1935
1936    #[test]
1937    fn det_direct_d2_known_value() {
1938        // [[1,2],[3,4]] → det = 1*4 - 2*3 = -2
1939        // black_box prevents compile-time constant folding of the const fn.
1940        let m = black_box(Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap());
1941        assert_abs_diff_eq!(m.det_direct().unwrap().unwrap(), -2.0, epsilon = 1e-15);
1942    }
1943
1944    #[test]
1945    fn det_direct_d3_known_value() {
1946        // Classic 3×3: det = 0
1947        let m = black_box(
1948            Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
1949                .unwrap(),
1950        );
1951        assert_abs_diff_eq!(m.det_direct().unwrap().unwrap(), 0.0, epsilon = 1e-12);
1952    }
1953
1954    #[test]
1955    fn det_direct_d3_dense_known_value() {
1956        // det = 1*(5*8 - 7*6) - 2*(4*8 - 7*2) + 3*(4*6 - 5*2) = 4
1957        let m = black_box(
1958            Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 7.0], [2.0, 6.0, 8.0]])
1959                .unwrap(),
1960        );
1961        let direct = m.det_direct().unwrap().unwrap();
1962        let paired = m.det_direct_with_errbound().unwrap().unwrap();
1963
1964        assert_abs_diff_eq!(direct, 4.0, epsilon = 1e-12);
1965        assert_eq!(paired.determinant().to_bits(), direct.to_bits());
1966    }
1967
1968    #[test]
1969    fn det_direct_d3_dense_reports_legitimate_overflow() {
1970        // The unscaled matrix has determinant 54, so scaling every entry by
1971        // 1.6e102 gives a determinant of approximately 2.21e308.
1972        let scale = 1.6e102;
1973        let m = black_box(
1974            Matrix::<3>::try_from_rows([
1975                [4.0 * scale, scale, scale],
1976                [scale, 4.0 * scale, scale],
1977                [scale, scale, 4.0 * scale],
1978            ])
1979            .unwrap(),
1980        );
1981        let expected = LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant);
1982
1983        assert_eq!(m.det_direct(), Err(expected));
1984        assert_eq!(m.det(), Err(expected));
1985    }
1986
1987    #[test]
1988    fn det_errbound_d3_dense_reports_legitimate_overflow() {
1989        // The unscaled matrix has determinant 54, so scaling every entry by
1990        // 1.6e102 gives a determinant of approximately 2.21e308.
1991        let scale = 1.6e102;
1992        let m = black_box(
1993            Matrix::<3>::try_from_rows([
1994                [4.0 * scale, scale, scale],
1995                [scale, 4.0 * scale, scale],
1996                [scale, scale, 4.0 * scale],
1997            ])
1998            .unwrap(),
1999        );
2000        let expected =
2001            LaError::non_finite_computation_scalar(ArithmeticOperation::DeterminantErrorBound);
2002
2003        assert_eq!(m.det_errbound(), Err(expected));
2004        assert_eq!(m.det_direct_with_errbound(), Err(expected));
2005    }
2006
2007    #[test]
2008    fn det_direct_d3_nonsingular() {
2009        // [[2,1,0],[0,3,1],[1,0,2]] → det = 2*(6-0) - 1*(0-1) + 0 = 13
2010        let m = black_box(
2011            Matrix::<3>::try_from_rows([[2.0, 1.0, 0.0], [0.0, 3.0, 1.0], [1.0, 0.0, 2.0]])
2012                .unwrap(),
2013        );
2014        assert_abs_diff_eq!(m.det_direct().unwrap().unwrap(), 13.0, epsilon = 1e-12);
2015    }
2016
2017    #[test]
2018    fn det_direct_d3_skips_zero_coefficient_minor_that_would_overflow() {
2019        let m = black_box(
2020            Matrix::<3>::try_from_rows([
2021                [1.0, 0.0, 0.0],
2022                [1.0e300, 1.0, 1.0e300],
2023                [1.0e300, 0.0, 1.0e300],
2024            ])
2025            .unwrap(),
2026        );
2027        assert_eq!(m.det_direct(), Ok(Some(1.0e300)));
2028    }
2029
2030    #[test]
2031    fn det_direct_d4_known_value() {
2032        // Diagonal matrix: det = product of diagonal entries.
2033        let mut rows = [[0.0f64; 4]; 4];
2034        rows[0][0] = 2.0;
2035        rows[1][1] = 3.0;
2036        rows[2][2] = 5.0;
2037        rows[3][3] = 7.0;
2038        let m = black_box(Matrix::<4>::try_from_rows(rows).unwrap());
2039        assert_abs_diff_eq!(m.det_direct().unwrap().unwrap(), 210.0, epsilon = 1e-12);
2040    }
2041
2042    #[test]
2043    fn det_direct_d4_dense_known_value() {
2044        let m = black_box(
2045            Matrix::<4>::try_from_rows([
2046                [4.0, 1.0, 3.0, 2.0],
2047                [1.0, 5.0, 2.0, 1.0],
2048                [7.0, 2.0, 6.0, 3.0],
2049                [1.0, 8.0, 4.0, 9.0],
2050            ])
2051            .unwrap(),
2052        );
2053        let direct = m.det_direct().unwrap().unwrap();
2054        let paired = m.det_direct_with_errbound().unwrap().unwrap();
2055
2056        assert_abs_diff_eq!(direct, 112.0, epsilon = 1e-12);
2057        assert_eq!(paired.determinant().to_bits(), direct.to_bits());
2058    }
2059
2060    #[test]
2061    fn det_direct_d4_dense_reports_legitimate_overflow() {
2062        // The unscaled matrix has determinant 189, so scaling every entry by
2063        // 3.2e76 gives a determinant of approximately 1.98e308.
2064        let scale = 3.2e76;
2065        let m = black_box(
2066            Matrix::<4>::try_from_rows([
2067                [4.0 * scale, scale, scale, scale],
2068                [scale, 4.0 * scale, scale, scale],
2069                [scale, scale, 4.0 * scale, scale],
2070                [scale, scale, scale, 4.0 * scale],
2071            ])
2072            .unwrap(),
2073        );
2074        let expected = LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant);
2075
2076        assert_eq!(m.det_direct(), Err(expected));
2077        assert_eq!(m.det(), Err(expected));
2078    }
2079
2080    #[test]
2081    fn det_errbound_d4_dense_reports_legitimate_overflow() {
2082        // The unscaled matrix has determinant 189, so scaling every entry by
2083        // 3.2e76 gives a determinant of approximately 1.98e308.
2084        let scale = 3.2e76;
2085        let m = black_box(
2086            Matrix::<4>::try_from_rows([
2087                [4.0 * scale, scale, scale, scale],
2088                [scale, 4.0 * scale, scale, scale],
2089                [scale, scale, 4.0 * scale, scale],
2090                [scale, scale, scale, 4.0 * scale],
2091            ])
2092            .unwrap(),
2093        );
2094        let expected =
2095            LaError::non_finite_computation_scalar(ArithmeticOperation::DeterminantErrorBound);
2096
2097        assert_eq!(m.det_errbound(), Err(expected));
2098        assert_eq!(m.det_direct_with_errbound(), Err(expected));
2099    }
2100
2101    #[test]
2102    fn det_direct_d4_skips_zero_coefficient_cofactors_that_would_overflow() {
2103        let m = black_box(
2104            Matrix::<4>::try_from_rows([
2105                [0.0, 0.0, 0.0, 0.0],
2106                [0.0, 0.0, 0.0, 0.0],
2107                [1.0e300, 0.0, 1.0e300, 1.0e300],
2108                [1.0e300, 0.0, 1.0e300, -1.0e300],
2109            ])
2110            .unwrap(),
2111        );
2112        assert_eq!(m.det_direct(), Ok(Some(0.0)));
2113    }
2114
2115    #[test]
2116    fn det_direct_d4_sparse_second_row_skips_inactive_overflowing_minors() {
2117        let m = black_box(
2118            Matrix::<4>::try_from_rows([
2119                [1.0e-300, 1.0, 1.0, 1.0],
2120                [0.0, 1.0, 0.0, 0.0],
2121                [0.0, 1.0e300, 1.0, 1.0e300],
2122                [0.0, 1.0e300, 0.0, 1.0e300],
2123            ])
2124            .unwrap(),
2125        );
2126
2127        assert_eq!(m.det_direct(), Ok(Some(1.0)));
2128        assert_eq!(m.det(), Ok(1.0));
2129    }
2130
2131    #[test]
2132    fn det_direct_d5_returns_none() {
2133        assert_eq!(Matrix::<5>::identity().det_direct(), Ok(None));
2134    }
2135
2136    #[test]
2137    fn det_direct_d8_returns_none() {
2138        assert_eq!(Matrix::<8>::zero().det_direct(), Ok(None));
2139    }
2140
2141    #[test]
2142    fn det_direct_rejects_computed_overflow() {
2143        let m = Matrix::<2>::try_from_rows([[1e300, 0.0], [0.0, 1e300]]).unwrap();
2144        assert_eq!(
2145            m.det_direct(),
2146            Err(LaError::non_finite_computation_scalar(
2147                ArithmeticOperation::Determinant
2148            ))
2149        );
2150    }
2151
2152    #[test]
2153    fn det_d5_rejects_lu_product_overflow() {
2154        let m = Matrix::<5>::try_from_rows([
2155            [1.0e100, 0.0, 0.0, 0.0, 0.0],
2156            [0.0, 1.0e100, 0.0, 0.0, 0.0],
2157            [0.0, 0.0, 1.0e100, 0.0, 0.0],
2158            [0.0, 0.0, 0.0, 1.0e100, 0.0],
2159            [0.0, 0.0, 0.0, 0.0, 1.0e100],
2160        ])
2161        .unwrap();
2162        assert_eq!(
2163            m.det(),
2164            Err(LaError::non_finite_computation_step(
2165                ArithmeticOperation::Determinant,
2166                4
2167            ))
2168        );
2169    }
2170
2171    #[test]
2172    fn det_d5_rejects_lu_trailing_update_overflow() {
2173        let m = Matrix::<5>::try_from_rows([
2174            [1.0, f64::MAX, 0.0, 0.0, 0.0],
2175            [-1.0, f64::MAX, 0.0, 0.0, 0.0],
2176            [0.0, 0.0, 1.0, 0.0, 0.0],
2177            [0.0, 0.0, 0.0, 1.0, 0.0],
2178            [0.0, 0.0, 0.0, 0.0, 1.0],
2179        ])
2180        .unwrap();
2181
2182        assert_eq!(
2183            m.det(),
2184            Err(LaError::non_finite_computation_matrix(
2185                ArithmeticOperation::LuFactorization,
2186                1,
2187                1
2188            ))
2189        );
2190    }
2191
2192    macro_rules! gen_det_direct_agrees_with_lu {
2193        ($d:literal) => {
2194            paste! {
2195                #[test]
2196                #[expect(
2197                    clippy::cast_precision_loss,
2198                    reason = "r, c, and D are tiny test integers exactly representable as f64"
2199                )]
2200                fn [<det_direct_agrees_with_lu_ $d d>]() {
2201                    // Well-conditioned matrix: diagonally dominant.
2202                    let mut rows = [[0.0f64; $d]; $d];
2203                    for r in 0..$d {
2204                        for c in 0..$d {
2205                            rows[r][c] = if r == c {
2206                                (r as f64) + f64::from($d) + 1.0
2207                            } else {
2208                                0.1 / ((r + c + 1) as f64)
2209                            };
2210                        }
2211                    }
2212                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
2213                    let direct = m.det_direct().unwrap().unwrap();
2214                    let lu_det = m.lu(DEFAULT_SINGULAR_TOL).unwrap().det().unwrap();
2215                    let eps = lu_det.abs().mul_add(1e-12, 1e-12);
2216                    assert_abs_diff_eq!(direct, lu_det, epsilon = eps);
2217                }
2218            }
2219        };
2220    }
2221
2222    gen_det_direct_agrees_with_lu!(1);
2223    gen_det_direct_agrees_with_lu!(2);
2224    gen_det_direct_agrees_with_lu!(3);
2225    gen_det_direct_agrees_with_lu!(4);
2226
2227    #[test]
2228    fn det_direct_identity_all_dims() {
2229        assert_abs_diff_eq!(
2230            Matrix::<1>::identity().det_direct().unwrap().unwrap(),
2231            1.0,
2232            epsilon = 0.0
2233        );
2234        assert_abs_diff_eq!(
2235            Matrix::<2>::identity().det_direct().unwrap().unwrap(),
2236            1.0,
2237            epsilon = 0.0
2238        );
2239        assert_abs_diff_eq!(
2240            Matrix::<3>::identity().det_direct().unwrap().unwrap(),
2241            1.0,
2242            epsilon = 0.0
2243        );
2244        assert_abs_diff_eq!(
2245            Matrix::<4>::identity().det_direct().unwrap().unwrap(),
2246            1.0,
2247            epsilon = 0.0
2248        );
2249    }
2250
2251    #[test]
2252    fn det_direct_zero_matrix() {
2253        assert_abs_diff_eq!(
2254            Matrix::<2>::zero().det_direct().unwrap().unwrap(),
2255            0.0,
2256            epsilon = 0.0
2257        );
2258        assert_abs_diff_eq!(
2259            Matrix::<3>::zero().det_direct().unwrap().unwrap(),
2260            0.0,
2261            epsilon = 0.0
2262        );
2263        assert_abs_diff_eq!(
2264            Matrix::<4>::zero().det_direct().unwrap().unwrap(),
2265            0.0,
2266            epsilon = 0.0
2267        );
2268    }
2269
2270    macro_rules! gen_det_singular_zero_matrix_tests {
2271        ($d:literal) => {
2272            paste! {
2273                #[test]
2274                fn [<det_singular_zero_matrix_returns_zero_ $d d>]() {
2275                    assert_abs_diff_eq!(
2276                        Matrix::<$d>::zero().det().unwrap(),
2277                        0.0,
2278                        epsilon = 0.0
2279                    );
2280                }
2281            }
2282        };
2283    }
2284
2285    gen_det_singular_zero_matrix_tests!(2);
2286    gen_det_singular_zero_matrix_tests!(3);
2287    gen_det_singular_zero_matrix_tests!(4);
2288
2289    #[test]
2290    fn det_singular_zero_matrix_d5_preserves_lu_error() {
2291        assert_eq!(
2292            Matrix::<5>::zero().det(),
2293            Err(LaError::singular_numerical(
2294                0,
2295                FactorizationKind::Lu,
2296                0.0,
2297                0.0
2298            ))
2299        );
2300    }
2301
2302    #[test]
2303    fn det_d5_does_not_turn_elimination_underflow_into_exact_zero() {
2304        let min_subnormal = f64::from_bits(1);
2305        let two_pow_800 = f64::from_bits(1823_u64 << 52);
2306        let m = Matrix::<5>::try_from_rows([
2307            [2.0, min_subnormal, 0.0, 0.0, 0.0],
2308            [1.0, 0.0, 0.0, 0.0, 0.0],
2309            [0.0, 0.0, two_pow_800, 0.0, 0.0],
2310            [0.0, 0.0, 0.0, 1.0, 0.0],
2311            [0.0, 0.0, 0.0, 0.0, 1.0],
2312        ])
2313        .unwrap();
2314
2315        assert_eq!(
2316            m.det(),
2317            Err(LaError::singular_numerical(
2318                1,
2319                FactorizationKind::Lu,
2320                0.0,
2321                0.0
2322            ))
2323        );
2324    }
2325
2326    #[test]
2327    fn det_d5_ignores_pivot_tolerance_for_tiny_nonsingular_matrix() {
2328        // A small nonzero determinant is still a determinant. `det` must not
2329        // flatten the value to zero merely because the default LU tolerance
2330        // would reject a pivot this small.
2331        let m = Matrix::<5>::try_from_rows([
2332            [1e-13, 0.0, 0.0, 0.0, 0.0],
2333            [0.0, 1.0, 0.0, 0.0, 0.0],
2334            [0.0, 0.0, 1.0, 0.0, 0.0],
2335            [0.0, 0.0, 0.0, 1.0, 0.0],
2336            [0.0, 0.0, 0.0, 0.0, 1.0],
2337        ])
2338        .unwrap();
2339
2340        assert_abs_diff_eq!(m.det().unwrap(), 1e-13, epsilon = 0.0);
2341        assert_eq!(
2342            m.lu(DEFAULT_SINGULAR_TOL),
2343            Err(LaError::singular_numerical(
2344                0,
2345                FactorizationKind::Lu,
2346                1e-13,
2347                DEFAULT_SINGULAR_TOL.get()
2348            ))
2349        );
2350    }
2351
2352    #[test]
2353    fn det_returns_non_finite_error_for_overflow_with_finite_entries() {
2354        // det_direct produces an overflowing f64 (1e300 * 1e300 = ∞) even
2355        // though every matrix entry is finite. The entry scan in `det`
2356        // falls through and reports a computed determinant overflow rather
2357        // than a NaN/∞ input.
2358        let m = Matrix::<2>::try_from_rows([[1e300, 0.0], [0.0, 1e300]]).unwrap();
2359        assert_eq!(
2360            m.det(),
2361            Err(LaError::non_finite_computation_scalar(
2362                ArithmeticOperation::Determinant
2363            ))
2364        );
2365    }
2366
2367    // === det_direct const-evaluability tests (D = 2..=5) ===
2368    //
2369    // Every dimension hits a distinct arm of the `match D { … }` body inside
2370    // `det_direct`, so exercising each at compile time is the tightest
2371    // const-fn proof available.
2372
2373    macro_rules! gen_det_direct_const_eval_tests {
2374        ($d:literal) => {
2375            paste! {
2376                /// `Matrix::<D>::det_direct()` on the identity must const-evaluate
2377                /// to `Ok(Some(1.0))` for every closed-form dimension `D ∈ {1, 2, 3, 4}`.
2378                #[test]
2379                fn [<det_direct_const_eval_ $d d>]() {
2380                    const DET: Result<Option<f64>, LaError> = Matrix::<$d>::identity().det_direct();
2381                    assert_eq!(DET, Ok(Some(1.0)));
2382                }
2383            }
2384        };
2385    }
2386
2387    gen_det_direct_const_eval_tests!(2);
2388    gen_det_direct_const_eval_tests!(3);
2389    gen_det_direct_const_eval_tests!(4);
2390
2391    #[test]
2392    fn det_direct_const_eval_d5_is_none() {
2393        // D ≥ 5 has no closed-form arm; `det_direct` returns `Ok(None)`.  Verify
2394        // that the wildcard arm is reachable in a `const { … }` context.
2395        const DET: Result<Option<f64>, LaError> = Matrix::<5>::identity().det_direct();
2396        assert_eq!(DET, Ok(None));
2397    }
2398
2399    // === det_errbound tests (no `exact` feature required) ===
2400
2401    #[test]
2402    fn det_errbound_matches_documented_coefficient_scale() {
2403        let m2 = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2404        let expected_2 = ERR_COEFF_2 * ((1.0_f64 * 4.0).abs() + (2.0_f64 * 3.0).abs());
2405        assert_abs_diff_eq!(
2406            m2.det_errbound().unwrap().unwrap(),
2407            expected_2,
2408            epsilon = 0.0
2409        );
2410
2411        assert_abs_diff_eq!(
2412            Matrix::<3>::identity().det_errbound().unwrap().unwrap(),
2413            ERR_COEFF_3,
2414            epsilon = 0.0
2415        );
2416        assert_abs_diff_eq!(
2417            Matrix::<4>::identity().det_errbound().unwrap().unwrap(),
2418            ERR_COEFF_4,
2419            epsilon = 0.0
2420        );
2421    }
2422
2423    #[test]
2424    fn det_errbound_d3_skips_zero_coefficient_minor_that_would_overflow() {
2425        let m = Matrix::<3>::try_from_rows([
2426            [1.0, 0.0, 0.0],
2427            [1.0e300, 1.0, 1.0e300],
2428            [1.0e300, 0.0, 1.0e300],
2429        ])
2430        .unwrap();
2431
2432        assert_eq!(m.det_errbound(), Ok(Some(ERR_COEFF_3 * 1.0e300)));
2433    }
2434
2435    #[test]
2436    fn det_errbound_d4_skips_zero_coefficient_cofactors_that_would_overflow() {
2437        let m = Matrix::<4>::try_from_rows([
2438            [0.0, 0.0, 0.0, 0.0],
2439            [0.0, 0.0, 0.0, 0.0],
2440            [1.0e300, 0.0, 1.0e300, 1.0e300],
2441            [1.0e300, 0.0, 1.0e300, -1.0e300],
2442        ])
2443        .unwrap();
2444
2445        assert_eq!(m.det_errbound(), Ok(Some(0.0)));
2446    }
2447
2448    #[test]
2449    fn det_errbound_d5_returns_none() {
2450        // D=5 has no fast filter
2451        assert_eq!(Matrix::<5>::identity().det_errbound(), Ok(None));
2452    }
2453
2454    #[test]
2455    fn combined_det_bound_wide_exponent_fast_path_matches_tracked_arithmetic() {
2456        let threshold = f64::from_bits(1007_u64 << 52); // 2^-16
2457        let at_threshold = Matrix::<2>::try_from_rows([[threshold, 0.0], [0.0, 2.0]]).unwrap();
2458        assert!(at_threshold.det_bound_inputs_have_wide_exponent_margin());
2459
2460        let tracked = at_threshold
2461            .det_direct_with_errbound_from_arithmetic(
2462                at_threshold
2463                    .det_direct_arithmetic::<true>()
2464                    .expect("D=2 has direct arithmetic"),
2465            )
2466            .unwrap();
2467        assert_eq!(at_threshold.det_direct_with_errbound().unwrap(), tracked);
2468
2469        let just_below = f64::from_bits(threshold.to_bits() - 1);
2470        let below_threshold = Matrix::<2>::try_from_rows([[just_below, 0.0], [0.0, 2.0]]).unwrap();
2471        assert!(!below_threshold.det_bound_inputs_have_wide_exponent_margin());
2472        assert!(!Matrix::<5>::identity().det_bound_inputs_have_wide_exponent_margin());
2473    }
2474
2475    #[test]
2476    fn det_direct_with_errbound_covers_zero_and_one_dimensions() {
2477        let empty = Matrix::<0>::zero()
2478            .det_direct_with_errbound()
2479            .unwrap()
2480            .unwrap();
2481        assert_abs_diff_eq!(empty.determinant(), 1.0, epsilon = 0.0);
2482        assert_abs_diff_eq!(empty.absolute_error_bound(), 0.0, epsilon = 0.0);
2483
2484        let scalar = Matrix::<1>::try_from_rows([[-7.0]])
2485            .unwrap()
2486            .det_direct_with_errbound()
2487            .unwrap()
2488            .unwrap();
2489        assert_abs_diff_eq!(scalar.determinant(), -7.0, epsilon = 0.0);
2490        assert_abs_diff_eq!(scalar.absolute_error_bound(), 0.0, epsilon = 0.0);
2491    }
2492
2493    #[test]
2494    fn det_direct_with_errbound_pairs_the_closed_form_values() {
2495        let matrix = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2496        let estimate = matrix.det_direct_with_errbound().unwrap().unwrap();
2497
2498        assert_abs_diff_eq!(
2499            estimate.determinant(),
2500            matrix.det_direct().unwrap().unwrap(),
2501            epsilon = 0.0
2502        );
2503        assert_abs_diff_eq!(
2504            estimate.absolute_error_bound(),
2505            ERR_COEFF_2 * (4.0_f64 + 6.0_f64),
2506            epsilon = 0.0
2507        );
2508    }
2509
2510    #[test]
2511    fn det_direct_with_errbound_d5_returns_none() {
2512        assert_eq!(Matrix::<5>::identity().det_direct_with_errbound(), Ok(None));
2513    }
2514
2515    #[test]
2516    fn det_errbound_rejects_computed_overflow() {
2517        let m = Matrix::<2>::try_from_rows([[1e300, 0.0], [0.0, 1e300]]).unwrap();
2518        assert_eq!(
2519            m.det_errbound(),
2520            Err(LaError::non_finite_computation_scalar(
2521                ArithmeticOperation::DeterminantErrorBound
2522            ))
2523        );
2524    }
2525
2526    // === det_errbound const-evaluability tests (D = 2..=5) ===
2527
2528    macro_rules! gen_det_errbound_const_eval_tests {
2529        ($d:literal) => {
2530            paste! {
2531                /// `Matrix::<D>::det_errbound()` on the identity must const-evaluate
2532                /// to `Ok(Some(bound))` with `bound > 0` for every closed-form dimension
2533                /// `D ∈ {2, 3, 4}`.  Each dimension hits a distinct arm of
2534                /// `det_errbound` with a dimension-specific permanent computation.
2535                #[test]
2536                fn [<det_errbound_const_eval_ $d d>]() {
2537                    const BOUND: Result<Option<f64>, LaError> = Matrix::<$d>::identity().det_errbound();
2538                    assert!(BOUND.unwrap().unwrap() > 0.0);
2539                }
2540            }
2541        };
2542    }
2543
2544    gen_det_errbound_const_eval_tests!(2);
2545    gen_det_errbound_const_eval_tests!(3);
2546    gen_det_errbound_const_eval_tests!(4);
2547
2548    #[test]
2549    fn det_errbound_const_eval_d5_is_none() {
2550        // D ≥ 5 has no fast-filter bound; `det_errbound` returns `Ok(None)`.
2551        const BOUND: Result<Option<f64>, LaError> = Matrix::<5>::identity().det_errbound();
2552        assert_eq!(BOUND, Ok(None));
2553    }
2554
2555    // === inf_norm const-evaluability tests (D = 2..=5) ===
2556
2557    macro_rules! gen_inf_norm_const_eval_tests {
2558        ($d:literal) => {
2559            paste! {
2560                /// `Matrix::<D>::inf_norm()` on the identity must const-evaluate
2561                /// to `1.0` for every `D ≥ 1` — each row has a single `1.0`
2562                /// entry, so the max absolute row sum is exactly `1.0`.
2563                #[test]
2564                fn [<inf_norm_const_eval_ $d d>]() {
2565                    const NORM: Result<f64, LaError> = Matrix::<$d>::identity().inf_norm();
2566                    assert!((NORM.unwrap() - 1.0).abs() <= 1e-12);
2567                }
2568            }
2569        };
2570    }
2571
2572    gen_inf_norm_const_eval_tests!(2);
2573    gen_inf_norm_const_eval_tests!(3);
2574    gen_inf_norm_const_eval_tests!(4);
2575    gen_inf_norm_const_eval_tests!(5);
2576
2577    // === is_symmetric / first_asymmetry (public LDLT preconditions helpers) ===
2578
2579    macro_rules! gen_is_symmetric_tests {
2580        ($d:literal) => {
2581            paste! {
2582                #[test]
2583                fn [<is_symmetric_true_for_identity_ $d d>]() {
2584                    let m = Matrix::<$d>::identity();
2585                    assert!(m.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2586                    assert_eq!(m.first_asymmetry(Tolerance::try_new(1e-12).unwrap()).unwrap(), None);
2587                }
2588
2589                #[test]
2590                fn [<is_symmetric_true_for_zero_ $d d>]() {
2591                    let m = Matrix::<$d>::zero();
2592                    assert!(m.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2593                    assert_eq!(m.first_asymmetry(Tolerance::try_new(1e-12).unwrap()).unwrap(), None);
2594                }
2595
2596                #[test]
2597                fn [<is_symmetric_true_for_constructed_symmetric_ $d d>]() {
2598                    // Construct A = M + Mᵀ so A is provably symmetric.
2599                    let mut m = [[0.0f64; $d]; $d];
2600                    for r in 0..$d {
2601                        for c in 0..$d {
2602                            #[expect(
2603                                clippy::cast_precision_loss,
2604                                reason = "matrix test indices are at most five and exactly representable as f64"
2605                            )]
2606                            {
2607                                m[r][c] = (r * $d + c) as f64;
2608                            }
2609                        }
2610                    }
2611                    let mut sym = [[0.0f64; $d]; $d];
2612                    for r in 0..$d {
2613                        for c in 0..$d {
2614                            sym[r][c] = m[r][c] + m[c][r];
2615                        }
2616                    }
2617                    let a = Matrix::<$d>::try_from_rows(sym).unwrap();
2618                    assert!(a.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2619                    assert_eq!(a.first_asymmetry(Tolerance::try_new(1e-12).unwrap()).unwrap(), None);
2620                }
2621
2622                #[test]
2623                fn [<is_symmetric_false_for_asymmetric_offdiagonal_ $d d>]() {
2624                    // Perturb a single off-diagonal entry so symmetry fails.
2625                    let mut rows = [[0.0f64; $d]; $d];
2626                    for i in 0..$d {
2627                        rows[i][i] = 1.0;
2628                    }
2629                    rows[0][$d - 1] = 1.0;
2630                    rows[$d - 1][0] = -1.0; // breaks symmetry
2631                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2632                    assert!(!a.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2633                    assert_eq!(
2634                        a.first_asymmetry(Tolerance::try_new(1e-12).unwrap()).unwrap(),
2635                        Some((0, $d - 1))
2636                    );
2637                }
2638
2639            }
2640        };
2641    }
2642
2643    gen_is_symmetric_tests!(2);
2644    gen_is_symmetric_tests!(3);
2645    gen_is_symmetric_tests!(4);
2646    gen_is_symmetric_tests!(5);
2647
2648    macro_rules! gen_ldlt_symmetry_proof_tests {
2649        ($d:literal) => {
2650            paste! {
2651                #[test]
2652                fn [<matrix_ldlt_accepts_exact_symmetric_spd_ $d d>]() {
2653                    // This exactly mirrored, strictly diagonally dominant
2654                    // tridiagonal matrix is positive definite.
2655                    let mut rows = [[0.0_f64; $d]; $d];
2656                    for (index, row) in rows.iter_mut().enumerate() {
2657                        row[index] = 2.0;
2658                    }
2659                    for index in 1..$d {
2660                        rows[index - 1][index] = 0.5;
2661                        rows[index][index - 1] = 0.5;
2662                    }
2663
2664                    let matrix = Matrix::<$d>::try_from_rows(rows).unwrap();
2665                    let symmetric = SymmetricMatrix::try_new(matrix).unwrap();
2666                    assert_eq!(symmetric.into_matrix(), matrix);
2667
2668                    let ldlt = matrix.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
2669                    assert!(ldlt.det().unwrap() > 0.0);
2670                }
2671
2672                #[test]
2673                fn [<symmetric_matrix_try_new_rejects_finite_asymmetric_ $d d>]() {
2674                    let mut rows = [[0.0f64; $d]; $d];
2675                    for (i, row) in rows.iter_mut().enumerate() {
2676                        row[i] = 1.0;
2677                    }
2678                    rows[0][$d - 1] = 1.0;
2679                    rows[$d - 1][0] = -1.0;
2680
2681                    assert_eq!(
2682                        Matrix::<$d>::try_from_rows(rows).and_then(SymmetricMatrix::try_new),
2683                        Err(LaError::asymmetric(0, $d - 1, $d, 1.0, -1.0, 0.0))
2684                    );
2685                }
2686            }
2687        };
2688    }
2689
2690    gen_ldlt_symmetry_proof_tests!(2);
2691    gen_ldlt_symmetry_proof_tests!(3);
2692    gen_ldlt_symmetry_proof_tests!(4);
2693    gen_ldlt_symmetry_proof_tests!(5);
2694
2695    #[test]
2696    fn symmetric_matrix_into_matrix_roundtrips_storage_internally() {
2697        let a = Matrix::<2>::try_from_rows([[2.0, 1.0], [1.0, 3.0]]).unwrap();
2698        let symmetric = SymmetricMatrix::try_new(a).unwrap();
2699
2700        assert_eq!(symmetric.into_matrix(), a);
2701    }
2702
2703    #[test]
2704    fn matrix_ldlt_accepts_opposite_signed_zero_mirrors() {
2705        let matrix = Matrix::<2>::try_from_rows([[2.0, 0.0], [-0.0, 2.0]]).unwrap();
2706        let ldlt = matrix.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
2707
2708        assert_eq!(ldlt.det(), Ok(4.0));
2709    }
2710
2711    #[test]
2712    fn is_symmetric_tolerance_scales_with_inf_norm() {
2713        // Off-diagonal entries differ by 1e-6.  With inf_norm ≈ 2e6, the
2714        // relative tolerance 1e-12 yields eps ≈ 2e-6, which accepts the gap;
2715        // a stricter tol of 1e-15 rejects it.
2716        let a = Matrix::<2>::try_from_rows([[1.0e6, 1.0e6 + 1.0e-6], [1.0e6, 1.0e6]]).unwrap();
2717        assert!(a.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2718        assert!(!a.is_symmetric(Tolerance::try_new(1e-15).unwrap()).unwrap());
2719    }
2720
2721    #[test]
2722    fn symmetry_epsilon_multiplies_after_row_sum_near_subnormal_boundary() {
2723        let min_subnormal = f64::from_bits(1);
2724        let mut rows = [[0.0; 5]; 5];
2725        let mut col = 0;
2726        while col < 4 {
2727            rows[0][col] = 0.4;
2728            rows[col][0] = 0.4;
2729            col += 1;
2730        }
2731        rows[0][4] = 2.0 * min_subnormal;
2732        rows[4][0] = 0.0;
2733
2734        let matrix = Matrix::<5>::try_from_rows(rows).unwrap();
2735        let tolerance = Tolerance::try_new(min_subnormal).unwrap();
2736        let expected_epsilon = tolerance.get() * matrix.inf_norm().unwrap().max(1.0);
2737
2738        assert_eq!(expected_epsilon.to_bits(), 2);
2739        assert_eq!(matrix.first_asymmetry(tolerance), Ok(None));
2740        assert_eq!(matrix.is_symmetric(tolerance), Ok(true));
2741    }
2742
2743    #[test]
2744    fn symmetry_epsilon_scales_terms_when_row_sum_overflows() {
2745        let matrix =
2746            Matrix::<2>::try_from_rows([[f64::MAX, f64::MAX], [f64::MAX / 2.0, f64::MAX]]).unwrap();
2747
2748        assert_eq!(
2749            matrix.inf_norm(),
2750            Err(LaError::non_finite_computation_matrix(
2751                ArithmeticOperation::MatrixInfinityNorm,
2752                0,
2753                1
2754            ))
2755        );
2756        assert_eq!(
2757            matrix.first_asymmetry(Tolerance::try_new(0.25).unwrap()),
2758            Ok(None)
2759        );
2760        assert_eq!(
2761            matrix.first_asymmetry(Tolerance::try_new(0.125).unwrap()),
2762            Ok(Some((0, 1)))
2763        );
2764    }
2765
2766    #[test]
2767    fn first_asymmetry_returns_lexicographically_first_pair() {
2768        // Two asymmetric pairs: (0, 2) and (1, 2).  We must get (0, 2) first.
2769        let a = Matrix::<3>::try_from_rows([[1.0, 0.0, 2.0], [0.0, 1.0, 3.0], [-2.0, -3.0, 1.0]])
2770            .unwrap();
2771        assert_eq!(
2772            a.first_asymmetry(Tolerance::try_new(1e-12).unwrap())
2773                .unwrap(),
2774            Some((0, 2))
2775        );
2776    }
2777
2778    #[test]
2779    fn first_asymmetry_strict_tol_survives_row_sum_overflow() {
2780        let a = Matrix::<3>::try_from_rows([
2781            [1.0, 1.0, 0.0],
2782            [2.0, f64::MAX, f64::MAX],
2783            [0.0, 0.0, 1.0],
2784        ])
2785        .unwrap();
2786
2787        assert_eq!(
2788            a.inf_norm(),
2789            Err(LaError::non_finite_computation_matrix(
2790                ArithmeticOperation::MatrixInfinityNorm,
2791                1,
2792                2
2793            ))
2794        );
2795        assert_eq!(
2796            a.first_asymmetry(Tolerance::try_new(0.0).unwrap()).unwrap(),
2797            Some((0, 1))
2798        );
2799        assert!(!a.is_symmetric(Tolerance::try_new(0.0).unwrap()).unwrap());
2800    }
2801
2802    #[test]
2803    fn first_asymmetry_rejects_scaled_epsilon_overflow() {
2804        let a = Matrix::<2>::try_from_rows([[0.0, 0.0], [2.0, 1.0]]).unwrap();
2805        let tol = Tolerance::try_new(f64::MAX).unwrap();
2806
2807        assert_eq!(
2808            a.first_asymmetry(tol),
2809            Err(LaError::non_finite_computation_matrix(
2810                ArithmeticOperation::SymmetryCheck,
2811                1,
2812                0
2813            ))
2814        );
2815        assert_eq!(
2816            a.is_symmetric(tol),
2817            Err(LaError::non_finite_computation_matrix(
2818                ArithmeticOperation::SymmetryCheck,
2819                1,
2820                0
2821            ))
2822        );
2823    }
2824
2825    #[test]
2826    fn first_asymmetry_flags_overflowed_finite_difference() {
2827        let a = Matrix::<2>::try_from_rows([[1.0, f64::MAX], [-f64::MAX, 1.0]]).unwrap();
2828        assert_eq!(
2829            a.first_asymmetry(Tolerance::try_new(1e-12).unwrap())
2830                .unwrap(),
2831            Some((0, 1))
2832        );
2833        assert!(!a.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2834    }
2835}