#![cfg_attr(not(feature = "std"), no_std)]
#![allow(clippy::needless_range_loop)]
#![allow(clippy::absurd_extreme_comparisons)]
use num_traits::Float;
pub mod multilinear;
pub use multilinear::{MultilinearRectilinear, MultilinearRegular};
pub mod multicubic;
pub use multicubic::{MulticubicRectilinear, MulticubicRegular};
pub mod linear {
pub use crate::multilinear::rectilinear;
pub use crate::multilinear::regular;
}
pub mod cubic {
pub use crate::multicubic::rectilinear;
pub use crate::multicubic::regular;
}
pub mod nearest;
pub use nearest::{NearestRectilinear, NearestRegular};
pub mod one_dim;
pub use one_dim::{
RectilinearGrid1D, RegularGrid1D, hold::Left1D, hold::Nearest1D, hold::Right1D,
linear::Linear1D, linear::LinearHoldLast1D,
};
#[cfg(feature = "par")]
use rayon::{
iter::{IndexedParallelIterator, ParallelIterator},
slice::ParallelSliceMut,
};
#[cfg(feature = "par")]
use std::sync::{LazyLock, Mutex};
#[cfg(feature = "std")]
pub mod utils;
#[cfg(all(test, feature = "std"))]
pub(crate) mod testing;
#[cfg(feature = "python")]
pub mod python;
#[derive(Clone, Copy)]
pub enum GridInterpMethod {
Linear,
Cubic,
Nearest,
}
#[derive(Clone, Copy)]
pub enum GridKind {
Regular,
Rectilinear,
}
const MAXDIMS: usize = 8;
const MAXDIMS_ERR: &str =
"Dimension exceeds maximum (8). Use interpolator struct directly for higher dimensions.";
const MIN_CHUNK_SIZE: usize = 1024;
#[cfg(feature = "par")]
static PHYSICAL_CORES: LazyLock<usize> = LazyLock::new(num_cpus::get_physical);
#[cfg(feature = "par")]
pub fn interpn<T: Float + Send + Sync>(
grids: &[&[T]],
vals: &[T],
obs: &[&[T]],
out: &mut [T],
method: GridInterpMethod,
assume_grid_kind: Option<GridKind>,
linearize_extrapolation: bool,
check_bounds_with_atol: Option<T>,
max_threads: Option<usize>,
) -> Result<(), &'static str> {
let ndims = grids.len();
if ndims > MAXDIMS {
return Err(MAXDIMS_ERR);
}
let n = out.len();
let kind = resolve_grid_kind(assume_grid_kind, grids)?;
if 2 * MIN_CHUNK_SIZE <= n {
let num_cores_physical = *PHYSICAL_CORES; let num_cores_pool = rayon::current_num_threads(); let num_cores_available = num_cores_physical.min(num_cores_pool).max(1); let num_cores = match max_threads {
Some(num_cores_requested) => num_cores_requested.min(num_cores_available),
None => num_cores_available,
};
let chunk = MIN_CHUNK_SIZE.max(n / num_cores);
let result: Mutex<Option<&'static str>> = Mutex::new(None);
let write_err = |msg: &'static str| {
let mut guard = result.lock().unwrap();
if guard.is_none() {
*guard = Some(msg);
}
};
out.par_chunks_mut(chunk).enumerate().for_each(|(i, outc)| {
let start = chunk * i;
let end = start + outc.len();
let mut obs_slices: [&[T]; 8] = [&[]; 8];
for (j, o) in obs.iter().enumerate() {
let s = &o.get(start..end);
match s {
Some(s) => obs_slices[j] = s,
None => {
write_err("Dimension mismatch");
return;
}
};
}
let res_inner = interpn_serial(
grids,
vals,
&obs_slices[..ndims],
outc,
method,
Some(kind),
linearize_extrapolation,
check_bounds_with_atol,
);
match res_inner {
Ok(()) => {}
Err(msg) => write_err(msg),
}
});
match *result.lock().unwrap() {
Some(msg) => Err(msg),
None => Ok(()),
}
} else {
interpn_serial(
grids,
vals,
obs,
out,
method,
Some(kind),
linearize_extrapolation,
check_bounds_with_atol,
)
}
}
#[cfg(feature = "par")]
pub fn interpn_alloc<T: Float + Send + Sync>(
grids: &[&[T]],
vals: &[T],
obs: &[&[T]],
out: Option<Vec<T>>,
method: GridInterpMethod,
assume_grid_kind: Option<GridKind>,
linearize_extrapolation: bool,
check_bounds_with_atol: Option<T>,
max_threads: Option<usize>,
) -> Result<Vec<T>, &'static str> {
if obs.len() == 0 {
return Ok(Vec::with_capacity(0));
}
let mut out = out.unwrap_or_else(|| vec![T::zero(); obs[0].len()]);
interpn(
grids,
vals,
obs,
&mut out,
method,
assume_grid_kind,
linearize_extrapolation,
check_bounds_with_atol,
max_threads,
)?;
Ok(out)
}
pub fn interpn_serial<T: Float>(
grids: &[&[T]],
vals: &[T],
obs: &[&[T]],
out: &mut [T],
method: GridInterpMethod,
assume_grid_kind: Option<GridKind>,
linearize_extrapolation: bool,
check_bounds_with_atol: Option<T>,
) -> Result<(), &'static str> {
let ndims = grids.len();
if ndims > MAXDIMS {
return Err(MAXDIMS_ERR);
}
let kind = resolve_grid_kind(assume_grid_kind, grids)?;
let get_regular_grid = || {
let mut dims = [0_usize; MAXDIMS];
let mut starts = [T::zero(); MAXDIMS];
let mut steps = [T::zero(); MAXDIMS];
for (i, grid) in grids.iter().enumerate() {
if grid.len() < 2 {
return Err("All grids must have at least two entries");
}
dims[i] = grid.len();
starts[i] = grid[0];
steps[i] = grid[1] - grid[0];
}
Ok((dims, starts, steps))
};
let maybe_check_bounds_regular = |dims: &[usize], starts: &[T], steps: &[T], obs: &[&[T]]| {
if let Some(atol) = check_bounds_with_atol {
let mut bounds = [false; MAXDIMS];
let out = &mut bounds[..ndims];
multilinear::regular::check_bounds(
&dims[..ndims],
&starts[..ndims],
&steps[..ndims],
obs,
atol,
out,
)?;
if bounds.iter().any(|x| *x) {
return Err("At least one observation point is outside the grid.");
}
}
Ok(())
};
let maybe_check_bounds_rectilinear = |grids, obs| {
if let Some(atol) = check_bounds_with_atol {
let mut bounds = [false; MAXDIMS];
let out = &mut bounds[..ndims];
multilinear::rectilinear::check_bounds(grids, obs, atol, out)?;
if bounds.iter().any(|x| *x) {
return Err("At least one observation point is outside the grid.");
}
}
Ok(())
};
match (method, kind) {
(GridInterpMethod::Linear, GridKind::Regular) => {
let (dims, starts, steps) = get_regular_grid()?;
maybe_check_bounds_regular(&dims, &starts, &steps, obs)?;
linear::regular::interpn(
&dims[..ndims],
&starts[..ndims],
&steps[..ndims],
vals,
obs,
out,
)
}
(GridInterpMethod::Linear, GridKind::Rectilinear) => {
maybe_check_bounds_rectilinear(grids, obs)?;
linear::rectilinear::interpn(grids, vals, obs, out)
}
(GridInterpMethod::Cubic, GridKind::Regular) => {
let (dims, starts, steps) = get_regular_grid()?;
maybe_check_bounds_regular(&dims, &starts, &steps, obs)?;
cubic::regular::interpn(
&dims[..ndims],
&starts[..ndims],
&steps[..ndims],
vals,
linearize_extrapolation,
obs,
out,
)
}
(GridInterpMethod::Cubic, GridKind::Rectilinear) => {
maybe_check_bounds_rectilinear(grids, obs)?;
cubic::rectilinear::interpn(grids, vals, linearize_extrapolation, obs, out)
}
(GridInterpMethod::Nearest, GridKind::Regular) => {
let (dims, starts, steps) = get_regular_grid()?;
maybe_check_bounds_regular(&dims, &starts, &steps, obs)?;
nearest::regular::interpn(
&dims[..ndims],
&starts[..ndims],
&steps[..ndims],
vals,
obs,
out,
)
}
(GridInterpMethod::Nearest, GridKind::Rectilinear) => {
maybe_check_bounds_rectilinear(grids, obs)?;
nearest::rectilinear::interpn(grids, vals, obs, out)
}
}
}
fn resolve_grid_kind<T: Float>(
assume_grid_kind: Option<GridKind>,
grids: &[&[T]],
) -> Result<GridKind, &'static str> {
let kind = match assume_grid_kind {
Some(GridKind::Regular) => GridKind::Regular,
Some(GridKind::Rectilinear) => GridKind::Rectilinear,
None => {
let mut is_regular = true;
for grid in grids.iter() {
if grid.len() < 2 {
return Err("All grids must have at least two entries");
}
let step = grid[1] - grid[0];
if !grid.windows(2).all(|pair| pair[1] - pair[0] == step) {
is_regular = false;
break;
}
}
if is_regular {
GridKind::Regular
} else {
GridKind::Rectilinear
}
}
};
Ok(kind)
}
#[inline]
pub(crate) fn index_arr_fixed_dims<T: Copy, const N: usize>(
loc: [usize; N],
dimprod: [usize; N],
data: &[T],
) -> T {
let mut i = 0;
for j in 0..N {
i += loc[j] * dimprod[j];
}
data[i]
}
#[inline(always)]
pub(crate) fn validate_regular_grid<T: Float, const N: usize>(
dims: &[usize; N],
steps: &[T; N],
vals: &[T],
) -> Result<(), &'static str> {
let nvals: usize = dims.iter().product();
if vals.len() != nvals {
return Err("Dimension mismatch");
}
let degenerate = dims.iter().any(|&x| x < 2);
if degenerate {
return Err("All grids must have at least two entries");
}
let steps_are_positive = steps.iter().all(|&x| x > T::zero());
if !steps_are_positive {
return Err("All grids must be monotonically increasing");
}
Ok(())
}
#[inline(always)]
pub(crate) fn validate_rectilinear_grid<T: Float, const N: usize>(
grids: &[&[T]; N],
vals: &[T],
) -> Result<[usize; N], &'static str> {
let mut dims = [1_usize; N];
(0..N).for_each(|i| dims[i] = grids[i].len());
let nvals: usize = dims.iter().product();
if vals.len() != nvals {
return Err("Dimension mismatch");
}
let degenerate = dims.iter().any(|&x| x < 2);
if degenerate {
return Err("All grids must have at least 2 entries");
}
let monotonic_maybe = grids.iter().all(|&g| g[1] > g[0]);
if !monotonic_maybe {
return Err("All grids must be monotonically increasing");
}
Ok(dims)
}
#[doc(hidden)]
#[macro_export]
macro_rules! dispatch_ndims {
($ndims:expr, $err:expr, [$($n:literal),+ $(,)?], |$N:ident| $body:expr $(,)?) => {{
match $ndims {
$(
$n => {
const $N: usize = $n;
$body
}
)+
_ => Err($err),
}
}};
}