Skip to main content

ogeom_math/
solve.rs

1//! Root finding, polynomial roots and minimization.
2//!
3//! The numerical substrate every intersection, projection and extrema algorithm
4//! in the kernel sits on. Nothing here is geometric. It is kept separate so
5//! those algorithms are about geometry rather than about convergence.
6//!
7//! # What to reach for
8//!
9//! - A root inside a known bracket: [`brent`]. Guaranteed to converge, and
10//!   nearly as fast as Newton in practice.
11//! - A root with a known derivative and a good starting point: [`newton`],
12//!   which falls back to bisection whenever a step would leave the bracket.
13//!   Unsafeguarded Newton diverges on the configurations that matter: a
14//!   tangential intersection is exactly where the derivative vanishes.
15//! - Roots of a polynomial: [`roots`]. Closed form up to the cubic, the
16//!   quadratic written to avoid the cancellation the schoolbook formula
17//!   suffers. Above that, or with no allocation, [`real_roots`]: each root
18//!   bracketed between its derivative's roots.
19//! - A system of equations: [`newton_system`]. Surface projection is two
20//!   equations in two unknowns. Intersection marching is much the same.
21//! - A minimum without derivatives: [`minimize`].
22
23use nalgebra::{DMatrix, DVector};
24use ogeom_core::{OgeomResult, ogeom_bail};
25
26/// How a solver finished.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum Convergence {
29    /// The residual fell below the requested tolerance.
30    Residual,
31    /// The step size fell below the requested tolerance.
32    Step,
33    /// The iteration limit was reached first. The result is the best estimate
34    /// found, and is *not* to be treated as a root.
35    Exhausted,
36}
37
38impl Convergence {
39    /// Whether the solver actually converged.
40    #[must_use]
41    pub const fn is_converged(self) -> bool {
42        !matches!(self, Self::Exhausted)
43    }
44}
45
46/// A solver result: the estimate, the residual there, and how it finished.
47#[derive(Debug, Clone, Copy, PartialEq)]
48pub struct Solution {
49    /// The parameter value.
50    pub value: f64,
51    /// The function's value there.
52    pub residual: f64,
53    /// How the iteration ended.
54    pub convergence: Convergence,
55    /// Iterations taken.
56    pub iterations: usize,
57}
58
59/// Stopping criteria for an iterative solver.
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct Criteria {
62    /// Stop when `|f(x)|` falls to this.
63    pub residual: f64,
64    /// Stop when the step falls to this.
65    pub step: f64,
66    /// Give up after this many iterations.
67    pub max_iterations: usize,
68}
69
70impl Default for Criteria {
71    fn default() -> Self {
72        Self {
73            // Tight enough for geometric work in f64 without chasing the last
74            // couple of bits, which costs iterations and buys nothing.
75            residual: 1e-13,
76            step: 1e-14,
77            max_iterations: 100,
78        }
79    }
80}
81
82impl Criteria {
83    /// Criteria with a given residual tolerance and the default step limit.
84    #[must_use]
85    pub fn with_residual(residual: f64) -> Self {
86        Self {
87            residual,
88            ..Self::default()
89        }
90    }
91}
92
93/// Find a root of `f` in `[a, b]` by Brent's method.
94///
95/// Combines bisection, the secant method and inverse quadratic interpolation,
96/// taking whichever step is both safe and fast. Guaranteed to converge for a
97/// continuous function that changes sign across the bracket, and superlinear in
98/// practice, the right default when a bracket is available.
99///
100/// # Errors
101///
102/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the bracket is
103/// malformed, or if `f` does not change sign across it, which means there is no
104/// root to find by this method.
105pub 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    // `b` is kept as the better estimate throughout.
138    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            // Inverse quadratic interpolation, when three distinct values allow
150            // it.
151            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        // Bisect instead whenever the interpolated step is outside the bracket
159        // or is not shrinking fast enough. This is what turns a fast but
160        // unreliable method into a guaranteed one.
161        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
221/// Find a root of `f` near `start`, using its derivative, safeguarded by a
222/// bracket.
223///
224/// Takes a Newton step when that lands inside `[a, b]` and reduces the residual,
225/// and bisects otherwise. Plain Newton is not usable here: it diverges wherever
226/// the derivative is small, and small derivatives are precisely the tangential
227/// configurations a geometry kernel spends its time on.
228///
229/// `f` returns the value and the derivative together, since evaluating them
230/// separately usually repeats most of the work.
231///
232/// # Errors
233///
234/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the bracket is
235/// malformed or `f` does not change sign across it.
236pub 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    // Orient so that f(low) < 0 < f(high). The bracket update is then a single
266    // comparison rather than a sign product.
267    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        // A step that has not at least halved is a sign Newton is not making
298        // progress here, so fall back rather than grind.
299        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
328/// The real roots of a polynomial, in increasing order.
329///
330/// `coefficients` are in ascending power order: `c[0] + c[1] x + c[2] x^2 ...`.
331/// Degrees up to three are solved in closed form. Above that each root is
332/// bracketed between the derivative's roots, as [`real_roots`] does.
333///
334/// Repeated roots are returned once each, since a geometry caller wants the
335/// distinct parameter values, and a double root (a tangency) is found
336/// although rounding leaves the polynomial a hair either side of zero there.
337/// The same slack merges two simple roots that stand close: where the
338/// polynomial's value between them is within `32 n ε` of its terms' size,
339/// they come back as one root between them. For a quartic with roots near
340/// one that is a pair a few times `1e-7` apart.
341///
342/// # Errors
343///
344/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if every
345/// coefficient is zero, where every value is a root.
346pub fn roots(coefficients: &[f64], tolerance: f64) -> OgeomResult<Vec<f64>> {
347    // Drop a leading term that is nothing where the roots lie, so the true
348    // degree drives the choice of method. A "cubic" whose cubic term is
349    // zero is a quadratic and must be solved as one, or the leading
350    // division blows up; one whose cubic term is rounding's leftover of
351    // zero is the same quadratic with a spurious root far away.
352    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/// Real roots of `a x^2 + b x + c`.
385///
386/// Uses the citardauq form for whichever root would otherwise be computed as a
387/// difference of nearly equal numbers. The schoolbook formula loses most of its
388/// precision for the smaller root when `b^2 >> 4ac`, which is the common case
389/// for a ray grazing a sphere.
390#[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    // A double root's discriminant is zero only up to the rounding of the
397    // coefficients, which can leave it a hair below.
398    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    // Add magnitudes rather than subtract them, then get the other root from
407    // the product relation.
408    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/// Real roots of `a x^3 + b x^2 + c x + d`.
414///
415/// Depressed cubic plus the trigonometric solution in the three-real-roots
416/// case, which avoids the complex arithmetic Cardano's formula would otherwise
417/// need there.
418#[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    // x = t - b/3 removes the quadratic term.
426    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    // What rounding leaves in `p`, `q` and so in the discriminant: a double
433    // root's is zero only to within it.
434    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            // t^3 + p t + q with a double root: 3q/p once, -3q/(2p) twice.
446            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        // Three distinct real roots, via trigonometry.
457        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
472/// Whether a leading coefficient `lead` is nothing beside the polynomial
473/// `rest` it tops, where that polynomial's roots lie.
474///
475/// Compared against the largest coefficient, a leading term is dropped
476/// whenever the roots are large: `(x - 1e6)^4` has a constant term of 1e24
477/// over a leading one of 1. So the comparison is made at the size of the
478/// roots instead: within twice Fujiwara's bound on `rest`'s roots, the
479/// leading term is at most `lead * bound^n`, and it is dropped where that
480/// is within `tolerance` of the largest term of `rest` there. A `rest`
481/// that is a constant has no roots to stand at, and its lead is kept: the
482/// roots of `x^2 - 1e10` are the leading term's.
483fn 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    // The largest term of `rest` at the bound, and the leading term there.
508    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
518/// Polynomials up to this degree are solved with their working on the stack.
519const STACK_DEGREE: usize = 16;
520
521/// The real roots of a polynomial, in increasing order, written to the front
522/// of `out`; returns how many there are.
523///
524/// `coefficients` are in ascending power order, as for [`roots`]; leading
525/// coefficients that are exactly zero are dropped. Nothing is allocated up
526/// to degree 16.
527///
528/// The derivative's real roots, found the same way down to a line, split
529/// the real line into intervals on which the polynomial is monotone. Each
530/// interval whose ends differ in sign holds one root, which safeguarded
531/// Newton finds with bisection to fall back on (Yuksel, "High-Performance
532/// Polynomial Root Finding for Graphics", HPG 2022). A root of even
533/// multiplicity, a tangency, has no sign change around it: it is a critical
534/// point where the polynomial's value is zero to rounding, or no larger
535/// than `touch` with no root on either side. `touch` is in the polynomial's
536/// own units, since only the caller knows what rounding its coefficients
537/// carry. Such a root comes back once. Two simple roots whose critical
538/// point between them has a value within that rounding (or within `touch`)
539/// come back as the one root there: for a quartic with roots near one, a
540/// pair a few times `1e-7` apart.
541///
542/// A polynomial whose every coefficient is zero, or one that is not finite,
543/// has no roots returned.
544///
545/// # Panics
546///
547/// If `out` has room for fewer values than the polynomial's degree.
548pub 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    // Fujiwara's bound on the roots' moduli, doubled so that no root stands
559    // near an end where rounding could flip the value's sign. By Gauss-Lucas
560    // every derivative's roots lie within it too.
561    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
581/// The roots of `c` (degree at least one, nonzero lead) within `bound`.
582fn 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
605/// The roots of `c`, from the roots of its derivative, with `slope` and
606/// `critical` as working space for the derivative and its roots.
607fn 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    // What Horner's rule and the coefficients' own rounding can leave in a
623    // value, relative to its terms' size, with room to spare: a value no
624    // larger is zero.
625    #[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    // Walk the monotone intervals left to right. A critical point within
636    // `touch` of zero is a root only once the intervals either side of it
637    // are known not to cross: if they do, its value is on the far side of
638    // zero and the crossings are the roots.
639    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
670/// A polynomial's value and slope at `x`, and the size of its terms there,
671/// `sum |c_k| |x|^k`, which bounds the rounding in the value.
672fn 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
683/// The one root of `c` in `[a, b]`, where `fa` and `fb` differ in sign:
684/// Newton from the secant's point, bisecting whenever a step would leave
685/// the bracket or does not halve the step before last. The bracket keeps
686/// the sign change, so the result is a root even where `c` is not monotone.
687fn 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        // A step below rounding has converged; so has a bracket with no
716        // float left inside it.
717        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
725/// Minimize a scalar function on `[a, b]` without derivatives.
726///
727/// Brent's method again: golden-section search with parabolic interpolation
728/// wherever the parabola is well behaved. Converges for any continuous function
729/// and is not fooled by the flat regions near a minimum, where a derivative
730/// method has nothing to work with.
731///
732/// # Errors
733///
734/// [`OgeomError::Construction`](ogeom_core::OgeomError::Construction) if the bracket is
735/// malformed.
736pub 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    // 1 - 1/phi: the golden-section step.
744    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            // Fit a parabola through the three best points so far.
769            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            // Accept the parabolic step only if it stays inside the bracket and
778            // is smaller than half the step before last.
779            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/// The outcome of solving a system of equations.
842#[derive(Debug, Clone, PartialEq)]
843pub struct SystemSolution {
844    /// The estimate.
845    pub value: Vec<f64>,
846    /// The norm of the residual vector there.
847    pub residual: f64,
848    /// How the iteration ended.
849    pub convergence: Convergence,
850    /// Iterations taken.
851    pub iterations: usize,
852}
853
854/// Solve `f(x) = 0` for a vector `x`, by damped Newton.
855///
856/// `f` returns the residual vector and the Jacobian, row-major. The step is
857/// halved until it actually reduces the residual. Undamped Newton overshoots
858/// badly from a poor start, and a geometry caller's start is often only a rough
859/// guess from a coarse sampling.
860///
861/// Surface projection is this with two equations in two unknowns, and so is a
862/// step of a surface/surface intersection march.
863///
864/// Where no root exists the residual has a positive minimum, and no damping
865/// finds a downhill step from it. That is reported as
866/// [`Convergence::Exhausted`] with the best estimate attached. "No root here"
867/// is a useful answer, and far better than iterating to the limit.
868///
869/// # Errors
870///
871/// [`OgeomError::Dimension`](ogeom_core::OgeomError::Dimension) if the Jacobian's shape
872/// disagrees with the residual, and
873/// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) if the Jacobian is singular
874/// at the starting point.
875pub 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        // Damping: keep halving until the residual actually falls. Without it,
913        // Newton happily steps past the solution and never comes back.
914        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            // No downhill step exists: this is a local minimum of the residual,
929            // not a root, and saying so is more useful than iterating forever.
930            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
969/// A fixed-size [`newton_system`] for `N` unknowns, allocation-free.
970///
971/// The same damped iteration (halving until the residual falls, the same
972/// three verdicts), on stack arrays and a fixed-size LU: the intersectors'
973/// three- and four-unknown systems run this millions of times per model,
974/// and the general path allocates for every residual, Jacobian and step.
975///
976/// # Errors
977///
978/// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) if the
979/// Jacobian is singular.
980pub 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    // The Jacobian of the point last measured: the lazy iteration asks for
989    // it only right after measuring the same point.
990    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
1003/// [`newton_system_fixed`] with the residual and the Jacobian measured
1004/// apart, the Jacobian only where a step is taken.
1005///
1006/// The damped search measures every trial point and keeps few, and where
1007/// the Jacobian costs more than the residual (a spline surface's
1008/// derivatives against its point) most of that goes on Jacobians never
1009/// used. The iteration is the one [`newton_system_fixed`] runs on
1010/// `|x| (residual(x), jacobian(x))`, a `None` Jacobian standing for a
1011/// point that cannot be measured: an infinite residual and a zero
1012/// Jacobian. So it answers the same to the bit.
1013///
1014/// # Errors
1015///
1016/// As [`newton_system_fixed`].
1017pub 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            // A point whose Jacobian cannot be measured is unmeasured, and
1050            // no step is accepted onto it.
1051            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/// `A x = b` for an `N × N` system, by LU with partial pivoting: the step
1083/// [`newton_system_fixed`] solves. `None` at a zero pivot.
1084#[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
1089/// `A x = b` by LU with partial pivoting, `None` at a zero pivot.
1090///
1091/// The arithmetic is the general solver's LU step for step: the first
1092/// largest magnitude pivots, the multipliers are the column times the
1093/// pivot's reciprocal, and the back substitution runs column by column from
1094/// the last. So a system moved from [`newton_system`] to
1095/// [`newton_system_fixed`] solves to the same bits.
1096fn 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
1130/// A two-unknown [`newton_system`], allocation-free.
1131///
1132/// The foot-point projection runs this system millions of times per real
1133/// model, and the general path pays a heap allocation for every residual,
1134/// Jacobian, vector and factorization of every damped step. The algorithm
1135/// here is the same (damped Newton, halving until the residual falls, the
1136/// same three convergence verdicts), with the two-by-two solve written out:
1137/// partial pivoting is one comparison, and singularity is a vanishing
1138/// pivot.
1139///
1140/// # Errors
1141///
1142/// [`OgeomError::Numeric`](ogeom_core::OgeomError::Numeric) if the Jacobian
1143/// is singular.
1144pub 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        // Solve J * delta = residual, partial pivoting on the first column.
1162        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        // Damping: keep halving until the residual actually falls.
1180        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    /// Every value in `expected` is among `found`, to the precision a double
1221    /// root keeps (half the digits).
1222    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    /// A quartic whose roots pair off by sign (a ray through a torus's
1234    /// middle) comes back, where the Schur iteration alone ran on for ever.
1235    #[test]
1236    fn a_quartic_with_roots_paired_by_sign_comes_back() {
1237        // (t^2 - 4)(t^2 - 16) = t^4 - 20 t^2 + 64.
1238        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        // And one with no real root at all.
1244        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            // (x - r)^2
1255            has_each(&quadratic_roots(1.0, -2.0 * r, r * r), &[r]);
1256            // (x - 1)(x - r)^2 = x^3 - (1 + 2r) x^2 + (2r + r^2) x - r^2
1257            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            // (x - 2)(x + 1)(x - r)^2, through the general solver.
1262            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        // Very flat near the root, then steep: pure secant crawls, bisection
1305        // alone is slow, and the hybrid must do better than either.
1306        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        // f(x) = x^3 has f'(0) = 0. Unsafeguarded Newton stalls. The bisection
1323        // fallback must carry it through.
1324        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        // x^2 - (1e8 + 1e-8) x + 1 has roots 1e8 and 1e-8. The schoolbook
1343        // formula computes the small one as a difference of nearly equal
1344        // numbers and loses almost all of it.
1345        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        // (x + 3)(x - 1)(x - 2) = x^3 - 7x + 6
1373        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        // x^3 + x + 1 has a single real root near -0.6823
1383        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        // (x - 2)^2 (x + 1) = x^3 - 3x^2 + 4
1391        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        // A triple root.
1396        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        // Presented as a cubic, but with a zero cubic term: solving it as one
1403        // would divide by zero.
1404        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        // (x - 1e6)^4: the constant term is 1e24 and the leading one is 1,
1413        // which is no reason to drop it. Dropped, the "cubic" that is left
1414        // has a root at a quarter of the true one.
1415        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        // (x - 1e6)^3 likewise, and a quadratic whose roots stand at 1e5.
1421        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        // A leading term that is rounding's leftover of zero is still
1431        // dropped: the quadratic's roots stand near one, and the cubic
1432        // term is nothing there.
1433        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        // (x-1)(x-2)(x-3)(x-4) = x^4 - 10x^3 + 35x^2 - 50x + 24
1442        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        // (x-1)(x-2)(x-3)(x-4)(x-5)
1452        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        // Quartic: the gradient vanishes to third order at the minimum, so a
1481        // derivative-based method has almost nothing to follow.
1482        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        // x^2 + y^2 = 25, x - y = 1  ->  (4, 3)
1496        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        // arctan is the classic case: an undamped Newton step from |x| > 1.4
1516        // overshoots to a *larger* residual, and each subsequent step is worse,
1517        // so the iteration runs away. From (5, 5) the first full step lands
1518        // near -30. Halving until the residual actually falls is what recovers
1519        // it.
1520        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        // No root exists: x^2 + 1 is never zero. The solver must stop at the
1542        // residual's minimum and say it did not converge, rather than iterate
1543        // to its limit or present the estimate as a solution.
1544        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        // One iteration cannot possibly converge. The result must say so rather
1584        // than present the first guess as an answer.
1585        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    /// The fixed-size solver lands on the general one's bits: the same
1600    /// iterates, verdict and count, on nonlinear systems of three, four and
1601    /// five unknowns whose Jacobians need pivoting and have ties for it.
1602    #[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    /// The bracketing solver against two oracles: roots built from known
1649    /// factors, and the companion matrix's real eigenvalues.
1650    mod bracketing {
1651        use super::*;
1652        use proptest::prelude::*;
1653
1654        /// The real eigenvalues of the companion matrix, with near-real
1655        /// pairs kept where the polynomial all but vanishes, and Durand-Kerner
1656        /// where the Schur iteration does not settle.
1657        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        /// The ascending coefficients of `lead * prod (x - r)`.
1732        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        /// `from_roots` times `x^2 - 2 re x + re^2 + im^2`, a pair with no
1746        /// real root.
1747        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        /// The roots of a line-torus quartic, touching within the
1759        /// intersection's threshold.
1760        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        /// `found` is `expected` one for one, in order, each within `relative`
1772        /// of the root's magnitude (and of `scale` near zero).
1773        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        /// The line-torus quartic, in the frame and units the intersection
1787        /// solves it in: offset `m` from the centre at the line's nearest
1788        /// approach, unit direction `d`, radii over their sum.
1789        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            // Four simple roots, both signs.
1811            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            // A double root between two simple ones, and at either end of
1819            // the real roots.
1820            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            // Two double roots, and a triple and a quadruple root.
1833            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            // x (x - 1)(x + 2)(x - 3): a root exactly at zero.
1844            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            // x^4 - 2 x^2 = x^2 (x^2 - 2): the double root at zero is a
1850            // critical point whose value is exactly zero.
1851            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            // x^4 alone, and x^4 - 1 with roots at the bound's scale.
1855            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            // Leading exact zeros drop the degree.
1866            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            // Degree twenty, past the stack's working space.
1869            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            // ((x - 1)^2 + e)((x + 3)^2 + 1): no real root, the minimum 17 e
1877            // above zero at one. Within rounding it is a root whatever the
1878            // threshold; past rounding, only within the threshold.
1879            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            // ((x - 1)^2 - e)((x + 3)^2 + 1): two real roots 2 sqrt(e) apart
1895            // where the dip between them, 17 e, stands clear of the rounding
1896            // slack; one root where it does not; never a third.
1897            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            /// Distinct real roots at any scale, with or without a complex
1915            /// pair beside them, are all found to within rounding of their
1916            /// conditioning.
1917            #[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            /// A double root among simple ones comes back once, to half the
1940            /// digits; a pair a little apart comes back as two.
1941            #[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                // One root either side, or both on one side a step apart.
1951                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            /// Any quartic: every root the companion matrix finds where the
1972            /// quartic crosses cleanly is found here, and nothing else.
1973            #[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            /// Line-torus quartics: every crossing the companion matrix finds
1997            /// is found here, every root found makes the quartic vanish, and a
1998            /// tangency comes back once.
1999            #[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        /// Lines against a torus: through the hole, along the axis, grazing
2031        /// the tube's top and the outer equator, and across the middle.
2032        #[test]
2033        fn line_torus_quartics() {
2034            let big = 0.75;
2035            let small = 0.25;
2036            // Across the middle in the torus's plane: four crossings.
2037            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            // Along the axis, and steeply through the hole: no crossing.
2043            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            // Along the tube's top: tangent twice.
2047            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            // Grazing the outer equator: one tangency.
2050            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            // Grazing the inner equator: one tangency between two crossings.
2053            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}