1use nalgebra::{DMatrix, DVector};
23use ogeom_core::{OgeomResult, ogeom_bail};
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum Convergence {
28 Residual,
30 Step,
32 Exhausted,
35}
36
37impl Convergence {
38 #[must_use]
40 pub const fn is_converged(self) -> bool {
41 !matches!(self, Self::Exhausted)
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
47pub struct Solution {
48 pub value: f64,
50 pub residual: f64,
52 pub convergence: Convergence,
54 pub iterations: usize,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct Criteria {
61 pub residual: f64,
63 pub step: f64,
65 pub max_iterations: usize,
67}
68
69impl Default for Criteria {
70 fn default() -> Self {
71 Self {
72 residual: 1e-13,
75 step: 1e-14,
76 max_iterations: 100,
77 }
78 }
79}
80
81impl Criteria {
82 #[must_use]
84 pub fn with_residual(residual: f64) -> Self {
85 Self {
86 residual,
87 ..Self::default()
88 }
89 }
90}
91
92pub fn brent<F>(mut f: F, a: f64, b: f64, criteria: Criteria) -> OgeomResult<Solution>
105where
106 F: FnMut(f64) -> f64,
107{
108 if !a.is_finite() || !b.is_finite() || a >= b {
109 ogeom_bail!(Construction, "bracket [{a}, {b}] is empty or non-finite");
110 }
111 let (mut fa, mut fb) = (f(a), f(b));
112 if fa == 0.0 {
113 return Ok(Solution {
114 value: a,
115 residual: 0.0,
116 convergence: Convergence::Residual,
117 iterations: 0,
118 });
119 }
120 if fb == 0.0 {
121 return Ok(Solution {
122 value: b,
123 residual: 0.0,
124 convergence: Convergence::Residual,
125 iterations: 0,
126 });
127 }
128 if fa * fb > 0.0 {
129 ogeom_bail!(
130 Construction,
131 "f does not change sign across [{a}, {b}]: f(a) = {fa}, f(b) = {fb}"
132 );
133 }
134
135 let (mut a, mut b) = (a, b);
136 if fa.abs() < fb.abs() {
138 core::mem::swap(&mut a, &mut b);
139 core::mem::swap(&mut fa, &mut fb);
140 }
141 let mut c = a;
142 let mut fc = fa;
143 let mut previous_step = b - a;
144 let mut used_bisection = true;
145
146 for iteration in 1..=criteria.max_iterations {
147 let mut s = if fa != fc && fb != fc {
148 a * fb * fc / ((fa - fb) * (fa - fc))
151 + b * fa * fc / ((fb - fa) * (fb - fc))
152 + c * fa * fb / ((fc - fa) * (fc - fb))
153 } else {
154 b - fb * (b - a) / (fb - fa)
155 };
156
157 let bounds = ((3.0 * a + b) / 4.0, b);
161 let outside = if bounds.0 < bounds.1 {
162 s < bounds.0 || s > bounds.1
163 } else {
164 s < bounds.1 || s > bounds.0
165 };
166 let step = (s - b).abs();
167 let stalled = if used_bisection {
168 step >= (b - c).abs() / 2.0
169 } else {
170 step >= previous_step.abs() / 2.0
171 };
172 if outside || stalled || previous_step.abs() < criteria.step {
173 s = f64::midpoint(a, b);
174 used_bisection = true;
175 } else {
176 used_bisection = false;
177 }
178
179 let fs = f(s);
180 previous_step = b - c;
181 c = b;
182 fc = fb;
183 if fa * fs < 0.0 {
184 b = s;
185 fb = fs;
186 } else {
187 a = s;
188 fa = fs;
189 }
190 if fa.abs() < fb.abs() {
191 core::mem::swap(&mut a, &mut b);
192 core::mem::swap(&mut fa, &mut fb);
193 }
194
195 if fb.abs() <= criteria.residual {
196 return Ok(Solution {
197 value: b,
198 residual: fb,
199 convergence: Convergence::Residual,
200 iterations: iteration,
201 });
202 }
203 if (b - a).abs() <= criteria.step {
204 return Ok(Solution {
205 value: b,
206 residual: fb,
207 convergence: Convergence::Step,
208 iterations: iteration,
209 });
210 }
211 }
212 Ok(Solution {
213 value: b,
214 residual: fb,
215 convergence: Convergence::Exhausted,
216 iterations: criteria.max_iterations,
217 })
218}
219
220pub fn newton<F>(mut f: F, a: f64, b: f64, start: f64, criteria: Criteria) -> OgeomResult<Solution>
236where
237 F: FnMut(f64) -> (f64, f64),
238{
239 if !a.is_finite() || !b.is_finite() || a >= b {
240 ogeom_bail!(Construction, "bracket [{a}, {b}] is empty or non-finite");
241 }
242 let (mut low, mut high) = (a, b);
243 let (fa, _) = f(low);
244 let (fb, _) = f(high);
245 if fa == 0.0 {
246 return Ok(Solution {
247 value: low,
248 residual: 0.0,
249 convergence: Convergence::Residual,
250 iterations: 0,
251 });
252 }
253 if fb == 0.0 {
254 return Ok(Solution {
255 value: high,
256 residual: 0.0,
257 convergence: Convergence::Residual,
258 iterations: 0,
259 });
260 }
261 if fa * fb > 0.0 {
262 ogeom_bail!(Construction, "f does not change sign across [{a}, {b}]");
263 }
264 if fa > 0.0 {
267 core::mem::swap(&mut low, &mut high);
268 }
269
270 let mut x = start.clamp(a, b);
271 let mut previous_step = (b - a).abs();
272
273 for iteration in 1..=criteria.max_iterations {
274 let (value, slope) = f(x);
275 if value.abs() <= criteria.residual {
276 return Ok(Solution {
277 value: x,
278 residual: value,
279 convergence: Convergence::Residual,
280 iterations: iteration,
281 });
282 }
283 if value < 0.0 {
284 low = x;
285 } else {
286 high = x;
287 }
288
289 let newton_step = if slope == 0.0 {
290 f64::INFINITY
291 } else {
292 value / slope
293 };
294 let candidate = x - newton_step;
295 let out_of_bracket = (candidate - low) * (candidate - high) > 0.0;
296 let too_slow = (2.0 * newton_step).abs() > previous_step;
299
300 let next = if out_of_bracket || too_slow || !candidate.is_finite() {
301 f64::midpoint(low, high)
302 } else {
303 candidate
304 };
305 previous_step = (next - x).abs();
306 x = next;
307
308 if previous_step <= criteria.step {
309 let (residual, _) = f(x);
310 return Ok(Solution {
311 value: x,
312 residual,
313 convergence: Convergence::Step,
314 iterations: iteration,
315 });
316 }
317 }
318 let (residual, _) = f(x);
319 Ok(Solution {
320 value: x,
321 residual,
322 convergence: Convergence::Exhausted,
323 iterations: criteria.max_iterations,
324 })
325}
326
327pub 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 let rounding = 8.0 * f64::EPSILON * b.mul_add(b, (4.0 * a * c).abs());
387 if discriminant.abs() <= rounding {
388 return vec![-b / (2.0 * a)];
389 }
390 if discriminant < 0.0 {
391 return Vec::new();
392 }
393 let sqrt = discriminant.sqrt();
394 let q = -0.5 * (b + b.signum() * sqrt);
397 let (r1, r2) = (q / a, if q == 0.0 { 0.0 } else { c / q });
398 if r1 <= r2 { vec![r1, r2] } else { vec![r2, r1] }
399}
400
401#[must_use]
407pub fn cubic_roots(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
408 if a == 0.0 {
409 return quadratic_roots(b, c, d);
410 }
411 let (b, c, d) = (b / a, c / a, d / a);
412 let shift = b / 3.0;
413 let p = shift.mul_add(-b, c);
415 let q = (2.0 / 27.0 * b * b).mul_add(b, shift.mul_add(-c, d));
416
417 let half_q = q / 2.0;
418 let third_p = p / 3.0;
419 let discriminant = half_q.mul_add(half_q, third_p * third_p * third_p);
420 let p_rounding = 8.0 * f64::EPSILON * ((shift * b).abs() + c.abs());
423 let q_rounding =
424 8.0 * f64::EPSILON * ((2.0 / 27.0 * b * b * b).abs() + (shift * c).abs() + d.abs());
425 let rounding = half_q.abs() * q_rounding
426 + third_p * third_p * p_rounding
427 + 8.0 * f64::EPSILON * (half_q * half_q + (third_p * third_p * third_p).abs());
428
429 if discriminant.abs() <= rounding {
430 if p.abs() <= p_rounding {
431 vec![-shift]
432 } else {
433 let mut r = vec![3.0 * q / p - shift, -1.5 * q / p - shift];
435 r.sort_by(|x, y| x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal));
436 r
437 }
438 } else if discriminant > 0.0 {
439 let sqrt = discriminant.sqrt();
440 let u = (-half_q + sqrt).cbrt();
441 let v = (-half_q - sqrt).cbrt();
442 vec![u + v - shift]
443 } else {
444 let radius = (-third_p).sqrt();
446 let cos = (-half_q / (radius * radius * radius)).clamp(-1.0, 1.0);
447 let angle = cos.acos() / 3.0;
448 let scale = 2.0 * radius;
449 let tau_third = core::f64::consts::TAU / 3.0;
450 let mut r = vec![
451 scale.mul_add(angle.cos(), -shift),
452 scale.mul_add((angle - tau_third).cos(), -shift),
453 scale.mul_add((angle + tau_third).cos(), -shift),
454 ];
455 r.sort_by(|x, y| x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal));
456 r
457 }
458}
459
460fn companion_roots(c: &[f64], tolerance: f64) -> Vec<f64> {
462 let n = c.len() - 1;
463 let lead = c[n];
464 let mut m = DMatrix::<f64>::zeros(n, n);
465 for i in 0..n {
466 m[(i, n - 1)] = -c[i] / lead;
467 if i + 1 < n {
468 m[(i + 1, i)] = 1.0;
469 }
470 }
471 let value = |x: f64| -> (f64, f64) {
476 let (mut p, mut size) = (0.0_f64, 0.0_f64);
477 for &coefficient in c.iter().rev() {
478 p = p.mul_add(x, coefficient);
479 size = size.mul_add(x.abs(), coefficient.abs());
480 }
481 (p, size)
482 };
483 let eigenvalues: Vec<nalgebra::Complex<f64>> =
488 match nalgebra::linalg::Schur::try_new(m, f64::EPSILON, 1000) {
489 Some(schur) => schur.complex_eigenvalues().iter().copied().collect(),
490 None => durand_kerner(c),
491 };
492 eigenvalues
493 .iter()
494 .filter_map(|e| {
495 let scale = e.re.abs().max(1.0);
496 if e.im.abs() <= tolerance.max(1e-9) * scale {
497 return Some(e.re);
498 }
499 if e.im.abs() > 1e-7 * scale {
500 return None;
501 }
502 let (p, size) = value(e.re);
503 (p.abs() <= 1e-10 * size).then_some(e.re)
504 })
505 .collect()
506}
507
508fn durand_kerner(c: &[f64]) -> Vec<nalgebra::Complex<f64>> {
513 use nalgebra::Complex;
514 let n = c.len() - 1;
515 let lead = c[n];
516 let monic: Vec<f64> = c.iter().map(|x| x / lead).collect();
517 let radius = 1.0 + monic[..n].iter().fold(0.0_f64, |m, x| m.max(x.abs()));
520 #[allow(clippy::cast_precision_loss, reason = "a degree")]
521 let mut z: Vec<Complex<f64>> = (0..n)
522 .map(|k| Complex::from_polar(radius, 0.4 + core::f64::consts::TAU * k as f64 / n as f64))
523 .collect();
524 let value = |x: Complex<f64>| {
525 let mut p = Complex::new(1.0, 0.0);
526 for &coefficient in monic[..n].iter().rev() {
527 p = p * x + coefficient;
528 }
529 p
530 };
531 for _ in 0..500 {
532 let mut largest = 0.0_f64;
533 for i in 0..n {
534 let mut denominator = Complex::new(1.0, 0.0);
535 for j in 0..n {
536 if i != j {
537 denominator *= z[i] - z[j];
538 }
539 }
540 if denominator.norm() == 0.0 {
541 continue;
542 }
543 let step = value(z[i]) / denominator;
544 z[i] -= step;
545 largest = largest.max(step.norm() / z[i].norm().max(1.0));
546 }
547 if largest <= f64::EPSILON * 4.0 {
548 break;
549 }
550 }
551 z
552}
553
554pub fn minimize<F>(mut f: F, a: f64, b: f64, criteria: Criteria) -> OgeomResult<Solution>
566where
567 F: FnMut(f64) -> f64,
568{
569 if !a.is_finite() || !b.is_finite() || a >= b {
570 ogeom_bail!(Construction, "bracket [{a}, {b}] is empty or non-finite");
571 }
572 const GOLDEN: f64 = 0.381_966_011_250_105_15;
574
575 let (mut low, mut high) = (a, b);
576 let mut x = GOLDEN.mul_add(b - a, a);
577 let (mut w, mut v) = (x, x);
578 let mut fx = f(x);
579 let (mut fw, mut fv) = (fx, fx);
580 let mut step = 0.0_f64;
581 let mut previous_step = 0.0_f64;
582
583 for iteration in 1..=criteria.max_iterations {
584 let middle = f64::midpoint(low, high);
585 let tolerance = criteria.step.mul_add(x.abs(), criteria.step);
586 if (x - middle).abs() <= 2.0f64.mul_add(tolerance, -((high - low) / 2.0)) {
587 return Ok(Solution {
588 value: x,
589 residual: fx,
590 convergence: Convergence::Step,
591 iterations: iteration,
592 });
593 }
594
595 let mut use_golden = true;
596 if previous_step.abs() > tolerance {
597 let r = (x - w) * (fx - fv);
599 let q = (x - v) * (fx - fw);
600 let mut p = (x - v) * q - (x - w) * r;
601 let mut q = 2.0 * (q - r);
602 if q > 0.0 {
603 p = -p;
604 }
605 q = q.abs();
606 if p.abs() < (0.5 * q * previous_step).abs() && p > q * (low - x) && p < q * (high - x)
609 {
610 step = p / q;
611 let candidate = x + step;
612 if candidate - low < 2.0 * tolerance || high - candidate < 2.0 * tolerance {
613 step = if x < middle { tolerance } else { -tolerance };
614 }
615 use_golden = false;
616 }
617 }
618 if use_golden {
619 previous_step = if x < middle { high - x } else { low - x };
620 step = GOLDEN * previous_step;
621 }
622
623 let next = if step.abs() >= tolerance {
624 x + step
625 } else if step > 0.0 {
626 x + tolerance
627 } else {
628 x - tolerance
629 };
630 let fnext = f(next);
631
632 if fnext <= fx {
633 if next < x {
634 high = x;
635 } else {
636 low = x;
637 }
638 v = w;
639 fv = fw;
640 w = x;
641 fw = fx;
642 x = next;
643 fx = fnext;
644 } else {
645 if next < x {
646 low = next;
647 } else {
648 high = next;
649 }
650 if fnext <= fw || w == x {
651 v = w;
652 fv = fw;
653 w = next;
654 fw = fnext;
655 } else if fnext <= fv || v == x || v == w {
656 v = next;
657 fv = fnext;
658 }
659 }
660 previous_step = step;
661 }
662 Ok(Solution {
663 value: x,
664 residual: fx,
665 convergence: Convergence::Exhausted,
666 iterations: criteria.max_iterations,
667 })
668}
669
670#[derive(Debug, Clone, PartialEq)]
672pub struct SystemSolution {
673 pub value: Vec<f64>,
675 pub residual: f64,
677 pub convergence: Convergence,
679 pub iterations: usize,
681}
682
683pub fn newton_system<F>(mut f: F, start: &[f64], criteria: Criteria) -> OgeomResult<SystemSolution>
705where
706 F: FnMut(&[f64]) -> (Vec<f64>, Vec<Vec<f64>>),
707{
708 let n = start.len();
709 let mut x = DVector::from_row_slice(start);
710
711 let evaluate = |x: &DVector<f64>, f: &mut F| {
712 let (r, j) = f(x.as_slice());
713 (r, j)
714 };
715
716 let (mut residual, mut jacobian) = evaluate(&x, &mut f);
717 if residual.len() != n || jacobian.len() != n || jacobian.iter().any(|row| row.len() != n) {
718 ogeom_bail!(
719 Dimension,
720 "expected a {n}-vector residual and {n}x{n} Jacobian"
721 );
722 }
723 let mut norm = residual.iter().map(|v| v * v).sum::<f64>().sqrt();
724
725 for iteration in 1..=criteria.max_iterations {
726 if norm <= criteria.residual {
727 return Ok(SystemSolution {
728 value: x.as_slice().to_vec(),
729 residual: norm,
730 convergence: Convergence::Residual,
731 iterations: iteration - 1,
732 });
733 }
734
735 let j = DMatrix::from_fn(n, n, |r, c| jacobian[r][c]);
736 let rhs = DVector::from_row_slice(&residual);
737 let Some(delta) = j.lu().solve(&rhs) else {
738 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
739 };
740
741 let mut scale = 1.0;
744 let mut accepted = None;
745 for _ in 0..30 {
746 let candidate = &x - &delta * scale;
747 let (r, jj) = evaluate(&candidate, &mut f);
748 let candidate_norm = r.iter().map(|v| v * v).sum::<f64>().sqrt();
749 if candidate_norm < norm || candidate_norm <= criteria.residual {
750 accepted = Some((candidate, r, jj, candidate_norm));
751 break;
752 }
753 scale *= 0.5;
754 }
755
756 let Some((next, r, jj, next_norm)) = accepted else {
757 return Ok(SystemSolution {
760 value: x.as_slice().to_vec(),
761 residual: norm,
762 convergence: Convergence::Exhausted,
763 iterations: iteration,
764 });
765 };
766
767 let step = (&next - &x).norm();
768 x = next;
769 residual = r;
770 jacobian = jj;
771 norm = next_norm;
772
773 if norm <= criteria.residual {
774 return Ok(SystemSolution {
775 value: x.as_slice().to_vec(),
776 residual: norm,
777 convergence: Convergence::Residual,
778 iterations: iteration,
779 });
780 }
781 if step <= criteria.step {
782 return Ok(SystemSolution {
783 value: x.as_slice().to_vec(),
784 residual: norm,
785 convergence: Convergence::Step,
786 iterations: iteration,
787 });
788 }
789 }
790 Ok(SystemSolution {
791 value: x.as_slice().to_vec(),
792 residual: norm,
793 convergence: Convergence::Exhausted,
794 iterations: criteria.max_iterations,
795 })
796}
797
798pub fn newton_system_fixed<const N: usize, F>(
810 mut f: F,
811 start: [f64; N],
812 criteria: Criteria,
813) -> OgeomResult<([f64; N], f64, Convergence, usize)>
814where
815 F: FnMut(&[f64; N]) -> ([f64; N], [[f64; N]; N]),
816{
817 let last = std::cell::Cell::new([[0.0; N]; N]);
820 newton_system_fixed_lazy(
821 |x| {
822 let (residual, jacobian) = f(x);
823 last.set(jacobian);
824 residual
825 },
826 |_| Some(last.get()),
827 start,
828 criteria,
829 )
830}
831
832pub fn newton_system_fixed_lazy<const N: usize, R, J>(
847 mut residual_at: R,
848 mut jacobian_at: J,
849 start: [f64; N],
850 criteria: Criteria,
851) -> OgeomResult<([f64; N], f64, Convergence, usize)>
852where
853 R: FnMut(&[f64; N]) -> [f64; N],
854 J: FnMut(&[f64; N]) -> Option<[[f64; N]; N]>,
855{
856 let norm_of = |r: &[f64; N]| r.iter().map(|v| v * v).sum::<f64>().sqrt();
857 let mut x = start;
858 let first = residual_at(&x);
859 let (mut residual, mut jacobian) =
860 jacobian_at(&x).map_or(([f64::INFINITY; N], [[0.0; N]; N]), |j| (first, j));
861 let mut norm = norm_of(&residual);
862 for iteration in 1..=criteria.max_iterations {
863 if norm <= criteria.residual {
864 return Ok((x, norm, Convergence::Residual, iteration - 1));
865 }
866 let Some(delta) = solve_fixed(jacobian, residual) else {
867 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
868 };
869 let mut scale = 1.0;
870 let mut accepted = None;
871 for _ in 0..30 {
872 let mut candidate = x;
873 for (value, d) in candidate.iter_mut().zip(delta.iter()) {
874 *value -= d * scale;
875 }
876 let r = residual_at(&candidate);
877 let candidate_norm = norm_of(&r);
878 if (candidate_norm < norm || candidate_norm <= criteria.residual)
881 && let Some(jj) = jacobian_at(&candidate)
882 {
883 accepted = Some((candidate, r, jj, candidate_norm));
884 break;
885 }
886 scale *= 0.5;
887 }
888 let Some((next, r, jj, next_norm)) = accepted else {
889 return Ok((x, norm, Convergence::Exhausted, iteration));
890 };
891 let step = next
892 .iter()
893 .zip(&x)
894 .map(|(a, b)| (a - b) * (a - b))
895 .sum::<f64>()
896 .sqrt();
897 x = next;
898 residual = r;
899 jacobian = jj;
900 norm = next_norm;
901 if norm <= criteria.residual {
902 return Ok((x, norm, Convergence::Residual, iteration));
903 }
904 if step <= criteria.step {
905 return Ok((x, norm, Convergence::Step, iteration));
906 }
907 }
908 Ok((x, norm, Convergence::Exhausted, criteria.max_iterations))
909}
910
911fn solve_fixed<const N: usize>(mut a: [[f64; N]; N], mut b: [f64; N]) -> Option<[f64; N]> {
919 for col in 0..N {
920 let mut pivot = col;
921 for row in col + 1..N {
922 if a[row][col].abs() > a[pivot][col].abs() {
923 pivot = row;
924 }
925 }
926 let diag = a[pivot][col];
927 if diag == 0.0 {
928 return None;
929 }
930 a.swap(col, pivot);
931 b.swap(col, pivot);
932 let inverse = 1.0 / diag;
933 let head = a[col];
934 for row in col + 1..N {
935 let factor = a[row][col] * inverse;
936 for (entry, above) in a[row].iter_mut().zip(&head).skip(col + 1) {
937 *entry -= factor * above;
938 }
939 b[row] -= factor * b[col];
940 }
941 }
942 for col in (0..N).rev() {
943 b[col] /= a[col][col];
944 let known = b[col];
945 for row in 0..col {
946 b[row] -= a[row][col] * known;
947 }
948 }
949 Some(b)
950}
951
952pub fn newton_system_2<F>(
967 mut f: F,
968 start: [f64; 2],
969 criteria: Criteria,
970) -> OgeomResult<([f64; 2], f64, Convergence, usize)>
971where
972 F: FnMut([f64; 2]) -> ([f64; 2], [[f64; 2]; 2]),
973{
974 let mut x = start;
975 let (mut residual, mut jacobian) = f(x);
976 let mut norm = residual[0].hypot(residual[1]);
977
978 for iteration in 1..=criteria.max_iterations {
979 if norm <= criteria.residual {
980 return Ok((x, norm, Convergence::Residual, iteration - 1));
981 }
982
983 let (row0, row1, rhs0, rhs1) = if jacobian[0][0].abs() >= jacobian[1][0].abs() {
985 (jacobian[0], jacobian[1], residual[0], residual[1])
986 } else {
987 (jacobian[1], jacobian[0], residual[1], residual[0])
988 };
989 if row0[0].abs() <= f64::EPSILON * (row1[0].abs() + row0[1].abs()).max(1.0) {
990 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
991 }
992 let factor = row1[0] / row0[0];
993 let denom = factor.mul_add(-row0[1], row1[1]);
994 if denom.abs() <= f64::EPSILON * row0[1].abs().max(1.0) {
995 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
996 }
997 let d1 = factor.mul_add(-rhs0, rhs1) / denom;
998 let d0 = d1.mul_add(-row0[1], rhs0) / row0[0];
999 let delta = [d0, d1];
1000
1001 let mut scale = 1.0;
1003 let mut accepted = None;
1004 for _ in 0..30 {
1005 let candidate = [
1006 delta[0].mul_add(-scale, x[0]),
1007 delta[1].mul_add(-scale, x[1]),
1008 ];
1009 let (r, jj) = f(candidate);
1010 let candidate_norm = r[0].hypot(r[1]);
1011 if candidate_norm < norm || candidate_norm <= criteria.residual {
1012 accepted = Some((candidate, r, jj, candidate_norm));
1013 break;
1014 }
1015 scale *= 0.5;
1016 }
1017 let Some((next, r, jj, next_norm)) = accepted else {
1018 return Ok((x, norm, Convergence::Exhausted, iteration));
1019 };
1020
1021 let step = (next[0] - x[0]).hypot(next[1] - x[1]);
1022 x = next;
1023 residual = r;
1024 jacobian = jj;
1025 norm = next_norm;
1026
1027 if norm <= criteria.residual {
1028 return Ok((x, norm, Convergence::Residual, iteration));
1029 }
1030 if step <= criteria.step {
1031 return Ok((x, norm, Convergence::Step, iteration));
1032 }
1033 }
1034 Ok((x, norm, Convergence::Exhausted, criteria.max_iterations))
1035}
1036
1037#[cfg(test)]
1038#[allow(clippy::unwrap_used)]
1039mod tests {
1040 use super::*;
1041
1042 fn has_each(found: &[f64], expected: &[f64]) {
1045 for e in expected {
1046 assert!(
1047 found
1048 .iter()
1049 .any(|f| (f - e).abs() <= 1e-6 * e.abs().max(1.0)),
1050 "{e} missing from {found:?}"
1051 );
1052 }
1053 }
1054
1055 #[test]
1058 fn a_quartic_with_roots_paired_by_sign_comes_back() {
1059 let found = roots(&[64.0, 0.0, -20.0, 0.0, 1.0], 1e-12).unwrap();
1061 assert_eq!(found.len(), 4, "{found:?}");
1062 for (got, want) in found.iter().zip([-4.0, -2.0, 2.0, 4.0]) {
1063 assert!((got - want).abs() < 1e-9, "{found:?}");
1064 }
1065 assert!(
1067 roots(&[64.0, 0.0, 4.0, 0.0, 1.0], 1e-12)
1068 .unwrap()
1069 .is_empty()
1070 );
1071 }
1072
1073 #[test]
1074 fn double_roots_survive_rounding() {
1075 for r in [0.1, 0.3, 0.7, 1.1, 3.3, 1000.0 / 3.0, -2.9, 1e-3 / 7.0] {
1076 has_each(&quadratic_roots(1.0, -2.0 * r, r * r), &[r]);
1078 has_each(
1080 &cubic_roots(1.0, -(1.0 + 2.0 * r), 2.0f64.mul_add(r, r * r), -(r * r)),
1081 &[1.0, r],
1082 );
1083 let quadratic = [r * r, -2.0 * r, 1.0];
1085 let pair = [-2.0, -1.0, 1.0];
1086 let mut quartic = [0.0; 5];
1087 for (i, a) in quadratic.iter().enumerate() {
1088 for (j, b) in pair.iter().enumerate() {
1089 quartic[i + j] += a * b;
1090 }
1091 }
1092 has_each(&roots(&quartic, 1e-12).unwrap(), &[-1.0, 2.0, r]);
1093 }
1094 }
1095 use approx::assert_relative_eq;
1096
1097 const C: Criteria = Criteria {
1098 residual: 1e-13,
1099 step: 1e-14,
1100 max_iterations: 100,
1101 };
1102
1103 #[test]
1104 fn brent_finds_a_simple_root() {
1105 let s = brent(|x| x * x - 2.0, 0.0, 2.0, C).unwrap();
1106 assert!(s.convergence.is_converged());
1107 assert_relative_eq!(s.value, core::f64::consts::SQRT_2, epsilon = 1e-12);
1108 }
1109
1110 #[test]
1111 fn brent_handles_a_root_at_a_bracket_end() {
1112 let s = brent(|x| x, -1.0, 0.0, C).unwrap();
1113 assert_relative_eq!(s.value, 0.0);
1114 assert_eq!(s.iterations, 0);
1115 }
1116
1117 #[test]
1118 fn brent_refuses_a_bracket_without_a_sign_change() {
1119 assert!(brent(|x| x * x + 1.0, -1.0, 1.0, C).is_err());
1120 assert!(brent(|x| x, 1.0, 0.0, C).is_err(), "reversed bracket");
1121 assert!(brent(|x| x, 0.0, f64::NAN, C).is_err());
1122 }
1123
1124 #[test]
1125 fn brent_converges_on_a_function_that_defeats_the_secant_method() {
1126 let s = brent(|x| x.powi(15) - 0.5, 0.0, 2.0, C).unwrap();
1129 assert!(s.convergence.is_converged());
1130 assert!(s.residual.abs() < 1e-12);
1131 assert!(s.iterations < 60, "took {} iterations", s.iterations);
1132 }
1133
1134 #[test]
1135 fn newton_converges_faster_than_bisection_when_it_can() {
1136 let s = newton(|x| (x * x - 2.0, 2.0 * x), 0.5, 2.0, 1.0, C).unwrap();
1137 assert!(s.convergence.is_converged());
1138 assert_relative_eq!(s.value, core::f64::consts::SQRT_2, epsilon = 1e-12);
1139 assert!(s.iterations < 12, "took {} iterations", s.iterations);
1140 }
1141
1142 #[test]
1143 fn newton_survives_a_vanishing_derivative() {
1144 let s = newton(|x| (x * x * x, 3.0 * x * x), -1.0, 2.0, 1.9, C).unwrap();
1147 assert!(s.value.abs() < 1e-4, "landed at {}", s.value);
1148 }
1149
1150 #[test]
1151 fn newton_survives_a_terrible_starting_point() {
1152 for start in [-0.999_f64, 0.0, 1.999, 1.0] {
1153 let s = newton(|x| (x * x - 2.0, 2.0 * x), -1.0, 2.0, start, C).unwrap();
1154 assert!(
1155 (s.value - core::f64::consts::SQRT_2).abs() < 1e-9,
1156 "start {start} gave {}",
1157 s.value
1158 );
1159 }
1160 }
1161
1162 #[test]
1163 fn quadratic_roots_stay_accurate_when_the_roots_are_far_apart() {
1164 let r = quadratic_roots(1.0, -(1e8 + 1e-8), 1.0);
1168 assert_eq!(r.len(), 2);
1169 assert_relative_eq!(r[0], 1e-8, max_relative = 1e-10);
1170 assert_relative_eq!(r[1], 1e8, max_relative = 1e-14);
1171 }
1172
1173 #[test]
1174 fn quadratic_edge_cases() {
1175 assert_eq!(
1176 quadratic_roots(1.0, 0.0, 1.0),
1177 Vec::<f64>::new(),
1178 "no real roots"
1179 );
1180 assert_eq!(quadratic_roots(1.0, -2.0, 1.0), vec![1.0], "double root");
1181 assert_eq!(
1182 quadratic_roots(0.0, 2.0, -4.0),
1183 vec![2.0],
1184 "degenerates to linear"
1185 );
1186 assert_eq!(quadratic_roots(0.0, 0.0, 1.0), Vec::<f64>::new());
1187 let r = quadratic_roots(1.0, 0.0, -4.0);
1188 assert_relative_eq!(r[0], -2.0);
1189 assert_relative_eq!(r[1], 2.0);
1190 }
1191
1192 #[test]
1193 fn cubic_with_three_real_roots() {
1194 let r = cubic_roots(1.0, 0.0, -7.0, 6.0);
1196 assert_eq!(r.len(), 3);
1197 assert_relative_eq!(r[0], -3.0, epsilon = 1e-12);
1198 assert_relative_eq!(r[1], 1.0, epsilon = 1e-12);
1199 assert_relative_eq!(r[2], 2.0, epsilon = 1e-12);
1200 }
1201
1202 #[test]
1203 fn cubic_with_one_real_root() {
1204 let r = cubic_roots(1.0, 0.0, 1.0, 1.0);
1206 assert_eq!(r.len(), 1);
1207 assert_relative_eq!(r[0], -0.682_327_803_828_019_3, epsilon = 1e-12);
1208 }
1209
1210 #[test]
1211 fn cubic_with_repeated_roots() {
1212 let r = cubic_roots(1.0, -3.0, 0.0, 4.0);
1214 assert_eq!(r.len(), 2, "a repeated root is reported once");
1215 assert_relative_eq!(r[0], -1.0, epsilon = 1e-9);
1216 assert_relative_eq!(r[1], 2.0, epsilon = 1e-9);
1217 let t = cubic_roots(1.0, 0.0, 0.0, 0.0);
1219 assert_eq!(t, vec![0.0]);
1220 }
1221
1222 #[test]
1223 fn roots_strips_leading_zeros_before_choosing_a_method() {
1224 let r = roots(&[-4.0, 0.0, 1.0, 0.0], 1e-12).unwrap();
1227 assert_eq!(r.len(), 2);
1228 assert_relative_eq!(r[0], -2.0, epsilon = 1e-12);
1229 assert_relative_eq!(r[1], 2.0, epsilon = 1e-12);
1230 }
1231
1232 #[test]
1233 fn roots_of_a_quartic() {
1234 let r = roots(&[24.0, -50.0, 35.0, -10.0, 1.0], 1e-9).unwrap();
1236 assert_eq!(r.len(), 4);
1237 for (got, want) in r.iter().zip([1.0, 2.0, 3.0, 4.0]) {
1238 assert_relative_eq!(got, &want, epsilon = 1e-7);
1239 }
1240 }
1241
1242 #[test]
1243 fn roots_of_a_high_degree_polynomial() {
1244 let r = roots(&[-120.0, 274.0, -225.0, 85.0, -15.0, 1.0], 1e-9).unwrap();
1246 assert_eq!(r.len(), 5);
1247 for (got, want) in r.iter().zip([1.0, 2.0, 3.0, 4.0, 5.0]) {
1248 assert_relative_eq!(got, &want, epsilon = 1e-6);
1249 }
1250 }
1251
1252 #[test]
1253 fn roots_degenerate_cases() {
1254 assert!(roots(&[], 1e-12).is_err());
1255 assert!(roots(&[0.0, 0.0], 1e-12).is_err());
1256 assert_eq!(
1257 roots(&[5.0], 1e-12).unwrap(),
1258 Vec::<f64>::new(),
1259 "a nonzero constant"
1260 );
1261 assert_eq!(roots(&[0.0, 1.0], 1e-12).unwrap(), vec![0.0]);
1262 }
1263
1264 #[test]
1265 fn minimize_finds_a_smooth_minimum() {
1266 let s = minimize(|x| (x - 0.3) * (x - 0.3) + 1.0, -2.0, 2.0, C).unwrap();
1267 assert_relative_eq!(s.value, 0.3, epsilon = 1e-7);
1268 assert_relative_eq!(s.residual, 1.0, epsilon = 1e-12);
1269 }
1270
1271 #[test]
1272 fn minimize_handles_a_flat_minimum() {
1273 let s = minimize(|x: f64| (x - 0.5).powi(4), -1.0, 2.0, C).unwrap();
1276 assert!((s.value - 0.5).abs() < 1e-3, "landed at {}", s.value);
1277 assert!(s.residual < 1e-12);
1278 }
1279
1280 #[test]
1281 fn minimize_refuses_a_malformed_bracket() {
1282 assert!(minimize(|x| x, 1.0, 0.0, C).is_err());
1283 assert!(minimize(|x| x, 0.0, f64::INFINITY, C).is_err());
1284 }
1285
1286 #[test]
1287 fn newton_system_solves_a_two_by_two() {
1288 let s = newton_system(
1290 |v| {
1291 let (x, y) = (v[0], v[1]);
1292 (
1293 vec![x.mul_add(x, y * y) - 25.0, x - y - 1.0],
1294 vec![vec![2.0 * x, 2.0 * y], vec![1.0, -1.0]],
1295 )
1296 },
1297 &[5.0, 1.0],
1298 C,
1299 )
1300 .unwrap();
1301 assert!(s.convergence.is_converged());
1302 assert_relative_eq!(s.value[0], 4.0, epsilon = 1e-10);
1303 assert_relative_eq!(s.value[1], 3.0, epsilon = 1e-10);
1304 }
1305
1306 #[test]
1307 fn newton_system_damping_survives_a_start_where_plain_newton_diverges() {
1308 let s = newton_system(
1314 |v| {
1315 let (x, y) = (v[0], v[1]);
1316 (
1317 vec![x.atan(), y.atan()],
1318 vec![
1319 vec![x.mul_add(x, 1.0).recip(), 0.0],
1320 vec![0.0, y.mul_add(y, 1.0).recip()],
1321 ],
1322 )
1323 },
1324 &[5.0, 5.0],
1325 C,
1326 )
1327 .unwrap();
1328 assert!(s.convergence.is_converged(), "{s:?}");
1329 assert!(s.value[0].abs() < 1e-9 && s.value[1].abs() < 1e-9, "{s:?}");
1330 }
1331
1332 #[test]
1333 fn newton_system_reports_a_residual_minimum_rather_than_looping() {
1334 let s = newton_system(
1338 |v| {
1339 let (x, y) = (v[0], v[1]);
1340 (
1341 vec![x.mul_add(x, 1.0), y],
1342 vec![vec![2.0 * x, 0.0], vec![0.0, 1.0]],
1343 )
1344 },
1345 &[2.0, 2.0],
1346 C,
1347 )
1348 .unwrap();
1349 assert!(!s.convergence.is_converged());
1350 assert!(s.residual >= 1.0, "the residual cannot go below 1 here");
1351 }
1352
1353 #[test]
1354 fn newton_system_reports_a_singular_jacobian_rather_than_looping() {
1355 let s = newton_system(
1356 |v| {
1357 (
1358 vec![v[0] * v[0], v[1]],
1359 vec![vec![2.0 * v[0], 0.0], vec![0.0, 0.0]],
1360 )
1361 },
1362 &[1.0, 1.0],
1363 C,
1364 );
1365 assert!(s.is_err());
1366 }
1367
1368 #[test]
1369 fn newton_system_checks_its_shapes() {
1370 let s = newton_system(|_| (vec![1.0], vec![vec![1.0, 2.0]]), &[0.0, 0.0], C);
1371 assert!(s.is_err());
1372 }
1373
1374 #[test]
1375 fn exhausted_is_reported_not_hidden() {
1376 let s = brent(
1379 |x| x * x - 2.0,
1380 0.0,
1381 2.0,
1382 Criteria {
1383 max_iterations: 1,
1384 ..C
1385 },
1386 )
1387 .unwrap();
1388 assert_eq!(s.convergence, Convergence::Exhausted);
1389 assert!(!s.convergence.is_converged());
1390 }
1391
1392 #[test]
1396 fn the_fixed_solver_matches_the_general_one_to_the_bit() {
1397 fn check<const N: usize>(start: [f64; N]) {
1398 let system = |x: &[f64]| {
1399 let mut r = vec![0.0; N];
1400 let mut j = vec![vec![0.0; N]; N];
1401 for i in 0..N {
1402 let k = (i + 1) % N;
1403 #[allow(clippy::cast_precision_loss)]
1404 let weight = 1.0 + i as f64 * 0.37;
1405 r[i] = x[i].mul_add(x[k], -weight) + x[k].sin() * 0.3;
1406 j[i][i] += x[k];
1407 j[i][k] += x[i] + x[k].cos() * 0.3;
1408 }
1409 (r, j)
1410 };
1411 let criteria = Criteria {
1412 residual: 1e-14,
1413 step: 1e-15,
1414 max_iterations: 60,
1415 };
1416 let general = newton_system(system, &start, criteria).unwrap();
1417 let fixed = newton_system_fixed(
1418 |x: &[f64; N]| {
1419 let (r, j) = system(x);
1420 let mut rows = [[0.0; N]; N];
1421 for (to, from) in rows.iter_mut().zip(&j) {
1422 to.copy_from_slice(from);
1423 }
1424 (r.try_into().unwrap(), rows)
1425 },
1426 start,
1427 criteria,
1428 )
1429 .unwrap();
1430 assert_eq!(general.value, fixed.0.to_vec());
1431 assert_eq!(general.residual.to_bits(), fixed.1.to_bits());
1432 assert_eq!(general.convergence, fixed.2);
1433 assert_eq!(general.iterations, fixed.3);
1434 }
1435 check([1.0, 1.0, 1.0]);
1436 check([0.5, 2.0, -1.0, 1.5]);
1437 check([1.0, -1.0, 1.0, -1.0, 2.0]);
1438 check([3.0, 0.2, 0.7, 1.1, 0.4]);
1439 }
1440}