From 2be7ccc7080015a88a7915ba38c4f28d310ea4dd Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Fri, 7 Aug 2026 01:02:15 -0600 Subject: [PATCH 1/2] Expose strategy::utils per-axis search helpers for custom strategies exact_index, locate_step_index, locate_lower_index_uniform, check_uniform_grid, and AxisLocation/locate_axis were pub(crate), even though they're the actual primitives Linear/LinearUniform/Step/StepLower/ StepUpper are built from. Widen all to pub so custom strategies can reuse them instead of reimplementing from scratch. check_uniform_grid's error message hardcoded "LinearUniform:" regardless of caller; generalized now that other strategies can call it directly. --- CHANGELOG.md | 6 ++++++ examples/custom_strategy.rs | 4 +++- src/strategy/traits.rs | 16 ++++++++++++---- src/strategy/utils.rs | 37 +++++++++++++++++++++---------------- 4 files changed, 42 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41ac60c..e8d5c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/custom_strategy.rs b/examples/custom_strategy.rs index 88703df..cb5215a 100644 --- a/examples/custom_strategy.rs +++ b/examples/custom_strategy.rs @@ -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 + RawDataClone + Clone, { // We can optionally define an initialization step, useful for strategies that need precalculation. diff --git a/src/strategy/traits.rs b/src/strategy/traits.rs index 66393ee..9f992cb 100644 --- a/src/strategy/traits.rs +++ b/src/strategy/traits.rs @@ -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, @@ -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, @@ -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, @@ -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, diff --git a/src/strategy/utils.rs b/src/strategy/utils.rs index bb81d34..0fd1edf 100644 --- a/src/strategy/utils.rs +++ b/src/strategy/utils.rs @@ -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 [`Linear`](super::Linear), [`LinearUniform`](super::LinearUniform), +//! [`Step`](super::Step), [`StepLower`](super::StepLower), and [`StepUpper`](super::StepUpper) are +//! implemented with, and are public for reuse in custom strategies (see [`crate::strategy::traits`]). use super::*; @@ -40,16 +44,23 @@ pub fn locate_lower_index(grid: ArrayView1, point: &T) -> usiz /// Per-axis locate for linear-family strategies: either an exact grid hit, /// or an interior interpolation position. -pub(crate) enum AxisLocation { +pub enum AxisLocation { + /// `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(grid: ArrayView1, point: &T) -> AxisLocation { +pub fn locate_axis(grid: ArrayView1, point: &T) -> AxisLocation { let lower = locate_lower_index(grid, point); match exact_index(grid, lower, point) { Some(idx) => AxisLocation::Exact(idx), @@ -64,7 +75,7 @@ pub(crate) fn locate_axis(grid: ArrayView1, 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( +pub fn locate_step_index( dir: StepDirection, grid: ArrayView1, point: &T, @@ -97,11 +108,7 @@ pub(crate) fn locate_step_index( /// 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( - grid: ArrayView1, - lower: usize, - point: &T, -) -> Option { +pub fn exact_index(grid: ArrayView1, lower: usize, point: &T) -> Option { if grid[lower] == *point { Some(lower) } else if grid[lower + 1] == *point { @@ -115,7 +122,7 @@ pub(crate) fn exact_index( /// /// 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(grid0: T, step: T, n: usize, point: T) -> usize { +pub fn locate_lower_index_uniform(grid0: T, step: T, n: usize, point: T) -> usize { let t = (point - grid0) / step; if t < T::zero() { 0 @@ -127,11 +134,9 @@ pub(crate) fn locate_lower_index_uniform(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( - grid: ArrayView1, - 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(grid: ArrayView1, dim: usize) -> Result<(), ValidateError> { let step = grid[1] - grid[0]; // 1024 * epsilon via 10 doublings — avoids numeric literal casting let tolerance = { @@ -145,7 +150,7 @@ pub(crate) fn check_uniform_grid( 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})" ))); } } From 31045415730a5f56d769f6fdcb49b52cbb2e2fe8 Mon Sep 17 00:00:00 2001 From: Kyle Carow Date: Fri, 7 Aug 2026 01:06:44 -0600 Subject: [PATCH 2/2] Fix redundant explicit rustdoc link targets in strategy::utils module docs Denied by CI's -D warnings on cargo doc: [Type](super::Type) is redundant when the label alone already resolves to the same intra-doc target. --- src/strategy/utils.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/strategy/utils.rs b/src/strategy/utils.rs index 0fd1edf..912c470 100644 --- a/src/strategy/utils.rs +++ b/src/strategy/utils.rs @@ -2,8 +2,8 @@ //! 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 [`Linear`](super::Linear), [`LinearUniform`](super::LinearUniform), -//! [`Step`](super::Step), [`StepLower`](super::StepLower), and [`StepUpper`](super::StepUpper) are +//! 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::*;