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>> {
347 let mut c = coefficients;
353 while let Some((&last, rest)) = c.split_last() {
354 if last == 0.0 || negligible_lead(last, rest, tolerance) {
355 c = rest;
356 } else {
357 break;
358 }
359 }
360
361 let mut out = match c.len() {
362 0 => ogeom_bail!(
363 Construction,
364 "the zero polynomial has every value as a root"
365 ),
366 1 => Vec::new(),
367 2 => vec![-c[0] / c[1]],
368 3 => quadratic_roots(c[2], c[1], c[0]),
369 4 => cubic_roots(c[3], c[2], c[1], c[0]),
370 n => {
371 let mut found = vec![0.0; n - 1];
372 let count = real_roots(c, 0.0, &mut found);
373 found.truncate(count);
374 found
375 }
376 };
377
378 out.retain(|r| r.is_finite());
379 out.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
380 out.dedup_by(|a, b| (*a - *b).abs() <= tolerance * a.abs().max(1.0));
381 Ok(out)
382}
383
384#[must_use]
391pub fn quadratic_roots(a: f64, b: f64, c: f64) -> Vec<f64> {
392 if a == 0.0 {
393 return if b == 0.0 { Vec::new() } else { vec![-c / b] };
394 }
395 let discriminant = b.mul_add(b, -(4.0 * a * c));
396 let rounding = 8.0 * f64::EPSILON * b.mul_add(b, (4.0 * a * c).abs());
399 if discriminant.abs() <= rounding {
400 return vec![-b / (2.0 * a)];
401 }
402 if discriminant < 0.0 {
403 return Vec::new();
404 }
405 let sqrt = discriminant.sqrt();
406 let q = -0.5 * (b + b.signum() * sqrt);
409 let (r1, r2) = (q / a, if q == 0.0 { 0.0 } else { c / q });
410 if r1 <= r2 { vec![r1, r2] } else { vec![r2, r1] }
411}
412
413#[must_use]
419pub fn cubic_roots(a: f64, b: f64, c: f64, d: f64) -> Vec<f64> {
420 if a == 0.0 {
421 return quadratic_roots(b, c, d);
422 }
423 let (b, c, d) = (b / a, c / a, d / a);
424 let shift = b / 3.0;
425 let p = shift.mul_add(-b, c);
427 let q = (2.0 / 27.0 * b * b).mul_add(b, shift.mul_add(-c, d));
428
429 let half_q = q / 2.0;
430 let third_p = p / 3.0;
431 let discriminant = half_q.mul_add(half_q, third_p * third_p * third_p);
432 let p_rounding = 8.0 * f64::EPSILON * ((shift * b).abs() + c.abs());
435 let q_rounding =
436 8.0 * f64::EPSILON * ((2.0 / 27.0 * b * b * b).abs() + (shift * c).abs() + d.abs());
437 let rounding = half_q.abs() * q_rounding
438 + third_p * third_p * p_rounding
439 + 8.0 * f64::EPSILON * (half_q * half_q + (third_p * third_p * third_p).abs());
440
441 if discriminant.abs() <= rounding {
442 if p.abs() <= p_rounding {
443 vec![-shift]
444 } else {
445 let mut r = vec![3.0 * q / p - shift, -1.5 * q / p - shift];
447 r.sort_by(|x, y| x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal));
448 r
449 }
450 } else if discriminant > 0.0 {
451 let sqrt = discriminant.sqrt();
452 let u = (-half_q + sqrt).cbrt();
453 let v = (-half_q - sqrt).cbrt();
454 vec![u + v - shift]
455 } else {
456 let radius = (-third_p).sqrt();
458 let cos = (-half_q / (radius * radius * radius)).clamp(-1.0, 1.0);
459 let angle = cos.acos() / 3.0;
460 let scale = 2.0 * radius;
461 let tau_third = core::f64::consts::TAU / 3.0;
462 let mut r = vec![
463 scale.mul_add(angle.cos(), -shift),
464 scale.mul_add((angle - tau_third).cos(), -shift),
465 scale.mul_add((angle + tau_third).cos(), -shift),
466 ];
467 r.sort_by(|x, y| x.partial_cmp(y).unwrap_or(core::cmp::Ordering::Equal));
468 r
469 }
470}
471
472fn negligible_lead(lead: f64, rest: &[f64], tolerance: f64) -> bool {
484 let Some(m) = rest.iter().rposition(|v| *v != 0.0) else {
485 return false;
486 };
487 if m == 0 {
488 return false;
489 }
490 let top = rest[m].abs();
491 let mut bound = 0.0_f64;
492 for k in 1..=m {
493 let ratio = (rest[m - k] / rest[m]).abs();
494 let ratio = if k == m { ratio / 2.0 } else { ratio };
495 #[allow(clippy::cast_precision_loss, reason = "a degree")]
496 let term = if k == 1 {
497 ratio
498 } else {
499 ratio.powf(1.0 / k as f64)
500 };
501 bound = bound.max(term);
502 }
503 let bound = (2.0 * bound).max(1.0);
504 if !bound.is_finite() {
505 return false;
506 }
507 let mut largest = 0.0_f64;
509 let mut power = 1.0_f64;
510 for v in rest {
511 largest = largest.max(v.abs() * power);
512 power *= bound;
513 }
514 let leading = lead.abs() * power;
515 leading <= tolerance * largest.max(top)
516}
517
518const STACK_DEGREE: usize = 16;
520
521pub fn real_roots(coefficients: &[f64], touch: f64, out: &mut [f64]) -> usize {
549 let mut c = coefficients;
550 while let Some((&0.0, rest)) = c.split_last() {
551 c = rest;
552 }
553 if c.len() < 2 || c.iter().any(|v| !v.is_finite()) {
554 return 0;
555 }
556 let n = c.len() - 1;
557 assert!(out.len() >= n, "room for {} of {n} roots", out.len());
558 let lead = c[n];
562 let mut bound = 0.0_f64;
563 for k in 1..=n {
564 let ratio = (c[n - k] / lead).abs();
565 let ratio = if k == n { ratio / 2.0 } else { ratio };
566 #[allow(clippy::cast_precision_loss, reason = "a degree")]
567 let term = if k == 1 {
568 ratio
569 } else {
570 ratio.powf(1.0 / k as f64)
571 };
572 bound = bound.max(term);
573 }
574 let bound = if bound > 0.0 { 4.0 * bound } else { 1.0 };
575 if !bound.is_finite() {
576 return 0;
577 }
578 monotone_roots(c, touch, bound, out)
579}
580
581fn monotone_roots(c: &[f64], touch: f64, bound: f64, out: &mut [f64]) -> usize {
583 let n = c.len() - 1;
584 if n == 1 {
585 out[0] = -c[0] / c[1];
586 return 1;
587 }
588 if n <= STACK_DEGREE {
589 let mut slope = [0.0; STACK_DEGREE];
590 let mut critical = [0.0; STACK_DEGREE];
591 split_roots(
592 c,
593 touch,
594 bound,
595 out,
596 &mut slope[..n],
597 &mut critical[..n - 1],
598 )
599 } else {
600 let (mut slope, mut critical) = (vec![0.0; n], vec![0.0; n - 1]);
601 split_roots(c, touch, bound, out, &mut slope, &mut critical)
602 }
603}
604
605fn split_roots(
608 c: &[f64],
609 touch: f64,
610 bound: f64,
611 out: &mut [f64],
612 slope: &mut [f64],
613 critical: &mut [f64],
614) -> usize {
615 let n = c.len() - 1;
616 for (k, s) in slope.iter_mut().enumerate() {
617 #[allow(clippy::cast_precision_loss, reason = "a degree")]
618 let power = (k + 1) as f64;
619 *s = power * c[k + 1];
620 }
621 let found = monotone_roots(slope, 0.0, bound, critical);
622 #[allow(clippy::cast_precision_loss, reason = "a degree")]
626 let rounding = 32.0 * n as f64 * f64::EPSILON;
627
628 let mut count = 0;
629 let mut push = |x: f64| {
630 if count < out.len() {
631 out[count] = x;
632 count += 1;
633 }
634 };
635 let mut a = -bound;
640 let (mut fa, _, _) = value_and_slope(c, a);
641 let mut a_touches = false;
642 let mut crossed_before = false;
643 let ends = critical[..found].iter().map(|&x| (x, true));
644 for (b, is_critical) in ends.chain(core::iter::once((bound, false))) {
645 if b <= a {
646 continue;
647 }
648 let (mut fb, _, size) = value_and_slope(c, b);
649 let b_touches = is_critical && fb.abs() <= touch;
650 if is_critical && fb.abs() <= rounding * size {
651 fb = 0.0;
652 }
653 let crosses = fa * fb < 0.0;
654 if fa == 0.0 || (a_touches && !crossed_before && !crosses) {
655 push(a);
656 }
657 if crosses {
658 push(monotone_root(c, a, b, fa, fb));
659 }
660 crossed_before = crosses;
661 a_touches = b_touches;
662 (a, fa) = (b, fb);
663 }
664 if fa == 0.0 {
665 push(a);
666 }
667 count
668}
669
670fn value_and_slope(c: &[f64], x: f64) -> (f64, f64, f64) {
673 let (mut p, mut dp, mut size) = (0.0_f64, 0.0_f64, 0.0_f64);
674 let magnitude = x.abs();
675 for &coefficient in c.iter().rev() {
676 dp = dp.mul_add(x, p);
677 p = p.mul_add(x, coefficient);
678 size = size.mul_add(magnitude, coefficient.abs());
679 }
680 (p, dp, size)
681}
682
683fn monotone_root(c: &[f64], mut a: f64, mut b: f64, fa: f64, fb: f64) -> f64 {
688 let rising = fb > 0.0;
689 let mut x = a - fa * (b - a) / (fb - fa);
690 if !(x > a && x < b) {
691 x = f64::midpoint(a, b);
692 }
693 let (mut step, mut previous) = (b - a, b - a);
694 for _ in 0..128 {
695 let (f, df, _) = value_and_slope(c, x);
696 if f == 0.0 {
697 return x;
698 }
699 if (f > 0.0) == rising {
700 b = x;
701 } else {
702 a = x;
703 }
704 let delta = f / df;
705 let newton = x - delta;
706 let next = if newton > a && newton < b && delta.abs() * 2.0 <= previous.abs() {
707 previous = step;
708 step = delta;
709 newton
710 } else {
711 previous = step;
712 step = 0.5 * (b - a);
713 f64::midpoint(a, b)
714 };
715 if next == x || step.abs() <= 2.0 * f64::EPSILON * next.abs() || next == a || next == b {
718 return next;
719 }
720 x = next;
721 }
722 x
723}
724
725pub fn minimize<F>(mut f: F, a: f64, b: f64, criteria: Criteria) -> OgeomResult<Solution>
737where
738 F: FnMut(f64) -> f64,
739{
740 if !a.is_finite() || !b.is_finite() || a >= b {
741 ogeom_bail!(Construction, "bracket [{a}, {b}] is empty or non-finite");
742 }
743 const GOLDEN: f64 = 0.381_966_011_250_105_15;
745
746 let (mut low, mut high) = (a, b);
747 let mut x = GOLDEN.mul_add(b - a, a);
748 let (mut w, mut v) = (x, x);
749 let mut fx = f(x);
750 let (mut fw, mut fv) = (fx, fx);
751 let mut step = 0.0_f64;
752 let mut previous_step = 0.0_f64;
753
754 for iteration in 1..=criteria.max_iterations {
755 let middle = f64::midpoint(low, high);
756 let tolerance = criteria.step.mul_add(x.abs(), criteria.step);
757 if (x - middle).abs() <= 2.0f64.mul_add(tolerance, -((high - low) / 2.0)) {
758 return Ok(Solution {
759 value: x,
760 residual: fx,
761 convergence: Convergence::Step,
762 iterations: iteration,
763 });
764 }
765
766 let mut use_golden = true;
767 if previous_step.abs() > tolerance {
768 let r = (x - w) * (fx - fv);
770 let q = (x - v) * (fx - fw);
771 let mut p = (x - v) * q - (x - w) * r;
772 let mut q = 2.0 * (q - r);
773 if q > 0.0 {
774 p = -p;
775 }
776 q = q.abs();
777 if p.abs() < (0.5 * q * previous_step).abs() && p > q * (low - x) && p < q * (high - x)
780 {
781 step = p / q;
782 let candidate = x + step;
783 if candidate - low < 2.0 * tolerance || high - candidate < 2.0 * tolerance {
784 step = if x < middle { tolerance } else { -tolerance };
785 }
786 use_golden = false;
787 }
788 }
789 if use_golden {
790 previous_step = if x < middle { high - x } else { low - x };
791 step = GOLDEN * previous_step;
792 }
793
794 let next = if step.abs() >= tolerance {
795 x + step
796 } else if step > 0.0 {
797 x + tolerance
798 } else {
799 x - tolerance
800 };
801 let fnext = f(next);
802
803 if fnext <= fx {
804 if next < x {
805 high = x;
806 } else {
807 low = x;
808 }
809 v = w;
810 fv = fw;
811 w = x;
812 fw = fx;
813 x = next;
814 fx = fnext;
815 } else {
816 if next < x {
817 low = next;
818 } else {
819 high = next;
820 }
821 if fnext <= fw || w == x {
822 v = w;
823 fv = fw;
824 w = next;
825 fw = fnext;
826 } else if fnext <= fv || v == x || v == w {
827 v = next;
828 fv = fnext;
829 }
830 }
831 previous_step = step;
832 }
833 Ok(Solution {
834 value: x,
835 residual: fx,
836 convergence: Convergence::Exhausted,
837 iterations: criteria.max_iterations,
838 })
839}
840
841#[derive(Debug, Clone, PartialEq)]
843pub struct SystemSolution {
844 pub value: Vec<f64>,
846 pub residual: f64,
848 pub convergence: Convergence,
850 pub iterations: usize,
852}
853
854pub fn newton_system<F>(mut f: F, start: &[f64], criteria: Criteria) -> OgeomResult<SystemSolution>
876where
877 F: FnMut(&[f64]) -> (Vec<f64>, Vec<Vec<f64>>),
878{
879 let n = start.len();
880 let mut x = DVector::from_row_slice(start);
881
882 let evaluate = |x: &DVector<f64>, f: &mut F| {
883 let (r, j) = f(x.as_slice());
884 (r, j)
885 };
886
887 let (mut residual, mut jacobian) = evaluate(&x, &mut f);
888 if residual.len() != n || jacobian.len() != n || jacobian.iter().any(|row| row.len() != n) {
889 ogeom_bail!(
890 Dimension,
891 "expected a {n}-vector residual and {n}x{n} Jacobian"
892 );
893 }
894 let mut norm = residual.iter().map(|v| v * v).sum::<f64>().sqrt();
895
896 for iteration in 1..=criteria.max_iterations {
897 if norm <= criteria.residual {
898 return Ok(SystemSolution {
899 value: x.as_slice().to_vec(),
900 residual: norm,
901 convergence: Convergence::Residual,
902 iterations: iteration - 1,
903 });
904 }
905
906 let j = DMatrix::from_fn(n, n, |r, c| jacobian[r][c]);
907 let rhs = DVector::from_row_slice(&residual);
908 let Some(delta) = j.lu().solve(&rhs) else {
909 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
910 };
911
912 let mut scale = 1.0;
915 let mut accepted = None;
916 for _ in 0..30 {
917 let candidate = &x - &delta * scale;
918 let (r, jj) = evaluate(&candidate, &mut f);
919 let candidate_norm = r.iter().map(|v| v * v).sum::<f64>().sqrt();
920 if candidate_norm < norm || candidate_norm <= criteria.residual {
921 accepted = Some((candidate, r, jj, candidate_norm));
922 break;
923 }
924 scale *= 0.5;
925 }
926
927 let Some((next, r, jj, next_norm)) = accepted else {
928 return Ok(SystemSolution {
931 value: x.as_slice().to_vec(),
932 residual: norm,
933 convergence: Convergence::Exhausted,
934 iterations: iteration,
935 });
936 };
937
938 let step = (&next - &x).norm();
939 x = next;
940 residual = r;
941 jacobian = jj;
942 norm = next_norm;
943
944 if norm <= criteria.residual {
945 return Ok(SystemSolution {
946 value: x.as_slice().to_vec(),
947 residual: norm,
948 convergence: Convergence::Residual,
949 iterations: iteration,
950 });
951 }
952 if step <= criteria.step {
953 return Ok(SystemSolution {
954 value: x.as_slice().to_vec(),
955 residual: norm,
956 convergence: Convergence::Step,
957 iterations: iteration,
958 });
959 }
960 }
961 Ok(SystemSolution {
962 value: x.as_slice().to_vec(),
963 residual: norm,
964 convergence: Convergence::Exhausted,
965 iterations: criteria.max_iterations,
966 })
967}
968
969pub fn newton_system_fixed<const N: usize, F>(
981 mut f: F,
982 start: [f64; N],
983 criteria: Criteria,
984) -> OgeomResult<([f64; N], f64, Convergence, usize)>
985where
986 F: FnMut(&[f64; N]) -> ([f64; N], [[f64; N]; N]),
987{
988 let last = std::cell::Cell::new([[0.0; N]; N]);
991 newton_system_fixed_lazy(
992 |x| {
993 let (residual, jacobian) = f(x);
994 last.set(jacobian);
995 residual
996 },
997 |_| Some(last.get()),
998 start,
999 criteria,
1000 )
1001}
1002
1003pub fn newton_system_fixed_lazy<const N: usize, R, J>(
1018 mut residual_at: R,
1019 mut jacobian_at: J,
1020 start: [f64; N],
1021 criteria: Criteria,
1022) -> OgeomResult<([f64; N], f64, Convergence, usize)>
1023where
1024 R: FnMut(&[f64; N]) -> [f64; N],
1025 J: FnMut(&[f64; N]) -> Option<[[f64; N]; N]>,
1026{
1027 let norm_of = |r: &[f64; N]| r.iter().map(|v| v * v).sum::<f64>().sqrt();
1028 let mut x = start;
1029 let first = residual_at(&x);
1030 let (mut residual, mut jacobian) =
1031 jacobian_at(&x).map_or(([f64::INFINITY; N], [[0.0; N]; N]), |j| (first, j));
1032 let mut norm = norm_of(&residual);
1033 for iteration in 1..=criteria.max_iterations {
1034 if norm <= criteria.residual {
1035 return Ok((x, norm, Convergence::Residual, iteration - 1));
1036 }
1037 let Some(delta) = solve_fixed(jacobian, residual) else {
1038 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
1039 };
1040 let mut scale = 1.0;
1041 let mut accepted = None;
1042 for _ in 0..30 {
1043 let mut candidate = x;
1044 for (value, d) in candidate.iter_mut().zip(delta.iter()) {
1045 *value -= d * scale;
1046 }
1047 let r = residual_at(&candidate);
1048 let candidate_norm = norm_of(&r);
1049 if (candidate_norm < norm || candidate_norm <= criteria.residual)
1052 && let Some(jj) = jacobian_at(&candidate)
1053 {
1054 accepted = Some((candidate, r, jj, candidate_norm));
1055 break;
1056 }
1057 scale *= 0.5;
1058 }
1059 let Some((next, r, jj, next_norm)) = accepted else {
1060 return Ok((x, norm, Convergence::Exhausted, iteration));
1061 };
1062 let step = next
1063 .iter()
1064 .zip(&x)
1065 .map(|(a, b)| (a - b) * (a - b))
1066 .sum::<f64>()
1067 .sqrt();
1068 x = next;
1069 residual = r;
1070 jacobian = jj;
1071 norm = next_norm;
1072 if norm <= criteria.residual {
1073 return Ok((x, norm, Convergence::Residual, iteration));
1074 }
1075 if step <= criteria.step {
1076 return Ok((x, norm, Convergence::Step, iteration));
1077 }
1078 }
1079 Ok((x, norm, Convergence::Exhausted, criteria.max_iterations))
1080}
1081
1082#[must_use]
1085pub fn linear_fixed<const N: usize>(a: [[f64; N]; N], b: [f64; N]) -> Option<[f64; N]> {
1086 solve_fixed(a, b)
1087}
1088
1089fn solve_fixed<const N: usize>(mut a: [[f64; N]; N], mut b: [f64; N]) -> Option<[f64; N]> {
1097 for col in 0..N {
1098 let mut pivot = col;
1099 for row in col + 1..N {
1100 if a[row][col].abs() > a[pivot][col].abs() {
1101 pivot = row;
1102 }
1103 }
1104 let diag = a[pivot][col];
1105 if diag == 0.0 {
1106 return None;
1107 }
1108 a.swap(col, pivot);
1109 b.swap(col, pivot);
1110 let inverse = 1.0 / diag;
1111 let head = a[col];
1112 for row in col + 1..N {
1113 let factor = a[row][col] * inverse;
1114 for (entry, above) in a[row].iter_mut().zip(&head).skip(col + 1) {
1115 *entry -= factor * above;
1116 }
1117 b[row] -= factor * b[col];
1118 }
1119 }
1120 for col in (0..N).rev() {
1121 b[col] /= a[col][col];
1122 let known = b[col];
1123 for row in 0..col {
1124 b[row] -= a[row][col] * known;
1125 }
1126 }
1127 Some(b)
1128}
1129
1130pub fn newton_system_2<F>(
1145 mut f: F,
1146 start: [f64; 2],
1147 criteria: Criteria,
1148) -> OgeomResult<([f64; 2], f64, Convergence, usize)>
1149where
1150 F: FnMut([f64; 2]) -> ([f64; 2], [[f64; 2]; 2]),
1151{
1152 let mut x = start;
1153 let (mut residual, mut jacobian) = f(x);
1154 let mut norm = residual[0].hypot(residual[1]);
1155
1156 for iteration in 1..=criteria.max_iterations {
1157 if norm <= criteria.residual {
1158 return Ok((x, norm, Convergence::Residual, iteration - 1));
1159 }
1160
1161 let (row0, row1, rhs0, rhs1) = if jacobian[0][0].abs() >= jacobian[1][0].abs() {
1163 (jacobian[0], jacobian[1], residual[0], residual[1])
1164 } else {
1165 (jacobian[1], jacobian[0], residual[1], residual[0])
1166 };
1167 if row0[0].abs() <= f64::EPSILON * (row1[0].abs() + row0[1].abs()).max(1.0) {
1168 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
1169 }
1170 let factor = row1[0] / row0[0];
1171 let denom = factor.mul_add(-row0[1], row1[1]);
1172 if denom.abs() <= f64::EPSILON * row0[1].abs().max(1.0) {
1173 ogeom_bail!(Numeric, "Jacobian is singular after {iteration} iterations");
1174 }
1175 let d1 = factor.mul_add(-rhs0, rhs1) / denom;
1176 let d0 = d1.mul_add(-row0[1], rhs0) / row0[0];
1177 let delta = [d0, d1];
1178
1179 let mut scale = 1.0;
1181 let mut accepted = None;
1182 for _ in 0..30 {
1183 let candidate = [
1184 delta[0].mul_add(-scale, x[0]),
1185 delta[1].mul_add(-scale, x[1]),
1186 ];
1187 let (r, jj) = f(candidate);
1188 let candidate_norm = r[0].hypot(r[1]);
1189 if candidate_norm < norm || candidate_norm <= criteria.residual {
1190 accepted = Some((candidate, r, jj, candidate_norm));
1191 break;
1192 }
1193 scale *= 0.5;
1194 }
1195 let Some((next, r, jj, next_norm)) = accepted else {
1196 return Ok((x, norm, Convergence::Exhausted, iteration));
1197 };
1198
1199 let step = (next[0] - x[0]).hypot(next[1] - x[1]);
1200 x = next;
1201 residual = r;
1202 jacobian = jj;
1203 norm = next_norm;
1204
1205 if norm <= criteria.residual {
1206 return Ok((x, norm, Convergence::Residual, iteration));
1207 }
1208 if step <= criteria.step {
1209 return Ok((x, norm, Convergence::Step, iteration));
1210 }
1211 }
1212 Ok((x, norm, Convergence::Exhausted, criteria.max_iterations))
1213}
1214
1215#[cfg(test)]
1216#[allow(clippy::unwrap_used)]
1217mod tests {
1218 use super::*;
1219
1220 fn has_each(found: &[f64], expected: &[f64]) {
1223 for e in expected {
1224 assert!(
1225 found
1226 .iter()
1227 .any(|f| (f - e).abs() <= 1e-6 * e.abs().max(1.0)),
1228 "{e} missing from {found:?}"
1229 );
1230 }
1231 }
1232
1233 #[test]
1236 fn a_quartic_with_roots_paired_by_sign_comes_back() {
1237 let found = roots(&[64.0, 0.0, -20.0, 0.0, 1.0], 1e-12).unwrap();
1239 assert_eq!(found.len(), 4, "{found:?}");
1240 for (got, want) in found.iter().zip([-4.0, -2.0, 2.0, 4.0]) {
1241 assert!((got - want).abs() < 1e-9, "{found:?}");
1242 }
1243 assert!(
1245 roots(&[64.0, 0.0, 4.0, 0.0, 1.0], 1e-12)
1246 .unwrap()
1247 .is_empty()
1248 );
1249 }
1250
1251 #[test]
1252 fn double_roots_survive_rounding() {
1253 for r in [0.1, 0.3, 0.7, 1.1, 3.3, 1000.0 / 3.0, -2.9, 1e-3 / 7.0] {
1254 has_each(&quadratic_roots(1.0, -2.0 * r, r * r), &[r]);
1256 has_each(
1258 &cubic_roots(1.0, -(1.0 + 2.0 * r), 2.0f64.mul_add(r, r * r), -(r * r)),
1259 &[1.0, r],
1260 );
1261 let quadratic = [r * r, -2.0 * r, 1.0];
1263 let pair = [-2.0, -1.0, 1.0];
1264 let mut quartic = [0.0; 5];
1265 for (i, a) in quadratic.iter().enumerate() {
1266 for (j, b) in pair.iter().enumerate() {
1267 quartic[i + j] += a * b;
1268 }
1269 }
1270 has_each(&roots(&quartic, 1e-12).unwrap(), &[-1.0, 2.0, r]);
1271 }
1272 }
1273 use approx::assert_relative_eq;
1274
1275 const C: Criteria = Criteria {
1276 residual: 1e-13,
1277 step: 1e-14,
1278 max_iterations: 100,
1279 };
1280
1281 #[test]
1282 fn brent_finds_a_simple_root() {
1283 let s = brent(|x| x * x - 2.0, 0.0, 2.0, C).unwrap();
1284 assert!(s.convergence.is_converged());
1285 assert_relative_eq!(s.value, core::f64::consts::SQRT_2, epsilon = 1e-12);
1286 }
1287
1288 #[test]
1289 fn brent_handles_a_root_at_a_bracket_end() {
1290 let s = brent(|x| x, -1.0, 0.0, C).unwrap();
1291 assert_relative_eq!(s.value, 0.0);
1292 assert_eq!(s.iterations, 0);
1293 }
1294
1295 #[test]
1296 fn brent_refuses_a_bracket_without_a_sign_change() {
1297 assert!(brent(|x| x * x + 1.0, -1.0, 1.0, C).is_err());
1298 assert!(brent(|x| x, 1.0, 0.0, C).is_err(), "reversed bracket");
1299 assert!(brent(|x| x, 0.0, f64::NAN, C).is_err());
1300 }
1301
1302 #[test]
1303 fn brent_converges_on_a_function_that_defeats_the_secant_method() {
1304 let s = brent(|x| x.powi(15) - 0.5, 0.0, 2.0, C).unwrap();
1307 assert!(s.convergence.is_converged());
1308 assert!(s.residual.abs() < 1e-12);
1309 assert!(s.iterations < 60, "took {} iterations", s.iterations);
1310 }
1311
1312 #[test]
1313 fn newton_converges_faster_than_bisection_when_it_can() {
1314 let s = newton(|x| (x * x - 2.0, 2.0 * x), 0.5, 2.0, 1.0, C).unwrap();
1315 assert!(s.convergence.is_converged());
1316 assert_relative_eq!(s.value, core::f64::consts::SQRT_2, epsilon = 1e-12);
1317 assert!(s.iterations < 12, "took {} iterations", s.iterations);
1318 }
1319
1320 #[test]
1321 fn newton_survives_a_vanishing_derivative() {
1322 let s = newton(|x| (x * x * x, 3.0 * x * x), -1.0, 2.0, 1.9, C).unwrap();
1325 assert!(s.value.abs() < 1e-4, "landed at {}", s.value);
1326 }
1327
1328 #[test]
1329 fn newton_survives_a_terrible_starting_point() {
1330 for start in [-0.999_f64, 0.0, 1.999, 1.0] {
1331 let s = newton(|x| (x * x - 2.0, 2.0 * x), -1.0, 2.0, start, C).unwrap();
1332 assert!(
1333 (s.value - core::f64::consts::SQRT_2).abs() < 1e-9,
1334 "start {start} gave {}",
1335 s.value
1336 );
1337 }
1338 }
1339
1340 #[test]
1341 fn quadratic_roots_stay_accurate_when_the_roots_are_far_apart() {
1342 let r = quadratic_roots(1.0, -(1e8 + 1e-8), 1.0);
1346 assert_eq!(r.len(), 2);
1347 assert_relative_eq!(r[0], 1e-8, max_relative = 1e-10);
1348 assert_relative_eq!(r[1], 1e8, max_relative = 1e-14);
1349 }
1350
1351 #[test]
1352 fn quadratic_edge_cases() {
1353 assert_eq!(
1354 quadratic_roots(1.0, 0.0, 1.0),
1355 Vec::<f64>::new(),
1356 "no real roots"
1357 );
1358 assert_eq!(quadratic_roots(1.0, -2.0, 1.0), vec![1.0], "double root");
1359 assert_eq!(
1360 quadratic_roots(0.0, 2.0, -4.0),
1361 vec![2.0],
1362 "degenerates to linear"
1363 );
1364 assert_eq!(quadratic_roots(0.0, 0.0, 1.0), Vec::<f64>::new());
1365 let r = quadratic_roots(1.0, 0.0, -4.0);
1366 assert_relative_eq!(r[0], -2.0);
1367 assert_relative_eq!(r[1], 2.0);
1368 }
1369
1370 #[test]
1371 fn cubic_with_three_real_roots() {
1372 let r = cubic_roots(1.0, 0.0, -7.0, 6.0);
1374 assert_eq!(r.len(), 3);
1375 assert_relative_eq!(r[0], -3.0, epsilon = 1e-12);
1376 assert_relative_eq!(r[1], 1.0, epsilon = 1e-12);
1377 assert_relative_eq!(r[2], 2.0, epsilon = 1e-12);
1378 }
1379
1380 #[test]
1381 fn cubic_with_one_real_root() {
1382 let r = cubic_roots(1.0, 0.0, 1.0, 1.0);
1384 assert_eq!(r.len(), 1);
1385 assert_relative_eq!(r[0], -0.682_327_803_828_019_3, epsilon = 1e-12);
1386 }
1387
1388 #[test]
1389 fn cubic_with_repeated_roots() {
1390 let r = cubic_roots(1.0, -3.0, 0.0, 4.0);
1392 assert_eq!(r.len(), 2, "a repeated root is reported once");
1393 assert_relative_eq!(r[0], -1.0, epsilon = 1e-9);
1394 assert_relative_eq!(r[1], 2.0, epsilon = 1e-9);
1395 let t = cubic_roots(1.0, 0.0, 0.0, 0.0);
1397 assert_eq!(t, vec![0.0]);
1398 }
1399
1400 #[test]
1401 fn roots_strips_leading_zeros_before_choosing_a_method() {
1402 let r = roots(&[-4.0, 0.0, 1.0, 0.0], 1e-12).unwrap();
1405 assert_eq!(r.len(), 2);
1406 assert_relative_eq!(r[0], -2.0, epsilon = 1e-12);
1407 assert_relative_eq!(r[1], 2.0, epsilon = 1e-12);
1408 }
1409
1410 #[test]
1411 fn roots_far_from_the_origin_keep_their_leading_term() {
1412 let r = 1e6_f64;
1416 let c = [r * r * r * r, -4.0 * r * r * r, 6.0 * r * r, -4.0 * r, 1.0];
1417 let found = roots(&c, 1e-9).unwrap();
1418 assert_eq!(found.len(), 1, "{found:?}");
1419 assert_relative_eq!(found[0], r, max_relative = 1e-6);
1420 let c = [-r * r * r, 3.0 * r * r, -3.0 * r, 1.0];
1422 let found = roots(&c, 1e-9).unwrap();
1423 assert_eq!(found.len(), 1, "{found:?}");
1424 assert_relative_eq!(found[0], r, max_relative = 1e-6);
1425 let s = 1e5_f64;
1426 let found = roots(&[-s * s, 0.0, 1.0], 1e-9).unwrap();
1427 assert_eq!(found.len(), 2, "{found:?}");
1428 assert_relative_eq!(found[0], -s, max_relative = 1e-12);
1429 assert_relative_eq!(found[1], s, max_relative = 1e-12);
1430 let found = roots(&[-4.0, 0.0, 1.0, 1e-14], 1e-9).unwrap();
1434 assert_eq!(found.len(), 2, "{found:?}");
1435 assert_relative_eq!(found[0], -2.0, epsilon = 1e-9);
1436 assert_relative_eq!(found[1], 2.0, epsilon = 1e-9);
1437 }
1438
1439 #[test]
1440 fn roots_of_a_quartic() {
1441 let r = roots(&[24.0, -50.0, 35.0, -10.0, 1.0], 1e-9).unwrap();
1443 assert_eq!(r.len(), 4);
1444 for (got, want) in r.iter().zip([1.0, 2.0, 3.0, 4.0]) {
1445 assert_relative_eq!(got, &want, epsilon = 1e-7);
1446 }
1447 }
1448
1449 #[test]
1450 fn roots_of_a_high_degree_polynomial() {
1451 let r = roots(&[-120.0, 274.0, -225.0, 85.0, -15.0, 1.0], 1e-9).unwrap();
1453 assert_eq!(r.len(), 5);
1454 for (got, want) in r.iter().zip([1.0, 2.0, 3.0, 4.0, 5.0]) {
1455 assert_relative_eq!(got, &want, epsilon = 1e-6);
1456 }
1457 }
1458
1459 #[test]
1460 fn roots_degenerate_cases() {
1461 assert!(roots(&[], 1e-12).is_err());
1462 assert!(roots(&[0.0, 0.0], 1e-12).is_err());
1463 assert_eq!(
1464 roots(&[5.0], 1e-12).unwrap(),
1465 Vec::<f64>::new(),
1466 "a nonzero constant"
1467 );
1468 assert_eq!(roots(&[0.0, 1.0], 1e-12).unwrap(), vec![0.0]);
1469 }
1470
1471 #[test]
1472 fn minimize_finds_a_smooth_minimum() {
1473 let s = minimize(|x| (x - 0.3) * (x - 0.3) + 1.0, -2.0, 2.0, C).unwrap();
1474 assert_relative_eq!(s.value, 0.3, epsilon = 1e-7);
1475 assert_relative_eq!(s.residual, 1.0, epsilon = 1e-12);
1476 }
1477
1478 #[test]
1479 fn minimize_handles_a_flat_minimum() {
1480 let s = minimize(|x: f64| (x - 0.5).powi(4), -1.0, 2.0, C).unwrap();
1483 assert!((s.value - 0.5).abs() < 1e-3, "landed at {}", s.value);
1484 assert!(s.residual < 1e-12);
1485 }
1486
1487 #[test]
1488 fn minimize_refuses_a_malformed_bracket() {
1489 assert!(minimize(|x| x, 1.0, 0.0, C).is_err());
1490 assert!(minimize(|x| x, 0.0, f64::INFINITY, C).is_err());
1491 }
1492
1493 #[test]
1494 fn newton_system_solves_a_two_by_two() {
1495 let s = newton_system(
1497 |v| {
1498 let (x, y) = (v[0], v[1]);
1499 (
1500 vec![x.mul_add(x, y * y) - 25.0, x - y - 1.0],
1501 vec![vec![2.0 * x, 2.0 * y], vec![1.0, -1.0]],
1502 )
1503 },
1504 &[5.0, 1.0],
1505 C,
1506 )
1507 .unwrap();
1508 assert!(s.convergence.is_converged());
1509 assert_relative_eq!(s.value[0], 4.0, epsilon = 1e-10);
1510 assert_relative_eq!(s.value[1], 3.0, epsilon = 1e-10);
1511 }
1512
1513 #[test]
1514 fn newton_system_damping_survives_a_start_where_plain_newton_diverges() {
1515 let s = newton_system(
1521 |v| {
1522 let (x, y) = (v[0], v[1]);
1523 (
1524 vec![x.atan(), y.atan()],
1525 vec![
1526 vec![x.mul_add(x, 1.0).recip(), 0.0],
1527 vec![0.0, y.mul_add(y, 1.0).recip()],
1528 ],
1529 )
1530 },
1531 &[5.0, 5.0],
1532 C,
1533 )
1534 .unwrap();
1535 assert!(s.convergence.is_converged(), "{s:?}");
1536 assert!(s.value[0].abs() < 1e-9 && s.value[1].abs() < 1e-9, "{s:?}");
1537 }
1538
1539 #[test]
1540 fn newton_system_reports_a_residual_minimum_rather_than_looping() {
1541 let s = newton_system(
1545 |v| {
1546 let (x, y) = (v[0], v[1]);
1547 (
1548 vec![x.mul_add(x, 1.0), y],
1549 vec![vec![2.0 * x, 0.0], vec![0.0, 1.0]],
1550 )
1551 },
1552 &[2.0, 2.0],
1553 C,
1554 )
1555 .unwrap();
1556 assert!(!s.convergence.is_converged());
1557 assert!(s.residual >= 1.0, "the residual cannot go below 1 here");
1558 }
1559
1560 #[test]
1561 fn newton_system_reports_a_singular_jacobian_rather_than_looping() {
1562 let s = newton_system(
1563 |v| {
1564 (
1565 vec![v[0] * v[0], v[1]],
1566 vec![vec![2.0 * v[0], 0.0], vec![0.0, 0.0]],
1567 )
1568 },
1569 &[1.0, 1.0],
1570 C,
1571 );
1572 assert!(s.is_err());
1573 }
1574
1575 #[test]
1576 fn newton_system_checks_its_shapes() {
1577 let s = newton_system(|_| (vec![1.0], vec![vec![1.0, 2.0]]), &[0.0, 0.0], C);
1578 assert!(s.is_err());
1579 }
1580
1581 #[test]
1582 fn exhausted_is_reported_not_hidden() {
1583 let s = brent(
1586 |x| x * x - 2.0,
1587 0.0,
1588 2.0,
1589 Criteria {
1590 max_iterations: 1,
1591 ..C
1592 },
1593 )
1594 .unwrap();
1595 assert_eq!(s.convergence, Convergence::Exhausted);
1596 assert!(!s.convergence.is_converged());
1597 }
1598
1599 #[test]
1603 fn the_fixed_solver_matches_the_general_one_to_the_bit() {
1604 fn check<const N: usize>(start: [f64; N]) {
1605 let system = |x: &[f64]| {
1606 let mut r = vec![0.0; N];
1607 let mut j = vec![vec![0.0; N]; N];
1608 for i in 0..N {
1609 let k = (i + 1) % N;
1610 #[allow(clippy::cast_precision_loss)]
1611 let weight = 1.0 + i as f64 * 0.37;
1612 r[i] = x[i].mul_add(x[k], -weight) + x[k].sin() * 0.3;
1613 j[i][i] += x[k];
1614 j[i][k] += x[i] + x[k].cos() * 0.3;
1615 }
1616 (r, j)
1617 };
1618 let criteria = Criteria {
1619 residual: 1e-14,
1620 step: 1e-15,
1621 max_iterations: 60,
1622 };
1623 let general = newton_system(system, &start, criteria).unwrap();
1624 let fixed = newton_system_fixed(
1625 |x: &[f64; N]| {
1626 let (r, j) = system(x);
1627 let mut rows = [[0.0; N]; N];
1628 for (to, from) in rows.iter_mut().zip(&j) {
1629 to.copy_from_slice(from);
1630 }
1631 (r.try_into().unwrap(), rows)
1632 },
1633 start,
1634 criteria,
1635 )
1636 .unwrap();
1637 assert_eq!(general.value, fixed.0.to_vec());
1638 assert_eq!(general.residual.to_bits(), fixed.1.to_bits());
1639 assert_eq!(general.convergence, fixed.2);
1640 assert_eq!(general.iterations, fixed.3);
1641 }
1642 check([1.0, 1.0, 1.0]);
1643 check([0.5, 2.0, -1.0, 1.5]);
1644 check([1.0, -1.0, 1.0, -1.0, 2.0]);
1645 check([3.0, 0.2, 0.7, 1.1, 0.4]);
1646 }
1647
1648 mod bracketing {
1651 use super::*;
1652 use proptest::prelude::*;
1653
1654 fn companion(c: &[f64]) -> Vec<f64> {
1658 let n = c.len() - 1;
1659 let lead = c[n];
1660 let mut m = DMatrix::<f64>::zeros(n, n);
1661 for i in 0..n {
1662 m[(i, n - 1)] = -c[i] / lead;
1663 if i + 1 < n {
1664 m[(i + 1, i)] = 1.0;
1665 }
1666 }
1667 let eigenvalues: Vec<nalgebra::Complex<f64>> =
1668 match nalgebra::linalg::Schur::try_new(m, f64::EPSILON, 1000) {
1669 Some(schur) => schur.complex_eigenvalues().iter().copied().collect(),
1670 None => durand_kerner(c),
1671 };
1672 let mut out: Vec<f64> = eigenvalues
1673 .iter()
1674 .filter_map(|e| {
1675 let scale = e.re.abs().max(1.0);
1676 if e.im.abs() <= 1e-9 * scale {
1677 return Some(e.re);
1678 }
1679 if e.im.abs() > 1e-7 * scale {
1680 return None;
1681 }
1682 let (p, _, size) = value_and_slope(c, e.re);
1683 (p.abs() <= 1e-10 * size).then_some(e.re)
1684 })
1685 .collect();
1686 out.sort_by(f64::total_cmp);
1687 out
1688 }
1689
1690 fn durand_kerner(c: &[f64]) -> Vec<nalgebra::Complex<f64>> {
1691 use nalgebra::Complex;
1692 let n = c.len() - 1;
1693 let monic: Vec<f64> = c.iter().map(|x| x / c[n]).collect();
1694 let radius = 1.0 + monic[..n].iter().fold(0.0_f64, |m, x| m.max(x.abs()));
1695 #[allow(clippy::cast_precision_loss)]
1696 let mut z: Vec<Complex<f64>> = (0..n)
1697 .map(|k| {
1698 Complex::from_polar(radius, 0.4 + core::f64::consts::TAU * k as f64 / n as f64)
1699 })
1700 .collect();
1701 let value = |x: Complex<f64>| {
1702 let mut p = Complex::new(1.0, 0.0);
1703 for &coefficient in monic[..n].iter().rev() {
1704 p = p * x + coefficient;
1705 }
1706 p
1707 };
1708 for _ in 0..500 {
1709 let mut largest = 0.0_f64;
1710 for i in 0..n {
1711 let mut denominator = Complex::new(1.0, 0.0);
1712 for j in 0..n {
1713 if i != j {
1714 denominator *= z[i] - z[j];
1715 }
1716 }
1717 if denominator.norm() == 0.0 {
1718 continue;
1719 }
1720 let step = value(z[i]) / denominator;
1721 z[i] -= step;
1722 largest = largest.max(step.norm() / z[i].norm().max(1.0));
1723 }
1724 if largest <= f64::EPSILON * 4.0 {
1725 break;
1726 }
1727 }
1728 z
1729 }
1730
1731 fn from_roots(lead: f64, roots: &[f64]) -> Vec<f64> {
1733 let mut c = vec![lead];
1734 for &r in roots {
1735 let mut next = vec![0.0; c.len() + 1];
1736 for (k, a) in c.iter().enumerate() {
1737 next[k + 1] += a;
1738 next[k] -= a * r;
1739 }
1740 c = next;
1741 }
1742 c
1743 }
1744
1745 fn with_pair(c: &[f64], re: f64, im: f64) -> Vec<f64> {
1748 let quadratic = [re.mul_add(re, im * im), -2.0 * re, 1.0];
1749 let mut out = vec![0.0; c.len() + 2];
1750 for (i, a) in c.iter().enumerate() {
1751 for (j, b) in quadratic.iter().enumerate() {
1752 out[i + j] += a * b;
1753 }
1754 }
1755 out
1756 }
1757
1758 fn on_torus(c: &[f64; 5]) -> Vec<f64> {
1761 solve(c, 1e-9 * c.iter().map(|x| x.abs()).sum::<f64>())
1762 }
1763
1764 fn solve(c: &[f64], touch: f64) -> Vec<f64> {
1765 let mut out = vec![0.0; c.len()];
1766 let n = real_roots(c, touch, &mut out);
1767 out.truncate(n);
1768 out
1769 }
1770
1771 fn matches(found: &[f64], expected: &[f64], relative: f64, scale: f64) -> bool {
1774 found.len() == expected.len()
1775 && found
1776 .iter()
1777 .zip(expected)
1778 .all(|(f, e)| (f - e).abs() <= relative * e.abs().max(scale))
1779 }
1780
1781 fn sorted(mut v: Vec<f64>) -> Vec<f64> {
1782 v.sort_by(f64::total_cmp);
1783 v
1784 }
1785
1786 fn torus_quartic(m: [f64; 3], d: [f64; 3], big: f64) -> [f64; 5] {
1790 let small = 1.0 - big;
1791 let dot = |u: [f64; 3], v: [f64; 3]| u[0] * v[0] + u[1] * v[1] + u[2] * v[2];
1792 let a = dot(d, d);
1793 let b = 2.0 * dot(m, d);
1794 let c = dot(m, m) + big * big - small * small;
1795 let p = d[0] * d[0] + d[1] * d[1];
1796 let q = 2.0 * (m[0] * d[0] + m[1] * d[1]);
1797 let s = m[0] * m[0] + m[1] * m[1];
1798 let four = 4.0 * big * big;
1799 [
1800 c * c - four * s,
1801 2.0 * b * c - four * q,
1802 b * b + 2.0 * a * c - four * p,
1803 2.0 * a * b,
1804 a * a,
1805 ]
1806 }
1807
1808 #[test]
1809 fn simple_and_repeated_roots_come_back_once_each() {
1810 let c = from_roots(1.0, &[-3.0, -0.5, 1.0, 7.0]);
1812 assert!(matches(
1813 &solve(&c, 0.0),
1814 &[-3.0, -0.5, 1.0, 7.0],
1815 1e-12,
1816 1.0
1817 ));
1818 for (roots, distinct) in [
1821 ([-1.0, 2.0, 2.0, 5.0], [-1.0, 2.0, 5.0]),
1822 ([-4.0, -4.0, 0.5, 3.0], [-4.0, 0.5, 3.0]),
1823 ([-2.0, 1.5, 6.0, 6.0], [-2.0, 1.5, 6.0]),
1824 ] {
1825 let c = from_roots(2.0, &roots);
1826 let found = solve(&c, 0.0);
1827 assert!(
1828 matches(&found, &distinct, 1e-7, 1.0),
1829 "{roots:?}: {found:?}"
1830 );
1831 }
1832 let found = solve(&from_roots(1.0, &[-1.0, -1.0, 3.0, 3.0]), 0.0);
1834 assert!(matches(&found, &[-1.0, 3.0], 1e-7, 1.0), "{found:?}");
1835 let found = solve(&from_roots(1.0, &[0.5, 2.0, 2.0, 2.0]), 0.0);
1836 assert!(matches(&found, &[0.5, 2.0], 1e-5, 1.0), "{found:?}");
1837 let found = solve(&from_roots(1.0, &[1.25; 4]), 0.0);
1838 assert!(matches(&found, &[1.25], 1e-3, 1.0), "{found:?}");
1839 }
1840
1841 #[test]
1842 fn a_root_at_zero_or_at_a_critical_point_is_found_once() {
1843 let found = solve(&from_roots(1.0, &[0.0, 1.0, -2.0, 3.0]), 0.0);
1845 assert!(
1846 matches(&found, &[-2.0, 0.0, 1.0, 3.0], 1e-12, 1.0),
1847 "{found:?}"
1848 );
1849 let found = solve(&[0.0, 0.0, -2.0, 0.0, 1.0], 0.0);
1852 let r = 2.0_f64.sqrt();
1853 assert!(matches(&found, &[-r, 0.0, r], 1e-12, 1.0), "{found:?}");
1854 assert_eq!(solve(&[0.0, 0.0, 0.0, 0.0, 1.0], 0.0), vec![0.0]);
1856 let found = solve(&[-1.0, 0.0, 0.0, 0.0, 1.0], 0.0);
1857 assert!(matches(&found, &[-1.0, 1.0], 1e-14, 1.0), "{found:?}");
1858 }
1859
1860 #[test]
1861 fn degenerate_input_has_no_roots_and_high_degree_works() {
1862 assert_eq!(real_roots(&[0.0; 5], 0.0, &mut [0.0; 4]), 0);
1863 assert_eq!(real_roots(&[1.0, f64::NAN, 1.0], 0.0, &mut [0.0; 2]), 0);
1864 assert_eq!(real_roots(&[3.0, 0.0, 0.0], 0.0, &mut []), 0);
1865 let found = solve(&[-4.0, 0.0, 1.0, 0.0, 0.0], 0.0);
1867 assert!(matches(&found, &[-2.0, 2.0], 1e-14, 1.0), "{found:?}");
1868 let roots: Vec<f64> = (0..20).map(|k| f64::from(k) * 0.25 - 2.0).collect();
1870 let found = solve(&from_roots(1.0, &roots), 0.0);
1871 assert!(matches(&found, &roots, 1e-6, 1.0), "{found:?}");
1872 }
1873
1874 #[test]
1875 fn a_near_tangency_is_a_touch_only_within_the_threshold() {
1876 for (e, touch, touches) in [
1880 (1e-14, 0.0, true),
1881 (1e-11, 0.0, false),
1882 (1e-11, 1e-9, true),
1883 (1e-8, 1e-9, false),
1884 ] {
1885 let c = [1.0 + e, -2.0, 1.0];
1886 let c = with_pair(&c, -3.0, 1.0);
1887 let found = solve(&c, touch);
1888 if touches {
1889 assert!(matches(&found, &[1.0], 1e-9, 1.0), "{e}: {found:?}");
1890 } else {
1891 assert!(found.is_empty(), "{e}: {found:?}");
1892 }
1893 }
1894 for (e, both) in [(1e-8, true), (1e-11, true), (1e-14, false)] {
1898 let c = with_pair(&[1.0 - e, -2.0, 1.0], -3.0, 1.0);
1899 let found = solve(&c, 0.0);
1900 let (low, high) = (1.0 - e.sqrt(), 1.0 + e.sqrt());
1901 if both {
1902 assert!(matches(&found, &[low, high], 1e-8, 1.0), "{e}: {found:?}");
1903 } else {
1904 assert!(matches(&found, &[1.0], 1e-6, 1.0), "{e}: {found:?}");
1905 }
1906 }
1907 }
1908
1909 fn real_root() -> impl Strategy<Value = f64> {
1910 -1.0..1.0f64
1911 }
1912
1913 proptest! {
1914 #[test]
1918 fn known_simple_roots_are_found_at_any_scale(
1919 first in real_root(),
1920 gaps in proptest::collection::vec(0.05..0.7f64, 1..=3),
1921 exponent in -6i32..=6,
1922 lead in prop_oneof![-1e3..-1e-3f64, 1e-3..1e3f64],
1923 pair in proptest::option::of((real_root(), 0.1..1.0f64)),
1924 ) {
1925 let mut roots = vec![first];
1926 for gap in gaps {
1927 roots.push(roots[roots.len() - 1] + gap);
1928 }
1929 let scale = 10f64.powi(exponent);
1930 let roots: Vec<f64> = roots.iter().map(|r| r * scale).collect();
1931 let mut c = from_roots(lead, &roots);
1932 if roots.len() <= 2 && let Some((re, im)) = pair {
1933 c = with_pair(&c, re * scale, im * scale);
1934 }
1935 let found = solve(&c, 0.0);
1936 prop_assert!(matches(&found, &roots, 1e-9, scale), "{roots:?}: {found:?}");
1937 }
1938
1939 #[test]
1942 fn double_and_near_double_roots(
1943 double in real_root(),
1944 apart in (0.1..1.0f64, 0.1..1.0f64),
1945 sides in (any::<bool>(), any::<bool>()),
1946 split in prop_oneof![Just(0.0), 1e-4..1e-2f64],
1947 exponent in -4i32..=4,
1948 ) {
1949 let scale = 10f64.powi(exponent);
1950 let side = |left: bool, d: f64| if left { double - d } else { double + d };
1952 let second = if sides.0 == sides.1 { apart.0 + apart.1 } else { apart.1 };
1953 let others = [side(sides.0, apart.0), side(sides.1, second)];
1954 let pair = [double - split, double + split];
1955 let all = [pair[0], pair[1], others[0], others[1]].map(|r| r * scale);
1956 let c = from_roots(1.0, &all);
1957 let found = solve(&c, 0.0);
1958 let expected = if split == 0.0 {
1959 sorted(vec![double, others[0], others[1]])
1960 } else {
1961 sorted(vec![pair[0], pair[1], others[0], others[1]])
1962 };
1963 let expected: Vec<f64> = expected.iter().map(|r| r * scale).collect();
1964 let precision = if split == 0.0 { 1e-6 } else { 1e-7 / split };
1965 prop_assert!(
1966 matches(&found, &expected, precision, scale),
1967 "{expected:?}: {found:?}"
1968 );
1969 }
1970
1971 #[test]
1974 fn agrees_with_the_companion_matrix(
1975 mut c in proptest::collection::vec(-10.0..10.0f64, 4),
1976 lead in prop_oneof![-10.0..-0.1f64, 0.1..10.0f64],
1977 ) {
1978 c.push(lead);
1979 let new = solve(&c, 1e-10);
1980 let old = companion(&c);
1981 let clean = |r: &f64| {
1982 let (_, slope, size) = value_and_slope(&c, *r);
1983 slope.abs() * (1.0 + r.abs()) > 1e-3 * size
1984 };
1985 let near = |set: &[f64], r: f64| {
1986 set.iter().any(|s| (s - r).abs() <= 1e-8 * r.abs().max(1.0))
1987 };
1988 for r in old.iter().filter(|r| clean(r)) {
1989 prop_assert!(near(&new, *r), "{r} of {old:?} missing from {new:?}");
1990 }
1991 for r in new.iter().filter(|r| clean(r)) {
1992 prop_assert!(near(&old, *r), "{r} of {new:?} not in {old:?}");
1993 }
1994 }
1995
1996 #[test]
2000 fn torus_quartics_agree_with_the_companion_matrix(
2001 offset in (-1.5..1.5f64, -1.5..1.5f64, -0.5..0.5f64),
2002 direction in (-1.0..1.0f64, -1.0..1.0f64, -1.0..1.0f64),
2003 big in 0.55..0.95f64,
2004 ) {
2005 let d = [direction.0, direction.1, direction.2];
2006 let length = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
2007 prop_assume!(length > 0.1);
2008 let d = d.map(|x| x / length);
2009 let m = [offset.0, offset.1, offset.2];
2010 let along = m[0] * d[0] + m[1] * d[1] + m[2] * d[2];
2011 let m = [0, 1, 2].map(|k| m[k] - along * d[k]);
2012 let c = torus_quartic(m, d, big);
2013 let new = on_torus(&c);
2014 let old = companion(&c);
2015 for r in &new {
2016 let (p, _, _) = value_and_slope(&c, *r);
2017 let size = c.iter().map(|x| x.abs()).sum::<f64>() * r.abs().max(1.0).powi(4);
2018 prop_assert!(p.abs() <= 1e-9 * size, "{r}: {p} of {size}");
2019 }
2020 prop_assert!(new.windows(2).all(|w| w[1] > w[0]), "{new:?}");
2021 for r in &old {
2022 prop_assert!(
2023 new.iter().any(|s| (s - r).abs() <= 1e-6),
2024 "{r} of {old:?} missing from {new:?}"
2025 );
2026 }
2027 }
2028 }
2029
2030 #[test]
2033 fn line_torus_quartics() {
2034 let big = 0.75;
2035 let small = 0.25;
2036 let found = on_torus(&torus_quartic([0.0; 3], [1.0, 0.0, 0.0], big));
2038 assert!(
2039 matches(&found, &[-1.0, -0.5, 0.5, 1.0], 1e-12, 1.0),
2040 "{found:?}"
2041 );
2042 assert!(on_torus(&torus_quartic([0.0; 3], [0.0, 0.0, 1.0], big)).is_empty());
2044 let found = on_torus(&torus_quartic([0.1, 0.05, 0.0], [0.1, 0.0, 0.995], big));
2045 assert!(found.is_empty(), "{found:?}");
2046 let found = on_torus(&torus_quartic([0.0, 0.0, small], [1.0, 0.0, 0.0], big));
2048 assert!(matches(&found, &[-big, big], 1e-7, 1.0), "{found:?}");
2049 let found = on_torus(&torus_quartic([0.0, 1.0, 0.0], [1.0, 0.0, 0.0], big));
2051 assert!(matches(&found, &[0.0], 1e-7, 1.0), "{found:?}");
2052 let found = on_torus(&torus_quartic([0.0, 0.5, 0.0], [1.0, 0.0, 0.0], big));
2054 let outer = 0.75_f64.sqrt();
2055 assert!(
2056 matches(&found, &[-outer, 0.0, outer], 1e-7, 1.0),
2057 "{found:?}"
2058 );
2059 }
2060 }
2061}