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}