use numra_core::Scalar;
use numra_linalg::{DenseMatrix, LUFactorization};
use crate::error::FitError;
use crate::types::FitResult;
pub fn polyfit<S>(x_data: &[S], y_data: &[S], deg: usize) -> Result<FitResult<S>, FitError>
where
S: Scalar + faer::SimpleEntity + faer::Conjugate<Canonical = S> + faer::ComplexField,
{
let m = x_data.len();
let n = deg + 1;
if m != y_data.len() {
return Err(FitError::LengthMismatch {
expected: m,
got: y_data.len(),
});
}
if m < n {
return Err(FitError::Underdetermined {
n_params: n,
n_data: m,
});
}
if m == 0 {
return Err(FitError::InsufficientData { need: 1, got: 0 });
}
let mut v_data = vec![S::ZERO; m * n];
for i in 0..m {
let mut xi_pow = S::ONE;
for j in (0..n).rev() {
v_data[i * n + j] = xi_pow;
xi_pow = xi_pow * x_data[i];
}
}
let vander = DenseMatrix::from_row_major(m, n, &v_data);
let coeffs = vander
.lstsq(y_data)
.map_err(|e| FitError::OptimizationFailed(format!("lstsq failed: {e}")))?;
let residuals: Vec<S> = (0..m)
.map(|i| {
let y_pred = polyval_internal(&coeffs, x_data[i]);
y_data[i] - y_pred
})
.collect();
let chi_squared: S = residuals.iter().map(|&r| r * r).sum();
let dof = m - n;
let reduced_chi_sq = if dof > 0 {
chi_squared / S::from_usize(dof)
} else {
S::ZERO
};
let y_mean: S = y_data.iter().copied().sum::<S>() / S::from_usize(m);
let ss_tot: S = y_data.iter().map(|&y| (y - y_mean) * (y - y_mean)).sum();
let r_squared = if ss_tot > S::ZERO {
S::ONE - chi_squared / ss_tot
} else {
S::ONE
};
let (covariance, std_errors) = if dof > 0 {
compute_poly_covariance(&v_data, m, n, reduced_chi_sq)
} else {
(None, None)
};
Ok(FitResult {
params: coeffs,
covariance,
std_errors,
residuals,
chi_squared,
reduced_chi_squared: reduced_chi_sq,
r_squared,
dof,
n_evaluations: 0,
converged: true,
})
}
pub fn polyval<S: Scalar>(coeffs: &[S], x: &[S]) -> Vec<S> {
x.iter().map(|&xi| polyval_internal(coeffs, xi)).collect()
}
fn polyval_internal<S: Scalar>(coeffs: &[S], x: S) -> S {
if coeffs.is_empty() {
return S::ZERO;
}
let mut result = coeffs[0];
for &c in &coeffs[1..] {
result = result * x + c;
}
result
}
fn compute_poly_covariance<S>(
v_data: &[S],
m: usize,
n: usize,
sigma_sq: S,
) -> (Option<Vec<S>>, Option<Vec<S>>)
where
S: Scalar + faer::SimpleEntity + faer::Conjugate<Canonical = S> + faer::ComplexField,
{
let mut vtv = vec![S::ZERO; n * n];
for i in 0..n {
for j in 0..n {
let mut sum = S::ZERO;
for k in 0..m {
sum = sum + v_data[k * n + i] * v_data[k * n + j];
}
vtv[i * n + j] = sum;
}
}
let dense = DenseMatrix::from_row_major(n, n, &vtv);
let lu = match LUFactorization::new(&dense) {
Ok(l) => l,
Err(_) => return (None, None),
};
let mut inv = vec![S::ZERO; n * n];
for j in 0..n {
let mut e = vec![S::ZERO; n];
e[j] = S::ONE;
let col = match lu.solve(&e) {
Ok(c) => c,
Err(_) => return (None, None),
};
for i in 0..n {
inv[i * n + j] = col[i] * sigma_sq;
}
}
let std_errors: Vec<S> = (0..n)
.map(|i| {
let v = inv[i * n + i];
if v > S::ZERO {
v.sqrt()
} else {
S::ZERO
}
})
.collect();
(Some(inv), Some(std_errors))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_polyfit_linear() {
let x: Vec<f64> = (0..10).map(|i| i as f64).collect();
let y: Vec<f64> = x.iter().map(|&xi| 2.0 * xi + 1.0).collect();
let result = polyfit(&x, &y, 1).unwrap();
assert!((result.params[0] - 2.0).abs() < 1e-10); assert!((result.params[1] - 1.0).abs() < 1e-10); assert!(result.r_squared > 1.0 - 1e-12);
assert_eq!(result.dof, 8);
}
#[test]
fn test_polyfit_quadratic() {
let x: Vec<f64> = (0..20).map(|i| i as f64 * 0.5).collect();
let y: Vec<f64> = x.iter().map(|&xi| 3.0 * xi * xi - 2.0 * xi + 1.0).collect();
let result = polyfit(&x, &y, 2).unwrap();
assert!((result.params[0] - 3.0).abs() < 1e-8);
assert!((result.params[1] - (-2.0)).abs() < 1e-8);
assert!((result.params[2] - 1.0).abs() < 1e-8);
assert!(result.covariance.is_some());
}
#[test]
fn test_polyfit_cubic() {
let x: Vec<f64> = (0..30).map(|i| -3.0 + i as f64 * 0.2).collect();
let y: Vec<f64> = x.iter().map(|&xi| xi * xi * xi - xi).collect();
let result = polyfit(&x, &y, 3).unwrap();
assert!((result.params[0] - 1.0).abs() < 1e-6); assert!(result.params[1].abs() < 1e-6); assert!((result.params[2] - (-1.0)).abs() < 1e-6); assert!(result.params[3].abs() < 1e-6); }
#[test]
fn test_polyfit_overdetermined() {
let x: Vec<f64> = (0..10).map(|i| i as f64).collect();
let y: Vec<f64> = x.iter().map(|&xi| xi * xi).collect();
let result = polyfit(&x, &y, 1).unwrap();
assert!(result.r_squared < 1.0);
assert!(result.chi_squared > 0.0);
}
#[test]
fn test_polyval() {
let coeffs = vec![1.0_f64, -2.0, 3.0];
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = polyval(&coeffs, &x);
assert!((y[0] - 3.0).abs() < 1e-10);
assert!((y[1] - 2.0).abs() < 1e-10);
assert!((y[2] - 3.0).abs() < 1e-10);
assert!((y[3] - 6.0).abs() < 1e-10);
}
#[test]
fn test_polyval_single_coeff() {
let coeffs = vec![5.0_f64];
let x = vec![1.0, 2.0, 100.0];
let y = polyval(&coeffs, &x);
for &yi in &y {
assert!((yi - 5.0).abs() < 1e-10);
}
}
#[test]
fn test_polyval_empty() {
let y = polyval::<f64>(&[], &[1.0, 2.0]);
assert_eq!(y.len(), 2);
assert!(y[0].abs() < 1e-10);
}
#[test]
fn test_polyfit_length_mismatch() {
let result = polyfit(&[1.0_f64, 2.0, 3.0], &[1.0, 2.0], 1);
assert!(matches!(result, Err(FitError::LengthMismatch { .. })));
}
#[test]
fn test_polyfit_underdetermined() {
let result = polyfit(&[1.0_f64, 2.0], &[1.0, 2.0], 2);
assert!(matches!(result, Err(FitError::Underdetermined { .. })));
}
#[test]
fn test_polyfit_constant() {
let x: Vec<f64> = (0..10).map(|i| i as f64).collect();
let y = vec![42.0_f64; 10];
let result = polyfit(&x, &y, 0).unwrap();
assert!((result.params[0] - 42.0).abs() < 1e-10);
}
}