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}