use ndarray::Array1;
use super::_native::run_idw;
use crate::common::arrays::{
ArrayLike, Targets, as_float64_1d, extract_target_xy, filter_finite_xyz, validate_same_size_xyz,
};
use crate::common::exceptions::{ApbaseError, IDW_STATUS_ERRORS, Result, raise_status_error};
use crate::common::validation::require_neighbor_bounds;
use crate::config::{resolve_n_threads, resolve_variogram_profile};
use crate::variogram::{ModelParams, Variogram, VariogramOptions};
#[derive(Clone, Debug, PartialEq)]
pub struct IDWOptions {
pub radius: Option<f64>,
pub power: f64,
pub max_neighbors: i64,
pub min_neighbors: i64,
}
impl Default for IDWOptions {
fn default() -> Self {
IDWOptions {
radius: None,
power: 2.0,
max_neighbors: 40,
min_neighbors: 3,
}
}
}
#[allow(clippy::upper_case_acronyms)]
#[derive(Clone, Debug)]
pub struct IDW {
radius: Option<f64>,
power: f64,
max_neighbors: i64,
min_neighbors: i64,
x: Option<Array1<f64>>,
y: Option<Array1<f64>>,
z: Option<Array1<f64>>,
model_params: Option<ModelParams>,
fitted: bool,
}
impl IDW {
pub fn new(options: IDWOptions) -> Result<Self> {
IDW::validate_config(
options.radius,
options.power,
options.max_neighbors,
options.min_neighbors,
)?;
Ok(IDW {
radius: options.radius,
power: options.power,
max_neighbors: options.max_neighbors,
min_neighbors: options.min_neighbors,
x: None,
y: None,
z: None,
model_params: None,
fitted: false,
})
}
pub fn radius(&self) -> Option<f64> {
self.radius
}
pub fn power(&self) -> f64 {
self.power
}
pub fn max_neighbors(&self) -> i64 {
self.max_neighbors
}
pub fn min_neighbors(&self) -> i64 {
self.min_neighbors
}
pub fn n_threads(&self) -> i64 {
resolve_n_threads()
}
pub fn from_data(
x: &(impl ArrayLike + ?Sized),
y: &(impl ArrayLike + ?Sized),
z: &(impl ArrayLike + ?Sized),
options: IDWOptions,
) -> Result<Self> {
let mut idw = IDW::new(options)?;
idw.fit(x, y, z)?;
Ok(idw)
}
pub fn is_fitted(&self) -> bool {
self.fitted
}
pub fn model_params(&self) -> Result<ModelParams> {
self.require_fitted()?;
self.model_params.clone().ok_or_else(|| {
ApbaseError::value_error("IDW was fit with an explicit radius; no variogram model was fitted")
})
}
pub fn fit(
&mut self,
x: &(impl ArrayLike + ?Sized),
y: &(impl ArrayLike + ?Sized),
z: &(impl ArrayLike + ?Sized),
) -> Result<&mut Self> {
let x_array = as_float64_1d(x, "x")?;
let y_array = as_float64_1d(y, "y")?;
let z_array = as_float64_1d(z, "z")?;
validate_same_size_xyz(&x_array, &y_array, &z_array)?;
let (x_valid, y_valid, z_valid) = filter_finite_xyz(&x_array, &y_array, &z_array);
if (x_valid.len() as i64) < self.min_neighbors {
return Err(ApbaseError::value_error(
"IDW requires at least min_neighbors finite points",
));
}
let mut model_params = None;
if self.radius.is_none() {
let (n_lags, max_pairs, max_distance) = resolve_variogram_profile();
let variogram = Variogram::from_data(
&x_valid,
&y_valid,
&z_valid,
VariogramOptions {
n_lags,
max_pairs,
max_distance,
},
)?;
model_params = Some(variogram.model_params()?);
}
self.x = Some(x_valid);
self.y = Some(y_valid);
self.z = Some(z_valid);
self.model_params = model_params;
self.fitted = true;
Ok(self)
}
pub fn interpolate(&self, targets: &(impl Targets + ?Sized)) -> Result<Array1<f64>> {
self.require_fitted()?;
let (x, y, z) = (self.x.as_ref(), self.y.as_ref(), self.z.as_ref());
let (x, y, z) = (x.expect("fitted"), y.expect("fitted"), z.expect("fitted"));
let radius_value = self.resolve_radius(self.model_params.as_ref(), x.len())?;
if (x.len() as i64) < self.min_neighbors {
return Err(ApbaseError::value_error(
"IDW requires at least min_neighbors finite points",
));
}
let (target_x, target_y) = extract_target_xy(targets)?;
if target_x.is_empty() {
return Err(ApbaseError::value_error(
"targets must contain at least one target point",
));
}
let (estimates, status) = run_idw(
x.as_slice().expect("contiguous"),
y.as_slice().expect("contiguous"),
z.as_slice().expect("contiguous"),
target_x.as_slice().expect("contiguous"),
target_y.as_slice().expect("contiguous"),
radius_value,
self.power,
self.max_neighbors,
self.min_neighbors,
self.n_threads(),
)?;
if status != 0 {
return Err(raise_status_error(
status,
IDW_STATUS_ERRORS,
"native IDW execution failed",
));
}
Ok(estimates)
}
pub fn predict(&self, targets: &(impl Targets + ?Sized)) -> Result<Array1<f64>> {
self.interpolate(targets)
}
pub fn call(&self, targets: &(impl Targets + ?Sized)) -> Result<Array1<f64>> {
self.interpolate(targets)
}
fn require_fitted(&self) -> Result<()> {
if !self.fitted {
return Err(ApbaseError::value_error(
"call fit(x, y, z) before interpolate(targets)",
));
}
Ok(())
}
fn resolve_radius(&self, model_params: Option<&ModelParams>, n_points: usize) -> Result<f64> {
if let Some(radius) = self.radius {
return Ok(radius);
}
let Some(model_params) = model_params else {
return Err(ApbaseError::value_error(
"IDW requires a fitted variogram or explicit radius",
));
};
let radius_value = model_params.search_radius(n_points);
if !radius_value.is_finite() || radius_value <= 0.0 {
return Err(ApbaseError::value_error(
"radius must be finite and greater than zero",
));
}
Ok(radius_value)
}
fn validate_config(
radius: Option<f64>,
power: f64,
max_neighbors: i64,
min_neighbors: i64,
) -> Result<()> {
if let Some(radius) = radius {
if !radius.is_finite() || radius <= 0.0 {
return Err(ApbaseError::value_error(
"radius must be finite and greater than zero",
));
}
}
if !power.is_finite() || power <= 0.0 {
return Err(ApbaseError::value_error(
"power must be finite and greater than zero",
));
}
require_neighbor_bounds(max_neighbors, min_neighbors, None)
}
}