Skip to main content

lox_core/math/
roots.rs

1// SPDX-FileCopyrightText: 2024 Helge Eichhorn <git@helgeeichhorn.de>
2//
3// SPDX-License-Identifier: MPL-2.0
4
5//! Root-finding algorithms: Steffensen, Newton, safeguarded Newton, and Brent
6//! methods.
7
8use lox_approx::approx_eq;
9use thiserror::Error;
10
11use crate::error::LoxError;
12use crate::math::callback::{Callback, CallbackWithDerivative};
13use crate::math::float::{abs, powi, sqrt};
14
15/// Finds a root of `f` starting from an initial guess.
16pub trait FindRoot {
17    /// Finds a root of `f` starting from `initial_guess`.
18    fn find(&self, f: impl Callback, initial_guess: f64) -> Result<f64, RootFinderError>;
19}
20
21/// Finds a root of `f` using both the function and its derivative.
22pub trait FindRootWithDerivative {
23    /// Finds a root of `f` using `derivative`, starting from `initial_guess`.
24    fn find_with_derivative(
25        &self,
26        f: impl Callback,
27        derivative: impl Callback,
28        initial_guess: f64,
29    ) -> Result<f64, RootFinderError>;
30}
31
32/// Finds a root of `f` within a bracket `(a, b)`.
33pub trait FindBracketedRoot {
34    /// Finds a root of `f` within `bracket`, reusing the function values at the
35    /// bracket endpoints instead of evaluating them again.
36    ///
37    /// `values` must equal `(f(bracket.0), f(bracket.1))`.
38    fn find_in_bracket_with_values(
39        &self,
40        f: impl Callback,
41        bracket: (f64, f64),
42        values: (f64, f64),
43    ) -> Result<f64, RootFinderError>;
44
45    /// Finds a root of `f` within the given `bracket`.
46    fn find_in_bracket(
47        &self,
48        f: impl Callback,
49        bracket: (f64, f64),
50    ) -> Result<f64, RootFinderError> {
51        let fa = f.call(bracket.0)?;
52        let fb = f.call(bracket.1)?;
53        self.find_in_bracket_with_values(f, bracket, (fa, fb))
54    }
55}
56
57/// Finds a root of `f` within a bracket `(a, b)`, using its derivative.
58pub trait FindBracketedRootWithDerivative {
59    /// Finds a root of `f` within `bracket`, reusing the function values at the
60    /// bracket endpoints instead of evaluating them again.
61    ///
62    /// `values` must equal `(f(bracket.0), f(bracket.1))`.
63    fn find_in_bracket_with_derivative_values(
64        &self,
65        f: impl CallbackWithDerivative,
66        bracket: (f64, f64),
67        values: (f64, f64),
68    ) -> Result<f64, RootFinderError>;
69
70    /// Finds a root of `f` within the given `bracket`.
71    fn find_in_bracket_with_derivative(
72        &self,
73        f: impl CallbackWithDerivative,
74        bracket: (f64, f64),
75    ) -> Result<f64, RootFinderError> {
76        let (fa, _) = f.call(bracket.0)?;
77        let (fb, _) = f.call(bracket.1)?;
78        self.find_in_bracket_with_derivative_values(f, bracket, (fa, fb))
79    }
80}
81
82/// Error returned by root-finding algorithms.
83#[derive(Debug, Error)]
84pub enum RootFinderError {
85    /// The algorithm did not converge within the maximum number of iterations.
86    #[error("not converged after {iterations} iterations at x = {x}, residual {residual}")]
87    NotConverged {
88        /// Number of iterations performed before giving up.
89        iterations: u32,
90        /// The best root estimate reached.
91        x: f64,
92        /// The residual `f(x)` at the best estimate.
93        residual: f64,
94    },
95    /// The root is not within the given bracket.
96    #[error("root not in bracket")]
97    NotInBracket,
98    /// The objective function returned a non-finite value.
99    #[error("function returned a non-finite value ({value}) at x = {x}")]
100    NonFinite {
101        /// The point at which the function was evaluated.
102        x: f64,
103        /// The non-finite value returned.
104        value: f64,
105    },
106    /// The derivative returned a non-finite value.
107    #[error("derivative returned a non-finite value ({value}) at x = {x}")]
108    NonFiniteDerivative {
109        /// The point at which the derivative was evaluated.
110        x: f64,
111        /// The non-finite value returned.
112        value: f64,
113    },
114    /// The iteration update diverged, e.g. due to a zero derivative at a
115    /// stationary point or a vanishing update denominator.
116    #[error("iteration step diverged at x = {x}")]
117    DivergedStep {
118        /// The iterate at which the update diverged.
119        x: f64,
120    },
121    /// The objective function returned an error.
122    #[error(transparent)]
123    Callback(#[from] LoxError),
124}
125
126/// Evaluates `f` at `x`, mapping a callback failure or non-finite result to the
127/// corresponding [`RootFinderError`].
128fn eval_finite<F: Callback>(f: &F, x: f64) -> Result<f64, RootFinderError> {
129    let value = f.call(x)?;
130    if !value.is_finite() {
131        return Err(RootFinderError::NonFinite { x, value });
132    }
133    Ok(value)
134}
135
136/// Evaluates `f` and its derivative at `x`, mapping a callback failure or a
137/// non-finite function value to the corresponding [`RootFinderError`].
138///
139/// A non-finite *derivative* is not an error here: safeguarded methods fall
140/// back to bisection when the derivative is unusable.
141fn eval_finite_with_derivative<F: CallbackWithDerivative>(
142    f: &F,
143    x: f64,
144) -> Result<(f64, f64), RootFinderError> {
145    let (value, derivative) = f.call(x)?;
146    if !value.is_finite() {
147        return Err(RootFinderError::NonFinite { x, value });
148    }
149    Ok((value, derivative))
150}
151
152/// Steffensen's method for root-finding (derivative-free).
153///
154/// The tolerances bound the iteration step, not the residual: near steep
155/// features the iteration can stall within tolerance where `f(x)` is not
156/// small and report a false root. Prefer [`Brent`] when a bracket is
157/// available.
158#[derive(Debug, Copy, Clone, PartialEq)]
159pub struct Steffensen {
160    max_iter: u32,
161    /// Absolute tolerance on the iteration step.
162    abs_tol: f64,
163    /// Relative tolerance on the iteration step.
164    rel_tol: f64,
165}
166
167impl Default for Steffensen {
168    fn default() -> Self {
169        Self {
170            max_iter: 1000,
171            abs_tol: sqrt(f64::EPSILON),
172            rel_tol: sqrt(f64::EPSILON),
173        }
174    }
175}
176
177impl Steffensen {
178    /// Sets the maximum number of iterations.
179    pub fn with_max_iter(mut self, max_iter: u32) -> Self {
180        self.max_iter = max_iter;
181        self
182    }
183
184    /// Sets the absolute tolerance on the iteration step.
185    pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
186        self.abs_tol = abs_tol;
187        self
188    }
189
190    /// Sets the relative tolerance on the iteration step.
191    pub fn with_rel_tol(mut self, rel_tol: f64) -> Self {
192        self.rel_tol = rel_tol;
193        self
194    }
195}
196
197impl FindRoot for Steffensen {
198    fn find(&self, f: impl Callback, initial_guess: f64) -> Result<f64, RootFinderError> {
199        let mut p0 = initial_guess;
200        let mut last: Option<(f64, f64)> = None;
201        for _ in 0..self.max_iter {
202            let fp0 = eval_finite(&f, p0)?;
203            // An initial guess that is already a root is returned directly,
204            // avoiding a 0/0 update.
205            if fp0 == 0.0 {
206                return Ok(p0);
207            }
208            last = Some((p0, fp0));
209            let f1 = p0 + fp0;
210            let ff1 = eval_finite(&f, f1)?;
211            let f2 = f1 + ff1;
212            let p = p0 - powi(f1 - p0, 2) / (f2 - 2.0 * f1 + p0);
213            if !p.is_finite() {
214                return Err(RootFinderError::DivergedStep { x: p0 });
215            }
216            if approx_eq!(p, p0, rtol <= self.rel_tol, atol <= self.abs_tol) {
217                return Ok(p);
218            }
219            p0 = p;
220        }
221        // Report the last point where `f` was actually evaluated rather than
222        // re-evaluating a stepped final iterate, which may lie outside the
223        // callback's valid domain and turn non-convergence into a callback error
224        // or panic. When `max_iter == 0` the loop never runs, so fall back to the
225        // still-in-domain initial guess.
226        let (x, residual) = match last {
227            Some(pair) => pair,
228            None => (p0, eval_finite(&f, p0)?),
229        };
230        Err(RootFinderError::NotConverged {
231            iterations: self.max_iter,
232            x,
233            residual,
234        })
235    }
236}
237
238/// Newton-Raphson method for root-finding (requires derivative).
239///
240/// The tolerances bound the iteration step, not the residual: near steep
241/// features the iteration can stall within tolerance where `f(x)` is not
242/// small and report a false root. Prefer [`Brent`] when a bracket is
243/// available.
244#[derive(Debug, Copy, Clone, PartialEq)]
245pub struct Newton {
246    max_iter: u32,
247    /// Absolute tolerance on the iteration step.
248    abs_tol: f64,
249    /// Relative tolerance on the iteration step.
250    rel_tol: f64,
251}
252
253impl Default for Newton {
254    fn default() -> Self {
255        Self {
256            max_iter: 50,
257            abs_tol: sqrt(f64::EPSILON),
258            rel_tol: sqrt(f64::EPSILON),
259        }
260    }
261}
262
263impl Newton {
264    /// Sets the maximum number of iterations.
265    pub fn with_max_iter(mut self, max_iter: u32) -> Self {
266        self.max_iter = max_iter;
267        self
268    }
269
270    /// Sets the absolute tolerance on the iteration step.
271    pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
272        self.abs_tol = abs_tol;
273        self
274    }
275
276    /// Sets the relative tolerance on the iteration step.
277    pub fn with_rel_tol(mut self, rel_tol: f64) -> Self {
278        self.rel_tol = rel_tol;
279        self
280    }
281}
282
283impl FindRootWithDerivative for Newton {
284    fn find_with_derivative(
285        &self,
286        f: impl Callback,
287        derivative: impl Callback,
288        initial_guess: f64,
289    ) -> Result<f64, RootFinderError> {
290        let mut p0 = initial_guess;
291        let mut last: Option<(f64, f64)> = None;
292        for _ in 0..self.max_iter {
293            let fx = eval_finite(&f, p0)?;
294            // An initial guess that is already a root is returned directly,
295            // avoiding a 0/0 update at a stationary point.
296            if fx == 0.0 {
297                return Ok(p0);
298            }
299            last = Some((p0, fx));
300            let dfx = derivative.call(p0)?;
301            if !dfx.is_finite() {
302                return Err(RootFinderError::NonFiniteDerivative { x: p0, value: dfx });
303            }
304            let p = p0 - fx / dfx;
305            if !p.is_finite() {
306                return Err(RootFinderError::DivergedStep { x: p0 });
307            }
308            if approx_eq!(p, p0, rtol <= self.rel_tol, atol <= self.abs_tol) {
309                return Ok(p);
310            }
311            p0 = p;
312        }
313        // Report the last point where `f` was actually evaluated rather than
314        // re-evaluating a stepped final iterate, which may lie outside the
315        // callback's valid domain and turn non-convergence into a callback error
316        // or panic. When `max_iter == 0` the loop never runs, so fall back to the
317        // still-in-domain initial guess.
318        let (x, residual) = match last {
319            Some(pair) => pair,
320            None => (p0, eval_finite(&f, p0)?),
321        };
322        Err(RootFinderError::NotConverged {
323            iterations: self.max_iter,
324            x,
325            residual,
326        })
327    }
328}
329
330/// Brent's method for bracketed root-finding.
331///
332/// The tolerances bound the root location `x`, not the residual `f(x)`: the
333/// returned root is accurate to within `abs_tol + rel_tol * |x|`, independent of
334/// how the objective is scaled.
335#[derive(Debug, Copy, Clone, PartialEq)]
336pub struct Brent {
337    max_iter: u32,
338    /// Absolute tolerance on the root location.
339    abs_tol: f64,
340    /// Relative tolerance on the root location.
341    rel_tol: f64,
342}
343
344impl Default for Brent {
345    fn default() -> Self {
346        Self {
347            max_iter: 100,
348            abs_tol: 1e-6,
349            rel_tol: sqrt(f64::EPSILON),
350        }
351    }
352}
353
354impl Brent {
355    /// Sets the maximum number of iterations.
356    pub fn with_max_iter(mut self, max_iter: u32) -> Self {
357        self.max_iter = max_iter;
358        self
359    }
360
361    /// Sets the absolute tolerance on the root location.
362    pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
363        self.abs_tol = abs_tol;
364        self
365    }
366
367    /// Sets the relative tolerance on the root location.
368    pub fn with_rel_tol(mut self, rel_tol: f64) -> Self {
369        self.rel_tol = rel_tol;
370        self
371    }
372}
373
374impl FindBracketedRoot for Brent {
375    fn find_in_bracket_with_values(
376        &self,
377        f: impl Callback,
378        bracket: (f64, f64),
379        values: (f64, f64),
380    ) -> Result<f64, RootFinderError> {
381        let mut fblk = 0.0;
382        let mut xblk = 0.0;
383        let (mut xpre, mut xcur) = bracket;
384        let mut spre = 0.0;
385        let mut scur = 0.0;
386
387        let (mut fpre, mut fcur) = values;
388
389        if !fpre.is_finite() {
390            return Err(RootFinderError::NonFinite {
391                x: xpre,
392                value: fpre,
393            });
394        }
395        if !fcur.is_finite() {
396            return Err(RootFinderError::NonFinite {
397                x: xcur,
398                value: fcur,
399            });
400        }
401
402        // An endpoint that is exactly a root is returned directly.
403        if fpre == 0.0 {
404            return Ok(xpre);
405        }
406        if fcur == 0.0 {
407            return Ok(xcur);
408        }
409
410        // The endpoints must straddle the root. Comparing the sign bits of the
411        // two finite, non-zero values avoids the underflow and NaN hazards of
412        // testing the sign of their product.
413        if fpre.is_sign_negative() == fcur.is_sign_negative() {
414            return Err(RootFinderError::NotInBracket);
415        }
416
417        for _ in 0..self.max_iter {
418            // Compare sign bits rather than the product to avoid the underflow
419            // that loses the sign of very small opposite-sign residuals.
420            if fpre.is_sign_negative() != fcur.is_sign_negative() {
421                xblk = xpre;
422                fblk = fpre;
423                spre = xcur - xpre;
424                scur = xcur - xpre;
425            }
426
427            if abs(fblk) < abs(fcur) {
428                xpre = xcur;
429                xcur = xblk;
430                xblk = xpre;
431                fpre = fcur;
432                fcur = fblk;
433                fblk = fpre;
434            }
435
436            let delta = (self.abs_tol + self.rel_tol * abs(xcur)) / 2.0;
437            let sbis = (xblk - xcur) / 2.0;
438
439            if fcur == 0.0 || abs(sbis) < delta {
440                return Ok(xcur);
441            }
442
443            if abs(spre) > delta && abs(fcur) < abs(fpre) {
444                let stry = if approx_eq!(xpre, xblk, rtol <= self.rel_tol) {
445                    // interpolate
446                    -fcur * (xcur - xpre) / (fcur - fpre)
447                } else {
448                    // extrapolate
449                    let dpre = (fpre - fcur) / (xpre - xcur);
450                    let dblk = (fblk - fcur) / (xblk - xcur);
451                    -fcur * (fblk * dblk - fpre * dpre) / (dblk * dpre * (fblk - fpre))
452                };
453
454                if 2.0 * abs(stry) < abs(spre).min(3.0 * abs(sbis) - delta) {
455                    spre = scur;
456                    scur = stry;
457                } else {
458                    // bisect
459                    spre = sbis;
460                    scur = sbis;
461                }
462            } else {
463                // bisect
464                spre = sbis;
465                scur = sbis;
466            }
467
468            xpre = xcur;
469            fpre = fcur;
470
471            if abs(scur) > delta {
472                xcur += scur
473            } else {
474                xcur += if sbis > 0.0 { delta } else { -delta };
475            }
476
477            fcur = eval_finite(&f, xcur)?;
478        }
479
480        Err(RootFinderError::NotConverged {
481            iterations: self.max_iter,
482            x: xcur,
483            residual: fcur,
484        })
485    }
486}
487
488/// Safeguarded ("rtsafe") Newton method for bracketed root-finding.
489///
490/// Takes a Newton step when it lands inside the current bracket and shrinks the
491/// interval quickly enough; otherwise bisects. This retains the guaranteed
492/// convergence of bisection while gaining Newton's quadratic rate near the
493/// root. A zero, non-finite, or otherwise unhelpful derivative simply yields a
494/// bisection step, so the method never fails as long as a valid bracket is
495/// maintained.
496///
497/// The tolerances bound the root location `x`, not the residual `f(x)`: the
498/// returned root is accurate to within `abs_tol + rel_tol * |x|`, independent of
499/// how the objective is scaled.
500///
501/// # References
502///
503/// - Press et al., *Numerical Recipes*, 3rd ed., §9.4 ("Newton-Raphson Method
504///   Using Derivative", `rtsafe`).
505#[derive(Debug, Copy, Clone, PartialEq)]
506pub struct BracketedNewton {
507    max_iter: u32,
508    /// Absolute tolerance on the root location.
509    abs_tol: f64,
510    /// Relative tolerance on the root location.
511    rel_tol: f64,
512}
513
514impl Default for BracketedNewton {
515    fn default() -> Self {
516        Self {
517            max_iter: 100,
518            abs_tol: 1e-6,
519            rel_tol: sqrt(f64::EPSILON),
520        }
521    }
522}
523
524impl BracketedNewton {
525    /// Sets the maximum number of iterations.
526    pub fn with_max_iter(mut self, max_iter: u32) -> Self {
527        self.max_iter = max_iter;
528        self
529    }
530
531    /// Sets the absolute tolerance on the root location.
532    pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
533        self.abs_tol = abs_tol;
534        self
535    }
536
537    /// Sets the relative tolerance on the root location.
538    pub fn with_rel_tol(mut self, rel_tol: f64) -> Self {
539        self.rel_tol = rel_tol;
540        self
541    }
542}
543
544impl FindBracketedRootWithDerivative for BracketedNewton {
545    fn find_in_bracket_with_derivative_values(
546        &self,
547        f: impl CallbackWithDerivative,
548        bracket: (f64, f64),
549        values: (f64, f64),
550    ) -> Result<f64, RootFinderError> {
551        let (x1, x2) = bracket;
552        let (f1, f2) = values;
553
554        if !f1.is_finite() {
555            return Err(RootFinderError::NonFinite { x: x1, value: f1 });
556        }
557        if !f2.is_finite() {
558            return Err(RootFinderError::NonFinite { x: x2, value: f2 });
559        }
560
561        // An endpoint that is exactly a root is returned directly.
562        if f1 == 0.0 {
563            return Ok(x1);
564        }
565        if f2 == 0.0 {
566            return Ok(x2);
567        }
568
569        // The endpoints must straddle the root. Comparing sign bits avoids the
570        // underflow that loses the sign of very small opposite-sign residuals.
571        if f1.is_sign_negative() == f2.is_sign_negative() {
572            return Err(RootFinderError::NotInBracket);
573        }
574
575        // Orient the bracket so that `f(xl) < 0 < f(xh)`.
576        let (mut xl, mut xh) = if f1.is_sign_negative() {
577            (x1, x2)
578        } else {
579            (x2, x1)
580        };
581
582        let mut rts = 0.5 * (x1 + x2);
583        let mut dx_old = abs(x2 - x1);
584        let mut dx = dx_old;
585        let (mut fx, mut dfx) = eval_finite_with_derivative(&f, rts)?;
586        // The midpoint may already be the root, in which case the derivative is
587        // never used (and could give a 0/0 step if it also vanishes there).
588        if fx == 0.0 {
589            return Ok(rts);
590        }
591
592        for _ in 0..self.max_iter {
593            let delta = self.abs_tol + self.rel_tol * abs(rts);
594
595            // Bisect when the derivative is unusable, when the Newton iterate
596            // would leave the bracket, or when it is not shrinking the interval
597            // fast enough; otherwise take the Newton step.
598            let bisect = !dfx.is_finite()
599                || ((rts - xh) * dfx - fx) * ((rts - xl) * dfx - fx) > 0.0
600                || abs(2.0 * fx) > abs(dx_old * dfx);
601
602            if bisect {
603                dx_old = dx;
604                dx = 0.5 * (xh - xl);
605                rts = xl + dx;
606                // The bisection step is below the representable resolution.
607                if xl == rts {
608                    return Ok(rts);
609                }
610            } else {
611                dx_old = dx;
612                dx = fx / dfx;
613                let prev = rts;
614                rts -= dx;
615                if prev == rts {
616                    return Ok(rts);
617                }
618            }
619
620            if abs(dx) < delta {
621                return Ok(rts);
622            }
623
624            (fx, dfx) = eval_finite_with_derivative(&f, rts)?;
625            if fx == 0.0 {
626                return Ok(rts);
627            }
628            // Maintain the sign-oriented bracket around the new iterate.
629            if fx.is_sign_negative() {
630                xl = rts;
631            } else {
632                xh = rts;
633            }
634        }
635
636        Err(RootFinderError::NotConverged {
637            iterations: self.max_iter,
638            x: rts,
639            residual: fx,
640        })
641    }
642}
643
644#[cfg(test)]
645mod tests {
646    use alloc::string::ToString;
647    use core::f64::consts::PI;
648    use lox_approx::assert_approx_eq;
649
650    use super::*;
651    use crate::error::BoxedError;
652    use crate::math::callback::Fallible;
653    use crate::math::float::{cos, sin};
654
655    type Result = core::result::Result<f64, BoxedError>;
656
657    #[test]
658    fn test_newton_kepler() {
659        fn mean_to_ecc(mean: f64, eccentricity: f64) -> core::result::Result<f64, RootFinderError> {
660            let newton = Newton::default();
661            newton.find_with_derivative(
662                |e: f64| e - eccentricity * sin(e) - mean,
663                |e: f64| 1.0 - eccentricity * cos(e),
664                mean,
665            )
666        }
667        let act = mean_to_ecc(PI / 2.0, 0.3).expect("should converge");
668        assert_approx_eq!(act, 1.85846841205333, rtol <= 1e-8);
669    }
670
671    #[test]
672    fn test_newton_cubic() {
673        let newton = Newton::default();
674        let act = newton
675            .find_with_derivative(
676                |x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0,
677                |x: f64| 2.0 * powi(x, 2) + 8.0 * x,
678                1.5,
679            )
680            .expect("should converge");
681        assert_approx_eq!(act, 1.3652300134140969, rtol <= 1e-8);
682    }
683
684    #[test]
685    fn test_newton_exact_root_initial_guess() {
686        // f(x) = x^2, f'(x) = 2x. At x = 0 both are zero; the guess is already
687        // the root and must be returned rather than producing 0/0 = NaN.
688        let newton = Newton::default();
689        let act = newton
690            .find_with_derivative(|x: f64| powi(x, 2), |x: f64| 2.0 * x, 0.0)
691            .expect("guess is already the root");
692        assert_eq!(act, 0.0);
693    }
694
695    #[test]
696    fn test_newton_zero_derivative_diverges() {
697        // f(x) = x^2 + 1 has no real root; at x = 0 the derivative is zero, so
698        // the update diverges and must be reported as a diverged step.
699        let newton = Newton::default();
700        let err = newton
701            .find_with_derivative(|x: f64| powi(x, 2) + 1.0, |x: f64| 2.0 * x, 0.0)
702            .unwrap_err();
703        assert!(matches!(err, RootFinderError::DivergedStep { x } if x == 0.0));
704    }
705
706    #[test]
707    fn test_newton_large_root_relative_tolerance() {
708        // A root at 1e8 is unreachable by an absolute step tolerance of
709        // sqrt(EPSILON); the relative tolerance lets it converge.
710        let newton = Newton::default();
711        let act = newton
712            .find_with_derivative(|x: f64| powi(x, 2) - 1e16, |x: f64| 2.0 * x, 9e7)
713            .expect("should converge");
714        assert_approx_eq!(act, 1e8, rtol <= 1e-9);
715    }
716
717    #[test]
718    fn test_steffensen_exact_root_initial_guess() {
719        // f(x) = x^2 - 4 has a root at 2; the guess is already the root.
720        let steffensen = Steffensen::default();
721        let act = steffensen
722            .find(|x: f64| powi(x, 2) - 4.0, 2.0)
723            .expect("guess is already the root");
724        assert_eq!(act, 2.0);
725    }
726
727    #[test]
728    fn test_steffensen_zero_denominator_diverges() {
729        // A constant non-zero residual makes the Aitken denominator vanish; the
730        // update diverges and must be reported as a diverged step.
731        let steffensen = Steffensen::default();
732        let err = steffensen.find(|_x: f64| 1.0, 0.0).unwrap_err();
733        assert!(matches!(err, RootFinderError::DivergedStep { x } if x == 0.0));
734    }
735
736    #[test]
737    fn test_steffensen_cubic() {
738        let steffensen = Steffensen::default();
739        let act = steffensen
740            .find(|x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0, 1.5)
741            .expect("should converge");
742        assert_approx_eq!(act, 1.3652300134140969, rtol <= 1e-8);
743    }
744
745    #[test]
746    fn test_brent_cubic() {
747        let brent = Brent::default();
748        let act = brent
749            .find_in_bracket(|x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0, (1.0, 1.5))
750            .expect("should converge");
751        assert_approx_eq!(act, 1.3652300134140969, rtol <= 1e-8);
752    }
753
754    #[test]
755    #[should_panic(expected = "derivative failed")]
756    fn test_newton_kepler_callback_error() {
757        let newton = Newton::default();
758        newton
759            .find_with_derivative(
760                |e: f64| e,
761                Fallible(|_e: f64| -> Result { Err("derivative failed".into()) }),
762                1.0,
763            )
764            .unwrap();
765    }
766
767    #[test]
768    #[should_panic(expected = "f failed")]
769    fn test_steffensen_cubic_error() {
770        let steffensen = Steffensen::default();
771        // function errors immediately
772        steffensen
773            .find(Fallible(|_x| -> Result { Err("f failed".into()) }), 1.0)
774            .unwrap();
775    }
776
777    #[test]
778    #[should_panic(expected = "negative x")]
779    fn test_brent_cubic_error() {
780        let brent = Brent::default();
781        // error at bracket endpoint, then during iteration
782        brent
783            .find_in_bracket(
784                Fallible(|x: f64| -> Result {
785                    if x.is_sign_negative() {
786                        Err("negative x".into())
787                    } else {
788                        Ok(x * x - 2.0)
789                    }
790                }),
791                (-1.0, 2.0),
792            )
793            .unwrap();
794    }
795
796    #[test]
797    fn test_find_in_bracket_with_values_reuses_endpoints() {
798        use core::cell::Cell;
799
800        let f = |x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0;
801        let (a, b) = (1.0, 1.5);
802        let fa = powi(a, 3) + 4.0 * powi(a, 2) - 10.0;
803        let fb = powi(b, 3) + 4.0 * powi(b, 2) - 10.0;
804
805        // The value-reuse entry point agrees with the recomputing one.
806        let brent = Brent::default();
807        let via_values = brent
808            .find_in_bracket_with_values(f, (a, b), (fa, fb))
809            .expect("should converge");
810        let via_recompute = brent.find_in_bracket(f, (a, b)).expect("should converge");
811        assert_approx_eq!(via_values, via_recompute, rtol <= 1e-12);
812
813        // The supplied endpoints are not re-evaluated.
814        let count = Cell::new(0usize);
815        let counting = |x: f64| {
816            if x == a || x == b {
817                count.set(count.get() + 1);
818            }
819            powi(x, 3) + 4.0 * powi(x, 2) - 10.0
820        };
821        Brent::default()
822            .find_in_bracket_with_values(counting, (a, b), (fa, fb))
823            .expect("should converge");
824        assert_eq!(count.get(), 0, "endpoints must not be re-evaluated");
825    }
826
827    #[test]
828    fn test_brent_rejects_non_finite_endpoint() {
829        let brent = Brent::default();
830        let err = brent
831            .find_in_bracket_with_values(|_x: f64| 1.0, (0.0, 1.0), (f64::NAN, 1.0))
832            .unwrap_err();
833        assert!(matches!(err, RootFinderError::NonFinite { .. }));
834    }
835
836    #[test]
837    fn test_brent_rejects_same_sign_underflowing_bracket() {
838        // Same-sign endpoints whose product underflows to 0.0 must still be
839        // rejected rather than accepted as a bracket (and returned as a root).
840        let brent = Brent::default();
841        let err = brent
842            .find_in_bracket(|x: f64| 1e-200 * (x + 1.0), (0.0, 1.0))
843            .unwrap_err();
844        assert!(matches!(err, RootFinderError::NotInBracket));
845    }
846
847    #[test]
848    fn test_brent_scale_independent() {
849        // A heavily down-scaled objective: f(0) = -1e-6 must not be mistaken for
850        // a root by a residual tolerance. The true root is at x = 1e6.
851        let brent = Brent::default();
852        let act = brent
853            .find_in_bracket(|x: f64| 1e-12 * (x - 1e6), (0.0, 2e6))
854            .expect("should converge");
855        assert_approx_eq!(act, 1e6, rtol <= 1e-5);
856    }
857
858    #[test]
859    fn test_brent_builder_tolerances() {
860        // Custom tolerances set via the builder still locate the root.
861        let brent = Brent::default()
862            .with_abs_tol(1e-2)
863            .with_rel_tol(1e-8)
864            .with_max_iter(50);
865        let act = brent
866            .find_in_bracket(|x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0, (1.0, 1.5))
867            .expect("should converge");
868        assert_approx_eq!(act, 1.3652300134140969, rtol <= 1e-2);
869    }
870
871    #[test]
872    fn test_newton_builder_max_iter() {
873        // A single iteration is not enough to converge on the cubic root, and
874        // the configured cap is reported back in the error.
875        let newton = Newton::default().with_max_iter(1);
876        let err = newton
877            .find_with_derivative(
878                |x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0,
879                |x: f64| 2.0 * powi(x, 2) + 8.0 * x,
880                1.5,
881            )
882            .unwrap_err();
883        assert!(matches!(
884            err,
885            RootFinderError::NotConverged { iterations: 1, .. }
886        ));
887    }
888
889    #[test]
890    fn test_brent_tiny_opposite_sign_residuals() {
891        // Residuals so small their product underflows to a signed zero; the
892        // in-loop bracketing must recognise the sign change from the sign bits
893        // rather than the product, which would otherwise return the endpoint.
894        let brent = Brent::default();
895        let act = brent
896            .find_in_bracket(|x: f64| 1e-200 * (x - 0.5), (0.0, 1.0))
897            .expect("should converge");
898        assert_approx_eq!(act, 0.5, rtol <= 1e-8);
899    }
900
901    #[test]
902    fn test_brent_interior_non_finite() {
903        // Finite, opposite-sign endpoints but a non-finite value at an interior
904        // point must be reported rather than silently accepted as a root.
905        let calls = core::cell::Cell::new(0u32);
906        let brent = Brent::default();
907        let err = brent
908            .find_in_bracket(
909                |x: f64| {
910                    let n = calls.get();
911                    calls.set(n + 1);
912                    // The first two calls evaluate the bracket endpoints.
913                    if n >= 2 { f64::NAN } else { x - 0.5 }
914                },
915                (0.0, 1.0),
916            )
917            .unwrap_err();
918        assert!(matches!(err, RootFinderError::NonFinite { .. }));
919    }
920
921    #[test]
922    fn test_newton_not_converged_does_not_re_evaluate() {
923        // The final iterate steps outside the callback's valid domain. Reaching
924        // the iteration cap must still yield NotConverged, not a callback error
925        // from re-evaluating the un-checked final iterate.
926        let newton = Newton::default().with_max_iter(1);
927        let err = newton
928            .find_with_derivative(
929                Fallible(|x: f64| -> Result {
930                    if x > 5.0 {
931                        Err("out of domain".into())
932                    } else {
933                        Ok(x * x - 2.0)
934                    }
935                }),
936                |_x: f64| 0.1,
937                1.0,
938            )
939            .unwrap_err();
940        assert!(matches!(err, RootFinderError::NotConverged { .. }));
941    }
942
943    #[test]
944    fn test_newton_zero_max_iter_reports_real_residual() {
945        // With no iterations the solver still reports a finite residual measured
946        // at the initial guess, not NaN.
947        let newton = Newton::default().with_max_iter(0);
948        let err = newton
949            .find_with_derivative(|x: f64| x * x - 2.0, |x: f64| 2.0 * x, 1.0)
950            .unwrap_err();
951        match err {
952            RootFinderError::NotConverged { x, residual, .. } => {
953                assert_eq!(x, 1.0);
954                assert_approx_eq!(residual, -1.0, atol <= 1e-12);
955            }
956            other => panic!("expected NotConverged, got {other:?}"),
957        }
958    }
959
960    #[test]
961    fn test_steffensen_zero_max_iter_reports_real_residual() {
962        let steffensen = Steffensen::default().with_max_iter(0);
963        let err = steffensen.find(|x: f64| x * x - 2.0, 1.0).unwrap_err();
964        match err {
965            RootFinderError::NotConverged { x, residual, .. } => {
966                assert_eq!(x, 1.0);
967                assert_approx_eq!(residual, -1.0, atol <= 1e-12);
968            }
969            other => panic!("expected NotConverged, got {other:?}"),
970        }
971    }
972
973    #[test]
974    fn test_root_finder_error_display() {
975        let not_converged = RootFinderError::NotConverged {
976            iterations: 7,
977            x: 1.5,
978            residual: -0.25,
979        };
980        assert_eq!(
981            not_converged.to_string(),
982            "not converged after 7 iterations at x = 1.5, residual -0.25"
983        );
984        assert_eq!(
985            RootFinderError::NotInBracket.to_string(),
986            "root not in bracket"
987        );
988        assert_eq!(
989            RootFinderError::NonFinite {
990                x: 1.5,
991                value: f64::INFINITY
992            }
993            .to_string(),
994            "function returned a non-finite value (inf) at x = 1.5"
995        );
996        assert_eq!(
997            RootFinderError::NonFiniteDerivative {
998                x: 1.5,
999                value: f64::NAN
1000            }
1001            .to_string(),
1002            "derivative returned a non-finite value (NaN) at x = 1.5"
1003        );
1004        assert_eq!(
1005            RootFinderError::DivergedStep { x: 0.5 }.to_string(),
1006            "iteration step diverged at x = 0.5"
1007        );
1008    }
1009
1010    #[test]
1011    fn test_steffensen_builder_tolerances() {
1012        // Custom tolerances set via the builder still locate the root.
1013        let steffensen = Steffensen::default()
1014            .with_abs_tol(1e-4)
1015            .with_rel_tol(1e-8)
1016            .with_max_iter(100);
1017        let act = steffensen
1018            .find(|x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0, 1.5)
1019            .expect("should converge");
1020        assert_approx_eq!(act, 1.3652300134140969, rtol <= 1e-3);
1021    }
1022
1023    #[test]
1024    fn test_newton_builder_tolerances() {
1025        let newton = Newton::default().with_abs_tol(1e-4).with_rel_tol(1e-8);
1026        let act = newton
1027            .find_with_derivative(
1028                |x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0,
1029                |x: f64| 2.0 * powi(x, 2) + 8.0 * x,
1030                1.5,
1031            )
1032            .expect("should converge");
1033        assert_approx_eq!(act, 1.3652300134140969, rtol <= 1e-3);
1034    }
1035
1036    #[test]
1037    fn test_steffensen_non_finite_function_value() {
1038        // A non-finite value at the initial guess is reported immediately.
1039        let steffensen = Steffensen::default();
1040        let err = steffensen.find(|_x: f64| f64::INFINITY, 1.0).unwrap_err();
1041        assert!(matches!(err, RootFinderError::NonFinite { .. }));
1042    }
1043
1044    #[test]
1045    fn test_steffensen_non_finite_aitken_probe() {
1046        // The first sample is finite, but the displaced Aitken probe f(p0 + f(p0))
1047        // is non-finite and must be reported.
1048        let steffensen = Steffensen::default();
1049        let err = steffensen
1050            .find(|x: f64| if x >= 2.0 { f64::INFINITY } else { x }, 1.5)
1051            .unwrap_err();
1052        assert!(matches!(err, RootFinderError::NonFinite { .. }));
1053    }
1054
1055    #[test]
1056    fn test_steffensen_not_converged_reports_last_evaluation() {
1057        // One iteration cannot converge; the error reports the point that was
1058        // actually evaluated (the initial guess) and its residual.
1059        let steffensen = Steffensen::default().with_max_iter(1);
1060        let err = steffensen
1061            .find(|x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0, 1.5)
1062            .unwrap_err();
1063        match err {
1064            RootFinderError::NotConverged {
1065                iterations: 1,
1066                x,
1067                residual,
1068            } => {
1069                assert_eq!(x, 1.5);
1070                assert_approx_eq!(residual, 2.375, atol <= 1e-9);
1071            }
1072            other => panic!("expected NotConverged, got {other:?}"),
1073        }
1074    }
1075
1076    #[test]
1077    fn test_newton_non_finite_function_value() {
1078        let newton = Newton::default();
1079        let err = newton
1080            .find_with_derivative(|_x: f64| f64::INFINITY, |_x: f64| 1.0, 1.0)
1081            .unwrap_err();
1082        assert!(matches!(err, RootFinderError::NonFinite { .. }));
1083    }
1084
1085    #[test]
1086    fn test_newton_non_finite_derivative_value() {
1087        // A finite function value but a non-finite derivative is reported
1088        // against the derivative, not the objective.
1089        let newton = Newton::default();
1090        let err = newton
1091            .find_with_derivative(|x: f64| x, |_x: f64| f64::INFINITY, 1.0)
1092            .unwrap_err();
1093        assert!(matches!(
1094            err,
1095            RootFinderError::NonFiniteDerivative { x, .. } if x == 1.0
1096        ));
1097    }
1098
1099    #[test]
1100    fn test_brent_rejects_non_finite_second_endpoint() {
1101        // The first endpoint is finite; the second is not.
1102        let brent = Brent::default();
1103        let err = brent
1104            .find_in_bracket_with_values(|_x: f64| 1.0, (0.0, 1.0), (1.0, f64::NAN))
1105            .unwrap_err();
1106        assert!(matches!(err, RootFinderError::NonFinite { .. }));
1107    }
1108
1109    #[test]
1110    fn test_brent_endpoint_is_root() {
1111        let brent = Brent::default();
1112        // The lower endpoint is exactly the root.
1113        let lo = brent
1114            .find_in_bracket_with_values(|x: f64| x, (0.0, 1.0), (0.0, 1.0))
1115            .expect("lower endpoint is the root");
1116        assert_eq!(lo, 0.0);
1117        // The upper endpoint is exactly the root.
1118        let hi = brent
1119            .find_in_bracket_with_values(|x: f64| x - 1.0, (0.0, 1.0), (-1.0, 0.0))
1120            .expect("upper endpoint is the root");
1121        assert_eq!(hi, 1.0);
1122    }
1123
1124    #[test]
1125    fn test_brent_not_converged() {
1126        // A single iteration cannot narrow a wide bracket below the tolerance.
1127        let brent = Brent::default().with_max_iter(1);
1128        let err = brent
1129            .find_in_bracket(|x: f64| powi(x, 3) - 0.5, (-1e6, 1e6))
1130            .unwrap_err();
1131        assert!(matches!(
1132            err,
1133            RootFinderError::NotConverged { iterations: 1, .. }
1134        ));
1135    }
1136
1137    #[test]
1138    fn test_zero_max_iter_non_finite_initial_guess() {
1139        // With no iterations the initial guess is still evaluated; a non-finite
1140        // value there is reported as NonFinite, not NotConverged.
1141        let newton = Newton::default().with_max_iter(0);
1142        let err = newton
1143            .find_with_derivative(|_x: f64| f64::INFINITY, |_x: f64| 1.0, 1.0)
1144            .unwrap_err();
1145        assert!(matches!(err, RootFinderError::NonFinite { .. }));
1146    }
1147
1148    #[test]
1149    fn test_brent_converges_on_stiff_function() {
1150        // A high-degree monomial has extreme curvature near its root, which
1151        // exercises Brent's switching between interpolation and bisection.
1152        let brent = Brent::default();
1153        let root = brent
1154            .find_in_bracket(|x: f64| powi(x, 15) - 0.5, (0.0, 1.0))
1155            .expect("should converge");
1156        assert!(abs(powi(root, 15) - 0.5) < 1e-3);
1157    }
1158
1159    #[test]
1160    fn test_brent_in_loop_callback_error() {
1161        // Endpoints evaluate cleanly, but an interior evaluation fails; the
1162        // callback error must propagate.
1163        let brent = Brent::default();
1164        let err = brent
1165            .find_in_bracket_with_values(
1166                Fallible(|x: f64| -> Result {
1167                    if x == -1.0 || x == 2.0 {
1168                        Ok(x * x - 2.0)
1169                    } else {
1170                        Err("interior failure".into())
1171                    }
1172                }),
1173                (-1.0, 2.0),
1174                (-1.0, 2.0),
1175            )
1176            .unwrap_err();
1177        assert!(matches!(err, RootFinderError::Callback(_)));
1178    }
1179
1180    // -----------------------------------------------------------------------
1181    // BracketedNewton (safeguarded Newton / rtsafe)
1182    // -----------------------------------------------------------------------
1183
1184    #[test]
1185    fn test_bracketed_newton_cubic() {
1186        let solver = BracketedNewton::default();
1187        let act = solver
1188            .find_in_bracket_with_derivative(
1189                |x: f64| {
1190                    (
1191                        powi(x, 3) + 4.0 * powi(x, 2) - 10.0,
1192                        3.0 * powi(x, 2) + 8.0 * x,
1193                    )
1194                },
1195                (1.0, 1.5),
1196            )
1197            .expect("should converge");
1198        assert_approx_eq!(act, 1.3652300134140969, rtol <= 1e-8);
1199    }
1200
1201    #[test]
1202    fn test_bracketed_newton_matches_brent() {
1203        // On a smooth function both solvers must land on the same root.
1204        let f = |x: f64| powi(x, 3) + 4.0 * powi(x, 2) - 10.0;
1205        let bracket = (1.0, 2.0);
1206
1207        let newton = BracketedNewton::default()
1208            .find_in_bracket_with_derivative(
1209                |x: f64| {
1210                    (
1211                        powi(x, 3) + 4.0 * powi(x, 2) - 10.0,
1212                        3.0 * powi(x, 2) + 8.0 * x,
1213                    )
1214                },
1215                bracket,
1216            )
1217            .expect("should converge");
1218        let brent = Brent::default()
1219            .find_in_bracket(f, bracket)
1220            .expect("should converge");
1221        assert_approx_eq!(newton, brent, rtol <= 1e-9);
1222    }
1223
1224    #[test]
1225    fn test_bracketed_newton_reuses_endpoints() {
1226        use core::cell::Cell;
1227
1228        let (a, b) = (1.0, 1.5);
1229        let fa = powi(a, 3) + 4.0 * powi(a, 2) - 10.0;
1230        let fb = powi(b, 3) + 4.0 * powi(b, 2) - 10.0;
1231
1232        let count = Cell::new(0usize);
1233        let counting = |x: f64| {
1234            if x == a || x == b {
1235                count.set(count.get() + 1);
1236            }
1237            powi(x, 3) + 4.0 * powi(x, 2) - 10.0
1238        };
1239        BracketedNewton::default()
1240            .find_in_bracket_with_derivative_values(
1241                |x: f64| (counting(x), 3.0 * powi(x, 2) + 8.0 * x),
1242                (a, b),
1243                (fa, fb),
1244            )
1245            .expect("should converge");
1246        assert_eq!(count.get(), 0, "endpoints must not be re-evaluated");
1247    }
1248
1249    #[test]
1250    fn test_bracketed_newton_endpoint_is_root() {
1251        let solver = BracketedNewton::default();
1252        let lo = solver
1253            .find_in_bracket_with_derivative_values(|x: f64| (x, 1.0), (0.0, 1.0), (0.0, 1.0))
1254            .expect("lower endpoint is the root");
1255        assert_eq!(lo, 0.0);
1256    }
1257
1258    #[test]
1259    fn test_bracketed_newton_rejects_non_bracket() {
1260        let solver = BracketedNewton::default();
1261        let err = solver
1262            .find_in_bracket_with_derivative_values(
1263                |x: f64| (x * x + 1.0, 2.0 * x),
1264                (1.0, 2.0),
1265                (2.0, 5.0),
1266            )
1267            .unwrap_err();
1268        assert!(matches!(err, RootFinderError::NotInBracket));
1269    }
1270
1271    #[test]
1272    fn test_bracketed_newton_zero_derivative_bisects() {
1273        // A derivative that is zero at the midpoint would give a 0/0 Newton
1274        // step; the safeguard must fall back to bisection and still converge.
1275        // f(x) = x^3 has f'(0) = 0 exactly at the bracket midpoint (-1, 1).
1276        let solver = BracketedNewton::default();
1277        let act = solver
1278            .find_in_bracket_with_derivative(|x: f64| (powi(x, 3), 3.0 * powi(x, 2)), (-1.0, 1.0))
1279            .expect("should converge via bisection fallback");
1280        assert_approx_eq!(act, 0.0, atol <= 1e-6);
1281    }
1282
1283    #[test]
1284    fn test_bracketed_newton_non_finite_derivative_bisects() {
1285        // A derivative that blows up must not fail the solve: the safeguard
1286        // bisects instead, keeping the bracket and converging on the root.
1287        let solver = BracketedNewton::default();
1288        let act = solver
1289            .find_in_bracket_with_derivative(|x: f64| (x - 0.5, f64::INFINITY), (0.0, 1.0))
1290            .expect("should converge despite non-finite derivative");
1291        assert_approx_eq!(act, 0.5, atol <= 1e-6);
1292    }
1293
1294    #[test]
1295    fn test_bracketed_newton_scale_independent() {
1296        // A heavily down-scaled objective must not be mistaken for a root by a
1297        // residual tolerance; the true root is at x = 1e6.
1298        let solver = BracketedNewton::default();
1299        let act = solver
1300            .find_in_bracket_with_derivative(|x: f64| (1e-12 * (x - 1e6), 1e-12), (0.0, 2e6))
1301            .expect("should converge");
1302        assert_approx_eq!(act, 1e6, rtol <= 1e-5);
1303    }
1304
1305    #[test]
1306    fn test_bracketed_newton_rejects_non_finite_endpoint() {
1307        let solver = BracketedNewton::default();
1308        let err = solver
1309            .find_in_bracket_with_derivative_values(
1310                |_x: f64| (1.0, 1.0),
1311                (0.0, 1.0),
1312                (f64::NAN, 1.0),
1313            )
1314            .unwrap_err();
1315        assert!(matches!(err, RootFinderError::NonFinite { .. }));
1316    }
1317
1318    #[test]
1319    fn test_bracketed_newton_not_converged() {
1320        // A single iteration cannot narrow a wide bracket below the tolerance.
1321        let solver = BracketedNewton::default().with_max_iter(1);
1322        let err = solver
1323            .find_in_bracket_with_derivative(
1324                |x: f64| (powi(x, 3) - 0.5, 3.0 * powi(x, 2)),
1325                (-1e6, 1e6),
1326            )
1327            .unwrap_err();
1328        assert!(matches!(
1329            err,
1330            RootFinderError::NotConverged { iterations: 1, .. }
1331        ));
1332    }
1333
1334    #[test]
1335    fn test_bracketed_newton_in_loop_callback_error() {
1336        // Endpoints evaluate cleanly, but an interior evaluation fails; the
1337        // callback error must propagate.
1338        struct FailsInterior;
1339
1340        impl CallbackWithDerivative for FailsInterior {
1341            fn call(&self, x: f64) -> core::result::Result<(f64, f64), LoxError> {
1342                if x == -1.0 || x == 2.0 {
1343                    Ok((x * x - 2.0, 2.0 * x))
1344                } else {
1345                    Err("interior failure".into())
1346                }
1347            }
1348        }
1349
1350        let solver = BracketedNewton::default();
1351        let err = solver
1352            .find_in_bracket_with_derivative_values(FailsInterior, (-1.0, 2.0), (-1.0, 2.0))
1353            .unwrap_err();
1354        assert!(matches!(err, RootFinderError::Callback(_)));
1355    }
1356}