rustyqlib/core/solvers/
bisection.rs1use super::solver_1d::{Root, Solver1d};
5use crate::core::errors::RustyQLibError;
6
7pub fn bisection(
10 cfg: &Solver1d,
11 f: impl Fn(f64) -> f64,
12 lo: f64,
13 hi: f64,
14) -> Result<Root, RustyQLibError> {
15 let (mut lo, mut hi) = (lo.min(hi), lo.max(hi));
16 let f_lo = f(lo);
17 if f_lo.abs() <= cfg.tol {
18 return Ok(Root { x: lo, iterations: 0, converged: true });
19 }
20 let f_hi = f(hi);
21 if f_hi.abs() <= cfg.tol {
22 return Ok(Root { x: hi, iterations: 0, converged: true });
23 }
24 if f_lo * f_hi > 0.0 {
25 return Err(RustyQLibError::NumericalError(format!(
26 "bisection needs a sign change: f({lo}) = {f_lo}, f({hi}) = {f_hi}"
27 )));
28 }
29 let mut x = 0.5 * (lo + hi);
30 for i in 1..=cfg.max_iter {
31 let fx = f(x);
32 if fx.abs() <= cfg.tol {
33 return Ok(Root { x, iterations: i, converged: true });
34 }
35 if fx * f_lo < 0.0 {
36 hi = x;
37 } else {
38 lo = x;
39 }
40 x = 0.5 * (lo + hi);
41 if hi - lo <= f64::EPSILON * (1.0 + x.abs()) {
42 return Ok(Root { x, iterations: i, converged: f(x).abs() <= cfg.tol });
43 }
44 }
45 Ok(Root { x, iterations: cfg.max_iter, converged: false })
46}
47
48#[cfg(test)]
49mod tests {
50 use super::*;
51
52 #[test]
53 fn finds_sqrt_two() {
54 let r = bisection(&Solver1d::default(), |x| x * x - 2.0, 0.0, 2.0).unwrap();
55 assert!(r.converged && (r.x - std::f64::consts::SQRT_2).abs() < 1e-9, "{r:?}");
56 }
57
58 #[test]
59 fn rejects_a_bracket_without_sign_change() {
60 assert!(bisection(&Solver1d::default(), |x| x * x - 2.0, 2.0, 3.0).is_err());
61 }
62}