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}