diff --git a/README.md b/README.md index 266c7d7..22df7dd 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,10 @@ An interpolation strategy (for example [`Linear`](https://docs.rs/ninterp/latest/ninterp/strategy/struct.Linear.html), [`LinearUniform`](https://docs.rs/ninterp/latest/ninterp/strategy/struct.LinearUniform.html), [`Nearest`](https://docs.rs/ninterp/latest/ninterp/strategy/struct.Nearest.html), -[`Step`](https://docs.rs/ninterp/latest/ninterp/strategy/struct.Step.html)) must be specified. +[`Step`](https://docs.rs/ninterp/latest/ninterp/strategy/struct.Step.html), +[`StepLower`](https://docs.rs/ninterp/latest/ninterp/strategy/struct.StepLower.html), +[`StepUpper`](https://docs.rs/ninterp/latest/ninterp/strategy/struct.StepUpper.html), +must be specified. To change the interpolation strategy, supply a `Strategy1DEnum`/etc. or `Box`/etc. at instantiation and call `set_strategy`. Custom strategies can be defined. See [`examples/custom_strategy.rs`](https://github.com/NatLabRockies/ninterp/blob/main/examples/custom_strategy.rs). diff --git a/src/interpolator/n/strategies.rs b/src/interpolator/n/strategies.rs index 7984ed6..8634c90 100644 --- a/src/interpolator/n/strategies.rs +++ b/src/interpolator/n/strategies.rs @@ -225,3 +225,49 @@ where false } } + +impl StrategyND for StepLower +where + D: Data + RawDataClone + Clone, + D::Elem: Num + PartialOrd + Copy + Debug, +{ + fn interpolate( + &self, + data: &InterpDataND, + point: &[D::Elem], + ) -> Result { + let n = data.values.ndim(); + let mut idx = vec![0usize; n]; + for dim in 0..n { + idx[dim] = step_index(StepDirection::Lower, data.grid[dim].view(), &point[dim]); + } + Ok(data.values.view()[idx.as_slice()]) + } + + fn allow_extrapolate(&self) -> bool { + false + } +} + +impl StrategyND for StepUpper +where + D: Data + RawDataClone + Clone, + D::Elem: Num + PartialOrd + Copy + Debug, +{ + fn interpolate( + &self, + data: &InterpDataND, + point: &[D::Elem], + ) -> Result { + let n = data.values.ndim(); + let mut idx = vec![0usize; n]; + for dim in 0..n { + idx[dim] = step_index(StepDirection::Upper, data.grid[dim].view(), &point[dim]); + } + Ok(data.values.view()[idx.as_slice()]) + } + + fn allow_extrapolate(&self) -> bool { + false + } +} diff --git a/src/interpolator/n/tests.rs b/src/interpolator/n/tests.rs index e40cb39..92aa074 100644 --- a/src/interpolator/n/tests.rs +++ b/src/interpolator/n/tests.rs @@ -297,6 +297,15 @@ fn test_step() { assert_eq!(interp.interpolate(&[0.3, 0.7, 0.6]).unwrap(), 0.); // floor→[0,0,0] assert_eq!(interp.interpolate(&[0.9, 0.9, 0.9]).unwrap(), 0.); // floor→[0,0,0] + let interp_lower = InterpND::new( + vec![array![0., 1.], array![0., 1.], array![0., 1.]], + array![[[0., 1.], [2., 3.]], [[4., 5.], [6., 7.]],].into_dyn(), + strategy::StepLower, + Extrapolate::Error, + ) + .unwrap(); + assert_eq!(interp_lower.interpolate(&[0.3, 0.7, 0.6]).unwrap(), 0.); + // Uniform Upper (ceiling) let interp_upper = InterpND::new( vec![array![0., 1.], array![0., 1.], array![0., 1.]], @@ -307,6 +316,18 @@ fn test_step() { .unwrap(); assert_eq!(interp_upper.interpolate(&[0.3, 0.7, 0.6]).unwrap(), 7.); // ceil→[1,1,1] + let interp_marker_upper = InterpND::new( + vec![array![0., 1.], array![0., 1.], array![0., 1.]], + array![[[0., 1.], [2., 3.]], [[4., 5.], [6., 7.]],].into_dyn(), + strategy::StepUpper, + Extrapolate::Error, + ) + .unwrap(); + assert_eq!( + interp_marker_upper.interpolate(&[0.3, 0.7, 0.6]).unwrap(), + 7. + ); + // Per-dimension: Lower in x, Upper in y, Lower in z let interp_mixed = InterpND::new( vec![array![0., 1.], array![0., 1.], array![0., 1.]], diff --git a/src/interpolator/one/strategies.rs b/src/interpolator/one/strategies.rs index 24fe95d..3f7725f 100644 --- a/src/interpolator/one/strategies.rs +++ b/src/interpolator/one/strategies.rs @@ -114,3 +114,41 @@ where false } } + +impl Strategy1D for StepLower +where + D: Data + RawDataClone + Clone, + D::Elem: Num + PartialOrd + Copy + Debug, +{ + fn interpolate( + &self, + data: &InterpData1D, + point: &[D::Elem; 1], + ) -> Result { + Ok(data.values[step_index(StepDirection::Lower, data.grid[0].view(), &point[0])]) + } + + /// Returns `false`. + fn allow_extrapolate(&self) -> bool { + false + } +} + +impl Strategy1D for StepUpper +where + D: Data + RawDataClone + Clone, + D::Elem: Num + PartialOrd + Copy + Debug, +{ + fn interpolate( + &self, + data: &InterpData1D, + point: &[D::Elem; 1], + ) -> Result { + Ok(data.values[step_index(StepDirection::Upper, data.grid[0].view(), &point[0])]) + } + + /// Returns `false`. + fn allow_extrapolate(&self) -> bool { + false + } +} diff --git a/src/interpolator/one/tests.rs b/src/interpolator/one/tests.rs index d07e979..5427479 100644 --- a/src/interpolator/one/tests.rs +++ b/src/interpolator/one/tests.rs @@ -75,6 +75,29 @@ fn test_right_nearest() { assert_eq!(interp.interpolate(&[4.00]).unwrap(), 1.0); } +#[test] +fn test_step_markers() { + let lower = Interp1D::new( + array![0., 1., 2., 3., 4.], + array![0.2, 0.4, 0.6, 0.8, 1.0], + strategy::StepLower, + Extrapolate::Error, + ) + .unwrap(); + assert_eq!(lower.interpolate(&[3.75]).unwrap(), 0.8); + assert_eq!(lower.interpolate(&[4.00]).unwrap(), 1.0); + + let upper = Interp1D::new( + array![0., 1., 2., 3., 4.], + array![0.2, 0.4, 0.6, 0.8, 1.0], + strategy::StepUpper, + Extrapolate::Error, + ) + .unwrap(); + assert_eq!(upper.interpolate(&[3.25]).unwrap(), 1.0); + assert_eq!(upper.interpolate(&[3.00]).unwrap(), 0.8); +} + #[test] fn test_nearest() { let interp = Interp1D::new( diff --git a/src/interpolator/three/strategies.rs b/src/interpolator/three/strategies.rs index b9fee55..253df29 100644 --- a/src/interpolator/three/strategies.rs +++ b/src/interpolator/three/strategies.rs @@ -241,3 +241,45 @@ where false } } + +impl Strategy3D for StepLower +where + D: Data + RawDataClone + Clone, + D::Elem: Num + PartialOrd + Copy + Debug, +{ + fn interpolate( + &self, + data: &InterpData3D, + point: &[D::Elem; 3], + ) -> Result { + let i = step_index(StepDirection::Lower, data.grid[0].view(), &point[0]); + let j = step_index(StepDirection::Lower, data.grid[1].view(), &point[1]); + let k = step_index(StepDirection::Lower, data.grid[2].view(), &point[2]); + Ok(data.values[[i, j, k]]) + } + + fn allow_extrapolate(&self) -> bool { + false + } +} + +impl Strategy3D for StepUpper +where + D: Data + RawDataClone + Clone, + D::Elem: Num + PartialOrd + Copy + Debug, +{ + fn interpolate( + &self, + data: &InterpData3D, + point: &[D::Elem; 3], + ) -> Result { + let i = step_index(StepDirection::Upper, data.grid[0].view(), &point[0]); + let j = step_index(StepDirection::Upper, data.grid[1].view(), &point[1]); + let k = step_index(StepDirection::Upper, data.grid[2].view(), &point[2]); + Ok(data.values[[i, j, k]]) + } + + fn allow_extrapolate(&self) -> bool { + false + } +} diff --git a/src/interpolator/three/tests.rs b/src/interpolator/three/tests.rs index e4f5b74..fc51911 100644 --- a/src/interpolator/three/tests.rs +++ b/src/interpolator/three/tests.rs @@ -155,6 +155,17 @@ fn test_step() { assert_eq!(interp.interpolate(&[0.3, 0.7, 0.6]).unwrap(), 0.); // floor→[0,0,0] assert_eq!(interp.interpolate(&[0.9, 0.4, 0.1]).unwrap(), 0.); // floor→[0,0,0] + let interp_lower = Interp3D::new( + array![0., 1.], + array![0., 1.], + array![0., 1.], + array![[[0., 1.], [2., 3.]], [[4., 5.], [6., 7.]]], + strategy::StepLower, + Extrapolate::Error, + ) + .unwrap(); + assert_eq!(interp_lower.interpolate(&[0.3, 0.7, 0.6]).unwrap(), 0.); + // Uniform Upper (ceiling) let interp_upper = Interp3D::new( array![0., 1.], @@ -167,6 +178,20 @@ fn test_step() { .unwrap(); assert_eq!(interp_upper.interpolate(&[0.3, 0.7, 0.6]).unwrap(), 7.); // ceil→[1,1,1] + let interp_marker_upper = Interp3D::new( + array![0., 1.], + array![0., 1.], + array![0., 1.], + array![[[0., 1.], [2., 3.]], [[4., 5.], [6., 7.]]], + strategy::StepUpper, + Extrapolate::Error, + ) + .unwrap(); + assert_eq!( + interp_marker_upper.interpolate(&[0.3, 0.7, 0.6]).unwrap(), + 7. + ); + // Per-dimension: Lower in x, Upper in y, Lower in z let interp_mixed = Interp3D::new( array![0., 1.], diff --git a/src/interpolator/two/strategies.rs b/src/interpolator/two/strategies.rs index 8d0537a..080f615 100644 --- a/src/interpolator/two/strategies.rs +++ b/src/interpolator/two/strategies.rs @@ -172,3 +172,43 @@ where false } } + +impl Strategy2D for StepLower +where + D: Data + RawDataClone + Clone, + D::Elem: Num + PartialOrd + Copy + Debug, +{ + fn interpolate( + &self, + data: &InterpData2D, + point: &[D::Elem; 2], + ) -> Result { + let i = step_index(StepDirection::Lower, data.grid[0].view(), &point[0]); + let j = step_index(StepDirection::Lower, data.grid[1].view(), &point[1]); + Ok(data.values[[i, j]]) + } + + fn allow_extrapolate(&self) -> bool { + false + } +} + +impl Strategy2D for StepUpper +where + D: Data + RawDataClone + Clone, + D::Elem: Num + PartialOrd + Copy + Debug, +{ + fn interpolate( + &self, + data: &InterpData2D, + point: &[D::Elem; 2], + ) -> Result { + let i = step_index(StepDirection::Upper, data.grid[0].view(), &point[0]); + let j = step_index(StepDirection::Upper, data.grid[1].view(), &point[1]); + Ok(data.values[[i, j]]) + } + + fn allow_extrapolate(&self) -> bool { + false + } +} diff --git a/src/interpolator/two/tests.rs b/src/interpolator/two/tests.rs index 594a4b9..d7cdbbc 100644 --- a/src/interpolator/two/tests.rs +++ b/src/interpolator/two/tests.rs @@ -119,6 +119,26 @@ fn test_step() { assert_eq!(interp.interpolate(&[0.7, 1.4]).unwrap(), f[[0, 1]]); // floor x→0, floor y→1 assert_eq!(interp.interpolate(&[1.9, 0.1]).unwrap(), f[[1, 0]]); // floor x→1, floor y→0 + let interp_lower = Interp2D::new( + grid_x.view(), + grid_y.view(), + values.view(), + strategy::StepLower, + Extrapolate::Error, + ) + .unwrap(); + assert_eq!(interp_lower.interpolate(&[0.7, 1.4]).unwrap(), f[[0, 1]]); + + let interp_upper = Interp2D::new( + grid_x.view(), + grid_y.view(), + values.view(), + strategy::StepUpper, + Extrapolate::Error, + ) + .unwrap(); + assert_eq!(interp_upper.interpolate(&[0.7, 1.4]).unwrap(), f[[1, 2]]); + // Per-dimension: Lower in x, Upper in y let interp_mixed = Interp2D::new( grid_x.view(), diff --git a/src/lib.rs b/src/lib.rs index 8395f12..f62b3f5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,8 @@ /// - [`strategy::Linear`] /// - [`strategy::LinearUniform`] /// - [`strategy::Nearest`] -/// - [`strategy::Step`] (replaces the former `LeftNearest`/`RightNearest`) +/// - [`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`] pub mod prelude { diff --git a/src/strategy/enums/mod.rs b/src/strategy/enums/mod.rs index 110e310..6abd76c 100644 --- a/src/strategy/enums/mod.rs +++ b/src/strategy/enums/mod.rs @@ -1,4 +1,6 @@ //! This module provides enums that allow mutable strategy swapping. +//! The enum variants and `From` impls here are hand-maintained per strategy type. +//! Adding a new strategy requires explicit wiring in each of the 1-D/2-D/3-D/N-D enum modules. //! //! This is an alternative to using a `Box`/etc. with a few key differences: //! - Better runtime performance @@ -34,10 +36,8 @@ //! assert_eq!(interp.interpolate(&[3.75]).unwrap(), 0.95); //! assert_eq!(interp.interpolate(&[4.00]).unwrap(), 1.0); //! -//! // Piecewise-constant: return value at nearest lower grid point -//! interp -//! .set_strategy(strategy::Step::from(strategy::StepDirection::Lower)) -//! .unwrap(); +//! // Piecewise-constant: fixed lower direction (zero-allocation marker strategy) +//! interp.set_strategy(strategy::StepLower).unwrap(); //! assert_eq!(interp.interpolate(&[3.75]).unwrap(), 0.8); //! assert_eq!(interp.interpolate(&[4.00]).unwrap(), 1.0); //! ``` @@ -85,9 +85,7 @@ mod tests { assert_eq!(interp.interpolate(&[3.75]).unwrap(), 1.0); assert_eq!(interp.interpolate(&[4.00]).unwrap(), 1.0); - interp - .set_strategy(strategy::Step::from(strategy::StepDirection::Lower)) - .unwrap(); + interp.set_strategy(strategy::StepLower).unwrap(); assert_eq!(interp.interpolate(&[3.00]).unwrap(), 0.8); assert_eq!(interp.interpolate(&[3.75]).unwrap(), 0.8); assert_eq!(interp.interpolate(&[4.00]).unwrap(), 1.0); diff --git a/src/strategy/enums/n.rs b/src/strategy/enums/n.rs index e70f48f..f437dfa 100644 --- a/src/strategy/enums/n.rs +++ b/src/strategy/enums/n.rs @@ -10,6 +10,8 @@ pub enum StrategyNDEnum { LinearUniform(strategy::LinearUniform), Nearest(strategy::Nearest), Step(strategy::Step), + StepLower(strategy::StepLower), + StepUpper(strategy::StepUpper), } impl From for StrategyNDEnum { @@ -40,6 +42,20 @@ impl From for StrategyNDEnum { } } +impl From for StrategyNDEnum { + #[inline] + fn from(strategy: StepLower) -> Self { + StrategyNDEnum::StepLower(strategy) + } +} + +impl From for StrategyNDEnum { + #[inline] + fn from(strategy: StepUpper) -> Self { + StrategyNDEnum::StepUpper(strategy) + } +} + impl StrategyND for StrategyNDEnum where D: Data + RawDataClone + Clone, @@ -52,6 +68,8 @@ where StrategyNDEnum::LinearUniform(strategy) => StrategyND::::init(strategy, data), StrategyNDEnum::Nearest(strategy) => StrategyND::::init(strategy, data), StrategyNDEnum::Step(strategy) => StrategyND::::init(strategy, data), + StrategyNDEnum::StepLower(strategy) => StrategyND::::init(strategy, data), + StrategyNDEnum::StepUpper(strategy) => StrategyND::::init(strategy, data), } } @@ -70,6 +88,12 @@ where StrategyND::::interpolate(strategy, data, point) } StrategyNDEnum::Step(strategy) => StrategyND::::interpolate(strategy, data, point), + StrategyNDEnum::StepLower(strategy) => { + StrategyND::::interpolate(strategy, data, point) + } + StrategyNDEnum::StepUpper(strategy) => { + StrategyND::::interpolate(strategy, data, point) + } } } @@ -80,6 +104,8 @@ where StrategyNDEnum::LinearUniform(strategy) => StrategyND::::allow_extrapolate(strategy), StrategyNDEnum::Nearest(strategy) => StrategyND::::allow_extrapolate(strategy), StrategyNDEnum::Step(strategy) => StrategyND::::allow_extrapolate(strategy), + StrategyNDEnum::StepLower(strategy) => StrategyND::::allow_extrapolate(strategy), + StrategyNDEnum::StepUpper(strategy) => StrategyND::::allow_extrapolate(strategy), } } } @@ -100,5 +126,13 @@ mod tests { serde_json::to_string(&StrategyNDEnum::from(Nearest)).unwrap(), serde_json::to_string(&Nearest).unwrap(), ); + assert_eq!( + serde_json::to_string(&StrategyNDEnum::from(StepLower)).unwrap(), + serde_json::to_string(&StepLower).unwrap(), + ); + assert_eq!( + serde_json::to_string(&StrategyNDEnum::from(StepUpper)).unwrap(), + serde_json::to_string(&StepUpper).unwrap(), + ); } } diff --git a/src/strategy/enums/one.rs b/src/strategy/enums/one.rs index d5df310..595938f 100644 --- a/src/strategy/enums/one.rs +++ b/src/strategy/enums/one.rs @@ -10,6 +10,8 @@ pub enum Strategy1DEnum { LinearUniform(strategy::LinearUniform), Nearest(strategy::Nearest), Step(strategy::Step), + StepLower(strategy::StepLower), + StepUpper(strategy::StepUpper), } impl From for Strategy1DEnum { @@ -40,6 +42,20 @@ impl From for Strategy1DEnum { } } +impl From for Strategy1DEnum { + #[inline] + fn from(strategy: StepLower) -> Self { + Self::StepLower(strategy) + } +} + +impl From for Strategy1DEnum { + #[inline] + fn from(strategy: StepUpper) -> Self { + Self::StepUpper(strategy) + } +} + impl Strategy1D for Strategy1DEnum where D: Data + RawDataClone + Clone, @@ -52,6 +68,8 @@ where Strategy1DEnum::LinearUniform(strategy) => Strategy1D::::init(strategy, data), Strategy1DEnum::Nearest(strategy) => Strategy1D::::init(strategy, data), Strategy1DEnum::Step(strategy) => Strategy1D::::init(strategy, data), + Strategy1DEnum::StepLower(strategy) => Strategy1D::::init(strategy, data), + Strategy1DEnum::StepUpper(strategy) => Strategy1D::::init(strategy, data), } } @@ -70,6 +88,12 @@ where Strategy1D::::interpolate(strategy, data, point) } Strategy1DEnum::Step(strategy) => Strategy1D::::interpolate(strategy, data, point), + Strategy1DEnum::StepLower(strategy) => { + Strategy1D::::interpolate(strategy, data, point) + } + Strategy1DEnum::StepUpper(strategy) => { + Strategy1D::::interpolate(strategy, data, point) + } } } @@ -80,6 +104,8 @@ where Strategy1DEnum::LinearUniform(strategy) => Strategy1D::::allow_extrapolate(strategy), Strategy1DEnum::Nearest(strategy) => Strategy1D::::allow_extrapolate(strategy), Strategy1DEnum::Step(strategy) => Strategy1D::::allow_extrapolate(strategy), + Strategy1DEnum::StepLower(strategy) => Strategy1D::::allow_extrapolate(strategy), + Strategy1DEnum::StepUpper(strategy) => Strategy1D::::allow_extrapolate(strategy), } } } @@ -100,5 +126,23 @@ mod tests { serde_json::to_string(&Strategy1DEnum::from(Nearest)).unwrap(), serde_json::to_string(&Nearest).unwrap(), ); + assert_eq!( + serde_json::to_string(&Strategy1DEnum::from(StepLower)).unwrap(), + serde_json::to_string(&StepLower).unwrap(), + ); + assert_eq!( + serde_json::to_string(&Strategy1DEnum::from(StepUpper)).unwrap(), + serde_json::to_string(&StepUpper).unwrap(), + ); + + // Legacy aliases deserialize through StepLower/StepUpper only. + assert!(matches!( + serde_json::from_str::("\"LeftNearest\"").unwrap(), + Strategy1DEnum::StepLower(_) + )); + assert!(matches!( + serde_json::from_str::("\"RightNearest\"").unwrap(), + Strategy1DEnum::StepUpper(_) + )); } } diff --git a/src/strategy/enums/three.rs b/src/strategy/enums/three.rs index b6c6086..0658cb9 100644 --- a/src/strategy/enums/three.rs +++ b/src/strategy/enums/three.rs @@ -10,6 +10,8 @@ pub enum Strategy3DEnum { LinearUniform(strategy::LinearUniform), Nearest(strategy::Nearest), Step(strategy::Step), + StepLower(strategy::StepLower), + StepUpper(strategy::StepUpper), } impl From for Strategy3DEnum { @@ -40,6 +42,20 @@ impl From for Strategy3DEnum { } } +impl From for Strategy3DEnum { + #[inline] + fn from(strategy: StepLower) -> Self { + Self::StepLower(strategy) + } +} + +impl From for Strategy3DEnum { + #[inline] + fn from(strategy: StepUpper) -> Self { + Self::StepUpper(strategy) + } +} + impl Strategy3D for Strategy3DEnum where D: Data + RawDataClone + Clone, @@ -52,6 +68,8 @@ where Strategy3DEnum::LinearUniform(strategy) => Strategy3D::::init(strategy, data), Strategy3DEnum::Nearest(strategy) => Strategy3D::::init(strategy, data), Strategy3DEnum::Step(strategy) => Strategy3D::::init(strategy, data), + Strategy3DEnum::StepLower(strategy) => Strategy3D::::init(strategy, data), + Strategy3DEnum::StepUpper(strategy) => Strategy3D::::init(strategy, data), } } @@ -70,6 +88,12 @@ where Strategy3D::::interpolate(strategy, data, point) } Strategy3DEnum::Step(strategy) => Strategy3D::::interpolate(strategy, data, point), + Strategy3DEnum::StepLower(strategy) => { + Strategy3D::::interpolate(strategy, data, point) + } + Strategy3DEnum::StepUpper(strategy) => { + Strategy3D::::interpolate(strategy, data, point) + } } } @@ -80,6 +104,8 @@ where Strategy3DEnum::LinearUniform(strategy) => Strategy3D::::allow_extrapolate(strategy), Strategy3DEnum::Nearest(strategy) => Strategy3D::::allow_extrapolate(strategy), Strategy3DEnum::Step(strategy) => Strategy3D::::allow_extrapolate(strategy), + Strategy3DEnum::StepLower(strategy) => Strategy3D::::allow_extrapolate(strategy), + Strategy3DEnum::StepUpper(strategy) => Strategy3D::::allow_extrapolate(strategy), } } } @@ -100,5 +126,13 @@ mod tests { serde_json::to_string(&Strategy3DEnum::from(Nearest)).unwrap(), serde_json::to_string(&Nearest).unwrap(), ); + assert_eq!( + serde_json::to_string(&Strategy3DEnum::from(StepLower)).unwrap(), + serde_json::to_string(&StepLower).unwrap(), + ); + assert_eq!( + serde_json::to_string(&Strategy3DEnum::from(StepUpper)).unwrap(), + serde_json::to_string(&StepUpper).unwrap(), + ); } } diff --git a/src/strategy/enums/two.rs b/src/strategy/enums/two.rs index 6a82236..d370527 100644 --- a/src/strategy/enums/two.rs +++ b/src/strategy/enums/two.rs @@ -10,6 +10,8 @@ pub enum Strategy2DEnum { LinearUniform(strategy::LinearUniform), Nearest(strategy::Nearest), Step(strategy::Step), + StepLower(strategy::StepLower), + StepUpper(strategy::StepUpper), } impl From for Strategy2DEnum { @@ -40,6 +42,20 @@ impl From for Strategy2DEnum { } } +impl From for Strategy2DEnum { + #[inline] + fn from(strategy: StepLower) -> Self { + Self::StepLower(strategy) + } +} + +impl From for Strategy2DEnum { + #[inline] + fn from(strategy: StepUpper) -> Self { + Self::StepUpper(strategy) + } +} + impl Strategy2D for Strategy2DEnum where D: Data + RawDataClone + Clone, @@ -52,6 +68,8 @@ where Strategy2DEnum::LinearUniform(strategy) => Strategy2D::::init(strategy, data), Strategy2DEnum::Nearest(strategy) => Strategy2D::::init(strategy, data), Strategy2DEnum::Step(strategy) => Strategy2D::::init(strategy, data), + Strategy2DEnum::StepLower(strategy) => Strategy2D::::init(strategy, data), + Strategy2DEnum::StepUpper(strategy) => Strategy2D::::init(strategy, data), } } @@ -70,6 +88,12 @@ where Strategy2D::::interpolate(strategy, data, point) } Strategy2DEnum::Step(strategy) => Strategy2D::::interpolate(strategy, data, point), + Strategy2DEnum::StepLower(strategy) => { + Strategy2D::::interpolate(strategy, data, point) + } + Strategy2DEnum::StepUpper(strategy) => { + Strategy2D::::interpolate(strategy, data, point) + } } } @@ -80,6 +104,8 @@ where Strategy2DEnum::LinearUniform(strategy) => Strategy2D::::allow_extrapolate(strategy), Strategy2DEnum::Nearest(strategy) => Strategy2D::::allow_extrapolate(strategy), Strategy2DEnum::Step(strategy) => Strategy2D::::allow_extrapolate(strategy), + Strategy2DEnum::StepLower(strategy) => Strategy2D::::allow_extrapolate(strategy), + Strategy2DEnum::StepUpper(strategy) => Strategy2D::::allow_extrapolate(strategy), } } } @@ -100,5 +126,13 @@ mod tests { serde_json::to_string(&Strategy2DEnum::from(Nearest)).unwrap(), serde_json::to_string(&Nearest).unwrap(), ); + assert_eq!( + serde_json::to_string(&Strategy2DEnum::from(StepLower)).unwrap(), + serde_json::to_string(&StepLower).unwrap(), + ); + assert_eq!( + serde_json::to_string(&Strategy2DEnum::from(StepUpper)).unwrap(), + serde_json::to_string(&StepUpper).unwrap(), + ); } } diff --git a/src/strategy/mod.rs b/src/strategy/mod.rs index 63f4d2f..dcdddaa 100644 --- a/src/strategy/mod.rs +++ b/src/strategy/mod.rs @@ -64,26 +64,42 @@ pub enum StepDirection { /// Piecewise-constant (step) interpolation. /// /// Returns the value at the nearest lower or upper grid point in each dimension. -/// Construct from a single [`StepDirection`] to broadcast the same direction across all -/// dimensions, or supply one direction per dimension for mixed behavior. +/// +/// Use [`Step`] when mixed per-dimension behavior is needed, or when direction is chosen at +/// runtime (for example from config). Use [`StepLower`] or [`StepUpper`] when a single direction +/// is known at compile time, especially in hot loops. +/// +/// Construct [`Step`] from a single [`StepDirection`] to broadcast the same direction across +/// all dimensions, or supply one direction per dimension for mixed behavior. /// /// # Examples /// ``` /// use ndarray::prelude::*; /// use ninterp::prelude::*; /// -/// // Floor (previous value): returns the value at the nearest lower grid point +/// // Returns the value at the nearest lower grid point /// let interp = Interp1D::new( /// array![0., 1., 2., 3., 4.], /// array![0.2, 0.4, 0.6, 0.8, 1.0], -/// strategy::Step::from(strategy::StepDirection::Lower), +/// strategy::StepLower, /// Extrapolate::Error, /// ) /// .unwrap(); /// assert_eq!(interp.interpolate(&[3.75]).unwrap(), 0.8); // floor → value at 3.0 /// assert_eq!(interp.interpolate(&[4.00]).unwrap(), 1.0); // exact grid point /// -/// // Ceiling (next value): returns the value at the nearest upper grid point +/// // Returns the value at the nearest upper grid point +/// let interp = Interp1D::new( +/// array![0., 1., 2., 3., 4.], +/// array![0.2, 0.4, 0.6, 0.8, 1.0], +/// strategy::StepUpper, +/// Extrapolate::Error, +/// ) +/// .unwrap(); +/// assert_eq!(interp.interpolate(&[3.25]).unwrap(), 1.0); // ceil → value at 4.0 +/// assert_eq!(interp.interpolate(&[3.00]).unwrap(), 0.8); // exact grid point +/// +/// // Behaves exactly like `StepUpper`, but allows the direction to be chosen at runtime /// let interp = Interp1D::new( /// array![0., 1., 2., 3., 4.], /// array![0.2, 0.4, 0.6, 0.8, 1.0], @@ -130,6 +146,22 @@ impl Step { } } +/// Piecewise-constant interpolation that always selects the nearest **lower** grid point. +/// +/// This is the zero-allocation, fixed-direction variant of [`Step`]. Prefer this when the +/// direction is known at compile time. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize_unit_struct))] +pub struct StepLower; + +/// Piecewise-constant interpolation that always selects the nearest **upper** grid point. +/// +/// This is the zero-allocation, fixed-direction variant of [`Step`]. Prefer this when the +/// direction is known at compile time. +#[derive(Debug, Clone, PartialEq)] +#[cfg_attr(feature = "serde", derive(Serialize_unit_struct))] +pub struct StepUpper; + #[cfg(feature = "serde")] mod step_serde { use super::*; @@ -161,6 +193,38 @@ mod step_serde { } } +#[cfg(feature = "serde")] +mod step_marker_serde { + use super::*; + use serde::{Deserialize, Deserializer}; + + #[derive(Deserialize)] + enum StepLowerDe { + #[serde(alias = "LeftNearest")] + StepLower, + } + + #[derive(Deserialize)] + enum StepUpperDe { + #[serde(alias = "RightNearest")] + StepUpper, + } + + impl<'de> Deserialize<'de> for StepLower { + fn deserialize>(deserializer: D) -> Result { + let _ = StepLowerDe::deserialize(deserializer)?; + Ok(StepLower) + } + } + + impl<'de> Deserialize<'de> for StepUpper { + fn deserialize>(deserializer: D) -> Result { + let _ = StepUpperDe::deserialize(deserializer)?; + Ok(StepUpper) + } + } +} + #[cfg(test)] mod tests { #[allow(unused_imports)] @@ -181,6 +245,14 @@ mod tests { serde_json::to_string(&Nearest).unwrap(), format!("\"{}\"", stringify!(Nearest)) ); + assert_eq!( + serde_json::to_string(&StepLower).unwrap(), + format!("\"{}\"", stringify!(StepLower)) + ); + assert_eq!( + serde_json::to_string(&StepUpper).unwrap(), + format!("\"{}\"", stringify!(StepUpper)) + ); assert_eq!( serde_json::to_string(&Step::from(StepDirection::Lower)).unwrap(), r#"{"Step":["Lower"]}"# @@ -193,5 +265,21 @@ mod tests { serde_json::to_string(&Step(vec![StepDirection::Lower, StepDirection::Upper])).unwrap(), r#"{"Step":["Lower","Upper"]}"# ); + + let step_lower: StepLower = serde_json::from_str("\"StepLower\"").unwrap(); + assert_eq!(step_lower, StepLower); + let step_upper: StepUpper = serde_json::from_str("\"StepUpper\"").unwrap(); + assert_eq!(step_upper, StepUpper); + // Backward-compatibility aliases for pre-Step serialized names. + assert_eq!( + serde_json::from_str::("\"LeftNearest\"").unwrap(), + StepLower + ); + assert_eq!( + serde_json::from_str::("\"RightNearest\"").unwrap(), + StepUpper + ); + // Aliases are intentionally scoped to StepLower/StepUpper, not StepDirection. + assert!(serde_json::from_str::(r#"{"Step":["LeftNearest"]}"#).is_err()); } }