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