polyfit_residuals/
lib.rs

1//! Efficiently compute the residual errors for all possible polynomial models up to some degree
2//! for given data.
3//!
4//! # Example
5//! For examples please have a look at the exported functions like [residuals_from_front].
6
7// Used for array based polynomials
8#![cfg_attr(feature = "nightly-only", feature(generic_const_exprs))]
9
10pub mod poly;
11
12use ndarray::{s, Array1, Array2, ArrayView1, ArrayView2, ArrayViewMut2, Axis};
13use num_traits::real::Real;
14use poly::{NewtonPolynomial, OwnedNewtonPolynomial};
15
16use std::mem::MaybeUninit;
17
18/// The different errors that can occur during the polynomial fitting process.
19#[derive(Debug, Clone, Copy, Eq, PartialEq)]
20pub enum FitError {
21    InputsOfDifferentLengths,
22    DegreeTooHigh,
23}
24
25/// Apply a givens rotation eliminating entry [i,j] of the given array *in-place*.
26#[inline]
27fn apply_givens<R>(mut arr: ArrayViewMut2<R>, i: usize, j: usize)
28where
29    R: Real,
30{
31    let a = arr[[j, j]];
32    let b = arr[[i, j]];
33    let r = a.hypot(b);
34    let c = a / r;
35    let s = -b / r;
36    for col_idx in 0..arr.shape()[1] {
37        let new_j = c * arr[[j, col_idx]] - s * arr[[i, col_idx]];
38        let new_i = s * arr[[j, col_idx]] + c * arr[[i, col_idx]];
39        arr[[j, col_idx]] = new_j;
40        arr[[i, col_idx]] = new_i;
41    }
42}
43
44/// Generate extended system matrix.
45///
46/// # Returns
47/// Generates the extended system block matrix [[L H]; [l h]] where L is the triangular
48/// matrix corresponding to the newton polynomial evaluations, H are the corresponding
49/// right hand sides and l and h are the entries which we'll use later on to store the
50/// newton polynomial evaluations and right hand sides for each data point that's not
51/// part of the basis.
52///
53/// # Arguments
54/// * `ys` - The "output values" / right hand sides corresponding to the basis elements `base`.
55/// * `base` - The collection of the (xᵢ)ᵢ corresponding to the terms (x - xᵢ) in the Newton basis
56///     being used.
57fn generate_system_matrix<R>(ys: ArrayView1<R>, base: ArrayView1<R>) -> Array2<R>
58where
59    R: Real,
60{
61    let max_dofs = base.len();
62    // Allocate zero initialized buffer for matrix; we'll write all the nonzero entries into it.
63    let mut system_matrix = Array2::zeros((max_dofs + 1, max_dofs + 1));
64
65    // write the rhs `ys` for the basis elements into the last column
66    system_matrix
67        .slice_mut(s![..max_dofs, max_dofs])
68        .assign(&ys.slice(s![..max_dofs]));
69
70    // set degree 0 column
71    system_matrix.slice_mut(s![.., 0]).fill(R::one());
72
73    // set all the higher order basis rows
74    // compute all the necessary differences of the basis elements
75    // The subtracted base elems start at index `0` of `base` and go
76    // to `base.len() - 1`.
77    let mut diff = {
78        let b1 = base
79            .slice(s![1_usize..])
80            .to_owned()
81            .into_shape_with_order([base.len() - 1, 1])
82            .unwrap();
83        b1 - base.slice(s![..base.len() - 1])
84    };
85
86    // compute cumprod along axis 1 (the columns) to get newton poly evals
87    diff.accumulate_axis_inplace(Axis(1), |&prev, curr| *curr = *curr * prev);
88
89    // write remainder of L block into the system matrix
90    system_matrix
91        .slice_mut(s![1..max_dofs, 1..max_dofs])
92        .assign(&diff);
93    system_matrix
94}
95
96/// Compute the residual squared errors (RSS) for all polynomials of degree at most `max_deg`
97/// for the data segments `xs[j..=i]`, `ys[j..=i]` for all `i`, `j`.
98///
99/// # Returns
100/// A vector such that element `j` contains the residuals for `data[j..=i]` for all `i`
101/// in the format of [residuals_from_front].
102///
103/// # Note
104/// This function has linear time and memory complexity in the maximal degree and quadratic ones
105/// in the length of the input.
106/// For further details and an example see [residuals_from_front].
107pub fn all_residuals<'x, 'y, R>(
108    xs: impl Into<ArrayView1<'x, R>>,
109    ys: impl Into<ArrayView1<'y, R>>,
110    max_degree: usize,
111) -> Vec<Array2<R>>
112where
113    R: Real + 'x + 'y,
114{
115    let xs = xs.into();
116    let ys = ys.into();
117    let mut ret = Vec::with_capacity(xs.len());
118    for j in 0..xs.len() {
119        let max_dof_on_seg = xs.len() - j;
120        ret.push(
121            residuals_from_front(
122                xs.slice(s![j..]),
123                ys.slice(s![j..]),
124                std::cmp::min(max_dof_on_seg - 1, max_degree),
125            )
126            .unwrap(),
127        );
128    }
129    ret
130}
131
132#[cfg(feature = "parallel_rayon")]
133/// A parallel version of [all_residuals_par]. Please have a look at the sequential version for details.
134pub fn all_residuals_par<'x, 'y, R>(
135    xs: impl Into<ArrayView1<'x, R>>,
136    ys: impl Into<ArrayView1<'y, R>>,
137    max_degree: usize,
138) -> Vec<Array2<R>>
139where
140    R: Real + Send + Sync + 'x + 'y,
141{
142    use rayon::prelude::*;
143    let xs = xs.into();
144    let ys = ys.into();
145
146    let ret = (0..xs.len())
147        .into_par_iter()
148        .map(|j| {
149            let max_dof_on_seg = xs.len() - j;
150            residuals_from_front(
151                xs.slice(s![j..]),
152                ys.slice(s![j..]),
153                std::cmp::min(max_dof_on_seg - 1, max_degree),
154            )
155            .unwrap()
156        })
157        .collect();
158    ret
159}
160
161/// Compute the residual squared errors (RSS) for all polynomials of degree at most `max_deg`
162/// for the data segments `xs[0..=i]`, `ys[0..=i]` for all `i`.
163///
164/// # Returns
165/// A 2D float array where the first dimension ranges over `i` and the second one over the
166/// polynomial degree. Note that the array will contain 0 at underdetermined indices (for
167/// example when fitting a degree 3 polynomial to `data[0..=1]`; with only 2 data point we
168/// can't determine 4 unique polynomial coefficients but we can guarantee the error to vanish).
169///
170/// # Arguments
171/// * `xs` - The "inputs values".
172/// * `ys` - The "output values" corresponding to the `xs`.
173/// * `max_degree` - The maximal polynomial degree we wanna calculate the residual errors for.
174///
175/// # Note
176/// This function has linear time and memory complexity in the length of the input and the
177/// maximal degree.
178/// We consider constant polynomials to have degree 0.
179///
180/// # Example
181/// ```
182/// use approx::assert_abs_diff_eq;
183/// use ndarray::{arr1, Array2};
184/// use polyfit_residuals::residuals_from_front;
185///
186/// let xs = arr1(&[1., 2., 3., 4., 5.]);
187/// let ys = arr1(&[2., 4., 6., 8., 10.]);
188/// // We want to fit polynomials with degree <= 2
189/// let residuals: Array2<f64> = residuals_from_front(xs.view(), ys.view(), 2).unwrap();
190/// // since ys = 2 * xs we expect the linear error to vanish on for example data[0..=2]
191/// assert_abs_diff_eq!(residuals[[2, 1]], 0.0)
192/// ```
193pub fn residuals_from_front<R>(
194    xs: ArrayView1<R>,
195    ys: ArrayView1<R>,
196    max_degree: usize,
197) -> Result<Array2<R>, FitError>
198where
199    R: Real,
200{
201    if xs.len() != ys.len() {
202        return Err(FitError::InputsOfDifferentLengths);
203    }
204    let data_len = xs.len();
205
206    let max_dofs = max_degree + 1;
207    if max_dofs > data_len {
208        return Err(FitError::DegreeTooHigh);
209    }
210
211    // select one base point for each degree of freedom
212    let base = xs.slice(s![..max_dofs]).to_owned();
213    let mut system_matrix = generate_system_matrix(ys, base.view());
214    // first axis is rb, second is deg such that on data[0..=rb] the polynomial of degree deg
215    // has the residual error at this index
216    let mut residuals: Array2<R> = Array2::zeros([data_len, max_dofs]);
217
218    for (i, mut row) in residuals
219        .slice_mut(s![..max_dofs, ..])
220        .rows_mut()
221        .into_iter()
222        .enumerate()
223    {
224        row[i] = R::zero();
225    }
226
227    let last_row_idx = system_matrix.shape()[0] - 1;
228    let last_col_idx = system_matrix.shape()[1] - 1;
229
230    // eliminate base mat downwards such that we can start our later eliminations at deg 0
231    for base_idx in 1..max_dofs {
232        // at base_idx i there's the highest nonzero deg term is i
233        for deg in 0..base_idx {
234            apply_givens(system_matrix.view_mut(), base_idx, deg);
235            residuals[[base_idx, deg]] =
236                system_matrix[[base_idx, last_col_idx]].powi(2) + residuals[[base_idx - 1, deg]];
237        }
238    }
239
240    // repeat procedure we already used on the basis entries: eliminate higher and higher
241    // orders for each data point in turn while accumulating the residuals along the way
242    for (i, (x, y)) in xs
243        .into_iter()
244        .zip(ys.into_iter())
245        .enumerate()
246        .skip(max_dofs)
247    {
248        // write newton poly evaluation at x into last row of system matrix
249        system_matrix[[last_row_idx, 0]] = R::one();
250        for col in 1..last_col_idx {
251            system_matrix[[last_row_idx, col]] =
252                system_matrix[[last_row_idx, col - 1]] * (*x - base[col - 1]);
253        }
254        // write y into last column of last row of system matrix
255        system_matrix[[last_row_idx, last_col_idx]] = *y;
256        // eliminate the complete row we just added back to zeros and note down the residuals
257        // computed along the way
258        for deg in 0..max_dofs {
259            apply_givens(system_matrix.view_mut(), last_row_idx, deg);
260            residuals[[i, deg]] =
261                system_matrix[[last_row_idx, last_col_idx]].powi(2) + residuals[[i - 1, deg]];
262        }
263    }
264    Ok(residuals)
265}
266
267pub mod weighted {
268    //! Provides versions of the main functions for the case of
269    //! a weighted least squares fit. So we calculate the optimal
270    //! target values of
271    //! min_{p polynomial of deg d} ∑ᵢ wᵢ(p(xᵢ) - yᵢ)²
272    //! for all d and valid discrete intervals of i.
273    use super::*;
274    use itertools::izip;
275
276    /// Compute the weighted residual squared errors for all polynomials of degree at most `max_deg`
277    /// for the data segments `xs[0..=i]`, `ys[0..=i]` for all `i`.
278    ///
279    /// # Returns
280    /// A 2D float array where the first dimension ranges over `i` and the second one over the
281    /// polynomial degree. Note that the array will contain 0 at underdetermined indices (for
282    /// example when fitting a degree 3 polynomial to `data[0..=1]`; with only 2 data point we
283    /// can't determine 4 unique polynomial coefficients but we can guarantee the error to vanish).
284    ///
285    /// # Arguments
286    /// * `xs` - The "inputs values".
287    /// * `ys` - The "output values" corresponding to the `xs`.
288    /// * `max_degree` - The maximal polynomial degree we wanna calculate the residual errors for.
289    /// * `weights` - The weights associated to the measurements.
290    ///
291    /// # Note
292    /// This function has linear time and memory complexity in the length of the input and the
293    /// maximal degree.
294    /// We consider constant polynomials to have degree 0.
295    ///
296    /// # Example
297    /// ```
298    /// use approx::assert_abs_diff_eq;
299    /// use ndarray::{arr1, Array2};
300    /// use polyfit_residuals::weighted::residuals_from_front;
301    ///
302    /// let xs = arr1(&[1., 2., 3., 4., 5.]);
303    /// let ys = arr1(&[2., 4., 6., 8., 10.]);
304    /// let weights = arr1(&[1.5, 1., 1., 0.8, 0.8]).map(|x| x * x);
305    /// // We want to fit polynomials with degree <= 2
306    /// let residuals: Array2<f64> = residuals_from_front(xs.view(), ys.view(), 2, weights.view()).unwrap();
307    /// // since ys = 2 * xs we expect the linear error to vanish on for example data[0..=2]
308    /// assert_abs_diff_eq!(residuals[[2, 1]], 0.0)
309    /// ```
310    pub fn residuals_from_front<R>(
311        xs: ArrayView1<R>,
312        ys: ArrayView1<R>,
313        max_degree: usize,
314        weights: ArrayView1<R>,
315    ) -> Result<Array2<R>, FitError>
316    where
317        R: Real,
318    {
319        if xs.len() != ys.len() || xs.len() != weights.len() {
320            return Err(FitError::InputsOfDifferentLengths);
321        }
322        let data_len = xs.len();
323
324        let max_dofs = max_degree + 1;
325        if max_dofs > data_len {
326            return Err(FitError::DegreeTooHigh);
327        }
328
329        // select one base point for each degree of freedom
330        let base = xs.slice(s![..max_dofs]).to_owned();
331        let mut system_matrix = generate_system_matrix(ys, base.view());
332
333        // apply weights to each row of the system matrix
334        for (mut row, &weight) in system_matrix.rows_mut().into_iter().zip(weights) {
335            for j in 0..=max_dofs {
336                row[j] = weight.sqrt() * row[j];
337            }
338        }
339
340        // first axis is rb, second is deg such that on data[0..=rb] the polynomial of degree deg
341        // has the residual error at this index
342        let mut residuals: Array2<R> = Array2::zeros([data_len, max_dofs]);
343
344        for (i, mut row) in residuals
345            .slice_mut(s![..max_dofs, ..])
346            .rows_mut()
347            .into_iter()
348            .enumerate()
349        {
350            row[i] = R::zero();
351        }
352
353        let last_row_idx = system_matrix.shape()[0] - 1;
354        let last_col_idx = system_matrix.shape()[1] - 1;
355
356        // eliminate base mat downwards such that we can start our later eliminations at deg 0
357        for base_idx in 1..max_dofs {
358            // at base_idx i there's the highest nonzero deg term is i
359            for deg in 0..base_idx {
360                apply_givens(system_matrix.view_mut(), base_idx, deg);
361                residuals[[base_idx, deg]] = system_matrix[[base_idx, last_col_idx]].powi(2)
362                    + residuals[[base_idx - 1, deg]];
363            }
364        }
365
366        // repeat procedure we already used on the basis entries: eliminate higher and higher
367        // orders for each data point in turn while accumulating the residuals along the way
368        for (i, (&x, &y, &weight)) in izip!(xs, ys, weights).enumerate().skip(max_dofs) {
369            // write weighted newton poly evaluation at x into last row of system matrix
370            system_matrix[[last_row_idx, 0]] = weight.sqrt();
371            for col in 1..last_col_idx {
372                system_matrix[[last_row_idx, col]] =
373                    system_matrix[[last_row_idx, col - 1]] * (x - base[col - 1]);
374            }
375            // write weighted y into last column of last row of system matrix
376            system_matrix[[last_row_idx, last_col_idx]] = y * weight.sqrt();
377            // eliminate the complete row we just added back to zeros and note down the residuals
378            // computed along the way
379            for deg in 0..max_dofs {
380                apply_givens(system_matrix.view_mut(), last_row_idx, deg);
381                residuals[[i, deg]] =
382                    system_matrix[[last_row_idx, last_col_idx]].powi(2) + residuals[[i - 1, deg]];
383            }
384        }
385        Ok(residuals)
386    }
387
388    /// Compute the weighted residual squared errors (RSS) for all polynomials of degree at most `max_deg`
389    /// for the data segments `xs[j..=i]`, `ys[j..=i]` for all `i`, `j`.
390    ///
391    /// # Returns
392    /// A vector such that element `j` contains the residuals for `data[j..=i]` for all `i`
393    /// in the format of [residuals_from_front].
394    ///
395    /// # Note
396    /// This function has linear time and memory complexity in the maximal degree and quadratic ones
397    /// in the length of the input.
398    /// For further details and an example see [residuals_from_front].
399    pub fn all_residuals<'x, 'y, 'w, R>(
400        xs: impl Into<ArrayView1<'x, R>>,
401        ys: impl Into<ArrayView1<'y, R>>,
402        max_degree: usize,
403        weights: impl Into<ArrayView1<'w, R>>,
404    ) -> Vec<Array2<R>>
405    where
406        R: Real + 'x + 'y + 'w,
407    {
408        let xs = xs.into();
409        let ys = ys.into();
410        let weights = weights.into();
411        let mut ret = Vec::with_capacity(xs.len());
412        for j in 0..xs.len() {
413            let max_dof_on_seg = xs.len() - j;
414            ret.push(
415                residuals_from_front(
416                    xs.slice(s![j..]),
417                    ys.slice(s![j..]),
418                    std::cmp::min(max_dof_on_seg - 1, max_degree),
419                    weights.slice(s![j..]),
420                )
421                .unwrap(),
422            );
423        }
424        ret
425    }
426
427    #[cfg(feature = "parallel_rayon")]
428    /// A parallel version of [all_residuals_par]. Please have a look at the sequential version for details.
429    pub fn all_residuals_par<'x, 'y, 'w, R>(
430        xs: impl Into<ArrayView1<'x, R>>,
431        ys: impl Into<ArrayView1<'y, R>>,
432        max_degree: usize,
433        weights: impl Into<ArrayView1<'w, R>>,
434    ) -> Vec<Array2<R>>
435    where
436        R: Real + Send + Sync + 'x + 'y + 'w,
437    {
438        use rayon::prelude::*;
439        let xs = xs.into();
440        let ys = ys.into();
441        let weights = weights.into();
442
443        let ret = (0..xs.len())
444            .into_par_iter()
445            .map(|j| {
446                let max_dof_on_seg = xs.len() - j;
447                residuals_from_front(
448                    xs.slice(s![j..]),
449                    ys.slice(s![j..]),
450                    std::cmp::min(max_dof_on_seg - 1, max_degree),
451                    weights.slice(s![j..]),
452                )
453                .unwrap()
454            })
455            .collect();
456        ret
457    }
458
459    /// Try fitting a polynomial to some data and also compute the residual error.
460    ///
461    /// # Examples
462    /// Fit a linear polynomial to some data
463    /// ```
464    /// use polyfit_residuals::{weighted::try_fit_poly_with_residual, PolyFit};
465    /// use approx::assert_abs_diff_eq;
466    /// use ndarray::arr1;
467    ///
468    /// let xs = arr1(&[1., 2., 3., 4.]);
469    /// let ys = arr1(&[2., 3., 4., 5.]);
470    /// let ws = arr1(&[0.1, 0.1, 0.2, 0.6]).map(|x| x * x);
471    /// let PolyFit { polynomial: poly, residual } =
472    ///     try_fit_poly_with_residual(xs.view(), ys.view(), 1, ws.view()).expect("Failed to fit linear polynomial to data");
473    /// // Note that the returned polynomial is given as P(x) = 2 + (x-1) where the 1 is the first
474    /// // element of `xs`
475    /// assert_abs_diff_eq!(residual, 1.11703937e-31);
476    /// assert_abs_diff_eq!(poly.left_eval(1.), 2., epsilon = 1e-12);
477    /// assert_abs_diff_eq!(poly.left_eval(2.), 3., epsilon = 1e-12);
478    /// assert_abs_diff_eq!(poly.left_eval(3.), 4., epsilon = 1e-12);
479    /// assert_abs_diff_eq!(poly.left_eval(4.), 5., epsilon = 1e-12);
480    /// ```
481    ///
482    /// Fit a cubic polynomial to some data
483    /// ```
484    /// use polyfit_residuals::{weighted::try_fit_poly_with_residual, PolyFit};
485    /// use approx::assert_abs_diff_eq;
486    /// use ndarray::arr1;
487    ///
488    /// let xs = arr1(&[0.        , 0.11111111, 0.22222222, 0.33333333, 0.44444444,
489    ///     0.55555556, 0.66666667, 0.77777778, 0.88888889, 1.        ]);
490    /// let ys = arr1(&[-4.99719342, -4.76675941, -4.57502219, -4.33219826, -4.14968982,
491    ///     -3.98840112, -3.91026478, -3.83464713, -3.93700013, -4.00937516]);
492    /// let ws = arr1(&[1., 2., 3., 1., 2., 3., 0.5, 0.7, 0.6, 3.]).map(|x| x * x);
493    /// let PolyFit { polynomial: poly, residual } =
494    ///     try_fit_poly_with_residual(xs.view(), ys.view(), 3, ws.view()).expect("Failed to fit cubic polynomial to data");
495    /// assert_abs_diff_eq!(residual, 0.00414158, epsilon = 1e-6);
496    /// assert_abs_diff_eq!(poly.left_eval(2.), -12.427840764131307, epsilon = 1e-6);
497    /// ```
498    pub fn try_fit_poly_with_residual<R>(
499        xs: ArrayView1<R>,
500        ys: ArrayView1<R>,
501        degree: usize,
502        weights: ArrayView1<R>,
503    ) -> Result<PolyFit<R, R>, FitError>
504    where
505        R: Real + 'static,
506    {
507        if xs.len() != ys.len() || xs.len() != weights.len() {
508            return Err(FitError::InputsOfDifferentLengths);
509        }
510        let data_len = xs.len();
511
512        let max_dofs = degree + 1;
513        if max_dofs > data_len {
514            return Err(FitError::DegreeTooHigh);
515        }
516
517        // select one base point for each degree of freedom
518        let base = xs.slice(s![..max_dofs]).to_owned();
519        let mut system_matrix = generate_system_matrix(ys, base.view());
520
521        // apply weights to each row of the system matrix
522        for (mut row, &weight) in system_matrix.rows_mut().into_iter().zip(weights) {
523            for j in 0..=max_dofs {
524                row[j] = weight.sqrt() * row[j];
525            }
526        }
527
528        // first axis is rb, second is deg such that on data[0..=rb] the polynomial of degree deg
529        // has the residual error at this index
530
531        let last_row_idx = system_matrix.shape()[0] - 1;
532        let last_col_idx = system_matrix.shape()[1] - 1;
533
534        // TODO: this isn't necessary if we don't wanna compute all the intermediary residuals
535        // so we could omit this step
536        // eliminate base mat downwards such that we can start our later eliminations at deg 0
537        for base_idx in 1..max_dofs {
538            // at base_idx i there's the highest nonzero deg term is i
539            for deg in 0..base_idx {
540                apply_givens(system_matrix.view_mut(), base_idx, deg);
541            }
542        }
543
544        let mut residual = R::zero();
545
546        // repeat procedure we already used on the basis entries: eliminate higher and higher
547        // orders for each data point in turn while accumulating the residuals along the way
548        for (&x, &y, &weight) in izip!(xs, ys, weights).skip(max_dofs) {
549            // write weighted newton poly evaluation at x into last row of system matrix
550            system_matrix[[last_row_idx, 0]] = weight.sqrt();
551            for col in 1..last_col_idx {
552                system_matrix[[last_row_idx, col]] =
553                    system_matrix[[last_row_idx, col - 1]] * (x - base[col - 1]);
554            }
555            // write weighted y into last column of last row of system matrix
556            system_matrix[[last_row_idx, last_col_idx]] = y * weight.sqrt();
557            // eliminate the complete row we just added back to zeros
558            for deg in 0..max_dofs {
559                apply_givens(system_matrix.view_mut(), last_row_idx, deg);
560            }
561            residual = residual + system_matrix[[last_row_idx, last_col_idx]].powi(2);
562        }
563
564        // find coeffs by solving first few rows of matrix
565        let coeffs = solve_upper_triangular_system(
566            system_matrix.slice(s![..last_row_idx, ..last_col_idx]),
567            system_matrix.slice(s![..last_row_idx, last_col_idx]),
568        );
569        Ok(PolyFit {
570            polynomial: NewtonPolynomial::new(
571                coeffs.to_vec(),
572                base.slice(s![0..base.len() - 1]).to_vec(),
573            ),
574            residual,
575        })
576    }
577
578    /// Try fitting a polynomial to some data.
579    ///
580    /// # Examples
581    /// Fit a linear polynomial to some data
582    /// ```
583    /// use polyfit_residuals::try_fit_poly;
584    /// use approx::assert_abs_diff_eq;
585    /// use ndarray::arr1;
586    ///
587    /// let xs = arr1(&[1., 2., 3., 4.]);
588    /// let ys = arr1(&[2., 3., 4., 5.]);
589    /// let poly =
590    ///     try_fit_poly(xs.view(), ys.view(), 1).expect("Failed to fit linear polynomial to data");
591    /// // Note that the returned polynomial is given as P(x) = 2 + (x-1) where the 1 is the first
592    /// // element of `xs`
593    /// assert_abs_diff_eq!(poly.left_eval(1.), 2., epsilon = 1e-12);
594    /// assert_abs_diff_eq!(poly.left_eval(2.), 3., epsilon = 1e-12);
595    /// assert_abs_diff_eq!(poly.left_eval(3.), 4., epsilon = 1e-12);
596    /// assert_abs_diff_eq!(poly.left_eval(4.), 5., epsilon = 1e-12);
597    /// ```
598    pub fn try_fit_poly<R>(
599        xs: ArrayView1<R>,
600        ys: ArrayView1<R>,
601        degree: usize,
602        weights: ArrayView1<R>,
603    ) -> Result<OwnedNewtonPolynomial<R, R>, FitError>
604    where
605        R: Real + 'static,
606    {
607        try_fit_poly_with_residual(xs, ys, degree, weights).map(|polyfit| polyfit.polynomial)
608    }
609
610    #[cfg(test)]
611    mod tests {
612        use approx::assert_abs_diff_eq;
613        use ndarray::{arr1, arr2};
614
615        use super::*;
616
617        #[test]
618        fn weights_one() {
619            let xs = arr1(&[1., 2., 3., 4., 5., 6., 7., 8., 9., 10.]);
620            let ys = arr1(&[3., 8., 15., 24., 35., 48., 63., 80., 99., 120.]);
621            let ws = arr1(&[1., 1., 1., 1., 1., 1., 1., 1., 1., 1.]);
622            let our_sol: Array2<f64> =
623                residuals_from_front(xs.view(), ys.view(), 3, ws.view()).unwrap();
624            let correct_sol: Array2<f64> = arr2(&[
625                [
626                    0.00000000e+00,
627                    0.00000000e+00,
628                    0.00000000e+00,
629                    0.00000000e+00,
630                ],
631                [
632                    1.25000000e+01,
633                    0.00000000e+00,
634                    0.00000000e+00,
635                    0.00000000e+00,
636                ],
637                [
638                    7.26666667e+01,
639                    6.66666667e-01,
640                    0.00000000e+00,
641                    0.00000000e+00,
642                ],
643                [
644                    2.49000000e+02,
645                    4.00000000e+00,
646                    1.10933565e-31,
647                    0.00000000e+00,
648                ],
649                [
650                    6.54000000e+02,
651                    1.40000000e+01,
652                    1.60237371e-31,
653                    1.52146949e-31,
654                ],
655                [
656                    1.45483333e+03,
657                    3.73333333e+01,
658                    7.25998552e-30,
659                    1.53688367e-30,
660                ],
661                [
662                    2.88400000e+03,
663                    8.40000000e+01,
664                    7.25998552e-30,
665                    5.54305496e-30,
666                ],
667                [
668                    5.25000000e+03,
669                    1.68000000e+02,
670                    1.04154291e-29,
671                    5.54372640e-30,
672                ],
673                [
674                    8.94800000e+03,
675                    3.08000000e+02,
676                    3.88144217e-29,
677                    3.18162402e-29,
678                ],
679                [
680                    1.44705000e+04,
681                    5.28000000e+02,
682                    2.94405355e-28,
683                    1.11382159e-28,
684                ],
685            ]);
686            assert_abs_diff_eq!(&our_sol, &correct_sol, epsilon = 1e-5);
687        }
688
689        #[test]
690        fn tiny_example() {
691            let xs = arr1(&[1., 2.]);
692            let ys = arr1(&[2.1, 4.2]);
693            let ws = arr1(&[1., 2.]).map(|x| x * x);
694            let correct_sol: Array2<f64> = arr2(&[[0.00000000e+00], [3.528]]);
695            let our_sol: Array2<f64> =
696                residuals_from_front(xs.view(), ys.view(), 0, ws.view()).unwrap();
697
698            assert_abs_diff_eq!(&our_sol, &correct_sol, epsilon = 1e-12);
699        }
700
701        #[test]
702        fn small_example() {
703            let xs = arr1(&[1., 2., 2.5, 3.]);
704            let ys = arr1(&[2.1, 4.2, 1.9, 4.]);
705            let ws = arr1(&[1., 2., 1., 0.8]).map(|x| x * x);
706
707            let correct_sol: Array2<f64> = arr2(&[
708                [0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
709                [3.52800000e+00, 0.00000000e+00, 0.00000000e+00],
710                [6.47333333e+00, 6.19172414e+00, 0.00000000e+00],
711                [6.63783133e+00, 6.19176377e+00, 4.49691980e+00],
712            ]);
713            let our_sol: Array2<f64> =
714                residuals_from_front(xs.view(), ys.view(), 2, ws.view()).unwrap();
715
716            assert_abs_diff_eq!(&our_sol, &correct_sol, epsilon = 1e-8);
717        }
718    }
719}
720
721/// Solves the linear system `matrix_product(lhs, x) = rhs` for `x`.
722///
723/// # Returns
724/// The solution vector.
725///
726/// # Arguments
727/// * `lhs` is a nonsingular upper triangular matrix.
728/// * `rhs` a vector with length matching `lhs`'s columns.
729///
730/// # Examples
731/// ```
732/// use approx::assert_abs_diff_eq;
733/// use ndarray::{arr1, arr2};
734/// use polyfit_residuals::solve_upper_triangular_system;
735///
736/// let lhs = arr2(&[[0.5, 0.5, 0.5], [0., 0.25, 0.25], [0., 0., 0.125]]);
737/// let rhs = arr1(&[1., 1., 1.]);
738/// let correct_sol = arr1(&[-2., -4., 8.]);
739/// let our_sol = solve_upper_triangular_system(lhs.view(), rhs.view());
740///
741/// assert_abs_diff_eq!(&our_sol, &correct_sol, epsilon = 1e-12);
742/// ```
743///
744/// # Safety
745/// Nonsingularity of the lhs isn't checked - the behaviour for singular matrices is
746/// safe but undefined.
747///
748/// # Note
749/// This is provided more as a convenience and not optimized.
750pub fn solve_upper_triangular_system<R>(lhs: ArrayView2<R>, rhs: ArrayView1<R>) -> Array1<R>
751where
752    R: Real + 'static,
753{
754    assert!(lhs.is_square());
755    assert_eq!(lhs.shape()[1], rhs.shape()[0]);
756    let row_count = rhs.shape()[0];
757    let mut sol = Array1::uninit(row_count);
758    for i in (0..row_count).rev() {
759        let already_solved = unsafe { sol.slice(s![i + 1..]).assume_init() };
760        let ax = lhs.slice(s![i, i + 1..]).dot(&already_solved);
761        sol[i] = MaybeUninit::new((rhs[i] - ax) / lhs[[i, i]]);
762    }
763    unsafe { sol.assume_init() }
764}
765
766/// A fit polynomial together with its residual error
767pub struct PolyFit<R, E> {
768    pub polynomial: OwnedNewtonPolynomial<R, R>,
769    pub residual: E,
770}
771
772/// Try fitting a polynomial to some data and also compute the residual error.
773///
774/// # Examples
775/// Fit a linear polynomial to some data
776/// ```
777/// use polyfit_residuals::{try_fit_poly_with_residual, PolyFit};
778/// use approx::assert_abs_diff_eq;
779/// use ndarray::arr1;
780///
781/// let xs = arr1(&[1., 2., 3., 4.]);
782/// let ys = arr1(&[2., 3., 4., 5.]);
783/// let PolyFit { polynomial: poly, residual } =
784///     try_fit_poly_with_residual(xs.view(), ys.view(), 1).expect("Failed to fit linear polynomial to data");
785/// // Note that the returned polynomial is given as P(x) = 2 + (x-1) where the 1 is the first
786/// // element of `xs`
787/// assert_abs_diff_eq!(residual, 0.);
788/// assert_abs_diff_eq!(poly.left_eval(1.), 2., epsilon = 1e-12);
789/// assert_abs_diff_eq!(poly.left_eval(2.), 3., epsilon = 1e-12);
790/// assert_abs_diff_eq!(poly.left_eval(3.), 4., epsilon = 1e-12);
791/// assert_abs_diff_eq!(poly.left_eval(4.), 5., epsilon = 1e-12);
792/// ```
793///
794/// Fit a cubic polynomial to some data
795/// ```
796/// use polyfit_residuals::{try_fit_poly_with_residual, PolyFit};
797/// use approx::assert_abs_diff_eq;
798/// use ndarray::arr1;
799///
800/// let xs = arr1(&[0.        , 0.11111111, 0.22222222, 0.33333333, 0.44444444,
801///     0.55555556, 0.66666667, 0.77777778, 0.88888889, 1.        ]);
802/// let ys = arr1(&[-4.99719342, -4.76675941, -4.57502219, -4.33219826, -4.14968982,
803///     -3.98840112, -3.91026478, -3.83464713, -3.93700013, -4.00937516]);
804/// let PolyFit { polynomial: poly, residual } =
805///     try_fit_poly_with_residual(xs.view(), ys.view(), 3).expect("Failed to fit cubic polynomial to data");
806/// assert_abs_diff_eq!(residual, 0.00334143, epsilon = 1e-6);
807/// assert_abs_diff_eq!(poly.left_eval(2.), -11.28209083, epsilon = 1e-6);
808/// ```
809pub fn try_fit_poly_with_residual<R>(
810    xs: ArrayView1<R>,
811    ys: ArrayView1<R>,
812    degree: usize,
813) -> Result<PolyFit<R, R>, FitError>
814where
815    R: Real + 'static,
816{
817    if xs.len() != ys.len() {
818        return Err(FitError::InputsOfDifferentLengths);
819    }
820    let data_len = xs.len();
821
822    let max_dofs = degree + 1;
823    if max_dofs > data_len {
824        return Err(FitError::DegreeTooHigh);
825    }
826
827    // select one base point for each degree of freedom
828    let base = xs.slice(s![..max_dofs]).to_owned();
829    let mut system_matrix = generate_system_matrix(ys, base.view());
830    // first axis is rb, second is deg such that on data[0..=rb] the polynomial of degree deg
831    // has the residual error at this index
832
833    let last_row_idx = system_matrix.shape()[0] - 1;
834    let last_col_idx = system_matrix.shape()[1] - 1;
835
836    // TODO: this isn't necessary if we don't wanna compute all the intermediary residuals
837    // so we could omit this step
838    // eliminate base mat downwards such that we can start our later eliminations at deg 0
839    for base_idx in 1..max_dofs {
840        // at base_idx i there's the highest nonzero deg term is i
841        for deg in 0..base_idx {
842            apply_givens(system_matrix.view_mut(), base_idx, deg);
843        }
844    }
845
846    let mut residual = R::zero();
847
848    // repeat procedure we already used on the basis entries: eliminate higher and higher
849    // orders for each data point in turn while accumulating the residuals along the way
850    for (x, y) in xs.into_iter().zip(ys.into_iter()).skip(max_dofs) {
851        // write newton poly evaluation at x into last row of system matrix
852        system_matrix[[last_row_idx, 0]] = R::one();
853        for col in 1..last_col_idx {
854            system_matrix[[last_row_idx, col]] =
855                system_matrix[[last_row_idx, col - 1]] * (*x - base[col - 1]);
856        }
857        // write y into last column of last row of system matrix
858        system_matrix[[last_row_idx, last_col_idx]] = *y;
859        // eliminate the complete row we just added back to zeros
860        for deg in 0..max_dofs {
861            apply_givens(system_matrix.view_mut(), last_row_idx, deg);
862        }
863        residual = residual + system_matrix[[last_row_idx, last_col_idx]].powi(2);
864    }
865    // find coeffs by solving first few rows of matrix
866    let coeffs = solve_upper_triangular_system(
867        system_matrix.slice(s![..last_row_idx, ..last_col_idx]),
868        system_matrix.slice(s![..last_row_idx, last_col_idx]),
869    );
870    Ok(PolyFit {
871        polynomial: NewtonPolynomial::new(
872            coeffs.to_vec(),
873            base.slice(s![0..base.len() - 1]).to_vec(),
874        ),
875        residual,
876    })
877}
878
879/// Try fitting a polynomial to some data.
880///
881/// # Examples
882/// Fit a linear polynomial to some data
883/// ```
884/// use polyfit_residuals::try_fit_poly;
885/// use approx::assert_abs_diff_eq;
886/// use ndarray::arr1;
887///
888/// let xs = arr1(&[1., 2., 3., 4.]);
889/// let ys = arr1(&[2., 3., 4., 5.]);
890/// let poly =
891///     try_fit_poly(xs.view(), ys.view(), 1).expect("Failed to fit linear polynomial to data");
892/// // Note that the returned polynomial is given as P(x) = 2 + (x-1) where the 1 is the first
893/// // element of `xs`
894/// assert_abs_diff_eq!(poly.left_eval(1.), 2., epsilon = 1e-12);
895/// assert_abs_diff_eq!(poly.left_eval(2.), 3., epsilon = 1e-12);
896/// assert_abs_diff_eq!(poly.left_eval(3.), 4., epsilon = 1e-12);
897/// assert_abs_diff_eq!(poly.left_eval(4.), 5., epsilon = 1e-12);
898/// ```
899pub fn try_fit_poly<R>(
900    xs: ArrayView1<R>,
901    ys: ArrayView1<R>,
902    degree: usize,
903) -> Result<OwnedNewtonPolynomial<R, R>, FitError>
904where
905    R: Real + 'static,
906{
907    try_fit_poly_with_residual(xs, ys, degree).map(|polyfit| polyfit.polynomial)
908}
909
910#[cfg(test)]
911mod tests {
912    use approx::assert_abs_diff_eq;
913    use ndarray::{arr1, arr2, Array1};
914
915    use super::*;
916
917    #[test]
918    fn big_example() {
919        let xs = arr1(&[1., 2., 3., 4., 5., 6., 7., 8., 9., 10.]);
920        let ys = arr1(&[3., 8., 15., 24., 35., 48., 63., 80., 99., 120.]);
921        let our_sol: Array2<f64> = residuals_from_front(xs.view(), ys.view(), 3).unwrap();
922        let correct_sol: Array2<f64> = arr2(&[
923            [
924                0.00000000e+00,
925                0.00000000e+00,
926                0.00000000e+00,
927                0.00000000e+00,
928            ],
929            [
930                1.25000000e+01,
931                0.00000000e+00,
932                0.00000000e+00,
933                0.00000000e+00,
934            ],
935            [
936                7.26666667e+01,
937                6.66666667e-01,
938                0.00000000e+00,
939                0.00000000e+00,
940            ],
941            [
942                2.49000000e+02,
943                4.00000000e+00,
944                1.10933565e-31,
945                0.00000000e+00,
946            ],
947            [
948                6.54000000e+02,
949                1.40000000e+01,
950                1.60237371e-31,
951                1.52146949e-31,
952            ],
953            [
954                1.45483333e+03,
955                3.73333333e+01,
956                7.25998552e-30,
957                1.53688367e-30,
958            ],
959            [
960                2.88400000e+03,
961                8.40000000e+01,
962                7.25998552e-30,
963                5.54305496e-30,
964            ],
965            [
966                5.25000000e+03,
967                1.68000000e+02,
968                1.04154291e-29,
969                5.54372640e-30,
970            ],
971            [
972                8.94800000e+03,
973                3.08000000e+02,
974                3.88144217e-29,
975                3.18162402e-29,
976            ],
977            [
978                1.44705000e+04,
979                5.28000000e+02,
980                2.94405355e-28,
981                1.11382159e-28,
982            ],
983        ]);
984        assert_abs_diff_eq!(&our_sol, &correct_sol, epsilon = 1e-5);
985    }
986
987    #[test]
988    fn small_example() {
989        let xs = arr1(&[1., 2., 3., 4., 5.]);
990        let ys = arr1(&[2., 4., 6., 8., 10.]);
991        let correct_sol: Array2<f64> = arr2(&[
992            [0.00000000e+00, 0.00000000e+00, 0.00000000e+00],
993            [2.00000000e+00, 0.00000000e+00, 0.00000000e+00],
994            [8.00000000e+00, 3.74256039e-31, 0.00000000e+00],
995            [2.00000000e+01, 1.42772768e-30, 1.53273315e-30],
996            [4.00000000e+01, 1.03537994e-30, 1.91026599e-31],
997        ]);
998        let our_sol: Array2<f64> = residuals_from_front(xs.view(), ys.view(), 2).unwrap();
999
1000        assert_abs_diff_eq!(&our_sol, &correct_sol, epsilon = 1e-12);
1001    }
1002
1003    #[test]
1004    fn too_many_dofs() {
1005        let xs = arr1(&[1., 2.]);
1006        let ys = arr1(&[2., 4.]);
1007        assert_eq!(
1008            residuals_from_front(xs.view(), ys.view(), 2),
1009            Err(FitError::DegreeTooHigh)
1010        );
1011    }
1012
1013    #[test]
1014    fn different_lengths() {
1015        let xs = arr1(&[1., 2.]);
1016        let ys = arr1(&[2., 4., 6.]);
1017        assert_eq!(
1018            residuals_from_front(xs.view(), ys.view(), 2),
1019            Err(FitError::InputsOfDifferentLengths)
1020        );
1021    }
1022
1023    #[test]
1024    fn empty_input() {
1025        let xs: Array1<f64> = arr1(&[]);
1026        let ys = arr1(&[]);
1027        assert_eq!(
1028            residuals_from_front(xs.view(), ys.view(), 2),
1029            Err(FitError::DegreeTooHigh)
1030        );
1031    }
1032
1033    #[test]
1034    fn all_residuals_shapes() {
1035        let xs = arr1(&[1., 2., 3., 4.]);
1036        let ys = arr1(&[2., 4., 6., 8.]);
1037        let sol = all_residuals(xs.view(), ys.view(), 2);
1038        assert_eq!(sol.len(), 4);
1039        assert_eq!(sol[0].shape(), [4, 3]);
1040        assert_eq!(sol[1].shape(), [3, 3]);
1041        assert_eq!(sol[2].shape(), [2, 2]);
1042        assert_eq!(sol[3].shape(), [1, 1]);
1043    }
1044
1045    #[test]
1046    fn solve_diag() {
1047        let lhs = arr2(&[[0.5, 0., 0.], [0., 0.25, 0.], [0., 0., 0.125]]);
1048        let rhs = arr1(&[1., 1., 1.]);
1049        let correct_sol = arr1(&[2., 4., 8.]);
1050        let our_sol = solve_upper_triangular_system(lhs.view(), rhs.view());
1051
1052        assert_abs_diff_eq!(&our_sol, &correct_sol, epsilon = 1e-12);
1053    }
1054
1055    #[test]
1056    fn all_residuals_values() {
1057        let xs = arr1(&[0., 1., 2., 3., 4., 5., 6.]);
1058        let ys = arr1(&[8., 9., 10., 1., 4., 9., 16.]);
1059        let sol = all_residuals(&xs, &ys, 5);
1060
1061        let correct_sol_0 = arr2(&[
1062            [0., 0., 0., 0., 0., 0.],
1063            [0.5000000000000001, 0., 0., 0., 0., 0.],
1064            [2.0000000000000004, 1.97215226e-31, 0., 0., 0., 0.],
1065            [50.00000000000001, 30., 4.999999999999998, 0., 0., 0.],
1066            [
1067                57.20000000000001,
1068                31.6,
1069                29.028571428571432,
1070                14.628571428571428,
1071                0.,
1072                0.,
1073            ],
1074            [
1075                62.83333333333332,
1076                57.67619048,
1077                48.34285714285713,
1078                16.2539682539682,
1079                16.253968253968257,
1080                0.,
1081            ],
1082            [
1083                134.85714285714286,
1084                123.28571429,
1085                58.09523809523813,
1086                25.42857142857143,
1087                17.922077922077925,
1088                12.160173160173189,
1089            ],
1090        ]);
1091        println!("{:25e}", &sol[0] - &correct_sol_0);
1092        assert_abs_diff_eq!(&sol[0], &correct_sol_0, epsilon = 1e-8);
1093
1094        let correct_sol_1 = arr2(&[
1095            [0., 0., 0., 0., 0., 0.],
1096            [0.5000000000000001, 0., 0., 0., 0., 0.],
1097            [48.66666666666667, 16.666666666666647, 0., 0., 0., 0.],
1098            [54.0, 25.199999999999996, 24.199999999999996, 0., 0., 0.],
1099            [61.2, 57.6, 29.02857142857143, 14.628571428571435, 0., 0.],
1100            [
1101                134.83333333333331,
1102                117.33333333333336,
1103                29.285714285714263,
1104                24.285714285714292,
1105                6.999999999999977,
1106                0.,
1107            ],
1108        ]);
1109
1110        println!("{:25e}", &sol[1] - &correct_sol_1);
1111        assert_abs_diff_eq!(&sol[1], &correct_sol_1, epsilon = 1e-8);
1112
1113        let correct_sol_2 = arr2(&[
1114            [0., 0., 0., 0., 0.],
1115            [40.5, 0., 0., 0., 0.],
1116            [42.0, 24.0, 0., 0., 0.],
1117            [54.0, 54.0, 5.0, 0., 0.],
1118            [134.0, 94.0, 11.428571428571436, 1.4285714285714333, 0.],
1119        ]);
1120        println!("{:25e}", &sol[2] - &correct_sol_2);
1121        assert_abs_diff_eq!(&sol[2], &correct_sol_2, epsilon = 1e-8);
1122
1123        let correct_sol_3 = arr2(&[
1124            [0., 0., 0., 0.],
1125            [4.5, 0., 0., 0.],
1126            [32.66666666666666666, 0.666666666666666666666, 0., 0.],
1127            [129.0, 4.0, 1.232595164407831e-30, 0.],
1128        ]);
1129        println!("{:25e}", &sol[3] - &correct_sol_3);
1130        assert_abs_diff_eq!(&sol[3], &correct_sol_3, epsilon = 1e-8);
1131
1132        let correct_sol_4 = arr2(&[
1133            [0., 0., 0.],
1134            [12.5, 0., 0.],
1135            [72.6666666666666666666, 0.6666666666666641, 0.],
1136        ]);
1137        println!("{:25e}", &sol[4] - &correct_sol_4);
1138        assert_abs_diff_eq!(&sol[4], &correct_sol_4, epsilon = 1e-8);
1139
1140        let correct_sol_5 = arr2(&[[0., 0.], [24.5, 0.]]);
1141        println!("{:25e}", &sol[5] - &correct_sol_5);
1142        assert_abs_diff_eq!(&sol[5], &correct_sol_5, epsilon = 1e-8);
1143    }
1144}