Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ Everything below is merged to `main` but not yet tagged/released.
`#[serde(serialize_with = "serialize_nested")]` on a field. Falls back to the `ndarray`
format on non-`is_human_readable` (binary) serializers, since there's nothing to nest
there and those formats can't read it back anyway.
- `strategy::utils::exact_index`, `locate_step_index`, `locate_lower_index_uniform`,
`check_uniform_grid`, and `AxisLocation`/`locate_axis` are now `pub` (previously
`pub(crate)`). They're the same per-axis primitives `Linear`/`LinearUniform`/`Step`/
`StepLower`/`StepUpper` are built from, now reusable from custom strategies instead of
needing to be reimplemented. `check_uniform_grid`'s error message no longer hardcodes
`"LinearUniform:"`, since other strategies can call it directly now too.

### Changed
- **Breaking:** `find_nearest_index` is renamed to `locate_lower_index` and, along with
Expand Down
4 changes: 3 additions & 1 deletion examples/custom_strategy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ where
// Note: when reading grid coordinates in `interpolate`, index `data.grid[i]` directly
// via ArrayView indexing (e.g. `data.grid[i][idx]`), not `.as_slice()`, which panics
// on non-contiguous storage. `Interp*Viewed` can produce that from a strided slice.
// For a bracket-search helper, see `ninterp::strategy::utils::locate_lower_index`.
// `ninterp::strategy::utils` has ready-made per-axis search helpers (bracket search,
// exact-match short-circuit, step-direction lookup, uniform-grid fast path) built from
// the same primitives the built-in strategies use.
D: Data<Elem = f32> + RawDataClone + Clone,
{
// We can optionally define an initialization step, useful for strategies that need precalculation.
Expand Down
16 changes: 12 additions & 4 deletions src/strategy/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ where
/// # Note for custom strategies
/// Index `data.grid[i]` directly via `ArrayView` indexing; avoid `.as_slice()`, which
/// panics on non-contiguous storage (possible with `Interp*Viewed`). See
/// [`crate::strategy::utils::locate_lower_index`] for a ready-made bracket search.
/// [`crate::strategy::utils`] for ready-made per-axis search helpers (bracket search,
/// exact-match short-circuit, step-direction lookup, uniform-grid fast path) built from
/// the same primitives the built-in strategies use.
fn interpolate(
&self,
data: &InterpData1D<D>,
Expand Down Expand Up @@ -73,7 +75,9 @@ where
/// # Note for custom strategies
/// Index `data.grid[i]` directly via `ArrayView` indexing; avoid `.as_slice()`, which
/// panics on non-contiguous storage (possible with `Interp*Viewed`). See
/// [`crate::strategy::utils::locate_lower_index`] for a ready-made bracket search.
/// [`crate::strategy::utils`] for ready-made per-axis search helpers (bracket search,
/// exact-match short-circuit, step-direction lookup, uniform-grid fast path) built from
/// the same primitives the built-in strategies use.
fn interpolate(
&self,
data: &InterpData2D<D>,
Expand Down Expand Up @@ -128,7 +132,9 @@ where
/// # Note for custom strategies
/// Index `data.grid[i]` directly via `ArrayView` indexing; avoid `.as_slice()`, which
/// panics on non-contiguous storage (possible with `Interp*Viewed`). See
/// [`crate::strategy::utils::locate_lower_index`] for a ready-made bracket search.
/// [`crate::strategy::utils`] for ready-made per-axis search helpers (bracket search,
/// exact-match short-circuit, step-direction lookup, uniform-grid fast path) built from
/// the same primitives the built-in strategies use.
fn interpolate(
&self,
data: &InterpData3D<D>,
Expand Down Expand Up @@ -183,7 +189,9 @@ where
/// # Note for custom strategies
/// Index `data.grid[i]` directly via `ArrayView` indexing; avoid `.as_slice()`, which
/// panics on non-contiguous storage (possible with `Interp*Viewed`). See
/// [`crate::strategy::utils::locate_lower_index`] for a ready-made bracket search.
/// [`crate::strategy::utils`] for ready-made per-axis search helpers (bracket search,
/// exact-match short-circuit, step-direction lookup, uniform-grid fast path) built from
/// the same primitives the built-in strategies use.
fn interpolate(
&self,
data: &InterpDataND<D>,
Expand Down
37 changes: 21 additions & 16 deletions src/strategy/utils.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
//! Single-axis primitives for locating a query point within one grid dimension.
//! Strategies compose these per axis to get 1D/2D/3D/ND behavior; none of them
//! iterate over dimensions themselves.
//!
//! These are the same building blocks [`super::Linear`], [`super::LinearUniform`],
//! [`super::Step`], [`super::StepLower`], and [`super::StepUpper`] are
//! implemented with, and are public for reuse in custom strategies (see [`crate::strategy::traits`]).

use super::*;

Expand Down Expand Up @@ -40,16 +44,23 @@ pub fn locate_lower_index<T: PartialOrd>(grid: ArrayView1<T>, point: &T) -> usiz

/// Per-axis locate for linear-family strategies: either an exact grid hit,
/// or an interior interpolation position.
pub(crate) enum AxisLocation<T> {
pub enum AxisLocation<T> {
/// `point` coincides exactly with `grid[_]` at this index.
Exact(usize),
Interp { lower: usize, frac: T },
/// `point` falls strictly between two grid coordinates.
Interp {
/// Index of the lower bracketing grid coordinate.
lower: usize,
/// Fractional position of `point` between `grid[lower]` and `grid[lower + 1]`, in `[0, 1)`.
frac: T,
},
}

/// Locates `point` along `grid`, resolving to an exact grid hit or an interpolation
/// position. Combines [`locate_lower_index`] (search + extrapolation clamp) with
/// [`exact_index`] (exact-match short-circuit) into the single call linear-family
/// strategies need per axis.
pub(crate) fn locate_axis<T: Float>(grid: ArrayView1<T>, point: &T) -> AxisLocation<T> {
pub fn locate_axis<T: Float>(grid: ArrayView1<T>, point: &T) -> AxisLocation<T> {
let lower = locate_lower_index(grid, point);
match exact_index(grid, lower, point) {
Some(idx) => AxisLocation::Exact(idx),
Expand All @@ -64,7 +75,7 @@ pub(crate) fn locate_axis<T: Float>(grid: ArrayView1<T>, point: &T) -> AxisLocat
///
/// Handles all exact grid-point edge cases that arise from [`locate_lower_index`]'s
/// interval semantics (returning the lower bracket rather than the exact position).
pub(crate) fn locate_step_index<T: PartialOrd + Copy>(
pub fn locate_step_index<T: PartialOrd + Copy>(
dir: StepDirection,
grid: ArrayView1<T>,
point: &T,
Expand Down Expand Up @@ -97,11 +108,7 @@ pub(crate) fn locate_step_index<T: PartialOrd + Copy>(
/// Returns the exact grid index if `point` lies on `grid[lower]` or `grid[lower+1]`, else `None`.
///
/// Used to short-circuit interpolation when a query point coincides with a grid coordinate.
pub(crate) fn exact_index<T: PartialOrd>(
grid: ArrayView1<T>,
lower: usize,
point: &T,
) -> Option<usize> {
pub fn exact_index<T: PartialOrd>(grid: ArrayView1<T>, lower: usize, point: &T) -> Option<usize> {
if grid[lower] == *point {
Some(lower)
} else if grid[lower + 1] == *point {
Expand All @@ -115,7 +122,7 @@ pub(crate) fn exact_index<T: PartialOrd>(
///
/// Equivalent to [`locate_lower_index`] but replaces binary search with direct arithmetic.
/// Only valid when the grid spacing is uniform — validate with [`check_uniform_grid`] first.
pub(crate) fn locate_lower_index_uniform<T: Float>(grid0: T, step: T, n: usize, point: T) -> usize {
pub fn locate_lower_index_uniform<T: Float>(grid0: T, step: T, n: usize, point: T) -> usize {
let t = (point - grid0) / step;
if t < T::zero() {
0
Expand All @@ -127,11 +134,9 @@ pub(crate) fn locate_lower_index_uniform<T: Float>(grid0: T, step: T, n: usize,
/// Validates that `grid` is uniformly spaced within floating-point tolerance.
///
/// Uses a relative tolerance of 1024 × ε to accommodate accumulated floating-point rounding
/// error in grids constructed from repeated arithmetic.
pub(crate) fn check_uniform_grid<T: Float>(
grid: ArrayView1<T>,
dim: usize,
) -> Result<(), ValidateError> {
/// error in grids constructed from repeated arithmetic. Pair with [`locate_lower_index_uniform`]
/// for the matching O(1) lookup.
pub fn check_uniform_grid<T: Float>(grid: ArrayView1<T>, dim: usize) -> Result<(), ValidateError> {
let step = grid[1] - grid[0];
// 1024 * epsilon via 10 doublings — avoids numeric literal casting
let tolerance = {
Expand All @@ -145,7 +150,7 @@ pub(crate) fn check_uniform_grid<T: Float>(
let gap = grid[i + 1] - grid[i];
if (gap - step).abs() > tolerance {
return Err(ValidateError::Other(format!(
"LinearUniform: grid[{dim}] is not uniformly spaced (gap at index {i})"
"grid[{dim}] is not uniformly spaced (gap at index {i})"
)));
}
}
Expand Down
Loading