1use nalgebra::{DMatrix, DVector};
24use ogeom_core::{OgeomResult, ogeom_bail};
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Convergence {
29 Residual,
31 Step,
33 Exhausted,
36}
37
38impl Convergence {
39 #[must_use]
41 pub const fn is_converged(self) -> bool {
42 !matches!(self, Self::Exhausted)
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct Solution {
49 pub value: f64,
51 pub residual: f64,
53 pub convergence: Convergence,
55 pub iterations: usize,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct Criteria {
62 pub residual: f64,
64 pub step: f64,
66 pub max_iterations: usize,
68}
69
70impl Default for Criteria {
71 fn default() -> Self {
72 Self {
73 residual: 1e-13,
76 step: 1e-14,
77 max_iterations: 100,
78 }
79 }
80}
81
82impl Criteria {
83 #[must_use]
85 pub fn with_residual(residual: f64) -> Self {
86 Self {
87 residual,
88 ..Self::default()
89 }
90 }
91}
92
93pub fn brent<F>(mut f: F, a: f64, b: f64, criteria: Criteria) -> OgeomResult<Solution>
106where
107 F: FnMut(f64) -> f64,
108{
109 if !a.is_finite() || !b.is_finite() || a >= b {
110 ogeom_bail!(Construction, "bracket [{a}, {b}] is empty or non-finite");
111 }
112 let (mut fa, mut fb) = (f(a), f(b));
113 if fa == 0.0 {
114 return Ok(Solution {
115 value: a,
116 residual: 0.0,
117 convergence: Convergence::Residual,
118 iterations: 0,
119 });
120 }
121 if fb == 0.0 {
122 return Ok(Solution {
123 value: b,
124 residual: 0.0,
125 convergence: Convergence::Residual,
126 iterations: 0,
127 });
128 }
129 if fa * fb > 0.0 {
130 ogeom_bail!(
131 Construction,
132 "f does not change sign across [{a}, {b}]: f(a) = {fa}, f(b) = {fb}"
133 );
134 }
135
136 let (mut a, mut b) = (a, b);
137 if fa.abs() < fb.abs() {
139 core::mem::swap(&mut a, &mut b);
140 core::mem::swap(&mut fa, &mut fb);
141 }
142 let mut c = a;
143 let mut fc = fa;
144 let mut previous_step = b - a;
145 let mut used_bisection = true;
146
147 for iteration in 1..=criteria.max_iterations {
148 let mut s = if fa != fc && fb != fc {
149 a * fb * fc / ((fa - fb) * (fa - fc))
152 + b * fa * fc / ((fb - fa) * (fb - fc))
153 + c * fa * fb / ((fc - fa) * (fc - fb))
154 } else {
155 b - fb * (b - a) / (fb - fa)
156 };
157
158 let bounds = ((3.0 * a + b) / 4.0, b);
162 let outside = if bounds.0 < bounds.1 {
163 s < bounds.0 || s > bounds.1
164 } else {
165 s < bounds.1 || s > bounds.0
166 };
167 let step = (s - b).abs();
168 let stalled = if used_bisection {
169 step >= (b - c).abs() / 2.0
170 } else {
171 step >= previous_step.abs() / 2.0
172 };
173 if outside || stalled || previous_step.abs() < criteria.step {
174 s = f64::midpoint(a, b);
175 used_bisection = true;
176 } else {
177 used_bisection = false;
178 }
179
180 let fs = f(s);
181 previous_step = b - c;
182 c = b;
183 fc = fb;
184 if fa * fs < 0.0 {
185 b = s;
186 fb = fs;
187 } else {
188 a = s;
189 fa = fs;
190 }
191 if fa.abs() < fb.abs() {
192 core::mem::swap(&mut a, &mut b);
193 core::mem::swap(&mut fa, &mut fb);
194 }
195
196 if fb.abs() <= criteria.residual {
197 return Ok(Solution {
198 value: b,
199 residual: fb,
200 convergence: Convergence::Residual,
201 iterations: iteration,
202 });
203 }
204 if (b - a).abs() <= criteria.step {
205 return Ok(Solution {
206 value: b,
207 residual: fb,
208 convergence: Convergence::Step,
209 iterations: iteration,
210 });
211 }
212 }
213 Ok(Solution {
214 value: b,
215 residual: fb,
216 convergence: Convergence::Exhausted,
217 iterations: criteria.max_iterations,
218 })
219}
220
221pub fn newton<F>(mut f: F, a: f64, b: f64, start: f64, criteria: Criteria) -> OgeomResult<Solution>
237where
238 F: FnMut(f64) -> (f64, f64),
239{
240 if !a.is_finite() || !b.is_finite() || a >= b {
241 ogeom_bail!(Construction, "bracket [{a}, {b}] is empty or non-finite");
242 }
243 let (mut low, mut high) = (a, b);
244 let (fa, _) = f(low);
245 let (fb, _) = f(high);
246 if fa == 0.0 {
247 return Ok(Solution {
248 value: low,
249 residual: 0.0,
250 convergence: Convergence::Residual,
251 iterations: 0,
252 });
253 }
254 if fb == 0.0 {
255 return Ok(Solution {
256 value: high,
257 residual: 0.0,
258 convergence: Convergence::Residual,
259 iterations: 0,
260 });
261 }
262 if fa * fb > 0.0 {
263 ogeom_bail!(Construction, "f does not change sign across [{a}, {b}]");
264 }
265 if fa > 0.0 {
268 core::mem::swap(&mut low, &mut high);
269 }
270
271 let mut x = start.clamp(a, b);
272 let mut previous_step = (b - a).abs();
273
274 for iteration in 1..=criteria.max_iterations {
275 let (value, slope) = f(x);
276 if value.abs() <= criteria.residual {
277 return Ok(Solution {
278 value: x,
279 residual: value,
280 convergence: Convergence::Residual,
281 iterations: iteration,
282 });
283 }
284 if value < 0.0 {
285 low = x;
286 } else {
287 high = x;
288 }
289
290 let newton_step = if slope == 0.0 {
291 f64::INFINITY
292 } else {
293 value / slope
294 };
295 let candidate = x - newton_step;
296 let out_of_bracket = (candidate - low) * (candidate - high) > 0.0;
297 let too_slow = (2.0 * newton_step).abs() > previous_step;
300
301 let next = if out_of_bracket || too_slow || !candidate.is_finite() {
302 f64::midpoint(low, high)
303 } else {
304 candidate
305 };
306 previous_step = (next - x).abs();
307 x = next;
308
309 if previous_step <= criteria.step {
310 let (residual, _) = f(x);
311 return Ok(Solution {
312 value: x,
313 residual,
314 convergence: Convergence::Step,
315 iterations: iteration,
316 });
317 }
318 }
319 let (residual, _) = f(x);
320 Ok(Solution {
321 value: x,
322 residual,
323 convergence: Convergence::Exhausted,
324 iterations: criteria.max_iterations,
325 })
326}
327
328pub fn roots(coefficients: &[f64], tolerance: f64) -> OgeomResult<Vec<f64>> {
342 let mut c = coefficients;
346 while let Some((&last, rest)) = c.split_last() {
347 if last.abs() <= tolerance * c.iter().fold(0.0_f64, |m, v| m.max(v.abs())).max(1.0) {
348 c = rest;
349 } else {
350 break;
351 }
352 }
353
354 let mut out = match c.len() {
355 0 => ogeom_bail!(
356 Construction,
357 "the zero polynomial has every value as a root"
358 ),
359 1 => Vec::new(),
360 2 => vec![-c[0] / c[1]],
361 3 => quadratic_roots(c[2], c[1], c[0]),
362 4 => cubic_roots(c[3], c[2], c[1], c[0]),
363 _ => companion_roots(c, tolerance),
364 };
365
366 out.retain(|r| r.is_finite());
367 out.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
368 out.dedup_by(|a, b| (*a - *b).abs() <= tolerance * a.abs().max(1.0));
369 Ok(out)
370}
371
372#[must_use]
379pub fn quadratic_roots(a: f64, b: f64, c: f64) -> Vec<f64> {
380 if a == 0.0 {
381 return if b == 0.0 { Vec::new() } else { vec![-c / b] };
382 }
383 let discriminant = b.mul_add(b, -(4.0 * a * c));
384 if discriminant < 0.0 {
385 return Vec::new();
386 }
387 if discriminant == 0.0 {
388 return vec![-b / (2.0 * a)];
389 }
390 let sqrt = discriminant.sqrt();
391 let q = -0.5 * (b + b.signum() * sqrt);
394 let (r1, r2) = (q / a, if q == 0.0 { 0.0 } else { c / q });
395 if r1 <= r2 { vec![r1, r2] } else { vec![r2, r1] }
396}
397
398#[must_use]
404pub fn cubic_roots(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
405 if a == 0.0 {
406 return quadratic_roots(b, c, d);
407 }
408 let (b, c, d) = (b / a, c / a, d / a);
409 let shift = b / 3.0;
410 let p = shift.mul_add(-b, c);
412 let q = (2.0 / 27.0 * b * b).mul_add(b, shift.mul_add(-c, d));
413
414 let half_q = q / 2.0;
415 let third_p = p / 3.0;
416 let discriminant = half_q.mul_add(half_q, third_p * third_p * third_p);
417
418 if discriminant > 0.0 {
419 let sqrt = discriminant.sqrt();
420 let u = (-half_q + sqrt).cbrt();
421 let v = (-half_q - sqrt).cbrt();
422 vec![u + v - shift]
423 } else if discriminant == 0.0 {
424 if p == 0.0 {
425 vec![-shift]
426 } else {
427 let u = (-half_q).cbrt();
428 let mut r = vec![2.0 * u - shift, -u - shift];
429 r.sort_by(|x, y| x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal));
430 r
431 }
432 } else {
433 let radius = (-third_p).sqrt();
435 let cos = (-half_q / (radius * radius * radius)).clamp(-1.0, 1.0);
436 let angle = cos.acos() / 3.0;
437 let scale = 2.0 * radius;
438 let tau_third = core::f64::consts::TAU / 3.0;
439 let mut r = vec![
440 scale.mul_add(angle.cos(), -shift),
441 scale.mul_add((angle - tau_third).cos(), -shift),
442 scale.mul_add((angle + tau_third).cos(), -shift),
443 ];
444 r.sort_by(|x, y| x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal));
445 r
446 }
447}
448
449fn companion_roots(c: &[f64], tolerance: f64) -> Vec<f64> {
451 let n = c.len() - 1;
452 let lead = c[n];
453 let mut m = DMatrix::<f64>::zeros(n, n);
454 for i in 0..n {
455 m[(i, n - 1)] = -c[i] / lead;
456 if i + 1 < n {
457 m[(i + 1, i)] = 1.0;
458 }
459 }
460 m.complex_eigenvalues()
462 .iter()
463 .filter(|e| e.im.abs() <= tolerance.max(1e-9) * e.re.abs().max(1.0))
464 .map(|e| e.re)
465 .collect()
466}
467
468pub fn minimize<F>(mut f: F, a: f64, b: f64, criteria: Criteria) -> OgeomResult<Solution>
480where
481 F: FnMut(f64) -> f64,
482{
483 if !a.is_finite() || !b.is_finite() || a >= b {
484 ogeom_bail!(Construction, "bracket [{a}, {b}] is empty or non-finite");
485 }
486 const GOLDEN: f64 = 0.381_966_011_250_105_15;
488
489 let (mut low, mut high) = (a, b);
490 let mut x = GOLDEN.mul_add(b - a, a);
491 let (mut w, mut v) = (x, x);
492 let mut fx = f(x);
493 let (mut fw, mut fv) = (fx, fx);
494 let mut step = 0.0_f64;
495 let mut previous_step = 0.0_f64;
496
497 for iteration in 1..=criteria.max_iterations {
498 let middle = f64::midpoint(low, high);
499 let tolerance = criteria.step.mul_add(x.abs(), criteria.step);
500 if (x - middle).abs() <= 2.0f64.mul_add(tolerance, -((high - low) / 2.0)) {
501 return Ok(Solution {
502 value: x,
503 residual: fx,
504 convergence: Convergence::Step,
505 iterations: iteration,
506 });
507 }
508
509 let mut use_golden = true;
510 if previous_step.abs() > tolerance {
511 let r = (x - w) * (fx - fv);
513 let q = (x - v) * (fx - fw);
514 let mut p = (x - v) * q - (x - w) * r;
515 let mut q = 2.0 * (q - r);
516 if q > 0.0 {
517 p = -p;
518 }
519 q = q.abs();
520 if p.abs() < (0.5 * q * previous_step).abs() && p > q * (low - x) && p < q * (high - x)
523 {
524 step = p / q;
525 let candidate = x + step;
526 if candidate - low < 2.0 * tolerance || high - candidate < 2.0 * tolerance {
527 step = if x < middle { tolerance } else { -tolerance };
528 }
529 use_golden = false;
530 }
531 }
532 if use_golden {
533 previous_step = if x < middle { high - x } else { low - x };
534 step = GOLDEN * previous_step;
535 }
536
537 let next = if step.abs() >= tolerance {
538 x + step
539 } else if step > 0.0 {
540 x + tolerance
541 } else {
542 x - tolerance
543 };
544 let fnext = f(next);
545
546 if fnext <= fx {
547 if next < x {
548 high = x;
549 } else {
550 low = x;
551 }
552 v = w;
553 fv = fw;
554 w = x;
555 fw = fx;
556 x = next;
557 fx = fnext;
558 } else {
559 if next < x {
560 low = next;
561 } else {
562 high = next;
563 }
564 if fnext <= fw || w == x {
565 v = w;
566 fv = fw;
567 w = next;
568 fw = fnext;
569 } else if fnext <= fv || v == x || v == w {
570 v = next;
571 fv = fnext;
572 }
573 }
574 previous_step = step;
575 }
576 Ok(Solution {
577 value: x,
578 residual: fx,
579 convergence: Convergence::Exhausted,
580 iterations: criteria.max_iterations,
581 })
582}
583
584#[derive(Debug, Clone, PartialEq)]
586pub struct SystemSolution {
587 pub value: Vec<f64>,
589 pub residual: f64,
591 pub convergence: Convergence,
593 pub iterations: usize,
595}
596
597pub fn newton_system<F>(mut f: F, start: &[f64], criteria: Criteria) -> OgeomResult<SystemSolution>
619where
620 F: FnMut(&[f64]) -> (Vec<f64>, Vec<Vec<f64>>),
621{
622 let n = start.len();
623 let mut x = DVector::from_row_slice(start);
624
625 let evaluate = |x: &DVector<f64>, f: &mut F| {
626 let (r, j) = f(x.as_slice());
627 (r, j)
628 };
629
630 let (mut residual, mut jacobian) = evaluate(&x, &mut f);
631 if residual.len() != n || jacobian.len() != n || jacobian.iter().any(|row| row.len() != n) {
632 ogeom_bail!(
633 Dimension,
634 "expected a {n}-vector residual and {n}x{n} Jacobian"
635 );
636 }
637 let mut norm = residual.iter().map(|v| v * v).sum::<f64>().sqrt();
638
639 for iteration in 1..=criteria.max_iterations {
640 if norm <= criteria.residual {
641 return Ok(SystemSolution {
642 value: x.as_slice().to_vec(),
643 residual: norm,
644 convergence: Convergence::Residual,
645 iterations: iteration - 1,
646 });
647 }
648
649 let j = DMatrix::from_fn(n, n, |r, c| jacobian[r][c]);
650 let rhs = DVector::from_row_slice(&residual);
651 let Some(delta) = j.lu().solve(&rhs) else {
652 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
653 };
654
655 let mut scale = 1.0;
658 let mut accepted = None;
659 for _ in 0..30 {
660 let candidate = &x - &delta * scale;
661 let (r, jj) = evaluate(&candidate, &mut f);
662 let candidate_norm = r.iter().map(|v| v * v).sum::<f64>().sqrt();
663 if candidate_norm < norm || candidate_norm <= criteria.residual {
664 accepted = Some((candidate, r, jj, candidate_norm));
665 break;
666 }
667 scale *= 0.5;
668 }
669
670 let Some((next, r, jj, next_norm)) = accepted else {
671 return Ok(SystemSolution {
674 value: x.as_slice().to_vec(),
675 residual: norm,
676 convergence: Convergence::Exhausted,
677 iterations: iteration,
678 });
679 };
680
681 let step = (&next - &x).norm();
682 x = next;
683 residual = r;
684 jacobian = jj;
685 norm = next_norm;
686
687 if norm <= criteria.residual {
688 return Ok(SystemSolution {
689 value: x.as_slice().to_vec(),
690 residual: norm,
691 convergence: Convergence::Residual,
692 iterations: iteration,
693 });
694 }
695 if step <= criteria.step {
696 return Ok(SystemSolution {
697 value: x.as_slice().to_vec(),
698 residual: norm,
699 convergence: Convergence::Step,
700 iterations: iteration,
701 });
702 }
703 }
704 Ok(SystemSolution {
705 value: x.as_slice().to_vec(),
706 residual: norm,
707 convergence: Convergence::Exhausted,
708 iterations: criteria.max_iterations,
709 })
710}
711
712pub fn newton_system_2<F>(
727 mut f: F,
728 start: [f64; 2],
729 criteria: Criteria,
730) -> OgeomResult<([f64; 2], f64, Convergence, usize)>
731where
732 F: FnMut([f64; 2]) -> ([f64; 2], [[f64; 2]; 2]),
733{
734 let mut x = start;
735 let (mut residual, mut jacobian) = f(x);
736 let mut norm = residual[0].hypot(residual[1]);
737
738 for iteration in 1..=criteria.max_iterations {
739 if norm <= criteria.residual {
740 return Ok((x, norm, Convergence::Residual, iteration - 1));
741 }
742
743 let (row0, row1, rhs0, rhs1) = if jacobian[0][0].abs() >= jacobian[1][0].abs() {
745 (jacobian[0], jacobian[1], residual[0], residual[1])
746 } else {
747 (jacobian[1], jacobian[0], residual[1], residual[0])
748 };
749 if row0[0].abs() <= f64::EPSILON * (row1[0].abs() + row0[1].abs()).max(1.0) {
750 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
751 }
752 let factor = row1[0] / row0[0];
753 let denom = factor.mul_add(-row0[1], row1[1]);
754 if denom.abs() <= f64::EPSILON * row0[1].abs().max(1.0) {
755 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
756 }
757 let d1 = factor.mul_add(-rhs0, rhs1) / denom;
758 let d0 = d1.mul_add(-row0[1], rhs0) / row0[0];
759 let delta = [d0, d1];
760
761 let mut scale = 1.0;
763 let mut accepted = None;
764 for _ in 0..30 {
765 let candidate = [
766 delta[0].mul_add(-scale, x[0]),
767 delta[1].mul_add(-scale, x[1]),
768 ];
769 let (r, jj) = f(candidate);
770 let candidate_norm = r[0].hypot(r[1]);
771 if candidate_norm < norm || candidate_norm <= criteria.residual {
772 accepted = Some((candidate, r, jj, candidate_norm));
773 break;
774 }
775 scale *= 0.5;
776 }
777 let Some((next, r, jj, next_norm)) = accepted else {
778 return Ok((x, norm, Convergence::Exhausted, iteration));
779 };
780
781 let step = (next[0] - x[0]).hypot(next[1] - x[1]);
782 x = next;
783 residual = r;
784 jacobian = jj;
785 norm = next_norm;
786
787 if norm <= criteria.residual {
788 return Ok((x, norm, Convergence::Residual, iteration));
789 }
790 if step <= criteria.step {
791 return Ok((x, norm, Convergence::Step, iteration));
792 }
793 }
794 Ok((x, norm, Convergence::Exhausted, criteria.max_iterations))
795}
796
797#[cfg(test)]
798#[allow(clippy::unwrap_used)]
799mod tests {
800 use super::*;
801 use approx::assert_relative_eq;
802
803 const C: Criteria = Criteria {
804 residual: 1e-13,
805 step: 1e-14,
806 max_iterations: 100,
807 };
808
809 #[test]
810 fn brent_finds_a_simple_root() {
811 let s = brent(|x| x * x - 2.0, 0.0, 2.0, C).unwrap();
812 assert!(s.convergence.is_converged());
813 assert_relative_eq!(s.value, core::f64::consts::SQRT_2, epsilon = 1e-12);
814 }
815
816 #[test]
817 fn brent_handles_a_root_at_a_bracket_end() {
818 let s = brent(|x| x, -1.0, 0.0, C).unwrap();
819 assert_relative_eq!(s.value, 0.0);
820 assert_eq!(s.iterations, 0);
821 }
822
823 #[test]
824 fn brent_refuses_a_bracket_without_a_sign_change() {
825 assert!(brent(|x| x * x + 1.0, -1.0, 1.0, C).is_err());
826 assert!(brent(|x| x, 1.0, 0.0, C).is_err(), "reversed bracket");
827 assert!(brent(|x| x, 0.0, f64::NAN, C).is_err());
828 }
829
830 #[test]
831 fn brent_converges_on_a_function_that_defeats_the_secant_method() {
832 let s = brent(|x| x.powi(15) - 0.5, 0.0, 2.0, C).unwrap();
835 assert!(s.convergence.is_converged());
836 assert!(s.residual.abs() < 1e-12);
837 assert!(s.iterations < 60, "took {} iterations", s.iterations);
838 }
839
840 #[test]
841 fn newton_converges_faster_than_bisection_when_it_can() {
842 let s = newton(|x| (x * x - 2.0, 2.0 * x), 0.5, 2.0, 1.0, C).unwrap();
843 assert!(s.convergence.is_converged());
844 assert_relative_eq!(s.value, core::f64::consts::SQRT_2, epsilon = 1e-12);
845 assert!(s.iterations < 12, "took {} iterations", s.iterations);
846 }
847
848 #[test]
849 fn newton_survives_a_vanishing_derivative() {
850 let s = newton(|x| (x * x * x, 3.0 * x * x), -1.0, 2.0, 1.9, C).unwrap();
853 assert!(s.value.abs() < 1e-4, "landed at {}", s.value);
854 }
855
856 #[test]
857 fn newton_survives_a_terrible_starting_point() {
858 for start in [-0.999_f64, 0.0, 1.999, 1.0] {
859 let s = newton(|x| (x * x - 2.0, 2.0 * x), -1.0, 2.0, start, C).unwrap();
860 assert!(
861 (s.value - core::f64::consts::SQRT_2).abs() < 1e-9,
862 "start {start} gave {}",
863 s.value
864 );
865 }
866 }
867
868 #[test]
869 fn quadratic_roots_stay_accurate_when_the_roots_are_far_apart() {
870 let r = quadratic_roots(1.0, -(1e8 + 1e-8), 1.0);
874 assert_eq!(r.len(), 2);
875 assert_relative_eq!(r[0], 1e-8, max_relative = 1e-10);
876 assert_relative_eq!(r[1], 1e8, max_relative = 1e-14);
877 }
878
879 #[test]
880 fn quadratic_edge_cases() {
881 assert_eq!(
882 quadratic_roots(1.0, 0.0, 1.0),
883 Vec::<f64>::new(),
884 "no real roots"
885 );
886 assert_eq!(quadratic_roots(1.0, -2.0, 1.0), vec![1.0], "double root");
887 assert_eq!(
888 quadratic_roots(0.0, 2.0, -4.0),
889 vec![2.0],
890 "degenerates to linear"
891 );
892 assert_eq!(quadratic_roots(0.0, 0.0, 1.0), Vec::<f64>::new());
893 let r = quadratic_roots(1.0, 0.0, -4.0);
894 assert_relative_eq!(r[0], -2.0);
895 assert_relative_eq!(r[1], 2.0);
896 }
897
898 #[test]
899 fn cubic_with_three_real_roots() {
900 let r = cubic_roots(1.0, 0.0, -7.0, 6.0);
902 assert_eq!(r.len(), 3);
903 assert_relative_eq!(r[0], -3.0, epsilon = 1e-12);
904 assert_relative_eq!(r[1], 1.0, epsilon = 1e-12);
905 assert_relative_eq!(r[2], 2.0, epsilon = 1e-12);
906 }
907
908 #[test]
909 fn cubic_with_one_real_root() {
910 let r = cubic_roots(1.0, 0.0, 1.0, 1.0);
912 assert_eq!(r.len(), 1);
913 assert_relative_eq!(r[0], -0.682_327_803_828_019_3, epsilon = 1e-12);
914 }
915
916 #[test]
917 fn cubic_with_repeated_roots() {
918 let r = cubic_roots(1.0, -3.0, 0.0, 4.0);
920 assert_eq!(r.len(), 2, "a repeated root is reported once");
921 assert_relative_eq!(r[0], -1.0, epsilon = 1e-9);
922 assert_relative_eq!(r[1], 2.0, epsilon = 1e-9);
923 let t = cubic_roots(1.0, 0.0, 0.0, 0.0);
925 assert_eq!(t, vec![0.0]);
926 }
927
928 #[test]
929 fn roots_strips_leading_zeros_before_choosing_a_method() {
930 let r = roots(&[-4.0, 0.0, 1.0, 0.0], 1e-12).unwrap();
933 assert_eq!(r.len(), 2);
934 assert_relative_eq!(r[0], -2.0, epsilon = 1e-12);
935 assert_relative_eq!(r[1], 2.0, epsilon = 1e-12);
936 }
937
938 #[test]
939 fn roots_of_a_quartic() {
940 let r = roots(&[24.0, -50.0, 35.0, -10.0, 1.0], 1e-9).unwrap();
942 assert_eq!(r.len(), 4);
943 for (got, want) in r.iter().zip([1.0, 2.0, 3.0, 4.0]) {
944 assert_relative_eq!(got, &want, epsilon = 1e-7);
945 }
946 }
947
948 #[test]
949 fn roots_of_a_high_degree_polynomial() {
950 let r = roots(&[-120.0, 274.0, -225.0, 85.0, -15.0, 1.0], 1e-9).unwrap();
952 assert_eq!(r.len(), 5);
953 for (got, want) in r.iter().zip([1.0, 2.0, 3.0, 4.0, 5.0]) {
954 assert_relative_eq!(got, &want, epsilon = 1e-6);
955 }
956 }
957
958 #[test]
959 fn roots_degenerate_cases() {
960 assert!(roots(&[], 1e-12).is_err());
961 assert!(roots(&[0.0, 0.0], 1e-12).is_err());
962 assert_eq!(
963 roots(&[5.0], 1e-12).unwrap(),
964 Vec::<f64>::new(),
965 "a nonzero constant"
966 );
967 assert_eq!(roots(&[0.0, 1.0], 1e-12).unwrap(), vec![0.0]);
968 }
969
970 #[test]
971 fn minimize_finds_a_smooth_minimum() {
972 let s = minimize(|x| (x - 0.3) * (x - 0.3) + 1.0, -2.0, 2.0, C).unwrap();
973 assert_relative_eq!(s.value, 0.3, epsilon = 1e-7);
974 assert_relative_eq!(s.residual, 1.0, epsilon = 1e-12);
975 }
976
977 #[test]
978 fn minimize_handles_a_flat_minimum() {
979 let s = minimize(|x: f64| (x - 0.5).powi(4), -1.0, 2.0, C).unwrap();
982 assert!((s.value - 0.5).abs() < 1e-3, "landed at {}", s.value);
983 assert!(s.residual < 1e-12);
984 }
985
986 #[test]
987 fn minimize_refuses_a_malformed_bracket() {
988 assert!(minimize(|x| x, 1.0, 0.0, C).is_err());
989 assert!(minimize(|x| x, 0.0, f64::INFINITY, C).is_err());
990 }
991
992 #[test]
993 fn newton_system_solves_a_two_by_two() {
994 let s = newton_system(
996 |v| {
997 let (x, y) = (v[0], v[1]);
998 (
999 vec![x.mul_add(x, y * y) - 25.0, x - y - 1.0],
1000 vec![vec![2.0 * x, 2.0 * y], vec![1.0, -1.0]],
1001 )
1002 },
1003 &[5.0, 1.0],
1004 C,
1005 )
1006 .unwrap();
1007 assert!(s.convergence.is_converged());
1008 assert_relative_eq!(s.value[0], 4.0, epsilon = 1e-10);
1009 assert_relative_eq!(s.value[1], 3.0, epsilon = 1e-10);
1010 }
1011
1012 #[test]
1013 fn newton_system_damping_survives_a_start_where_plain_newton_diverges() {
1014 let s = newton_system(
1020 |v| {
1021 let (x, y) = (v[0], v[1]);
1022 (
1023 vec![x.atan(), y.atan()],
1024 vec![
1025 vec![x.mul_add(x, 1.0).recip(), 0.0],
1026 vec![0.0, y.mul_add(y, 1.0).recip()],
1027 ],
1028 )
1029 },
1030 &[5.0, 5.0],
1031 C,
1032 )
1033 .unwrap();
1034 assert!(s.convergence.is_converged(), "{s:?}");
1035 assert!(s.value[0].abs() < 1e-9 && s.value[1].abs() < 1e-9, "{s:?}");
1036 }
1037
1038 #[test]
1039 fn newton_system_reports_a_residual_minimum_rather_than_looping() {
1040 let s = newton_system(
1044 |v| {
1045 let (x, y) = (v[0], v[1]);
1046 (
1047 vec![x.mul_add(x, 1.0), y],
1048 vec![vec![2.0 * x, 0.0], vec![0.0, 1.0]],
1049 )
1050 },
1051 &[2.0, 2.0],
1052 C,
1053 )
1054 .unwrap();
1055 assert!(!s.convergence.is_converged());
1056 assert!(s.residual >= 1.0, "the residual cannot go below 1 here");
1057 }
1058
1059 #[test]
1060 fn newton_system_reports_a_singular_jacobian_rather_than_looping() {
1061 let s = newton_system(
1062 |v| {
1063 (
1064 vec![v[0] * v[0], v[1]],
1065 vec![vec![2.0 * v[0], 0.0], vec![0.0, 0.0]],
1066 )
1067 },
1068 &[1.0, 1.0],
1069 C,
1070 );
1071 assert!(s.is_err());
1072 }
1073
1074 #[test]
1075 fn newton_system_checks_its_shapes() {
1076 let s = newton_system(|_| (vec![1.0], vec![vec![1.0, 2.0]]), &[0.0, 0.0], C);
1077 assert!(s.is_err());
1078 }
1079
1080 #[test]
1081 fn exhausted_is_reported_not_hidden() {
1082 let s = brent(
1085 |x| x * x - 2.0,
1086 0.0,
1087 2.0,
1088 Criteria {
1089 max_iterations: 1,
1090 ..C
1091 },
1092 )
1093 .unwrap();
1094 assert_eq!(s.convergence, Convergence::Exhausted);
1095 assert!(!s.convergence.is_converged());
1096 }
1097}