use numra_core::Scalar;
use crate::error::InterpError;
use crate::{validate_data, Interpolant};
pub struct BarycentricLagrange<S: Scalar> {
x: Vec<S>,
y: Vec<S>,
w: Vec<S>, }
impl<S: Scalar> BarycentricLagrange<S> {
pub fn new(x: &[S], y: &[S]) -> Result<Self, InterpError> {
validate_data(x, y, 1)?;
let w = compute_barycentric_weights(x);
Ok(Self {
x: x.to_vec(),
y: y.to_vec(),
w,
})
}
pub fn chebyshev<F: Fn(S) -> S>(f: F, a: S, b: S, n: usize) -> Self {
let mut x = Vec::with_capacity(n);
let mut y = Vec::with_capacity(n);
let mut orig_weights = Vec::with_capacity(n);
for k in 0..n {
let theta_f = (2 * k + 1) as f64 / (2 * n) as f64 * core::f64::consts::PI;
let theta = S::from_f64(theta_f);
let node = (a + b) * S::HALF + (b - a) * S::HALF * theta.cos();
x.push(node);
y.push(f(node));
let sign = if k % 2 == 0 { 1.0 } else { -1.0 };
orig_weights.push(sign * theta_f.sin());
}
let mut indices: Vec<usize> = (0..n).collect();
indices.sort_by(|&i, &j| x[i].to_f64().partial_cmp(&x[j].to_f64()).unwrap());
let x_sorted: Vec<S> = indices.iter().map(|&i| x[i]).collect();
let y_sorted: Vec<S> = indices.iter().map(|&i| y[i]).collect();
let w: Vec<S> = indices
.iter()
.map(|&i| S::from_f64(orig_weights[i]))
.collect();
Self {
x: x_sorted,
y: y_sorted,
w,
}
}
}
impl<S: Scalar> Interpolant<S> for BarycentricLagrange<S> {
fn interpolate(&self, x: S) -> S {
let n = self.x.len();
for i in 0..n {
let diff = x - self.x[i];
if diff.abs() < S::EPSILON * S::from_f64(100.0) {
return self.y[i];
}
}
let mut numer = S::ZERO;
let mut denom = S::ZERO;
for i in 0..n {
let t = self.w[i] / (x - self.x[i]);
numer += t * self.y[i];
denom += t;
}
numer / denom
}
}
fn compute_barycentric_weights<S: Scalar>(x: &[S]) -> Vec<S> {
let n = x.len();
let xf: Vec<f64> = x.iter().map(|xi| xi.to_f64()).collect();
let mut wf = vec![1.0_f64; n];
for i in 0..n {
for j in 0..n {
if i != j {
wf[i] /= xf[i] - xf[j];
}
}
}
let max_abs = wf.iter().map(|w| w.abs()).fold(0.0_f64, f64::max);
if max_abs > 0.0 {
for w in &mut wf {
*w /= max_abs;
}
}
wf.iter().map(|&w| S::from_f64(w)).collect()
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_lagrange_at_knots() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y = vec![1.0, 3.0, 2.0, 4.0];
let p = BarycentricLagrange::new(&x, &y).unwrap();
for (&xi, &yi) in x.iter().zip(y.iter()) {
assert_relative_eq!(p.interpolate(xi), yi, epsilon = 1e-10);
}
}
#[test]
fn test_lagrange_polynomial_exact() {
let x = vec![0.0, 1.0, 2.0];
let y = vec![0.0, 1.0, 4.0];
let p = BarycentricLagrange::new(&x, &y).unwrap();
assert_relative_eq!(p.interpolate(0.5), 0.25, epsilon = 1e-10);
assert_relative_eq!(p.interpolate(1.5), 2.25, epsilon = 1e-10);
}
#[test]
fn test_lagrange_cubic_exact() {
let x = vec![0.0, 1.0, 2.0, 3.0];
let y: Vec<f64> = x.iter().map(|&xi| xi.powi(3) - xi).collect();
let p = BarycentricLagrange::new(&x, &y).unwrap();
assert_relative_eq!(p.interpolate(0.5), 0.5_f64.powi(3) - 0.5, epsilon = 1e-10);
assert_relative_eq!(p.interpolate(2.5), 2.5_f64.powi(3) - 2.5, epsilon = 1e-10);
}
#[test]
fn test_chebyshev_sin() {
let p = BarycentricLagrange::chebyshev(|x: f64| x.sin(), 0.0, core::f64::consts::PI, 10);
for k in 1..10 {
let x = k as f64 * core::f64::consts::PI / 10.0;
let err = (p.interpolate(x) - x.sin()).abs();
assert!(err < 1e-7, "Chebyshev error at x={}: {}", x, err);
}
}
#[test]
fn test_chebyshev_runge() {
let p = BarycentricLagrange::chebyshev(|x: f64| 1.0 / (1.0 + 25.0 * x * x), -1.0, 1.0, 50);
assert_relative_eq!(p.interpolate(0.0), 1.0, epsilon = 1e-3);
let y_edge = p.interpolate(0.9);
let exact = 1.0 / (1.0 + 25.0 * 0.81);
assert!(
(y_edge - exact).abs() < 1e-4,
"Runge at 0.9: {} vs {}",
y_edge,
exact
);
}
#[test]
fn test_lagrange_single_point() {
let p = BarycentricLagrange::new(&[1.0], &[5.0]).unwrap();
assert_relative_eq!(p.interpolate(1.0), 5.0, epsilon = 1e-14);
assert_relative_eq!(p.interpolate(2.0), 5.0, epsilon = 1e-14);
}
#[test]
fn test_lagrange_f32() {
let p = BarycentricLagrange::new(&[0.0f32, 1.0, 2.0], &[0.0, 1.0, 4.0]).unwrap();
assert!((p.interpolate(0.5f32) - 0.25).abs() < 1e-4);
}
}