The ninterp crate provides multivariate interpolation over rectilinear grids of any dimensionality.
It is built on ndarray and uses ndarray arrays/views throughout its API.
Hard-coded interpolators are provided for N = 1, 2, and 3, based on the observed runtime tradeoff versus a general N-D implementation.
For higher dimensionalities (N >= 4), use InterpND.
All interpolators work with both owned and borrowed arrays (array views) of various types.
A variety of interpolation strategies are implemented and exposed in the prelude module.
Custom interpolation strategies can be defined in downstream crates.
cargo add ninterp
Bring common API types into scope:
use ninterp::prelude::*;Minimal end-to-end interpolation example:
use ndarray::prelude::*;
use ninterp::prelude::*;
let interp = Interp1D::new(
array![0.0, 1.0, 2.0, 3.0], // x
array![0.0, 1.0, 4.0, 9.0], // f(x)
strategy::Linear,
Extrapolate::Error,
)
.unwrap();
let y = interp.interpolate(&[1.5]).unwrap();
assert_eq!(y, 2.5);Minimal N-D interpolation example:
use ndarray::prelude::*;
use ninterp::prelude::*;
let interp_nd = InterpND::new(
// grid
vec![
array![0.0, 1.0], // x0, x1
array![0.0, 1.0], // y0, y1
],
// values
array![
[0.0, 1.0], // f(x0, y0), f(x0, y1)
[1.0, 2.0], // f(x1, y0), f(x1, y1)
].into_dyn(),
strategy::Linear,
Extrapolate::Error,
)
.unwrap();
let z = interp_nd.interpolate(&[0.25, 0.75]).unwrap();
assert_eq!(z, 1.0);Instantiation is done by calling an interpolator's new method.
For dimensionalities N >= 1, this executes a validation step that prevents runtime panics.
serde: support forserde1.x using ndarray's built-in array formatcargo add ninterp --features serdeserde_ndim: enableserdefeature and switch output to the column-major nested array format fromserde-ndimcargo add ninterp --features serde_ndim
The prelude exposes these interpolators:
Interp0D: constant-value interpolatorInterp1D: hard-coded 1-D interpolatorInterp2D: hard-coded 2-D interpolatorInterp3D: hard-coded 3-D interpolatorInterpND: general N-D interpolator
Use Interp0D when working with heterogeneous collections such as an InterpolatorEnum or Box<dyn Interpolator>.
| Approach | Runtime swapping | serde |
Custom strategies | Runtime cost |
|---|---|---|---|---|
Interp*<_, ConcreteStrategy> |
No | Yes | N/A | Lowest |
Interp*<_, strategy::enums::Strategy*Enum> |
Strategy only | Yes | No | Low |
Interp*<_, Box<dyn Strategy*>> |
Strategy only | No | Yes | Medium |
InterpolatorEnum |
Interpolator + strategy | Yes | No | Low |
Box<dyn Interpolator<_>> |
Interpolator + strategy | No | Yes | Highest |
After editing interpolator data, call the InterpData validate method or
Interpolator::validate
to rerun data/extrapolate validation checks.
validate only checks the data (shape, monotonicity) and extrapolate setting; it does
not re-run a strategy's own init. If editing data directly could violate a strategy's
own requirements (for example LinearUniform's uniform-grid requirement, or Step's
per-dimension direction count), or after deserializing an interpolator with a stateful
custom strategy, call init_strategy to re-run it.
Grid and values shapes must match by axis order.
Examples:
- 1-D:
x.len() == f_x.len() - 2-D:
x.len() == f_xy.shape()[0]andy.len() == f_xy.shape()[1] - 3-D:
x.len() == f_xyz.shape()[0],y.len() == f_xyz.shape()[1],z.len() == f_xyz.shape()[2] - N-D: for every dimension
n,grid[n].len() == values.shape()[n]
Grid coordinates in each dimension must be monotonically increasing.
An interpolation strategy (for example
Linear,
LinearUniform,
Nearest,
Step,
StepLower,
StepUpper,
must be specified.
To change the interpolation strategy, supply a Strategy1DEnum/etc. or Box<dyn Strategy1D>/etc. at instantiation and call set_strategy.
Custom strategies can be defined. See examples/custom_strategy.rs.
An Extrapolate
setting must be provided in new.
This controls behavior when points are beyond the supplied coordinate range.
Available for all interpolation strategies:
Extrapolate::Fill(T)Extrapolate::ClampExtrapolate::WrapExtrapolate::Error
Extrapolate::Enable is valid for Linear and LinearUniform for all dimensionalities.
If you are unsure which variant to choose, Extrapolate::Error is a good default.
To change extrapolation behavior after construction, call set_extrapolate.
Interpolation is executed by calling Interpolator::interpolate.
The query point must contain one coordinate per dimension. For example:
- 1-D interpolator:
&[x] - 2-D interpolator:
&[x, y] - 3-D interpolator:
&[x, y, z] - N-D interpolator:
&[x0, x1, ..., x_{N-1}]
If the number of coordinates does not match dimensionality, interpolation returns an error.
Retrieve dimensionality using Interpolator::ndim.
Validation-time (new / validate):
- Fewer than 2 grid coordinates in a dimension (
ValidateError::InsufficientGridPoints) - Non-monotonic coordinates (
ValidateError::NonMonotonic) - Grid/value shape mismatch (
ValidateError::IncompatibleShapes) - Inapplicable extrapolation setting (
ValidateError::InvalidExtrapolate)
Interpolation-time (interpolate):
- Query point has wrong dimensionality (
InterpolateError::PointLength) - Query point is out of bounds while using
Extrapolate::Error(InterpolateError::ExtrapolateError)
All interpolators support both owned and borrowed data via the generic D bound on
ndarray::Data.
The crate also re-exports ndarray
and num_traits,
so either of these import styles are valid:
use ninterp::ndarray::prelude::*;
// or
use ndarray::prelude::*;Type aliases in the prelude
make ownership intent explicit, for example in 1-D:
Interp1DOwned- Data is owned by the interpolator object
- Useful for struct fields
use ndarray::prelude::*; use ninterp::prelude::*; let interp: Interp1DOwned<f64, _> = Interp1D::new( array![0.0, 1.0, 2.0, 3.0], array![0.0, 1.0, 4.0, 9.0], strategy::Linear, Extrapolate::Error, ) .unwrap();
Interp1DViewed- Data is borrowed by the interpolator object
- Use when interpolator data should be owned by another object
use ndarray::prelude::*; use ninterp::prelude::*; let x = array![0.0, 1.0, 2.0, 3.0]; let f_x = array![0.0, 1.0, 4.0, 9.0]; let interp: Interp1DViewed<&f64, _> = Interp1D::new( x.view(), f_x.view(), strategy::Linear, Extrapolate::Error, ) .unwrap();
Typically, the compiler can infer concrete types from arguments passed to new.
Some examples use explicit annotations for clarity.
See examples in new method documentation:
Also see the examples directory for advanced examples:
- Swapping strategies at runtime:
dynamic_strategy.rs- Strategy enums (
strategy::enums::Strategy1DEnum/etc.):serde-compatible, custom strategies not supported Box<dyn Strategy1D>/etc. (dynamic dispatch): custom strategies supported, notserde-compatible, runtime cost
- Strategy enums (
- Swapping interpolators at runtime:
dynamic_interpolator.rsInterpolatorEnum:serde-compatible, custom strategies not supportedBox<dyn Interpolator>(dynamic dispatch): custom strategies supported, notserde-compatible, runtime cost
- Defining custom strategies:
custom_strategy.rs - Using transmutable (transparent) types such as
uom::si::Quantity:uom.rs