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_rational::BigRational;
55
56mod ldlt;
57mod lu;
58mod matrix;
59mod vector;
60
61use core::fmt;
62
63// ---------------------------------------------------------------------------
64// Error-bound constants for determinant error analysis.
65//
66// These constants bound the absolute error of `det_direct()` relative to the
67// *permanent* (sum of absolute products in the Leibniz expansion). The
68// constants are conservative over-estimates following Shewchuk's methodology.
69//
70// These are NOT feature-gated because they use pure f64 arithmetic and are
71// useful for adaptive-precision logic even without the `exact` feature.
72// ---------------------------------------------------------------------------
73
74const EPS: f64 = f64::EPSILON; // 2^-52
75
76/// Error coefficient for D=2 determinant error bound.
77///
78/// Accounts for one f64 multiply + one FMA → 2 rounding events.
79/// Used in computing the absolute error bound for 2×2 determinants.
80pub const ERR_COEFF_2: f64 = 3.0 * EPS + 16.0 * EPS * EPS;
81
82/// Error coefficient for D=3 determinant error bound.
83///
84/// Accounts for three 2×2 FMA minors + nested FMA combination.
85/// Used in computing the absolute error bound for 3×3 determinants.
86pub const ERR_COEFF_3: f64 = 8.0 * EPS + 64.0 * EPS * EPS;
87
88/// Error coefficient for D=4 determinant error bound.
89///
90/// Accounts for six hoisted 2×2 minors → four 3×3 cofactors → FMA row combination.
91/// Used in computing the absolute error bound for 4×4 determinants.
92pub const ERR_COEFF_4: f64 = 12.0 * EPS + 128.0 * EPS * EPS;
93
94/// Default absolute threshold used for singularity/degeneracy detection.
95///
96/// This is intentionally conservative for geometric predicates and small systems.
97///
98/// Conceptually, this is an absolute bound for deciding when a scalar should be treated
99/// as "numerically zero" (e.g. LU pivots, LDLT diagonal entries).
100pub const DEFAULT_SINGULAR_TOL: f64 = 1e-12;
101
102/// Default absolute pivot magnitude threshold used for LU pivot selection / singularity detection.
103///
104/// This name is kept for backwards compatibility; prefer [`DEFAULT_SINGULAR_TOL`] when the
105/// tolerance is not specifically about pivot selection.
106pub const DEFAULT_PIVOT_TOL: f64 = DEFAULT_SINGULAR_TOL;
107
108/// Linear algebra errors.
109///
110/// This enum is `#[non_exhaustive]` — downstream `match` arms must include a
111/// wildcard (`_`) pattern to compile, allowing new variants to be added in
112/// future minor releases without breaking existing code.
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114#[non_exhaustive]
115pub enum LaError {
116    /// The matrix is (numerically) singular.
117    Singular {
118        /// The factorization column/step where a suitable pivot/diagonal could not be found.
119        pivot_col: usize,
120    },
121    /// A non-finite value (NaN/∞) was encountered in the input.
122    NonFinite {
123        /// The column where a non-finite value was detected.
124        col: usize,
125    },
126    /// The exact result overflows the target representation (e.g. `f64`).
127    ///
128    /// This is returned by `Matrix::det_exact_f64` (requires `exact` feature)
129    /// when the exact `BigRational` determinant is too large to represent as
130    /// a finite `f64`.
131    ///
132    /// *Added in 0.3.0.*
133    Overflow,
134}
135
136impl fmt::Display for LaError {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        match *self {
139            Self::Singular { pivot_col } => {
140                write!(f, "singular matrix at pivot column {pivot_col}")
141            }
142            Self::NonFinite { col } => {
143                write!(f, "non-finite value encountered at column {col}")
144            }
145            Self::Overflow => {
146                write!(f, "exact result overflows the target representation")
147            }
148        }
149    }
150}
151
152impl std::error::Error for LaError {}
153
154pub use ldlt::Ldlt;
155pub use lu::Lu;
156pub use matrix::Matrix;
157pub use vector::Vector;
158
159/// Common imports for ergonomic usage.
160///
161/// This prelude re-exports the primary types and constants: [`Matrix`], [`Vector`], [`Lu`],
162/// [`Ldlt`], [`LaError`], [`DEFAULT_PIVOT_TOL`], [`DEFAULT_SINGULAR_TOL`], and the determinant
163/// error bound coefficients [`ERR_COEFF_2`], [`ERR_COEFF_3`], and [`ERR_COEFF_4`].
164///
165/// When the `exact` feature is enabled, `BigRational` is also
166/// re-exported for use with `Matrix::det_exact`.
167pub mod prelude {
168    pub use crate::{
169        DEFAULT_PIVOT_TOL, DEFAULT_SINGULAR_TOL, ERR_COEFF_2, ERR_COEFF_3, ERR_COEFF_4, LaError,
170        Ldlt, Lu, Matrix, Vector,
171    };
172
173    #[cfg(feature = "exact")]
174    pub use crate::BigRational;
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    use approx::assert_abs_diff_eq;
182
183    #[test]
184    fn default_singular_tol_is_expected() {
185        assert_abs_diff_eq!(DEFAULT_SINGULAR_TOL, 1e-12, epsilon = 0.0);
186        assert_abs_diff_eq!(DEFAULT_PIVOT_TOL, DEFAULT_SINGULAR_TOL, epsilon = 0.0);
187    }
188
189    #[test]
190    fn laerror_display_formats_singular() {
191        let err = LaError::Singular { pivot_col: 3 };
192        assert_eq!(err.to_string(), "singular matrix at pivot column 3");
193    }
194
195    #[test]
196    fn laerror_display_formats_nonfinite() {
197        let err = LaError::NonFinite { col: 2 };
198        assert_eq!(err.to_string(), "non-finite value encountered at column 2");
199    }
200
201    #[test]
202    fn laerror_display_formats_overflow() {
203        let err = LaError::Overflow;
204        assert_eq!(
205            err.to_string(),
206            "exact result overflows the target representation"
207        );
208    }
209
210    #[test]
211    fn laerror_is_std_error_with_no_source() {
212        let err = LaError::Singular { pivot_col: 0 };
213        let e: &dyn std::error::Error = &err;
214        assert!(e.source().is_none());
215    }
216
217    #[test]
218    fn prelude_reexports_compile_and_work() {
219        use crate::prelude::*;
220
221        // Use the items so we know they are in scope and usable.
222        let m = Matrix::<2>::identity();
223        let v = Vector::<2>::new([1.0, 2.0]);
224        let _ = m.lu(DEFAULT_PIVOT_TOL).unwrap().solve_vec(v).unwrap();
225        let _ = m.ldlt(DEFAULT_SINGULAR_TOL).unwrap().solve_vec(v).unwrap();
226    }
227}