Skip to main content

axiolid_numeric/
poly.rs

1//! Real polynomials in the power basis and all their real roots in an
2//! interval.
3//!
4//! # Method
5//!
6//! Roots of `p` are isolated by the roots of `p'`, recursively (the
7//! approach of OCCT `math_DirectPolynomialRoots` generalised, and of
8//! Collins and Loos' derivative sequence): between consecutive critical
9//! points `p` is monotone, so a sign change there is exactly one root,
10//! found by [`find_root`].
11//!
12//! Every evaluation carries a rigorous bound on its own rounding error
13//! (Horner with `gamma(2n) * sum |a_i| |x|^i`, Higham, *Accuracy and
14//! Stability of Numerical Algorithms*, 5.1). A sign is trusted only where
15//! `|p(x)|` exceeds that bound. Where it does not -- at a multiple root, a
16//! cluster closer than `f64` can separate, or a near-touch of the axis --
17//! the answer is [`RootKind::Unresolved`] with the region and the most
18//! roots it can hold, rather than a guessed count.
19//!
20//! The roots are those of the polynomial whose coefficients are the given
21//! `f64` values. Monotonicity between critical points holds to working
22//! precision: the critical points come from the rounded derivative.
23
24use crate::error::{all_finite, finite, NumericError, NumericResult};
25use crate::root::find_root;
26
27/// A real polynomial `a[0] + a[1] x + ... + a[n] x^n` with finite
28/// coefficients.
29#[derive(Debug, Clone, PartialEq)]
30pub struct Polynomial {
31    coefficients: Vec<f64>,
32}
33
34/// How certain a reported root is.
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum RootKind {
37    /// The polynomial is monotone on `[lower, upper]` and its sign
38    /// certifiably changes across it: exactly one simple root lies inside.
39    Simple,
40    /// `|p|` is within its rounding bound near `x`. Between zero and
41    /// `max_count` roots, counted with multiplicity, lie in
42    /// `[lower, upper]`: a multiple root, a cluster, or a near-touch that
43    /// `f64` evaluation cannot tell apart. A root at an end of the
44    /// searched interval is reported this way, with `max_count` 1, since
45    /// it may lie just outside.
46    Unresolved {
47        /// Most roots the region can hold (by Rolle's theorem: one more
48        /// than the critical points inside it).
49        max_count: usize,
50    },
51}
52
53/// A real root, or an unresolved root region, of a polynomial.
54#[derive(Debug, Clone, Copy, PartialEq)]
55pub struct PolynomialRoot {
56    /// Best estimate.
57    pub x: f64,
58    /// Lower end of the enclosure.
59    pub lower: f64,
60    /// Upper end of the enclosure. `|x - root| <= upper - lower` for every
61    /// root the entry accounts for.
62    pub upper: f64,
63    /// Whether the entry is one certified simple root.
64    pub kind: RootKind,
65}
66
67impl PolynomialRoot {
68    /// A bound on the distance from `x` to each root this entry accounts
69    /// for: the enclosure width.
70    pub fn error_bound(&self) -> f64 {
71        self.upper - self.lower
72    }
73}
74
75impl Polynomial {
76    /// A polynomial from coefficients in ascending order of power.
77    /// Trailing zero coefficients are dropped.
78    ///
79    /// # Errors
80    ///
81    /// Refuses a non-finite coefficient.
82    pub fn new(coefficients: &[f64]) -> NumericResult<Self> {
83        all_finite(coefficients, "polynomial coefficient")?;
84        let mut coefficients = coefficients.to_vec();
85        while coefficients.last() == Some(&0.0) {
86            coefficients.pop();
87        }
88        Ok(Self { coefficients })
89    }
90
91    /// `(x - r_0)(x - r_1)...`, expanded. A convenience for building test
92    /// and reference polynomials.
93    ///
94    /// # Errors
95    ///
96    /// Refuses a non-finite root or an expansion that overflows.
97    pub fn from_roots(roots: &[f64]) -> NumericResult<Self> {
98        all_finite(roots, "root")?;
99        let mut c = vec![1.0];
100        for &r in roots {
101            let mut next = vec![0.0; c.len() + 1];
102            for (i, &ci) in c.iter().enumerate() {
103                next[i + 1] += ci;
104                next[i] -= r * ci;
105            }
106            c = next;
107        }
108        if c.iter().any(|v| !v.is_finite()) {
109            return Err(NumericError::InvalidArgument {
110                name: "roots",
111                reason: "expand to coefficients that overflow",
112            });
113        }
114        Self::new(&c)
115    }
116
117    /// Coefficients in ascending order of power, without trailing zeros.
118    pub fn coefficients(&self) -> &[f64] {
119        &self.coefficients
120    }
121
122    /// Degree, or `None` for the zero polynomial.
123    pub fn degree(&self) -> Option<usize> {
124        self.coefficients.len().checked_sub(1)
125    }
126
127    /// `p(x)` by Horner's rule.
128    pub fn evaluate(&self, x: f64) -> f64 {
129        self.coefficients
130            .iter()
131            .rev()
132            .fold(0.0, |acc, &a| acc * x + a)
133    }
134
135    /// `p(x)` and a rigorous bound on the rounding error of that value.
136    ///
137    /// The bound is `gamma(2n + 1) * sum |a_i| |x|^i` plus an underflow
138    /// allowance, where `gamma(k) = k u / (1 - k u)`.
139    pub fn evaluate_with_bound(&self, x: f64) -> (f64, f64) {
140        let n = self.coefficients.len();
141        let mut value = 0.0f64;
142        let mut magnitude = 0.0f64;
143        let ax = x.abs();
144        for &a in self.coefficients.iter().rev() {
145            value = value * x + a;
146            magnitude = magnitude * ax + a.abs();
147        }
148        let u = f64::EPSILON / 2.0;
149        let k = (2 * n + 1) as f64;
150        let gamma = k * u / (1.0 - k * u);
151        // Products may underflow: allow one smallest normal per operation.
152        let bound = 2.0 * gamma * magnitude + (2 * n) as f64 * f64::MIN_POSITIVE;
153        (value, bound)
154    }
155
156    /// The derivative.
157    pub fn derivative(&self) -> Polynomial {
158        let coefficients: Vec<f64> = self
159            .coefficients
160            .iter()
161            .enumerate()
162            .skip(1)
163            .map(|(i, &a)| a * i as f64)
164            .collect();
165        Polynomial { coefficients }
166    }
167
168    /// Every real root in `[lower, upper]`, in increasing order.
169    ///
170    /// Each entry is either one certified [`RootKind::Simple`] root with an
171    /// enclosure, or an [`RootKind::Unresolved`] region with the most
172    /// roots it can hold. No root of the polynomial in the interval is left
173    /// out: a root not reported as simple lies in an unresolved region.
174    ///
175    /// # Errors
176    ///
177    /// Refuses non-finite bounds, `lower > upper`, and the zero polynomial
178    /// ([`NumericError::ZeroPolynomial`]).
179    pub fn real_roots(&self, lower: f64, upper: f64) -> NumericResult<Vec<PolynomialRoot>> {
180        finite(lower, "lower bound")?;
181        finite(upper, "upper bound")?;
182        if lower > upper {
183            return Err(NumericError::InvalidArgument {
184                name: "interval",
185                reason: "must have lower <= upper",
186            });
187        }
188        if self.coefficients.is_empty() {
189            return Err(NumericError::ZeroPolynomial);
190        }
191        self.isolate(lower, upper)
192    }
193
194    fn sign(&self, x: f64) -> i8 {
195        let (v, bound) = self.evaluate_with_bound(x);
196        if v.abs() <= bound {
197            0
198        } else if v > 0.0 {
199            1
200        } else {
201            -1
202        }
203    }
204
205    fn isolate(&self, lower: f64, upper: f64) -> NumericResult<Vec<PolynomialRoot>> {
206        if self.coefficients.len() <= 1 {
207            // A non-zero constant has no roots.
208            return Ok(Vec::new());
209        }
210        let critical = self.derivative().isolate(lower, upper)?;
211
212        // Breakpoints: the interval ends and each critical point, with the
213        // number of critical points (with multiplicity) each carries.
214        let mut points: Vec<(f64, usize)> = vec![(lower, 0)];
215        for c in &critical {
216            let count = match c.kind {
217                RootKind::Simple => 1,
218                RootKind::Unresolved { max_count } => max_count,
219            };
220            let x = c.x.clamp(lower, upper);
221            match points.last_mut() {
222                Some(last) if last.0 == x => last.1 += count,
223                _ => points.push((x, count)),
224            }
225        }
226        match points.last_mut() {
227            Some(last) if last.0 == upper => {}
228            _ => points.push((upper, 0)),
229        }
230        let signs: Vec<i8> = points.iter().map(|&(x, _)| self.sign(x)).collect();
231
232        let mut roots = Vec::new();
233        let mut i = 0;
234        while i < points.len() {
235            if signs[i] == 0 {
236                // A run of breakpoints where p is indistinguishable from zero.
237                let start = i;
238                let mut crit_count = 0;
239                while i < points.len() && signs[i] == 0 {
240                    crit_count += points[i].1;
241                    i += 1;
242                }
243                let end = i - 1;
244                let left_limit = if start > 0 {
245                    points[start - 1].0
246                } else {
247                    lower
248                };
249                let right_limit = if i < points.len() { points[i].0 } else { upper };
250                let x = if start == end {
251                    points[start].0
252                } else {
253                    0.5 * points[start].0 + 0.5 * points[end].0
254                };
255                roots.push(PolynomialRoot {
256                    x,
257                    lower: self.boundary(points[start].0, left_limit),
258                    upper: self.boundary(points[end].0, right_limit),
259                    kind: RootKind::Unresolved {
260                        max_count: crit_count + 1,
261                    },
262                });
263                continue;
264            }
265            if i + 1 < points.len() && signs[i + 1] != 0 && signs[i] != signs[i + 1] {
266                let (a, b) = (points[i].0, points[i + 1].0);
267                let root = find_root(|x| self.evaluate(x), a, b, 0.0)?;
268                let lo = if self.sign(root.lower) != 0 {
269                    root.lower
270                } else {
271                    self.boundary(root.lower, a)
272                };
273                let hi = if self.sign(root.upper) != 0 {
274                    root.upper
275                } else {
276                    self.boundary(root.upper, b)
277                };
278                roots.push(PolynomialRoot {
279                    x: root.x,
280                    lower: lo,
281                    upper: hi,
282                    kind: RootKind::Simple,
283                });
284            }
285            i += 1;
286        }
287        Ok(roots)
288    }
289
290    /// From `inside`, where the sign is uncertain, move toward `limit` to
291    /// the nearest point found whose sign is certain. Returns `limit` if
292    /// the sign is uncertain there too.
293    fn boundary(&self, inside: f64, limit: f64) -> f64 {
294        if inside == limit || self.sign(limit) == 0 {
295            return limit;
296        }
297        let (mut uncertain, mut certain) = (inside, limit);
298        for _ in 0..200 {
299            let mid = 0.5 * uncertain + 0.5 * certain;
300            if mid == uncertain || mid == certain {
301                break;
302            }
303            if self.sign(mid) == 0 {
304                uncertain = mid;
305            } else {
306                certain = mid;
307            }
308        }
309        certain
310    }
311}