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}