const SMALL: f64 = 1e-8;
pub fn fitpoly(x: &[f64], y: &[f64], mask: Option<&[f64]>) -> Option<(f64, f64, f64)> {
let n = x.len().min(y.len());
if n < 3 {
return None;
}
let mut beta = [0.0f64; 3];
let (mut a00, mut a01, mut a11, mut a12, mut a22) = (0.0f64, 0.0, 0.0, 0.0, 0.0);
for i in 0..n {
if let Some(m) = mask
&& !admits(m[i])
{
continue;
}
let (xi, yi) = (x[i], y[i]);
beta[0] += yi;
beta[1] += yi * xi;
beta[2] += yi * xi * xi;
a00 += 1.0;
a01 += xi;
a11 += xi * xi;
a12 += xi * xi * xi;
a22 += xi * xi * xi * xi;
}
let alpha = [[a00, a01, a11], [a01, a11, a12], [a11, a12, a22]];
let inv = invert3x3(&alpha)?;
let mut c = 0.0;
let mut b = 0.0;
let mut a = 0.0;
for j in 0..3 {
c += beta[j] * inv[j][0];
b += beta[j] * inv[j][1];
a += beta[j] * inv[j][2];
}
Some((c, b, a))
}
fn admits(m: f64) -> bool {
matches!(m.partial_cmp(&SMALL), Some(std::cmp::Ordering::Greater))
}
fn invert3x3(m: &[[f64; 3]; 3]) -> Option<[[f64; 3]; 3]> {
let (a, b, c) = (m[0][0], m[0][1], m[0][2]);
let (d, e, f) = (m[1][0], m[1][1], m[1][2]);
let (g, h, i) = (m[2][0], m[2][1], m[2][2]);
let det = a * (e * i - h * f) + b * (f * g - i * d) + c * (d * h - g * e);
if det.abs() < SMALL {
return None;
}
Some([
[
(e * i - f * h) / det,
(c * h - b * i) / det,
(b * f - c * e) / det,
],
[
(f * g - d * i) / det,
(a * i - c * g) / det,
(c * d - a * f) / det,
],
[
(d * h - e * g) / det,
(b * g - a * h) / det,
(a * e - b * d) / det,
],
])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fitpoly_quadratic() {
let x: Vec<f64> = (0..11).map(|i| i as f64).collect();
let y: Vec<f64> = x.iter().map(|&xi| 1.0 + 2.0 * xi + 3.0 * xi * xi).collect();
let (c, b, a) = fitpoly(&x, &y, None).expect("11 points, non-singular");
assert!((c - 1.0).abs() < 1e-6, "c={c}");
assert!((b - 2.0).abs() < 1e-6, "b={b}");
assert!((a - 3.0).abs() < 1e-6, "a={a}");
}
#[test]
fn test_fitpoly_with_mask() {
let x: Vec<f64> = (0..11).map(|i| i as f64).collect();
let mut y: Vec<f64> = x.iter().map(|&xi| 1.0 + 2.0 * xi + 3.0 * xi * xi).collect();
y[5] = 1000.0; let mut mask = vec![1.0; 11];
mask[5] = 0.0; let (c, b, a) = fitpoly(&x, &y, Some(&mask)).expect("10 unmasked points");
assert!((c - 1.0).abs() < 1e-4, "c={c}");
assert!((b - 2.0).abs() < 1e-3, "b={b}");
assert!((a - 3.0).abs() < 1e-3, "a={a}");
}
#[test]
fn test_mask_is_a_threshold_not_a_truthiness_test() {
let x: Vec<f64> = (0..11).map(|i| i as f64).collect();
let mut y: Vec<f64> = x.iter().map(|&xi| 1.0 + 2.0 * xi + 3.0 * xi * xi).collect();
y[5] = 1000.0;
let mut mask = vec![1.0; 11];
mask[5] = 1e-9; let (c, _, a) = fitpoly(&x, &y, Some(&mask)).expect("10 unmasked points");
assert!(
(c - 1.0).abs() < 1e-4,
"1e-9 must mask the outlier out: c={c}"
);
assert!((a - 3.0).abs() < 1e-3, "a={a}");
mask[5] = 1e-7;
let (c, _, _) = fitpoly(&x, &y, Some(&mask)).expect("11 unmasked points");
assert!(
(c - 1.0).abs() > 1.0,
"1e-7 must keep the outlier in: c={c}"
);
}
#[test]
fn test_fewer_than_three_points_fails() {
assert_eq!(fitpoly(&[0.0, 1.0], &[1.0, 2.0], None), None);
assert_eq!(fitpoly(&[], &[], None), None);
}
#[test]
fn test_all_masked_out_fails() {
let x: Vec<f64> = (0..11).map(|i| i as f64).collect();
let y: Vec<f64> = x.iter().map(|&xi| 1.0 + 2.0 * xi).collect();
assert_eq!(fitpoly(&x, &y, Some(&[0.0; 11])), None);
let mut mask = vec![0.0; 11];
mask[0] = 1.0;
mask[1] = 1.0;
assert_eq!(
fitpoly(&x, &y, Some(&mask)),
None,
"two unmasked points cannot determine three coefficients"
);
}
}