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::try_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 for a finite, non-negative module-local literal.
23    const fn new_unchecked(value: f64) -> Self {
24        Self { value }
25    }
26
27    /// Exact zero tolerance for crate-internal algorithms.
28    pub(crate) const ZERO: Self = Self::new_unchecked(0.0);
29
30    /// Construct a finite, non-negative tolerance.
31    ///
32    /// # Examples
33    /// ```
34    /// use la_stack::prelude::*;
35    ///
36    /// # fn main() -> Result<(), LaError> {
37    /// let tol = Tolerance::try_new(1e-12)?;
38    /// assert_eq!(tol.get(), 1e-12);
39    /// # Ok(())
40    /// # }
41    /// ```
42    ///
43    /// # Errors
44    /// Returns [`LaError::InvalidTolerance`] with
45    /// [`crate::InvalidToleranceReason::NotFinite`] for NaN/infinity or
46    /// [`crate::InvalidToleranceReason::Negative`] for a finite negative
47    /// value. Both signed-zero representations are accepted and preserved.
48    #[inline]
49    pub const fn try_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::try_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#[cfg(test)]
96mod tests {
97    use core::assert_matches;
98
99    use approx::assert_abs_diff_eq;
100
101    use super::*;
102    use crate::InvalidToleranceReason;
103
104    #[test]
105    fn default_singular_tol_is_expected() {
106        assert_abs_diff_eq!(DEFAULT_SINGULAR_TOL.get(), 1e-12, epsilon = 0.0);
107        assert_eq!(Tolerance::ZERO.get().to_bits(), 0.0f64.to_bits());
108    }
109
110    #[test]
111    fn try_new_accepts_finite_non_negative_values() {
112        assert_eq!(
113            Tolerance::try_new(0.0).unwrap().get().to_bits(),
114            0.0f64.to_bits()
115        );
116        assert_eq!(
117            Tolerance::try_new(1e-12).unwrap().get().to_bits(),
118            1e-12f64.to_bits()
119        );
120        assert_eq!(
121            Tolerance::try_new(f64::MAX).unwrap().get().to_bits(),
122            f64::MAX.to_bits()
123        );
124    }
125
126    #[test]
127    fn try_new_accepts_and_preserves_negative_zero() {
128        let tolerance = Tolerance::try_new(-0.0).unwrap();
129
130        assert_eq!(tolerance.get().to_bits(), (-0.0f64).to_bits());
131    }
132
133    #[test]
134    fn try_new_rejects_negative_finite_values() {
135        assert_eq!(
136            Tolerance::try_new(-1.0),
137            Err(LaError::InvalidTolerance {
138                value: -1.0,
139                reason: InvalidToleranceReason::Negative,
140            })
141        );
142    }
143
144    #[test]
145    fn try_new_rejects_non_finite_values_with_structured_reason() {
146        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
147            assert_matches!(
148                Tolerance::try_new(value),
149                Err(LaError::InvalidTolerance {
150                    value: observed,
151                    reason: InvalidToleranceReason::NotFinite,
152                }) if observed.to_bits() == value.to_bits()
153            );
154        }
155    }
156}