Skip to main content

use_calculus/
limit.rs

1use crate::CalculusError;
2
3/// Symmetric two-sided limit approximation settings.
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct LimitApproximator {
6    step: f64,
7    tolerance: f64,
8}
9
10impl LimitApproximator {
11    /// Creates a limit approximator from a sample step and comparison
12    /// tolerance.
13    #[must_use]
14    pub const fn new(step: f64, tolerance: f64) -> Self {
15        Self { step, tolerance }
16    }
17
18    /// Creates a limit approximator from a finite positive step and a finite
19    /// non-negative tolerance.
20    ///
21    /// # Errors
22    ///
23    /// Returns [`CalculusError::NonFiniteStep`] or
24    /// [`CalculusError::NonPositiveStep`] when `step` is invalid, and returns
25    /// [`CalculusError::NonFiniteTolerance`] or
26    /// [`CalculusError::NegativeTolerance`] when `tolerance` is invalid.
27    pub fn try_new(step: f64, tolerance: f64) -> Result<Self, CalculusError> {
28        CalculusError::validate_step(step)?;
29        CalculusError::validate_tolerance(tolerance)?;
30
31        Ok(Self::new(step, tolerance))
32    }
33
34    /// Validates that the stored step and tolerance are acceptable.
35    ///
36    /// # Errors
37    ///
38    /// Returns the same error variants as [`Self::try_new`].
39    pub fn validate(self) -> Result<Self, CalculusError> {
40        Self::try_new(self.step, self.tolerance)
41    }
42
43    /// Returns the symmetric sample step.
44    #[must_use]
45    pub const fn step(&self) -> f64 {
46        self.step
47    }
48
49    /// Returns the acceptance tolerance between the left and right samples.
50    #[must_use]
51    pub const fn tolerance(&self) -> f64 {
52        self.tolerance
53    }
54
55    /// Approximates a two-sided limit at `at` using one symmetric sample scale.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`CalculusError`] when the stored step or tolerance is invalid,
60    /// `at` is not finite, sampled evaluations are not finite, or the left and
61    /// right samples disagree by more than `tolerance`.
62    pub fn at<F>(self, function: F, at: f64) -> Result<f64, CalculusError>
63    where
64        F: FnMut(f64) -> f64,
65    {
66        symmetric_limit(function, at, self.step, self.tolerance)
67    }
68}
69
70/// Approximates a two-sided limit with one symmetric sample scale.
71///
72/// # Errors
73///
74/// Returns [`CalculusError`] when `step` or `tolerance` is invalid, `at` is
75/// not finite, sampled evaluations are not finite, or the left and right
76/// samples disagree by more than `tolerance`.
77///
78/// # Examples
79///
80/// ```
81/// use use_calculus::symmetric_limit;
82///
83/// let limit = symmetric_limit(
84///     |x| {
85///         if x == 0.0 {
86///             1.0
87///         } else {
88///             x.sin() / x
89///         }
90///     },
91///     0.0,
92///     1.0e-6,
93///     1.0e-5,
94/// )?;
95///
96/// assert!((limit - 1.0).abs() < 1.0e-5);
97/// # Ok::<(), use_calculus::CalculusError>(())
98/// ```
99#[must_use = "limit estimates should be used or handled"]
100pub fn symmetric_limit<F>(
101    mut function: F,
102    at: f64,
103    step: f64,
104    tolerance: f64,
105) -> Result<f64, CalculusError>
106where
107    F: FnMut(f64) -> f64,
108{
109    let at = CalculusError::validate_point("at", at)?;
110    let step = CalculusError::validate_step(step)?;
111    let tolerance = CalculusError::validate_tolerance(tolerance)?;
112    let left = evaluate(&mut function, at - step)?;
113    let right = evaluate(&mut function, at + step)?;
114
115    if (left - right).abs() > tolerance {
116        return Err(CalculusError::LimitMismatch {
117            left,
118            right,
119            tolerance,
120        });
121    }
122
123    Ok(f64::midpoint(left, right))
124}
125
126fn evaluate<F>(function: &mut F, input: f64) -> Result<f64, CalculusError>
127where
128    F: FnMut(f64) -> f64,
129{
130    let input = CalculusError::validate_point("sample", input)?;
131    let value = function(input);
132
133    CalculusError::validate_evaluation(input, value)
134}
135
136#[cfg(test)]
137mod tests {
138    use super::{CalculusError, LimitApproximator, symmetric_limit};
139
140    fn assert_close(left: f64, right: f64, tolerance: f64) {
141        assert!(
142            (left - right).abs() <= tolerance,
143            "expected {left} to be within {tolerance} of {right}"
144        );
145    }
146
147    #[test]
148    fn validates_limit_configuration() {
149        assert!(matches!(
150            LimitApproximator::try_new(0.0, 1.0e-4),
151            Err(CalculusError::NonPositiveStep(0.0))
152        ));
153        assert!(matches!(
154            LimitApproximator::try_new(1.0e-4, -1.0),
155            Err(CalculusError::NegativeTolerance(-1.0))
156        ));
157    }
158
159    #[test]
160    fn approximates_two_sided_limits() -> Result<(), CalculusError> {
161        let limit = symmetric_limit(
162            |x| {
163                if x == 0.0 { 1.0 } else { x.sin() / x }
164            },
165            0.0,
166            1.0e-6,
167            1.0e-5,
168        )?;
169
170        assert_close(limit, 1.0, 1.0e-5);
171        Ok(())
172    }
173
174    #[test]
175    fn rejects_mismatched_limits() {
176        assert!(matches!(
177            symmetric_limit(|x| if x < 0.0 { -1.0 } else { 1.0 }, 0.0, 1.0e-6, 1.0e-3,),
178            Err(CalculusError::LimitMismatch { .. })
179        ));
180    }
181}