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}