use alloc::vec;
use alloc::vec::Vec;
#[cfg(not(feature = "std"))]
#[allow(unused_imports)]
use crate::math::FloatExt;
const RANK_TOLERANCE: f64 = 1e-13;
const CONSTANT_VARIANCE: f64 = 1e-15;
#[must_use]
pub fn fit_polynomial(
values: &[f32],
max_degree: usize,
error_threshold: f64,
) -> Option<(Vec<f64>, usize, f64)> {
let xs = unit_xs(values.len());
let (mut coeffs, degree, err) = fit_generic(&xs, values, max_degree, error_threshold)?;
#[allow(clippy::cast_precision_loss)]
let h = 1.0 / (values.len().saturating_sub(1).max(1)) as f64;
let mut scale = 1.0;
for c in &mut coeffs {
*c *= scale;
scale *= h;
}
Some((coeffs, degree, err))
}
#[must_use]
#[allow(clippy::cast_possible_truncation)]
pub fn polynomial_at(coefficients: &[f64], x: f64) -> f32 {
horner_ascending(coefficients, x) as f32
}
#[must_use]
pub fn generate_polynomial(n: usize, coefficients: &[f64]) -> Vec<f32> {
#[allow(clippy::cast_precision_loss)]
(0..n)
.map(|i| polynomial_at(coefficients, i as f64))
.collect()
}
#[must_use]
pub fn fit_polynomial_unit(
values: &[f32],
max_degree: usize,
error_threshold: f64,
) -> Option<(Vec<f64>, usize, f64)> {
let xs = unit_xs(values.len());
let (mut coeffs, degree, err) = fit_generic(&xs, values, max_degree, error_threshold)?;
coeffs.reverse();
Some((coeffs, degree, err))
}
#[must_use]
pub fn generate_polynomial_unit(n: usize, coefficients: &[f64]) -> Vec<f32> {
#[allow(clippy::cast_possible_truncation)]
unit_xs(n)
.into_iter()
.map(|x| horner_descending(coefficients, x) as f32)
.collect()
}
#[allow(clippy::cast_precision_loss)]
fn unit_xs(n: usize) -> Vec<f64> {
let rcp = 1.0 / (n.saturating_sub(1).max(1)) as f64;
(0..n).map(|i| i as f64 * rcp).collect()
}
#[inline]
pub(crate) fn horner_ascending(coeffs: &[f64], x: f64) -> f64 {
coeffs.iter().rev().fold(0.0_f64, |acc, &c| acc * x + c)
}
#[inline]
fn horner_descending(coeffs: &[f64], x: f64) -> f64 {
coeffs.iter().fold(0.0_f64, |acc, &c| acc * x + c)
}
#[allow(clippy::cast_precision_loss)]
fn fit_generic(
xs: &[f64],
values: &[f32],
max_degree: usize,
error_threshold: f64,
) -> Option<(Vec<f64>, usize, f64)> {
let n = values.len();
if n < 2 {
return None;
}
let ys: Vec<f64> = values.iter().map(|v| f64::from(*v)).collect();
let inv_n = 1.0 / n as f64;
let mean = ys.iter().sum::<f64>() * inv_n;
let variance = ys.iter().map(|y| (y - mean) * (y - mean)).sum::<f64>() * inv_n;
if variance < CONSTANT_VARIANCE {
return Some((vec![mean], 0, 0.0));
}
for degree in 1..=max_degree.min(n - 1) {
let Some(coeffs) = least_squares_fit(xs, &ys, degree) else {
continue;
};
let mse = xs
.iter()
.zip(&ys)
.map(|(&x, &y)| {
let d = y - horner_ascending(&coeffs, x);
d * d
})
.sum::<f64>()
* inv_n;
let rel = mse / variance;
if rel < error_threshold {
return Some((coeffs, degree, rel));
}
}
None
}
pub(crate) fn least_squares_fit(xs: &[f64], ys: &[f64], degree: usize) -> Option<Vec<f64>> {
let n = xs.len();
let m = degree + 1;
if n < m {
return None;
}
let mut a = vec![0.0_f64; n * m];
for (i, &x) in xs.iter().enumerate() {
let mut pow = 1.0;
for j in 0..m {
a[j * n + i] = pow;
pow *= x;
}
}
let mut b = ys.to_vec();
let mut diag = vec![0.0_f64; m];
for k in 0..m {
let norm = a[k * n + k..(k + 1) * n]
.iter()
.map(|v| v * v)
.sum::<f64>()
.sqrt();
if norm == 0.0 {
return None;
}
let alpha = if a[k * n + k] > 0.0 { -norm } else { norm };
a[k * n + k] -= alpha;
let vnorm2: f64 = a[k * n + k..(k + 1) * n].iter().map(|v| v * v).sum();
if vnorm2 == 0.0 {
return None;
}
for j in (k + 1)..m {
let dot: f64 = (k..n).map(|i| a[k * n + i] * a[j * n + i]).sum();
let f = 2.0 * dot / vnorm2;
for i in k..n {
a[j * n + i] -= f * a[k * n + i];
}
}
let dot: f64 = (k..n).map(|i| a[k * n + i] * b[i]).sum();
let f = 2.0 * dot / vnorm2;
for i in k..n {
b[i] -= f * a[k * n + i];
}
diag[k] = alpha;
}
let r_max = diag.iter().fold(0.0_f64, |acc, d| acc.max(d.abs()));
if diag.iter().any(|d| d.abs() <= RANK_TOLERANCE * r_max) {
return None;
}
let mut coeffs = vec![0.0_f64; m];
for k in (0..m).rev() {
let mut sum = b[k];
for j in (k + 1)..m {
sum -= a[j * n + k] * coeffs[j];
}
coeffs[k] = sum / diag[k];
if !coeffs[k].is_finite() {
return None;
}
}
Some(coeffs)
}
#[cfg(test)]
#[allow(clippy::cast_precision_loss, clippy::float_cmp)]
mod tests {
use super::*;
#[test]
fn generate_polynomial_linear() {
let out = generate_polynomial(5, &[3.0, 2.0]);
assert_eq!(out, vec![3.0, 5.0, 7.0, 9.0, 11.0]);
}
#[test]
fn generate_polynomial_quadratic() {
let out = generate_polynomial(4, &[1.0, 1.0, 1.0]);
assert_eq!(out, vec![1.0, 3.0, 7.0, 13.0]);
}
#[test]
fn generate_polynomial_empty() {
assert!(generate_polynomial(0, &[1.0, 2.0]).is_empty());
assert_eq!(generate_polynomial(3, &[]), vec![0.0; 3]);
}
#[test]
fn fit_polynomial_linear_series() {
let values: Vec<f32> = (0..20).map(|i| 3.0 + 2.0 * i as f32).collect();
let (coefs, deg, err) = fit_polynomial(&values, 3, 1e-6).expect("linear fit");
assert_eq!(deg, 1);
assert!((coefs[0] - 3.0).abs() < 1e-4);
assert!((coefs[1] - 2.0).abs() < 1e-4);
assert!(err < 1e-6);
}
#[test]
fn fit_polynomial_quadratic_series() {
let values: Vec<f32> = (0..20)
.map(|i| 1.0 + i as f32 + (i as f32) * (i as f32))
.collect();
let (coefs, deg, _) = fit_polynomial(&values, 4, 1e-4).expect("quadratic fit");
assert_eq!(deg, 2);
assert!((coefs[0] - 1.0).abs() < 1e-3);
assert!((coefs[1] - 1.0).abs() < 1e-3);
assert!((coefs[2] - 1.0).abs() < 1e-3);
}
#[test]
fn fit_polynomial_noise_returns_none() {
let values: Vec<f32> = (0..40)
.map(|i| if i % 2 == 0 { 1.0 } else { -1.0 })
.collect();
assert!(fit_polynomial(&values, 3, 0.0001).is_none());
}
#[test]
fn fit_polynomial_too_short() {
assert!(fit_polynomial(&[1.0], 2, 0.01).is_none());
assert!(fit_polynomial(&[], 2, 0.01).is_none());
}
#[test]
fn fit_polynomial_constant_is_degree_zero_mean() {
let (c, d, e) = fit_polynomial(&[4.5; 10], 3, 0.01).unwrap();
assert_eq!((c, d, e), (vec![4.5], 0, 0.0));
}
#[test]
fn generate_polynomial_unit_quadratic() {
let signal = generate_polynomial_unit(101, &[1.0, 0.0, 0.0]);
assert_eq!(signal.len(), 101);
assert!(signal[0].abs() < 1e-6);
assert!((signal[50] - 0.25).abs() < 1e-6);
assert!((signal[100] - 1.0).abs() < 1e-6);
assert_eq!(generate_polynomial_unit(3, &[]), vec![0.0; 3]);
assert_eq!(generate_polynomial_unit(1, &[7.0, 5.0]), vec![5.0]);
}
#[test]
fn fit_polynomial_unit_recovers_descending_coefficients() {
let data: Vec<f32> = (0..100)
.map(|i| {
let x = i as f32 / 99.0;
x * x + 2.0 * x + 1.0
})
.collect();
let (coeffs, degree, error) = fit_polynomial_unit(&data, 5, 1e-6).unwrap();
assert_eq!(degree, 2);
assert!(error < 1e-6);
assert!((coeffs[0] - 1.0).abs() < 1e-3, "{coeffs:?}");
assert!((coeffs[1] - 2.0).abs() < 1e-3, "{coeffs:?}");
assert!((coeffs[2] - 1.0).abs() < 1e-3, "{coeffs:?}");
let recon = generate_polynomial_unit(100, &coeffs);
for (a, b) in data.iter().zip(&recon) {
assert!((a - b).abs() < 1e-4);
}
}
#[test]
fn horner_conventions_are_mirrors() {
let asc = [1.0, 2.0, 3.0];
let desc = [3.0, 2.0, 1.0];
for x in [0.0, 0.5, 1.0, 2.0, -1.5] {
assert_eq!(horner_ascending(&asc, x), horner_descending(&desc, x));
}
}
}