use crate::error::{InterpolateError, InterpolateResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1, ArrayView2};
use std::collections::HashMap;
use std::f64::consts::PI;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Triangle {
a: usize,
b: usize,
c: usize,
}
impl Triangle {
fn new(a: usize, b: usize, c: usize) -> Self {
Self { a, b, c }
}
fn circumcircle(&self, pts: &[[f64; 2]]) -> Option<([f64; 2], f64)> {
let [ax, ay] = pts[self.a];
let [bx, by] = pts[self.b];
let [cx, cy] = pts[self.c];
let d = 2.0 * (ax * (by - cy) + bx * (cy - ay) + cx * (ay - by));
if d.abs() < 1e-14 {
return None; }
let ux = ((ax * ax + ay * ay) * (by - cy)
+ (bx * bx + by * by) * (cy - ay)
+ (cx * cx + cy * cy) * (ay - by))
/ d;
let uy = ((ax * ax + ay * ay) * (cx - bx)
+ (bx * bx + by * by) * (ax - cx)
+ (cx * cx + cy * cy) * (bx - ax))
/ d;
let r2 = (ax - ux).powi(2) + (ay - uy).powi(2);
Some(([ux, uy], r2))
}
fn edges(&self) -> [(usize, usize); 3] {
[
(self.a.min(self.b), self.a.max(self.b)),
(self.b.min(self.c), self.b.max(self.c)),
(self.a.min(self.c), self.a.max(self.c)),
]
}
}
#[derive(Debug, Clone)]
struct DelaunayTriangulation {
pts: Vec<[f64; 2]>,
triangles: Vec<Triangle>,
n_real: usize,
}
impl DelaunayTriangulation {
fn new(points: &[[f64; 2]]) -> InterpolateResult<Self> {
let n_real = points.len();
if n_real < 3 {
return Err(InterpolateError::InsufficientData(
"Delaunay triangulation requires at least 3 points".to_string(),
));
}
let (mut xmin, mut xmax) = (f64::INFINITY, f64::NEG_INFINITY);
let (mut ymin, mut ymax) = (f64::INFINITY, f64::NEG_INFINITY);
for &[x, y] in points {
xmin = xmin.min(x);
xmax = xmax.max(x);
ymin = ymin.min(y);
ymax = ymax.max(y);
}
let dx = (xmax - xmin).max(1e-8);
let dy = (ymax - ymin).max(1e-8);
let delta = dx.max(dy);
let sx = xmin + dx * 0.5;
let sy = ymin + dy * 0.5;
let st0 = [sx - 20.0 * delta, sy - delta];
let st1 = [sx, sy + 20.0 * delta];
let st2 = [sx + 20.0 * delta, sy - delta];
let mut pts: Vec<[f64; 2]> = Vec::with_capacity(n_real + 3);
pts.extend_from_slice(points);
pts.push(st0);
pts.push(st1);
pts.push(st2);
let super_tri = Triangle::new(n_real, n_real + 1, n_real + 2);
let mut dt = DelaunayTriangulation {
pts,
triangles: vec![super_tri],
n_real,
};
for i in 0..n_real {
dt.insert(i)?;
}
dt.triangles.retain(|t| {
t.a < n_real && t.b < n_real && t.c < n_real
});
Ok(dt)
}
fn insert(&mut self, idx: usize) -> InterpolateResult<()> {
let p = self.pts[idx];
let mut bad: Vec<usize> = Vec::new();
for (ti, tri) in self.triangles.iter().enumerate() {
if let Some(([cx, cy], r2)) = tri.circumcircle(&self.pts) {
let d2 = (p[0] - cx).powi(2) + (p[1] - cy).powi(2);
if d2 < r2 + 1e-14 {
bad.push(ti);
}
}
}
let mut edge_count: HashMap<(usize, usize), usize> = HashMap::new();
for &ti in &bad {
for edge in self.triangles[ti].edges() {
*edge_count.entry(edge).or_insert(0) += 1;
}
}
let boundary: Vec<(usize, usize)> =
edge_count.into_iter().filter(|&(_, c)| c == 1).map(|(e, _)| e).collect();
let mut bad_sorted = bad.clone();
bad_sorted.sort_unstable_by(|a, b| b.cmp(a));
for ti in bad_sorted {
self.triangles.swap_remove(ti);
}
for (ea, eb) in boundary {
self.triangles.push(Triangle::new(ea, eb, idx));
}
Ok(())
}
fn find_containing_triangle(&self, p: [f64; 2]) -> Option<usize> {
for (ti, tri) in self.triangles.iter().enumerate() {
if self.point_in_triangle(p, tri) {
return Some(ti);
}
}
None
}
fn point_in_triangle(&self, p: [f64; 2], tri: &Triangle) -> bool {
let [ax, ay] = self.pts[tri.a];
let [bx, by] = self.pts[tri.b];
let [cx, cy] = self.pts[tri.c];
let [px, py] = p;
let d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by);
let d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy);
let d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay);
let has_neg = (d1 < 0.0) || (d2 < 0.0) || (d3 < 0.0);
let has_pos = (d1 > 0.0) || (d2 > 0.0) || (d3 > 0.0);
!(has_neg && has_pos)
}
fn neighbours(&self, idx: usize) -> Vec<usize> {
let mut set: std::collections::HashSet<usize> = std::collections::HashSet::new();
for tri in &self.triangles {
if tri.a == idx || tri.b == idx || tri.c == idx {
for &v in &[tri.a, tri.b, tri.c] {
if v != idx {
set.insert(v);
}
}
}
}
set.into_iter().collect()
}
}
fn voronoi_cell_area(dt: &DelaunayTriangulation, idx: usize) -> f64 {
let mut centres: Vec<[f64; 2]> = Vec::new();
for tri in &dt.triangles {
if tri.a == idx || tri.b == idx || tri.c == idx {
if let Some((cc, _)) = tri.circumcircle(&dt.pts) {
centres.push(cc);
}
}
}
if centres.len() < 3 {
return 0.0;
}
let [vx, vy] = dt.pts[idx];
centres.sort_by(|a, b| {
let ta = (a[1] - vy).atan2(a[0] - vx);
let tb = (b[1] - vy).atan2(b[0] - vx);
ta.partial_cmp(&tb).unwrap_or(std::cmp::Ordering::Equal)
});
polygon_area(¢res)
}
fn polygon_area(pts: &[[f64; 2]]) -> f64 {
let n = pts.len();
if n < 3 {
return 0.0;
}
let mut sum = 0.0_f64;
for i in 0..n {
let j = (i + 1) % n;
sum += pts[i][0] * pts[j][1] - pts[j][0] * pts[i][1];
}
sum.abs() * 0.5
}
fn sibson_weights(
base: &DelaunayTriangulation,
query: [f64; 2],
) -> InterpolateResult<Vec<(usize, f64)>> {
let q_idx = base.pts.len();
let mut tmp_pts = base.pts.clone();
tmp_pts.push(query);
let all_pts: Vec<[f64; 2]> = base.pts[..base.n_real].iter().copied().chain(std::iter::once(query)).collect();
let tmp_dt = match DelaunayTriangulation::new(&all_pts) {
Ok(dt) => dt,
Err(_) => {
return Ok(Vec::new());
}
};
let q_in_tmp = base.n_real;
let neighbours_of_q = tmp_dt.neighbours(q_in_tmp);
if neighbours_of_q.is_empty() {
return Ok(Vec::new());
}
let mut raw_weights: Vec<(usize, f64)> = Vec::new();
let mut total = 0.0_f64;
for ni in &neighbours_of_q {
if *ni >= base.n_real {
continue; }
let area_base = voronoi_cell_area(base, *ni);
let area_tmp = voronoi_cell_area(&tmp_dt, *ni);
let stolen = (area_base - area_tmp).max(0.0);
raw_weights.push((*ni, stolen));
total += stolen;
}
if total < 1e-15 {
return Ok(Vec::new());
}
let weights: Vec<(usize, f64)> = raw_weights.iter().map(|&(i, w)| (i, w / total)).collect();
Ok(weights)
}
fn laplace_weights(
dt: &DelaunayTriangulation,
query: [f64; 2],
) -> InterpolateResult<Vec<(usize, f64)>> {
let all_pts: Vec<[f64; 2]> = dt.pts[..dt.n_real].iter().copied().chain(std::iter::once(query)).collect();
let tmp_dt = match DelaunayTriangulation::new(&all_pts) {
Ok(d) => d,
Err(_) => return Ok(Vec::new()),
};
let q_in_tmp = dt.n_real; let neighbours = tmp_dt.neighbours(q_in_tmp);
if neighbours.is_empty() {
return Ok(Vec::new());
}
let mut raw: Vec<(usize, f64)> = Vec::new();
let mut total = 0.0_f64;
for &ni in &neighbours {
if ni >= dt.n_real {
continue;
}
let shared_tris: Vec<&Triangle> = tmp_dt
.triangles
.iter()
.filter(|t| {
(t.a == q_in_tmp || t.b == q_in_tmp || t.c == q_in_tmp)
&& (t.a == ni || t.b == ni || t.c == ni)
})
.collect();
let voro_edge_len = if shared_tris.len() >= 2 {
let cc0 = shared_tris[0].circumcircle(&tmp_dt.pts).map(|(c, _)| c);
let cc1 = shared_tris[1].circumcircle(&tmp_dt.pts).map(|(c, _)| c);
match (cc0, cc1) {
(Some([x0, y0]), Some([x1, y1])) => {
((x1 - x0).powi(2) + (y1 - y0).powi(2)).sqrt()
}
_ => 0.0,
}
} else if shared_tris.len() == 1 {
let cc = shared_tris[0].circumcircle(&tmp_dt.pts).map(|(c, _)| c);
let [qx, qy] = tmp_dt.pts[q_in_tmp];
let [nx, ny] = tmp_dt.pts[ni];
let mx = (qx + nx) * 0.5;
let my = (qy + ny) * 0.5;
match cc {
Some([cx, cy]) => ((cx - mx).powi(2) + (cy - my).powi(2)).sqrt() * 2.0,
None => 0.0,
}
} else {
0.0
};
let [qx, qy] = query;
let [nx, ny] = dt.pts[ni];
let dist = ((qx - nx).powi(2) + (qy - ny).powi(2)).sqrt().max(1e-14);
let lambda = voro_edge_len / dist;
raw.push((ni, lambda));
total += lambda;
}
if total < 1e-15 {
return Ok(Vec::new());
}
Ok(raw.iter().map(|&(i, w)| (i, w / total)).collect())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NaturalNeighborVariant {
Sibson,
Laplace,
}
#[derive(Debug, Clone)]
pub struct NaturalNeighborInterp {
dt: DelaunayTriangulation,
values: Vec<f64>,
variant: NaturalNeighborVariant,
}
impl NaturalNeighborInterp {
pub fn fit(
points: &Array2<f64>,
values: &Array1<f64>,
variant: NaturalNeighborVariant,
) -> InterpolateResult<Self> {
let n = points.nrows();
let d = points.ncols();
if d != 2 {
return Err(InterpolateError::InvalidInput {
message: format!(
"NaturalNeighborInterp requires 2-D input points, got {d}-D"
),
});
}
if n != values.len() {
return Err(InterpolateError::DimensionMismatch(format!(
"points has {n} rows but values has {} entries",
values.len()
)));
}
if n < 3 {
return Err(InterpolateError::InsufficientData(
"NaturalNeighborInterp requires at least 3 input sites".to_string(),
));
}
let raw_pts: Vec<[f64; 2]> = (0..n)
.map(|i| [points[[i, 0]], points[[i, 1]]])
.collect();
let dt = DelaunayTriangulation::new(&raw_pts)?;
let vals: Vec<f64> = values.iter().copied().collect();
Ok(Self { dt, values: vals, variant })
}
pub fn interpolate(&self, query: &Array2<f64>) -> InterpolateResult<Array1<f64>> {
let m = query.nrows();
let d = query.ncols();
if d != 2 {
return Err(InterpolateError::InvalidInput {
message: format!(
"NaturalNeighborInterp.interpolate requires 2-D query points, got {d}-D"
),
});
}
let mut result = Array1::zeros(m);
for i in 0..m {
let q = [query[[i, 0]], query[[i, 1]]];
result[i] = self.interpolate_single(q)?;
}
Ok(result)
}
pub fn interpolate_single(&self, q: [f64; 2]) -> InterpolateResult<f64> {
for (i, pt) in self.dt.pts[..self.dt.n_real].iter().enumerate() {
let dx = q[0] - pt[0];
let dy = q[1] - pt[1];
if dx * dx + dy * dy < 1e-20 {
return Ok(self.values[i]);
}
}
let weights = match self.variant {
NaturalNeighborVariant::Sibson => sibson_weights(&self.dt, q)?,
NaturalNeighborVariant::Laplace => laplace_weights(&self.dt, q)?,
};
if weights.is_empty() {
return self.nearest_value(q);
}
let mut acc = 0.0_f64;
for (idx, w) in weights {
if idx < self.values.len() {
acc += w * self.values[idx];
}
}
Ok(acc)
}
fn nearest_value(&self, q: [f64; 2]) -> InterpolateResult<f64> {
let mut best_idx = 0usize;
let mut best_d2 = f64::INFINITY;
for (i, pt) in self.dt.pts[..self.dt.n_real].iter().enumerate() {
let d2 = (q[0] - pt[0]).powi(2) + (q[1] - pt[1]).powi(2);
if d2 < best_d2 {
best_d2 = d2;
best_idx = i;
}
}
if best_idx < self.values.len() {
Ok(self.values[best_idx])
} else {
Err(InterpolateError::InsufficientData(
"No data sites available for nearest-neighbour fallback".to_string(),
))
}
}
pub fn n_sites(&self) -> usize {
self.dt.n_real
}
pub fn variant(&self) -> NaturalNeighborVariant {
self.variant
}
}
pub fn make_sibson_natural_neighbor(
points: &Array2<f64>,
values: &Array1<f64>,
) -> InterpolateResult<NaturalNeighborInterp> {
NaturalNeighborInterp::fit(points, values, NaturalNeighborVariant::Sibson)
}
pub fn make_laplace_natural_neighbor(
points: &Array2<f64>,
values: &Array1<f64>,
) -> InterpolateResult<NaturalNeighborInterp> {
NaturalNeighborInterp::fit(points, values, NaturalNeighborVariant::Laplace)
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::{Array1, Array2};
fn grid_points() -> (Array2<f64>, Array1<f64>) {
let pts = Array2::from_shape_vec(
(9, 2),
vec![
0.0, 0.0, 1.0, 0.0, 2.0, 0.0, 0.0, 1.0, 1.0, 1.0, 2.0, 1.0, 0.0, 2.0, 1.0, 2.0,
2.0, 2.0,
],
)
.expect("valid shape");
let vals: Array1<f64> = Array1::from_vec(vec![0.0, 1.0, 2.0, 1.0, 2.0, 3.0, 2.0, 3.0, 4.0]);
(pts, vals)
}
#[test]
fn test_sibson_exact_at_sites() {
let (pts, vals) = grid_points();
let interp = NaturalNeighborInterp::fit(&pts, &vals, NaturalNeighborVariant::Sibson)
.expect("fit should succeed");
for i in 0..pts.nrows() {
let q = Array2::from_shape_vec((1, 2), vec![pts[[i, 0]], pts[[i, 1]]]).expect("test: should succeed");
let r = interp.interpolate(&q).expect("test: should succeed");
assert!(
(r[0] - vals[i]).abs() < 1e-8,
"Exact reproduction failed at site {i}: got {}, expected {}",
r[0],
vals[i]
);
}
}
#[test]
fn test_laplace_interior_point() {
let (pts, vals) = grid_points();
let interp = NaturalNeighborInterp::fit(&pts, &vals, NaturalNeighborVariant::Laplace)
.expect("fit should succeed");
let q = Array2::from_shape_vec((1, 2), vec![1.0_f64, 1.0]).expect("test: should succeed");
let r = interp.interpolate(&q).expect("test: should succeed");
assert!(
(r[0] - 2.0).abs() < 0.5,
"Interior interpolation off: got {}", r[0]
);
}
#[test]
fn test_sibson_linear_field() {
let pts = Array2::from_shape_vec(
(6, 2),
vec![0.0, 0.0, 3.0, 0.0, 0.0, 3.0, 3.0, 3.0, 1.5, 0.0, 0.0, 1.5],
)
.expect("test: should succeed");
let a = 2.0_f64;
let b = 3.0_f64;
let c = 1.0_f64;
let vals: Array1<f64> = Array1::from_vec(
(0..6)
.map(|i| a * pts[[i, 0]] + b * pts[[i, 1]] + c)
.collect(),
);
let interp = NaturalNeighborInterp::fit(&pts, &vals, NaturalNeighborVariant::Sibson)
.expect("test: should succeed");
let qx = 1.0_f64;
let qy = 1.0_f64;
let q = Array2::from_shape_vec((1, 2), vec![qx, qy]).expect("test: should succeed");
let r = interp.interpolate(&q).expect("test: should succeed");
let expected = a * qx + b * qy + c;
assert!(
(r[0] - expected).abs() < 0.5,
"Linear field not reproduced: got {}, expected {expected}", r[0]
);
}
#[test]
fn test_n_sites() {
let (pts, vals) = grid_points();
let interp = make_sibson_natural_neighbor(&pts, &vals).expect("test: should succeed");
assert_eq!(interp.n_sites(), 9);
}
#[test]
fn test_wrong_dimension_error() {
let pts3d = Array2::<f64>::zeros((5, 3));
let vals = Array1::<f64>::zeros(5);
let result = NaturalNeighborInterp::fit(&pts3d, &vals, NaturalNeighborVariant::Sibson);
assert!(result.is_err());
}
#[test]
fn test_too_few_points_error() {
let pts = Array2::from_shape_vec((2, 2), vec![0.0, 0.0, 1.0, 1.0]).expect("test: should succeed");
let vals = Array1::from_vec(vec![0.0, 1.0]);
let result = NaturalNeighborInterp::fit(&pts, &vals, NaturalNeighborVariant::Sibson);
assert!(result.is_err());
}
}