Skip to main content

optionstratlib_math/geometrics/interpolation/
traits.rs

1use crate::error::InterpolationError;
2use crate::geometrics::{
3    BiLinearInterpolation, CubicInterpolation, GeometricObject, InterpolationType,
4    LinearInterpolation, SplineInterpolation,
5};
6use rust_decimal::Decimal;
7
8/// A trait for performing various types of interpolation on a set of 2D points.
9///
10/// # Overview
11/// The `Interpolate` trait unifies methods for interpolating values in 2D Cartesian space.
12/// Implementers of this trait must support linear, bilinear, cubic, and spline interpolation methods.
13///
14/// This trait is designed for use with numerical and graphical applications requiring
15/// high-precision interpolation of data points. It provides functionality
16/// to retrieve the points, interpolate a value, and find bracketing points for a given x-coordinate.
17///
18/// # Type Parameters
19/// - `Point`: The point type that contains coordinates, must implement `HasX` and `Clone`
20/// - `Input`: The input type for interpolation queries, must implement `HasX`
21///
22/// # Methods
23///
24/// ## `interpolate`
25/// Interpolates a value for a given x-coordinate using a specified interpolation method.
26///
27/// ### Parameters
28/// - `x`: The x-coordinate at which to interpolate
29/// - `interpolation_type`: The type of interpolation algorithm to use
30///
31/// ### Returns
32/// - `Result<Point, InterpolationError>`: The interpolated point or an error
33///
34/// ## `find_bracket_points`
35/// Identifies the pair of points that bracket the target x-coordinate for interpolation.
36///
37/// ### Parameters
38/// - `x`: The x-coordinate to bracket
39///
40/// ### Returns
41/// - `Result<(usize, usize), InterpolationError>`: The indices of the bracketing points or an error
42///
43/// # Error Handling
44/// Methods in this trait return `InterpolationError` to represent various issues during interpolation:
45/// - Insufficient points for interpolation (need at least two points)
46/// - X-coordinate outside the valid range of data points
47/// - Failure to find bracketing points
48/// - Specific errors from the various interpolation algorithms
49///
50/// # Requirements
51/// Implementers must also implement the following traits:
52/// - `LinearInterpolation`
53/// - `BiLinearInterpolation`
54/// - `CubicInterpolation`
55/// - `SplineInterpolation`
56/// - `GeometricObject`
57///
58pub trait Interpolate<Point, Input>:
59    LinearInterpolation<Point, Input>
60    + BiLinearInterpolation<Point, Input>
61    + CubicInterpolation<Point, Input>
62    + SplineInterpolation<Point, Input>
63    + GeometricObject<Point, Input>
64where
65    Input: HasX,
66    Point: HasX + Clone,
67{
68    /// Interpolates a value at the given x-coordinate using the specified interpolation method.
69    ///
70    /// This method acts as a facade over the individual interpolation algorithms, delegating
71    /// to the appropriate method based on the requested interpolation type.
72    ///
73    /// # Parameters
74    /// - `x`: The x-coordinate at which to interpolate
75    /// - `interpolation_type`: The interpolation algorithm to use
76    ///
77    /// # Returns
78    /// - `Ok(Point)`: The successfully interpolated point
79    /// - `Err(InterpolationError)`: If interpolation fails for any reason
80    ///
81    /// # Errors
82    ///
83    /// Dispatches to the requested algorithm and propagates any
84    /// [`InterpolationError`] it produces; see the per-algorithm
85    /// traits ([`LinearInterpolation`], [`CubicInterpolation`],
86    /// [`BiLinearInterpolation`], [`SplineInterpolation`]) for the
87    /// specific variant breakdown, including what each of them does with a
88    /// sample that carries several ordinates at one abscissa.
89    fn interpolate(
90        &self,
91        x: Input,
92        interpolation_type: InterpolationType,
93    ) -> Result<Point, InterpolationError> {
94        match interpolation_type {
95            InterpolationType::Linear => self.linear_interpolate(x),
96            InterpolationType::Cubic => self.cubic_interpolate(x),
97            InterpolationType::Spline => self.spline_interpolate(x),
98            InterpolationType::Bilinear => self.bilinear_interpolate(x),
99        }
100    }
101
102    /// Finds the indices of points that bracket the given x-coordinate.
103    ///
104    /// This utility method identifies the pair of consecutive points in the dataset
105    /// where the first point's x-coordinate is less than or equal to the target x,
106    /// and the second point's x-coordinate is greater than or equal to the target x.
107    ///
108    /// # Parameters
109    /// - `x`: The x-coordinate for which to find bracketing points
110    ///
111    /// # Returns
112    /// - `Ok((usize, usize))`: The indices of the two bracketing points
113    /// - `Err(InterpolationError)`: If bracketing points cannot be found
114    ///
115    /// # One point per abscissa
116    ///
117    /// The sample is assumed to be a function of its abscissa; see
118    /// [`Curve::new`](crate::curves::Curve::new). This returns the *first*
119    /// window `(i, i + 1)` containing `x`, so when `x` is an abscissa
120    /// carrying several ordinates the window it returns is the degenerate
121    /// one spanning two of them, of zero width. That is deliberate: the
122    /// callers divide by that width and report
123    /// [`InterpolationError::DegenerateInterval`], instead of bracketing
124    /// against an arbitrary member of the stack.
125    ///
126    /// # Errors
127    /// - If there are fewer than 2 points in the dataset
128    /// - If the requested x-coordinate is outside the range of available points
129    /// - If bracketing points cannot be determined for any other reason
130    fn find_bracket_points(&self, x: Input) -> Result<(usize, usize), InterpolationError> {
131        let points: Vec<&Point> = self.get_points().into_iter().collect();
132        // Edge cases
133        if points.len() < 2 {
134            return Err(InterpolationError::EmptyData);
135        }
136        let (Some(first), Some(last)) = (points.first(), points.last()) else {
137            return Err(InterpolationError::EmptyData);
138        };
139        if x.get_x() < first.get_x() || x.get_x() > last.get_x() {
140            return Err(InterpolationError::OutOfRange {
141                target: x.get_x().to_string(),
142            });
143        }
144        // Find points that bracket x
145        for (i, pair) in points.windows(2).enumerate() {
146            let (Some(left), Some(right)) = (pair.first(), pair.get(1)) else {
147                return Err(InterpolationError::DegenerateInterval);
148            };
149            if left.get_x() <= x.get_x() && x.get_x() <= right.get_x() {
150                // `i + 1` is the index of `right`, inside the slice.
151                return i
152                    .checked_add(1)
153                    .map(|j| (i, j))
154                    .ok_or(InterpolationError::DegenerateInterval);
155            }
156        }
157        Err(InterpolationError::DegenerateInterval)
158    }
159}
160
161/// A trait for types that provide access to an X-coordinate value.
162///
163/// This trait defines a standard interface for any type that contains or can compute
164/// an X-coordinate represented as a `Decimal` value. Implementing this trait allows
165/// objects to be used in contexts where X-coordinate access is required, such as:
166///
167/// - Geometric calculations
168/// - Plotting and visualization
169/// - Interpolation algorithms
170/// - Data point analysis
171///
172/// # Required Method
173///
174/// ## `get_x`
175///
176/// Returns the X-coordinate value of the implementing type.
177///
178/// ### Returns
179/// - `Decimal`: The X-coordinate value, typically representing a position along the x-axis.
180///
181/// # Implementation Notes
182///
183/// When implementing this trait, ensure that:
184/// - The returned value accurately represents the X-coordinate in the appropriate scale and units
185/// - The implementation handles any necessary internal conversions or calculations
186/// - The method is computationally efficient if it will be called frequently
187///
188/// # Usage Examples
189///
190/// This trait can be implemented for various types such as:
191/// - 2D or 3D points
192/// - Data samples with timestamps or sequential positions
193/// - Geometric shapes with a defined reference point
194///
195/// # See Also
196/// - `Decimal`: The numeric type used to represent the X-coordinate value
197pub trait HasX {
198    /// Returns the X-coordinate value of this object.
199    ///
200    /// # Returns
201    /// - `Decimal`: The X-coordinate value as a `Decimal` type.
202    fn get_x(&self) -> Decimal;
203}
204
205/// A bare `Decimal` is its own X-coordinate, so scalar samples can feed the
206/// same interpolators as points.
207impl HasX for Decimal {
208    fn get_x(&self) -> Decimal {
209        *self
210    }
211}
212
213#[cfg(test)]
214mod tests_interpolate {
215    use super::*;
216    use crate::curves::Point2D;
217    use crate::error::InterpolationError;
218    use rust_decimal_macros::dec;
219    use std::collections::BTreeSet;
220
221    struct MockInterpolator {
222        points: BTreeSet<Point2D>,
223    }
224
225    impl LinearInterpolation<Point2D, Decimal> for MockInterpolator {
226        fn linear_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
227            // Validate points first
228            if self.points.len() < 2 {
229                return Err(InterpolationError::Linear(
230                    "Need at least two points for interpolation".to_string(),
231                ));
232            }
233            Ok(Point2D::new(x, x))
234        }
235    }
236
237    impl BiLinearInterpolation<Point2D, Decimal> for MockInterpolator {
238        fn bilinear_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
239            if self.points.len() < 4 {
240                return Err(InterpolationError::Bilinear(
241                    "Need at least four points for bilinear interpolation".to_string(),
242                ));
243            }
244            Ok(Point2D::new(x, x))
245        }
246    }
247
248    impl CubicInterpolation<Point2D, Decimal> for MockInterpolator {
249        fn cubic_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
250            if self.points.len() < 4 {
251                return Err(InterpolationError::Cubic(
252                    "Need at least four points for cubic interpolation".to_string(),
253                ));
254            }
255            Ok(Point2D::new(x, x))
256        }
257    }
258
259    impl SplineInterpolation<Point2D, Decimal> for MockInterpolator {
260        fn spline_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
261            if self.points.len() < 3 {
262                return Err(InterpolationError::Spline(
263                    "Need at least three points for spline interpolation".to_string(),
264                ));
265            }
266            Ok(Point2D::new(x, x))
267        }
268    }
269
270    impl GeometricObject<Point2D, Decimal> for MockInterpolator {
271        type Error = ();
272
273        fn get_points(&self) -> BTreeSet<&Point2D> {
274            self.points.iter().collect()
275        }
276
277        fn from_vector<T>(_points: Vec<T>) -> Self
278        where
279            Self: Sized,
280            T: Into<Point2D> + Clone,
281        {
282            unreachable!()
283        }
284
285        fn construct<T>(_method: T) -> Result<Self, Self::Error>
286        where
287            Self: Sized,
288        {
289            unreachable!()
290        }
291    }
292
293    impl Interpolate<Point2D, Decimal> for MockInterpolator {}
294
295    fn create_mock_interpolator(points: BTreeSet<Point2D>) -> MockInterpolator {
296        MockInterpolator { points }
297    }
298
299    #[test]
300    fn test_interpolate_empty_points() {
301        let interpolator = create_mock_interpolator(BTreeSet::new());
302
303        let linear = interpolator.interpolate(dec!(0.5), InterpolationType::Linear);
304        assert!(linear.is_err());
305
306        let bilinear = interpolator.interpolate(dec!(0.5), InterpolationType::Bilinear);
307        assert!(bilinear.is_err());
308
309        let cubic = interpolator.interpolate(dec!(0.5), InterpolationType::Cubic);
310        assert!(cubic.is_err());
311
312        let spline = interpolator.interpolate(dec!(0.5), InterpolationType::Spline);
313        assert!(spline.is_err());
314    }
315
316    #[test]
317    fn test_interpolate_insufficient_points() {
318        // Test with only one point
319        let interpolator =
320            create_mock_interpolator(BTreeSet::from_iter(vec![Point2D::new(dec!(0), dec!(0))]));
321
322        let linear = interpolator.interpolate(dec!(0.5), InterpolationType::Linear);
323        assert!(linear.is_err());
324
325        // Test with two points (should fail for all except linear)
326        let interpolator = create_mock_interpolator(BTreeSet::from_iter(vec![
327            Point2D::new(dec!(0), dec!(0)),
328            Point2D::new(dec!(1), dec!(1)),
329        ]));
330
331        assert!(
332            interpolator
333                .interpolate(dec!(0.5), InterpolationType::Bilinear)
334                .is_err()
335        );
336        assert!(
337            interpolator
338                .interpolate(dec!(0.5), InterpolationType::Cubic)
339                .is_err()
340        );
341        assert!(
342            interpolator
343                .interpolate(dec!(0.5), InterpolationType::Spline)
344                .is_err()
345        );
346    }
347
348    #[test]
349    fn test_get_points() {
350        let points = BTreeSet::from_iter(vec![
351            Point2D::new(dec!(0), dec!(0)),
352            Point2D::new(dec!(1), dec!(1)),
353        ]);
354        let interpolator = create_mock_interpolator(points.clone());
355
356        assert_eq!(
357            interpolator.vector(),
358            points.iter().collect::<Vec<_>>().as_slice()
359        );
360    }
361
362    #[test]
363    fn test_interpolate_routing() {
364        let points = BTreeSet::from_iter(vec![
365            Point2D::new(dec!(0), dec!(0)), // Point 1
366            Point2D::new(dec!(1), dec!(1)), // Point 2
367            Point2D::new(dec!(2), dec!(2)), // Point 3
368            Point2D::new(dec!(3), dec!(3)), // Point 4
369        ]);
370        let interpolator = create_mock_interpolator(points);
371        let x = dec!(0.5);
372
373        // Test that each interpolation type routes to its corresponding method
374        let linear = interpolator
375            .interpolate(x, InterpolationType::Linear)
376            .unwrap();
377        let bilinear = interpolator
378            .interpolate(x, InterpolationType::Bilinear)
379            .unwrap();
380        let cubic = interpolator
381            .interpolate(x, InterpolationType::Cubic)
382            .unwrap();
383        let spline = interpolator
384            .interpolate(x, InterpolationType::Spline)
385            .unwrap();
386
387        // In our mock implementation, all methods return (x, x)
388        assert_eq!(linear.x, x);
389        assert_eq!(bilinear.x, x);
390        assert_eq!(cubic.x, x);
391        assert_eq!(spline.x, x);
392    }
393
394    #[test]
395    fn test_find_bracket_points_success() {
396        let points = BTreeSet::from_iter(vec![
397            Point2D::new(dec!(0), dec!(0)),
398            Point2D::new(dec!(1), dec!(1)),
399            Point2D::new(dec!(2), dec!(2)),
400        ]);
401        let interpolator = create_mock_interpolator(points);
402
403        // Test finding brackets for a point in the middle
404        let (i, j) = interpolator.find_bracket_points(dec!(0.5)).unwrap();
405        assert_eq!(i, 0);
406        assert_eq!(j, 1);
407
408        // Test finding brackets for a point exactly on a known point
409        let (i, j) = interpolator.find_bracket_points(dec!(1.0)).unwrap();
410        assert_eq!(i, 0);
411        assert_eq!(j, 1);
412    }
413
414    #[test]
415    fn test_find_bracket_points_insufficient_points() {
416        let points = BTreeSet::from_iter(vec![Point2D::new(dec!(0), dec!(0))]);
417        let interpolator = create_mock_interpolator(points);
418
419        assert!(interpolator.find_bracket_points(dec!(0.5)).is_err());
420    }
421
422    #[test]
423    fn test_find_bracket_points_out_of_range() {
424        let points = BTreeSet::from_iter(vec![
425            Point2D::new(dec!(0), dec!(0)),
426            Point2D::new(dec!(1), dec!(1)),
427        ]);
428        let interpolator = create_mock_interpolator(points);
429
430        // Test x below range
431        assert!(interpolator.find_bracket_points(dec!(-0.5)).is_err());
432
433        // Test x above range
434        assert!(interpolator.find_bracket_points(dec!(1.5)).is_err());
435    }
436
437    #[test]
438    fn test_find_bracket_points_edge_cases() {
439        let points = BTreeSet::from_iter(vec![
440            Point2D::new(dec!(0), dec!(0)),
441            Point2D::new(dec!(1), dec!(1)),
442            Point2D::new(dec!(2), dec!(2)),
443        ]);
444        let interpolator = create_mock_interpolator(points);
445
446        // Test at lower boundary
447        let (i, j) = interpolator.find_bracket_points(dec!(0)).unwrap();
448        assert_eq!(i, 0);
449        assert_eq!(j, 1);
450
451        // Test at upper boundary
452        let (i, j) = interpolator.find_bracket_points(dec!(2)).unwrap();
453        assert_eq!(i, 1);
454        assert_eq!(j, 2);
455    }
456
457    #[test]
458    fn test_interpolate_with_empty_points() {
459        let interpolator = create_mock_interpolator(BTreeSet::from_iter(vec![]));
460        let result = interpolator.interpolate(dec!(0.5), InterpolationType::Linear);
461        assert!(result.is_err());
462    }
463
464    #[test]
465    fn test_interpolate_routing_minimum_points() {
466        // Test each interpolation type with its minimum required points
467
468        // Linear needs 2 points
469        let linear_points = BTreeSet::from_iter(vec![
470            Point2D::new(dec!(0), dec!(0)),
471            Point2D::new(dec!(1), dec!(1)),
472        ]);
473        let interpolator = create_mock_interpolator(linear_points);
474        let linear = interpolator.interpolate(dec!(0.5), InterpolationType::Linear);
475        assert!(linear.is_ok());
476
477        // Spline needs 3 points
478        let spline_points = BTreeSet::from_iter(vec![
479            Point2D::new(dec!(0), dec!(0)),
480            Point2D::new(dec!(1), dec!(1)),
481            Point2D::new(dec!(2), dec!(2)),
482        ]);
483        let interpolator = create_mock_interpolator(spline_points);
484        let spline = interpolator.interpolate(dec!(0.5), InterpolationType::Spline);
485        assert!(spline.is_ok());
486
487        // Bilinear and Cubic need 4 points
488        let four_points = BTreeSet::from_iter(vec![
489            Point2D::new(dec!(0), dec!(0)),
490            Point2D::new(dec!(1), dec!(1)),
491            Point2D::new(dec!(2), dec!(2)),
492            Point2D::new(dec!(3), dec!(3)),
493        ]);
494        let interpolator = create_mock_interpolator(four_points);
495        let bilinear = interpolator.interpolate(dec!(0.5), InterpolationType::Bilinear);
496        let cubic = interpolator.interpolate(dec!(0.5), InterpolationType::Cubic);
497        assert!(bilinear.is_ok());
498        assert!(cubic.is_ok());
499    }
500}