mathhook_core/algebra/root_finding/
secant.rs

1//! Secant method for root finding
2//!
3//! Implements the secant method which approximates the derivative
4//! using two function evaluations. No analytical or numerical derivative required.
5//!
6//! # Algorithm
7//!
8//! Given two initial guesses x0 and x1:
9//! x(n+1) = x(n) - f(x(n)) * (x(n) - x(n-1)) / (f(x(n)) - f(x(n-1)))
10//!
11//! # Convergence
12//!
13//! - Superlinear convergence rate: ≈ 1.618 (golden ratio)
14//! - Faster than bisection, slower than Newton-Raphson
15//! - No derivative calculation required
16//! - Requires two initial guesses (not necessarily a bracket)
17
18use super::{RootFinder, RootFindingConfig, RootResult};
19use crate::error::MathError;
20
21/// Secant method for root finding
22pub struct SecantMethod {
23    /// First initial guess
24    pub x0: f64,
25    /// Second initial guess
26    pub x1: f64,
27}
28
29impl SecantMethod {
30    /// Create a new secant method with two initial guesses
31    ///
32    /// # Arguments
33    ///
34    /// * `x0` - First initial guess
35    /// * `x1` - Second initial guess (should be close to but different from x0)
36    ///
37    /// # Examples
38    ///
39    /// ```rust
40    /// use mathhook_core::algebra::root_finding::SecantMethod;
41    ///
42    /// let method = SecantMethod::new(1.0, 2.0);
43    /// ```
44    pub fn new(x0: f64, x1: f64) -> Self {
45        Self { x0, x1 }
46    }
47}
48
49impl RootFinder for SecantMethod {
50    fn find_root<F>(&self, f: F, config: &RootFindingConfig) -> Result<RootResult, MathError>
51    where
52        F: Fn(f64) -> f64,
53    {
54        let mut x_prev = self.x0;
55        let mut x_curr = self.x1;
56        let mut f_prev = f(x_prev);
57        let mut f_curr = f(x_curr);
58
59        for iteration in 0..config.max_iterations {
60            if f_curr.abs() < config.tolerance {
61                return Ok(RootResult {
62                    root: x_curr,
63                    iterations: iteration + 1,
64                    function_value: f_curr,
65                    converged: true,
66                });
67            }
68
69            let denominator = f_curr - f_prev;
70            if denominator.abs() < 1e-14 {
71                return Err(MathError::ConvergenceFailed {
72                    reason: format!(
73                        "Denominator too small: f({}) - f({}) = {}",
74                        x_curr, x_prev, denominator
75                    ),
76                });
77            }
78
79            let x_new = x_curr - f_curr * (x_curr - x_prev) / denominator;
80
81            if x_new.is_nan() || x_new.is_infinite() {
82                return Err(MathError::ConvergenceFailed {
83                    reason: format!("Iteration produced invalid value: {}", x_new),
84                });
85            }
86
87            let f_new = f(x_new);
88
89            if (x_new - x_curr).abs() < config.tolerance * x_new.abs().max(1.0)
90                && f_new.abs() < config.tolerance
91            {
92                return Ok(RootResult {
93                    root: x_new,
94                    iterations: iteration + 1,
95                    function_value: f_new,
96                    converged: true,
97                });
98            }
99
100            x_prev = x_curr;
101            f_prev = f_curr;
102            x_curr = x_new;
103            f_curr = f_new;
104        }
105
106        Err(MathError::MaxIterationsReached {
107            max_iterations: config.max_iterations,
108        })
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn test_secant_simple_linear() {
118        let method = SecantMethod::new(0.0, 2.0);
119        let config = RootFindingConfig::default();
120
121        let result = method.find_root(|x| x - 1.0, &config).unwrap();
122
123        assert!((result.root - 1.0).abs() < 1e-9);
124        assert!(result.converged);
125    }
126
127    #[test]
128    fn test_secant_quadratic() {
129        let method = SecantMethod::new(1.0, 2.0);
130        let config = RootFindingConfig::default();
131
132        let result = method.find_root(|x| x * x - 2.0, &config).unwrap();
133
134        assert!((result.root - 2.0_f64.sqrt()).abs() < 1e-10);
135        assert!(result.converged);
136    }
137
138    #[test]
139    fn test_secant_cubic() {
140        let method = SecantMethod::new(-2.0, 0.0);
141        let config = RootFindingConfig::default();
142
143        let result = method
144            .find_root(|x| x * x * x + x * x - 1.0, &config)
145            .unwrap();
146
147        let f_at_root = result.root.powi(3) + result.root.powi(2) - 1.0;
148        assert!(f_at_root.abs() < 1e-9);
149        assert!(result.converged);
150    }
151
152    #[test]
153    fn test_secant_transcendental() {
154        let method = SecantMethod::new(0.5, 1.0);
155        let config = RootFindingConfig::default();
156
157        let result = method.find_root(|x| x.cos() - x, &config).unwrap();
158
159        let expected = 0.7390851332151607;
160        assert!((result.root - expected).abs() < 1e-10);
161        assert!(result.converged);
162    }
163
164    #[test]
165    fn test_secant_exponential() {
166        let method = SecantMethod::new(0.5, 1.0);
167        let config = RootFindingConfig::default();
168
169        let result = method.find_root(|x| x.exp() - 2.0, &config).unwrap();
170
171        assert!((result.root - 2.0_f64.ln()).abs() < 1e-10);
172        assert!(result.converged);
173    }
174
175    #[test]
176    fn test_secant_sine() {
177        let method = SecantMethod::new(3.0, 3.5);
178        let config = RootFindingConfig::default();
179
180        let result = method.find_root(|x| x.sin(), &config).unwrap();
181
182        assert!((result.root - std::f64::consts::PI).abs() < 1e-10);
183        assert!(result.converged);
184    }
185
186    #[test]
187    fn test_secant_polynomial() {
188        let method = SecantMethod::new(0.0, 1.0);
189        let config = RootFindingConfig::default();
190
191        let result = method
192            .find_root(|x| x * x * x - 3.0 * x + 1.0, &config)
193            .unwrap();
194
195        let f_at_root = result.root.powi(3) - 3.0 * result.root + 1.0;
196        assert!(f_at_root.abs() < 1e-9);
197        assert!(result.converged);
198    }
199
200    #[test]
201    fn test_secant_logarithmic() {
202        let method = SecantMethod::new(2.0, 3.0);
203        let config = RootFindingConfig::default();
204
205        let result = method.find_root(|x| x.ln() - 1.0, &config).unwrap();
206
207        assert!((result.root - std::f64::consts::E).abs() < 1e-10);
208        assert!(result.converged);
209    }
210
211    #[test]
212    fn test_secant_convergence_rate() {
213        let method = SecantMethod::new(1.0, 2.0);
214        let config = RootFindingConfig {
215            tolerance: 1e-12,
216            ..Default::default()
217        };
218
219        let result = method.find_root(|x| x * x - 2.0, &config).unwrap();
220
221        assert!(result.iterations < 15);
222        assert!((result.root - 2.0_f64.sqrt()).abs() < 1e-12);
223    }
224
225    #[test]
226    fn test_secant_negative_root() {
227        let method = SecantMethod::new(-2.0, -1.0);
228        let config = RootFindingConfig::default();
229
230        let result = method.find_root(|x| x * x - 2.0, &config).unwrap();
231
232        assert!((result.root + 2.0_f64.sqrt()).abs() < 1e-10);
233        assert!(result.converged);
234    }
235
236    #[test]
237    fn test_secant_trigonometric_combination() {
238        let method = SecantMethod::new(0.5, 1.5);
239        let config = RootFindingConfig::default();
240
241        let result = method
242            .find_root(|x| x.sin() + x.cos() - 1.0, &config)
243            .unwrap();
244
245        let f_at_root = result.root.sin() + result.root.cos() - 1.0;
246        assert!(f_at_root.abs() < 1e-9);
247        assert!(result.converged);
248    }
249
250    #[test]
251    fn test_secant_rational_function() {
252        let method = SecantMethod::new(1.5, 2.5);
253        let config = RootFindingConfig::default();
254
255        let result = method
256            .find_root(|x| (x * x - 4.0) / (x + 1.0), &config)
257            .unwrap();
258
259        assert!((result.root - 2.0).abs() < 1e-10 || (result.root + 2.0).abs() < 1e-10);
260        assert!(result.converged);
261    }
262
263    #[test]
264    fn test_secant_hyperbolic() {
265        let method = SecantMethod::new(0.5, 1.5);
266        let config = RootFindingConfig::default();
267
268        let result = method.find_root(|x| x.sinh() - 1.0, &config).unwrap();
269
270        assert!((result.root - 1.0_f64.asinh()).abs() < 1e-10);
271        assert!(result.converged);
272    }
273
274    #[test]
275    fn test_secant_close_initial_guesses() {
276        let method = SecantMethod::new(1.4, 1.5);
277        let config = RootFindingConfig::default();
278
279        let result = method.find_root(|x| x * x - 2.0, &config).unwrap();
280
281        assert!((result.root - 2.0_f64.sqrt()).abs() < 1e-10);
282        assert!(result.converged);
283    }
284
285    #[test]
286    fn test_secant_wide_initial_guesses() {
287        let method = SecantMethod::new(0.5, 5.0);
288        let config = RootFindingConfig::default();
289
290        let result = method.find_root(|x| x * x - 2.0, &config).unwrap();
291
292        assert!((result.root - 2.0_f64.sqrt()).abs() < 1e-10);
293        assert!(result.converged);
294    }
295
296    #[test]
297    fn test_secant_tolerance_control() {
298        let method = SecantMethod::new(1.0, 2.0);
299
300        let config_loose = RootFindingConfig {
301            tolerance: 1e-6,
302            ..Default::default()
303        };
304        let result_loose = method.find_root(|x| x * x - 2.0, &config_loose).unwrap();
305
306        let config_tight = RootFindingConfig {
307            tolerance: 1e-12,
308            ..Default::default()
309        };
310        let result_tight = method.find_root(|x| x * x - 2.0, &config_tight).unwrap();
311
312        assert!(result_tight.function_value.abs() < result_loose.function_value.abs());
313    }
314}