1use 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
15pub trait FindRoot {
17 fn find(&self, f: impl Callback, initial_guess: f64) -> Result<f64, RootFinderError>;
19}
20
21pub trait FindRootWithDerivative {
23 fn find_with_derivative(
25 &self,
26 f: impl Callback,
27 derivative: impl Callback,
28 initial_guess: f64,
29 ) -> Result<f64, RootFinderError>;
30}
31
32pub trait FindBracketedRoot {
34 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 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
57pub trait FindBracketedRootWithDerivative {
59 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 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#[derive(Debug, Error)]
84pub enum RootFinderError {
85 #[error("not converged after {iterations} iterations at x = {x}, residual {residual}")]
87 NotConverged {
88 iterations: u32,
90 x: f64,
92 residual: f64,
94 },
95 #[error("root not in bracket")]
97 NotInBracket,
98 #[error("function returned a non-finite value ({value}) at x = {x}")]
100 NonFinite {
101 x: f64,
103 value: f64,
105 },
106 #[error("derivative returned a non-finite value ({value}) at x = {x}")]
108 NonFiniteDerivative {
109 x: f64,
111 value: f64,
113 },
114 #[error("iteration step diverged at x = {x}")]
117 DivergedStep {
118 x: f64,
120 },
121 #[error(transparent)]
123 Callback(#[from] LoxError),
124}
125
126fn 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
136fn 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#[derive(Debug, Copy, Clone, PartialEq)]
159pub struct Steffensen {
160 max_iter: u32,
161 abs_tol: f64,
163 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 pub fn with_max_iter(mut self, max_iter: u32) -> Self {
180 self.max_iter = max_iter;
181 self
182 }
183
184 pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
186 self.abs_tol = abs_tol;
187 self
188 }
189
190 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 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 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#[derive(Debug, Copy, Clone, PartialEq)]
245pub struct Newton {
246 max_iter: u32,
247 abs_tol: f64,
249 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 pub fn with_max_iter(mut self, max_iter: u32) -> Self {
266 self.max_iter = max_iter;
267 self
268 }
269
270 pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
272 self.abs_tol = abs_tol;
273 self
274 }
275
276 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 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 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#[derive(Debug, Copy, Clone, PartialEq)]
336pub struct Brent {
337 max_iter: u32,
338 abs_tol: f64,
340 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 pub fn with_max_iter(mut self, max_iter: u32) -> Self {
357 self.max_iter = max_iter;
358 self
359 }
360
361 pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
363 self.abs_tol = abs_tol;
364 self
365 }
366
367 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 if fpre == 0.0 {
404 return Ok(xpre);
405 }
406 if fcur == 0.0 {
407 return Ok(xcur);
408 }
409
410 if fpre.is_sign_negative() == fcur.is_sign_negative() {
414 return Err(RootFinderError::NotInBracket);
415 }
416
417 for _ in 0..self.max_iter {
418 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 -fcur * (xcur - xpre) / (fcur - fpre)
447 } else {
448 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 spre = sbis;
460 scur = sbis;
461 }
462 } else {
463 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#[derive(Debug, Copy, Clone, PartialEq)]
506pub struct BracketedNewton {
507 max_iter: u32,
508 abs_tol: f64,
510 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 pub fn with_max_iter(mut self, max_iter: u32) -> Self {
527 self.max_iter = max_iter;
528 self
529 }
530
531 pub fn with_abs_tol(mut self, abs_tol: f64) -> Self {
533 self.abs_tol = abs_tol;
534 self
535 }
536
537 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 if f1 == 0.0 {
563 return Ok(x1);
564 }
565 if f2 == 0.0 {
566 return Ok(x2);
567 }
568
569 if f1.is_sign_negative() == f2.is_sign_negative() {
572 return Err(RootFinderError::NotInBracket);
573 }
574
575 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 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 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 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 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 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}