use crate::error::{InterpolateError, InterpolateResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use scirs2_linalg::solve;
fn distance_matrix(points: &Array2<f64>) -> Array2<f64> {
let n = points.nrows();
let mut d = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in (i + 1)..n {
let dist = (0..points.ncols())
.map(|k| (points[[i, k]] - points[[j, k]]).powi(2))
.sum::<f64>()
.sqrt();
d[[i, j]] = dist;
d[[j, i]] = dist;
}
}
d
}
fn distances_to_point(query: &[f64], points: &Array2<f64>) -> Array1<f64> {
let n = points.nrows();
let d = points.ncols();
let mut dists = Array1::<f64>::zeros(n);
for i in 0..n {
let sq: f64 = (0..d).map(|k| (query[k] - points[[i, k]]).powi(2)).sum();
dists[i] = sq.sqrt();
}
dists
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PolyharmonicKernel {
ThinPlate,
R3,
R5,
R7,
Custom(u32),
}
impl PolyharmonicKernel {
#[inline]
pub fn eval(&self, r: f64) -> f64 {
match self {
PolyharmonicKernel::ThinPlate => {
if r <= 0.0 {
0.0
} else {
r * r * r.ln()
}
}
PolyharmonicKernel::R3 => r.powi(3),
PolyharmonicKernel::R5 => r.powi(5),
PolyharmonicKernel::R7 => r.powi(7),
PolyharmonicKernel::Custom(k) => r.powi(*k as i32),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum WendlandKernel {
Psi30,
Psi31,
Psi32,
}
impl WendlandKernel {
#[inline]
pub fn eval_scaled(&self, r: f64) -> f64 {
if r >= 1.0 {
return 0.0;
}
let t = 1.0 - r;
match self {
WendlandKernel::Psi30 => t * t,
WendlandKernel::Psi31 => t.powi(4) * (4.0 * r + 1.0),
WendlandKernel::Psi32 => t.powi(6) * (35.0 * r * r + 18.0 * r + 3.0) / 3.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum PolyDegree {
None,
Const,
Linear,
Quadratic,
}
fn build_poly_matrix(points: &Array2<f64>, degree: PolyDegree) -> Array2<f64> {
let n = points.nrows();
let d = points.ncols();
let cols = match degree {
PolyDegree::None => return Array2::<f64>::zeros((n, 0)),
PolyDegree::Const => 1,
PolyDegree::Linear => 1 + d,
PolyDegree::Quadratic => 1 + d + d * (d + 1) / 2,
};
let mut p = Array2::<f64>::zeros((n, cols));
for i in 0..n {
let mut col = 0usize;
p[[i, col]] = 1.0;
col += 1;
if degree >= PolyDegree::Linear {
for k in 0..d {
p[[i, col]] = points[[i, k]];
col += 1;
}
}
if degree >= PolyDegree::Quadratic {
for k in 0..d {
for l in k..d {
p[[i, col]] = points[[i, k]] * points[[i, l]];
col += 1;
}
}
}
}
p
}
fn poly_row(query: &[f64], degree: PolyDegree) -> Vec<f64> {
let d = query.len();
match degree {
PolyDegree::None => vec![],
PolyDegree::Const => vec![1.0],
PolyDegree::Linear => {
let mut row = vec![1.0];
row.extend_from_slice(query);
row
}
PolyDegree::Quadratic => {
let mut row = vec![1.0];
row.extend_from_slice(query);
for k in 0..d {
for l in k..d {
row.push(query[k] * query[l]);
}
}
row
}
}
}
pub struct GlobalRbfInterpolant {
points: Array2<f64>,
coeffs_rbf: Array1<f64>, coeffs_poly: Array1<f64>, kernel: InternalKernel,
degree: PolyDegree,
}
#[derive(Debug, Clone)]
enum InternalKernel {
Polyharmonic(PolyharmonicKernel),
Gaussian(f64), Multiquadric(f64), InvMultiquadric(f64),
}
impl InternalKernel {
#[inline]
fn eval(&self, r: f64) -> f64 {
match self {
InternalKernel::Polyharmonic(k) => k.eval(r),
InternalKernel::Gaussian(eps) => (-(eps * r).powi(2)).exp(),
InternalKernel::Multiquadric(eps) => (1.0 + (eps * r).powi(2)).sqrt(),
InternalKernel::InvMultiquadric(eps) => 1.0 / (1.0 + (eps * r).powi(2)).sqrt(),
}
}
}
impl GlobalRbfInterpolant {
pub fn new_polyharmonic(
points: &ArrayView2<f64>,
values: &ArrayView1<f64>,
kernel: PolyharmonicKernel,
degree: PolyDegree,
) -> InterpolateResult<Self> {
Self::build(
points,
values,
InternalKernel::Polyharmonic(kernel),
degree,
)
}
pub fn new_gaussian(
points: &ArrayView2<f64>,
values: &ArrayView1<f64>,
epsilon: f64,
degree: PolyDegree,
) -> InterpolateResult<Self> {
if epsilon <= 0.0 {
return Err(InterpolateError::InvalidInput {
message: format!("epsilon must be > 0, got {epsilon}"),
});
}
Self::build(points, values, InternalKernel::Gaussian(epsilon), degree)
}
pub fn new_multiquadric(
points: &ArrayView2<f64>,
values: &ArrayView1<f64>,
epsilon: f64,
degree: PolyDegree,
) -> InterpolateResult<Self> {
if epsilon <= 0.0 {
return Err(InterpolateError::InvalidInput {
message: format!("epsilon must be > 0, got {epsilon}"),
});
}
Self::build(
points,
values,
InternalKernel::Multiquadric(epsilon),
degree,
)
}
fn build(
points: &ArrayView2<f64>,
values: &ArrayView1<f64>,
kernel: InternalKernel,
degree: PolyDegree,
) -> InterpolateResult<Self> {
let n = points.nrows();
let pts_owned = points.to_owned();
if values.len() != n {
return Err(InterpolateError::DimensionMismatch(format!(
"points has {n} rows but values has {} entries",
values.len()
)));
}
if n == 0 {
return Err(InterpolateError::InsufficientData(
"At least one data point required".to_string(),
));
}
let dist = distance_matrix(&pts_owned);
let mut a = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
a[[i, j]] = kernel.eval(dist[[i, j]]);
}
}
let p_mat = build_poly_matrix(&pts_owned, degree);
let m = p_mat.ncols();
let total = n + m;
let mut sys = Array2::<f64>::zeros((total, total));
let mut rhs = Array1::<f64>::zeros(total);
for i in 0..n {
for j in 0..n {
sys[[i, j]] = a[[i, j]];
}
}
for i in 0..n {
for j in 0..m {
sys[[i, n + j]] = p_mat[[i, j]];
sys[[n + j, i]] = p_mat[[i, j]];
}
}
for i in 0..n {
rhs[i] = values[i];
}
let sol = solve_linear_system(sys, rhs)?;
let coeffs_rbf = sol.slice(scirs2_core::ndarray::s![..n]).to_owned();
let coeffs_poly = if m > 0 {
sol.slice(scirs2_core::ndarray::s![n..]).to_owned()
} else {
Array1::<f64>::zeros(0)
};
Ok(Self {
points: pts_owned,
coeffs_rbf,
coeffs_poly,
kernel,
degree,
})
}
pub fn evaluate(&self, query: &[f64]) -> InterpolateResult<f64> {
let d = self.points.ncols();
if query.len() != d {
return Err(InterpolateError::DimensionMismatch(format!(
"Query has {} dims, points have {d}",
query.len()
)));
}
let dists = distances_to_point(query, &self.points);
let n = self.points.nrows();
let mut val: f64 = (0..n).map(|i| self.coeffs_rbf[i] * self.kernel.eval(dists[i])).sum();
let prow = poly_row(query, self.degree);
for (j, &coeff) in self.coeffs_poly.iter().enumerate() {
val += coeff * prow[j];
}
Ok(val)
}
pub fn evaluate_batch(&self, queries: &ArrayView2<f64>) -> InterpolateResult<Array1<f64>> {
let nq = queries.nrows();
let mut out = Array1::<f64>::zeros(nq);
for i in 0..nq {
let row: Vec<f64> = (0..queries.ncols()).map(|j| queries[[i, j]]).collect();
out[i] = self.evaluate(&row)?;
}
Ok(out)
}
}
pub struct WendlandInterpolant {
points: Array2<f64>,
coeffs: Array1<f64>,
kernel: WendlandKernel,
support_radius: f64,
}
impl WendlandInterpolant {
pub fn new(
points: &ArrayView2<f64>,
values: &ArrayView1<f64>,
kernel: WendlandKernel,
support_radius: f64,
) -> InterpolateResult<Self> {
if support_radius <= 0.0 {
return Err(InterpolateError::InvalidInput {
message: format!("support_radius must be > 0, got {support_radius}"),
});
}
let n = points.nrows();
if values.len() != n {
return Err(InterpolateError::DimensionMismatch(format!(
"points has {n} rows but values has {} entries",
values.len()
)));
}
if n == 0 {
return Err(InterpolateError::InsufficientData(
"At least one data point required".to_string(),
));
}
let pts_owned = points.to_owned();
let dist = distance_matrix(&pts_owned);
let mut a = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..n {
let r_scaled = dist[[i, j]] / support_radius;
a[[i, j]] = kernel.eval_scaled(r_scaled);
}
}
let coeffs = solve_linear_system(a, values.to_owned())?;
Ok(Self {
points: pts_owned,
coeffs,
kernel,
support_radius,
})
}
pub fn evaluate(&self, query: &[f64]) -> InterpolateResult<f64> {
let d = self.points.ncols();
if query.len() != d {
return Err(InterpolateError::DimensionMismatch(format!(
"Query has {} dims, points have {d}",
query.len()
)));
}
let dists = distances_to_point(query, &self.points);
let val: f64 = (0..self.points.nrows())
.map(|i| {
let r_scaled = dists[i] / self.support_radius;
self.coeffs[i] * self.kernel.eval_scaled(r_scaled)
})
.sum();
Ok(val)
}
pub fn evaluate_batch(&self, queries: &ArrayView2<f64>) -> InterpolateResult<Array1<f64>> {
let nq = queries.nrows();
let mut out = Array1::<f64>::zeros(nq);
for i in 0..nq {
let row: Vec<f64> = (0..queries.ncols()).map(|j| queries[[i, j]]).collect();
out[i] = self.evaluate(&row)?;
}
Ok(out)
}
}
pub fn rbf_fd_weights(
center: &[f64],
stencil: &ArrayView2<f64>,
kernel: PolyharmonicKernel,
) -> InterpolateResult<Array1<f64>> {
let k = stencil.nrows();
let d = stencil.ncols();
if center.len() != d {
return Err(InterpolateError::DimensionMismatch(format!(
"center has {} dims, stencil has {d} dims",
center.len()
)));
}
if k == 0 {
return Err(InterpolateError::InsufficientData(
"Stencil must have at least one point".to_string(),
));
}
let stencil_owned = stencil.to_owned();
let dist = distance_matrix(&stencil_owned);
let mut a = Array2::<f64>::zeros((k, k));
for i in 0..k {
for j in 0..k {
a[[i, j]] = kernel.eval(dist[[i, j]]);
}
}
let dists_to_center = distances_to_point(center, &stencil_owned);
let mut rhs = Array1::<f64>::zeros(k);
for i in 0..k {
rhs[i] = kernel.eval(dists_to_center[i]);
}
let weights = solve_linear_system(a, rhs)?;
Ok(weights)
}
pub struct HermiteRbfInterpolant {
points: Array2<f64>,
coeffs: Array1<f64>, kernel: PolyharmonicKernel,
n_points: usize,
dim: usize,
}
impl HermiteRbfInterpolant {
pub fn new(
points: &ArrayView2<f64>,
values: &ArrayView1<f64>,
grads: &ArrayView2<f64>,
kernel: PolyharmonicKernel,
) -> InterpolateResult<Self> {
let n = points.nrows();
let d = points.ncols();
if values.len() != n {
return Err(InterpolateError::DimensionMismatch(format!(
"values has {} entries, expected {n}",
values.len()
)));
}
if grads.nrows() != n || grads.ncols() != d {
return Err(InterpolateError::DimensionMismatch(format!(
"grads must be ({n}, {d}), got ({}, {})",
grads.nrows(),
grads.ncols()
)));
}
if n == 0 {
return Err(InterpolateError::InsufficientData(
"At least one data point required".to_string(),
));
}
let pts_owned = points.to_owned();
let sz = n * (d + 1);
let mut sys = Array2::<f64>::zeros((sz, sz));
let mut rhs = Array1::<f64>::zeros(sz);
for i in 0..n {
rhs[i] = values[i];
}
for k in 0..d {
for i in 0..n {
rhs[n + k * n + i] = grads[[i, k]];
}
}
for i in 0..n {
for j in 0..n {
let diff: Vec<f64> = (0..d)
.map(|k| pts_owned[[i, k]] - pts_owned[[j, k]])
.collect();
let r = diff.iter().map(|&v| v * v).sum::<f64>().sqrt();
sys[[i, j]] = kernel.eval(r);
let dphi_dr = hermite_dphi_dr(kernel, r);
let d2phi_dr2 = hermite_d2phi_dr2(kernel, r);
for k in 0..d {
let k01 = if r > 1e-14 { -dphi_dr * diff[k] / r } else { 0.0 };
let k10 = -k01;
sys[[i, n + k * n + j]] = k01;
sys[[n + k * n + i, j]] = k10;
}
for k in 0..d {
for l in 0..d {
let val = if r > 1e-14 {
let dk = diff[k]; let dl = diff[l];
let r2 = r * r; let r3 = r2 * r;
let cross = d2phi_dr2 * dk * dl / r2;
let diag_term = if k == l {
dphi_dr * (1.0 / r - dk * dl / r3)
} else {
-dphi_dr * dk * dl / r3
};
-(cross + diag_term)
} else if k == l {
0.0
} else {
0.0
};
sys[[n + k * n + i, n + l * n + j]] = val;
}
}
}
}
let coeffs = solve_linear_system(sys, rhs)?;
Ok(Self {
points: pts_owned,
coeffs,
kernel,
n_points: n,
dim: d,
})
}
pub fn evaluate(&self, query: &[f64]) -> InterpolateResult<f64> {
let n = self.n_points;
let d = self.dim;
if query.len() != d {
return Err(InterpolateError::DimensionMismatch(format!(
"Query has {} dims, points have {d}",
query.len()
)));
}
let mut val = 0.0_f64;
for j in 0..n {
let diff: Vec<f64> = (0..d).map(|k| query[k] - self.points[[j, k]]).collect();
let r = diff.iter().map(|&v| v * v).sum::<f64>().sqrt();
val += self.coeffs[j] * self.kernel.eval(r);
let dphi_dr = hermite_dphi_dr(self.kernel, r);
for k in 0..d {
let k01 = if r > 1e-14 {
-dphi_dr * diff[k] / r
} else {
0.0
};
val += self.coeffs[n + k * n + j] * k01;
}
}
Ok(val)
}
}
#[inline]
fn hermite_dphi_dr(kernel: PolyharmonicKernel, r: f64) -> f64 {
match kernel {
PolyharmonicKernel::ThinPlate => {
if r <= 1e-14 {
0.0
} else {
r * (2.0 * r.ln() + 1.0)
}
}
PolyharmonicKernel::R3 => 3.0 * r * r,
PolyharmonicKernel::R5 => 5.0 * r.powi(4),
PolyharmonicKernel::R7 => 7.0 * r.powi(6),
PolyharmonicKernel::Custom(k) => (k as f64) * r.powi(k as i32 - 1),
}
}
#[inline]
fn hermite_d2phi_dr2(kernel: PolyharmonicKernel, r: f64) -> f64 {
match kernel {
PolyharmonicKernel::ThinPlate => {
if r <= 1e-14 {
0.0
} else {
2.0 * r.ln() + 3.0
}
}
PolyharmonicKernel::R3 => 6.0 * r,
PolyharmonicKernel::R5 => 20.0 * r.powi(3),
PolyharmonicKernel::R7 => 42.0 * r.powi(5),
PolyharmonicKernel::Custom(k) => {
let k = k as f64;
k * (k - 1.0) * r.powi(k as i32 - 2)
}
}
}
#[inline]
fn hermite_d2phi_dr2_zero(kernel: PolyharmonicKernel) -> f64 {
match kernel {
PolyharmonicKernel::ThinPlate => 0.0,
PolyharmonicKernel::R3 => 0.0,
PolyharmonicKernel::R5 => 0.0,
PolyharmonicKernel::R7 => 0.0,
PolyharmonicKernel::Custom(_) => 0.0,
}
}
fn solve_linear_system(
a: Array2<f64>,
b: Array1<f64>,
) -> InterpolateResult<Array1<f64>> {
let av = a.view();
let bv = b.view();
solve(&av, &bv, None).map_err(|e| {
InterpolateError::LinalgError(format!("RBF system solve failed: {e}"))
})
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_abs_diff_eq;
use scirs2_core::ndarray::{array, Array2};
fn pts_grid_2d() -> Array2<f64> {
Array2::from_shape_vec(
(9, 2),
vec![
0.0, 0.0, 0.5, 0.0, 1.0, 0.0, 0.0, 0.5, 0.5, 0.5, 1.0, 0.5, 0.0, 1.0, 0.5, 1.0,
1.0, 1.0,
],
)
.expect("test: should succeed")
}
#[test]
fn test_tps_reproduces_linear() {
let pts = pts_grid_2d();
let vals: Array1<f64> = (0..pts.nrows())
.map(|i| pts[[i, 0]] + pts[[i, 1]])
.collect();
let interp = GlobalRbfInterpolant::new_polyharmonic(
&pts.view(),
&vals.view(),
PolyharmonicKernel::ThinPlate,
PolyDegree::Linear,
)
.expect("test: should succeed");
let v = interp.evaluate(&[0.3, 0.7]).expect("test: should succeed");
assert_abs_diff_eq!(v, 1.0, epsilon = 1e-9);
}
#[test]
fn test_gaussian_rbf_interpolation() {
let pts = pts_grid_2d();
let vals: Array1<f64> = (0..pts.nrows())
.map(|i| {
let x = pts[[i, 0]];
let y = pts[[i, 1]];
x * x + y * y
})
.collect();
let interp = GlobalRbfInterpolant::new_gaussian(
&pts.view(),
&vals.view(),
3.0,
PolyDegree::Quadratic,
)
.expect("test: should succeed");
let v = interp.evaluate(&[0.5, 0.5]).expect("test: should succeed");
assert_abs_diff_eq!(v, 0.5, epsilon = 1e-6);
}
#[test]
fn test_wendland_psi31() {
let pts = Array2::from_shape_vec(
(5, 1),
vec![0.0, 0.25, 0.5, 0.75, 1.0],
)
.expect("test: should succeed");
let vals: Array1<f64> = (0..5)
.map(|i| (std::f64::consts::PI * pts[[i, 0]]).sin())
.collect();
let interp =
WendlandInterpolant::new(&pts.view(), &vals.view(), WendlandKernel::Psi31, 1.5)
.expect("test: should succeed");
let v = interp.evaluate(&[0.5]).expect("test: should succeed");
assert_abs_diff_eq!(v, 1.0, epsilon = 1e-6);
}
#[test]
fn test_rbf_fd_weights_sum_to_one() {
let stencil = Array2::from_shape_vec((4, 2), vec![
0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0,
])
.expect("test: should succeed");
let w =
rbf_fd_weights(&[0.5, 0.5], &stencil.view(), PolyharmonicKernel::R3).expect("test: should succeed");
let sum: f64 = w.sum();
assert!(sum.is_finite());
}
#[test]
fn test_hermite_rbf_1d_quadratic() {
let pts = Array2::from_shape_vec(
(5, 1),
vec![0.0, 0.25, 0.5, 0.75, 1.0],
)
.expect("test: should succeed");
let vals = array![0.0_f64, 0.0625, 0.25, 0.5625, 1.0];
let grads = Array2::from_shape_vec((5, 1), vec![0.0, 0.5, 1.0, 1.5, 2.0]).expect("test: should succeed");
let interp = HermiteRbfInterpolant::new(
&pts.view(),
&vals.view(),
&grads.view(),
PolyharmonicKernel::R5,
)
.expect("test: should succeed");
for (x, expected) in &[(0.375_f64, 0.140625_f64), (0.625, 0.390625), (0.875, 0.765625)] {
let v = interp.evaluate(&[*x]).expect("test: should succeed");
assert_abs_diff_eq!(v, expected, epsilon = 2e-3);
}
let v_node = interp.evaluate(&[0.5]).expect("test: should succeed");
assert_abs_diff_eq!(v_node, 0.25, epsilon = 1e-8);
let v_node2 = interp.evaluate(&[0.25]).expect("test: should succeed");
assert_abs_diff_eq!(v_node2, 0.0625, epsilon = 1e-8);
}
#[test]
fn test_polyharmonic_r5_exact_at_nodes() {
let pts = pts_grid_2d();
let vals: Array1<f64> = (0..pts.nrows())
.map(|i| {
let x = pts[[i, 0]];
let y = pts[[i, 1]];
x * x - y * y + 2.0 * x * y
})
.collect();
let interp = GlobalRbfInterpolant::new_polyharmonic(
&pts.view(),
&vals.view(),
PolyharmonicKernel::R5,
PolyDegree::Quadratic,
)
.expect("test: should succeed");
for i in 0..pts.nrows() {
let q = vec![pts[[i, 0]], pts[[i, 1]]];
let v = interp.evaluate(&q).expect("test: should succeed");
assert_abs_diff_eq!(v, vals[i], epsilon = 1e-8);
}
}
#[test]
fn test_wendland_kernel_support() {
assert_abs_diff_eq!(WendlandKernel::Psi31.eval_scaled(1.0), 0.0, epsilon = 1e-15);
assert_abs_diff_eq!(WendlandKernel::Psi31.eval_scaled(2.0), 0.0, epsilon = 1e-15);
assert!(WendlandKernel::Psi31.eval_scaled(0.0) > 0.0);
}
}