diff --git a/CHANGELOG.md b/CHANGELOG.md index d730bd3..c626dda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,11 @@ Everything below is merged to `main` but not yet tagged/released. `Num + PartialOrd`). Other strategies (`Nearest`, `Step`, etc.) keep looser numeric bounds after an initial, overly broad `Float` restriction across the whole strategy surface was narrowed back down to just the two strategies that actually need it. +- **Breaking:** `ValidateError` variants renamed for consistency, and no longer read as + full sentences: `ExtrapolateSelection` -> `InvalidExtrapolate`, `Monotonicity` -> + `NonMonotonic`. `EmptyGrid` is removed outright; a grid dimension with 0 or 1 points + is now rejected by the same `InsufficientGridPoints`, since a single point can't + bracket a query either. - Significant ND performance work: `Linear`/`Nearest` no longer build coordinate permutation tables via `itertools::multi_cartesian_product` (removing the `itertools` dependency); corner values are now gathered into a flat buffer and reduced with an @@ -64,6 +69,12 @@ Everything below is merged to `main` but not yet tagged/released. dimension without touching it) to `find_nearest_index`, whose binary search calls `.first().unwrap()` and panics on an empty dimension. Fixed by skipping empty grid dimensions in that loop. +- A grid dimension with exactly 1 point passed construction and then panicked on the + first `interpolate` call (integer underflow in the lower-bracket search, or an + out-of-bounds index right after it), for every strategy except when + `Extrapolate::Enable` was selected, since the "at least 2 points" check only ran for + that one setting. It's now checked unconditionally at construction, so this is a + `ValidateError::InsufficientGridPoints` instead of a panic. ## [0.9.1] - 2026-08-03 diff --git a/README.md b/README.md index cdec9aa..8e5e38d 100644 --- a/README.md +++ b/README.md @@ -170,10 +170,10 @@ Retrieve dimensionality using [`Interpolator::ndim`](https://docs.rs/ninterp/lat ### Common Errors Validation-time (`new` / `validate`): -- Empty grid coordinates (`ValidateError::EmptyGrid`) -- Non-monotonic coordinates (`ValidateError::Monotonicity`) +- 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::ExtrapolateSelection`) +- Inapplicable extrapolation setting (`ValidateError::InvalidExtrapolate`) Interpolation-time (`interpolate`): - Query point has wrong dimensionality (`InterpolateError::PointLength`) diff --git a/src/error.rs b/src/error.rs index aaae9de..c696608 100644 --- a/src/error.rs +++ b/src/error.rs @@ -8,11 +8,11 @@ use thiserror::Error; #[derive(Error, Clone, PartialEq)] pub enum ValidateError { #[error("selected `Extrapolate` variant ({0}) is unimplemented/inapplicable for interpolator")] - ExtrapolateSelection(String), - #[error("supplied grid coordinates cannot be empty: dim {0}")] - EmptyGrid(usize), + InvalidExtrapolate(String), + #[error("at least 2 grid points are required per dimension: dim {0}")] + InsufficientGridPoints(usize), #[error("supplied coordinates must be monotonically increasing: dim {0}")] - Monotonicity(usize), + NonMonotonic(usize), #[error("supplied grid and values are not compatible shapes: dim {0}")] IncompatibleShapes(usize), #[error("{0}")] diff --git a/src/interpolator/data.rs b/src/interpolator/data.rs index 9f25808..5c17797 100644 --- a/src/interpolator/data.rs +++ b/src/interpolator/data.rs @@ -86,13 +86,13 @@ where { for i in 0..N { let i_grid_len = self.grid[i].len(); - // Check that each grid dimension has elements - if i_grid_len == 0 { - return Err(ValidateError::EmptyGrid(i)); + // Every strategy needs at least 2 points per dimension to bracket a query point + if i_grid_len < 2 { + return Err(ValidateError::InsufficientGridPoints(i)); } // Check that grid points are monotonically increasing if !self.grid[i].windows(2).into_iter().all(|w| w[0] <= w[1]) { - return Err(ValidateError::Monotonicity(i)); + return Err(ValidateError::NonMonotonic(i)); } // Check that grid and values are compatible shapes if i_grid_len != self.values.shape()[i] { diff --git a/src/interpolator/mod.rs b/src/interpolator/mod.rs index d674562..c661f48 100644 --- a/src/interpolator/mod.rs +++ b/src/interpolator/mod.rs @@ -87,22 +87,11 @@ macro_rules! extrapolate_impl { // Check applicability of strategy and extrapolate setting if matches!(extrapolate, Extrapolate::Enable) && !self.strategy.allow_extrapolate() { - return Err(ValidateError::ExtrapolateSelection(format!( + return Err(ValidateError::InvalidExtrapolate(format!( "{:?}", self.extrapolate ))); } - // If using Extrapolate::Enable, - // check that each grid dimension has at least two elements - if matches!(self.extrapolate, Extrapolate::Enable) { - for (i, g) in self.data.grid.iter().enumerate() { - if g.len() < 2 { - return Err(ValidateError::Other(format!( - "at least 2 data points are required for extrapolation: dim {i}", - ))); - } - } - } Ok(()) } } diff --git a/src/interpolator/n/mod.rs b/src/interpolator/n/mod.rs index f2220f3..b08abf9 100644 --- a/src/interpolator/n/mod.rs +++ b/src/interpolator/n/mod.rs @@ -96,14 +96,13 @@ where } for i in 0..n { let i_grid_len = self.grid[i].len(); - // Check that each grid dimension has elements - // Indexing `grid` directly is okay because empty dimensions are caught at compilation - if i_grid_len == 0 { - return Err(ValidateError::EmptyGrid(i)); + // Every strategy needs at least 2 points per dimension to bracket a query point + if i_grid_len < 2 { + return Err(ValidateError::InsufficientGridPoints(i)); } // Check that grid points are monotonically increasing if !self.grid[i].windows(2).into_iter().all(|w| w[0] <= w[1]) { - return Err(ValidateError::Monotonicity(i)); + return Err(ValidateError::NonMonotonic(i)); } // Check that grid and values are compatible shapes if i_grid_len != self.values.shape()[i] { diff --git a/src/interpolator/n/tests.rs b/src/interpolator/n/tests.rs index c33087f..4347503 100644 --- a/src/interpolator/n/tests.rs +++ b/src/interpolator/n/tests.rs @@ -366,7 +366,7 @@ fn test_extrapolate_inputs() { Extrapolate::Enable, ) .unwrap_err(), - ValidateError::ExtrapolateSelection(_) + ValidateError::InvalidExtrapolate(_) )); // Extrapolate::Error let interp = InterpND::new( diff --git a/src/interpolator/one/tests.rs b/src/interpolator/one/tests.rs index b6f766d..fcc1586 100644 --- a/src/interpolator/one/tests.rs +++ b/src/interpolator/one/tests.rs @@ -16,6 +16,22 @@ fn test_invalid_args() { assert_eq!(interp.interpolate(&[1.0]).unwrap(), 0.4); } +#[test] +fn test_insufficient_grid_points() { + // A single grid point can't bracket anything, regardless of `Extrapolate` setting. + // Previously this passed construction and panicked on the first `interpolate` call. + assert!(matches!( + Interp1D::new( + array![5.0], + array![10.0], + strategy::Linear, + Extrapolate::Error + ) + .unwrap_err(), + ValidateError::InsufficientGridPoints(0) + )); +} + #[test] fn test_linear() { let interp = Interp1D::new( @@ -224,7 +240,7 @@ fn test_extrapolate_inputs() { Extrapolate::Enable, ) .unwrap_err(), - ValidateError::ExtrapolateSelection(_) + ValidateError::InvalidExtrapolate(_) )); // Extrapolate::Error diff --git a/src/interpolator/three/tests.rs b/src/interpolator/three/tests.rs index 35229ff..86df873 100644 --- a/src/interpolator/three/tests.rs +++ b/src/interpolator/three/tests.rs @@ -222,7 +222,7 @@ fn test_extrapolate_inputs() { Extrapolate::Enable, ) .unwrap_err(), - ValidateError::ExtrapolateSelection(_) + ValidateError::InvalidExtrapolate(_) )); // Extrapolate::Error let interp = Interp3D::new( diff --git a/src/interpolator/two/tests.rs b/src/interpolator/two/tests.rs index 78cfe74..f198888 100644 --- a/src/interpolator/two/tests.rs +++ b/src/interpolator/two/tests.rs @@ -181,7 +181,7 @@ fn test_extrapolate_inputs() { Extrapolate::Enable, ) .unwrap_err(), - ValidateError::ExtrapolateSelection(_) + ValidateError::InvalidExtrapolate(_) )); // Extrapolate::Error let interp = Interp2D::new(