use super::{bisection, halley, newton_raphson, newton_safeguarded, secant};
use crate::core::errors::RustyQLibError;
#[derive(Debug, Clone, Copy)]
pub struct Root {
pub x: f64,
pub iterations: usize,
pub converged: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct Solver1d {
pub tol: f64,
pub max_iter: usize,
}
impl Default for Solver1d {
fn default() -> Self {
Self { tol: 1e-12, max_iter: 100 }
}
}
impl Solver1d {
pub fn new(tol: f64, max_iter: usize) -> Self {
Self { tol, max_iter }
}
pub fn bisection(&self, f: impl Fn(f64) -> f64, lo: f64, hi: f64) -> Result<Root, RustyQLibError> {
bisection::bisection(self, f, lo, hi)
}
pub fn newton_raphson(
&self,
f: impl Fn(f64) -> f64,
df: impl Fn(f64) -> f64,
x0: f64,
) -> Root {
newton_raphson::newton_raphson(self, f, df, x0)
}
pub fn secant(&self, f: impl Fn(f64) -> f64, x0: f64, x1: f64) -> Root {
secant::secant(self, f, x0, x1)
}
pub fn halley(
&self,
f: impl Fn(f64) -> f64,
df: impl Fn(f64) -> f64,
d2f: impl Fn(f64) -> f64,
x0: f64,
) -> Root {
halley::halley(self, f, df, d2f, x0)
}
pub fn newton_safeguarded(
&self,
f: impl Fn(f64) -> f64,
df: impl Fn(f64) -> f64,
lo: f64,
hi: f64,
x0: f64,
) -> Root {
newton_safeguarded::newton_safeguarded(self, f, df, lo, hi, x0)
}
pub fn solve(&self, method: Method, problem: &Problem) -> Result<Root, RustyQLibError> {
let f = |x: f64| (problem.f)(x);
let df = |x: f64| match problem.df {
Some(df) => df(x),
None => numeric_derivative(problem.f, x),
};
let d2f = |x: f64| match problem.d2f {
Some(d2f) => d2f(x),
None => numeric_second_derivative(problem.f, x),
};
let bracket = |name: &str| {
problem
.bracket
.ok_or_else(|| RustyQLibError::NumericalError(format!("{name} needs a bracket: use Problem::with_bracket")))
};
match method {
Method::Bisection => {
let (lo, hi) = bracket("bisection")?;
self.bisection(f, lo, hi)
}
Method::NewtonRaphson => Ok(self.newton_raphson(f, df, problem.x0)),
Method::Secant => {
let x1 = match problem.bracket {
Some((lo, hi)) => {
if (problem.x0 - lo).abs() > (problem.x0 - hi).abs() { lo } else { hi }
}
None => problem.x0 + 1e-4 * (1.0 + problem.x0.abs()),
};
Ok(self.secant(f, problem.x0, x1))
}
Method::Halley => Ok(self.halley(f, df, d2f, problem.x0)),
Method::NewtonSafeguarded => {
let (lo, hi) = bracket("newton_safeguarded")?;
Ok(self.newton_safeguarded(f, df, lo, hi, problem.x0))
}
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
Bisection,
NewtonRaphson,
Secant,
Halley,
NewtonSafeguarded,
}
pub struct Problem<'a> {
pub f: &'a dyn Fn(f64) -> f64,
pub df: Option<&'a dyn Fn(f64) -> f64>,
pub d2f: Option<&'a dyn Fn(f64) -> f64>,
pub x0: f64,
pub bracket: Option<(f64, f64)>,
}
impl<'a> Problem<'a> {
pub fn new(f: &'a dyn Fn(f64) -> f64, x0: f64) -> Self {
Self { f, df: None, d2f: None, x0, bracket: None }
}
pub fn with_derivative(mut self, df: &'a dyn Fn(f64) -> f64) -> Self {
self.df = Some(df);
self
}
pub fn with_second_derivative(mut self, d2f: &'a dyn Fn(f64) -> f64) -> Self {
self.d2f = Some(d2f);
self
}
pub fn with_bracket(mut self, lo: f64, hi: f64) -> Self {
self.bracket = Some((lo.min(hi), lo.max(hi)));
self
}
}
fn numeric_derivative(f: &dyn Fn(f64) -> f64, x: f64) -> f64 {
let h = 1e-6 * (1.0 + x.abs());
(f(x + h) - f(x - h)) / (2.0 * h)
}
fn numeric_second_derivative(f: &dyn Fn(f64) -> f64, x: f64) -> f64 {
let h = 1e-4 * (1.0 + x.abs());
(f(x + h) - 2.0 * f(x) + f(x - h)) / (h * h)
}
#[cfg(test)]
mod tests {
use super::*;
const SQRT2: f64 = std::f64::consts::SQRT_2;
fn f(x: f64) -> f64 {
x * x - 2.0
}
fn df(x: f64) -> f64 {
2.0 * x
}
#[test]
fn every_method_is_pluggable_on_one_problem() {
let obj = |x: f64| f(x);
let problem = Problem::new(&obj, 1.0).with_bracket(0.0, 2.0);
let solver = Solver1d::default();
for method in [
Method::Bisection,
Method::NewtonRaphson,
Method::Secant,
Method::Halley,
Method::NewtonSafeguarded,
] {
let root = solver.solve(method, &problem).unwrap();
assert!(
root.converged && (root.x - SQRT2).abs() < 1e-9,
"{method:?}: {root:?}"
);
}
}
#[test]
fn numeric_derivative_fallback_matches_analytic() {
let obj = |x: f64| f(x);
let d = |x: f64| df(x);
let with = Problem::new(&obj, 1.0).with_derivative(&d);
let without = Problem::new(&obj, 1.0);
let solver = Solver1d::default();
let ra = solver.solve(Method::NewtonRaphson, &with).unwrap();
let rn = solver.solve(Method::NewtonRaphson, &without).unwrap();
assert!(ra.converged && rn.converged);
assert!((ra.x - rn.x).abs() < 1e-9, "{} vs {}", ra.x, rn.x);
}
#[test]
fn bracketed_methods_error_without_a_bracket() {
let obj = |x: f64| f(x);
let problem = Problem::new(&obj, 1.0);
let solver = Solver1d::default();
assert!(solver.solve(Method::Bisection, &problem).is_err());
assert!(solver.solve(Method::NewtonSafeguarded, &problem).is_err());
assert!(solver.solve(Method::Secant, &problem).unwrap().converged);
}
#[test]
fn iteration_counts_are_reported() {
let r = Solver1d::default().newton_raphson(f, df, 1.0);
assert!(r.iterations > 0 && r.iterations < 10);
}
}