Skip to main content

arris_math/
roots.rs

1//! Real roots of polynomials of degree at most four, with multiplicity,
2//! and a Newton iteration that never leaves its bracket.
3//!
4//! The method is real-root isolation by the derivative: the real roots of
5//! `p'` split the line into intervals on which `p` is monotone, so each
6//! holds at most one root, found by bracketed Newton when the signs at
7//! its ends differ; a critical point where `p` vanishes to rounding is a
8//! multiple root, of multiplicity one more than its multiplicity in `p'`.
9//! No discriminant is compared against zero, no complex arithmetic is
10//! rounded back to the real line, and a double root is located as the
11//! *simple* root of `p'` it is — to full precision, where a closed form
12//! would lose half the digits.
13
14use core::fmt;
15use core::ops::Deref;
16
17use crate::Interval;
18
19/// Rounding slack for deciding that a polynomial *vanishes* at a point:
20/// `|p(x)| ≤ POLYNOMIAL_ROUNDING · Σ|aₖ||x|ᵏ` is zero in floating point.
21/// Horner's evaluation error is under `8ε` times that sum for degree four,
22/// the coefficients a caller expanded from factors carry about as much
23/// again, amplified by up to `2⁴` where terms of opposite sign cancelled,
24/// and the rest is slack. Like [`crate::RELATIVE_ROUNDING`] it is a statement
25/// about `f64`, never a geometric tolerance: two roots closer than the
26/// rounding of their polynomial allows are one root of multiplicity two,
27/// because nothing in `f64` can tell them apart.
28pub const POLYNOMIAL_ROUNDING: f64 = 256.0 * f64::EPSILON;
29
30/// The most iterations a bracketed Newton runs before returning its
31/// current bracket: bisection alone halves a bracket of `2^1024` width to
32/// an ulp in fewer, and Newton inside a bracket only narrows faster.
33const MAX_ITERATIONS: usize = 1100;
34
35/// A real root and how many times it is one.
36#[derive(Debug, Clone, Copy, PartialEq)]
37pub struct Root {
38    /// Where.
39    pub value: f64,
40    /// How many times: `1` for a simple root, `2` where `p` and `p'`
41    /// vanish together, and so on.
42    pub multiplicity: usize,
43}
44
45/// The real roots of a polynomial of degree at most four, ascending by
46/// value, each once with its multiplicity. Derefs to a slice.
47///
48/// ```
49/// use arris_math::roots::quadratic;
50///
51/// let r = quadratic(1.0, -3.0, 2.0).unwrap(); // (x − 1)(x − 2)
52/// assert_eq!(r.len(), 2);
53/// assert!((r[0].value - 1.0).abs() < 1e-15 && (r[1].value - 2.0).abs() < 1e-15);
54/// let double = quadratic(1.0, -2.0, 1.0).unwrap(); // (x − 1)²
55/// assert_eq!(double.len(), 1);
56/// assert_eq!(double[0].multiplicity, 2);
57/// assert!(quadratic(1.0, 0.0, 1.0).unwrap().is_empty()); // x² + 1
58/// ```
59#[derive(Debug, Clone, Copy, PartialEq)]
60pub struct Roots {
61    len: usize,
62    items: [Root; 4],
63}
64
65impl Roots {
66    const EMPTY: Roots = Roots {
67        len: 0,
68        items: [Root {
69            value: 0.0,
70            multiplicity: 0,
71        }; 4],
72    };
73
74    /// Appends a root. Total multiplicity never exceeds the degree, so
75    /// the array never fills; a fifth root would be a bug and is dropped
76    /// rather than panicked on.
77    fn push(&mut self, root: Root) {
78        if self.len < self.items.len() {
79            self.items[self.len] = root;
80            self.len += 1;
81        }
82    }
83
84    /// The roots as a slice.
85    pub fn as_slice(&self) -> &[Root] {
86        &self.items[..self.len]
87    }
88
89    /// The sum of the multiplicities: how many roots there are counted
90    /// with multiplicity.
91    pub fn total_multiplicity(&self) -> usize {
92        self.as_slice().iter().map(|r| r.multiplicity).sum()
93    }
94}
95
96impl Default for Roots {
97    fn default() -> Self {
98        Roots::EMPTY
99    }
100}
101
102impl Deref for Roots {
103    type Target = [Root];
104
105    fn deref(&self) -> &[Root] {
106        self.as_slice()
107    }
108}
109
110/// Why a root query has no answer.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub enum RootError {
113    /// A coefficient, a bracket end, a tolerance or a function value is
114    /// not finite (a negative tolerance counts), or the coefficients are
115    /// so far apart that the root bound overflows `f64`.
116    NonFinite,
117    /// Every coefficient is zero: every `x` is a root.
118    Zero,
119    /// The function has the same sign at both ends of the bracket, so the
120    /// bracket is no bracket.
121    NoSignChange,
122}
123
124impl fmt::Display for RootError {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        f.write_str(match self {
127            RootError::NonFinite => "root finding on a non-finite input",
128            RootError::Zero => "every coefficient is zero: every point is a root",
129            RootError::NoSignChange => "the function has the same sign at both bracket ends",
130        })
131    }
132}
133
134impl std::error::Error for RootError {}
135
136/// The real roots of `a x² + b x + c`, ascending, with multiplicity. A
137/// zero `a` lowers the degree; a double root is reported once with
138/// multiplicity two.
139///
140/// Errors: [`RootError::NonFinite`] for a non-finite coefficient,
141/// [`RootError::Zero`] when all three are zero.
142pub fn quadratic(a: f64, b: f64, c: f64) -> Result<Roots, RootError> {
143    real_roots(&[c, b, a])
144}
145
146/// The real roots of `a x³ + b x² + c x + d`, ascending, with
147/// multiplicity. A zero `a` lowers the degree. Errors as [`quadratic`].
148///
149/// ```
150/// use arris_math::roots::cubic;
151///
152/// let r = cubic(1.0, -6.0, 11.0, -6.0).unwrap(); // (x − 1)(x − 2)(x − 3)
153/// let values: Vec<f64> = r.iter().map(|r| r.value).collect();
154/// assert!(values.iter().zip([1.0, 2.0, 3.0]).all(|(a, b)| (a - b).abs() < 1e-14));
155/// ```
156pub fn cubic(a: f64, b: f64, c: f64, d: f64) -> Result<Roots, RootError> {
157    real_roots(&[d, c, b, a])
158}
159
160/// The real roots of `a x⁴ + b x³ + c x² + d x + e`, ascending, with
161/// multiplicity. A zero `a` lowers the degree. Errors as [`quadratic`].
162///
163/// ```
164/// use arris_math::roots::quartic;
165///
166/// // (x² − 1)(x² + 1): two real roots, one complex pair.
167/// let r = quartic(1.0, 0.0, 0.0, 0.0, -1.0).unwrap();
168/// assert_eq!(r.len(), 2);
169/// assert!((r[0].value + 1.0).abs() < 1e-15 && (r[1].value - 1.0).abs() < 1e-15);
170/// ```
171pub fn quartic(a: f64, b: f64, c: f64, d: f64, e: f64) -> Result<Roots, RootError> {
172    real_roots(&[e, d, c, b, a])
173}
174
175/// A root of `f` inside `bracket`, by Newton steps that fall back to
176/// bisection whenever a step would leave the part of the bracket that
177/// still holds the sign change; the result never leaves the bracket, and
178/// the iteration converges for any continuous `f` that changes sign.
179/// Stops when the bracket is narrower than `tol` — or, for a zero `tol`,
180/// than rounding — or when `f` is exactly zero. `df` is the derivative;
181/// where it is zero or the step misbehaves, the step is a bisection.
182///
183/// Errors: [`RootError::NoSignChange`] when `f` has the same sign at both
184/// ends (an end where `f` is exactly zero is returned as the root),
185/// [`RootError::NonFinite`] for a non-finite end, tolerance or value.
186///
187/// ```
188/// use arris_math::Interval;
189/// use arris_math::roots::newton_in_interval;
190///
191/// let f = |x: f64| x * x * x - 2.0;
192/// let df = |x: f64| 3.0 * x * x;
193/// let root = newton_in_interval(f, df, Interval::new(0.0, 4.0).unwrap(), 0.0).unwrap();
194/// assert!((root - 2f64.cbrt()).abs() < 1e-15);
195/// ```
196pub fn newton_in_interval(
197    f: impl Fn(f64) -> f64,
198    df: impl Fn(f64) -> f64,
199    bracket: Interval,
200    tol: f64,
201) -> Result<f64, RootError> {
202    if !(bracket.is_bounded() && tol.is_finite() && tol >= 0.0) {
203        return Err(RootError::NonFinite);
204    }
205    let (lo, hi) = (bracket.lo(), bracket.hi());
206    let (f_lo, f_hi) = (f(lo), f(hi));
207    if !(f_lo.is_finite() && f_hi.is_finite()) {
208        return Err(RootError::NonFinite);
209    }
210    if f_lo == 0.0 {
211        return Ok(lo);
212    }
213    if f_hi == 0.0 {
214        return Ok(hi);
215    }
216    if (f_lo < 0.0) == (f_hi < 0.0) {
217        return Err(RootError::NoSignChange);
218    }
219    bracketed_newton(&f, &df, lo, hi, f_lo < 0.0, tol)
220}
221
222/// Newton with a bisection guard on `[lo, hi]`, where `f(lo)` is negative
223/// iff `lo_negative` and `f(hi)` has the other sign.
224fn bracketed_newton(
225    f: &dyn Fn(f64) -> f64,
226    df: &dyn Fn(f64) -> f64,
227    mut lo: f64,
228    mut hi: f64,
229    lo_negative: bool,
230    tol: f64,
231) -> Result<f64, RootError> {
232    let mut x = 0.5 * (lo + hi);
233    for _ in 0..MAX_ITERATIONS {
234        let fx = f(x);
235        if !fx.is_finite() {
236            return Err(RootError::NonFinite);
237        }
238        if fx == 0.0 {
239            return Ok(x);
240        }
241        if (fx < 0.0) == lo_negative {
242            lo = x;
243        } else {
244            hi = x;
245        }
246        // A zero `tol` runs to the ulp: a bracket one rounding step wide
247        // holds nothing Newton could still improve.
248        let width = hi - lo;
249        if width <= tol.max(f64::EPSILON * lo.abs().max(hi.abs())) {
250            return Ok(x);
251        }
252        let d = df(x);
253        let newton = x - fx / d;
254        let next = if d != 0.0 && newton > lo && newton < hi {
255            newton
256        } else {
257            0.5 * (lo + hi)
258        };
259        if next == x {
260            return Ok(x);
261        }
262        x = next;
263    }
264    Ok(x)
265}
266
267/// Horner's evaluation of `c[0] + c[1] x + …`.
268fn eval(c: &[f64], x: f64) -> f64 {
269    c.iter().rev().fold(0.0, |acc, &k| acc * x + k)
270}
271
272/// `Σ |cₖ| |x|ᵏ`: the magnitude against which `p(x)` is zero to rounding.
273fn eval_abs(c: &[f64], x: f64) -> f64 {
274    let x = x.abs();
275    c.iter().rev().fold(0.0, |acc, &k| acc * x + k.abs())
276}
277
278/// `1 + max |cₖ / cₙ|`: every real root has a smaller magnitude, and the
279/// leading term decides the sign of `p` beyond it.
280fn cauchy_bound(c: &[f64]) -> Result<f64, RootError> {
281    let lead = c[c.len() - 1];
282    let ratio = c[..c.len() - 1]
283        .iter()
284        .map(|k| (k / lead).abs())
285        .fold(0.0, f64::max);
286    let bound = 1.0 + ratio;
287    if bound.is_finite() && eval_abs(c, bound).is_finite() {
288        Ok(bound)
289    } else {
290        Err(RootError::NonFinite)
291    }
292}
293
294/// The real roots of the polynomial with ascending coefficients `c`, of
295/// degree at most four; leading zeros lower the degree.
296fn real_roots(c: &[f64]) -> Result<Roots, RootError> {
297    if c.iter().any(|k| !k.is_finite()) {
298        return Err(RootError::NonFinite);
299    }
300    let degree = match c.iter().rposition(|&k| k != 0.0) {
301        None => return Err(RootError::Zero),
302        Some(0) => return Ok(Roots::EMPTY),
303        Some(n) => n,
304    };
305    let c = &c[..=degree];
306    let mut out = Roots::EMPTY;
307    if degree == 1 {
308        out.push(Root {
309            value: -c[0] / c[1],
310            multiplicity: 1,
311        });
312        return Ok(out);
313    }
314    let mut derivative = [0.0; 4];
315    for (k, d) in derivative.iter_mut().enumerate().take(degree) {
316        *d = c[k + 1] * (k + 1) as f64;
317    }
318    let critical = real_roots(&derivative[..degree])?;
319    let bound = cauchy_bound(c)?;
320    let lead_negative = c[degree] < 0.0;
321    // The sign of p at −bound is the leading term's, flipped for odd degree.
322    let mut lo = -bound;
323    let mut lo_negative = lead_negative != (degree % 2 == 1);
324    // After a multiple root the neighbouring monotone interval holds only
325    // that root's rounding twin, so it is not searched.
326    let mut skip = false;
327    let f = |x| eval(c, x);
328    let df = |x| eval(&derivative[..degree], x);
329    for cp in critical.iter() {
330        let x = cp.value;
331        if x <= -bound || x >= bound {
332            continue;
333        }
334        let fx = f(x);
335        if fx.abs() <= POLYNOMIAL_ROUNDING * eval_abs(c, x) {
336            out.push(Root {
337                value: x,
338                multiplicity: cp.multiplicity + 1,
339            });
340            lo = x;
341            skip = true;
342            continue;
343        }
344        let negative = fx < 0.0;
345        if !skip && negative != lo_negative {
346            out.push(Root {
347                value: bracketed_newton(&f, &df, lo, x, lo_negative, 0.0)?,
348                multiplicity: 1,
349            });
350        }
351        lo = x;
352        lo_negative = negative;
353        skip = false;
354    }
355    if !skip && lead_negative != lo_negative {
356        out.push(Root {
357            value: bracketed_newton(&f, &df, lo, bound, lo_negative, 0.0)?,
358            multiplicity: 1,
359        });
360    }
361    Ok(out)
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn leading_zeros_lower_the_degree() {
370        let r = quartic(0.0, 0.0, 1.0, 0.0, -4.0).unwrap();
371        assert_eq!(r.len(), 2);
372        assert_eq!((r[0].value, r[1].value), (-2.0, 2.0));
373        let linear = cubic(0.0, 0.0, 2.0, -1.0).unwrap();
374        assert_eq!(
375            linear.as_slice(),
376            &[Root {
377                value: 0.5,
378                multiplicity: 1
379            }]
380        );
381        assert!(quadratic(0.0, 0.0, 3.0).unwrap().is_empty());
382    }
383
384    #[test]
385    fn degenerate_inputs_are_errors() {
386        assert_eq!(quadratic(0.0, 0.0, 0.0), Err(RootError::Zero));
387        assert_eq!(quadratic(f64::NAN, 1.0, 0.0), Err(RootError::NonFinite));
388        assert_eq!(
389            quartic(1e-300, 0.0, 0.0, 0.0, 1e300),
390            Err(RootError::NonFinite)
391        );
392        let f = |x: f64| x * x + 1.0;
393        assert_eq!(
394            newton_in_interval(f, |x| 2.0 * x, Interval::UNIT, 0.0),
395            Err(RootError::NoSignChange)
396        );
397        assert_eq!(
398            newton_in_interval(f, |x| 2.0 * x, Interval::REAL, 0.0),
399            Err(RootError::NonFinite)
400        );
401    }
402
403    #[test]
404    fn a_triple_root_is_found_once() {
405        // (x − 2)³ = x³ − 6x² + 12x − 8
406        let r = cubic(1.0, -6.0, 12.0, -8.0).unwrap();
407        assert_eq!(r.len(), 1);
408        assert_eq!(r[0].multiplicity, 3);
409        assert!((r[0].value - 2.0).abs() < 1e-14);
410        assert_eq!(r.total_multiplicity(), 3);
411    }
412
413    #[test]
414    fn newton_returns_an_end_that_is_exactly_a_root() {
415        let f = |x: f64| x - 1.0;
416        let bracket = Interval::new(1.0, 3.0).unwrap();
417        assert_eq!(newton_in_interval(f, |_| 1.0, bracket, 0.0), Ok(1.0));
418        let bracket = Interval::new(-1.0, 1.0).unwrap();
419        assert_eq!(newton_in_interval(f, |_| 1.0, bracket, 0.0), Ok(1.0));
420    }
421}