use ndarray::{Array, Array1, ArrayBase, Data, Dimension};
use super::_native::{run_evaluate_variogram, run_select_variogram, run_variogram};
use crate::common::arrays::{ArrayLike, Element, as_float64_1d, filter_finite_xyz, validate_same_size_xyz};
use crate::common::exceptions::{ApbaseError, Result, SELECT_STATUS_ERRORS, raise_status_error};
use crate::config::{resolve_n_threads, resolve_variogram_max_pairs_ceiling};
pub const VARIOGRAM_NAMES: [(i64, &str); 3] = [(1, "spherical"), (2, "exponential"), (3, "gaussian")];
pub(crate) fn variogram_name(model_id: i64) -> Option<&'static str> {
VARIOGRAM_NAMES
.iter()
.find(|(id, _)| *id == model_id)
.map(|(_, name)| *name)
}
#[derive(Clone, Debug, PartialEq)]
pub struct ModelParams {
pub model_id: i64,
pub model_name: &'static str,
pub nugget: f64,
pub partial_sill: f64,
pub sill: f64,
pub range: f64,
pub sse: Option<f64>,
}
pub const FULL_RANGE_RADIUS_MAX_POINTS: usize = 10_000;
impl ModelParams {
pub fn search_radius(&self, n_points: usize) -> f64 {
if n_points < FULL_RANGE_RADIUS_MAX_POINTS {
self.range
} else {
self.range / 3.0
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct VariogramOptions {
pub n_lags: i64,
pub max_pairs: i64,
pub max_distance: f64,
}
impl Default for VariogramOptions {
fn default() -> Self {
VariogramOptions {
n_lags: 50,
max_pairs: 100_000,
max_distance: 0.0,
}
}
}
pub trait DistanceLike {
type Output;
fn distances(&self) -> Vec<f64>;
fn reshape(&self, gamma: Vec<f64>) -> Self::Output;
}
impl DistanceLike for f64 {
type Output = f64;
fn distances(&self) -> Vec<f64> {
vec![*self]
}
fn reshape(&self, gamma: Vec<f64>) -> f64 {
gamma[0]
}
}
impl<T: Element> DistanceLike for [T] {
type Output = Array1<f64>;
fn distances(&self) -> Vec<f64> {
self.to_f64_vec()
}
fn reshape(&self, gamma: Vec<f64>) -> Array1<f64> {
Array1::from(gamma)
}
}
impl<T: Element> DistanceLike for Vec<T> {
type Output = Array1<f64>;
fn distances(&self) -> Vec<f64> {
self.to_f64_vec()
}
fn reshape(&self, gamma: Vec<f64>) -> Array1<f64> {
Array1::from(gamma)
}
}
impl<T: Element, const N: usize> DistanceLike for [T; N] {
type Output = Array1<f64>;
fn distances(&self) -> Vec<f64> {
self.to_f64_vec()
}
fn reshape(&self, gamma: Vec<f64>) -> Array1<f64> {
Array1::from(gamma)
}
}
impl<S, D> DistanceLike for ArrayBase<S, D>
where
S: Data,
S::Elem: Element,
D: Dimension,
{
type Output = Array<f64, D>;
fn distances(&self) -> Vec<f64> {
ArrayLike::to_f64_vec(self)
}
fn reshape(&self, gamma: Vec<f64>) -> Array<f64, D> {
Array::from_shape_vec(self.raw_dim(), gamma).expect("one semivariance per distance")
}
}
#[derive(Clone, Debug)]
pub struct Variogram {
n_lags: i64,
max_pairs: i64,
max_distance: f64,
x: Option<Array1<f64>>,
y: Option<Array1<f64>>,
z: Option<Array1<f64>>,
model_values: Option<Array1<f64>>,
model_params: Option<ModelParams>,
fitted: bool,
}
impl Variogram {
pub fn new(options: VariogramOptions) -> Result<Self> {
let variogram = Variogram {
n_lags: options.n_lags,
max_pairs: options.max_pairs,
max_distance: options.max_distance,
x: None,
y: None,
z: None,
model_values: None,
model_params: None,
fitted: false,
};
variogram.validate_config()?;
Ok(variogram)
}
pub fn n_lags(&self) -> i64 {
self.n_lags
}
pub fn max_pairs(&self) -> i64 {
self.max_pairs
}
pub fn max_distance(&self) -> f64 {
self.max_distance
}
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: VariogramOptions,
) -> Result<Self> {
let mut variogram = Variogram::new(options)?;
variogram.fit(x, y, z)?;
Ok(variogram)
}
pub fn from_model(model_values: &(impl ArrayLike + ?Sized)) -> Result<Self> {
let mut variogram = Variogram::new(VariogramOptions::default())?;
variogram.set_model(model_values)?;
Ok(variogram)
}
pub fn is_fitted(&self) -> bool {
self.fitted
}
pub fn model_values(&self) -> Result<&Array1<f64>> {
self.require_fitted()?;
Ok(self
.model_values
.as_ref()
.expect("fitted variogram has model values"))
}
pub fn model_params(&self) -> Result<ModelParams> {
self.require_fitted()?;
Ok(self
.model_params
.clone()
.expect("fitted variogram has model params"))
}
pub fn range(&self) -> Result<f64> {
Ok(self.model_params()?.range)
}
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.is_empty() {
return Err(ApbaseError::value_error(
"variogram requires at least one finite point",
));
}
let model_values = run_variogram(
x_valid.as_slice().expect("contiguous"),
y_valid.as_slice().expect("contiguous"),
z_valid.as_slice().expect("contiguous"),
self.n_lags,
self.max_pairs,
self.max_distance,
self.n_threads(),
)?;
let model_params = Variogram::select_model_params(&model_values)?;
self.x = Some(x_valid);
self.y = Some(y_valid);
self.z = Some(z_valid);
self.model_values = Some(model_values);
self.model_params = Some(model_params);
self.fitted = true;
Ok(self)
}
pub fn set_model(&mut self, model_values: &(impl ArrayLike + ?Sized)) -> Result<&mut Self> {
let model_array = Variogram::normalize_model_values(model_values)?;
let model_params = Variogram::select_model_params(&model_array)?;
self.x = None;
self.y = None;
self.z = None;
self.model_values = Some(model_array);
self.model_params = Some(model_params);
self.fitted = true;
Ok(self)
}
pub fn evaluate<D: DistanceLike + ?Sized>(&self, distance: &D) -> Result<D::Output> {
self.require_fitted()?;
let (gamma, status) = run_evaluate_variogram(
self.model_values()?.as_slice().expect("contiguous"),
&distance.distances(),
self.n_threads(),
)?;
if status != 0 {
return Err(raise_status_error(
status,
SELECT_STATUS_ERRORS,
"error evaluating variogram",
));
}
Ok(distance.reshape(gamma))
}
pub fn call<D: DistanceLike + ?Sized>(&self, distance: &D) -> Result<D::Output> {
self.evaluate(distance)
}
fn require_fitted(&self) -> Result<()> {
if !self.fitted {
return Err(ApbaseError::value_error(
"call fit(x, y, z) or set_model(model_values) before using the variogram",
));
}
Ok(())
}
fn validate_config(&self) -> Result<()> {
if self.n_lags <= 0 {
return Err(ApbaseError::value_error("n_lags must be greater than zero"));
}
if self.max_pairs < 0 {
return Err(ApbaseError::value_error("max_pairs must be >= 0"));
}
let max_pairs_ceiling = resolve_variogram_max_pairs_ceiling();
if self.max_pairs > max_pairs_ceiling {
return Err(ApbaseError::value_error(format!(
"max_pairs must be <= {max_pairs_ceiling}"
)));
}
if !self.max_distance.is_finite() || self.max_distance < 0.0 {
return Err(ApbaseError::value_error("max_distance must be finite and >= 0"));
}
Ok(())
}
pub(crate) fn normalize_model_values(model_values: &(impl ArrayLike + ?Sized)) -> Result<Array1<f64>> {
let model_array = as_float64_1d(model_values, "model_values")?;
if model_array.len() != 5 {
return Err(ApbaseError::value_error("model_values must contain 5 values"));
}
Ok(model_array)
}
pub(crate) fn select_model_params(model_values: &Array1<f64>) -> Result<ModelParams> {
let (params, status) = run_select_variogram(model_values.as_slice().expect("contiguous"))?;
if status != 0 {
return Err(raise_status_error(
status,
SELECT_STATUS_ERRORS,
"error selecting variogram",
));
}
let model_id = crate::common::_numpy::rint(params[0]) as i64;
let Some(model_name) = variogram_name(model_id) else {
return Err(ApbaseError::value_error(format!(
"invalid model returned by the native extension: {model_id}"
)));
};
Ok(ModelParams {
model_id,
model_name,
nugget: params[1],
partial_sill: params[2],
sill: params[3],
range: params[4],
sse: Some(params[5]),
})
}
}