poly_cool/
poly.rs

1use crate::{Cubic, InputError, TerminationCondition, different_signs};
2
3/// A polynomial of dynamic degree.
4///
5/// It would be nice to have polynomials of type-level degree,
6/// but that's a bit awkward without const generic expressions
7/// (e.g. to express the type of the derivative). It could be
8/// done with `typenum` and `generic_array`...
9#[derive(Clone, Debug)]
10pub struct Poly {
11    /// Coefficients in increasing order of degree.
12    ///
13    /// For example, `coeffs[0]` is the constant term.
14    coeffs: Vec<f64>,
15}
16
17impl<'a> std::ops::Mul<&'a Poly> for &'a Poly {
18    type Output = Poly;
19
20    fn mul(self, rhs: &Poly) -> Poly {
21        let mut coeffs = vec![0.0; (self.coeffs.len() + rhs.coeffs.len()).saturating_sub(1)];
22
23        for (i, c) in self.coeffs.iter().enumerate() {
24            for (j, d) in rhs.coeffs.iter().enumerate() {
25                coeffs[i + j] += c * d;
26            }
27        }
28        Poly { coeffs }
29    }
30}
31
32impl std::ops::Mul<&Poly> for Poly {
33    type Output = Poly;
34
35    fn mul(self, rhs: &Poly) -> Poly {
36        (&self) * rhs
37    }
38}
39
40impl Poly {
41    /// Constructs a new polynomial from coefficients.
42    ///
43    /// The first coefficient provided will be the constant term, the second will
44    /// be the linear term, and so on.
45    pub fn new(coeffs: impl IntoIterator<Item = f64>) -> Self {
46        Poly {
47            coeffs: coeffs.into_iter().collect(),
48        }
49    }
50
51    fn is_finite(&self) -> bool {
52        self.coeffs.iter().all(|c| c.is_finite())
53    }
54
55    /// Returns the polynomial that's the derivative of this polynomial.
56    pub fn deriv(&self) -> Poly {
57        let mut coeffs = Vec::with_capacity(self.coeffs.len() - 1);
58        // If we're empty (meaning that we're the constant zero polynomial),
59        // this will just return the zero polynomial again: no need for a
60        // special case.
61        for (i, c) in self.coeffs.iter().enumerate().skip(1) {
62            coeffs.push(c * (i as f64));
63        }
64        Poly { coeffs }
65    }
66
67    /// Evaluates this polynomial at a point.
68    pub fn eval(&self, x: f64) -> f64 {
69        let mut ret = 0.0;
70        let mut x_pow = 1.0;
71        for &c in &self.coeffs {
72            ret += c * x_pow;
73            x_pow *= x;
74        }
75        ret
76    }
77
78    /// The degree of this polynomial.
79    ///
80    /// This function only looks at the *presence* of coefficients, not their
81    /// value. If you construct a polynomial with three coefficients, this
82    /// method will say that it has degree 2 even if all of those coefficients
83    /// are zero.
84    ///
85    /// A polynomial with no coefficients will give zero as its degree, as will
86    /// a polynomial with one coefficient.
87    pub fn degree(&self) -> usize {
88        self.coeffs.len().saturating_sub(1)
89    }
90
91    /// If this polynomial has degree 3 or less, converts it to a [cubic](crate::Cubic).
92    pub fn to_cubic(&self) -> Option<Cubic> {
93        if self.degree() <= 3 {
94            Some(Cubic {
95                c0: self.coeffs.first().copied().unwrap_or(0.0),
96                c1: self.coeffs.get(1).copied().unwrap_or(0.0),
97                c2: self.coeffs.get(2).copied().unwrap_or(0.0),
98                c3: self.coeffs.get(3).copied().unwrap_or(0.0),
99            })
100        } else {
101            None
102        }
103    }
104
105    fn one_root<Term: TerminationCondition>(
106        &self,
107        deriv: &Poly,
108        mut lower: f64,
109        mut upper: f64,
110        val_lower: f64,
111        val_upper: f64,
112        term: Term,
113    ) -> f64 {
114        if !val_lower.is_finite() || !val_upper.is_finite() || !deriv.is_finite() {
115            return f64::NAN;
116        }
117        debug_assert!(different_signs(val_lower, val_upper));
118
119        let mut x = lower + (upper - lower) / 2.0;
120        let mut val_x = self.eval(x);
121        let mut step = (upper - lower) / 2.0;
122
123        while x.is_finite() && !term.stop(step, val_x) {
124            let root_in_first_half = different_signs(val_lower, val_x);
125            if root_in_first_half {
126                upper = x;
127            } else {
128                lower = x;
129            }
130
131            let deriv_x = self.deriv().eval(x);
132            debug_assert!(deriv_x.is_finite());
133            debug_assert!(val_x.is_finite());
134
135            step = -val_x / deriv_x;
136            let mut new_x = x + step;
137
138            if new_x <= lower || new_x >= upper {
139                new_x = lower + (upper - lower) / 2.0;
140
141                if new_x == upper || new_x == lower {
142                    // This should be rare, but it happens if they ask for more
143                    // accuracy than is reasonable. For example, suppse (because
144                    // of large coefficients) the output value jumps from -1.0
145                    // to 1.0 between adjacent floats and they ask for an output
146                    // error of smaller than 0.5. Then we'll eventually shrink
147                    // the search interval to a pair of adjacent floats and hit
148                    // this case.
149                    return new_x;
150                }
151            }
152            step = new_x - x;
153            x = new_x;
154            val_x = self.eval(x);
155        }
156        x
157    }
158
159    /// Finds all the roots in an interval, using Yuksel's algorithm.
160    ///
161    /// This is a numerical, iterative method. It first constructs critical
162    /// points to find bracketing intervals (intervals `[x0, x1]` where
163    /// `self.eval(x0)` and `self.eval(x1)` have different signs). Then it uses
164    /// a kind of modified Newton method to find a root on each bracketing
165    /// interval. It has a few limitations:
166    ///
167    /// - if there is only a small interval where the polynomial changes sign,
168    ///   it can miss roots. For example, when two roots are very close together
169    ///   it can miss them both.
170    /// - run time is quadratic in the degree. However, it is often very fast
171    ///   in practice for polynomials of low degree, especially if the interval
172    ///   `[lower, upper]` contains few roots.
173    pub fn roots_in(&self, lower: f64, upper: f64, x_error: f64) -> Vec<f64> {
174        let mut ret = Vec::new();
175
176        if let Some(c) = self.to_cubic() {
177            ret.extend(c.all_roots(lower, upper, x_error));
178            return ret;
179        }
180
181        let deriv = self.deriv();
182        let mut possible_endpoints = deriv.roots_in(lower, upper, x_error);
183        possible_endpoints.push(upper);
184        let mut last = lower;
185        let mut last_val = self.eval(last);
186        for x in possible_endpoints {
187            if x > last && x <= upper {
188                let val = self.eval(x);
189                if different_signs(last_val, val) {
190                    ret.push(self.one_root(&deriv, last, x, last_val, val, InputError(x_error)));
191                }
192
193                last = x;
194                last_val = val;
195            }
196        }
197        ret
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn smoke() {
207        let x_minus_1 = Poly::new([-1.0, 1.0]);
208        let x_minus_2 = Poly::new([-2.0, 1.0]);
209        let x_minus_3 = Poly::new([-3.0, 1.0]);
210        let x_minus_4 = Poly::new([-4.0, 1.0]);
211
212        let p = &x_minus_1 * &x_minus_2 * &x_minus_3 * &x_minus_4;
213
214        let roots = p.roots_in(0.0, 5.0, 1e-6);
215        assert_eq!(roots.len(), 4);
216        assert!((roots[0] - 1.0).abs() <= 1e-6);
217        assert!((roots[1] - 2.0).abs() <= 1e-6);
218        assert!((roots[2] - 3.0).abs() <= 1e-6);
219        assert!((roots[3] - 4.0).abs() <= 1e-6);
220    }
221}