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) => return Ok(None),
1339            Err(error) => return Err(error),
1340        };
1341        if !det.value.is_finite() {
1342            cold_path();
1343            return Err(LaError::non_finite_computation_scalar(
1344                ArithmeticOperation::Determinant,
1345            ));
1346        }
1347        Ok(Some(DeterminantWithErrorBound {
1348            determinant: det.value,
1349            absolute_error_bound: bound,
1350        }))
1351    }
1352
1353    /// Compute a bound after the matching determinant tree has been evaluated.
1354    const fn det_errbound_from_arithmetic<const TRACK_UNDERFLOW: bool>(
1355        &self,
1356        det: FilterArithmetic<TRACK_UNDERFLOW>,
1357    ) -> Result<Option<f64>, LaError> {
1358        if !det.underflow_safe {
1359            cold_path();
1360            return Ok(None);
1361        }
1362
1363        match D {
1364            0 | 1 => Self::computed_scalar_result(ArithmeticOperation::DeterminantErrorBound, 0.0),
1365            2 => {
1366                let r = &self.rows;
1367                let product_0 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r[0][0], r[1][1]);
1368                let product_1 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r[0][1], r[1][0]);
1369                let mut permanent = FilterArithmetic::<TRACK_UNDERFLOW>::add_non_negative(
1370                    product_0.value.abs(),
1371                    product_1.value.abs(),
1372                );
1373                permanent.underflow_safe &= product_0.underflow_safe && product_1.underflow_safe;
1374                Self::certified_error_bound(ERR_COEFF_2, permanent)
1375            }
1376            3 => {
1377                let r = &self.rows;
1378                let permanent = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1379                    [r[0][0], r[0][1], r[0][2]],
1380                    [r[1][0], r[1][1], r[1][2]],
1381                    [r[2][0], r[2][1], r[2][2]],
1382                );
1383                Self::certified_error_bound(ERR_COEFF_3, permanent)
1384            }
1385            4 => self.det4_errbound::<TRACK_UNDERFLOW>(),
1386            _ => {
1387                cold_path();
1388                Ok(None)
1389            }
1390        }
1391    }
1392
1393    /// Compute the D=4 determinant error bound after the dimension dispatch.
1394    const fn det4_errbound<const TRACK_UNDERFLOW: bool>(&self) -> Result<Option<f64>, LaError> {
1395        if !TRACK_UNDERFLOW && let Some(input) = Det4SharedMinorInput::try_new(self) {
1396            return Self::certified_error_bound(
1397                ERR_COEFF_4,
1398                FilterArithmetic::<TRACK_UNDERFLOW> {
1399                    value: Self::det4_dense_abs_permanent_elements(input),
1400                    underflow_safe: true,
1401                },
1402            );
1403        }
1404
1405        let r = &self.rows;
1406        let mut permanent = if r[0][3] == 0.0 {
1407            FilterArithmetic {
1408                value: 0.0,
1409                underflow_safe: true,
1410            }
1411        } else {
1412            let pc3 = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1413                [r[1][0], r[1][1], r[1][2]],
1414                [r[2][0], r[2][1], r[2][2]],
1415                [r[3][0], r[3][1], r[3][2]],
1416            );
1417            let mut term = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r[0][3].abs(), pc3.value);
1418            term.underflow_safe &= pc3.underflow_safe;
1419            term
1420        };
1421        if r[0][2] != 0.0 {
1422            let pc2 = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1423                [r[1][0], r[1][1], r[1][3]],
1424                [r[2][0], r[2][1], r[2][3]],
1425                [r[3][0], r[3][1], r[3][3]],
1426            );
1427            let prior_safe = permanent.underflow_safe && pc2.underflow_safe;
1428            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1429                r[0][2].abs(),
1430                pc2.value,
1431                permanent.value,
1432            );
1433            permanent.underflow_safe &= prior_safe;
1434        }
1435        if r[0][1] != 0.0 {
1436            let pc1 = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1437                [r[1][0], r[1][2], r[1][3]],
1438                [r[2][0], r[2][2], r[2][3]],
1439                [r[3][0], r[3][2], r[3][3]],
1440            );
1441            let prior_safe = permanent.underflow_safe && pc1.underflow_safe;
1442            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1443                r[0][1].abs(),
1444                pc1.value,
1445                permanent.value,
1446            );
1447            permanent.underflow_safe &= prior_safe;
1448        }
1449        if r[0][0] != 0.0 {
1450            let pc0 = Self::det3_abs_permanent_elements::<TRACK_UNDERFLOW>(
1451                [r[1][1], r[1][2], r[1][3]],
1452                [r[2][1], r[2][2], r[2][3]],
1453                [r[3][1], r[3][2], r[3][3]],
1454            );
1455            let prior_safe = permanent.underflow_safe && pc0.underflow_safe;
1456            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1457                r[0][0].abs(),
1458                pc0.value,
1459                permanent.value,
1460            );
1461            permanent.underflow_safe &= prior_safe;
1462        }
1463        Self::certified_error_bound(ERR_COEFF_4, permanent)
1464    }
1465
1466    /// Evaluate a 3×3 determinant expansion with a guarded sparse fallback.
1467    ///
1468    /// When all three first-row coefficients are non-zero, one branch-free
1469    /// closed form is used. The sparse fallback protects the public
1470    /// [`det_direct`](Self::det_direct) contract: a mathematically absent term
1471    /// must not compute an overflowing minor and poison the determinant with
1472    /// `0.0 * inf == NaN`. Nonzero terms keep the same fused multiply-add
1473    /// ordering as the closed-form expansion.
1474    #[expect(
1475        clippy::inline_always,
1476        reason = "det_direct callers must eliminate unused filter-safety bookkeeping"
1477    )]
1478    #[inline(always)]
1479    const fn det3_elements<const TRACK_UNDERFLOW: bool>(
1480        r0: [f64; 3],
1481        r1: [f64; 3],
1482        r2: [f64; 3],
1483    ) -> FilterArithmetic<TRACK_UNDERFLOW> {
1484        let dense = (r0[0] != 0.0) && (r0[1] != 0.0) && (r0[2] != 0.0);
1485        if !TRACK_UNDERFLOW && dense {
1486            let m00 = r1[1].mul_add(r2[2], -(r1[2] * r2[1]));
1487            let m01 = r1[0].mul_add(r2[2], -(r1[2] * r2[0]));
1488            let m02 = r1[0].mul_add(r2[1], -(r1[1] * r2[0]));
1489            return FilterArithmetic {
1490                value: r0[0].mul_add(m00, (-r0[1]).mul_add(m01, r0[2] * m02)),
1491                underflow_safe: true,
1492            };
1493        }
1494
1495        let mut det = if r0[2] == 0.0 {
1496            FilterArithmetic {
1497                value: 0.0,
1498                underflow_safe: true,
1499            }
1500        } else {
1501            let subtrahend = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[1], r2[0]);
1502            let mut m02 =
1503                FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r1[0], r2[1], -subtrahend.value);
1504            m02.underflow_safe &= subtrahend.underflow_safe;
1505            let mut term = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r0[2], m02.value);
1506            term.underflow_safe &= m02.underflow_safe;
1507            term
1508        };
1509        if r0[1] != 0.0 {
1510            let subtrahend = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[2], r2[0]);
1511            let mut m01 =
1512                FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r1[0], r2[2], -subtrahend.value);
1513            m01.underflow_safe &= subtrahend.underflow_safe;
1514            let prior_safe = det.underflow_safe && m01.underflow_safe;
1515            det = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(-r0[1], m01.value, det.value);
1516            det.underflow_safe &= prior_safe;
1517        }
1518        if r0[0] != 0.0 {
1519            let subtrahend = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[2], r2[1]);
1520            let mut m00 =
1521                FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r1[1], r2[2], -subtrahend.value);
1522            m00.underflow_safe &= subtrahend.underflow_safe;
1523            let prior_safe = det.underflow_safe && m00.underflow_safe;
1524            det = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(r0[0], m00.value, det.value);
1525            det.underflow_safe &= prior_safe;
1526        }
1527        det
1528    }
1529
1530    /// Evaluate a 3×3 absolute permanent while skipping zero coefficients.
1531    ///
1532    /// This mirrors [`det3_elements`](Self::det3_elements) for error-bound
1533    /// computation: absent determinant terms should not force evaluation of an
1534    /// overflowing absolute minor.
1535    #[expect(
1536        clippy::inline_always,
1537        reason = "error-bound call-site specialization avoids tracked-helper overhead"
1538    )]
1539    #[inline(always)]
1540    const fn det3_abs_permanent_elements<const TRACK_UNDERFLOW: bool>(
1541        r0: [f64; 3],
1542        r1: [f64; 3],
1543        r2: [f64; 3],
1544    ) -> FilterArithmetic<TRACK_UNDERFLOW> {
1545        let dense = (r0[0] != 0.0) && (r0[1] != 0.0) && (r0[2] != 0.0);
1546        if !TRACK_UNDERFLOW && dense {
1547            let pm00 = (r1[1] * r2[2]).abs() + (r1[2] * r2[1]).abs();
1548            let pm01 = (r1[0] * r2[2]).abs() + (r1[2] * r2[0]).abs();
1549            let pm02 = (r1[0] * r2[1]).abs() + (r1[1] * r2[0]).abs();
1550            return FilterArithmetic {
1551                value: r0[2]
1552                    .abs()
1553                    .mul_add(pm02, r0[1].abs().mul_add(pm01, r0[0].abs() * pm00)),
1554                underflow_safe: true,
1555            };
1556        }
1557
1558        let mut permanent = if r0[2] == 0.0 {
1559            FilterArithmetic {
1560                value: 0.0,
1561                underflow_safe: true,
1562            }
1563        } else {
1564            let product_0 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[0], r2[1]);
1565            let product_1 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[1], r2[0]);
1566            let mut pm02 = FilterArithmetic::<TRACK_UNDERFLOW>::add_non_negative(
1567                product_0.value.abs(),
1568                product_1.value.abs(),
1569            );
1570            pm02.underflow_safe &= product_0.underflow_safe && product_1.underflow_safe;
1571            let mut term = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r0[2].abs(), pm02.value);
1572            term.underflow_safe &= pm02.underflow_safe;
1573            term
1574        };
1575        if r0[1] != 0.0 {
1576            let product_0 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[0], r2[2]);
1577            let product_1 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[2], r2[0]);
1578            let mut pm01 = FilterArithmetic::<TRACK_UNDERFLOW>::add_non_negative(
1579                product_0.value.abs(),
1580                product_1.value.abs(),
1581            );
1582            pm01.underflow_safe &= product_0.underflow_safe && product_1.underflow_safe;
1583            let prior_safe = permanent.underflow_safe && pm01.underflow_safe;
1584            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1585                r0[1].abs(),
1586                pm01.value,
1587                permanent.value,
1588            );
1589            permanent.underflow_safe &= prior_safe;
1590        }
1591        if r0[0] != 0.0 {
1592            let product_0 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[1], r2[2]);
1593            let product_1 = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(r1[2], r2[1]);
1594            let mut pm00 = FilterArithmetic::<TRACK_UNDERFLOW>::add_non_negative(
1595                product_0.value.abs(),
1596                product_1.value.abs(),
1597            );
1598            pm00.underflow_safe &= product_0.underflow_safe && product_1.underflow_safe;
1599            let prior_safe = permanent.underflow_safe && pm00.underflow_safe;
1600            permanent = FilterArithmetic::<TRACK_UNDERFLOW>::mul_add(
1601                r0[0].abs(),
1602                pm00.value,
1603                permanent.value,
1604            );
1605            permanent.underflow_safe &= prior_safe;
1606        }
1607        permanent
1608    }
1609
1610    /// Finish a determinant error bound only when its full arithmetic tree is
1611    /// outside the gradual-underflow regime.
1612    const fn certified_error_bound<const TRACK_UNDERFLOW: bool>(
1613        coefficient: f64,
1614        permanent: FilterArithmetic<TRACK_UNDERFLOW>,
1615    ) -> Result<Option<f64>, LaError> {
1616        let mut bound = FilterArithmetic::<TRACK_UNDERFLOW>::multiply(coefficient, permanent.value);
1617        bound.underflow_safe &= permanent.underflow_safe;
1618        if bound.underflow_safe {
1619            Self::computed_scalar_result(ArithmeticOperation::DeterminantErrorBound, bound.value)
1620        } else {
1621            cold_path();
1622            Ok(None)
1623        }
1624    }
1625
1626    /// Return a computed scalar result for a matrix with finite stored entries.
1627    const fn computed_scalar_result(
1628        operation: ArithmeticOperation,
1629        value: f64,
1630    ) -> Result<Option<f64>, LaError> {
1631        if value.is_finite() {
1632            Ok(Some(value))
1633        } else {
1634            Err(LaError::non_finite_computation_scalar(operation))
1635        }
1636    }
1637}
1638
1639impl<const D: usize> Default for Matrix<D> {
1640    #[inline]
1641    fn default() -> Self {
1642        Self::zero()
1643    }
1644}
1645
1646#[cfg(all(doc, feature = "exact"))]
1647mod det_errbound_doctests {
1648    /// ```rust
1649    /// use la_stack::prelude::*;
1650    ///
1651    /// fn adaptive_det_sign<const D: usize>(
1652    ///     matrix: &Matrix<D>,
1653    /// ) -> DeterminantSign {
1654    ///     if let Ok(Some(estimate)) = matrix.det_direct_with_errbound() {
1655    ///         if estimate.determinant().abs() > estimate.absolute_error_bound() {
1656    ///             return if estimate.determinant() > 0.0 {
1657    ///                 DeterminantSign::Positive
1658    ///             } else {
1659    ///                 DeterminantSign::Negative
1660    ///             };
1661    ///         }
1662    ///     }
1663    ///
1664    ///     matrix.det_sign_exact()
1665    /// }
1666    ///
1667    /// # fn main() -> Result<(), LaError> {
1668    /// let identity = Matrix::<3>::identity();
1669    /// assert_eq!(
1670    ///     adaptive_det_sign(&identity),
1671    ///     DeterminantSign::Positive
1672    /// );
1673    ///
1674    /// let singular = Matrix::<3>::try_from_rows([
1675    ///     [1.0, 2.0, 3.0],
1676    ///     [4.0, 5.0, 6.0],
1677    ///     [7.0, 8.0, 9.0],
1678    /// ])?;
1679    /// assert_eq!(adaptive_det_sign(&singular), DeterminantSign::Zero);
1680    ///
1681    /// let big = f64::MAX / 2.0;
1682    /// let overflowing = Matrix::<3>::try_from_rows([
1683    ///     [0.0, 0.0, 1.0],
1684    ///     [big, 0.0, 1.0],
1685    ///     [0.0, big, 1.0],
1686    /// ])?;
1687    /// assert_eq!(
1688    ///     adaptive_det_sign(&overflowing),
1689    ///     DeterminantSign::Positive
1690    /// );
1691    /// # Ok(())
1692    /// # }
1693    /// ```
1694    fn adaptive_precision_pattern() {}
1695}
1696
1697#[cfg(test)]
1698mod tests {
1699    use core::hint::black_box;
1700
1701    use approx::assert_abs_diff_eq;
1702    use pastey::paste;
1703
1704    use super::*;
1705    use crate::{DEFAULT_SINGULAR_TOL, FactorizationKind, Vector};
1706
1707    macro_rules! gen_matrix_tests {
1708        ($d:literal) => {
1709            paste! {
1710                #[test]
1711                fn [<matrix_try_from_rows_get_set_bounds_checked_ $d d>]() {
1712                    let mut rows = [[0.0f64; $d]; $d];
1713                    rows[0][0] = 1.0;
1714                    rows[$d - 1][$d - 1] = -2.0;
1715
1716                    let mut m = Matrix::<$d>::try_from_rows(rows).unwrap();
1717
1718                    assert_eq!(m.get(0, 0), Some(1.0));
1719                    assert_eq!(m.get($d - 1, $d - 1), Some(-2.0));
1720                    assert_eq!(m.try_get(0, 0), Ok(1.0));
1721                    assert_eq!(m.try_get($d - 1, $d - 1), Ok(-2.0));
1722
1723                    // Out-of-bounds is None.
1724                    assert_eq!(m.get($d, 0), None);
1725                    assert_eq!(
1726                        m.try_get($d, 0),
1727                        Err(LaError::IndexOutOfBounds {
1728                            row: $d,
1729                            col: 0,
1730                            dim: $d,
1731                        })
1732                    );
1733
1734                    // Out-of-bounds set fails.
1735                    let before_failed_set = m;
1736                    assert_eq!(
1737                        m.set($d, 0, 3.0),
1738                        Err(LaError::IndexOutOfBounds {
1739                            row: $d,
1740                            col: 0,
1741                            dim: $d,
1742                        })
1743                    );
1744                    assert_eq!(m, before_failed_set);
1745                    assert_eq!(
1746                        m.set(0, $d, 3.0),
1747                        Err(LaError::IndexOutOfBounds {
1748                            row: 0,
1749                            col: $d,
1750                            dim: $d,
1751                        })
1752                    );
1753                    assert_eq!(m, before_failed_set);
1754                    assert_eq!(m.get(0, 0), Some(1.0));
1755
1756                    // In-bounds set works.
1757                    assert_eq!(m.set(0, $d - 1, 3.0), Ok(()));
1758                    assert_eq!(m.get(0, $d - 1), Some(3.0));
1759                    assert_eq!(m.set($d - 1, 0, 4.0), Ok(()));
1760                    assert_eq!(m.try_get($d - 1, 0), Ok(4.0));
1761                }
1762
1763                #[test]
1764                fn [<matrix_set_rejects_non_finite_and_preserves_storage_ $d d>]() {
1765                    for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1766                        let mut m = Matrix::<$d>::identity();
1767                        let before = m;
1768                        assert_eq!(
1769                            m.set($d - 1, 0, value),
1770                            Err(LaError::non_finite_input_matrix($d - 1, 0))
1771                        );
1772                        assert_eq!(m, before);
1773                    }
1774                }
1775
1776                #[test]
1777                fn [<matrix_try_from_rows_rejects_non_finite_ $d d>]() {
1778                    for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
1779                        let mut rows = [[0.0f64; $d]; $d];
1780                        rows[$d - 1][$d - 1] = value;
1781                        assert_eq!(
1782                            Matrix::<$d>::try_from_rows(rows),
1783                            Err(LaError::non_finite_input_matrix($d - 1, $d - 1))
1784                        );
1785                    }
1786
1787                    let mut rows = [[0.0f64; $d]; $d];
1788                    rows[0][$d - 1] = f64::INFINITY;
1789                    rows[$d - 1][0] = f64::NAN;
1790                    assert_eq!(
1791                        Matrix::<$d>::try_from_rows(rows),
1792                        Err(LaError::non_finite_input_matrix(0, $d - 1))
1793                    );
1794                }
1795
1796                #[test]
1797                fn [<matrix_zero_and_default_are_zero_ $d d>]() {
1798                    let z = Matrix::<$d>::zero();
1799                    assert_abs_diff_eq!(z.inf_norm().unwrap(), 0.0, epsilon = 0.0);
1800
1801                    let d = Matrix::<$d>::default();
1802                    assert_abs_diff_eq!(d.inf_norm().unwrap(), 0.0, epsilon = 0.0);
1803                }
1804
1805                #[test]
1806                fn [<matrix_inf_norm_max_row_sum_ $d d>]() {
1807                    let mut rows = [[0.0f64; $d]; $d];
1808
1809                    // Row 0 has a smaller absolute row sum.
1810                    for c in 0..$d {
1811                        rows[0][c] = 0.5;
1812                    }
1813
1814                    // The last row has absolute row sum = D.
1815                    for c in 0..$d {
1816                        rows[$d - 1][c] = -1.0;
1817                    }
1818
1819                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1820                    assert_abs_diff_eq!(m.inf_norm().unwrap(), f64::from($d), epsilon = 0.0);
1821                }
1822
1823                #[test]
1824                fn [<matrix_inf_norm_reports_first_overflowing_column_ $d d>]() {
1825                    let mut rows = [[0.0f64; $d]; $d];
1826                    rows[$d - 1][0] = f64::MAX;
1827                    rows[$d - 1][1] = f64::MAX;
1828
1829                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1830                    assert_eq!(
1831                        m.inf_norm(),
1832                        Err(LaError::non_finite_computation_matrix(
1833                            ArithmeticOperation::MatrixInfinityNorm,
1834                            $d - 1,
1835                            1,
1836                        ))
1837                    );
1838                }
1839
1840                #[test]
1841                fn [<matrix_inf_norm_reports_first_overflowing_row_ $d d>]() {
1842                    let mut rows = [[0.0f64; $d]; $d];
1843                    rows[0][0] = f64::MAX;
1844                    rows[0][$d - 1] = f64::MAX;
1845                    rows[$d - 1][0] = f64::MAX;
1846                    rows[$d - 1][1] = f64::MAX;
1847
1848                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
1849                    assert_eq!(
1850                        m.inf_norm(),
1851                        Err(LaError::non_finite_computation_matrix(
1852                            ArithmeticOperation::MatrixInfinityNorm,
1853                            0,
1854                            $d - 1,
1855                        ))
1856                    );
1857                }
1858
1859                #[test]
1860                fn [<matrix_identity_lu_det_solve_ $d d>]() {
1861                    let m = Matrix::<$d>::identity();
1862
1863                    // Identity has ones on diag and zeros off diag.
1864                    for r in 0..$d {
1865                        for c in 0..$d {
1866                            let expected = if r == c { 1.0 } else { 0.0 };
1867                            assert_abs_diff_eq!(m.get(r, c).unwrap(), expected, epsilon = 0.0);
1868                        }
1869                    }
1870
1871                    // Determinant is 1.
1872                    let det = m.det().unwrap();
1873                    assert_abs_diff_eq!(det, 1.0, epsilon = 1e-12);
1874
1875                    // LU solve on identity returns the RHS.
1876                    let lu = m.lu(DEFAULT_SINGULAR_TOL).unwrap();
1877
1878                    let b_arr = {
1879                        let mut arr = [0.0f64; $d];
1880                        let values = [1.0f64, 2.0, 3.0, 4.0, 5.0];
1881                        for (dst, src) in arr.iter_mut().zip(values.iter()) {
1882                            *dst = *src;
1883                        }
1884                        arr
1885                    };
1886
1887                    let b = Vector::<$d>::new(b_arr);
1888                    let x = lu.solve(b).unwrap().into_array();
1889
1890                    for (x_i, b_i) in x.iter().zip(b_arr.iter()) {
1891                        assert_abs_diff_eq!(*x_i, *b_i, epsilon = 1e-12);
1892                    }
1893                }
1894
1895            }
1896        };
1897    }
1898
1899    // Mirror delaunay-style multi-dimension tests.
1900    gen_matrix_tests!(2);
1901    gen_matrix_tests!(3);
1902    gen_matrix_tests!(4);
1903    gen_matrix_tests!(5);
1904
1905    #[test]
1906    fn matrix_inf_norm_preserves_left_to_right_row_sum_order() {
1907        let large = 9_007_199_254_740_992.0;
1908        let matrix =
1909            Matrix::<4>::try_from_rows([[large, 1.0, 1.0, 1.0], [0.0; 4], [0.0; 4], [0.0; 4]])
1910                .unwrap();
1911
1912        assert_eq!(matrix.inf_norm(), Ok(large));
1913    }
1914
1915    // === det_direct tests ===
1916
1917    #[test]
1918    fn det_direct_d0_is_one() {
1919        assert_eq!(Matrix::<0>::zero().det_direct(), Ok(Some(1.0)));
1920    }
1921
1922    #[test]
1923    fn det_direct_d1_returns_element() {
1924        let m = Matrix::<1>::try_from_rows([[42.0]]).unwrap();
1925        assert_eq!(m.det_direct(), Ok(Some(42.0)));
1926    }
1927
1928    #[test]
1929    fn det_direct_d2_known_value() {
1930        // [[1,2],[3,4]] → det = 1*4 - 2*3 = -2
1931        // black_box prevents compile-time constant folding of the const fn.
1932        let m = black_box(Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap());
1933        assert_abs_diff_eq!(m.det_direct().unwrap().unwrap(), -2.0, epsilon = 1e-15);
1934    }
1935
1936    #[test]
1937    fn det_direct_d3_known_value() {
1938        // Classic 3×3: det = 0
1939        let m = black_box(
1940            Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
1941                .unwrap(),
1942        );
1943        assert_abs_diff_eq!(m.det_direct().unwrap().unwrap(), 0.0, epsilon = 1e-12);
1944    }
1945
1946    #[test]
1947    fn det_direct_d3_dense_known_value() {
1948        // det = 1*(5*8 - 7*6) - 2*(4*8 - 7*2) + 3*(4*6 - 5*2) = 4
1949        let m = black_box(
1950            Matrix::<3>::try_from_rows([[1.0, 2.0, 3.0], [4.0, 5.0, 7.0], [2.0, 6.0, 8.0]])
1951                .unwrap(),
1952        );
1953        let direct = m.det_direct().unwrap().unwrap();
1954        let paired = m.det_direct_with_errbound().unwrap().unwrap();
1955
1956        assert_abs_diff_eq!(direct, 4.0, epsilon = 1e-12);
1957        assert_eq!(paired.determinant().to_bits(), direct.to_bits());
1958    }
1959
1960    #[test]
1961    fn det_direct_d3_dense_reports_legitimate_overflow() {
1962        // The unscaled matrix has determinant 54, so scaling every entry by
1963        // 1.6e102 gives a determinant of approximately 2.21e308.
1964        let scale = 1.6e102;
1965        let m = black_box(
1966            Matrix::<3>::try_from_rows([
1967                [4.0 * scale, scale, scale],
1968                [scale, 4.0 * scale, scale],
1969                [scale, scale, 4.0 * scale],
1970            ])
1971            .unwrap(),
1972        );
1973        let expected = LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant);
1974
1975        assert_eq!(m.det_direct(), Err(expected));
1976        assert_eq!(m.det(), Err(expected));
1977    }
1978
1979    #[test]
1980    fn det_errbound_d3_dense_reports_legitimate_overflow() {
1981        // The unscaled matrix has determinant 54, so scaling every entry by
1982        // 1.6e102 gives a determinant of approximately 2.21e308.
1983        let scale = 1.6e102;
1984        let m = black_box(
1985            Matrix::<3>::try_from_rows([
1986                [4.0 * scale, scale, scale],
1987                [scale, 4.0 * scale, scale],
1988                [scale, scale, 4.0 * scale],
1989            ])
1990            .unwrap(),
1991        );
1992        let expected =
1993            LaError::non_finite_computation_scalar(ArithmeticOperation::DeterminantErrorBound);
1994
1995        assert_eq!(m.det_errbound(), Err(expected));
1996        assert_eq!(m.det_direct_with_errbound(), Err(expected));
1997    }
1998
1999    #[test]
2000    fn det_direct_d3_nonsingular() {
2001        // [[2,1,0],[0,3,1],[1,0,2]] → det = 2*(6-0) - 1*(0-1) + 0 = 13
2002        let m = black_box(
2003            Matrix::<3>::try_from_rows([[2.0, 1.0, 0.0], [0.0, 3.0, 1.0], [1.0, 0.0, 2.0]])
2004                .unwrap(),
2005        );
2006        assert_abs_diff_eq!(m.det_direct().unwrap().unwrap(), 13.0, epsilon = 1e-12);
2007    }
2008
2009    #[test]
2010    fn det_direct_d3_skips_zero_coefficient_minor_that_would_overflow() {
2011        let m = black_box(
2012            Matrix::<3>::try_from_rows([
2013                [1.0, 0.0, 0.0],
2014                [1.0e300, 1.0, 1.0e300],
2015                [1.0e300, 0.0, 1.0e300],
2016            ])
2017            .unwrap(),
2018        );
2019        assert_eq!(m.det_direct(), Ok(Some(1.0e300)));
2020    }
2021
2022    #[test]
2023    fn det_direct_d4_known_value() {
2024        // Diagonal matrix: det = product of diagonal entries.
2025        let mut rows = [[0.0f64; 4]; 4];
2026        rows[0][0] = 2.0;
2027        rows[1][1] = 3.0;
2028        rows[2][2] = 5.0;
2029        rows[3][3] = 7.0;
2030        let m = black_box(Matrix::<4>::try_from_rows(rows).unwrap());
2031        assert_abs_diff_eq!(m.det_direct().unwrap().unwrap(), 210.0, epsilon = 1e-12);
2032    }
2033
2034    #[test]
2035    fn det_direct_d4_dense_known_value() {
2036        let m = black_box(
2037            Matrix::<4>::try_from_rows([
2038                [4.0, 1.0, 3.0, 2.0],
2039                [1.0, 5.0, 2.0, 1.0],
2040                [7.0, 2.0, 6.0, 3.0],
2041                [1.0, 8.0, 4.0, 9.0],
2042            ])
2043            .unwrap(),
2044        );
2045        let direct = m.det_direct().unwrap().unwrap();
2046        let paired = m.det_direct_with_errbound().unwrap().unwrap();
2047
2048        assert_abs_diff_eq!(direct, 112.0, epsilon = 1e-12);
2049        assert_eq!(paired.determinant().to_bits(), direct.to_bits());
2050    }
2051
2052    #[test]
2053    fn det_direct_d4_dense_reports_legitimate_overflow() {
2054        // The unscaled matrix has determinant 189, so scaling every entry by
2055        // 3.2e76 gives a determinant of approximately 1.98e308.
2056        let scale = 3.2e76;
2057        let m = black_box(
2058            Matrix::<4>::try_from_rows([
2059                [4.0 * scale, scale, scale, scale],
2060                [scale, 4.0 * scale, scale, scale],
2061                [scale, scale, 4.0 * scale, scale],
2062                [scale, scale, scale, 4.0 * scale],
2063            ])
2064            .unwrap(),
2065        );
2066        let expected = LaError::non_finite_computation_scalar(ArithmeticOperation::Determinant);
2067
2068        assert_eq!(m.det_direct(), Err(expected));
2069        assert_eq!(m.det(), Err(expected));
2070    }
2071
2072    #[test]
2073    fn det_errbound_d4_dense_reports_legitimate_overflow() {
2074        // The unscaled matrix has determinant 189, so scaling every entry by
2075        // 3.2e76 gives a determinant of approximately 1.98e308.
2076        let scale = 3.2e76;
2077        let m = black_box(
2078            Matrix::<4>::try_from_rows([
2079                [4.0 * scale, scale, scale, scale],
2080                [scale, 4.0 * scale, scale, scale],
2081                [scale, scale, 4.0 * scale, scale],
2082                [scale, scale, scale, 4.0 * scale],
2083            ])
2084            .unwrap(),
2085        );
2086        let expected =
2087            LaError::non_finite_computation_scalar(ArithmeticOperation::DeterminantErrorBound);
2088
2089        assert_eq!(m.det_errbound(), Err(expected));
2090        assert_eq!(m.det_direct_with_errbound(), Err(expected));
2091    }
2092
2093    #[test]
2094    fn det_direct_d4_skips_zero_coefficient_cofactors_that_would_overflow() {
2095        let m = black_box(
2096            Matrix::<4>::try_from_rows([
2097                [0.0, 0.0, 0.0, 0.0],
2098                [0.0, 0.0, 0.0, 0.0],
2099                [1.0e300, 0.0, 1.0e300, 1.0e300],
2100                [1.0e300, 0.0, 1.0e300, -1.0e300],
2101            ])
2102            .unwrap(),
2103        );
2104        assert_eq!(m.det_direct(), Ok(Some(0.0)));
2105    }
2106
2107    #[test]
2108    fn det_direct_d4_sparse_second_row_skips_inactive_overflowing_minors() {
2109        let m = black_box(
2110            Matrix::<4>::try_from_rows([
2111                [1.0e-300, 1.0, 1.0, 1.0],
2112                [0.0, 1.0, 0.0, 0.0],
2113                [0.0, 1.0e300, 1.0, 1.0e300],
2114                [0.0, 1.0e300, 0.0, 1.0e300],
2115            ])
2116            .unwrap(),
2117        );
2118
2119        assert_eq!(m.det_direct(), Ok(Some(1.0)));
2120        assert_eq!(m.det(), Ok(1.0));
2121    }
2122
2123    #[test]
2124    fn det_direct_d5_returns_none() {
2125        assert_eq!(Matrix::<5>::identity().det_direct(), Ok(None));
2126    }
2127
2128    #[test]
2129    fn det_direct_d8_returns_none() {
2130        assert_eq!(Matrix::<8>::zero().det_direct(), Ok(None));
2131    }
2132
2133    #[test]
2134    fn det_direct_rejects_computed_overflow() {
2135        let m = Matrix::<2>::try_from_rows([[1e300, 0.0], [0.0, 1e300]]).unwrap();
2136        assert_eq!(
2137            m.det_direct(),
2138            Err(LaError::non_finite_computation_scalar(
2139                ArithmeticOperation::Determinant
2140            ))
2141        );
2142    }
2143
2144    #[test]
2145    fn det_d5_rejects_lu_product_overflow() {
2146        let m = Matrix::<5>::try_from_rows([
2147            [1.0e100, 0.0, 0.0, 0.0, 0.0],
2148            [0.0, 1.0e100, 0.0, 0.0, 0.0],
2149            [0.0, 0.0, 1.0e100, 0.0, 0.0],
2150            [0.0, 0.0, 0.0, 1.0e100, 0.0],
2151            [0.0, 0.0, 0.0, 0.0, 1.0e100],
2152        ])
2153        .unwrap();
2154        assert_eq!(
2155            m.det(),
2156            Err(LaError::non_finite_computation_step(
2157                ArithmeticOperation::Determinant,
2158                4
2159            ))
2160        );
2161    }
2162
2163    #[test]
2164    fn det_d5_rejects_lu_trailing_update_overflow() {
2165        let m = Matrix::<5>::try_from_rows([
2166            [1.0, f64::MAX, 0.0, 0.0, 0.0],
2167            [-1.0, f64::MAX, 0.0, 0.0, 0.0],
2168            [0.0, 0.0, 1.0, 0.0, 0.0],
2169            [0.0, 0.0, 0.0, 1.0, 0.0],
2170            [0.0, 0.0, 0.0, 0.0, 1.0],
2171        ])
2172        .unwrap();
2173
2174        assert_eq!(
2175            m.det(),
2176            Err(LaError::non_finite_computation_matrix(
2177                ArithmeticOperation::LuFactorization,
2178                1,
2179                1
2180            ))
2181        );
2182    }
2183
2184    macro_rules! gen_det_direct_agrees_with_lu {
2185        ($d:literal) => {
2186            paste! {
2187                #[test]
2188                #[expect(
2189                    clippy::cast_precision_loss,
2190                    reason = "r, c, and D are tiny test integers exactly representable as f64"
2191                )]
2192                fn [<det_direct_agrees_with_lu_ $d d>]() {
2193                    // Well-conditioned matrix: diagonally dominant.
2194                    let mut rows = [[0.0f64; $d]; $d];
2195                    for r in 0..$d {
2196                        for c in 0..$d {
2197                            rows[r][c] = if r == c {
2198                                (r as f64) + f64::from($d) + 1.0
2199                            } else {
2200                                0.1 / ((r + c + 1) as f64)
2201                            };
2202                        }
2203                    }
2204                    let m = Matrix::<$d>::try_from_rows(rows).unwrap();
2205                    let direct = m.det_direct().unwrap().unwrap();
2206                    let lu_det = m.lu(DEFAULT_SINGULAR_TOL).unwrap().det().unwrap();
2207                    let eps = lu_det.abs().mul_add(1e-12, 1e-12);
2208                    assert_abs_diff_eq!(direct, lu_det, epsilon = eps);
2209                }
2210            }
2211        };
2212    }
2213
2214    gen_det_direct_agrees_with_lu!(1);
2215    gen_det_direct_agrees_with_lu!(2);
2216    gen_det_direct_agrees_with_lu!(3);
2217    gen_det_direct_agrees_with_lu!(4);
2218
2219    #[test]
2220    fn det_direct_identity_all_dims() {
2221        assert_abs_diff_eq!(
2222            Matrix::<1>::identity().det_direct().unwrap().unwrap(),
2223            1.0,
2224            epsilon = 0.0
2225        );
2226        assert_abs_diff_eq!(
2227            Matrix::<2>::identity().det_direct().unwrap().unwrap(),
2228            1.0,
2229            epsilon = 0.0
2230        );
2231        assert_abs_diff_eq!(
2232            Matrix::<3>::identity().det_direct().unwrap().unwrap(),
2233            1.0,
2234            epsilon = 0.0
2235        );
2236        assert_abs_diff_eq!(
2237            Matrix::<4>::identity().det_direct().unwrap().unwrap(),
2238            1.0,
2239            epsilon = 0.0
2240        );
2241    }
2242
2243    #[test]
2244    fn det_direct_zero_matrix() {
2245        assert_abs_diff_eq!(
2246            Matrix::<2>::zero().det_direct().unwrap().unwrap(),
2247            0.0,
2248            epsilon = 0.0
2249        );
2250        assert_abs_diff_eq!(
2251            Matrix::<3>::zero().det_direct().unwrap().unwrap(),
2252            0.0,
2253            epsilon = 0.0
2254        );
2255        assert_abs_diff_eq!(
2256            Matrix::<4>::zero().det_direct().unwrap().unwrap(),
2257            0.0,
2258            epsilon = 0.0
2259        );
2260    }
2261
2262    macro_rules! gen_det_singular_zero_matrix_tests {
2263        ($d:literal) => {
2264            paste! {
2265                #[test]
2266                fn [<det_singular_zero_matrix_returns_zero_ $d d>]() {
2267                    assert_abs_diff_eq!(
2268                        Matrix::<$d>::zero().det().unwrap(),
2269                        0.0,
2270                        epsilon = 0.0
2271                    );
2272                }
2273            }
2274        };
2275    }
2276
2277    gen_det_singular_zero_matrix_tests!(2);
2278    gen_det_singular_zero_matrix_tests!(3);
2279    gen_det_singular_zero_matrix_tests!(4);
2280
2281    #[test]
2282    fn det_singular_zero_matrix_d5_preserves_lu_error() {
2283        assert_eq!(
2284            Matrix::<5>::zero().det(),
2285            Err(LaError::singular_numerical(
2286                0,
2287                FactorizationKind::Lu,
2288                0.0,
2289                0.0
2290            ))
2291        );
2292    }
2293
2294    #[test]
2295    fn det_d5_does_not_turn_elimination_underflow_into_exact_zero() {
2296        let min_subnormal = f64::from_bits(1);
2297        let two_pow_800 = f64::from_bits(1823_u64 << 52);
2298        let m = Matrix::<5>::try_from_rows([
2299            [2.0, min_subnormal, 0.0, 0.0, 0.0],
2300            [1.0, 0.0, 0.0, 0.0, 0.0],
2301            [0.0, 0.0, two_pow_800, 0.0, 0.0],
2302            [0.0, 0.0, 0.0, 1.0, 0.0],
2303            [0.0, 0.0, 0.0, 0.0, 1.0],
2304        ])
2305        .unwrap();
2306
2307        assert_eq!(
2308            m.det(),
2309            Err(LaError::singular_numerical(
2310                1,
2311                FactorizationKind::Lu,
2312                0.0,
2313                0.0
2314            ))
2315        );
2316    }
2317
2318    #[test]
2319    fn det_d5_ignores_pivot_tolerance_for_tiny_nonsingular_matrix() {
2320        // A small nonzero determinant is still a determinant. `det` must not
2321        // flatten the value to zero merely because the default LU tolerance
2322        // would reject a pivot this small.
2323        let m = Matrix::<5>::try_from_rows([
2324            [1e-13, 0.0, 0.0, 0.0, 0.0],
2325            [0.0, 1.0, 0.0, 0.0, 0.0],
2326            [0.0, 0.0, 1.0, 0.0, 0.0],
2327            [0.0, 0.0, 0.0, 1.0, 0.0],
2328            [0.0, 0.0, 0.0, 0.0, 1.0],
2329        ])
2330        .unwrap();
2331
2332        assert_abs_diff_eq!(m.det().unwrap(), 1e-13, epsilon = 0.0);
2333        assert_eq!(
2334            m.lu(DEFAULT_SINGULAR_TOL),
2335            Err(LaError::singular_numerical(
2336                0,
2337                FactorizationKind::Lu,
2338                1e-13,
2339                DEFAULT_SINGULAR_TOL.get()
2340            ))
2341        );
2342    }
2343
2344    #[test]
2345    fn det_returns_non_finite_error_for_overflow_with_finite_entries() {
2346        // det_direct produces an overflowing f64 (1e300 * 1e300 = ∞) even
2347        // though every matrix entry is finite. The entry scan in `det`
2348        // falls through and reports a computed determinant overflow rather
2349        // than a NaN/∞ input.
2350        let m = Matrix::<2>::try_from_rows([[1e300, 0.0], [0.0, 1e300]]).unwrap();
2351        assert_eq!(
2352            m.det(),
2353            Err(LaError::non_finite_computation_scalar(
2354                ArithmeticOperation::Determinant
2355            ))
2356        );
2357    }
2358
2359    // === det_direct const-evaluability tests (D = 2..=5) ===
2360    //
2361    // Every dimension hits a distinct arm of the `match D { … }` body inside
2362    // `det_direct`, so exercising each at compile time is the tightest
2363    // const-fn proof available.
2364
2365    macro_rules! gen_det_direct_const_eval_tests {
2366        ($d:literal) => {
2367            paste! {
2368                /// `Matrix::<D>::det_direct()` on the identity must const-evaluate
2369                /// to `Ok(Some(1.0))` for every closed-form dimension `D ∈ {1, 2, 3, 4}`.
2370                #[test]
2371                fn [<det_direct_const_eval_ $d d>]() {
2372                    const DET: Result<Option<f64>, LaError> = Matrix::<$d>::identity().det_direct();
2373                    assert_eq!(DET, Ok(Some(1.0)));
2374                }
2375            }
2376        };
2377    }
2378
2379    gen_det_direct_const_eval_tests!(2);
2380    gen_det_direct_const_eval_tests!(3);
2381    gen_det_direct_const_eval_tests!(4);
2382
2383    #[test]
2384    fn det_direct_const_eval_d5_is_none() {
2385        // D ≥ 5 has no closed-form arm; `det_direct` returns `Ok(None)`.  Verify
2386        // that the wildcard arm is reachable in a `const { … }` context.
2387        const DET: Result<Option<f64>, LaError> = Matrix::<5>::identity().det_direct();
2388        assert_eq!(DET, Ok(None));
2389    }
2390
2391    // === det_errbound tests (no `exact` feature required) ===
2392
2393    #[test]
2394    fn det_errbound_matches_documented_coefficient_scale() {
2395        let m2 = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2396        let expected_2 = ERR_COEFF_2 * ((1.0_f64 * 4.0).abs() + (2.0_f64 * 3.0).abs());
2397        assert_abs_diff_eq!(
2398            m2.det_errbound().unwrap().unwrap(),
2399            expected_2,
2400            epsilon = 0.0
2401        );
2402
2403        assert_abs_diff_eq!(
2404            Matrix::<3>::identity().det_errbound().unwrap().unwrap(),
2405            ERR_COEFF_3,
2406            epsilon = 0.0
2407        );
2408        assert_abs_diff_eq!(
2409            Matrix::<4>::identity().det_errbound().unwrap().unwrap(),
2410            ERR_COEFF_4,
2411            epsilon = 0.0
2412        );
2413    }
2414
2415    #[test]
2416    fn det_errbound_d3_skips_zero_coefficient_minor_that_would_overflow() {
2417        let m = Matrix::<3>::try_from_rows([
2418            [1.0, 0.0, 0.0],
2419            [1.0e300, 1.0, 1.0e300],
2420            [1.0e300, 0.0, 1.0e300],
2421        ])
2422        .unwrap();
2423
2424        assert_eq!(m.det_errbound(), Ok(Some(ERR_COEFF_3 * 1.0e300)));
2425    }
2426
2427    #[test]
2428    fn det_errbound_d4_skips_zero_coefficient_cofactors_that_would_overflow() {
2429        let m = Matrix::<4>::try_from_rows([
2430            [0.0, 0.0, 0.0, 0.0],
2431            [0.0, 0.0, 0.0, 0.0],
2432            [1.0e300, 0.0, 1.0e300, 1.0e300],
2433            [1.0e300, 0.0, 1.0e300, -1.0e300],
2434        ])
2435        .unwrap();
2436
2437        assert_eq!(m.det_errbound(), Ok(Some(0.0)));
2438    }
2439
2440    #[test]
2441    fn det_errbound_d5_returns_none() {
2442        // D=5 has no fast filter
2443        assert_eq!(Matrix::<5>::identity().det_errbound(), Ok(None));
2444    }
2445
2446    #[test]
2447    fn combined_det_bound_wide_exponent_fast_path_matches_tracked_arithmetic() {
2448        let threshold = f64::from_bits(1007_u64 << 52); // 2^-16
2449        let at_threshold = Matrix::<2>::try_from_rows([[threshold, 0.0], [0.0, 2.0]]).unwrap();
2450        assert!(at_threshold.det_bound_inputs_have_wide_exponent_margin());
2451
2452        let tracked = at_threshold
2453            .det_direct_with_errbound_from_arithmetic(
2454                at_threshold
2455                    .det_direct_arithmetic::<true>()
2456                    .expect("D=2 has direct arithmetic"),
2457            )
2458            .unwrap();
2459        assert_eq!(at_threshold.det_direct_with_errbound().unwrap(), tracked);
2460
2461        let just_below = f64::from_bits(threshold.to_bits() - 1);
2462        let below_threshold = Matrix::<2>::try_from_rows([[just_below, 0.0], [0.0, 2.0]]).unwrap();
2463        assert!(!below_threshold.det_bound_inputs_have_wide_exponent_margin());
2464        assert!(!Matrix::<5>::identity().det_bound_inputs_have_wide_exponent_margin());
2465    }
2466
2467    #[test]
2468    fn det_direct_with_errbound_covers_zero_and_one_dimensions() {
2469        let empty = Matrix::<0>::zero()
2470            .det_direct_with_errbound()
2471            .unwrap()
2472            .unwrap();
2473        assert_abs_diff_eq!(empty.determinant(), 1.0, epsilon = 0.0);
2474        assert_abs_diff_eq!(empty.absolute_error_bound(), 0.0, epsilon = 0.0);
2475
2476        let scalar = Matrix::<1>::try_from_rows([[-7.0]])
2477            .unwrap()
2478            .det_direct_with_errbound()
2479            .unwrap()
2480            .unwrap();
2481        assert_abs_diff_eq!(scalar.determinant(), -7.0, epsilon = 0.0);
2482        assert_abs_diff_eq!(scalar.absolute_error_bound(), 0.0, epsilon = 0.0);
2483    }
2484
2485    #[test]
2486    fn det_direct_with_errbound_pairs_the_closed_form_values() {
2487        let matrix = Matrix::<2>::try_from_rows([[1.0, 2.0], [3.0, 4.0]]).unwrap();
2488        let estimate = matrix.det_direct_with_errbound().unwrap().unwrap();
2489
2490        assert_abs_diff_eq!(
2491            estimate.determinant(),
2492            matrix.det_direct().unwrap().unwrap(),
2493            epsilon = 0.0
2494        );
2495        assert_abs_diff_eq!(
2496            estimate.absolute_error_bound(),
2497            ERR_COEFF_2 * (4.0_f64 + 6.0_f64),
2498            epsilon = 0.0
2499        );
2500    }
2501
2502    #[test]
2503    fn det_direct_with_errbound_d5_returns_none() {
2504        assert_eq!(Matrix::<5>::identity().det_direct_with_errbound(), Ok(None));
2505    }
2506
2507    #[test]
2508    fn det_errbound_rejects_computed_overflow() {
2509        let m = Matrix::<2>::try_from_rows([[1e300, 0.0], [0.0, 1e300]]).unwrap();
2510        assert_eq!(
2511            m.det_errbound(),
2512            Err(LaError::non_finite_computation_scalar(
2513                ArithmeticOperation::DeterminantErrorBound
2514            ))
2515        );
2516    }
2517
2518    // === det_errbound const-evaluability tests (D = 2..=5) ===
2519
2520    macro_rules! gen_det_errbound_const_eval_tests {
2521        ($d:literal) => {
2522            paste! {
2523                /// `Matrix::<D>::det_errbound()` on the identity must const-evaluate
2524                /// to `Ok(Some(bound))` with `bound > 0` for every closed-form dimension
2525                /// `D ∈ {2, 3, 4}`.  Each dimension hits a distinct arm of
2526                /// `det_errbound` with a dimension-specific permanent computation.
2527                #[test]
2528                fn [<det_errbound_const_eval_ $d d>]() {
2529                    const BOUND: Result<Option<f64>, LaError> = Matrix::<$d>::identity().det_errbound();
2530                    assert!(BOUND.unwrap().unwrap() > 0.0);
2531                }
2532            }
2533        };
2534    }
2535
2536    gen_det_errbound_const_eval_tests!(2);
2537    gen_det_errbound_const_eval_tests!(3);
2538    gen_det_errbound_const_eval_tests!(4);
2539
2540    #[test]
2541    fn det_errbound_const_eval_d5_is_none() {
2542        // D ≥ 5 has no fast-filter bound; `det_errbound` returns `Ok(None)`.
2543        const BOUND: Result<Option<f64>, LaError> = Matrix::<5>::identity().det_errbound();
2544        assert_eq!(BOUND, Ok(None));
2545    }
2546
2547    // === inf_norm const-evaluability tests (D = 2..=5) ===
2548
2549    macro_rules! gen_inf_norm_const_eval_tests {
2550        ($d:literal) => {
2551            paste! {
2552                /// `Matrix::<D>::inf_norm()` on the identity must const-evaluate
2553                /// to `1.0` for every `D ≥ 1` — each row has a single `1.0`
2554                /// entry, so the max absolute row sum is exactly `1.0`.
2555                #[test]
2556                fn [<inf_norm_const_eval_ $d d>]() {
2557                    const NORM: Result<f64, LaError> = Matrix::<$d>::identity().inf_norm();
2558                    assert!((NORM.unwrap() - 1.0).abs() <= 1e-12);
2559                }
2560            }
2561        };
2562    }
2563
2564    gen_inf_norm_const_eval_tests!(2);
2565    gen_inf_norm_const_eval_tests!(3);
2566    gen_inf_norm_const_eval_tests!(4);
2567    gen_inf_norm_const_eval_tests!(5);
2568
2569    // === is_symmetric / first_asymmetry (public LDLT preconditions helpers) ===
2570
2571    macro_rules! gen_is_symmetric_tests {
2572        ($d:literal) => {
2573            paste! {
2574                #[test]
2575                fn [<is_symmetric_true_for_identity_ $d d>]() {
2576                    let m = Matrix::<$d>::identity();
2577                    assert!(m.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2578                    assert_eq!(m.first_asymmetry(Tolerance::try_new(1e-12).unwrap()).unwrap(), None);
2579                }
2580
2581                #[test]
2582                fn [<is_symmetric_true_for_zero_ $d d>]() {
2583                    let m = Matrix::<$d>::zero();
2584                    assert!(m.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2585                    assert_eq!(m.first_asymmetry(Tolerance::try_new(1e-12).unwrap()).unwrap(), None);
2586                }
2587
2588                #[test]
2589                fn [<is_symmetric_true_for_constructed_symmetric_ $d d>]() {
2590                    // Construct A = M + Mᵀ so A is provably symmetric.
2591                    let mut m = [[0.0f64; $d]; $d];
2592                    for r in 0..$d {
2593                        for c in 0..$d {
2594                            #[expect(
2595                                clippy::cast_precision_loss,
2596                                reason = "matrix test indices are at most five and exactly representable as f64"
2597                            )]
2598                            {
2599                                m[r][c] = (r * $d + c) as f64;
2600                            }
2601                        }
2602                    }
2603                    let mut sym = [[0.0f64; $d]; $d];
2604                    for r in 0..$d {
2605                        for c in 0..$d {
2606                            sym[r][c] = m[r][c] + m[c][r];
2607                        }
2608                    }
2609                    let a = Matrix::<$d>::try_from_rows(sym).unwrap();
2610                    assert!(a.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2611                    assert_eq!(a.first_asymmetry(Tolerance::try_new(1e-12).unwrap()).unwrap(), None);
2612                }
2613
2614                #[test]
2615                fn [<is_symmetric_false_for_asymmetric_offdiagonal_ $d d>]() {
2616                    // Perturb a single off-diagonal entry so symmetry fails.
2617                    let mut rows = [[0.0f64; $d]; $d];
2618                    for i in 0..$d {
2619                        rows[i][i] = 1.0;
2620                    }
2621                    rows[0][$d - 1] = 1.0;
2622                    rows[$d - 1][0] = -1.0; // breaks symmetry
2623                    let a = Matrix::<$d>::try_from_rows(rows).unwrap();
2624                    assert!(!a.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2625                    assert_eq!(
2626                        a.first_asymmetry(Tolerance::try_new(1e-12).unwrap()).unwrap(),
2627                        Some((0, $d - 1))
2628                    );
2629                }
2630
2631            }
2632        };
2633    }
2634
2635    gen_is_symmetric_tests!(2);
2636    gen_is_symmetric_tests!(3);
2637    gen_is_symmetric_tests!(4);
2638    gen_is_symmetric_tests!(5);
2639
2640    macro_rules! gen_ldlt_symmetry_proof_tests {
2641        ($d:literal) => {
2642            paste! {
2643                #[test]
2644                fn [<matrix_ldlt_accepts_exact_symmetric_spd_ $d d>]() {
2645                    // This exactly mirrored, strictly diagonally dominant
2646                    // tridiagonal matrix is positive definite.
2647                    let mut rows = [[0.0_f64; $d]; $d];
2648                    for (index, row) in rows.iter_mut().enumerate() {
2649                        row[index] = 2.0;
2650                    }
2651                    for index in 1..$d {
2652                        rows[index - 1][index] = 0.5;
2653                        rows[index][index - 1] = 0.5;
2654                    }
2655
2656                    let matrix = Matrix::<$d>::try_from_rows(rows).unwrap();
2657                    let symmetric = SymmetricMatrix::try_new(matrix).unwrap();
2658                    assert_eq!(symmetric.into_matrix(), matrix);
2659
2660                    let ldlt = matrix.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
2661                    assert!(ldlt.det().unwrap() > 0.0);
2662                }
2663
2664                #[test]
2665                fn [<symmetric_matrix_try_new_rejects_finite_asymmetric_ $d d>]() {
2666                    let mut rows = [[0.0f64; $d]; $d];
2667                    for (i, row) in rows.iter_mut().enumerate() {
2668                        row[i] = 1.0;
2669                    }
2670                    rows[0][$d - 1] = 1.0;
2671                    rows[$d - 1][0] = -1.0;
2672
2673                    assert_eq!(
2674                        Matrix::<$d>::try_from_rows(rows).and_then(SymmetricMatrix::try_new),
2675                        Err(LaError::asymmetric(0, $d - 1, $d, 1.0, -1.0, 0.0))
2676                    );
2677                }
2678            }
2679        };
2680    }
2681
2682    gen_ldlt_symmetry_proof_tests!(2);
2683    gen_ldlt_symmetry_proof_tests!(3);
2684    gen_ldlt_symmetry_proof_tests!(4);
2685    gen_ldlt_symmetry_proof_tests!(5);
2686
2687    #[test]
2688    fn symmetric_matrix_into_matrix_roundtrips_storage_internally() {
2689        let a = Matrix::<2>::try_from_rows([[2.0, 1.0], [1.0, 3.0]]).unwrap();
2690        let symmetric = SymmetricMatrix::try_new(a).unwrap();
2691
2692        assert_eq!(symmetric.into_matrix(), a);
2693    }
2694
2695    #[test]
2696    fn matrix_ldlt_accepts_opposite_signed_zero_mirrors() {
2697        let matrix = Matrix::<2>::try_from_rows([[2.0, 0.0], [-0.0, 2.0]]).unwrap();
2698        let ldlt = matrix.ldlt(DEFAULT_SINGULAR_TOL).unwrap();
2699
2700        assert_eq!(ldlt.det(), Ok(4.0));
2701    }
2702
2703    #[test]
2704    fn is_symmetric_tolerance_scales_with_inf_norm() {
2705        // Off-diagonal entries differ by 1e-6.  With inf_norm ≈ 2e6, the
2706        // relative tolerance 1e-12 yields eps ≈ 2e-6, which accepts the gap;
2707        // a stricter tol of 1e-15 rejects it.
2708        let a = Matrix::<2>::try_from_rows([[1.0e6, 1.0e6 + 1.0e-6], [1.0e6, 1.0e6]]).unwrap();
2709        assert!(a.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2710        assert!(!a.is_symmetric(Tolerance::try_new(1e-15).unwrap()).unwrap());
2711    }
2712
2713    #[test]
2714    fn symmetry_epsilon_multiplies_after_row_sum_near_subnormal_boundary() {
2715        let min_subnormal = f64::from_bits(1);
2716        let mut rows = [[0.0; 5]; 5];
2717        let mut col = 0;
2718        while col < 4 {
2719            rows[0][col] = 0.4;
2720            rows[col][0] = 0.4;
2721            col += 1;
2722        }
2723        rows[0][4] = 2.0 * min_subnormal;
2724        rows[4][0] = 0.0;
2725
2726        let matrix = Matrix::<5>::try_from_rows(rows).unwrap();
2727        let tolerance = Tolerance::try_new(min_subnormal).unwrap();
2728        let expected_epsilon = tolerance.get() * matrix.inf_norm().unwrap().max(1.0);
2729
2730        assert_eq!(expected_epsilon.to_bits(), 2);
2731        assert_eq!(matrix.first_asymmetry(tolerance), Ok(None));
2732        assert_eq!(matrix.is_symmetric(tolerance), Ok(true));
2733    }
2734
2735    #[test]
2736    fn symmetry_epsilon_scales_terms_when_row_sum_overflows() {
2737        let matrix =
2738            Matrix::<2>::try_from_rows([[f64::MAX, f64::MAX], [f64::MAX / 2.0, f64::MAX]]).unwrap();
2739
2740        assert_eq!(
2741            matrix.inf_norm(),
2742            Err(LaError::non_finite_computation_matrix(
2743                ArithmeticOperation::MatrixInfinityNorm,
2744                0,
2745                1
2746            ))
2747        );
2748        assert_eq!(
2749            matrix.first_asymmetry(Tolerance::try_new(0.25).unwrap()),
2750            Ok(None)
2751        );
2752        assert_eq!(
2753            matrix.first_asymmetry(Tolerance::try_new(0.125).unwrap()),
2754            Ok(Some((0, 1)))
2755        );
2756    }
2757
2758    #[test]
2759    fn first_asymmetry_returns_lexicographically_first_pair() {
2760        // Two asymmetric pairs: (0, 2) and (1, 2).  We must get (0, 2) first.
2761        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]])
2762            .unwrap();
2763        assert_eq!(
2764            a.first_asymmetry(Tolerance::try_new(1e-12).unwrap())
2765                .unwrap(),
2766            Some((0, 2))
2767        );
2768    }
2769
2770    #[test]
2771    fn first_asymmetry_strict_tol_survives_row_sum_overflow() {
2772        let a = Matrix::<3>::try_from_rows([
2773            [1.0, 1.0, 0.0],
2774            [2.0, f64::MAX, f64::MAX],
2775            [0.0, 0.0, 1.0],
2776        ])
2777        .unwrap();
2778
2779        assert_eq!(
2780            a.inf_norm(),
2781            Err(LaError::non_finite_computation_matrix(
2782                ArithmeticOperation::MatrixInfinityNorm,
2783                1,
2784                2
2785            ))
2786        );
2787        assert_eq!(
2788            a.first_asymmetry(Tolerance::try_new(0.0).unwrap()).unwrap(),
2789            Some((0, 1))
2790        );
2791        assert!(!a.is_symmetric(Tolerance::try_new(0.0).unwrap()).unwrap());
2792    }
2793
2794    #[test]
2795    fn first_asymmetry_rejects_scaled_epsilon_overflow() {
2796        let a = Matrix::<2>::try_from_rows([[0.0, 0.0], [2.0, 1.0]]).unwrap();
2797        let tol = Tolerance::try_new(f64::MAX).unwrap();
2798
2799        assert_eq!(
2800            a.first_asymmetry(tol),
2801            Err(LaError::non_finite_computation_matrix(
2802                ArithmeticOperation::SymmetryCheck,
2803                1,
2804                0
2805            ))
2806        );
2807        assert_eq!(
2808            a.is_symmetric(tol),
2809            Err(LaError::non_finite_computation_matrix(
2810                ArithmeticOperation::SymmetryCheck,
2811                1,
2812                0
2813            ))
2814        );
2815    }
2816
2817    #[test]
2818    fn first_asymmetry_flags_overflowed_finite_difference() {
2819        let a = Matrix::<2>::try_from_rows([[1.0, f64::MAX], [-f64::MAX, 1.0]]).unwrap();
2820        assert_eq!(
2821            a.first_asymmetry(Tolerance::try_new(1e-12).unwrap())
2822                .unwrap(),
2823            Some((0, 1))
2824        );
2825        assert!(!a.is_symmetric(Tolerance::try_new(1e-12).unwrap()).unwrap());
2826    }
2827}