diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8b11a9c..04ba29c 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,6 +23,14 @@ jobs: - name: check formatting run: cargo fmt --check + - name: clippy + run: cargo clippy --all-features --all-targets -- -D warnings + + - name: doc + run: cargo doc --all-features --no-deps + env: + RUSTDOCFLAGS: -D warnings + - name: test # Note: this will be unwieldy if there are many features, but for now it is fine. run: cargo hack test --feature-powerset --verbose diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..ad3f236 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,303 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +This project is pre-1.0 and follows the common pre-1.0 convention: breaking changes +bump the minor version (`0.x` -> `0.(x+1)`), other changes bump the patch version. + +## [Unreleased] + +Everything below is merged to `main` but not yet tagged/released. + +### Added +- `strategy::LinearUniform`: an O(1)-index alternative to `Linear` for uniformly-spaced + grids (1D/2D/3D/ND). Validates grid uniformity at construction/`init` time (1024 * ε + relative tolerance) instead of silently falling back to a search. +- `strategy::Step`: a parameterized step (piecewise-constant) strategy, replacing + `LeftNearest`/`RightNearest` with a single strategy that works across all + dimensionalities. A single `StepDirection` broadcasts to every axis, or a `Vec` gives + per-axis control. +- `strategy::StepLower` / `strategy::StepUpper`: zero-sized marker strategies for the + common fixed-direction case, avoiding `Step`'s direction-vector allocation and + per-call direction matching. `Step` remains the choice for mixed per-dimension or + runtime-selected direction. +- `strategy::LinearUniform`, `Step`, `StepLower`, and `StepUpper` are all available for + every dimensionality and included in the corresponding `Strategy*Enum` types. + +### Changed +- **Breaking:** `LeftNearest` and `RightNearest` are removed. Migrate to + `Step::from(StepDirection::Lower)` / `Step::from(StepDirection::Upper)`, or the + leaner `StepLower` / `StepUpper` markers. +- **Breaking:** `Linear` and `LinearUniform` now require `D::Elem: Float` (previously + `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. +- 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 + in-place bitmask/butterfly pass, cutting allocations from O(N * 2^N) to O(1)-O(N). + 1D strategies no longer open with an O(M) linear scan for exact grid-point matches. + 2D/3D `Linear` now short-circuits per-dimension when a query point lands exactly on a + grid coordinate. See PR #13 for benchmark numbers (roughly 50-65% faster on 1D/2D + hardcoded and multilinear paths). +- Serde: `StepLower`/`StepUpper` accept the legacy `"LeftNearest"`/`"RightNearest"` + names on deserialization for backward compatibility; `Step`'s own wire format + (`{"Step": [...]}`) is unchanged and does not accept those aliases. +- Loosened `Step`/`Nearest` strategy trait bounds back down after the `LinearUniform` + work had temporarily tightened them further than necessary. +- Various documentation and README improvements; CI workflow polish. + +### Fixed +- `InterpND` panicked on `n == 0` (the 0-D-via-`InterpND` case, e.g. after + dimensionality reduction collapses every axis). The ND `Linear` strategy's exact-match + scan was rewritten from `iter().position()` (which returned `None` on an empty grid + 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. + +## [0.9.1] - 2026-08-03 + +### Changed +- **Breaking:** human-readable array output via `serde-ndim` is now opt-in through the + `serde_ndim` feature, rather than always enabled alongside `serde`. + +### Notes +- Repository moved from the `NREL` to `NatLabRockies` GitHub organization (URLs + redirect automatically). + +## [0.9.0] - 2026-08-02 + +### Added +- `serde-ndim` integration under the existing `serde` feature. +- Compatibility deserializers that accept the new simple array representation, the + legacy `serde-ndim` representation, or a mix of the two (simple grid + legacy values, + or vice versa). + +### Changed +- **Breaking (serialization output only):** default serialized output for interpolator + grid/values now uses a simpler, sequence-style representation instead of the prior + `serde-ndim` format. If you snapshot-test or schema-validate the exact serialized + payload shape, update your fixtures. + +### Notes +- Deserialization remains backward compatible with the prior payload structure, so + existing persisted data is still readable without migration. + +## [0.8.2] - 2026-02-19 + +### Fixed +- Misleading error message for coordinate validation ([#12], [@meredithdoan]). + +## [0.8.1] - 2025-11-25 + +### Changed +- `ndarray` dependency loosened to `^0.16` for downstream compatibility ([#11], + [@robfitzgerald]). + +## [0.8.0] - 2025-11-15 + +### Changed +- **Breaking (version bump only):** raised the maximum supported `ndarray` version to + include the 0.17.x line. Bumped as a new "major" (`0.x`) version specifically so a + downstream `Cargo.lock` rebuild wouldn't silently pull in the new `ndarray` major + version without an explicit opt-in. + +## [0.7.3] - 2025-05-29 + +### Added +- `into_owned()` methods. + +### Fixed +- Bug in `InterpDataOwned`. + +## [0.7.2] - 2025-05-29 + +### Added +- `view()` method for interpolators. + +## [0.7.1] - 2025-05-19 + +### Changed +- Error types now have a hand-written `Debug` impl that delegates to the + `thiserror`-derived `Display`, instead of `#[derive(Debug)]`. Unwrapped errors read + as a message instead of a raw struct dump. + +### Notes +- Documentation improvements. + +## [0.7.0] - 2025-05-02 + +### Changed +- `#[serde(untagged)]` applied to all enum types: they now (de)serialize identically to + their contained variant, so switching a downstream project from a concrete + interpolator type to `InterpolatorEnum` doesn't change the serialized shape. +- Strategies now serialize to their stringified name instead of `null`. + +## [0.6.4] - 2025-04-22 + +### Changed +- Serde deserialize bounds changed from `DeserializeOwned` to `Deserialize<'de>`. +- Minor syntax cleanup; removed some unnecessary allocations. + +## [0.6.3] - 2025-03-24 + +### Changed +- `set_extrapolate` moved onto the `Interpolator` trait. + +## [0.6.2] - 2025-03-21 + +### Fixed +- `PartialEq` impls: `#[derive(PartialEq)]` doesn't work for types with a `D: Data` + bound, since `ndarray::Data` itself doesn't implement `PartialEq` even though + `ArrayBase` does. Switched to manual impls. + +### Changed +- Owned and viewed type aliases now exposed in `prelude`. + +## [0.6.1] - 2025-03-20 + +### Added +- Strategy and interpolator enums (`Strategy1DEnum`/etc., `InterpolatorEnum`), enabling + `serde` support for runtime-swappable interpolators and strategies. + +## [0.6.0] - 2025-03-19 + +### Changed +- **Breaking:** namespace reorganized. Strategies are now accessed as `strategy::Linear` + etc. after `use prelude::*`, instead of being re-exported flat at the top level. This + makes room for more complex strategy organization (e.g. cubic strategies) without + polluting the downstream namespace. + +### Added +- Strategy `init` step, letting a strategy mutate/precompute its own internal state + ahead of interpolation calls, enabling more complex strategies. +- `Extrapolate::Wrap`: wrap around to the other end of periodic data. + +## [0.5.2] - 2025-03-12 + +### Added +- `Clone` now derived for all public types ([#4], [@kylecarow]). + +## [0.5.1] - 2025-03-09 + +### Changed +- Extrapolation handling moved into the macro-generated impls; a separate `extrapolate` + call is no longer necessary, and it's no longer incorrectly applicable to `Interp0D`. + +## [0.5.0] - 2025-03-08 + +### Changed +- **Breaking:** whole-crate rewrite onto generics, operating directly on `ndarray` data + (owned and viewed) instead of a fixed internal representation ([#3], "NDArray & + generics rewrite"). + +## [0.4.0] - 2025-03-07 + +### Changed +- **Breaking:** full rewrite ([#2]). Introduced custom strategies via + `Strategy1D`/`Strategy2D`/`Strategy3D` traits (renamed from `Interp1DStrategy`/etc.), + added `set_strategy`, reorganized modules into per-dimensionality folders, and + removed the old `Interpolator` enum in favor of the concrete-type design used since. + +## [0.3.0] - 2025-03-03 + +### Changed +- **Breaking:** error types renamed for clarity: `ValidationError` -> `ValidateError`, + `InterpolationError` -> `InterpolateError`, + `InterpolationError::ExtrapolationError` -> `InterpolateError::ExtrapolateError`, + `Error::NoSuchField` now carries a `&'static str`. +- **Breaking:** `Extrapolate::FillValue` renamed to `Extrapolate::Fill`. + +## [0.2.7] - 2025-03-03 + +### Added +- `Extrapolate::FillValue(f64)`. + +### Fixed +- `Interp3D` clamp bug. + +## [0.2.6] - 2025-03-01 + +### Added +- `Extrapolate::Enable` support for `Linear` across all dimensionalities. +- `Nearest` strategy available for all dimensionalities. + +## [0.2.5] - 2025-02-25 + +### Changed +- Minor internal cleanup (removed an unnecessary `return`) and documentation cleanup. + +## [0.2.4] - 2025-02-21 + +### Added +- `prelude` module to simplify downstream imports. + +## [0.2.3] - 2025-02-21 + +### Changed +- `Clone` now derived on relevant types, per a downstream request from the FASTSim + team. + +### Notes +- Minor internal/CI polish. + +## [0.2.2] - 2025-02-21 + +### Changed +- `new_*d` constructor methods now return `crate::Error`. + +## [0.2.1] - 2025-01-24 + +### Changed +- Extrapolation error messages improved to include every out-of-bounds grid dimension, + not just the first. + +## [0.2.0] - 2025-01-23 + +### Changed +- **Breaking:** instantiation moved to dimensionality-specific `new_1d`/`new_2d`/ + `new_3d`/`new_nd` methods. + +## [0.1.0] - 2024-11-27 + +Initial release. + +[#2]: https://github.com/NatLabRockies/ninterp/pull/2 +[#3]: https://github.com/NatLabRockies/ninterp/pull/3 +[#4]: https://github.com/NatLabRockies/ninterp/pull/4 +[#11]: https://github.com/NatLabRockies/ninterp/pull/11 +[#12]: https://github.com/NatLabRockies/ninterp/pull/12 +[@kylecarow]: https://github.com/kylecarow +[@robfitzgerald]: https://github.com/robfitzgerald +[@meredithdoan]: https://github.com/meredithdoan + +[Unreleased]: https://github.com/NatLabRockies/ninterp/compare/v0.9.1...main +[0.9.1]: https://github.com/NatLabRockies/ninterp/compare/v0.9.0...v0.9.1 +[0.9.0]: https://github.com/NatLabRockies/ninterp/compare/v0.8.2...v0.9.0 +[0.8.2]: https://github.com/NatLabRockies/ninterp/compare/v0.8.1...v0.8.2 +[0.8.1]: https://github.com/NatLabRockies/ninterp/compare/v0.8.0...v0.8.1 +[0.8.0]: https://github.com/NatLabRockies/ninterp/compare/v0.7.3...v0.8.0 +[0.7.3]: https://github.com/NatLabRockies/ninterp/compare/v0.7.2...v0.7.3 +[0.7.2]: https://github.com/NatLabRockies/ninterp/compare/v0.7.1...v0.7.2 +[0.7.1]: https://github.com/NatLabRockies/ninterp/compare/v0.7.0...v0.7.1 +[0.7.0]: https://github.com/NatLabRockies/ninterp/compare/v0.6.4...v0.7.0 +[0.6.4]: https://github.com/NatLabRockies/ninterp/compare/v0.6.3...v0.6.4 +[0.6.3]: https://github.com/NatLabRockies/ninterp/compare/v0.6.2...v0.6.3 +[0.6.2]: https://github.com/NatLabRockies/ninterp/compare/v0.6.1...v0.6.2 +[0.6.1]: https://github.com/NatLabRockies/ninterp/compare/v0.6.0...v0.6.1 +[0.6.0]: https://github.com/NatLabRockies/ninterp/compare/v0.5.2...v0.6.0 +[0.5.2]: https://github.com/NatLabRockies/ninterp/compare/v0.5.1...v0.5.2 +[0.5.1]: https://github.com/NatLabRockies/ninterp/compare/v0.5.0...v0.5.1 +[0.5.0]: https://github.com/NatLabRockies/ninterp/compare/v0.4.0...v0.5.0 +[0.4.0]: https://github.com/NatLabRockies/ninterp/compare/v0.3.0...v0.4.0 +[0.3.0]: https://github.com/NatLabRockies/ninterp/compare/v0.2.7...v0.3.0 +[0.2.7]: https://github.com/NatLabRockies/ninterp/compare/v0.2.6...v0.2.7 +[0.2.6]: https://github.com/NatLabRockies/ninterp/compare/v0.2.5...v0.2.6 +[0.2.5]: https://github.com/NatLabRockies/ninterp/compare/v0.2.4...v0.2.5 +[0.2.4]: https://github.com/NatLabRockies/ninterp/compare/v0.2.3...v0.2.4 +[0.2.3]: https://github.com/NatLabRockies/ninterp/compare/v0.2.2...v0.2.3 +[0.2.2]: https://github.com/NatLabRockies/ninterp/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/NatLabRockies/ninterp/compare/v0.2.0...v0.2.1 +[0.2.0]: https://github.com/NatLabRockies/ninterp/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/NatLabRockies/ninterp/releases/tag/v0.1.0 \ No newline at end of file diff --git a/README.md b/README.md index 22df7dd..cdec9aa 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,13 @@ Use `Interp0D` when working with heterogeneous collections such as an `Interpola ### Validation Lifecycle After editing interpolator data, call the InterpData `validate` method or [`Interpolator::validate`](https://docs.rs/ninterp/latest/ninterp/interpolator/trait.Interpolator.html#tymethod.validate) -to rerun validation checks. +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. ### Data Shape Contract Grid and values shapes must match by axis order. diff --git a/examples/custom_strategy.rs b/examples/custom_strategy.rs index ed1af6c..c9bdb54 100644 --- a/examples/custom_strategy.rs +++ b/examples/custom_strategy.rs @@ -22,8 +22,9 @@ where D: Data + RawDataClone + Clone, { // We can optionally define an initialization step, useful for strategies that need precalculation. - // This is called from Interpolator::validate, thus is run on construction for all interpolators. - // It takes a mutable reference, so you can edit any data contained in `CustomStrategy`. + // This is called on construction (`new`) and whenever the strategy is swapped (`set_strategy`), + // for all interpolators. It takes a mutable reference, so you can edit any data contained in + // `CustomStrategy`. // // There is a default implementation that just returns `Ok(())`, so leave this out if not needed. fn init(&mut self, _data: &InterpData2D) -> Result<(), ninterp::error::ValidateError> { diff --git a/examples/uom.rs b/examples/uom.rs index de7ff6a..06fb761 100644 --- a/examples/uom.rs +++ b/examples/uom.rs @@ -14,8 +14,8 @@ fn main() { // This means we can get the contained type via transmuting. let interp: Interp1DViewed<&f64, _> = unsafe { Interp1D::new( - std::mem::transmute(x.view()), - std::mem::transmute(f_x.view()), + std::mem::transmute::, ArrayView1>(x.view()), + std::mem::transmute::, ArrayView1>(f_x.view()), strategy::Linear, Extrapolate::Error, ) diff --git a/src/interpolator/enums.rs b/src/interpolator/enums.rs index fd3c428..c95cdee 100644 --- a/src/interpolator/enums.rs +++ b/src/interpolator/enums.rs @@ -233,7 +233,7 @@ where } #[inline] - fn validate(&mut self) -> Result<(), ValidateError> { + fn validate(&self) -> Result<(), ValidateError> { match self { InterpolatorEnum::Interp0D(_) => Ok(()), InterpolatorEnum::Interp1D(interp) => interp.validate(), diff --git a/src/interpolator/mod.rs b/src/interpolator/mod.rs index 47517cf..d674562 100644 --- a/src/interpolator/mod.rs +++ b/src/interpolator/mod.rs @@ -26,7 +26,7 @@ pub trait Interpolator: DynClone { /// Interpolator dimensionality. fn ndim(&self) -> usize; /// Validate interpolator data. - fn validate(&mut self) -> Result<(), ValidateError>; + fn validate(&self) -> Result<(), ValidateError>; /// Interpolate at supplied point. fn interpolate(&self, point: &[T]) -> Result; /// Set [`Extrapolate`] variant, checking validity. @@ -39,7 +39,7 @@ impl Interpolator for Box> { fn ndim(&self) -> usize { (**self).ndim() } - fn validate(&mut self) -> Result<(), ValidateError> { + fn validate(&self) -> Result<(), ValidateError> { (**self).validate() } fn interpolate(&self, point: &[T]) -> Result { diff --git a/src/interpolator/n/mod.rs b/src/interpolator/n/mod.rs index 6d202aa..f2220f3 100644 --- a/src/interpolator/n/mod.rs +++ b/src/interpolator/n/mod.rs @@ -246,6 +246,16 @@ where Ok(interpolator) } + /// Re-run the strategy's [`StrategyND::init`] against the current data. + /// + /// `new` and `set_strategy` already call this internally, so this is only needed + /// after bypassing them: mutating the public `data`/`strategy` fields directly, or + /// deserializing an interpolator with a stateful custom strategy (`Deserialize` + /// does not call `init`). + pub fn init_strategy(&mut self) -> Result<(), ValidateError> { + self.strategy.init(&self.data) + } + /// Return an interpolator with viewed data. pub fn view(&self) -> InterpNDViewed<&D::Elem, S> where @@ -284,10 +294,9 @@ where self.data.ndim() } - fn validate(&mut self) -> Result<(), ValidateError> { + fn validate(&self) -> Result<(), ValidateError> { self.check_extrapolate(&self.extrapolate)?; self.data.validate()?; - self.strategy.init(&self.data)?; Ok(()) } @@ -359,10 +368,15 @@ where D: Data + RawDataClone + Clone, D::Elem: PartialEq + Debug, { - /// Update strategy dynamically. + /// Update strategy at runtime, calling [`StrategyND::init`] on the new strategy + /// against the current data. + /// + /// To swap in a strategy without re-running `init` (e.g. one whose state was + /// already established elsewhere), assign the `strategy` field directly instead. pub fn set_strategy(&mut self, strategy: Box>) -> Result<(), ValidateError> { self.strategy = strategy; - self.check_extrapolate(&self.extrapolate) + self.check_extrapolate(&self.extrapolate)?; + self.strategy.init(&self.data) } } @@ -371,12 +385,17 @@ where D: Data + RawDataClone + Clone, D::Elem: Float + Debug, { - /// Update strategy dynamically. + /// Update strategy at runtime, calling [`StrategyND::init`] on the new strategy + /// against the current data. + /// + /// To swap in a strategy without re-running `init` (e.g. one whose state was + /// already established elsewhere), assign the `strategy` field directly instead. pub fn set_strategy( &mut self, strategy: impl Into, ) -> Result<(), ValidateError> { self.strategy = strategy.into(); - self.check_extrapolate(&self.extrapolate) + self.check_extrapolate(&self.extrapolate)?; + self.strategy.init(&self.data) } } diff --git a/src/interpolator/n/strategies.rs b/src/interpolator/n/strategies.rs index fae9e6c..8d8c046 100644 --- a/src/interpolator/n/strategies.rs +++ b/src/interpolator/n/strategies.rs @@ -75,20 +75,19 @@ where let size = 1usize << n; let mut vals = vec![D::Elem::zero(); size]; let mut idx = vec![0usize; n]; - for mask in 0..size { + for (mask, val) in vals.iter_mut().enumerate() { for d in 0..n { idx[d] = lower_idxs[d] + ((mask >> (n - 1 - d)) & 1); } - vals[mask] = values_view[idx.as_slice()]; + *val = values_view[idx.as_slice()]; } // Butterfly reduction: one pass per dimension. // After pass d, vals[0..2^(n-d-1)] holds the result with dimensions 0..=d blended. - for d in 0..n { + for (d, diff) in interp_diffs.iter().enumerate() { let half = 1 << (n - 1 - d); for i in 0..half { - vals[i] = - vals[i] * (D::Elem::one() - interp_diffs[d]) + vals[i + half] * interp_diffs[d]; + vals[i] = vals[i] * (D::Elem::one() - *diff) + vals[i + half] * *diff; } } @@ -122,11 +121,10 @@ where let n = data.values.ndim(); let mut lower_idxs = Vec::with_capacity(n); let mut interp_diffs = Vec::with_capacity(n); - for dim in 0..n { - let step = data.grid[dim][1] - data.grid[dim][0]; - let lower_idx = - uniform_lower_index(data.grid[dim][0], step, data.grid[dim].len(), point[dim]); - let diff = (point[dim] - data.grid[dim][lower_idx]) / step; + for (grid_dim, &point_dim) in data.grid.iter().zip(point.iter()) { + let step = grid_dim[1] - grid_dim[0]; + let lower_idx = uniform_lower_index(grid_dim[0], step, grid_dim.len(), point_dim); + let diff = (point_dim - grid_dim[lower_idx]) / step; lower_idxs.push(lower_idx); interp_diffs.push(diff); } @@ -134,17 +132,16 @@ where let size = 1usize << n; let mut vals = vec![D::Elem::zero(); size]; let mut idx = vec![0usize; n]; - for mask in 0..size { + for (mask, val) in vals.iter_mut().enumerate() { for d in 0..n { idx[d] = lower_idxs[d] + ((mask >> (n - 1 - d)) & 1); } - vals[mask] = data.values.view()[idx.as_slice()]; + *val = data.values.view()[idx.as_slice()]; } - for d in 0..n { + for (d, diff) in interp_diffs.iter().enumerate() { let half = 1 << (n - 1 - d); for i in 0..half { - vals[i] = - vals[i] * (D::Elem::one() - interp_diffs[d]) + vals[i + half] * interp_diffs[d]; + vals[i] = vals[i] * (D::Elem::one() - *diff) + vals[i + half] * *diff; } } Ok(vals[0]) diff --git a/src/interpolator/n/tests.rs b/src/interpolator/n/tests.rs index 92aa074..c33087f 100644 --- a/src/interpolator/n/tests.rs +++ b/src/interpolator/n/tests.rs @@ -534,18 +534,18 @@ fn test_serde() { // simple format (new serialization output) let ser0 = "{\"grid\":[[0.1,1.1],[0.2,1.2],[0.3,1.3]],\"values\":[[[0.0,1.0],[2.0,3.0]],[[4.0,5.0],[6.0,7.0]]]}"; - let de0: InterpDataND<_> = serde_json::from_str(&ser0).unwrap(); + let de0: InterpDataND<_> = serde_json::from_str(ser0).unwrap(); assert_eq!(interp.data, de0); // mixed format (simple grid) let ser1 = "{\"grid\":[[0.1,1.1],[0.2,1.2],[0.3,1.3]],\"values\":{\"v\":1,\"dim\":[2,2,2],\"data\":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0]}}"; - let de1: InterpDataND<_> = serde_json::from_str(&ser1).unwrap(); + let de1: InterpDataND<_> = serde_json::from_str(ser1).unwrap(); assert_eq!(interp.data, de1); // mixed format (simple values) let ser2 = "{\"grid\":[{\"v\":1,\"dim\":[2],\"data\":[0.1,1.1]},{\"v\":1,\"dim\":[2],\"data\":[0.2,1.2]},{\"v\":1,\"dim\":[2],\"data\":[0.3,1.3]}],\"values\":[[[0.0,1.0],[2.0,3.0]],[[4.0,5.0],[6.0,7.0]]]}"; - let de2: InterpDataND<_> = serde_json::from_str(&ser2).unwrap(); + let de2: InterpDataND<_> = serde_json::from_str(ser2).unwrap(); assert_eq!(interp.data, de2); // complex format (legacy serialization output) let ser3 = "{\"grid\":[{\"v\":1,\"dim\":[2],\"data\":[0.1,1.1]},{\"v\":1,\"dim\":[2],\"data\":[0.2,1.2]},{\"v\":1,\"dim\":[2],\"data\":[0.3,1.3]}],\"values\":{\"v\":1,\"dim\":[2,2,2],\"data\":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0]}}"; - let de3: InterpDataND<_> = serde_json::from_str(&ser3).unwrap(); + let de3: InterpDataND<_> = serde_json::from_str(ser3).unwrap(); assert_eq!(interp.data, de3); } diff --git a/src/interpolator/one/mod.rs b/src/interpolator/one/mod.rs index da6266f..ec0a178 100644 --- a/src/interpolator/one/mod.rs +++ b/src/interpolator/one/mod.rs @@ -112,6 +112,16 @@ where Ok(interpolator) } + /// Re-run the strategy's [`Strategy1D::init`] against the current data. + /// + /// `new` and `set_strategy` already call this internally, so this is only needed + /// after bypassing them: mutating the public `data`/`strategy` fields directly, or + /// deserializing an interpolator with a stateful custom strategy (`Deserialize` + /// does not call `init`). + pub fn init_strategy(&mut self) -> Result<(), ValidateError> { + self.strategy.init(&self.data) + } + /// Return an interpolator with viewed data. pub fn view(&self) -> Interp1DViewed<&D::Elem, S> where @@ -151,10 +161,9 @@ where N } - fn validate(&mut self) -> Result<(), ValidateError> { + fn validate(&self) -> Result<(), ValidateError> { self.check_extrapolate(&self.extrapolate)?; self.data.validate()?; - self.strategy.init(&self.data)?; Ok(()) } @@ -207,10 +216,15 @@ where D: Data + RawDataClone + Clone, D::Elem: PartialEq + Debug, { - /// Update strategy dynamically. + /// Update strategy at runtime, calling [`Strategy1D::init`] on the new strategy + /// against the current data. + /// + /// To swap in a strategy without re-running `init` (e.g. one whose state was + /// already established elsewhere), assign the `strategy` field directly instead. pub fn set_strategy(&mut self, strategy: Box>) -> Result<(), ValidateError> { self.strategy = strategy; - self.check_extrapolate(&self.extrapolate) + self.check_extrapolate(&self.extrapolate)?; + self.strategy.init(&self.data) } } @@ -219,12 +233,17 @@ where D: Data + RawDataClone + Clone, D::Elem: Float + Debug, { - /// Update strategy dynamically. + /// Update strategy at runtime, calling [`Strategy1D::init`] on the new strategy + /// against the current data. + /// + /// To swap in a strategy without re-running `init` (e.g. one whose state was + /// already established elsewhere), assign the `strategy` field directly instead. pub fn set_strategy( &mut self, strategy: impl Into, ) -> Result<(), ValidateError> { self.strategy = strategy.into(); - self.check_extrapolate(&self.extrapolate) + self.check_extrapolate(&self.extrapolate)?; + self.strategy.init(&self.data) } } diff --git a/src/interpolator/one/tests.rs b/src/interpolator/one/tests.rs index 5427479..b6f766d 100644 --- a/src/interpolator/one/tests.rs +++ b/src/interpolator/one/tests.rs @@ -329,18 +329,18 @@ fn test_serde() { // simple format (new serialization output) let ser0 = "{\"grid\":[[0.0,1.0,2.0,3.0,4.0]],\"values\":[0.2,0.4,0.6,0.8,1.0]}"; - let de0: InterpData1D<_> = serde_json::from_str(&ser0).unwrap(); + let de0: InterpData1D<_> = serde_json::from_str(ser0).unwrap(); assert_eq!(interp.data, de0); // mixed format (simple grid) let ser1 = "{\"grid\":[[0.0,1.0,2.0,3.0,4.0]],\"values\":{\"v\":1,\"dim\":[5],\"data\":[0.2,0.4,0.6,0.8,1.0]}}"; - let de1: InterpData1D<_> = serde_json::from_str(&ser1).unwrap(); + let de1: InterpData1D<_> = serde_json::from_str(ser1).unwrap(); assert_eq!(interp.data, de1); // mixed format (simple values) let ser2 = "{\"grid\":[{\"v\":1,\"dim\":[5],\"data\":[0.0,1.0,2.0,3.0,4.0]}],\"values\":[0.2,0.4,0.6,0.8,1.0]}"; - let de2: InterpData1D<_> = serde_json::from_str(&ser2).unwrap(); + let de2: InterpData1D<_> = serde_json::from_str(ser2).unwrap(); assert_eq!(interp.data, de2); // complex format (legacy serialization output) let ser3 = "{\"grid\":[{\"v\":1,\"dim\":[5],\"data\":[0.0,1.0,2.0,3.0,4.0]}],\"values\":{\"v\":1,\"dim\":[5],\"data\":[0.2,0.4,0.6,0.8,1.0]}}"; - let de3: InterpData1D<_> = serde_json::from_str(&ser3).unwrap(); + let de3: InterpData1D<_> = serde_json::from_str(ser3).unwrap(); assert_eq!(interp.data, de3); } diff --git a/src/interpolator/three/mod.rs b/src/interpolator/three/mod.rs index d1c8102..2087a78 100644 --- a/src/interpolator/three/mod.rs +++ b/src/interpolator/three/mod.rs @@ -138,6 +138,16 @@ where Ok(interpolator) } + /// Re-run the strategy's [`Strategy3D::init`] against the current data. + /// + /// `new` and `set_strategy` already call this internally, so this is only needed + /// after bypassing them: mutating the public `data`/`strategy` fields directly, or + /// deserializing an interpolator with a stateful custom strategy (`Deserialize` + /// does not call `init`). + pub fn init_strategy(&mut self) -> Result<(), ValidateError> { + self.strategy.init(&self.data) + } + /// Return an interpolator with viewed data. pub fn view(&self) -> Interp3DViewed<&D::Elem, S> where @@ -177,10 +187,9 @@ where N } - fn validate(&mut self) -> Result<(), ValidateError> { + fn validate(&self) -> Result<(), ValidateError> { self.check_extrapolate(&self.extrapolate)?; self.data.validate()?; - self.strategy.init(&self.data)?; Ok(()) } @@ -243,10 +252,15 @@ where D: Data + RawDataClone + Clone, D::Elem: PartialEq + Debug, { - /// Update strategy dynamically. + /// Update strategy at runtime, calling [`Strategy3D::init`] on the new strategy + /// against the current data. + /// + /// To swap in a strategy without re-running `init` (e.g. one whose state was + /// already established elsewhere), assign the `strategy` field directly instead. pub fn set_strategy(&mut self, strategy: Box>) -> Result<(), ValidateError> { self.strategy = strategy; - self.check_extrapolate(&self.extrapolate) + self.check_extrapolate(&self.extrapolate)?; + self.strategy.init(&self.data) } } @@ -255,12 +269,17 @@ where D: Data + RawDataClone + Clone, D::Elem: Float + Debug, { - /// Update strategy dynamically. + /// Update strategy at runtime, calling [`Strategy3D::init`] on the new strategy + /// against the current data. + /// + /// To swap in a strategy without re-running `init` (e.g. one whose state was + /// already established elsewhere), assign the `strategy` field directly instead. pub fn set_strategy( &mut self, strategy: impl Into, ) -> Result<(), ValidateError> { self.strategy = strategy.into(); - self.check_extrapolate(&self.extrapolate) + self.check_extrapolate(&self.extrapolate)?; + self.strategy.init(&self.data) } } diff --git a/src/interpolator/three/tests.rs b/src/interpolator/three/tests.rs index fc51911..35229ff 100644 --- a/src/interpolator/three/tests.rs +++ b/src/interpolator/three/tests.rs @@ -323,18 +323,18 @@ fn test_serde() { // simple format (new serialization output) let ser0 = "{\"grid\":[[0.0,1.0],[0.0,1.0,2.0],[0.0,1.0,2.0,3.0]],\"values\":[[[0.6,0.8,1.0,1.2],[0.8,1.0,1.2,1.4],[1.0,1.2,1.4,1.6]],[[0.8,1.0,1.2,1.4],[1.0,1.2,1.4,1.6],[1.2,1.4,1.6,1.8]]]}"; - let de0: InterpData3D<_> = serde_json::from_str(&ser0).unwrap(); + let de0: InterpData3D<_> = serde_json::from_str(ser0).unwrap(); assert_eq!(interp.data, de0); // mixed format (simple grid) let ser1 = "{\"grid\":[[0.0,1.0],[0.0,1.0,2.0],[0.0,1.0,2.0,3.0]],\"values\":{\"v\":1,\"dim\":[2,3,4],\"data\":[0.6,0.8,1.0,1.2,0.8,1.0,1.2,1.4,1.0,1.2,1.4,1.6,0.8,1.0,1.2,1.4,1.0,1.2,1.4,1.6,1.2,1.4,1.6,1.8]}}"; - let de1: InterpData3D<_> = serde_json::from_str(&ser1).unwrap(); + let de1: InterpData3D<_> = serde_json::from_str(ser1).unwrap(); assert_eq!(interp.data, de1); // mixed format (simple values) let ser2 = "{\"grid\":[{\"v\":1,\"dim\":[2],\"data\":[0.0,1.0]},{\"v\":1,\"dim\":[3],\"data\":[0.0,1.0,2.0]},{\"v\":1,\"dim\":[4],\"data\":[0.0,1.0,2.0,3.0]}],\"values\":[[[0.6,0.8,1.0,1.2],[0.8,1.0,1.2,1.4],[1.0,1.2,1.4,1.6]],[[0.8,1.0,1.2,1.4],[1.0,1.2,1.4,1.6],[1.2,1.4,1.6,1.8]]]}"; - let de2: InterpData3D<_> = serde_json::from_str(&ser2).unwrap(); + let de2: InterpData3D<_> = serde_json::from_str(ser2).unwrap(); assert_eq!(interp.data, de2); // complex format (legacy serialization output) let ser3 = "{\"grid\":[{\"v\":1,\"dim\":[2],\"data\":[0.0,1.0]},{\"v\":1,\"dim\":[3],\"data\":[0.0,1.0,2.0]},{\"v\":1,\"dim\":[4],\"data\":[0.0,1.0,2.0,3.0]}],\"values\":{\"v\":1,\"dim\":[2,3,4],\"data\":[0.6,0.8,1.0,1.2,0.8,1.0,1.2,1.4,1.0,1.2,1.4,1.6,0.8,1.0,1.2,1.4,1.0,1.2,1.4,1.6,1.2,1.4,1.6,1.8]}}"; - let de3: InterpData3D<_> = serde_json::from_str(&ser3).unwrap(); + let de3: InterpData3D<_> = serde_json::from_str(ser3).unwrap(); assert_eq!(interp.data, de3); } diff --git a/src/interpolator/two/mod.rs b/src/interpolator/two/mod.rs index 5ce9dff..6edc981 100644 --- a/src/interpolator/two/mod.rs +++ b/src/interpolator/two/mod.rs @@ -126,6 +126,16 @@ where Ok(interpolator) } + /// Re-run the strategy's [`Strategy2D::init`] against the current data. + /// + /// `new` and `set_strategy` already call this internally, so this is only needed + /// after bypassing them: mutating the public `data`/`strategy` fields directly, or + /// deserializing an interpolator with a stateful custom strategy (`Deserialize` + /// does not call `init`). + pub fn init_strategy(&mut self) -> Result<(), ValidateError> { + self.strategy.init(&self.data) + } + /// Return an interpolator with viewed data. pub fn view(&self) -> Interp2DViewed<&D::Elem, S> where @@ -165,10 +175,9 @@ where N } - fn validate(&mut self) -> Result<(), ValidateError> { + fn validate(&self) -> Result<(), ValidateError> { self.check_extrapolate(&self.extrapolate)?; self.data.validate()?; - self.strategy.init(&self.data)?; Ok(()) } @@ -231,10 +240,15 @@ where D: Data + RawDataClone + Clone, D::Elem: PartialEq + Debug, { - /// Update strategy dynamically. + /// Update strategy at runtime, calling [`Strategy2D::init`] on the new strategy + /// against the current data. + /// + /// To swap in a strategy without re-running `init` (e.g. one whose state was + /// already established elsewhere), assign the `strategy` field directly instead. pub fn set_strategy(&mut self, strategy: Box>) -> Result<(), ValidateError> { self.strategy = strategy; - self.check_extrapolate(&self.extrapolate) + self.check_extrapolate(&self.extrapolate)?; + self.strategy.init(&self.data) } } @@ -243,12 +257,17 @@ where D: Data + RawDataClone + Clone, D::Elem: Float + Debug, { - /// Update strategy dynamically. + /// Update strategy at runtime, calling [`Strategy2D::init`] on the new strategy + /// against the current data. + /// + /// To swap in a strategy without re-running `init` (e.g. one whose state was + /// already established elsewhere), assign the `strategy` field directly instead. pub fn set_strategy( &mut self, strategy: impl Into, ) -> Result<(), ValidateError> { self.strategy = strategy.into(); - self.check_extrapolate(&self.extrapolate) + self.check_extrapolate(&self.extrapolate)?; + self.strategy.init(&self.data) } } diff --git a/src/interpolator/two/tests.rs b/src/interpolator/two/tests.rs index d7cdbbc..78cfe74 100644 --- a/src/interpolator/two/tests.rs +++ b/src/interpolator/two/tests.rs @@ -235,6 +235,45 @@ fn test_dyn_strategy() { assert_eq!(interp.interpolate(&[0.2, 0.]).unwrap(), 0.); } +#[test] +fn test_set_strategy_runs_init() { + // `Step`'s `init` validates its direction count against dimensionality, + // so swapping in a `Step` with the wrong count via `set_strategy` must + // surface that error rather than silently leaving the strategy unvalidated. + let mut interp: Interp2D<_, strategy::enums::Strategy2DEnum> = Interp2D::new( + array![0., 1.], + array![0., 1.], + array![[0., 1.], [2., 3.]], + strategy::Linear.into(), + Extrapolate::Error, + ) + .unwrap(); + let bad_step = strategy::Step(vec![strategy::StepDirection::Lower; 3]); + assert!(matches!( + interp.set_strategy(bad_step).unwrap_err(), + ValidateError::Other(_) + )); +} + +#[test] +fn test_init_strategy() { + // `validate()` only checks data/extrapolate, not strategy-specific requirements + // like `LinearUniform`'s uniform grid, so directly mutating `data` to break that + // invariant passes `validate()`. `init_strategy()` is the way to catch it, since + // it re-runs the same check `new`/`set_strategy` do internally. + let mut interp = Interp2D::new( + array![0., 1., 2.], + array![0., 1., 2.], + array![[0., 1., 2.], [3., 4., 5.], [6., 7., 8.]], + strategy::LinearUniform, + Extrapolate::Error, + ) + .unwrap(); + interp.data.grid[0] = array![0., 1., 5.]; // still monotonic, no longer uniform + assert!(interp.validate().is_ok()); + assert!(interp.init_strategy().is_err()); +} + #[test] fn test_extrapolate_clamp() { let interp = Interp2D::new( @@ -278,18 +317,18 @@ fn test_serde() { // simple format (new serialization output) let ser0 = "{\"grid\":[[0.05,0.1,0.15],[0.1,0.2,0.3]],\"values\":[[0.0,1.0,2.0],[3.0,4.0,5.0],[6.0,7.0,8.0]]}"; - let de0: InterpData2D<_> = serde_json::from_str(&ser0).unwrap(); + let de0: InterpData2D<_> = serde_json::from_str(ser0).unwrap(); assert_eq!(interp.data, de0); // mixed format (simple grid) let ser1 = "{\"grid\":[[0.05,0.1,0.15],[0.1,0.2,0.3]],\"values\":{\"v\":1,\"dim\":[3,3],\"data\":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0]}}"; - let de1: InterpData2D<_> = serde_json::from_str(&ser1).unwrap(); + let de1: InterpData2D<_> = serde_json::from_str(ser1).unwrap(); assert_eq!(interp.data, de1); // mixed format (simple values) let ser2 = "{\"grid\":[{\"v\":1,\"dim\":[3],\"data\":[0.05,0.1,0.15]},{\"v\":1,\"dim\":[3],\"data\":[0.1,0.2,0.3]}],\"values\":[[0.0,1.0,2.0],[3.0,4.0,5.0],[6.0,7.0,8.0]]}"; - let de2: InterpData2D<_> = serde_json::from_str(&ser2).unwrap(); + let de2: InterpData2D<_> = serde_json::from_str(ser2).unwrap(); assert_eq!(interp.data, de2); // complex format (legacy serialization output) let ser3 = "{\"grid\":[{\"v\":1,\"dim\":[3],\"data\":[0.05,0.1,0.15]},{\"v\":1,\"dim\":[3],\"data\":[0.1,0.2,0.3]}],\"values\":{\"v\":1,\"dim\":[3,3],\"data\":[0.0,1.0,2.0,3.0,4.0,5.0,6.0,7.0,8.0]}}"; - let de3: InterpData2D<_> = serde_json::from_str(&ser3).unwrap(); + let de3: InterpData2D<_> = serde_json::from_str(ser3).unwrap(); assert_eq!(interp.data, de3); } diff --git a/src/interpolator/zero/mod.rs b/src/interpolator/zero/mod.rs index 3d8c912..ac311df 100644 --- a/src/interpolator/zero/mod.rs +++ b/src/interpolator/zero/mod.rs @@ -41,7 +41,7 @@ where /// Returns `Ok(())`. #[inline] - fn validate(&mut self) -> Result<(), ValidateError> { + fn validate(&self) -> Result<(), ValidateError> { Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index e7bca2f..7552e04 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,7 +10,7 @@ /// - [`InterpND`](`interpolator::InterpND`) /// - A `serde`-compatible interpolator enum [`InterpolatorEnum`](`interpolator::enums::InterpolatorEnum`) /// - `Owned` and `Viewed` type aliases for all of the above -/// - Their common trait: [`Interpolator`] +/// - Their common trait: [`Interpolator`](`interpolator::Interpolator`) /// - The [`strategy`] mod, containing pre-defined interpolation strategies: /// - [`strategy::Linear`] /// - [`strategy::LinearUniform`] @@ -18,7 +18,7 @@ /// - [`strategy::Step`] (per-dimension and/or runtime-selected step directions) /// - [`strategy::StepLower`] / [`strategy::StepUpper`] /// - `serde`-compatible strategy enums: [`strategy::enums::Strategy1DEnum`]/etc. -/// - The extrapolation setting enum: [`Extrapolate`] +/// - The extrapolation setting enum: [`Extrapolate`](`interpolator::Extrapolate`) pub mod prelude { pub use crate::strategy; diff --git a/src/strategy/traits.rs b/src/strategy/traits.rs index b339163..b35b2e7 100644 --- a/src/strategy/traits.rs +++ b/src/strategy/traits.rs @@ -135,7 +135,7 @@ where Ok(()) } - /// Execute interpolation (after handling [`Extrapolate`] setting). + /// Execute interpolation (after handling [`Extrapolate`](`crate::interpolator::Extrapolate`) setting). fn interpolate( &self, data: &InterpData1D, @@ -185,7 +185,7 @@ where Ok(()) } - /// Execute interpolation (after handling [`Extrapolate`] setting). + /// Execute interpolation (after handling [`Extrapolate`](`crate::interpolator::Extrapolate`) setting). fn interpolate( &self, data: &InterpData2D, @@ -235,7 +235,7 @@ where Ok(()) } - /// Execute interpolation (after handling [`Extrapolate`] setting). + /// Execute interpolation (after handling [`Extrapolate`](`crate::interpolator::Extrapolate`) setting). fn interpolate( &self, data: &InterpData3D, @@ -285,7 +285,7 @@ where Ok(()) } - /// Execute interpolation (after handling [`Extrapolate`] setting). + /// Execute interpolation (after handling [`Extrapolate`](`crate::interpolator::Extrapolate`) setting). fn interpolate( &self, data: &InterpDataND,