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