Skip to main content

poly_cool/
poly.rs

1use arrayvec::ArrayVec;
2
3/// A polynomial whose degree is known at compile-time.
4///
5/// Although this supports polynomials of arbitrary degree, it is intended
6/// for low-degree polynomials. For example, the coefficients are stored
7/// in an array, and so they will be stack-allocated (unless you `Box`
8/// the `Poly`, of course) tend to be copied around.
9///
10/// Polynomial multiplication is not yet implemented, because doing it "nicely"
11/// would require const generic expressions: ideally we'd do something like
12///
13/// ```ignore
14/// impl<N, M> Mul<Poly<M>> for Poly<N> {
15///     type Output = Poly<{M + N - 1}>;
16/// }
17/// ```
18///
19/// It's possible to work around this with macros, but there are lots of
20/// possibilities and I didn't feel like it was worth the trouble (and the hit
21/// to compilation time).
22#[derive(Clone, Copy, Debug, PartialEq, PartialOrd)]
23pub struct Poly<const N: usize> {
24    pub(crate) coeffs: [f64; N],
25}
26
27/// A polynomial of degree 2.
28pub type Quadratic = Poly<3>;
29
30/// A polynomial of degree 3.
31pub type Cubic = Poly<4>;
32
33/// A polynomial of degree 4.
34pub type Quartic = Poly<5>;
35
36/// A polynomial of degree 5.
37pub type Quintic = Poly<6>;
38
39impl<const N: usize> Poly<N> {
40    /// Creates a new polynomial with the provided coefficients.
41    ///
42    /// The constant coefficient comes first, then the linear coefficient, and
43    /// so on. So if you pass `[c, b, a]` you'll get the polynomial
44    /// `a x^2 + b x + c`.
45    pub const fn new(coeffs: [f64; N]) -> Poly<N> {
46        Poly { coeffs }
47    }
48
49    /// The coefficients of this polynomial.
50    ///
51    /// In the returned array, the coefficient of `x^i` is at index `i`.
52    pub fn coeffs(&self) -> &[f64; N] {
53        &self.coeffs
54    }
55
56    /// Evaluates this polynomial at a point.
57    pub fn eval(&self, x: f64) -> f64 {
58        let mut acc = 0.0;
59        for c in self.coeffs.iter().rev() {
60            // It would be nice to use `f64::mul_add` here, but it's slow on
61            // architectures that don't have a dedicated instruction.
62            acc = acc * x + c;
63        }
64        acc
65    }
66
67    /// Returns the largest absolute value of any coefficient.
68    ///
69    /// Always returns a non-negative number, or NaN if some coefficient is NaN.
70    pub fn magnitude(&self) -> f64 {
71        let mut max = 0.0f64;
72        for c in &self.coeffs {
73            max = max.max(c.abs());
74        }
75        max
76    }
77
78    /// Are all the coefficients finite?
79    pub fn is_finite(&self) -> bool {
80        self.coeffs.iter().all(|c| c.is_finite())
81    }
82}
83
84macro_rules! impl_deriv_and_deflate {
85    ($N:literal, $N_MINUS_ONE:literal) => {
86        impl Poly<$N> {
87            /// Compute the derivative of this polynomial, as a polynomial with
88            /// one less coefficient.
89            pub fn deriv(&self) -> Poly<$N_MINUS_ONE> {
90                let mut coeffs = [0.0; $N_MINUS_ONE];
91                for (i, (d, c)) in coeffs.iter_mut().zip(&self.coeffs[1..]).enumerate() {
92                    *d = (i + 1) as f64 * c;
93                }
94                Poly::new(coeffs)
95            }
96
97            /// Divide this polynomial by the polynomial `x - root`, returning the
98            /// quotient (as a polynomial with one less coefficient) and ignoring
99            /// the remainder.
100            ///
101            /// If `root` is actually a root of `self` (as the name suggests
102            /// it should be, but this is not actually required), the
103            /// remainder will be zero. In general, the remainder will be
104            /// `self.eval(root)`.
105            pub fn deflate(&self, root: f64) -> Poly<$N_MINUS_ONE> {
106                let mut acc = 0.0;
107                let mut coeffs = [0.0; $N_MINUS_ONE];
108                for (d, c) in coeffs.iter_mut().zip(&self.coeffs[1..]).rev() {
109                    acc = acc * root + c;
110                    *d = acc;
111                }
112                Poly::new(coeffs)
113            }
114        }
115    };
116}
117
118macro_rules! impl_roots_between_recursive {
119    ($N:literal, $N_MINUS_ONE:literal) => {
120        impl Poly<$N> {
121            /// Computes all roots between `lower` and `upper`, to the desired accuracy.
122            ///
123            /// We make no guarantees about multiplicity. For example, if there's a
124            /// double-root that isn't a triple-root (and therefore has no sign change
125            /// nearby) then there's a good chance we miss it altogether. This is
126            /// fine if you're using this root-finding to find critical points for
127            /// optimizing a polynomial, because roots that don't come with a sign
128            /// change aren't local extrema.
129            pub fn roots_between(
130                self,
131                lower: f64,
132                upper: f64,
133                x_error: f64,
134            ) -> ArrayVec<f64, $N_MINUS_ONE> {
135                let mut ret = ArrayVec::new();
136                let mut scratch = ArrayVec::new();
137                self.roots_between_with_buffer(lower, upper, x_error, &mut scratch, &mut ret);
138                ret
139            }
140
141            // This would ideally have a `where M >= N - 1` bound on it,
142            // but it's private so it shouldn't matter too much.
143            // We assume that `scratch` and `out` are both empty.
144            fn roots_between_with_buffer<const M: usize>(
145                self,
146                lower: f64,
147                upper: f64,
148                x_error: f64,
149                scratch: &mut ArrayVec<f64, M>,
150                out: &mut ArrayVec<f64, M>,
151            ) {
152                let deriv = self.deriv();
153                if !deriv.is_finite() {
154                    return;
155                }
156                deriv.roots_between_with_buffer(lower, upper, x_error, out, scratch);
157                scratch.push(upper);
158                out.clear();
159                let mut last = lower;
160                let mut last_val = self.eval(last);
161
162                // `endpoint` now contains all the critical points (in increasing order)
163                // and the upper endpoint of the interval. These are the endpoints
164                // of the potential bracketing intervals of our polynomial.
165                for &mut x in scratch {
166                    let val = self.eval(x);
167                    if $crate::different_signs(last_val, val) {
168                        out.push($crate::yuksel::find_root(
169                            |x| self.eval(x),
170                            |x| deriv.eval(x),
171                            last,
172                            x,
173                            last_val,
174                            val,
175                            x_error,
176                        ));
177                    }
178
179                    last = x;
180                    last_val = val;
181                }
182            }
183        }
184    };
185}
186
187impl_deriv_and_deflate!(3, 2);
188impl_deriv_and_deflate!(4, 3);
189impl_deriv_and_deflate!(5, 4);
190impl_deriv_and_deflate!(6, 5);
191impl_deriv_and_deflate!(7, 6);
192impl_deriv_and_deflate!(8, 7);
193impl_deriv_and_deflate!(9, 8);
194impl_deriv_and_deflate!(10, 9);
195
196impl_roots_between_recursive!(5, 4);
197impl_roots_between_recursive!(6, 5);
198impl_roots_between_recursive!(7, 6);
199impl_roots_between_recursive!(8, 7);
200impl_roots_between_recursive!(9, 8);
201impl_roots_between_recursive!(10, 9);
202
203impl<const N: usize> core::ops::Mul<f64> for Poly<N> {
204    type Output = Poly<N>;
205
206    fn mul(mut self, scale: f64) -> Poly<N> {
207        self *= scale;
208        self
209    }
210}
211
212impl<const N: usize> core::ops::MulAssign<f64> for Poly<N> {
213    fn mul_assign(&mut self, scale: f64) {
214        for c in &mut self.coeffs {
215            *c *= scale;
216        }
217    }
218}
219
220impl<const N: usize> core::ops::Mul<f64> for &Poly<N> {
221    type Output = Poly<N>;
222
223    fn mul(self, scale: f64) -> Poly<N> {
224        (*self) * scale
225    }
226}
227
228impl<const N: usize> core::ops::Div<f64> for Poly<N> {
229    type Output = Poly<N>;
230
231    fn div(mut self, scale: f64) -> Poly<N> {
232        self /= scale;
233        self
234    }
235}
236
237impl<const N: usize> core::ops::DivAssign<f64> for Poly<N> {
238    fn div_assign(&mut self, scale: f64) {
239        for c in &mut self.coeffs {
240            *c /= scale;
241        }
242    }
243}
244
245impl<const N: usize> core::ops::Div<f64> for &Poly<N> {
246    type Output = Poly<N>;
247
248    fn div(self, scale: f64) -> Poly<N> {
249        (*self) / scale
250    }
251}
252
253impl<const N: usize> core::ops::AddAssign<&Poly<N>> for Poly<N> {
254    fn add_assign(&mut self, rhs: &Poly<N>) {
255        for (c, d) in self.coeffs.iter_mut().zip(rhs.coeffs) {
256            *c += d;
257        }
258    }
259}
260
261impl<const N: usize> core::ops::AddAssign<Poly<N>> for Poly<N> {
262    fn add_assign(&mut self, rhs: Poly<N>) {
263        *self += &rhs;
264    }
265}
266
267impl<const N: usize> core::ops::Add<Poly<N>> for Poly<N> {
268    type Output = Poly<N>;
269
270    fn add(mut self, rhs: Poly<N>) -> Poly<N> {
271        self += rhs;
272        self
273    }
274}
275
276impl<const N: usize> core::ops::Add<&Poly<N>> for Poly<N> {
277    type Output = Poly<N>;
278
279    fn add(mut self, rhs: &Poly<N>) -> Poly<N> {
280        self += rhs;
281        self
282    }
283}
284
285impl<const N: usize> core::ops::Add<Poly<N>> for &Poly<N> {
286    type Output = Poly<N>;
287
288    fn add(self, mut rhs: Poly<N>) -> Poly<N> {
289        rhs += self;
290        rhs
291    }
292}
293
294impl<const N: usize> core::ops::SubAssign<&Poly<N>> for Poly<N> {
295    fn sub_assign(&mut self, rhs: &Poly<N>) {
296        for (c, d) in self.coeffs.iter_mut().zip(rhs.coeffs) {
297            *c -= d;
298        }
299    }
300}
301
302impl<const N: usize> core::ops::SubAssign<Poly<N>> for Poly<N> {
303    fn sub_assign(&mut self, rhs: Poly<N>) {
304        *self -= &rhs;
305    }
306}
307
308impl<const N: usize> core::ops::Sub<Poly<N>> for Poly<N> {
309    type Output = Poly<N>;
310
311    fn sub(mut self, rhs: Poly<N>) -> Poly<N> {
312        self -= rhs;
313        self
314    }
315}
316
317impl<const N: usize> core::ops::Sub<&Poly<N>> for Poly<N> {
318    type Output = Poly<N>;
319
320    fn sub(mut self, rhs: &Poly<N>) -> Poly<N> {
321        self -= rhs;
322        self
323    }
324}
325
326impl<const N: usize> core::ops::Sub<Poly<N>> for &Poly<N> {
327    type Output = Poly<N>;
328
329    fn sub(self, mut rhs: Poly<N>) -> Poly<N> {
330        rhs -= self;
331        rhs
332    }
333}
334
335// We do property-testing with two strategies:
336//
337// - for the "value-testing" strategy, we test that the polynomial
338//   approximately evaluates to zero on all the claimed roots.
339// - for the "planted root" strategy, we generate a polynomial with
340//   a known root and check that we find it
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn smoke() {
347        let p = Poly::new([-6.0, 11.0, -6.0, 1.0]);
348
349        let roots = p.roots_between(0.0, 5.0, 1e-6);
350        assert_eq!(roots.len(), 3);
351        assert!((roots[0] - 1.0).abs() <= 1e-6);
352        assert!((roots[1] - 2.0).abs() <= 1e-6);
353        assert!((roots[2] - 3.0).abs() <= 1e-6);
354
355        let p = Poly::new([24.0, -50.0, 35.0, -10.0, 1.0]);
356
357        let roots = p.roots_between(0.0, 5.0, 1e-6);
358        assert_eq!(roots.len(), 4);
359        assert!((roots[0] - 1.0).abs() <= 1e-6);
360        assert!((roots[1] - 2.0).abs() <= 1e-6);
361        assert!((roots[2] - 3.0).abs() <= 1e-6);
362        assert!((roots[3] - 4.0).abs() <= 1e-6);
363    }
364
365    // Asserts that the supplied "roots" are close to being roots of the
366    // cubic, in the sense that the cubic evaluates to approximately zero
367    // on each of the roots.
368    fn check_root_values<const N: usize>(p: &Poly<N>, roots: &[f64]) {
369        // Arbitrary cubics can have coefficients with wild magnitudes,
370        // so we need to adjust our error expectations accordingly.
371        let magnitude = p.magnitude().max(1.0);
372        let accuracy = magnitude * 1e-12;
373
374        for r in roots {
375            // We can't expect great accuracy for very large roots,
376            // because the polynomial evaluation will involve very
377            // large terms.
378            let accuracy = accuracy * r.abs().powi(N as i32 - 1).max(1.0);
379            let y = p.eval(*r);
380            if y.is_finite() {
381                assert!(
382                    y.abs() <= accuracy,
383                    "poly {p:?} had root {r} evaluate to {y:?}, but expected {accuracy:?}"
384                );
385            }
386        }
387    }
388
389    #[test]
390    fn root_evaluation_deg4() {
391        arbtest::arbtest(|u| {
392            let poly: Poly<5> = crate::arbitrary::poly(u)?;
393            let roots = poly.roots_between(-10.0, 10.0, 1e-13);
394            check_root_values(&poly, &roots);
395            Ok(())
396        })
397        .budget_ms(5_000);
398    }
399
400    #[test]
401    fn root_evaluation_deg9() {
402        arbtest::arbtest(|u| {
403            let poly: Poly<10> = crate::arbitrary::poly(u)?;
404            let roots = poly.roots_between(-10.0, 10.0, 1e-13);
405            check_root_values(&poly, &roots);
406            Ok(())
407        })
408        .budget_ms(5_000);
409    }
410
411    #[test]
412    fn planted_root_deg5() {
413        arbtest::arbtest(|u| {
414            let planted_root = crate::arbitrary::float_in_unit_interval(u)?;
415            let poly: Poly<6> = crate::arbitrary::poly_with_planted_root(u, planted_root, 1e-6)?;
416
417            // Bear in mind that Yuksel's algorithm needs iterated derivatives to be
418            // finite (and that we aren't doing any preconditioning or normalization yet),
419            // ensure that the polynomial isn't too big.
420            if (poly.magnitude() * 1024.0).is_infinite() {
421                return Err(arbitrary::Error::IncorrectFormat);
422            }
423            let roots = poly.roots_between(-2.0, 2.0, 1e-13);
424
425            // Check that the roots are sorted.
426            if roots.iter().all(|r| r.is_finite()) {
427                assert!(roots.is_sorted());
428            }
429
430            // We can't expect great accuracy for huge coefficients, because the
431            // evaluations during Newton iteration are subject to error.
432            let error = poly.magnitude().max(1.0) * 1e-12;
433            assert!(roots.iter().any(|r| (r - planted_root).abs() <= error));
434            Ok(())
435        })
436        .budget_ms(5_000);
437    }
438}