Skip to main content

la_stack/
tolerance.rs

1#![forbid(unsafe_code)]
2
3//! Finite tolerance values used by numerical predicates and factorizations.
4
5use crate::LaError;
6
7/// Finite, non-negative tolerance used by numerical predicates and factorizations.
8///
9/// Construct with [`Tolerance::new`] when accepting raw caller input. Once
10/// constructed, the stored value is guaranteed to be finite and `>= 0`, so
11/// downstream algorithms do not need to revalidate the tolerance.
12///
13/// This is the crate-wide tolerance contract: raw negative, NaN, and infinite
14/// values are rejected with [`LaError::InvalidTolerance`] at construction time.
15#[must_use]
16#[derive(Clone, Copy, Debug, PartialEq)]
17pub struct Tolerance {
18    value: f64,
19}
20
21impl Tolerance {
22    /// Construct a tolerance without checking the raw value.
23    ///
24    /// This crate-internal escape hatch is only for constants whose finite,
25    /// non-negative value is visible at the call site. Public callers should
26    /// use [`Tolerance::new`] so the returned value carries the validation
27    /// proof.
28    pub(crate) const fn new_unchecked(value: f64) -> Self {
29        Self { value }
30    }
31
32    /// Construct a finite, non-negative tolerance.
33    ///
34    /// # Examples
35    /// ```
36    /// use la_stack::prelude::*;
37    ///
38    /// # fn main() -> Result<(), LaError> {
39    /// let tol = Tolerance::new(1e-12)?;
40    /// assert_eq!(tol.get(), 1e-12);
41    /// # Ok(())
42    /// # }
43    /// ```
44    ///
45    /// # Errors
46    /// Returns [`LaError::InvalidTolerance`] when `value` is NaN, infinite, or
47    /// negative.
48    #[inline]
49    pub const fn new(value: f64) -> Result<Self, LaError> {
50        if value >= 0.0 && value.is_finite() {
51            Ok(Self::new_unchecked(value))
52        } else {
53            Err(LaError::invalid_tolerance(value))
54        }
55    }
56
57    /// Return the raw finite, non-negative tolerance value.
58    ///
59    /// # Examples
60    /// ```
61    /// use la_stack::prelude::*;
62    ///
63    /// # fn main() -> Result<(), LaError> {
64    /// let tol = Tolerance::new(0.0)?;
65    /// assert_eq!(tol.get(), 0.0);
66    /// # Ok(())
67    /// # }
68    /// ```
69    #[inline]
70    #[must_use]
71    pub const fn get(self) -> f64 {
72        self.value
73    }
74}
75
76/// Default absolute threshold used for singularity/degeneracy detection.
77///
78/// This is intentionally conservative for geometric predicates and small systems.
79///
80/// Conceptually, this is an absolute bound for deciding when a scalar should be treated
81/// as "numerically zero" (e.g. LU pivots, LDLT diagonal entries).
82///
83/// # Examples
84/// ```
85/// use la_stack::prelude::*;
86///
87/// # fn main() -> Result<(), LaError> {
88/// let lu = Matrix::<2>::identity().lu(DEFAULT_SINGULAR_TOL)?;
89/// assert_eq!(lu.det()?, 1.0);
90/// # Ok(())
91/// # }
92/// ```
93pub const DEFAULT_SINGULAR_TOL: Tolerance = Tolerance::new_unchecked(1e-12);
94
95/// Relative tolerance used to validate matrices for LDLT factorization.
96///
97/// This is crate-internal because LDLT callers provide the factorization
98/// tolerance separately; the symmetry tolerance is a fixed domain check used to
99/// parse a public [`Matrix`](crate::Matrix) into the internal symmetric proof
100/// type before factorization.
101pub const LDLT_SYMMETRY_REL_TOL: Tolerance = Tolerance::new_unchecked(1e-12);
102
103#[cfg(test)]
104mod tests {
105    use core::assert_matches;
106
107    use approx::assert_abs_diff_eq;
108
109    use super::*;
110
111    #[test]
112    fn default_singular_tol_is_expected() {
113        assert_abs_diff_eq!(DEFAULT_SINGULAR_TOL.get(), 1e-12, epsilon = 0.0);
114    }
115
116    #[test]
117    fn tolerance_new_accepts_finite_non_negative_values() {
118        assert_eq!(
119            Tolerance::new(0.0).unwrap().get().to_bits(),
120            0.0f64.to_bits()
121        );
122        assert_eq!(
123            Tolerance::new(1e-12).unwrap().get().to_bits(),
124            1e-12f64.to_bits()
125        );
126        assert_eq!(
127            Tolerance::new(f64::MAX).unwrap().get().to_bits(),
128            f64::MAX.to_bits()
129        );
130    }
131
132    #[test]
133    fn tolerance_new_rejects_negative_nan_and_infinity() {
134        assert_eq!(
135            Tolerance::new(-1.0),
136            Err(LaError::InvalidTolerance { value: -1.0 })
137        );
138        assert_matches!(
139            Tolerance::new(f64::NAN),
140            Err(LaError::InvalidTolerance { value }) if value.is_nan()
141        );
142        assert_eq!(
143            Tolerance::new(f64::INFINITY),
144            Err(LaError::InvalidTolerance {
145                value: f64::INFINITY,
146            })
147        );
148        assert_eq!(
149            Tolerance::new(f64::NEG_INFINITY),
150            Err(LaError::InvalidTolerance {
151                value: f64::NEG_INFINITY,
152            })
153        );
154    }
155}