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