pub fn fit_polynomial_lsq(samples: &[(f64, f64)], degree: usize) -> Option<Vec<f64>> {
let n_coeffs = degree + 1;
if samples.len() < n_coeffs {
return None;
}
let mut a = vec![vec![0.0f64; n_coeffs]; n_coeffs];
let mut b = vec![0.0f64; n_coeffs];
for &(x, y) in samples {
let mut powers = vec![1.0f64; n_coeffs];
for k in 1..n_coeffs {
powers[k] = powers[k - 1] * x;
}
for i in 0..n_coeffs {
b[i] += powers[i] * y;
for j in 0..n_coeffs {
a[i][j] += powers[i] * powers[j];
}
}
}
solve_linear_system(a, b)
}
#[allow(clippy::needless_range_loop)]
fn solve_linear_system(mut a: Vec<Vec<f64>>, mut b: Vec<f64>) -> Option<Vec<f64>> {
let n = b.len();
for col in 0..n {
let mut pivot = col;
for row in (col + 1)..n {
if a[row][col].abs() > a[pivot][col].abs() {
pivot = row;
}
}
if a[pivot][col].abs() < 1e-12 {
return None;
}
a.swap(col, pivot);
b.swap(col, pivot);
for row in (col + 1)..n {
let factor = a[row][col] / a[col][col];
for k in col..n {
a[row][k] -= factor * a[col][k];
}
b[row] -= factor * b[col];
}
}
let mut x = vec![0.0f64; n];
for row in (0..n).rev() {
let mut sum = b[row];
for k in (row + 1)..n {
sum -= a[row][k] * x[k];
}
x[row] = sum / a[row][row];
}
if x.iter().all(|v| v.is_finite()) {
Some(x)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn recovers_a_known_cubic_exactly() {
let f = |x: f64| 1.0 + 2.0 * x - 3.0 * x * x + 0.5 * x * x * x;
let samples: Vec<(f64, f64)> = (0..12)
.map(|i| {
let x = i as f64 / 11.0;
(x, f(x))
})
.collect();
let coeffs = fit_polynomial_lsq(&samples, 3).expect("fit should succeed");
let expected = [1.0, 2.0, -3.0, 0.5];
assert_eq!(coeffs.len(), 4);
for (got, want) in coeffs.iter().zip(expected) {
assert!((got - want).abs() < 1e-6, "coeff {got} vs {want}");
}
}
#[test]
fn underdetermined_returns_none() {
let samples = [(0.0, 1.0), (1.0, 2.0)];
assert!(fit_polynomial_lsq(&samples, 5).is_none());
}
#[test]
fn fits_higher_degree_smooth_function_within_tolerance() {
let f = |x: f64| (x * std::f64::consts::PI).sin();
let samples: Vec<(f64, f64)> = (0..27)
.map(|i| {
let x = i as f64 / 26.0;
(x, f(x))
})
.collect();
let coeffs = fit_polynomial_lsq(&samples, 8).expect("fit");
let eval = |x: f64| coeffs.iter().rev().fold(0.0, |acc, c| acc * x + c);
for i in 0..50 {
let x = i as f64 / 49.0;
assert!((eval(x) - f(x)).abs() < 1e-4, "residual too large at {x}");
}
}
}