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}