mathhook_core/algebra/root_finding/
secant.rs1use super::{RootFinder, RootFindingConfig, RootResult};
19use crate::error::MathError;
20
21pub struct SecantMethod {
23 pub x0: f64,
25 pub x1: f64,
27}
28
29impl SecantMethod {
30 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}