Skip to main content

la_stack/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3#![doc = include_str!("../README.md")]
4
5#[cfg(doc)]
6mod readme_doctests {
7    //! Executable versions of README examples.
8    /// ```rust
9    /// use la_stack::prelude::*;
10    ///
11    /// // This system requires pivoting (a[0][0] = 0), so it's a good LU demo.
12    /// let a = Matrix::<5>::from_rows([
13    ///     [0.0, 1.0, 1.0, 1.0, 1.0],
14    ///     [1.0, 0.0, 1.0, 1.0, 1.0],
15    ///     [1.0, 1.0, 0.0, 1.0, 1.0],
16    ///     [1.0, 1.0, 1.0, 0.0, 1.0],
17    ///     [1.0, 1.0, 1.0, 1.0, 0.0],
18    /// ]);
19    ///
20    /// let b = Vector::<5>::new([14.0, 13.0, 12.0, 11.0, 10.0]);
21    ///
22    /// let lu = a.lu(DEFAULT_PIVOT_TOL).unwrap();
23    /// let x = lu.solve_vec(b).unwrap().into_array();
24    ///
25    /// // Floating-point rounding is expected; compare with a tolerance.
26    /// let expected = [1.0, 2.0, 3.0, 4.0, 5.0];
27    /// for (x_i, e_i) in x.iter().zip(expected.iter()) {
28    ///     assert!((*x_i - *e_i).abs() <= 1e-12);
29    /// }
30    /// ```
31    fn solve_5x5_example() {}
32
33    /// ```rust
34    /// use la_stack::prelude::*;
35    ///
36    /// // This matrix is symmetric positive-definite (A = L*L^T) so LDLT works without pivoting.
37    /// let a = Matrix::<5>::from_rows([
38    ///     [1.0, 1.0, 0.0, 0.0, 0.0],
39    ///     [1.0, 2.0, 1.0, 0.0, 0.0],
40    ///     [0.0, 1.0, 2.0, 1.0, 0.0],
41    ///     [0.0, 0.0, 1.0, 2.0, 1.0],
42    ///     [0.0, 0.0, 0.0, 1.0, 2.0],
43    /// ]);
44    ///
45    /// let det = a.ldlt(DEFAULT_SINGULAR_TOL).unwrap().det();
46    /// assert!((det - 1.0).abs() <= 1e-12);
47    /// ```
48    fn det_5x5_ldlt_example() {}
49}
50
51#[cfg(feature = "exact")]
52mod exact;
53#[cfg(feature = "exact")]
54pub use num_bigint::BigInt;
55#[cfg(feature = "exact")]
56pub use num_rational::BigRational;
57#[cfg(feature = "exact")]
58pub use num_traits::{FromPrimitive, Signed, ToPrimitive};
59
60mod ldlt;
61mod lu;
62mod matrix;
63mod vector;
64
65use core::fmt;
66
67// ---------------------------------------------------------------------------
68// Error-bound constants for `Matrix::det_errbound()`.
69//
70// For `D ∈ {2, 3, 4}`, `Matrix::det_direct()` evaluates the Leibniz expansion
71// of the determinant as a tree of f64 multiplies and fused multiply-adds
72// (FMAs).  Following Shewchuk's error-analysis methodology (REFERENCES.md
73// [8]), the absolute error of that computation is bounded by
74//
75//     |det_direct(A) - det_exact(A)|  ≤  ERR_COEFF_D · p(|A|)
76//
77// where `p(|A|)` is the **absolute Leibniz sum**
78//
79//     p(|A|) = Σ_σ ∏ᵢ |A[i, σ(i)]|,
80//
81// i.e. the same cofactor-expansion tree as `det_direct` but with each
82// entry replaced by its magnitude.  Note that `p(|A|)` is *not* the
83// combinatorial matrix permanent — the name "permanent" appears in the
84// source for brevity and to match the cited literature.
85//
86// Each constant has the shape `a · EPS + b · EPS²`: the linear term bounds
87// the first-order rounding and the quadratic term absorbs the interaction
88// of errors in nested FMAs.  The coefficients `a` and `b` are conservative
89// over-estimates derived from the longest dependency chain of `det_direct`
90// at that dimension.
91//
92// These constants are NOT feature-gated — they rely only on f64 arithmetic
93// and are useful for adaptive-precision logic even without the `exact`
94// feature.  Most callers should prefer `Matrix::det_errbound()`, which
95// applies these constants to the actual matrix; the raw constants are
96// exposed for advanced use cases (composing the bound with a pre-reduced
97// permanent, rolling a custom adaptive filter, etc.).  See
98// `Matrix::det_sign_exact()` (behind the `exact` feature) for the
99// reference adaptive-filter that consumes these internally.
100// ---------------------------------------------------------------------------
101
102const EPS: f64 = f64::EPSILON; // 2^-52
103
104/// Absolute error coefficient for [`Matrix::<2>::det_direct`](crate::Matrix::det_direct).
105///
106/// For any 2×2 matrix `A = [[a, b], [c, d]]` with finite f64 entries,
107///
108/// ```text
109/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_2 · (|a·d| + |b·c|)
110/// ```
111///
112/// `det_direct` evaluates `a·d - b·c` as one multiply followed by one FMA
113/// (2 rounding events); the linear `3·EPS` term bounds those roundings
114/// and the quadratic `16·EPS²` term is a conservative cushion for their
115/// interaction.  Derivation follows Shewchuk's framework; see
116/// `REFERENCES.md` \[8\].
117///
118/// Prefer [`Matrix::det_errbound`](crate::Matrix::det_errbound) unless
119/// you already have the absolute-Leibniz sum available; see
120/// [`Matrix::det_sign_exact`](crate::Matrix::det_sign_exact) (under the
121/// `exact` feature) for the reference adaptive-precision filter.
122///
123/// # Example
124/// ```
125/// use la_stack::prelude::*;
126///
127/// let m = Matrix::<2>::from_rows([[1.0, 2.0], [3.0, 4.0]]);
128/// let det = m.det_direct().unwrap();
129/// // Compute the bound from the raw constant for illustration; most
130/// // callers would just use `m.det_errbound().unwrap()` instead.
131/// let p = (1.0_f64 * 4.0).abs() + (2.0_f64 * 3.0).abs();
132/// let bound = ERR_COEFF_2 * p;
133/// if det.abs() > bound {
134///     // The f64 sign is provably correct without exact arithmetic.
135/// }
136/// ```
137pub const ERR_COEFF_2: f64 = 3.0 * EPS + 16.0 * EPS * EPS;
138
139/// Absolute error coefficient for [`Matrix::<3>::det_direct`](crate::Matrix::det_direct).
140///
141/// For any 3×3 matrix `A` with finite f64 entries,
142///
143/// ```text
144/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_3 · p(|A|)
145/// ```
146///
147/// where `p(|A|)` is the absolute Leibniz sum (the same cofactor
148/// expansion as `det_direct` but with `|·|` at every leaf).
149/// `det_direct` for D=3 uses three 2×2 FMA minors combined by a nested
150/// FMA, yielding the `8·EPS + 64·EPS²` bound.  See `REFERENCES.md`
151/// \[8\] for the Shewchuk framework these bounds follow.
152///
153/// Prefer [`Matrix::det_errbound`](crate::Matrix::det_errbound) over this
154/// constant for typical use; see [`ERR_COEFF_2`] for a worked example.
155pub const ERR_COEFF_3: f64 = 8.0 * EPS + 64.0 * EPS * EPS;
156
157/// Absolute error coefficient for [`Matrix::<4>::det_direct`](crate::Matrix::det_direct).
158///
159/// For any 4×4 matrix `A` with finite f64 entries,
160///
161/// ```text
162/// |A.det_direct() - det_exact(A)|  ≤  ERR_COEFF_4 · p(|A|)
163/// ```
164///
165/// where `p(|A|)` is the absolute Leibniz sum.  `det_direct` for D=4
166/// hoists six 2×2 minors, combines them into four 3×3 cofactors, then
167/// reduces those with an FMA row combination, yielding the
168/// `12·EPS + 128·EPS²` bound.  See `REFERENCES.md` \[8\] for the
169/// Shewchuk framework these bounds follow.
170///
171/// Prefer [`Matrix::det_errbound`](crate::Matrix::det_errbound) over this
172/// constant for typical use; see [`ERR_COEFF_2`] for a worked example.
173pub const ERR_COEFF_4: f64 = 12.0 * EPS + 128.0 * EPS * EPS;
174
175/// Default absolute threshold used for singularity/degeneracy detection.
176///
177/// This is intentionally conservative for geometric predicates and small systems.
178///
179/// Conceptually, this is an absolute bound for deciding when a scalar should be treated
180/// as "numerically zero" (e.g. LU pivots, LDLT diagonal entries).
181pub const DEFAULT_SINGULAR_TOL: f64 = 1e-12;
182
183/// Default absolute pivot magnitude threshold used for LU pivot selection / singularity detection.
184///
185/// This name is kept for backwards compatibility; prefer [`DEFAULT_SINGULAR_TOL`] when the
186/// tolerance is not specifically about pivot selection.
187pub const DEFAULT_PIVOT_TOL: f64 = DEFAULT_SINGULAR_TOL;
188
189/// Linear algebra errors.
190///
191/// This enum is `#[non_exhaustive]` — downstream `match` arms must include a
192/// wildcard (`_`) pattern to compile, allowing new variants to be added in
193/// future minor releases without breaking existing code.
194#[derive(Clone, Copy, Debug, PartialEq, Eq)]
195#[non_exhaustive]
196pub enum LaError {
197    /// The matrix is (numerically) singular.
198    Singular {
199        /// The factorization column/step where a suitable pivot/diagonal could not be found.
200        pivot_col: usize,
201    },
202    /// A non-finite value (NaN/∞) was encountered.
203    ///
204    /// The `(row, col)` coordinate follows a consistent convention across the crate:
205    ///
206    /// - `row: Some(r), col: c` — a *stored* matrix cell at `(r, c)` is non-finite.
207    ///   Used by `Matrix::det`, `Lu::factor`, `Ldlt::factor`, and the `solve_vec`
208    ///   paths when they detect a corrupt stored factor (only reachable via
209    ///   direct struct construction; `factor` itself rejects such inputs).
210    /// - `row: None, col: c` — the non-finite value is either a *vector input*
211    ///   entry at index `c`, or a *computed intermediate* at step `c`
212    ///   (e.g. an accumulator that overflowed during forward/back substitution).
213    NonFinite {
214        /// Row of the non-finite entry for a stored matrix cell, or `None` for
215        /// a vector-input entry or a computed intermediate. See the variant
216        /// docs for the full convention.
217        row: Option<usize>,
218        /// Column index (stored cell), vector index, or factorization/solve
219        /// step where the non-finite value was detected.
220        col: usize,
221    },
222    /// The exact result overflows the target representation (e.g. `f64`).
223    ///
224    /// Returned by `Matrix::det_exact_f64` and `Matrix::solve_exact_f64`
225    /// (requires `exact` feature) when an exact value is too large to
226    /// represent as a finite `f64`.
227    Overflow {
228        /// For vector results (e.g. `solve_exact_f64`), the index of the
229        /// component that overflowed.  `None` for scalar results.
230        index: Option<usize>,
231    },
232}
233
234impl LaError {
235    /// Construct a [`LaError::NonFinite`] pinpointing a stored matrix cell at `(row, col)`.
236    ///
237    /// Use this for non-finite values read from a stored `Matrix` entry or
238    /// factorization cell.  The resulting error has `row: Some(row), col`,
239    /// matching the stored-cell convention documented on
240    /// [`NonFinite`](Self::NonFinite).  For vector-input entries or computed
241    /// intermediates, use [`non_finite_at`](Self::non_finite_at).
242    #[inline]
243    #[must_use]
244    pub const fn non_finite_cell(row: usize, col: usize) -> Self {
245        Self::NonFinite {
246            row: Some(row),
247            col,
248        }
249    }
250
251    /// Construct a [`LaError::NonFinite`] pinpointing a vector-input entry or
252    /// computed-intermediate step at index `col`.
253    ///
254    /// Use this for non-finite values in a `Vector` input or an accumulator
255    /// that overflowed during forward/back substitution.  The resulting error
256    /// has `row: None, col`, matching the vector/intermediate convention
257    /// documented on [`NonFinite`](Self::NonFinite).  For stored matrix cells,
258    /// use [`non_finite_cell`](Self::non_finite_cell).
259    #[inline]
260    #[must_use]
261    pub const fn non_finite_at(col: usize) -> Self {
262        Self::NonFinite { row: None, col }
263    }
264}
265
266impl fmt::Display for LaError {
267    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268        match *self {
269            Self::Singular { pivot_col } => {
270                write!(f, "singular matrix at pivot column {pivot_col}")
271            }
272            Self::NonFinite { row: Some(r), col } => {
273                write!(f, "non-finite value at ({r}, {col})")
274            }
275            Self::NonFinite { row: None, col } => {
276                write!(f, "non-finite value at index {col}")
277            }
278            Self::Overflow { index: Some(i) } => {
279                write!(
280                    f,
281                    "exact result overflows the target representation at index {i}"
282                )
283            }
284            Self::Overflow { index: None } => {
285                write!(f, "exact result overflows the target representation")
286            }
287        }
288    }
289}
290
291impl std::error::Error for LaError {}
292
293pub use ldlt::Ldlt;
294pub use lu::Lu;
295pub use matrix::Matrix;
296pub use vector::Vector;
297
298/// Common imports for ergonomic usage.
299///
300/// This prelude re-exports the primary types and constants: [`Matrix`], [`Vector`], [`Lu`],
301/// [`Ldlt`], [`LaError`], [`DEFAULT_PIVOT_TOL`], [`DEFAULT_SINGULAR_TOL`], and the determinant
302/// error bound coefficients [`ERR_COEFF_2`], [`ERR_COEFF_3`], and [`ERR_COEFF_4`].
303///
304/// When the `exact` feature is enabled, [`BigInt`] and [`BigRational`]
305/// are also re-exported so callers can construct exact values (e.g. as
306/// the expected result of `Matrix::det_exact`) without adding
307/// `num-bigint` / `num-rational` to their own dependencies.  The most
308/// commonly needed `num-traits` items are re-exported alongside them:
309/// [`FromPrimitive`] for `BigRational::from_f64` / `from_i64`,
310/// [`ToPrimitive`] for `BigRational::to_f64` / `to_i64`, and [`Signed`]
311/// for `.is_positive()` / `.is_negative()` / `.abs()`.
312pub mod prelude {
313    pub use crate::{
314        DEFAULT_PIVOT_TOL, DEFAULT_SINGULAR_TOL, ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4, LaError,
315        Ldlt, Lu, Matrix, Vector,
316    };
317
318    #[cfg(feature = "exact")]
319    pub use crate::{BigInt, BigRational, FromPrimitive, Signed, ToPrimitive};
320}
321
322#[cfg(test)]
323mod tests {
324    use super::*;
325
326    use approx::assert_abs_diff_eq;
327
328    #[test]
329    fn default_singular_tol_is_expected() {
330        assert_abs_diff_eq!(DEFAULT_SINGULAR_TOL, 1e-12, epsilon = 0.0);
331        assert_abs_diff_eq!(DEFAULT_PIVOT_TOL, DEFAULT_SINGULAR_TOL, epsilon = 0.0);
332    }
333
334    #[test]
335    fn laerror_display_formats_singular() {
336        let err = LaError::Singular { pivot_col: 3 };
337        assert_eq!(err.to_string(), "singular matrix at pivot column 3");
338    }
339
340    #[test]
341    fn laerror_display_formats_nonfinite_with_row() {
342        let err = LaError::NonFinite {
343            row: Some(1),
344            col: 2,
345        };
346        assert_eq!(err.to_string(), "non-finite value at (1, 2)");
347    }
348
349    #[test]
350    fn laerror_display_formats_nonfinite_without_row() {
351        let err = LaError::NonFinite { row: None, col: 3 };
352        assert_eq!(err.to_string(), "non-finite value at index 3");
353    }
354
355    #[test]
356    fn laerror_display_formats_overflow() {
357        let err = LaError::Overflow { index: None };
358        assert_eq!(
359            err.to_string(),
360            "exact result overflows the target representation"
361        );
362    }
363
364    #[test]
365    fn laerror_display_formats_overflow_with_index() {
366        let err = LaError::Overflow { index: Some(2) };
367        assert_eq!(
368            err.to_string(),
369            "exact result overflows the target representation at index 2"
370        );
371    }
372
373    #[test]
374    fn laerror_is_std_error_with_no_source() {
375        let err = LaError::Singular { pivot_col: 0 };
376        let e: &dyn std::error::Error = &err;
377        assert!(e.source().is_none());
378    }
379
380    #[test]
381    fn prelude_reexports_compile_and_work() {
382        use crate::prelude::*;
383
384        // Use the items so we know they are in scope and usable.
385        let m = Matrix::<2>::identity();
386        let v = Vector::<2>::new([1.0, 2.0]);
387        let _ = m.lu(DEFAULT_PIVOT_TOL).unwrap().solve_vec(v).unwrap();
388        let _ = m.ldlt(DEFAULT_SINGULAR_TOL).unwrap().solve_vec(v).unwrap();
389    }
390
391    /// Exercise every exact-feature re-export via the prelude so a future
392    /// refactor that drops one (e.g. removing `Signed` from the prelude
393    /// list) fails to compile rather than silently breaking downstream.
394    #[cfg(feature = "exact")]
395    #[test]
396    fn prelude_exact_reexports_compile_and_work() {
397        use crate::prelude::*;
398
399        // `BigInt` and `BigRational` constructors.
400        let n = BigInt::from(7);
401        let r = BigRational::from_integer(n.clone());
402        assert_eq!(*r.numer(), n);
403
404        // `FromPrimitive::from_f64` / `from_i64` on `BigRational`.
405        let half = BigRational::from_f64(0.5).unwrap();
406        let two = BigRational::from_i64(2).unwrap();
407        assert_eq!(
408            half.clone() + half.clone(),
409            BigRational::from_integer(BigInt::from(1))
410        );
411
412        // `Signed::is_positive` / `is_negative` / `abs`.
413        assert!(half.is_positive());
414        assert!(!half.is_negative());
415        let neg = -half.clone();
416        assert!(neg.is_negative());
417        assert_eq!(neg.abs(), half);
418
419        // `ToPrimitive::to_f64` / `to_i64`.
420        assert!((half.to_f64().unwrap() - 0.5).abs() <= f64::EPSILON);
421        assert_eq!(two.to_i64().unwrap(), 2);
422    }
423}