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
103    #[test]
104    fn default_singular_tol_is_expected() {
105        assert_abs_diff_eq!(DEFAULT_SINGULAR_TOL.get(), 1e-12, epsilon = 0.0);
106        assert_eq!(Tolerance::ZERO.get().to_bits(), 0.0f64.to_bits());
107    }
108
109    #[test]
110    fn try_new_accepts_finite_non_negative_values() {
111        assert_eq!(
112            Tolerance::try_new(0.0).unwrap().get().to_bits(),
113            0.0f64.to_bits()
114        );
115        assert_eq!(
116            Tolerance::try_new(1e-12).unwrap().get().to_bits(),
117            1e-12f64.to_bits()
118        );
119        assert_eq!(
120            Tolerance::try_new(f64::MAX).unwrap().get().to_bits(),
121            f64::MAX.to_bits()
122        );
123    }
124
125    #[test]
126    fn try_new_accepts_and_preserves_negative_zero() {
127        let tolerance = Tolerance::try_new(-0.0).unwrap();
128
129        assert_eq!(tolerance.get().to_bits(), (-0.0f64).to_bits());
130    }
131
132    #[test]
133    fn try_new_rejects_negative_finite_values() {
134        assert_eq!(
135            Tolerance::try_new(-1.0),
136            Err(LaError::InvalidTolerance {
137                value: -1.0,
138                reason: crate::InvalidToleranceReason::Negative,
139            })
140        );
141    }
142
143    #[test]
144    fn try_new_rejects_non_finite_values_with_structured_reason() {
145        for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
146            assert_matches!(
147                Tolerance::try_new(value),
148                Err(LaError::InvalidTolerance {
149                    value: observed,
150                    reason: crate::InvalidToleranceReason::NotFinite,
151                }) if observed.to_bits() == value.to_bits()
152            );
153        }
154    }
155}