Skip to main content

optionstratlib_math/geometrics/operations/
axis.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 23/1/25
5******************************************************************************/
6use crate::geometrics::InterpolationType;
7use itertools::Itertools;
8use rust_decimal::Decimal;
9use std::collections::HashSet;
10use std::hash::Hash;
11
12/// Trait for handling axis-based operations on geometric structures.
13///
14/// This trait provides methods for efficient lookups and manipulations
15/// of points based on their coordinate values. It is designed to work
16/// with both 2D curves and 3D surfaces.
17///
18/// # Type Parameters
19/// * `Point` - The complete point type (Point2D for curves, Point3D for surfaces)
20/// * `Input` - The input coordinate type (Decimal for curves, Point2D for surfaces)
21pub trait AxisOperations<Point, Input>
22where
23    Input: Hash + Eq + Clone + Ord,
24{
25    /// The type of error that can occur during point operations
26    type Error;
27
28    /// Checks if a coordinate value exists in the structure.
29    ///
30    /// # Arguments
31    /// * `x` - The coordinate value to check (x for curves, xy-point for surfaces)
32    ///
33    /// # Returns
34    /// * `bool` - `true` if the coordinate exists, `false` otherwise
35    fn contains_point(&self, x: &Input) -> bool;
36
37    /// Returns a vector of references to all index values in the structure.
38    ///
39    /// For curves, this returns x-coordinates.
40    /// For surfaces, this returns xy-coordinates.
41    ///
42    /// # Returns
43    /// * `Vec<&Input>` - Vector of references to index values
44    fn get_index_values(&self) -> Vec<Input>;
45
46    /// Returns a vector of references to dependent values for a given coordinate.
47    ///
48    /// For curves, returns y-values for a given x-coordinate.
49    /// For surfaces, returns z-values for a given xy-coordinate.
50    ///
51    /// # Arguments
52    /// * `x` - The coordinate value to lookup
53    ///
54    /// # Returns
55    /// * `Vec<&Decimal>` - Vector of references to dependent values
56    fn get_values(&self, x: Input) -> Vec<&Decimal>;
57
58    /// Finds the closest point to the given coordinate value.
59    ///
60    /// # Arguments
61    /// * `x` - The reference coordinate value
62    ///
63    /// # Returns
64    /// * `Result<&Point, Self::Error>` - The closest point or an error if no points exist
65    ///
66    /// # Errors
67    ///
68    /// Returns the implementation's `Self::Error` (typically an
69    /// empty-set variant) when the underlying container holds no
70    /// points to compare against.
71    fn get_closest_point(&self, x: &Input) -> Result<&Point, Self::Error>;
72
73    /// Finds the closest point to the given coordinate value.
74    ///
75    /// # Arguments
76    /// * `x` - The reference coordinate value
77    ///
78    /// # Returns
79    /// * `Result<&Point, Self::Error>` - The closest point or an error if no points exist
80    fn get_point(&self, x: &Input) -> Option<&Point>;
81
82    /// Merges the index values from the current structure with an additional set of indices.
83    /// This combines self.get_index_values() with the provided axis vector to create
84    /// a single vector of unique indices.
85    ///
86    /// # Arguments
87    /// * `axis` - Additional index values to merge with current structure's indices
88    ///
89    /// # Returns
90    /// * `Vec<&Input>` - Vector containing unique combined indices
91    fn merge_indexes(&self, axis: Vec<Input>) -> Vec<Input> {
92        let self_indexes: Vec<Input> = self.get_index_values();
93        let other_indexes: Vec<Input> = axis;
94
95        match (self_indexes.len(), other_indexes.len()) {
96            (0, _) => vec![],
97            (_, 0) => vec![],
98            _ => {
99                // Find the overlapping range. The match arm above
100                // guarantees both vectors are non-empty, so .first() /
101                // .last() are statically Some — fall back to a no-op
102                // empty result if that invariant is ever broken.
103                let (Some(min_self), Some(max_self), Some(min_other), Some(max_other)) = (
104                    self_indexes.first(),
105                    self_indexes.last(),
106                    other_indexes.first(),
107                    other_indexes.last(),
108                ) else {
109                    return vec![];
110                };
111
112                // Determine the common range
113                let start = std::cmp::max(min_self, min_other);
114                let end = std::cmp::min(max_self, max_other);
115
116                // Collect points within the common range from both sets
117                self_indexes
118                    .iter()
119                    .chain(other_indexes.iter())
120                    .filter(|&x| x >= start && x <= end)
121                    .cloned()
122                    .collect::<HashSet<_>>()
123                    .into_iter()
124                    .sorted()
125                    .collect()
126            }
127        }
128    }
129}
130
131/// Trait for merging and interpolating axes between compatible geometric structures.
132///
133/// This trait extends `AxisOperations` by providing methods to merge index values
134/// from two structures and interpolate points across the combined axes. This
135/// functionality is essential for operations that require aligning points from
136/// different structures onto a common set of coordinates.
137///
138/// # Type Parameters
139/// * `Point` - The complete point type (typically Point2D or Point3D)
140/// * `Input` - The input coordinate type (typically Decimal for 1D axes or Point2D for 2D axes)
141///
142pub trait MergeAxisInterpolate<Point, Input>: AxisOperations<Point, Input>
143where
144    Point: Clone,
145    Input: Hash + Eq + Clone + Ord,
146{
147    /// Merges the index values from two structures into a single ordered vector.
148    ///
149    /// This method combines the index values from `self` and `other` to create
150    /// a common set of indices that can be used for interpolation or alignment.
151    ///
152    /// # Arguments
153    /// * `other` - Another structure implementing the same trait
154    ///
155    /// # Returns
156    /// * `Vec<Input>` - Vector containing merged index values
157    fn merge_axis_index<'a>(&'a self, other: &'a Self) -> Vec<Input> {
158        let self_indexes: Vec<Input> = other.get_index_values();
159        self.merge_indexes(self_indexes)
160    }
161
162    /// Interpolates both structures to align them on a common set of index values.
163    ///
164    /// This method ensures that both structures have points at exactly the same
165    /// coordinate positions by adding interpolated points where necessary.
166    ///
167    /// # Arguments
168    /// * `other` - Another structure implementing the same trait
169    /// * `interpolation` - The interpolation method to use when creating new points
170    ///
171    /// # Returns
172    /// * `Result<(Self, Self), Self::Error>` - A tuple containing both structures
173    ///   with aligned coordinate points, or an error if interpolation fails
174    ///
175    /// # Errors
176    ///
177    /// Returns the implementation's `Self::Error` when either
178    /// structure is empty or when the per-axis interpolation on the
179    /// merged coordinate grid fails at any sample.
180    fn merge_axis_interpolate(
181        &self,
182        other: &Self,
183        interpolation: InterpolationType,
184    ) -> Result<(Self, Self), Self::Error>
185    where
186        Self: Sized;
187}
188
189#[cfg(test)]
190mod tests_merge_indexes {
191    use super::*;
192    use crate::curves::{Point2D, create_linear_curve};
193    use num_traits::ToPrimitive;
194    use rust_decimal_macros::dec;
195    use std::collections::BTreeSet;
196
197    // Mock struct for testing
198    struct TestCurve {
199        points: BTreeSet<Point2D>,
200    }
201
202    impl AxisOperations<Point2D, Decimal> for TestCurve {
203        type Error = String;
204
205        fn contains_point(&self, x: &Decimal) -> bool {
206            self.points.iter().any(|p| p.x == *x)
207        }
208
209        fn get_index_values(&self) -> Vec<Decimal> {
210            self.points.iter().map(|p| p.x).collect()
211        }
212
213        fn get_values(&self, x: Decimal) -> Vec<&Decimal> {
214            self.points
215                .iter()
216                .filter(|p| p.x == x)
217                .map(|p| &p.y)
218                .collect()
219        }
220
221        fn get_closest_point(&self, x: &Decimal) -> Result<&Point2D, Self::Error> {
222            self.points
223                .iter()
224                .min_by(|a, b| {
225                    let dist_a = (a.x - x).abs();
226                    let dist_b = (b.x - x).abs();
227                    dist_a.partial_cmp(&dist_b).unwrap()
228                })
229                .ok_or_else(|| "No points available".to_string())
230        }
231
232        fn get_point(&self, _x: &Decimal) -> Option<&Point2D> {
233            unreachable!()
234        }
235    }
236
237    fn create_test_curve_1() -> TestCurve {
238        let points = BTreeSet::from_iter(vec![
239            Point2D::new(dec!(1.0), dec!(2.0)),
240            Point2D::new(dec!(2.0), dec!(4.0)),
241            Point2D::new(dec!(4.0), dec!(8.0)),
242            Point2D::new(dec!(5.0), dec!(10.0)),
243        ]);
244        TestCurve { points }
245    }
246
247    fn create_test_curve_2() -> TestCurve {
248        let points = BTreeSet::from_iter(vec![
249            Point2D::new(dec!(2.0), dec!(3.0)),
250            Point2D::new(dec!(3.0), dec!(6.0)),
251            Point2D::new(dec!(5.0), dec!(9.0)),
252            Point2D::new(dec!(6.0), dec!(12.0)),
253        ]);
254        TestCurve { points }
255    }
256
257    #[test]
258    fn test_merge_indexes_common_points() {
259        let curve1 = create_test_curve_1();
260        let curve2 = create_test_curve_2();
261
262        let merged_indexes = curve1.merge_indexes(curve2.get_index_values());
263
264        // Should only contain common points
265        assert_eq!(merged_indexes.len(), 4);
266        let x1 = dec!(2.0);
267        let x2 = dec!(5.0);
268        assert!(merged_indexes.contains(&x1));
269        assert!(merged_indexes.contains(&x2));
270    }
271
272    #[test]
273    fn test_merge_indexes_empty_curve() {
274        let curve1 = create_test_curve_1();
275        let empty_curve: TestCurve = TestCurve {
276            points: BTreeSet::new(),
277        };
278        let merged_indexes = curve1.merge_indexes(empty_curve.get_index_values());
279
280        // Should be empty when one curve has no points
281        assert!(merged_indexes.is_empty());
282    }
283
284    #[test]
285    fn test_merge_indexes_no_common_points() {
286        let curve1 = TestCurve {
287            points: BTreeSet::from_iter(vec![Point2D::new(dec!(1.0), dec!(2.0))]),
288        };
289        let curve2 = TestCurve {
290            points: BTreeSet::from_iter(vec![Point2D::new(dec!(7.0), dec!(3.0))]),
291        };
292
293        let merged_indexes = curve1.merge_indexes(curve2.get_index_values());
294
295        // Should be empty when no common points
296        assert!(merged_indexes.is_empty());
297    }
298
299    #[test]
300    fn test_merge_indexes_normal() {
301        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(0.5)).unwrap();
302        let curve2 = create_linear_curve(dec!(5.0), dec!(15.0), dec!(1.0)).unwrap();
303        let merged_indexes = curve1.merge_indexes(curve2.get_index_values());
304
305        assert_eq!(merged_indexes.len(), 6);
306        assert_eq!(merged_indexes[0].to_f64().unwrap(), 5.0);
307        assert_eq!(merged_indexes[5].to_f64().unwrap(), 10.0);
308    }
309
310    #[test]
311    fn test_merge_indexes_normal_bis() {
312        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(0.5)).unwrap();
313        let curve2 = create_linear_curve(dec!(4.0), dec!(20.0), dec!(1.0)).unwrap();
314        let merged_indexes = curve1.merge_indexes(curve2.get_index_values());
315
316        assert_eq!(merged_indexes.len(), 10);
317        // [4.0, 5.0, 5.6, 6.0, 7.0, 7.2, 8.0, 8.8, 9.0, 10.0]
318        assert_eq!(merged_indexes[0].to_f64().unwrap(), 4.0);
319        assert_eq!(merged_indexes[5].to_f64().unwrap(), 7.2);
320        assert_eq!(merged_indexes[9].to_f64().unwrap(), 10.0);
321    }
322
323    #[test]
324    fn test_merge_indexes_identical_curves() {
325        let curve = create_test_curve_1();
326        let merged_indexes = curve.merge_indexes(curve.get_index_values());
327
328        // Should return all points from the curve
329        assert_eq!(merged_indexes.len(), 4);
330    }
331}