poly_interp/
lib.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright 2025 UxuginPython
3//!A simple but powerful polynomial library focused on interpolation between points. Works with
4//!both standard polynomials and parametric curves. Mostly no_std although a dynamic allocator is
5//!required.
6#![warn(missing_docs)]
7#![cfg_attr(not(feature = "std"), no_std)]
8extern crate alloc;
9use alloc::{vec, vec::Vec};
10use core::cmp::max;
11use core::ops::*;
12///A point with x and y coordinates.
13#[derive(Clone, Copy, Debug, Default, PartialEq)]
14pub struct PointXY {
15    ///The x coordinate.
16    pub x: f64,
17    ///The y coordinate.
18    pub y: f64,
19}
20impl PointXY {
21    ///Constructor for `PointXY`.
22    #[inline]
23    pub fn new(x: f64, y: f64) -> Self {
24        Self { x, y }
25    }
26}
27///Performs an iterative process to approximate the inverse of a function using its derivative at a
28///given y coordinate. (Solves f(x)=y for x at a given y.) This requires an initial "guess" at the
29///desired x value and a maximum number of iterations to perform when calculating it. The function
30///returns early if it detects that x has been calculated perfectly (`function(x)` returns exactly
31///`y`). It may return NaN or infinity if the derivative returns 0 at one of the progressive
32///estimates of x. The function returns (x, f(x)) and not (x, y) if they are different.
33pub fn newtons_method<F: Fn(f64) -> f64, D: Fn(f64) -> f64>(
34    function: F,
35    derivative: D,
36    y: f64,
37    x_guess: f64,
38    max_iterations: u16,
39) -> PointXY {
40    let mut x = x_guess;
41    for _ in 0..max_iterations {
42        let function_evaluation_minus_y = function(x) - y;
43        if function_evaluation_minus_y == 0.0 {
44            break;
45        }
46        x -= function_evaluation_minus_y / derivative(x);
47    }
48    PointXY::new(x, function(x))
49}
50///A polynomial function.
51#[derive(Clone, Debug, Default, PartialEq)]
52pub struct Polynomial {
53    coefficients: Vec<f64>,
54}
55impl Polynomial {
56    ///Constructor for `Polynomial`. The index of each coefficient in the `Vec` is its
57    ///corresponding exponent. For example, the polynomial x^4+2x^2+3x+1 would be constructed with
58    ///`Polynomial::new(vec![1.0, 3.0, 2.0, 0.0, 1.0])`.
59    #[inline]
60    pub fn new(coefficients: Vec<f64>) -> Self {
61        let mut coefficients = coefficients;
62        while coefficients.last() == Some(&0.0) {
63            coefficients.pop();
64        }
65        Self { coefficients }
66    }
67    ///Construct a `Polynomial` where `polynomial.evaluate(x) == PointXY::new(x, 0.0)` for every
68    ///value of `x` in the `zeros` `Vec`.
69    pub fn from_zeros(zeros: Vec<f64>) -> Self {
70        let mut new_self = Polynomial::new(vec![1.0]);
71        for x in zeros {
72            new_self = new_self * Polynomial::new(vec![-x, 1.0]);
73        }
74        new_self
75    }
76    ///Interpolate the lowest-degree polynomial going through every point in the `points` `Vec`
77    ///using an algorithm based on Newton's form.
78    pub fn interpolate(points: Vec<PointXY>) -> Self {
79        //In the Newton polynomial form, coefficients are almost always seen with their
80        //corresponding binomial factors, e.g., c3(x-x0)(x-x1)(x-x2). This generates these binomial
81        //factors without needing to calculate the coefficients themselves yet.
82        let mut zeroing_polynomials = Vec::with_capacity(points.len());
83        for i in 0..points.len() {
84            let mut new_zeros = Vec::with_capacity(i);
85            for j in 0..i {
86                new_zeros.push(points[j].x);
87            }
88            zeroing_polynomials.push(Self::from_zeros(new_zeros));
89        }
90        //Now we do actually calculate the coefficients. Here's an example showing the algebra used
91        //to derive the math for this section:
92        //y3 = c0 + c1(x3-x0) + c2(x3-x0)(x3-x1) + c3(x3-x0)(x3-x1)(x3-x2)
93        //y3 - c0 - c1(x3-x0) - c2(x3-x0)(x3-x1) = c3(x3-x0)(x3-x1)(x3-x2)
94        //[y3 - c0 - c1(x3-x0) - c2(x3-x0)(x3-x1)] / [(x3-x0)(x3-x1)(x3-x2)] = c3
95        //Remember that we generated the binomial factors (for example (x3-x0)(x3-x1)(x3-x2))
96        //before, so we new just fetch them from that Vec and they don't look very interesting
97        //here.
98        let mut coefficients = Vec::with_capacity(points.len());
99        for i in 0..points.len() {
100            let mut numerator = points[i].y;
101            for j in 0..i {
102                numerator -= zeroing_polynomials[j].evaluate(points[i].x).y * coefficients[j];
103            }
104            coefficients.push(numerator / zeroing_polynomials[i].evaluate(points[i].x).y);
105        }
106        //This is the final summation into, to continue our example,
107        //y = c0 + c1(x-x0) + c2(x-x0)(x-x1) + c3(x-x0)(x-x1)(x-x2)
108        let mut final_polynomial = Self::default();
109        for (i, zeroing_polynomial) in zeroing_polynomials.into_iter().enumerate() {
110            final_polynomial = final_polynomial + zeroing_polynomial * coefficients[i];
111        }
112        final_polynomial
113    }
114    ///Evaluate the polynomial. Returns a `PointXY` containing (x, f(x)) where x is `evaluate`'s
115    ///input and f(x) is the mathematical function the `Polynomial` object represents.
116    pub fn evaluate(&self, x: f64) -> PointXY {
117        let mut y = 0.0;
118        let mut x_power = 1.0;
119        for coefficient in &self.coefficients {
120            y += coefficient * x_power;
121            x_power *= x;
122        }
123        PointXY::new(x, y)
124    }
125    ///Solve for the derivative function of the polynomial using the power rule.
126    pub fn derivative(&self) -> Polynomial {
127        let mut derivative_coefficients = Vec::with_capacity(self.coefficients.len() - 1);
128        for (i, coefficient) in self.coefficients.iter().enumerate().skip(1) {
129            derivative_coefficients.push(i as f64 * coefficient);
130        }
131        Self::new(derivative_coefficients)
132    }
133    ///Calculate the indefinite integral of the polynomial using the power rule given c, the
134    ///constant of integration.
135    pub fn integral(&self, c: f64) -> Polynomial {
136        let mut integral_coefficients = Vec::with_capacity(self.coefficients.len() + 1);
137        integral_coefficients.push(c);
138        for (i, coefficient) in self.coefficients.iter().enumerate() {
139            integral_coefficients.push(coefficient / (i + 1) as f64)
140        }
141        Polynomial::new(integral_coefficients)
142    }
143    ///Estimate an x for which `polynomial.evaluate(x)` will return (x, y) for a given y using
144    ///Newton's method. This requires an initial "guess" at this x value and a maximum number of
145    ///iterations to perform when estimating x. See the documentation for [`newtons_method`], the
146    ///function which this uses internally, for more information.
147    pub fn newtons_method(&self, y: f64, x_guess: f64, max_iterations: u16) -> PointXY {
148        let derivative = self.derivative();
149        newtons_method(
150            |x| self.evaluate(x).y,
151            |x| derivative.evaluate(x).y,
152            y,
153            x_guess,
154            max_iterations,
155        )
156    }
157}
158impl From<f64> for Polynomial {
159    fn from(was: f64) -> Self {
160        Self::new(vec![was])
161    }
162}
163impl Neg for Polynomial {
164    type Output = Self;
165    fn neg(self) -> Self {
166        let mut coefficients = self.coefficients;
167        for coefficient in &mut coefficients {
168            *coefficient *= -1.0;
169        }
170        Self::new(coefficients)
171    }
172}
173impl Add for Polynomial {
174    type Output = Self;
175    fn add(self, rhs: Self) -> Self {
176        let mut new_coefficients = vec![0.0; max(self.coefficients.len(), rhs.coefficients.len())];
177        for (i, coefficient) in self.coefficients.iter().enumerate() {
178            new_coefficients[i] += coefficient;
179        }
180        for (i, coefficient) in rhs.coefficients.iter().enumerate() {
181            new_coefficients[i] += coefficient;
182        }
183        Self::new(new_coefficients)
184    }
185}
186impl Sub for Polynomial {
187    type Output = Self;
188    fn sub(self, rhs: Self) -> Self {
189        self + -rhs
190    }
191}
192impl Mul<f64> for Polynomial {
193    type Output = Self;
194    fn mul(self, rhs: f64) -> Self {
195        let mut coefficients = self.coefficients;
196        for coefficient in &mut coefficients {
197            *coefficient *= rhs;
198        }
199        Self::new(coefficients)
200    }
201}
202impl Div<f64> for Polynomial {
203    type Output = Self;
204    fn div(self, rhs: f64) -> Self {
205        let mut coefficients = self.coefficients;
206        for coefficient in &mut coefficients {
207            *coefficient /= rhs;
208        }
209        Self::new(coefficients)
210    }
211}
212impl Mul for Polynomial {
213    type Output = Self;
214    fn mul(self, rhs: Self) -> Self {
215        let mut coefficients = vec![0.0; self.coefficients.len() + rhs.coefficients.len() - 1];
216        for (my_exponent, my_coefficient) in self.coefficients.iter().enumerate() {
217            for (rhs_exponent, rhs_coefficient) in rhs.coefficients.iter().enumerate() {
218                coefficients[my_exponent + rhs_exponent] += my_coefficient * rhs_coefficient;
219            }
220        }
221        Polynomial::new(coefficients)
222    }
223}
224///A point with x, y, and t coordinates. t is usually the independent variable here.
225#[derive(Clone, Copy, Debug, Default, PartialEq)]
226pub struct PointXYT {
227    ///The x coordinate.
228    pub x: f64,
229    ///The y coordinate.
230    pub y: f64,
231    ///The t coordinate. This is usually the independent variable.
232    pub t: f64,
233}
234impl PointXYT {
235    ///Constructor for `PointXYT`.
236    #[inline]
237    pub fn new(x: f64, y: f64, t: f64) -> Self {
238        Self { x, y, t }
239    }
240    ///Get the point (x, y) as a `PointXY`.
241    #[inline]
242    pub fn xy(&self) -> PointXY {
243        PointXY::new(self.x, self.y)
244    }
245    ///Get the point (t, x) as a `PointXY`.
246    #[inline]
247    pub fn tx(&self) -> PointXY {
248        PointXY::new(self.t, self.x)
249    }
250    ///Get the point (t, y) as a `PointXY`.
251    #[inline]
252    pub fn ty(&self) -> PointXY {
253        PointXY::new(self.t, self.y)
254    }
255}
256///A smooth curve based on functions x(t) and y(t).
257#[derive(Clone, Debug, PartialEq)]
258pub struct XYTCurve {
259    x_polynomial: Polynomial,
260    y_polynomial: Polynomial,
261}
262impl XYTCurve {
263    ///Interpolate an `XYTCurve` that will go through a set of points using lowest-degree
264    ///polynomials for the internal x(t) and y(t) functions.
265    pub fn new(points: Vec<PointXYT>) -> Self {
266        let mut x_polynomial_points = Vec::with_capacity(points.len());
267        for point in &points {
268            x_polynomial_points.push(point.tx());
269        }
270        let mut y_polynomial_points = Vec::with_capacity(points.len());
271        for point in points {
272            y_polynomial_points.push(point.ty());
273        }
274        Self {
275            x_polynomial: Polynomial::interpolate(x_polynomial_points),
276            y_polynomial: Polynomial::interpolate(y_polynomial_points),
277        }
278    }
279    ///Evaluate the curve at a given t. Returns (x(t), y(t), t) as a `PointXYT`.
280    #[inline]
281    pub fn evaluate(&self, t: f64) -> PointXYT {
282        PointXYT::new(
283            self.x_polynomial.evaluate(t).y,
284            self.y_polynomial.evaluate(t).y,
285            t,
286        )
287    }
288    ///Return the derivative of the curve. This can be thought of as the "velocity" at which a
289    ///point moves down the curve as t increases at a constant rate.
290    #[inline]
291    pub fn derivative(&self) -> Self {
292        Self {
293            x_polynomial: self.x_polynomial.derivative(),
294            y_polynomial: self.y_polynomial.derivative(),
295        }
296    }
297    ///Estimate a t at which `curve.evaluate(t)` will return (x, y(t), t) for a given x using
298    ///Newton's method. This requires a "guess" at this t value and a maximum number of iterations
299    ///to perform when estimating t. See the documentation of [`newtons_method`], the function
300    ///which this calls internally, for more information.
301    pub fn newtons_method_x(&self, x: f64, t_guess: f64, max_iterations: u16) -> PointXYT {
302        let newton_output = self.x_polynomial.newtons_method(x, t_guess, max_iterations);
303        let x = newton_output.y;
304        let t = newton_output.x;
305        let y = self.y_polynomial.evaluate(t).y;
306        PointXYT::new(x, y, t)
307    }
308    ///Estimate a t at which `curve.evaluate(t)` will return (x(t), y, t) for a given y using
309    ///Newton's method. This requires a "guess" at this t value and a maximum number of iterations
310    ///to perform when estimating t. See the documentation of [`newtons_method`], the function
311    ///which this calls internally, for more information.
312    pub fn newtons_method_y(&self, y: f64, t_guess: f64, max_iterations: u16) -> PointXYT {
313        let newton_output = self.y_polynomial.newtons_method(y, t_guess, max_iterations);
314        let y = newton_output.y;
315        let t = newton_output.x;
316        let x = self.x_polynomial.evaluate(t).y;
317        PointXYT::new(x, y, t)
318    }
319    //This would work in no_std if the last line was changed to `x * x + y * y`; it's just that
320    //it's a private method and it's not used outside of std-only functions.
321    #[cfg(feature = "std")]
322    fn t_to_speed_squared(&self, t: f64) -> f64 {
323        let derivative = self.derivative();
324        let x = derivative.x_polynomial.evaluate(t).y;
325        let y = derivative.y_polynomial.evaluate(t).y;
326        x.powi(2) + y.powi(2)
327    }
328    ///Calculates the distance along the curve between 0 and t.
329    #[cfg(feature = "std")]
330    pub fn t_to_distance(&self, t: f64) -> f64 {
331        //Integral of square root
332        self.t_to_speed_squared(t).powf(1.5) / 1.5
333    }
334    ///Uses Newton's method to calculate t at a given distance along the curve. This requires an
335    ///initial "guess" at this t and a maximum number of iterations to perform when estimating it.
336    ///See the documentation of [`newtons_method`], the function which this calls internally, for
337    ///more information.
338    #[cfg(feature = "std")]
339    pub fn newtons_method_distance_to_t(
340        &self,
341        distance: f64,
342        t_guess: f64,
343        max_iterations: u16,
344    ) -> f64 {
345        newtons_method(
346            |t| self.t_to_distance(t),
347            |t| self.t_to_speed_squared(t).sqrt(),
348            distance,
349            t_guess,
350            max_iterations,
351        )
352        .y
353    }
354}