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}