1use crate::curves::{Point2D, grid_coordinate};
9use crate::error::{InterpolationError, MetricsError, SurfaceError};
10use crate::geometrics::{
11 Arithmetic, AxisOperations, BasicMetrics, BiLinearInterpolation, ConstructionMethod,
12 ConstructionParams, CubicInterpolation, GeometricObject, GeometricTransformations, Interpolate,
13 InterpolationType, LinearInterpolation, MergeAxisInterpolate, MergeOperation, MetricsExtractor,
14 RangeMetrics, RiskMetrics, ShapeMetrics, SplineInterpolation, TrendMetrics,
15 coefficient_of_variation, population_std_dev, powu_checked,
16};
17use crate::surfaces::Point3D;
18use crate::surfaces::types::Axis;
19use optionstratlib_core::error::DecimalError;
20use optionstratlib_core::model::decimal::{
21 d_add, d_div, d_mul, d_product_iter, d_sqrt, d_sub, d_sum_iter, decimal_to_f64,
22};
23use optionstratlib_core::utils::Len;
24
25use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
26use rust_decimal::Decimal;
27use rust_decimal_macros::dec;
28use serde::{Deserialize, Serialize};
29use std::cmp::Reverse;
30use std::collections::{BTreeSet, btree_set};
31use std::ops::Index;
32use std::sync::Arc;
33use tracing::warn;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
81#[serde(deny_unknown_fields)]
82pub struct Surface {
83 pub points: BTreeSet<Point3D>,
89 pub x_range: (Decimal, Decimal),
91 pub y_range: (Decimal, Decimal),
93}
94
95impl Surface {
96 #[must_use]
154 pub fn new(points: BTreeSet<Point3D>) -> Self {
155 let x_range = Self::calculate_range(points.iter().map(|p| p.x));
156 let y_range = Self::calculate_range(points.iter().map(|p| p.y));
157 Self {
158 points,
159 x_range,
160 y_range,
161 }
162 }
163
164 #[must_use]
207 pub fn project_onto(&self, axis: Axis) -> Vec<Point2D> {
208 let mut points: Vec<Point2D> = self
209 .points
210 .iter()
211 .map(|p| match axis {
212 Axis::X => Point2D::new(p.y, p.z),
213 Axis::Y => Point2D::new(p.x, p.z),
214 Axis::Z => Point2D::new(p.x, p.y),
215 })
216 .collect();
217 points.sort();
220 points
221 }
222
223 fn one_dimensional_spline_interpolation<T>(
248 &self,
249 points: &[T],
250 target: Decimal,
251 x_selector: fn(&T) -> Decimal,
252 z_selector: fn(&T) -> Decimal,
253 ) -> Result<Decimal, InterpolationError>
254 where
255 T: Clone,
256 {
257 let mut sorted_points = points.to_vec();
259 sorted_points.sort_by(|a, b| {
260 x_selector(a)
261 .partial_cmp(&x_selector(b))
262 .unwrap_or(std::cmp::Ordering::Equal)
263 });
264
265 if sorted_points.len() < 2 {
267 return Err(InterpolationError::Spline(
268 "Insufficient points for interpolation".to_string(),
269 ));
270 }
271
272 let missing = |index: usize| {
273 InterpolationError::Spline(format!(
274 "spline knot {index} is out of bounds for {} samples",
275 sorted_points.len()
276 ))
277 };
278
279 let first = sorted_points.first().ok_or_else(|| missing(0))?;
281 if target <= x_selector(first) {
282 return Ok(z_selector(first));
283 }
284
285 let last_index = sorted_points
287 .len()
288 .checked_sub(1)
289 .ok_or_else(|| missing(0))?;
290 let last = sorted_points
291 .get(last_index)
292 .ok_or_else(|| missing(last_index))?;
293 if target >= x_selector(last) {
294 return Ok(z_selector(last));
295 }
296
297 let (left_index, right_index) = match sorted_points
301 .iter()
302 .enumerate()
303 .find(|(_, p)| x_selector(p) > target)
304 {
305 Some((index, _)) => (
306 index.checked_sub(1).ok_or_else(|| {
307 InterpolationError::Spline(
308 "spline bracket starts before the first knot".to_string(),
309 )
310 })?,
311 index,
312 ),
313 None => (
314 last_index.checked_sub(1).ok_or_else(|| {
315 InterpolationError::Spline(
316 "spline bracket needs at least two knots".to_string(),
317 )
318 })?,
319 last_index,
320 ),
321 };
322
323 let left = sorted_points
325 .get(left_index)
326 .ok_or_else(|| missing(left_index))?;
327 let right = sorted_points
328 .get(right_index)
329 .ok_or_else(|| missing(right_index))?;
330 let x0 = x_selector(left);
331 let x1 = x_selector(right);
332 let z0 = z_selector(left);
333 let z1 = z_selector(right);
334
335 let op = "Surface::one_dimensional_spline_interpolation";
337 let run = d_sub(x1, x0, op).map_err(interp_err(InterpolationError::Spline))?;
338 if run.is_zero() {
339 return Err(InterpolationError::DegenerateInterval);
340 }
341 let rise = d_sub(z1, z0, op).map_err(interp_err(InterpolationError::Spline))?;
342 let offset = d_sub(target, x0, op).map_err(interp_err(InterpolationError::Spline))?;
343 let ratio = d_div(offset, run, op).map_err(interp_err(InterpolationError::Spline))?;
344 let step = d_mul(rise, ratio, op).map_err(interp_err(InterpolationError::Spline))?;
345 let interpolated_z = d_add(z0, step, op).map_err(interp_err(InterpolationError::Spline))?;
346
347 Ok(interpolated_z)
348 }
349
350 fn points_at(&self, xy: &Point2D) -> btree_set::Range<'_, Point3D> {
356 self.points.range(
357 Point3D {
358 x: xy.x,
359 y: xy.y,
360 z: Decimal::MIN,
361 }..=Point3D {
362 x: xy.x,
363 y: xy.y,
364 z: Decimal::MAX,
365 },
366 )
367 }
368
369 fn point_at(&self, index: usize) -> Result<&Point3D, SurfaceError> {
372 self.points.iter().nth(index).ok_or_else(|| {
373 SurfaceError::AnalysisError(format!(
374 "point index {index} is out of bounds for a surface of {} points",
375 self.points.len()
376 ))
377 })
378 }
379
380 pub fn get_f64_points(&self) -> Result<Vec<(f64, f64, f64)>, SurfaceError> {
417 let to_f64 = |value: Decimal| {
418 decimal_to_f64(value).map_err(|_| SurfaceError::Point3DError {
419 reason: "Error converting Decimal to f64",
420 })
421 };
422 self.points
423 .iter()
424 .map(|p| Ok((to_f64(p.x)?, to_f64(p.z)?, to_f64(p.y)?)))
425 .collect()
426 }
427}
428
429fn interp_err(
432 kind: fn(String) -> InterpolationError,
433) -> impl Fn(DecimalError) -> InterpolationError {
434 move |err| kind(err.to_string())
435}
436
437fn construction_err(err: DecimalError) -> SurfaceError {
439 SurfaceError::ConstructionError(err.to_string())
440}
441
442fn analysis_err(err: DecimalError) -> SurfaceError {
444 SurfaceError::AnalysisError(err.to_string())
445}
446
447fn trend_err(err: DecimalError) -> MetricsError {
449 MetricsError::TrendError(err.to_string())
450}
451
452fn risk_err(err: DecimalError) -> MetricsError {
454 MetricsError::RiskError(err.to_string())
455}
456
457fn squared_distance(
461 point: &Point3D,
462 x: Decimal,
463 y: Decimal,
464 op: &'static str,
465) -> Result<Decimal, DecimalError> {
466 let dx = d_sub(point.x, x, op)?;
467 let dy = d_sub(point.y, y, op)?;
468 d_add(powu_checked(dx, 2, op)?, powu_checked(dy, 2, op)?, op)
469}
470
471fn sort_by_distance<'a>(
475 points: &mut [&'a Point3D],
476 x: Decimal,
477 y: Decimal,
478 op: &'static str,
479) -> Result<(), DecimalError> {
480 let mut keyed: Vec<(Decimal, &'a Point3D)> = Vec::with_capacity(points.len());
481 for point in points.iter() {
482 keyed.push((squared_distance(point, x, y, op)?, point));
483 }
484 keyed.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
485 for (slot, (_, point)) in points.iter_mut().zip(keyed) {
486 *slot = point;
487 }
488 Ok(())
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
496enum TreeOrder {
497 Before(Reverse<usize>),
498 From(usize),
499}
500
501fn distances_fit(points: &BTreeSet<Point3D>, x: Decimal, y: Decimal, op: &'static str) -> bool {
510 let (Some(first), Some(last)) = (points.first(), points.last()) else {
511 return true;
512 };
513 let (y_min, y_max) = points
514 .iter()
515 .fold((first.y, first.y), |(lo, hi), p| (lo.min(p.y), hi.max(p.y)));
516 let widest = |lo: Decimal, hi: Decimal, at: Decimal| -> Option<Decimal> {
517 let below = d_sub(lo, at, op).ok()?.abs();
518 let above = d_sub(hi, at, op).ok()?.abs();
519 powu_checked(below.max(above), 2, op).ok()
520 };
521 match (widest(first.x, last.x, x), widest(y_min, y_max, y)) {
522 (Some(dx2), Some(dy2)) => d_add(dx2, dy2, op).is_ok(),
523 _ => false,
524 }
525}
526
527fn nearest<'a>(
543 points: &'a BTreeSet<Point3D>,
544 x: Decimal,
545 y: Decimal,
546 k: usize,
547 op: &'static str,
548) -> Result<Vec<&'a Point3D>, DecimalError> {
549 if !distances_fit(points, x, y, op) {
550 let mut all: Vec<&Point3D> = points.iter().collect();
551 sort_by_distance(&mut all, x, y, op)?;
552 all.truncate(k);
553 return Ok(all);
554 }
555
556 let mut best: Vec<(Decimal, TreeOrder, &'a Point3D)> = Vec::with_capacity(k);
557 let mut offer = |point: &'a Point3D, order: TreeOrder| -> Result<bool, DecimalError> {
560 let full = best.len() >= k;
561 if full {
562 let reach = powu_checked(d_sub(point.x, x, op)?, 2, op)?;
563 match best.last() {
564 Some(&(kth, _, _)) if reach > kth => return Ok(false),
565 None => return Ok(false),
566 Some(_) => {}
567 }
568 }
569 let distance = squared_distance(point, x, y, op)?;
570 let at = best.partition_point(|&(d, o, _)| (d, o) < (distance, order));
571 if at < k {
572 if full {
573 best.pop();
574 }
575 best.insert(at, (distance, order, point));
576 }
577 Ok(true)
578 };
579
580 let split = Point3D {
581 x,
582 y: Decimal::MIN,
583 z: Decimal::MIN,
584 };
585 for (steps, point) in points.range(split..).enumerate() {
586 if !offer(point, TreeOrder::From(steps))? {
587 break;
588 }
589 }
590 for (steps, point) in points.range(..split).rev().enumerate() {
591 if !offer(point, TreeOrder::Before(Reverse(steps)))? {
592 break;
593 }
594 }
595 Ok(best.into_iter().map(|(_, _, point)| point).collect())
596}
597
598fn nth<'a>(
601 points: &[&'a Point3D],
602 index: usize,
603 kind: fn(String) -> InterpolationError,
604) -> Result<&'a Point3D, InterpolationError> {
605 points.get(index).copied().ok_or_else(|| {
606 kind(format!(
607 "neighbour {index} is out of bounds for a window of {} points",
608 points.len()
609 ))
610 })
611}
612
613fn third_quartile_index(len: usize) -> Result<usize, MetricsError> {
617 len.checked_mul(3)
618 .map(|three_len| three_len / 4)
619 .ok_or_else(|| {
620 MetricsError::BasicError(format!("third quartile index of {len} samples overflowed"))
621 })
622}
623
624fn sample_at(
625 values: &[Decimal],
626 index: usize,
627 what: &'static str,
628) -> Result<Decimal, MetricsError> {
629 values.get(index).copied().ok_or_else(|| {
630 MetricsError::BasicError(format!(
631 "{what}: sample {index} is out of bounds for {} values",
632 values.len()
633 ))
634 })
635}
636
637fn mean_of(values: &[Decimal], op: &'static str) -> Result<Decimal, DecimalError> {
640 let sum = d_sum_iter(values.iter().copied(), op)?;
641 d_div(sum, Decimal::from(values.len()), op)
642}
643
644fn central_moment(
647 values: &[Decimal],
648 mean: Decimal,
649 order: u64,
650 op: &'static str,
651) -> Result<Decimal, DecimalError> {
652 let mut acc = Decimal::ZERO;
653 for &value in values {
654 let centered = d_sub(value, mean, op)?;
655 acc = d_add(acc, powu_checked(centered, order, op)?, op)?;
656 }
657 Ok(acc)
658}
659
660impl Default for Surface {
661 fn default() -> Self {
662 Self {
663 points: BTreeSet::new(),
664 x_range: (Decimal::ZERO, Decimal::ZERO),
665 y_range: (Decimal::ZERO, Decimal::ZERO),
666 }
667 }
668}
669
670impl GeometricObject<Point3D, Point2D> for Surface {
691 type Error = SurfaceError;
692
693 fn get_points(&self) -> BTreeSet<&Point3D> {
722 self.points.iter().collect()
723 }
724
725 fn from_vector<T>(points: Vec<T>) -> Self
765 where
766 T: Into<Point3D> + Clone,
767 {
768 let points: BTreeSet<Point3D> = points.into_iter().map(|p| p.into()).collect();
769 let x_range = Self::calculate_range(points.iter().map(|p| p.x));
770 let y_range = Self::calculate_range(points.iter().map(|p| p.y));
771 Surface {
772 points,
773 x_range,
774 y_range,
775 }
776 }
777
778 fn construct<T>(method: T) -> Result<Self, Self::Error>
844 where
845 Self: Sized,
846 T: Into<ConstructionMethod<Point3D, Point2D, SurfaceError>>,
847 {
848 let method = method.into();
849 match method {
850 ConstructionMethod::FromData { points } => {
851 if points.is_empty() {
852 return Err(SurfaceError::Point3DError {
853 reason: "Empty points array",
854 });
855 }
856 Ok(Surface::new(points))
857 }
858 ConstructionMethod::Parametric { f, params } => {
859 let (x_start, x_end, y_start, y_end, x_steps, y_steps) = match params {
860 ConstructionParams::D3 {
861 x_start,
862 x_end,
863 y_start,
864 y_end,
865 x_steps,
866 y_steps,
867 } => (x_start, x_end, y_start, y_end, x_steps, y_steps),
868 _ => {
869 return Err(SurfaceError::ConstructionError(
870 "Invalid parameters".to_string(),
871 ));
872 }
873 };
874 if x_steps == 0 || y_steps == 0 {
875 return Err(SurfaceError::ConstructionError(
876 "Parametric construction needs at least one step on each axis".to_string(),
877 ));
878 }
879 let op = "Surface::construct::step";
880 let x_span = d_sub(x_end, x_start, op).map_err(construction_err)?;
881 let x_step = d_div(x_span, Decimal::from(x_steps), op).map_err(construction_err)?;
882 let y_span = d_sub(y_end, y_start, op).map_err(construction_err)?;
883 let y_step = d_div(y_span, Decimal::from(y_steps), op).map_err(construction_err)?;
884
885 let f = Arc::new(f);
887
888 let points: Result<BTreeSet<Point3D>, SurfaceError> = (0..=x_steps)
889 .into_par_iter()
890 .flat_map(|i| {
891 let f = Arc::clone(&f);
892 (0..=y_steps).into_par_iter().map(move |j| {
893 let x_offset =
894 d_mul(x_step, Decimal::from(i), op).map_err(construction_err)?;
895 let x = grid_coordinate(
898 d_add(x_start, x_offset, op).map_err(construction_err)?,
899 x_start,
900 x_end,
901 i == x_steps,
902 );
903 let y_offset =
904 d_mul(y_step, Decimal::from(j), op).map_err(construction_err)?;
905 let y = grid_coordinate(
906 d_add(y_start, y_offset, op).map_err(construction_err)?,
907 y_start,
908 y_end,
909 j == y_steps,
910 );
911 let t = Point2D::new(x, y);
912 f(t)
913 })
914 })
915 .collect();
916
917 points.map(Surface::new)
918 }
919 }
920 }
921}
922
923impl Index<usize> for Surface {
942 type Output = Point3D;
943
944 fn index(&self, index: usize) -> &Self::Output {
953 match self.points.iter().nth(index) {
957 Some(p) => p,
958 None => {
959 let len = self.points.len();
964 panic!("Surface::index: out of bounds (index = {index}, len = {len})") }
966 }
967 }
968}
969
970impl Interpolate<Point3D, Point2D> for Surface {}
1005
1006impl LinearInterpolation<Point3D, Point2D> for Surface {
1028 fn linear_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
1043 let first = match self.points.iter().next() {
1044 Some(p) => p,
1045 None => {
1046 return Err(InterpolationError::Linear(
1047 "No points in the surface".to_string(),
1048 ));
1049 }
1050 };
1051 let all_same_xy = self.points.iter().all(|p| p.x == first.x && p.y == first.y);
1052
1053 if all_same_xy && (first.x == xy.x && first.y == xy.y) {
1054 return Err(InterpolationError::Linear(
1055 "Degenerate triangle detected".to_string(),
1056 ));
1057 }
1058
1059 if xy.x < self.x_range.0
1060 || xy.x > self.x_range.1
1061 || xy.y < self.y_range.0
1062 || xy.y > self.y_range.1
1063 {
1064 return Err(InterpolationError::Linear(
1065 "Point is outside the surface's range".to_string(),
1066 ));
1067 }
1068
1069 let last = self.points.last().unwrap_or(first);
1073 if (first.x, first.y) == (last.x, last.y) {
1074 return Err(InterpolationError::Linear(
1075 "Degenerate triangle detected".to_string(),
1076 ));
1077 }
1078
1079 if let Some(point) = self.points_at(&xy).next() {
1081 return Ok(*point);
1082 }
1083
1084 if self.points.len() < 3 {
1087 return Err(InterpolationError::Linear(
1088 "Need at least three points for linear interpolation".to_string(),
1089 ));
1090 }
1091
1092 let op = "Surface::linear_interpolate";
1093 let nearest_points = nearest(&self.points, xy.x, xy.y, 3, op)
1094 .map_err(interp_err(InterpolationError::Linear))?;
1095
1096 let p1 = nth(&nearest_points, 0, InterpolationError::Linear)?;
1097 let p2 = nth(&nearest_points, 1, InterpolationError::Linear)?;
1098 let p3 = nth(&nearest_points, 2, InterpolationError::Linear)?;
1099
1100 let y23 = d_sub(p2.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
1101 let x13 = d_sub(p1.x, p3.x, op).map_err(interp_err(InterpolationError::Linear))?;
1102 let x32 = d_sub(p3.x, p2.x, op).map_err(interp_err(InterpolationError::Linear))?;
1103 let y13 = d_sub(p1.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
1104 let denominator = d_add(
1105 d_mul(y23, x13, op).map_err(interp_err(InterpolationError::Linear))?,
1106 d_mul(x32, y13, op).map_err(interp_err(InterpolationError::Linear))?,
1107 op,
1108 )
1109 .map_err(interp_err(InterpolationError::Linear))?;
1110 if denominator.is_zero() {
1111 return Err(InterpolationError::Linear(
1114 "Degenerate triangle detected: the three nearest points are collinear".to_string(),
1115 ));
1116 }
1117
1118 let qx3 = d_sub(xy.x, p3.x, op).map_err(interp_err(InterpolationError::Linear))?;
1119 let qy3 = d_sub(xy.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
1120 let y31 = d_sub(p3.y, p1.y, op).map_err(interp_err(InterpolationError::Linear))?;
1121
1122 let w1_num = d_add(
1123 d_mul(y23, qx3, op).map_err(interp_err(InterpolationError::Linear))?,
1124 d_mul(x32, qy3, op).map_err(interp_err(InterpolationError::Linear))?,
1125 op,
1126 )
1127 .map_err(interp_err(InterpolationError::Linear))?;
1128 let w1 = d_div(w1_num, denominator, op).map_err(interp_err(InterpolationError::Linear))?;
1129
1130 let w2_num = d_add(
1131 d_mul(y31, qx3, op).map_err(interp_err(InterpolationError::Linear))?,
1132 d_mul(x13, qy3, op).map_err(interp_err(InterpolationError::Linear))?,
1133 op,
1134 )
1135 .map_err(interp_err(InterpolationError::Linear))?;
1136 let w2 = d_div(w2_num, denominator, op).map_err(interp_err(InterpolationError::Linear))?;
1137
1138 let w3 = d_sub(
1139 d_sub(Decimal::ONE, w1, op).map_err(interp_err(InterpolationError::Linear))?,
1140 w2,
1141 op,
1142 )
1143 .map_err(interp_err(InterpolationError::Linear))?;
1144
1145 let z = d_sum_iter(
1146 [
1147 d_mul(w1, p1.z, op).map_err(interp_err(InterpolationError::Linear))?,
1148 d_mul(w2, p2.z, op).map_err(interp_err(InterpolationError::Linear))?,
1149 d_mul(w3, p3.z, op).map_err(interp_err(InterpolationError::Linear))?,
1150 ],
1151 op,
1152 )
1153 .map_err(interp_err(InterpolationError::Linear))?;
1154
1155 Ok(Point3D::new(xy.x, xy.y, z))
1156 }
1157}
1158
1159impl BiLinearInterpolation<Point3D, Point2D> for Surface {
1160 fn bilinear_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
1161 if self.points.len() < 4 {
1163 return Err(InterpolationError::Bilinear(
1164 "Need at least four points for bilinear interpolation".to_string(),
1165 ));
1166 }
1167
1168 if xy.x < self.x_range.0
1170 || xy.x > self.x_range.1
1171 || xy.y < self.y_range.0
1172 || xy.y > self.y_range.1
1173 {
1174 return Err(InterpolationError::Bilinear(
1175 "Point is outside the surface's range".to_string(),
1176 ));
1177 }
1178
1179 if self.points_at(&xy).count() == 4 {
1182 return Err(InterpolationError::Bilinear(
1183 "Invalid quadrilateral".to_string(),
1184 ));
1185 }
1186
1187 if let Some(point) = self.points_at(&xy).next() {
1189 return Ok(*point);
1190 }
1191
1192 let op = "Surface::bilinear_interpolate";
1194 let sorted_points = nearest(&self.points, xy.x, xy.y, 4, op)
1195 .map_err(interp_err(InterpolationError::Bilinear))?;
1196
1197 let closest_points = sorted_points.get(0..4).ok_or_else(|| {
1198 InterpolationError::Bilinear(format!(
1199 "need four neighbours, found {}",
1200 sorted_points.len()
1201 ))
1202 })?;
1203
1204 let mut quad_points: Vec<&Point3D> = closest_points.to_vec();
1206 quad_points.sort_by(|a, b| {
1207 let a_key = (a.y, a.x);
1208 let b_key = (b.y, b.x);
1209 a_key
1210 .partial_cmp(&b_key)
1211 .unwrap_or(std::cmp::Ordering::Equal)
1212 });
1213
1214 let q11 = nth(&quad_points, 0, InterpolationError::Bilinear)?; let q12 = nth(&quad_points, 1, InterpolationError::Bilinear)?; let q21 = nth(&quad_points, 2, InterpolationError::Bilinear)?; let q22 = nth(&quad_points, 3, InterpolationError::Bilinear)?; let x_span = d_sub(q12.x, q11.x, op).map_err(interp_err(InterpolationError::Bilinear))?;
1222 let y_span = d_sub(q21.y, q11.y, op).map_err(interp_err(InterpolationError::Bilinear))?;
1223 if x_span.is_zero() || y_span.is_zero() {
1224 return Err(InterpolationError::DegenerateInterval);
1227 }
1228 let x_offset = d_sub(xy.x, q11.x, op).map_err(interp_err(InterpolationError::Bilinear))?;
1229 let x_ratio =
1230 d_div(x_offset, x_span, op).map_err(interp_err(InterpolationError::Bilinear))?;
1231 let y_offset = d_sub(xy.y, q11.y, op).map_err(interp_err(InterpolationError::Bilinear))?;
1232 let y_ratio =
1233 d_div(y_offset, y_span, op).map_err(interp_err(InterpolationError::Bilinear))?;
1234
1235 let inv_x =
1237 d_sub(Decimal::ONE, x_ratio, op).map_err(interp_err(InterpolationError::Bilinear))?;
1238 let inv_y =
1239 d_sub(Decimal::ONE, y_ratio, op).map_err(interp_err(InterpolationError::Bilinear))?;
1240 let corner = |a: Decimal, b: Decimal, z: Decimal| -> Result<Decimal, InterpolationError> {
1241 let weight = d_mul(a, b, op).map_err(interp_err(InterpolationError::Bilinear))?;
1242 d_mul(weight, z, op).map_err(interp_err(InterpolationError::Bilinear))
1243 };
1244 let z = d_sum_iter(
1245 [
1246 corner(inv_x, inv_y, q11.z)?,
1247 corner(x_ratio, inv_y, q12.z)?,
1248 corner(inv_x, y_ratio, q21.z)?,
1249 corner(x_ratio, y_ratio, q22.z)?,
1250 ],
1251 op,
1252 )
1253 .map_err(interp_err(InterpolationError::Bilinear))?;
1254
1255 Ok(Point3D::new(xy.x, xy.y, z))
1256 }
1257}
1258
1259impl CubicInterpolation<Point3D, Point2D> for Surface {
1260 fn cubic_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
1261 if self.points.len() < 9 {
1263 return Err(InterpolationError::Cubic(
1264 "Need at least nine points for cubic interpolation".to_string(),
1265 ));
1266 }
1267
1268 if xy.x < self.x_range.0
1270 || xy.x > self.x_range.1
1271 || xy.y < self.y_range.0
1272 || xy.y > self.y_range.1
1273 {
1274 return Err(InterpolationError::Cubic(
1275 "Point is outside the surface's range".to_string(),
1276 ));
1277 }
1278
1279 if let Some(point) = self.points_at(&xy).next() {
1281 return Ok(*point);
1282 }
1283
1284 let op = "Surface::cubic_interpolate";
1286 let sorted_points = nearest(&self.points, xy.x, xy.y, 9, op)
1287 .map_err(interp_err(InterpolationError::Cubic))?;
1288
1289 let closest_points = sorted_points.get(0..9).ok_or_else(|| {
1290 InterpolationError::Cubic(format!(
1291 "need nine neighbours, found {}",
1292 sorted_points.len()
1293 ))
1294 })?;
1295
1296 let mut weights: Vec<Decimal> = Vec::with_capacity(closest_points.len());
1301 for &point in closest_points {
1302 let sq = squared_distance(point, xy.x, xy.y, op)
1303 .map_err(interp_err(InterpolationError::Cubic))?;
1304 let dist = match d_sqrt(sq, op) {
1305 Ok(d) => d,
1306 Err(_) => {
1307 warn!(
1313 "cubic_interpolate: sqrt failed for operand ({sq}); dropping point from weighting"
1314 );
1315 weights.push(Decimal::ZERO);
1316 continue;
1317 }
1318 };
1319 let shifted = d_add(dist, Decimal::new(1, 6), op)
1321 .map_err(interp_err(InterpolationError::Cubic))?;
1322 weights.push(
1323 d_div(Decimal::ONE, shifted, op).map_err(interp_err(InterpolationError::Cubic))?,
1324 );
1325 }
1326
1327 let mut numerator_z = Decimal::ZERO;
1329 let mut denominator = Decimal::ZERO;
1330
1331 for (&point, &weight) in closest_points.iter().zip(weights.iter()) {
1332 let cubic_weight =
1334 powu_checked(weight, 3, op).map_err(interp_err(InterpolationError::Cubic))?;
1335 let contribution =
1336 d_mul(point.z, cubic_weight, op).map_err(interp_err(InterpolationError::Cubic))?;
1337 numerator_z = d_add(numerator_z, contribution, op)
1338 .map_err(interp_err(InterpolationError::Cubic))?;
1339 denominator = d_add(denominator, cubic_weight, op)
1340 .map_err(interp_err(InterpolationError::Cubic))?;
1341 }
1342
1343 let interpolated_z = if denominator != Decimal::ZERO {
1345 d_div(numerator_z, denominator, op).map_err(interp_err(InterpolationError::Cubic))?
1346 } else {
1347 let zs: Vec<Decimal> = closest_points.iter().map(|p| p.z).collect();
1349 let sum = d_sum_iter(zs.iter().copied(), op)
1350 .map_err(interp_err(InterpolationError::Cubic))?;
1351 d_div(sum, Decimal::from(closest_points.len()), op)
1352 .map_err(interp_err(InterpolationError::Cubic))?
1353 };
1354
1355 Ok(Point3D::new(xy.x, xy.y, interpolated_z))
1356 }
1357}
1358
1359impl SplineInterpolation<Point3D, Point2D> for Surface {
1360 fn spline_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
1361 if self.points.len() < 9 {
1363 return Err(InterpolationError::Spline(
1364 "Need at least nine points for spline interpolation".to_string(),
1365 ));
1366 }
1367
1368 if xy.x < self.x_range.0
1370 || xy.x > self.x_range.1
1371 || xy.y < self.y_range.0
1372 || xy.y > self.y_range.1
1373 {
1374 return Err(InterpolationError::Spline(
1375 "Point is outside the surface's range".to_string(),
1376 ));
1377 }
1378
1379 if let Some(point) = self.points_at(&xy).next() {
1381 return Ok(*point);
1382 }
1383
1384 let mut sorted_points: Vec<&Point3D> = self.points.iter().collect();
1386 sorted_points.sort_by(|a, b| {
1387 let a_key = (a.x, a.y);
1388 let b_key = (b.x, b.y);
1389 a_key
1390 .partial_cmp(&b_key)
1391 .unwrap_or(std::cmp::Ordering::Equal)
1392 });
1393
1394 let mut x_groups: std::collections::HashMap<Decimal, Vec<&Point3D>> =
1396 std::collections::HashMap::new();
1397 let mut y_groups: std::collections::HashMap<Decimal, Vec<&Point3D>> =
1398 std::collections::HashMap::new();
1399
1400 for &point in &sorted_points {
1401 x_groups.entry(point.x).or_default().push(point);
1402 y_groups.entry(point.y).or_default().push(point);
1403 }
1404
1405 let y_values: Vec<Decimal> = y_groups.keys().cloned().collect();
1407
1408 let mut interpolated_x_points: Vec<Point3D> = Vec::new();
1413 for &y in &y_values {
1414 let y_points: Vec<&Point3D> = sorted_points
1415 .iter()
1416 .filter(|&&p| p.y == y)
1417 .cloned()
1418 .collect();
1419
1420 if y_points.len() < 2 {
1421 continue;
1422 }
1423
1424 let x_interpolated =
1426 self.one_dimensional_spline_interpolation(&y_points, xy.x, |p| p.x, |p| p.z);
1427
1428 if let Ok(z) = x_interpolated {
1429 interpolated_x_points.push(Point3D::new(xy.x, y, z));
1430 }
1431 }
1432
1433 if interpolated_x_points.is_empty() {
1435 return Err(InterpolationError::Spline(
1436 "Could not interpolate along x-axis".to_string(),
1437 ));
1438 }
1439
1440 let y_interpolated = self.one_dimensional_spline_interpolation(
1442 &interpolated_x_points,
1443 xy.y,
1444 |p| p.y,
1445 |p| p.z,
1446 );
1447
1448 y_interpolated.map(|z| Point3D::new(xy.x, xy.y, z))
1450 }
1451}
1452
1453impl Len for Surface {
1454 fn len(&self) -> usize {
1455 self.points.len()
1456 }
1457
1458 fn is_empty(&self) -> bool {
1459 self.points.is_empty()
1460 }
1461}
1462
1463impl MetricsExtractor for Surface {
1464 fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
1465 let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
1466
1467 if z_values.is_empty() {
1470 return Ok(BasicMetrics {
1471 mean: Decimal::ZERO,
1472 median: Decimal::ZERO,
1473 mode: Decimal::ZERO,
1474 std_dev: Decimal::ZERO,
1475 });
1476 }
1477
1478 let op = "Surface::compute_basic_metrics";
1479 let mean = mean_of(&z_values, op).map_err(|e| MetricsError::BasicError(e.to_string()))?;
1480
1481 let mut sorted = z_values.clone();
1482 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1483 let median = sample_at(&sorted, sorted.len() / 2, "median")?;
1484
1485 let mode = {
1487 let mut freq_map = std::collections::HashMap::new();
1488 for &val in &z_values {
1489 let count = freq_map.entry(val).or_insert(0usize);
1490 *count = count.checked_add(1).ok_or_else(|| {
1491 MetricsError::BasicError("mode: frequency count overflowed".to_string())
1492 })?;
1493 }
1494 freq_map
1495 .into_iter()
1496 .max_by_key(|&(_, count)| count)
1497 .map(|(val, _)| val)
1498 .unwrap_or(Decimal::ZERO)
1499 };
1500
1501 let sum_sq = central_moment(&z_values, mean, 2, op)
1502 .map_err(|e| MetricsError::BasicError(e.to_string()))?;
1503 let variance = d_div(sum_sq, Decimal::from(z_values.len()), op)
1504 .map_err(|e| MetricsError::BasicError(e.to_string()))?;
1505 let std_dev = d_sqrt(variance, op).map_err(|e| MetricsError::BasicError(e.to_string()))?;
1507
1508 Ok(BasicMetrics {
1509 mean,
1510 median,
1511 mode,
1512 std_dev,
1513 })
1514 }
1515
1516 fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
1517 let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
1518
1519 if z_values.len() < 2 {
1523 return Ok(ShapeMetrics {
1524 skewness: Decimal::ZERO,
1525 kurtosis: Decimal::ZERO,
1526 peaks: vec![],
1527 valleys: vec![],
1528 inflection_points: vec![],
1529 });
1530 }
1531
1532 let op = "Surface::compute_shape_metrics";
1533 let mean = mean_of(&z_values, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1534 let sum_sq = central_moment(&z_values, mean, 2, op)
1535 .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1536 let variance = d_div(sum_sq, Decimal::from(z_values.len()), op)
1537 .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1538 let std_dev = d_sqrt(variance, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1540 if std_dev.is_zero() {
1541 return Err(MetricsError::ShapeError(format!(
1542 "standard deviation ({std_dev}) is too small to compute skewness/kurtosis; the surface is degenerate"
1543 )));
1544 }
1545
1546 let n = Decimal::from(z_values.len());
1547
1548 let skew_num = central_moment(&z_values, mean, 3, op)
1549 .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1550 let skew_den = d_mul(
1551 n,
1552 powu_checked(std_dev, 3, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?,
1553 op,
1554 )
1555 .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1556 let skewness =
1557 d_div(skew_num, skew_den, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1558
1559 let kurt_num = central_moment(&z_values, mean, 4, op)
1560 .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1561 let kurt_den = d_mul(
1562 n,
1563 powu_checked(std_dev, 4, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?,
1564 op,
1565 )
1566 .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1567 let kurtosis =
1568 d_div(kurt_num, kurt_den, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1569
1570 Ok(ShapeMetrics {
1571 skewness,
1572 kurtosis,
1573 peaks: vec![],
1574 valleys: vec![],
1575 inflection_points: vec![],
1576 })
1577 }
1578
1579 fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
1580 let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
1581
1582 if z_values.is_empty() {
1585 return Ok(RangeMetrics {
1586 min: Point2D::new(Decimal::ZERO, Decimal::ZERO),
1587 max: Point2D::new(Decimal::ZERO, Decimal::ZERO),
1588 range: Decimal::ZERO,
1589 quartiles: (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
1590 interquartile_range: Decimal::ZERO,
1591 });
1592 }
1593
1594 let mut sorted = z_values.clone();
1595 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1596
1597 let min = sorted.first().copied().unwrap_or(Decimal::ZERO);
1598 let max = sorted.last().copied().unwrap_or(Decimal::ZERO);
1599
1600 let op = "Surface::compute_range_metrics";
1601 let len = sorted.len();
1602 let q1 = sample_at(&sorted, len / 4, "first quartile")?;
1603 let q2 = sample_at(&sorted, len / 2, "median")?;
1604 let q3 = sample_at(&sorted, third_quartile_index(len)?, "third quartile")?;
1605
1606 let range = d_sub(max, min, op).map_err(|e| MetricsError::RangeError(e.to_string()))?;
1607 let iqr = d_sub(q3, q1, op).map_err(|e| MetricsError::RangeError(e.to_string()))?;
1608
1609 Ok(RangeMetrics {
1610 min: Point2D::new(Decimal::ZERO, min),
1611 max: Point2D::new(Decimal::ZERO, max),
1612 range,
1613 quartiles: (q1, q2, q3),
1614 interquartile_range: iqr,
1615 })
1616 }
1617
1618 fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
1619 let points: Vec<Point2D> = self.points.iter().map(|p| Point2D::new(p.x, p.z)).collect();
1620
1621 if points.len() < 2 {
1623 return Ok(TrendMetrics {
1624 slope: Decimal::ZERO,
1625 intercept: Decimal::ZERO,
1626 r_squared: Decimal::ONE,
1627 moving_average: vec![],
1628 });
1629 }
1630
1631 let n = Decimal::from(points.len());
1633 let x_vals: Vec<Decimal> = points.iter().map(|p| p.x).collect();
1634 let z_vals: Vec<Decimal> = points.iter().map(|p| p.y).collect();
1635
1636 let op = "Surface::compute_trend_metrics";
1637 let sum_x = d_sum_iter(x_vals.iter().copied(), op).map_err(trend_err)?;
1638 let sum_z = d_sum_iter(z_vals.iter().copied(), op).map_err(trend_err)?;
1639
1640 let first_z = sample_at(&z_vals, 0, "trend baseline")?;
1642 let is_identical_points = z_vals.iter().all(|&z| z == first_z);
1643
1644 let (slope, intercept, r_squared) = if is_identical_points {
1645 (Decimal::ZERO, first_z, Decimal::ONE)
1647 } else {
1648 let regression = || -> Result<(Decimal, Decimal, Decimal), DecimalError> {
1649 let mut sum_xz = Decimal::ZERO;
1650 let mut sum_xx = Decimal::ZERO;
1651 for (x, z) in x_vals.iter().zip(&z_vals) {
1652 sum_xz = d_add(sum_xz, d_mul(*x, *z, op)?, op)?;
1653 sum_xx = d_add(sum_xx, d_mul(*x, *x, op)?, op)?;
1654 }
1655
1656 let numerator = d_sub(d_mul(n, sum_xz, op)?, d_mul(sum_x, sum_z, op)?, op)?;
1657 let denominator = d_sub(d_mul(n, sum_xx, op)?, d_mul(sum_x, sum_x, op)?, op)?;
1658 let slope = d_div(numerator, denominator, op)?;
1659 let intercept = d_div(d_sub(sum_z, d_mul(slope, sum_x, op)?, op)?, n, op)?;
1660
1661 let mean_z = d_div(sum_z, n, op)?;
1663 let sst = central_moment(&z_vals, mean_z, 2, op)?;
1664
1665 let mut ssr = Decimal::ZERO;
1666 for (z, x) in z_vals.iter().zip(&x_vals) {
1667 let z_predicted = d_add(d_mul(slope, *x, op)?, intercept, op)?;
1668 let residual = d_sub(*z, z_predicted, op)?;
1669 ssr = d_add(ssr, powu_checked(residual, 2, op)?, op)?;
1670 }
1671
1672 let r_squared = if sst == Decimal::ZERO {
1673 Decimal::ONE
1674 } else {
1675 d_sub(Decimal::ONE, d_div(ssr, sst, op)?, op)?
1676 };
1677
1678 Ok((slope, intercept, r_squared))
1679 };
1680
1681 regression().map_err(trend_err)?
1685 };
1686
1687 let window_sizes = [3, 5, 7];
1689 let mut moving_average: Vec<Point2D> = Vec::new();
1690 for window in window_sizes {
1691 if window > points.len() {
1692 continue;
1693 }
1694 for window_points in points.windows(window) {
1695 let xs: Vec<Decimal> = window_points.iter().map(|p| p.x).collect();
1696 let ys: Vec<Decimal> = window_points.iter().map(|p| p.y).collect();
1697 let avg_x = mean_of(&xs, op).map_err(trend_err)?;
1698 let avg_y = mean_of(&ys, op).map_err(trend_err)?;
1699 moving_average.push(Point2D::new(avg_x, avg_y));
1700 }
1701 }
1702
1703 Ok(TrendMetrics {
1704 slope,
1705 intercept,
1706 r_squared,
1707 moving_average,
1708 })
1709 }
1710
1711 fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
1712 let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
1713
1714 if z_values.is_empty() {
1717 return Ok(RiskMetrics {
1718 volatility: Decimal::ZERO,
1719 value_at_risk: Decimal::ZERO,
1720 expected_shortfall: Decimal::ZERO,
1721 coefficient_of_variation: Decimal::ZERO,
1722 sharpe_ratio: Decimal::ZERO,
1723 });
1724 }
1725
1726 let op = "Surface::compute_risk_metrics";
1727 let mean = mean_of(&z_values, op).map_err(risk_err)?;
1728 let volatility = population_std_dev(&z_values, mean, op).map_err(risk_err)?;
1732
1733 let z_score = dec!(1.645); let scaled_vol = d_mul(z_score, volatility, op).map_err(risk_err)?;
1739 let var = d_sub(mean, scaled_vol, op).map_err(risk_err)?;
1740
1741 let tail: Vec<Decimal> = z_values.iter().copied().filter(|&x| x < var).collect();
1744 let expected_shortfall = if tail.is_empty() {
1745 Decimal::ZERO
1746 } else {
1747 mean_of(&tail, op).map_err(risk_err)?
1748 };
1749
1750 let coefficient_of_variation = coefficient_of_variation(volatility, mean, op)?;
1753
1754 let sharpe_ratio = if volatility.is_zero() {
1759 Decimal::ZERO
1760 } else {
1761 d_div(mean, volatility, op).map_err(risk_err)?
1762 };
1763
1764 Ok(RiskMetrics {
1765 volatility,
1766 value_at_risk: var,
1767 expected_shortfall,
1768 coefficient_of_variation,
1769 sharpe_ratio,
1770 })
1771 }
1772}
1773
1774impl Arithmetic<Surface> for Surface {
1775 type Error = SurfaceError;
1776
1777 fn merge(surfaces: &[&Surface], operation: MergeOperation) -> Result<Surface, Self::Error> {
1806 if surfaces.is_empty() {
1807 return Err(SurfaceError::invalid_parameters(
1808 "merge_surfaces",
1809 "No surfaces provided for merging",
1810 ));
1811 }
1812
1813 if let [only] = surfaces {
1814 return Ok((*only).clone());
1815 }
1816
1817 let min_x = surfaces
1819 .iter()
1820 .map(|s| s.x_range.0)
1821 .max()
1822 .unwrap_or(Decimal::ZERO);
1823 let max_x = surfaces
1824 .iter()
1825 .map(|s| s.x_range.1)
1826 .min()
1827 .unwrap_or(Decimal::ZERO);
1828 let min_y = surfaces
1829 .iter()
1830 .map(|s| s.y_range.0)
1831 .max()
1832 .unwrap_or(Decimal::ZERO);
1833 let max_y = surfaces
1834 .iter()
1835 .map(|s| s.y_range.1)
1836 .min()
1837 .unwrap_or(Decimal::ZERO);
1838
1839 if min_x >= max_x || min_y >= max_y {
1841 return Err(SurfaceError::invalid_parameters(
1842 "merge_surfaces",
1843 "Surfaces have incompatible ranges",
1844 ));
1845 }
1846
1847 let steps = 50;
1849 let op = "Surface::merge";
1850 let x_span = d_sub(max_x, min_x, op).map_err(construction_err)?;
1851 let x_step = d_div(x_span, Decimal::from(steps), op).map_err(construction_err)?;
1852 let y_span = d_sub(max_y, min_y, op).map_err(construction_err)?;
1853 let y_step = d_div(y_span, Decimal::from(steps), op).map_err(construction_err)?;
1854
1855 let result_points: Result<Vec<Point3D>, SurfaceError> = (0..=steps)
1856 .into_par_iter()
1857 .flat_map(|i| {
1858 (0..=steps).into_par_iter().map(move |j| {
1859 let x_offset = d_mul(x_step, Decimal::from(i), op).map_err(construction_err)?;
1860 let x = d_add(min_x, x_offset, op)
1865 .map_err(construction_err)?
1866 .min(max_x);
1867 let y_offset = d_mul(y_step, Decimal::from(j), op).map_err(construction_err)?;
1868 let y = d_add(min_y, y_offset, op)
1869 .map_err(construction_err)?
1870 .min(max_y);
1871 let point = Point2D::new(x, y);
1872
1873 let z_values: Result<Vec<Decimal>, SurfaceError> = surfaces
1875 .iter()
1876 .map(|surface| {
1877 surface
1878 .interpolate(point, InterpolationType::Cubic)
1879 .map(|point3d| point3d.z)
1880 .map_err(SurfaceError::from)
1881 })
1882 .collect();
1883
1884 let z_values = z_values?;
1885
1886 let result_z = match operation {
1888 MergeOperation::Add => {
1889 d_sum_iter(z_values.iter().copied(), op).map_err(construction_err)?
1890 }
1891 MergeOperation::Subtract => {
1892 let first = z_values.first().cloned().unwrap_or(Decimal::ZERO);
1893 let remaining_sum = d_sum_iter(z_values.iter().skip(1).copied(), op)
1894 .map_err(construction_err)?;
1895 d_sub(first, remaining_sum, op).map_err(construction_err)?
1896 }
1897 MergeOperation::Multiply => d_product_iter(z_values.iter().copied(), op)
1898 .map_err(construction_err)?,
1899 MergeOperation::Divide => {
1900 let mut divisors = z_values.iter().copied();
1908 let first = divisors.next().unwrap_or(Decimal::ONE);
1909 divisors.try_fold(first, |acc, val| {
1910 if val == Decimal::ZERO {
1911 Ok(acc)
1912 } else {
1913 d_div(acc, val, op).map_err(construction_err)
1914 }
1915 })?
1916 }
1917 MergeOperation::Max => z_values
1918 .par_iter()
1919 .cloned()
1920 .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1921 .unwrap_or(Decimal::ZERO),
1922 MergeOperation::Min => z_values
1923 .par_iter()
1924 .cloned()
1925 .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1926 .unwrap_or(Decimal::ZERO),
1927 };
1928
1929 Ok(Point3D::new(x, y, result_z))
1930 })
1931 })
1932 .collect();
1933
1934 let result_points = result_points?;
1935 Ok(Surface::from_vector(result_points))
1936 }
1937
1938 fn merge_with(
1939 &self,
1940 other: &Surface,
1941 operation: MergeOperation,
1942 ) -> Result<Surface, Self::Error> {
1943 Self::merge(&[self, other], operation)
1944 }
1945}
1946
1947impl AxisOperations<Point3D, Point2D> for Surface {
1948 type Error = SurfaceError;
1949
1950 fn contains_point(&self, x: &Point2D) -> bool {
1956 self.points_at(x).next().is_some()
1957 }
1958
1959 fn get_index_values(&self) -> Vec<Point2D> {
1964 self.points.iter().map(|p| Point2D::new(p.x, p.y)).collect()
1965 }
1966
1967 fn get_values(&self, x: Point2D) -> Vec<&Decimal> {
1974 self.points_at(&x).map(|p| &p.z).collect()
1975 }
1976
1977 fn get_closest_point(&self, x: &Point2D) -> Result<&Point3D, Self::Error> {
1978 let mut closest: Option<(&Point3D, Decimal)> = None;
1984 for point in &self.points {
1985 let squared = squared_distance(point, x.x, x.y, "Surface::get_closest_point")
1986 .map_err(analysis_err)?;
1987 match closest {
1990 Some((_, best)) if best <= squared => {}
1991 _ => closest = Some((point, squared)),
1992 }
1993 }
1994
1995 closest
1996 .map(|(point, _)| point)
1997 .ok_or(SurfaceError::Point3DError {
1998 reason: "No points found",
1999 })
2000 }
2001
2002 fn get_point(&self, x: &Point2D) -> Option<&Point3D> {
2009 self.points_at(x).next()
2010 }
2011}
2012
2013impl MergeAxisInterpolate<Point3D, Point2D> for Surface
2014where
2015 Self: Sized,
2016{
2017 fn merge_axis_interpolate(
2027 &self,
2028 other: &Self,
2029 interpolation: InterpolationType,
2030 ) -> Result<(Self, Self), Self::Error> {
2031 let merged_xy_values = self.merge_axis_index(other);
2033
2034 let mut interpolated_self_points = BTreeSet::new();
2035 let mut interpolated_other_points = BTreeSet::new();
2036
2037 for xy in &merged_xy_values {
2038 if self.contains_point(xy) {
2039 let pt = self
2040 .points_at(xy)
2041 .next()
2042 .ok_or_else(|| {
2043 SurfaceError::AnalysisError(format!(
2044 "merge_axis_interpolate: missing self point at ({},{}) despite contains_point()",
2045 xy.x, xy.y
2046 ))
2047 })?;
2048 interpolated_self_points.insert(*pt);
2049 } else {
2050 let interpolated_point = self.interpolate(*xy, interpolation)?;
2051 interpolated_self_points.insert(interpolated_point);
2052 }
2053
2054 if other.contains_point(xy) {
2055 let pt = other
2056 .points_at(xy)
2057 .next()
2058 .ok_or_else(|| {
2059 SurfaceError::AnalysisError(format!(
2060 "merge_axis_interpolate: missing other point at ({},{}) despite contains_point()",
2061 xy.x, xy.y
2062 ))
2063 })?;
2064 interpolated_other_points.insert(*pt);
2065 } else {
2066 let interpolated_point = other.interpolate(*xy, interpolation)?;
2067 interpolated_other_points.insert(interpolated_point);
2068 }
2069 }
2070
2071 Ok((
2072 Surface::new(interpolated_self_points),
2073 Surface::new(interpolated_other_points),
2074 ))
2075 }
2076}
2077
2078impl GeometricTransformations<Point3D> for Surface {
2079 type Error = SurfaceError;
2080
2081 fn translate(&self, deltas: Vec<&Decimal>) -> Result<Self, Self::Error> {
2082 if deltas.len() != 3 {
2083 return Err(SurfaceError::invalid_parameters(
2084 "translate",
2085 "Expected 3 deltas for 3D translation",
2086 ));
2087 }
2088
2089 let (Some(dx), Some(dy), Some(dz)) = (deltas.first(), deltas.get(1), deltas.get(2)) else {
2090 return Err(SurfaceError::invalid_parameters(
2091 "translate",
2092 "Expected 3 deltas for 3D translation",
2093 ));
2094 };
2095
2096 let translated_points = self
2097 .points
2098 .iter()
2099 .map(|point| {
2100 let x = d_add(point.x, **dx, "Surface::translate::x")?;
2101 let y = d_add(point.y, **dy, "Surface::translate::y")?;
2102 let z = d_add(point.z, **dz, "Surface::translate::z")?;
2103 Ok(Point3D::new(x, y, z))
2104 })
2105 .collect::<Result<BTreeSet<Point3D>, DecimalError>>()
2106 .map_err(construction_err)?;
2107
2108 Ok(Surface::new(translated_points))
2109 }
2110
2111 fn scale(&self, factors: Vec<&Decimal>) -> Result<Self, Self::Error> {
2112 if factors.len() != 3 {
2113 return Err(SurfaceError::invalid_parameters(
2114 "scale",
2115 "Expected 3 factors for 3D scaling",
2116 ));
2117 }
2118
2119 let (Some(fx), Some(fy), Some(fz)) = (factors.first(), factors.get(1), factors.get(2))
2120 else {
2121 return Err(SurfaceError::invalid_parameters(
2122 "scale",
2123 "Expected 3 factors for 3D scaling",
2124 ));
2125 };
2126
2127 let scaled_points = self
2128 .points
2129 .iter()
2130 .map(|point| {
2131 let x = d_mul(point.x, **fx, "Surface::scale::x")?;
2132 let y = d_mul(point.y, **fy, "Surface::scale::y")?;
2133 let z = d_mul(point.z, **fz, "Surface::scale::z")?;
2134 Ok(Point3D::new(x, y, z))
2135 })
2136 .collect::<Result<BTreeSet<Point3D>, DecimalError>>()
2137 .map_err(construction_err)?;
2138
2139 Ok(Surface::new(scaled_points))
2140 }
2141
2142 fn intersect_with(&self, other: &Self) -> Result<Vec<Point3D>, Self::Error> {
2143 let mut intersections = Vec::new();
2144 let epsilon = Decimal::new(1, 6); let op = "Surface::intersect_with";
2146
2147 for p1 in self.points.iter() {
2148 for p2 in other.points.iter() {
2149 let dx = d_sub(p1.x, p2.x, op).map_err(analysis_err)?.abs();
2150 if dx >= epsilon {
2151 continue;
2152 }
2153 let dy = d_sub(p1.y, p2.y, op).map_err(analysis_err)?.abs();
2154 if dy >= epsilon {
2155 continue;
2156 }
2157 let dz = d_sub(p1.z, p2.z, op).map_err(analysis_err)?.abs();
2158 if dz < epsilon {
2159 intersections.push(*p1);
2160 }
2161 }
2162 }
2163
2164 Ok(intersections)
2165 }
2166
2167 fn derivative_at(&self, point: &Point3D) -> Result<Vec<Decimal>, Self::Error> {
2168 if self.points.len() < 2 {
2170 return Err(SurfaceError::invalid_parameters(
2171 "derivative_at",
2172 "Surface needs at least 2 points for derivative calculation",
2173 ));
2174 }
2175
2176 let op = "Surface::derivative_at";
2177
2178 if self.points.len() <= 3 {
2180 let p0 = self.point_at(0)?;
2181 let p1 = self.point_at(1)?;
2182
2183 if p0 == p1 {
2185 return Err(SurfaceError::invalid_parameters(
2186 "derivative_at",
2187 "Points are identical, cannot calculate derivatives",
2188 ));
2189 }
2190
2191 let rise = d_sub(p1.z, p0.z, op).map_err(analysis_err)?;
2194 let run_x = d_sub(p1.x, p0.x, op).map_err(analysis_err)?;
2195 let dx = if run_x == Decimal::ZERO {
2196 Decimal::MAX
2197 } else {
2198 d_div(rise, run_x, op).map_err(analysis_err)?
2199 };
2200
2201 let run_y = d_sub(p1.y, p0.y, op).map_err(analysis_err)?;
2202 let dy = if run_y == Decimal::ZERO {
2203 Decimal::MAX
2204 } else {
2205 d_div(rise, run_y, op).map_err(analysis_err)?
2206 };
2207
2208 return Ok(vec![dx, dy]);
2209 }
2210
2211 if !(self.x_range.0..=self.x_range.1).contains(&point.x)
2212 || !(self.y_range.0..=self.y_range.1).contains(&point.y)
2213 {
2214 return Err(SurfaceError::invalid_parameters(
2215 "derivative_at",
2216 "Point is outside the surface's range",
2217 ));
2218 }
2219
2220 let tolerance = dec!(0.5);
2222
2223 let mut x_points: BTreeSet<Point3D> = BTreeSet::new();
2224 let mut y_points: BTreeSet<Point3D> = BTreeSet::new();
2225 for candidate in self.get_points() {
2226 if d_sub(candidate.x, point.x, op).map_err(analysis_err)?.abs() < tolerance {
2227 x_points.insert(*candidate);
2228 }
2229 if d_sub(candidate.y, point.y, op).map_err(analysis_err)?.abs() < tolerance {
2230 y_points.insert(*candidate);
2231 }
2232 }
2233
2234 let x_candidates = if x_points.len() < 2 {
2236 &self.points
2237 } else {
2238 &x_points
2239 };
2240 let y_candidates = if y_points.len() < 2 {
2241 &self.points
2242 } else {
2243 &y_points
2244 };
2245
2246 if x_candidates.len() < 2 || y_candidates.len() < 2 {
2248 return Err(SurfaceError::invalid_parameters(
2249 "derivative_at",
2250 "Could not find suitable points for derivative calculation",
2251 ));
2252 }
2253
2254 let mut x_sorted: Vec<_> = x_candidates.iter().collect();
2256 x_sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
2257
2258 let mut y_sorted: Vec<_> = y_candidates.iter().collect();
2259 y_sorted.sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal));
2260
2261 let missing = || {
2262 SurfaceError::AnalysisError(
2263 "derivative_at: fewer than two candidates after filtering".to_string(),
2264 )
2265 };
2266 let (Some(x0), Some(x1)) = (x_sorted.first(), x_sorted.get(1)) else {
2267 return Err(missing());
2268 };
2269 let (Some(y0), Some(y1)) = (y_sorted.first(), y_sorted.get(1)) else {
2270 return Err(missing());
2271 };
2272
2273 let dx = if x0.x == x1.x {
2275 Decimal::ZERO
2276 } else {
2277 let rise = d_sub(x1.z, x0.z, op).map_err(analysis_err)?;
2278 let run = d_sub(x1.x, x0.x, op).map_err(analysis_err)?;
2279 d_div(rise, run, op).map_err(analysis_err)?
2280 };
2281
2282 let dy = if y0.y == y1.y {
2283 Decimal::ZERO
2284 } else {
2285 let rise = d_sub(y1.z, y0.z, op).map_err(analysis_err)?;
2286 let run = d_sub(y1.y, y0.y, op).map_err(analysis_err)?;
2287 d_div(rise, run, op).map_err(analysis_err)?
2288 };
2289
2290 Ok(vec![dx, dy])
2291 }
2292
2293 fn extrema(&self) -> Result<(Point3D, Point3D), Self::Error> {
2294 if self.points.is_empty() {
2295 return Err(SurfaceError::invalid_parameters(
2296 "extrema",
2297 "Surface has no points",
2298 ));
2299 }
2300
2301 let min_point = self
2302 .points
2303 .iter()
2304 .min_by(|a, b| a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
2305 .cloned()
2306 .ok_or_else(|| {
2307 SurfaceError::AnalysisError("extrema: empty point set in min_by".to_string())
2308 })?;
2309
2310 let max_point = self
2311 .points
2312 .iter()
2313 .max_by(|a, b| a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
2314 .cloned()
2315 .ok_or_else(|| {
2316 SurfaceError::AnalysisError("extrema: empty point set in max_by".to_string())
2317 })?;
2318
2319 Ok((min_point, max_point))
2320 }
2321
2322 fn measure_under(&self, base_value: &Decimal) -> Result<Decimal, Self::Error> {
2323 if self.points.len() < 3 {
2324 return Ok(Decimal::ZERO);
2325 }
2326
2327 let mut volume = Decimal::ZERO;
2329 let points: Vec<_> = self.points.iter().collect();
2330
2331 let op = "Surface::measure_under";
2332
2333 for window in points.windows(3) {
2335 let (Some(p1), Some(p2), Some(p3)) = (window.first(), window.get(1), window.get(2))
2337 else {
2338 return Err(SurfaceError::AnalysisError(
2339 "measure_under: triangle window is shorter than three points".to_string(),
2340 ));
2341 };
2342
2343 let cross_a = d_mul(
2344 d_sub(p2.x, p1.x, op).map_err(analysis_err)?,
2345 d_sub(p3.y, p1.y, op).map_err(analysis_err)?,
2346 op,
2347 )
2348 .map_err(analysis_err)?;
2349 let cross_b = d_mul(
2350 d_sub(p3.x, p1.x, op).map_err(analysis_err)?,
2351 d_sub(p2.y, p1.y, op).map_err(analysis_err)?,
2352 op,
2353 )
2354 .map_err(analysis_err)?;
2355 let cross = d_sub(cross_a, cross_b, op).map_err(analysis_err)?;
2356 let area = d_div(cross.abs(), dec!(2), op).map_err(analysis_err)?;
2357
2358 let heights = d_sum_iter(
2360 [
2361 d_sub(p1.z, *base_value, op).map_err(analysis_err)?,
2362 d_sub(p2.z, *base_value, op).map_err(analysis_err)?,
2363 d_sub(p3.z, *base_value, op).map_err(analysis_err)?,
2364 ],
2365 op,
2366 )
2367 .map_err(analysis_err)?;
2368 let avg_height = d_div(heights, dec!(3), op).map_err(analysis_err)?;
2369
2370 let prism = d_mul(area, avg_height, op).map_err(analysis_err)?;
2371 volume = d_add(volume, prism, op).map_err(analysis_err)?;
2372 }
2373
2374 Ok(volume.abs())
2375 }
2376}
2377
2378#[cfg(test)]
2379mod tests_surface_basic {
2380 use super::*;
2381 use rust_decimal_macros::dec;
2382
2383 fn create_test_points() -> BTreeSet<Point3D> {
2385 BTreeSet::from_iter(vec![
2386 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2387 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
2388 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
2389 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2390 Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
2391 ])
2392 }
2393
2394 #[test]
2395 fn test_surface_new() {
2396 let points = create_test_points();
2397 let surface = Surface::new(points.clone());
2398
2399 assert_eq!(surface.points, points);
2401
2402 assert_eq!(surface.x_range.0, dec!(0.0));
2404 assert_eq!(surface.x_range.1, dec!(1.0));
2405
2406 assert_eq!(surface.y_range.0, dec!(0.0));
2408 assert_eq!(surface.y_range.1, dec!(1.0));
2409 }
2410
2411 #[test]
2422 fn test_project_onto_x_axis_keeps_every_projected_point() {
2423 let points = create_test_points();
2424 let surface = Surface::new(points);
2425 let projection = surface.project_onto(Axis::X);
2426
2427 assert_eq!(
2428 projection,
2429 vec![
2430 Point2D::new(dec!(0.0), dec!(0.0)),
2431 Point2D::new(dec!(0.0), dec!(1.0)),
2432 Point2D::new(dec!(0.5), dec!(1.5)),
2433 Point2D::new(dec!(1.0), dec!(1.0)),
2434 Point2D::new(dec!(1.0), dec!(2.0)),
2435 ]
2436 );
2437 assert_eq!(projection.len(), surface.points.len());
2438
2439 let points = match surface.get_f64_points() {
2440 Ok(points) => points,
2441 Err(e) => panic!("finite coordinates convert: {e:?}"),
2442 };
2443 assert_eq!(points.len(), 5);
2444 assert_eq!(points[0].0, 0.0);
2445 assert_eq!(points[0].1, 0.0);
2446 assert_eq!(points[0].2, 0.0);
2447
2448 let default = Surface::default();
2449 assert_eq!(default.points.len(), 0);
2450 assert_eq!(default.x_range, (Decimal::ZERO, Decimal::ZERO));
2451 assert_eq!(default.y_range, (Decimal::ZERO, Decimal::ZERO));
2452 }
2453
2454 #[test]
2458 fn test_project_onto_y_axis_keeps_every_projected_point() {
2459 let points = create_test_points();
2460 let surface = Surface::new(points);
2461 let projection = surface.project_onto(Axis::Y);
2462
2463 assert_eq!(
2464 projection,
2465 vec![
2466 Point2D::new(dec!(0.0), dec!(0.0)),
2467 Point2D::new(dec!(0.0), dec!(1.0)),
2468 Point2D::new(dec!(0.5), dec!(1.5)),
2469 Point2D::new(dec!(1.0), dec!(1.0)),
2470 Point2D::new(dec!(1.0), dec!(2.0)),
2471 ]
2472 );
2473 assert_eq!(projection.len(), surface.points.len());
2474 }
2475
2476 #[test]
2480 fn test_project_onto_z_axis_keeps_every_projected_point() {
2481 let points = create_test_points();
2482 let surface = Surface::new(points);
2483 let projection = surface.project_onto(Axis::Z);
2484
2485 assert_eq!(
2486 projection,
2487 vec![
2488 Point2D::new(dec!(0.0), dec!(0.0)),
2489 Point2D::new(dec!(0.0), dec!(1.0)),
2490 Point2D::new(dec!(0.5), dec!(0.5)),
2491 Point2D::new(dec!(1.0), dec!(0.0)),
2492 Point2D::new(dec!(1.0), dec!(1.0)),
2493 ]
2494 );
2495 assert_eq!(projection.len(), surface.points.len());
2496 }
2497
2498 #[test]
2503 fn test_project_onto_coincident_images_keeps_both_points() {
2504 let surface = Surface::new(BTreeSet::from_iter(vec![
2507 Point3D::new(dec!(0.0), dec!(0.0), dec!(7.0)),
2508 Point3D::new(dec!(0.0), dec!(1.0), dec!(9.0)),
2509 Point3D::new(dec!(1.0), dec!(0.0), dec!(7.0)),
2510 Point3D::new(dec!(1.0), dec!(1.0), dec!(9.0)),
2511 ]));
2512
2513 let projection = surface.project_onto(Axis::X);
2514
2515 assert_eq!(projection.len(), 4);
2516 assert_eq!(
2517 projection,
2518 vec![
2519 Point2D::new(dec!(0.0), dec!(7.0)),
2520 Point2D::new(dec!(0.0), dec!(7.0)),
2521 Point2D::new(dec!(1.0), dec!(9.0)),
2522 Point2D::new(dec!(1.0), dec!(9.0)),
2523 ]
2524 );
2525
2526 let as_set: BTreeSet<Point2D> = projection.into_iter().collect();
2529 assert_eq!(as_set.len(), 2);
2530 }
2531
2532 #[test]
2533 fn test_one_dimensional_spline_interpolation_basic() {
2534 let surface = Surface::new(create_test_points());
2535
2536 let test_points = vec![
2538 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2539 Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
2540 Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
2541 ];
2542
2543 let test_cases = vec![
2545 (dec!(0.25), dec!(0.5)), (dec!(0.0), dec!(0.0)), (dec!(1.0), dec!(2.0)), (dec!(0.75), dec!(1.5)), ];
2550
2551 for (target, expected) in test_cases {
2552 let result = surface
2553 .one_dimensional_spline_interpolation(&test_points, target, |p| p.x, |p| p.z)
2554 .unwrap();
2555
2556 assert!(
2558 (result - expected).abs() < dec!(0.1),
2559 "Failed for target {target}, expected {expected}, got {result}"
2560 );
2561 }
2562 }
2563
2564 #[test]
2565 fn test_one_dimensional_spline_interpolation_insufficient_points() {
2566 let surface = Surface::new(create_test_points());
2567
2568 let test_points = vec![Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0))];
2570
2571 let result =
2572 surface.one_dimensional_spline_interpolation(&test_points, dec!(0.5), |p| p.x, |p| p.z);
2573
2574 assert!(matches!(
2575 result,
2576 Err(InterpolationError::Spline(msg)) if msg.contains("Insufficient points")
2577 ));
2578 }
2579
2580 #[test]
2581 fn test_one_dimensional_spline_interpolation_out_of_range() {
2582 let surface = Surface::new(create_test_points());
2583
2584 let test_points = vec![
2585 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2586 Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
2587 ];
2588
2589 let out_of_range_cases = vec![
2591 (dec!(-0.5), dec!(0.0)), (dec!(1.5), dec!(2.0)), ];
2594
2595 for (target, expected) in out_of_range_cases {
2596 let result = surface
2597 .one_dimensional_spline_interpolation(&test_points, target, |p| p.x, |p| p.z)
2598 .unwrap();
2599
2600 assert_eq!(result, expected, "Failed for out-of-range target {target}");
2602 }
2603 }
2604}
2605
2606#[cfg(test)]
2607mod tests_surface_geometric_object {
2608 use super::*;
2609 use rust_decimal_macros::dec;
2610 #[derive(Debug, thiserror::Error)]
2613 #[error("{0}")]
2614 struct GeneratorFailure(&'static str);
2615
2616 fn create_test_points() -> Vec<Point3D> {
2618 vec![
2619 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2620 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
2621 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
2622 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2623 ]
2624 }
2625
2626 #[test]
2627 fn test_get_points() {
2628 let points = create_test_points();
2629 let surface = Surface::from_vector(points.clone());
2630
2631 let retrieved_points: Vec<&Point3D> = surface.get_points().into_iter().collect();
2632
2633 assert_eq!(retrieved_points.len(), points.len());
2634 for point in &points {
2635 assert!(retrieved_points.contains(&point));
2636 }
2637 }
2638
2639 #[test]
2640 fn test_from_vector() {
2641 let points = create_test_points();
2642 let surface = Surface::from_vector(points.clone());
2643
2644 assert_eq!(surface.points.len(), points.len());
2645
2646 assert_eq!(surface.x_range.0, dec!(0.0));
2648 assert_eq!(surface.x_range.1, dec!(1.0));
2649 assert_eq!(surface.y_range.0, dec!(0.0));
2650 assert_eq!(surface.y_range.1, dec!(1.0));
2651 }
2652
2653 #[test]
2654 fn test_construct_from_data() {
2655 let points = BTreeSet::from_iter(create_test_points());
2656 let result = Surface::construct(ConstructionMethod::FromData { points });
2657
2658 assert!(result.is_ok());
2659 let surface = result.unwrap();
2660 assert_eq!(surface.points.len(), 4);
2661 }
2662
2663 #[test]
2664 fn test_construct_from_data_empty() {
2665 let points: BTreeSet<Point3D> = BTreeSet::new();
2666 let result = Surface::construct(ConstructionMethod::FromData { points });
2667
2668 assert!(matches!(
2669 result,
2670 Err(SurfaceError::Point3DError { reason: _ })
2671 ));
2672 }
2673
2674 #[test]
2675 fn test_construct_parametric() {
2676 let parametric_func: Box<dyn Fn(Point2D) -> Result<Point3D, SurfaceError> + Send + Sync> =
2678 Box::new(move |t: Point2D| -> Result<Point3D, SurfaceError> {
2679 Ok(Point3D::new(
2680 t.x,
2681 t.y,
2682 t.x * t.y, ))
2684 });
2685
2686 let params = ConstructionParams::D3 {
2687 x_start: dec!(0.0),
2688 x_end: dec!(1.0),
2689 y_start: dec!(0.0),
2690 y_end: dec!(1.0),
2691 x_steps: 2,
2692 y_steps: 2,
2693 };
2694
2695 let result = Surface::construct(ConstructionMethod::Parametric {
2696 f: parametric_func,
2697 params,
2698 });
2699
2700 assert!(result.is_ok());
2701 let surface = result.unwrap();
2702
2703 assert_eq!(surface.points.len(), 9); }
2706
2707 #[test]
2708 fn test_construct_parametric_invalid_params() {
2709 let parametric_func: Box<dyn Fn(Point2D) -> Result<Point3D, SurfaceError> + Send + Sync> =
2710 Box::new(move |_: Point2D| -> Result<Point3D, SurfaceError> {
2711 Ok(Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)))
2712 });
2713
2714 let params = ConstructionParams::D2 {
2716 t_start: Decimal::ZERO,
2717 t_end: Decimal::ONE,
2718 steps: 2,
2719 };
2720
2721 let result = Surface::construct(ConstructionMethod::Parametric {
2722 f: parametric_func,
2723 params,
2724 });
2725
2726 assert!(matches!(result, Err(SurfaceError::ConstructionError(_))));
2727 }
2728
2729 #[test]
2730 fn test_construct_parametric_error_handling() {
2731 let parametric_func: Box<dyn Fn(Point2D) -> Result<Point3D, SurfaceError> + Send + Sync> =
2733 Box::new(move |t: Point2D| -> Result<Point3D, SurfaceError> {
2734 if t.x > dec!(0.5) && t.y > dec!(0.5) {
2735 Err(SurfaceError::generator(GeneratorFailure("Test error")))
2736 } else {
2737 Ok(Point3D::new(t.x, t.y, t.x * t.y))
2738 }
2739 });
2740
2741 let params = ConstructionParams::D3 {
2742 x_start: dec!(0.0),
2743 x_end: dec!(1.0),
2744 y_start: dec!(0.0),
2745 y_end: dec!(1.0),
2746 x_steps: 2,
2747 y_steps: 2,
2748 };
2749
2750 let result = Surface::construct(ConstructionMethod::Parametric {
2751 f: parametric_func,
2752 params,
2753 });
2754
2755 match result {
2758 Err(SurfaceError::Generator(source)) => {
2759 let cause = source
2760 .downcast_ref::<GeneratorFailure>()
2761 .expect("the generator's own error must survive boxing");
2762 assert!(cause.to_string().contains("Test error"));
2763 }
2764 other => panic!("expected SurfaceError::Generator, got {other:?}"),
2765 }
2766 }
2767
2768 #[test]
2769 fn test_range_calculation() {
2770 let points = create_test_points();
2771 let surface = Surface::from_vector(points);
2772
2773 assert_eq!(surface.x_range.0, dec!(0.0));
2775 assert_eq!(surface.x_range.1, dec!(1.0));
2776 assert_eq!(surface.y_range.0, dec!(0.0));
2777 assert_eq!(surface.y_range.1, dec!(1.0));
2778 }
2779}
2780
2781#[cfg(test)]
2782mod tests_surface_linear_interpolation {
2783 use super::*;
2784 use rust_decimal_macros::dec;
2785
2786 fn create_test_surface() -> Surface {
2787 let points = BTreeSet::from_iter(vec![
2788 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2789 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
2790 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
2791 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2792 ]);
2793 Surface::new(points)
2794 }
2795
2796 #[test]
2797 fn test_point_out_of_range() {
2798 let surface = create_test_surface();
2799 let result = surface.linear_interpolate(Point2D::new(dec!(-1.0), dec!(0.5)));
2800 assert!(matches!(
2801 result,
2802 Err(InterpolationError::Linear(msg)) if msg.contains("outside the surface's range")
2803 ));
2804 }
2805
2806 #[test]
2807 fn test_exact_point_match() {
2808 let surface = create_test_surface();
2809 let result = surface
2810 .linear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
2811 .unwrap();
2812 assert_eq!(result.z, dec!(0.0));
2813 }
2814
2815 #[test]
2816 fn test_midpoint_interpolation() {
2817 let surface = create_test_surface();
2818 let result = surface
2819 .linear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
2820 .unwrap();
2821 assert_eq!(result.z, dec!(1.0));
2822 }
2823
2824 #[test]
2825 fn test_quarter_point_interpolation() {
2826 let surface = create_test_surface();
2827 let result = surface
2828 .linear_interpolate(Point2D::new(dec!(0.25), dec!(0.25)))
2829 .unwrap();
2830 assert!(result.z > dec!(0.0) && result.z < dec!(1.0));
2832 }
2833
2834 #[test]
2835 fn test_degenerate_triangle() {
2836 let points = BTreeSet::from_iter(vec![
2837 Point3D::new(dec!(1.0), dec!(1.0), dec!(0.0)),
2838 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
2839 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2840 ]);
2841 let surface = Surface::new(points);
2842 let result = surface.linear_interpolate(Point2D::new(dec!(1.0), dec!(1.0)));
2843 assert!(matches!(
2844 result,
2845 Err(InterpolationError::Linear(msg)) if msg.contains("Degenerate triangle")
2846 ));
2847 }
2848
2849 #[test]
2850 fn test_boundary_interpolation() {
2851 let surface = create_test_surface();
2852 let result = surface
2854 .linear_interpolate(Point2D::new(dec!(0.0), dec!(0.5)))
2855 .unwrap();
2856 assert_eq!(result.z, dec!(0.5));
2857 }
2858
2859 #[test]
2860 fn test_uniform_gradient() {
2861 let points = BTreeSet::from_iter(vec![
2863 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2864 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
2865 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
2866 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2867 ]);
2868 let surface = Surface::new(points);
2869
2870 let result = surface
2872 .linear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
2873 .unwrap();
2874 assert_eq!(result.z, dec!(1.0));
2875 }
2876
2877 #[test]
2878 fn test_interpolation_precision() {
2879 let surface = create_test_surface();
2880 let result = surface
2881 .linear_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
2882 .unwrap();
2883 assert!(result.z >= dec!(0.0) && result.z <= dec!(2.0));
2885 }
2886}
2887
2888#[cfg(test)]
2889mod tests_surface_bilinear_interpolation {
2890 use super::*;
2891 use rust_decimal_macros::dec;
2892
2893 fn create_test_surface() -> Surface {
2894 let points = BTreeSet::from_iter(vec![
2895 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)), Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)), Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)), Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)), ]);
2900 Surface::new(points)
2901 }
2902
2903 #[test]
2904 fn test_insufficient_points() {
2905 let points = BTreeSet::from_iter(vec![
2906 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2907 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
2908 Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
2909 ]);
2910 let surface = Surface::new(points);
2911 let result = surface.bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
2912 assert!(matches!(
2913 result,
2914 Err(InterpolationError::Bilinear(msg)) if msg.contains("Need at least four points")
2915 ));
2916 }
2917
2918 #[test]
2919 fn test_point_out_of_range() {
2920 let surface = create_test_surface();
2921 let result = surface.bilinear_interpolate(Point2D::new(dec!(-1.0), dec!(0.5)));
2922 assert!(matches!(
2923 result,
2924 Err(InterpolationError::Bilinear(msg)) if msg.contains("outside the surface's range")
2925 ));
2926 }
2927
2928 #[test]
2929 fn test_exact_point_match() {
2930 let surface = create_test_surface();
2931 let result = surface
2932 .bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
2933 .unwrap();
2934 assert_eq!(result.z, dec!(0.0));
2935 }
2936
2937 #[test]
2938 fn test_midpoint_interpolation() {
2939 let surface = create_test_surface();
2940 let result = surface
2941 .bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
2942 .unwrap();
2943 assert_eq!(result.z, dec!(1.0));
2945 }
2946
2947 #[test]
2948 fn test_quarter_point_interpolation() {
2949 let surface = create_test_surface();
2950 let result = surface
2951 .bilinear_interpolate(Point2D::new(dec!(0.25), dec!(0.25)))
2952 .unwrap();
2953 assert!(result.z > dec!(0.0) && result.z < dec!(1.0));
2955 }
2956
2957 #[test]
2973 fn test_invalid_quadrilateral() {
2974 let points = BTreeSet::from_iter(vec![
2975 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2976 Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
2977 Point3D::new(dec!(0.0), dec!(0.0), dec!(2.0)),
2978 Point3D::new(dec!(0.0), dec!(0.0), dec!(3.0)),
2979 ]);
2980 assert_eq!(points.len(), 4, "all four heights are distinct points");
2981
2982 let surface = Surface::new(points);
2983 let result = surface.bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)));
2984 assert!(result.is_err());
2985 assert!(matches!(
2986 result,
2987 Err(InterpolationError::Bilinear(msg)) if msg.contains("Invalid quadrilateral")
2988 ));
2989 }
2990
2991 #[test]
2992 fn test_boundary_interpolation() {
2993 let surface = create_test_surface();
2994 let result = surface
2996 .bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.5)))
2997 .unwrap();
2998 assert_eq!(result.z, dec!(0.5));
2999 }
3000
3001 #[test]
3002 fn test_uniform_gradient() {
3003 let points = BTreeSet::from_iter(vec![
3005 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3006 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
3007 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
3008 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3009 ]);
3010 let surface = Surface::new(points);
3011 let result = surface
3012 .bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
3013 .unwrap();
3014 assert_eq!(result.z, dec!(1.0));
3015 }
3016
3017 #[test]
3018 fn test_interpolation_precision() {
3019 let surface = create_test_surface();
3020 let result = surface
3021 .bilinear_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
3022 .unwrap();
3023 assert!(result.z >= dec!(0.0) && result.z <= dec!(2.0));
3025 }
3026
3027 #[test]
3028 fn test_corners_interpolation() {
3029 let surface = create_test_surface();
3030
3031 let bl = surface
3033 .bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
3034 .unwrap();
3035 let br = surface
3036 .bilinear_interpolate(Point2D::new(dec!(1.0), dec!(0.0)))
3037 .unwrap();
3038 let tl = surface
3039 .bilinear_interpolate(Point2D::new(dec!(0.0), dec!(1.0)))
3040 .unwrap();
3041 let tr = surface
3042 .bilinear_interpolate(Point2D::new(dec!(1.0), dec!(1.0)))
3043 .unwrap();
3044
3045 assert_eq!(bl.z, dec!(0.0));
3046 assert_eq!(br.z, dec!(1.0));
3047 assert_eq!(tl.z, dec!(1.0));
3048 assert_eq!(tr.z, dec!(2.0));
3049 }
3050}
3051
3052#[cfg(test)]
3053mod tests_surface_cubic_interpolation {
3054 use super::*;
3055 use rust_decimal_macros::dec;
3056
3057 fn create_test_surface() -> Surface {
3059 let points = BTreeSet::from_iter(vec![
3060 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3061 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
3062 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
3063 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3064 Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
3065 Point3D::new(dec!(0.2), dec!(0.8), dec!(0.7)),
3066 Point3D::new(dec!(0.8), dec!(0.2), dec!(0.7)),
3067 Point3D::new(dec!(0.3), dec!(0.3), dec!(0.3)),
3068 Point3D::new(dec!(0.7), dec!(0.7), dec!(1.7)),
3069 ]);
3070 Surface::new(points)
3071 }
3072
3073 #[test]
3074 fn test_insufficient_points() {
3075 let points = BTreeSet::from_iter(vec![
3076 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3077 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3078 Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
3079 ]);
3080 let surface = Surface::new(points);
3081 let result = surface.cubic_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
3082
3083 assert!(matches!(
3084 result,
3085 Err(InterpolationError::Cubic(msg)) if msg.contains("Need at least nine points")
3086 ));
3087 }
3088
3089 #[test]
3090 fn test_point_out_of_range() {
3091 let surface = create_test_surface();
3092 let result = surface.cubic_interpolate(Point2D::new(dec!(2.0), dec!(2.0)));
3093
3094 assert!(matches!(
3095 result,
3096 Err(InterpolationError::Cubic(msg)) if msg.contains("outside the surface's range")
3097 ));
3098 }
3099
3100 #[test]
3101 fn test_exact_point_match() {
3102 let surface = create_test_surface();
3103 let result = surface
3104 .cubic_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
3105 .unwrap();
3106
3107 assert_eq!(result.z, dec!(1.5));
3108 }
3109
3110 #[test]
3111 fn test_midpoint_interpolation() {
3112 let surface = create_test_surface();
3113 let result = surface
3114 .cubic_interpolate(Point2D::new(dec!(0.4), dec!(0.4)))
3115 .unwrap();
3116
3117 assert!(result.z > dec!(0.3) && result.z < dec!(1.5));
3119 }
3120
3121 #[test]
3122 fn test_interpolation_consistency() {
3123 let surface = create_test_surface();
3124
3125 let test_points = vec![
3127 Point2D::new(dec!(0.2), dec!(0.2)),
3128 Point2D::new(dec!(0.6), dec!(0.6)),
3129 Point2D::new(dec!(0.8), dec!(0.3)),
3130 ];
3131
3132 for point in test_points {
3133 let result = surface.cubic_interpolate(point).unwrap();
3134
3135 assert!(
3137 result.z >= dec!(0.0) && result.z <= dec!(2.0),
3138 "Failed for point {point:?}"
3139 );
3140
3141 assert_eq!(result.x, point.x);
3143 assert_eq!(result.y, point.y);
3144 }
3145 }
3146
3147 #[test]
3148 fn test_boundary_interpolation() {
3149 let surface = create_test_surface();
3150
3151 let boundary_points = vec![
3153 Point2D::new(dec!(0.0), dec!(0.5)),
3154 Point2D::new(dec!(0.5), dec!(0.0)),
3155 Point2D::new(dec!(1.0), dec!(0.5)),
3156 Point2D::new(dec!(0.5), dec!(1.0)),
3157 ];
3158
3159 for point in boundary_points {
3160 let result = surface.cubic_interpolate(point).unwrap();
3161
3162 assert!(
3164 result.z > dec!(0.0) && result.z < dec!(2.0),
3165 "Failed for boundary point {point:?}"
3166 );
3167 }
3168 }
3169
3170 #[test]
3171 fn test_interpolation_precision() {
3172 let surface = create_test_surface();
3173 let result = surface
3174 .cubic_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
3175 .unwrap();
3176
3177 assert!(result.z > dec!(0.0) && result.z < dec!(2.0));
3179 }
3180
3181 #[test]
3182 fn test_repeated_interpolation() {
3183 let surface = create_test_surface();
3184
3185 let point = Point2D::new(dec!(0.4), dec!(0.4));
3187 let results: Vec<Decimal> = (0..5)
3188 .map(|_| surface.cubic_interpolate(point).unwrap().z)
3189 .collect();
3190
3191 let max_diff = results
3193 .iter()
3194 .max_by(|a, b| a.partial_cmp(b).unwrap())
3195 .unwrap()
3196 - results
3197 .iter()
3198 .min_by(|a, b| a.partial_cmp(b).unwrap())
3199 .unwrap();
3200
3201 assert!(
3202 max_diff < dec!(0.001),
3203 "Interpolation results should be consistent"
3204 );
3205 }
3206
3207 #[test]
3208 fn test_extreme_point_locations() {
3209 let surface = create_test_surface();
3210
3211 let extreme_points = vec![
3213 Point2D::new(dec!(0.001), dec!(0.001)),
3214 Point2D::new(dec!(0.999), dec!(0.999)),
3215 ];
3216
3217 for point in extreme_points {
3218 let result = surface.cubic_interpolate(point).unwrap();
3219
3220 assert!(
3222 result.z >= dec!(0.0) && result.z <= dec!(2.0),
3223 "Failed for extreme point {point:?}"
3224 );
3225 }
3226 }
3227}
3228
3229#[cfg(test)]
3230mod tests_surface_spline_interpolation {
3231 use super::*;
3232 use rust_decimal_macros::dec;
3233
3234 fn create_test_surface() -> Surface {
3236 let points = BTreeSet::from_iter(vec![
3237 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3238 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
3239 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
3240 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3241 Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
3242 Point3D::new(dec!(0.2), dec!(0.8), dec!(0.7)),
3243 Point3D::new(dec!(0.8), dec!(0.2), dec!(0.7)),
3244 Point3D::new(dec!(0.3), dec!(0.3), dec!(0.3)),
3245 Point3D::new(dec!(0.7), dec!(0.7), dec!(1.7)),
3246 Point3D::new(dec!(0.4), dec!(0.6), dec!(1.1)),
3247 Point3D::new(dec!(0.6), dec!(0.4), dec!(1.2)),
3248 ]);
3249 Surface::new(points)
3250 }
3251
3252 #[test]
3253 fn test_insufficient_points() {
3254 let points = BTreeSet::from_iter(vec![
3255 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3256 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3257 ]);
3258 let surface = Surface::new(points);
3259 let result = surface.spline_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
3260
3261 assert!(matches!(
3262 result,
3263 Err(InterpolationError::Spline(msg)) if msg.contains("Need at least nine points")
3264 ));
3265 }
3266
3267 #[test]
3268 fn test_point_out_of_range() {
3269 let surface = create_test_surface();
3270 let result = surface.spline_interpolate(Point2D::new(dec!(2.0), dec!(2.0)));
3271
3272 assert!(matches!(
3273 result,
3274 Err(InterpolationError::Spline(msg)) if msg.contains("outside the surface's range")
3275 ));
3276 }
3277
3278 #[test]
3279 fn test_exact_point_match() {
3280 let surface = create_test_surface();
3281 let result = surface
3282 .spline_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
3283 .unwrap();
3284
3285 assert_eq!(result.z, dec!(1.5));
3286 }
3287
3288 #[test]
3289 fn test_midpoint_interpolation() {
3290 let surface = create_test_surface();
3291 let result = surface
3292 .spline_interpolate(Point2D::new(dec!(0.4), dec!(0.4)))
3293 .unwrap();
3294
3295 assert!(result.z > dec!(0.3) && result.z < dec!(1.5));
3297 }
3298
3299 #[test]
3300 fn test_interpolation_consistency() {
3301 let surface = create_test_surface();
3302
3303 let test_points = vec![
3305 Point2D::new(dec!(0.2), dec!(0.2)),
3306 Point2D::new(dec!(0.6), dec!(0.6)),
3307 Point2D::new(dec!(0.8), dec!(0.3)),
3308 ];
3309
3310 for point in test_points {
3311 let result = surface.spline_interpolate(point).unwrap();
3312
3313 assert!(
3315 result.z >= dec!(0.0) && result.z <= dec!(2.0),
3316 "Failed for point {point:?}"
3317 );
3318
3319 assert_eq!(result.x, point.x);
3321 assert_eq!(result.y, point.y);
3322 }
3323 }
3324
3325 #[test]
3326 fn test_boundary_interpolation() {
3327 let surface = create_test_surface();
3328
3329 let boundary_points = vec![
3331 Point2D::new(dec!(0.0), dec!(0.5)),
3332 Point2D::new(dec!(0.5), dec!(0.0)),
3333 Point2D::new(dec!(1.0), dec!(0.5)),
3334 Point2D::new(dec!(0.5), dec!(1.0)),
3335 ];
3336
3337 for point in boundary_points {
3338 let result = surface.spline_interpolate(point).unwrap();
3339
3340 assert!(
3342 result.z > dec!(0.0) && result.z < dec!(2.0),
3343 "Failed for boundary point {point:?}"
3344 );
3345 }
3346 }
3347
3348 #[test]
3349 fn test_interpolation_precision() {
3350 let surface = create_test_surface();
3351 let result = surface
3352 .spline_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
3353 .unwrap();
3354
3355 assert!(result.z > dec!(0.0) && result.z < dec!(2.0));
3357 }
3358
3359 #[test]
3360 fn test_repeated_interpolation() {
3361 let surface = create_test_surface();
3362
3363 let point = Point2D::new(dec!(0.4), dec!(0.4));
3365 let results: Vec<Decimal> = (0..5)
3366 .map(|_| surface.spline_interpolate(point).unwrap().z)
3367 .collect();
3368
3369 let max_diff = results
3371 .iter()
3372 .max_by(|a, b| a.partial_cmp(b).unwrap())
3373 .unwrap()
3374 - results
3375 .iter()
3376 .min_by(|a, b| a.partial_cmp(b).unwrap())
3377 .unwrap();
3378
3379 assert!(
3380 max_diff < dec!(0.001),
3381 "Interpolation results should be consistent"
3382 );
3383 }
3384
3385 #[test]
3386 fn test_extreme_point_locations() {
3387 let surface = create_test_surface();
3388
3389 let extreme_points = vec![
3391 Point2D::new(dec!(0.001), dec!(0.001)),
3392 Point2D::new(dec!(0.999), dec!(0.999)),
3393 ];
3394
3395 for point in extreme_points {
3396 let result = surface.spline_interpolate(point).unwrap();
3397
3398 assert!(
3400 result.z >= dec!(0.0) && result.z <= dec!(2.0),
3401 "Failed for extreme point {point:?}"
3402 );
3403 }
3404 }
3405
3406 #[test]
3407 fn test_one_dimensional_spline_interpolation() {
3408 let surface = create_test_surface();
3409
3410 let points = vec![
3412 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3413 Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
3414 Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
3415 ];
3416
3417 let test_points = vec![
3419 (dec!(0.25), dec!(0.5)), (dec!(0.0), dec!(0.0)), (dec!(1.0), dec!(2.0)), (dec!(0.75), dec!(1.5)), ];
3424
3425 for (target, expected) in test_points {
3426 let result = surface
3427 .one_dimensional_spline_interpolation(&points, target, |p| p.x, |p| p.z)
3428 .unwrap();
3429
3430 assert!(
3432 (result - expected).abs() < dec!(0.1),
3433 "Failed for target {target}, expected {expected}, got {result}"
3434 );
3435 }
3436 }
3437
3438 #[test]
3439 fn test_interpolation_edge_cases() {
3440 let surface = create_test_surface();
3441
3442 let edge_points = vec![
3444 Point2D::new(dec!(0.001), dec!(0.001)),
3445 Point2D::new(dec!(0.999), dec!(0.999)),
3446 Point2D::new(dec!(0.5), dec!(0.5)),
3447 ];
3448
3449 for point in edge_points {
3450 let result = surface.spline_interpolate(point);
3451 assert!(result.is_ok(), "Failed for point {point:?}");
3452
3453 let interpolated_point = result.unwrap();
3454 assert_eq!(interpolated_point.x, point.x);
3455 assert_eq!(interpolated_point.y, point.y);
3456 }
3457 }
3458}
3459
3460#[cfg(test)]
3461mod tests_surface_arithmetic {
3462 use super::*;
3463 use optionstratlib_core::error::OperationErrorKind;
3464 use rust_decimal_macros::dec;
3465
3466 fn create_test_surface() -> Surface {
3467 let points = BTreeSet::from_iter(vec![
3468 Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
3469 Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
3470 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
3471 Point3D::new(dec!(0.0), dec!(0.5), dec!(1.0)),
3472 Point3D::new(dec!(0.5), dec!(0.5), dec!(1.0)),
3473 Point3D::new(dec!(1.0), dec!(0.5), dec!(1.0)),
3474 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
3475 Point3D::new(dec!(0.5), dec!(1.0), dec!(1.0)),
3476 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3477 ]);
3478 Surface::new(points)
3479 }
3480
3481 const LONG_MANTISSA_MAX: Decimal = dec!(10.000000000000000000000000018);
3486
3487 fn long_mantissa_surface() -> Surface {
3490 let axis = [dec!(0), dec!(2.5), dec!(5), dec!(7.5), LONG_MANTISSA_MAX];
3491 let points = axis
3492 .iter()
3493 .flat_map(|&x| axis.iter().map(move |&y| Point3D::new(x, y, x + y)))
3494 .collect();
3495 Surface::new(points)
3496 }
3497
3498 #[test]
3499 fn test_merge_long_mantissa_ranges_end_on_the_maximum() {
3500 let surface = long_mantissa_surface();
3501 let max = LONG_MANTISSA_MAX;
3502
3503 let result = surface.merge_with(&surface, MergeOperation::Add).unwrap();
3504
3505 assert_eq!(result.points.len(), 51 * 51);
3507 assert!(result.points.iter().all(|p| p.x <= max && p.y <= max));
3508 assert_eq!(result.x_range, (Decimal::ZERO, max));
3509 assert_eq!(result.y_range, (Decimal::ZERO, max));
3510 }
3511
3512 #[test]
3513 fn test_merge_empty_surfaces() {
3514 let result = Surface::merge(&[], MergeOperation::Add);
3515 assert!(matches!(
3516 result,
3517 Err(SurfaceError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
3518 if operation == "merge_surfaces" && reason.contains("No surfaces")
3519 ));
3520 }
3521
3522 #[test]
3523 fn test_merge_single_surface() {
3524 let surface = create_test_surface();
3525 let result = Surface::merge(&[&surface], MergeOperation::Add).unwrap();
3526 assert_eq!(result.points.len(), surface.points.len());
3527 }
3528
3529 #[test]
3530 fn test_merge_add() {
3531 let surface1 = create_test_surface();
3532 let surface2 = create_test_surface();
3533 let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Add).unwrap();
3534
3535 let mid_point = result
3536 .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3537 .unwrap();
3538 assert_eq!(mid_point.z, dec!(2.0));
3539 }
3540
3541 #[test]
3542 fn test_merge_subtract() {
3543 let surface1 = create_test_surface();
3544 let surface2 = create_test_surface();
3545 let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Subtract).unwrap();
3546
3547 let mid_point = result
3549 .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3550 .unwrap();
3551 assert_eq!(mid_point.z, dec!(0.0));
3552 }
3553
3554 fn constant_surface(height: Decimal) -> Surface {
3555 let points =
3556 BTreeSet::from_iter([dec!(0.0), dec!(0.5), dec!(1.0)].into_iter().flat_map(|x| {
3557 [dec!(0.0), dec!(0.5), dec!(1.0)]
3558 .into_iter()
3559 .map(move |y| Point3D::new(x, y, height))
3560 }));
3561 Surface::new(points)
3562 }
3563
3564 #[test]
3569 fn test_merge_divide_folds_every_divisor() {
3570 let eight = constant_surface(dec!(8));
3571 let two_a = constant_surface(dec!(2));
3572 let two_b = constant_surface(dec!(2));
3573
3574 let result = Surface::merge(&[&eight, &two_a, &two_b], MergeOperation::Divide).unwrap();
3575
3576 let mid_point = result
3577 .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3578 .unwrap();
3579 assert!(
3580 (mid_point.z - dec!(2)).abs() < dec!(0.0001),
3581 "expected 8 / 2 / 2 = 2, got {}",
3582 mid_point.z
3583 );
3584 }
3585
3586 const DETERMINISM_REPEATS: usize = 24;
3595
3596 fn height_fingerprint(surface: &Surface) -> Vec<(i128, u32)> {
3602 surface
3603 .points
3604 .iter()
3605 .map(|point| (point.z.mantissa(), point.z.scale()))
3606 .collect()
3607 }
3608
3609 #[test]
3618 fn test_merge_surfaces_multiply_is_reproducible() {
3619 let a = constant_surface(dec!(0.010989010989010989010989011));
3620 let b = constant_surface(dec!(0.010752688172043010752688172));
3621 let c = constant_surface(dec!(0.0094339622641509433962264151));
3622
3623 let expected = dec!(0.0000011147302687168785768908);
3624 let first = Surface::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
3625 for point in first.points.iter() {
3626 assert_eq!(
3627 (point.z.mantissa(), point.z.scale()),
3628 (expected.mantissa(), expected.scale()),
3629 "expected the left-to-right product {expected} at ({}, {}), got {}",
3630 point.x,
3631 point.y,
3632 point.z
3633 );
3634 }
3635
3636 let reference = height_fingerprint(&first);
3637 for run in 1..DETERMINISM_REPEATS {
3638 let again = Surface::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
3639 assert_eq!(
3640 height_fingerprint(&again),
3641 reference,
3642 "run {run} of {DETERMINISM_REPEATS} produced different digits from the first"
3643 );
3644 }
3645 }
3646
3647 #[test]
3653 fn test_merge_divide_reports_overflow_instead_of_dropping_it() {
3654 let tiny = constant_surface(Decimal::new(1, 20));
3655 let big = constant_surface(dec!(100000000000000000000));
3656
3657 let result = Surface::merge(&[&big, &tiny], MergeOperation::Divide);
3658 assert!(
3659 matches!(result, Err(SurfaceError::ConstructionError(_))),
3660 "a quotient outside the Decimal range must be reported, got {result:?}"
3661 );
3662 }
3663
3664 #[test]
3665 fn test_incompatible_ranges() {
3666 let surface1 = Surface::new(BTreeSet::from_iter(vec![
3667 Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
3668 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3669 ]));
3670
3671 let surface2 = Surface::new(BTreeSet::from_iter(vec![
3672 Point3D::new(dec!(2.0), dec!(2.0), dec!(1.0)),
3673 Point3D::new(dec!(3.0), dec!(3.0), dec!(1.0)),
3674 ]));
3675
3676 let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Add);
3677 assert!(matches!(
3678 result,
3679 Err(SurfaceError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
3680 if operation == "merge_surfaces" && reason.contains("incompatible ranges")
3681 ));
3682 }
3683
3684 #[test]
3685 fn test_merge_with() {
3686 let surface1 = create_test_surface();
3687 let surface2 = create_test_surface();
3688
3689 let result1 = surface1.merge_with(&surface2, MergeOperation::Add).unwrap();
3690 let result2 = Surface::merge(&[&surface1, &surface2], MergeOperation::Add).unwrap();
3691
3692 assert_eq!(result1.points.len(), result2.points.len());
3693
3694 let test_point = Point2D::new(dec!(0.5), dec!(0.5));
3696 let z1 = result1
3697 .interpolate(test_point, InterpolationType::Cubic)
3698 .unwrap();
3699 let z2 = result2
3700 .interpolate(test_point, InterpolationType::Cubic)
3701 .unwrap();
3702 assert_eq!(z1.z, z2.z);
3703 }
3704
3705 #[test]
3706 fn test_merge_multiply() {
3707 let surface1 = create_test_surface();
3708 let surface2 = create_test_surface();
3709 let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Multiply).unwrap();
3710
3711 let mid_point = result
3712 .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3713 .unwrap();
3714 assert_eq!(mid_point.z, dec!(1.0)); }
3716
3717 #[test]
3718 fn test_merge_divide() {
3719 let surface1 = create_test_surface();
3720 let surface2 = create_test_surface();
3721 let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Divide).unwrap();
3722
3723 let mid_point = result
3724 .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3725 .unwrap();
3726 assert_eq!(mid_point.z, dec!(1.0)); }
3728
3729 #[test]
3730 fn test_merge_max() {
3731 let surface1 = create_test_surface(); let points2 = BTreeSet::from_iter(vec![
3735 Point3D::new(dec!(0.0), dec!(0.0), dec!(2.0)),
3736 Point3D::new(dec!(0.5), dec!(0.0), dec!(2.0)),
3737 Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
3738 Point3D::new(dec!(0.0), dec!(0.5), dec!(2.0)),
3739 Point3D::new(dec!(0.5), dec!(0.5), dec!(2.0)),
3740 Point3D::new(dec!(1.0), dec!(0.5), dec!(2.0)),
3741 Point3D::new(dec!(0.0), dec!(1.0), dec!(2.0)),
3742 Point3D::new(dec!(0.5), dec!(1.0), dec!(2.0)),
3743 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3744 ]);
3745 let surface2 = Surface::new(points2);
3746
3747 let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Max).unwrap();
3748
3749 let mid_point = result
3750 .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3751 .unwrap();
3752 assert_eq!(mid_point.z, dec!(2.0));
3753 }
3754
3755 #[test]
3756 fn test_merge_min() {
3757 let surface1 = create_test_surface();
3758 let mut surface2 = create_test_surface();
3759
3760 surface2
3762 .points
3763 .insert(Point3D::new(dec!(0.5), dec!(0.5), dec!(0.5)));
3764
3765 let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Min).unwrap();
3766
3767 let mid_point = result
3768 .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3769 .unwrap();
3770 assert_eq!(mid_point.z, dec!(0.5));
3771 }
3772}
3773
3774#[cfg(test)]
3775mod tests_metrics {
3776 use super::*;
3777 use rust_decimal_macros::dec;
3778
3779 fn create_test_surface() -> Surface {
3780 let points = BTreeSet::from_iter(vec![
3781 Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
3782 Point3D::new(dec!(0.5), dec!(0.0), dec!(2.0)),
3783 Point3D::new(dec!(1.0), dec!(0.0), dec!(3.0)),
3784 Point3D::new(dec!(0.0), dec!(0.5), dec!(2.0)),
3785 Point3D::new(dec!(0.5), dec!(0.5), dec!(3.0)),
3786 Point3D::new(dec!(1.0), dec!(0.5), dec!(4.0)),
3787 Point3D::new(dec!(0.0), dec!(1.0), dec!(3.0)),
3788 Point3D::new(dec!(0.5), dec!(1.0), dec!(4.0)),
3789 Point3D::new(dec!(1.0), dec!(1.0), dec!(5.0)),
3790 ]);
3791 Surface::new(points)
3792 }
3793
3794 #[test]
3795 fn test_basic_metrics() {
3796 let surface = create_test_surface();
3797 let metrics = surface.compute_basic_metrics().unwrap();
3798
3799 assert_eq!(metrics.mean, dec!(3.0));
3800 assert_eq!(metrics.median, dec!(3.0));
3801 assert_eq!(metrics.std_dev, dec!(1.1547005383792515290182975610));
3802 }
3803
3804 #[test]
3805 fn test_shape_metrics() {
3806 let surface = create_test_surface();
3807 let metrics = surface.compute_shape_metrics().unwrap();
3808
3809 assert!(metrics.skewness.abs() < dec!(0.001));
3810 assert!((metrics.kurtosis - dec!(2.25)).abs() < dec!(0.001));
3811 }
3812
3813 #[test]
3814 fn test_range_metrics() {
3815 let surface = create_test_surface();
3816 let metrics = surface.compute_range_metrics().unwrap();
3817
3818 assert_eq!(metrics.min.y, dec!(1.0));
3819 assert_eq!(metrics.max.y, dec!(5.0));
3820 assert_eq!(metrics.range, dec!(4.0));
3821
3822 let (q1, q2, q3) = metrics.quartiles;
3823 assert_eq!(q1, dec!(2.0));
3824 assert_eq!(q2, dec!(3.0));
3825 assert_eq!(q3, dec!(4.0));
3826 assert_eq!(metrics.interquartile_range, dec!(2.0));
3827 }
3828
3829 #[test]
3830 fn test_trend_metrics() {
3831 let surface = create_test_surface();
3832 let metrics = surface.compute_trend_metrics().unwrap();
3833
3834 assert!((metrics.slope - dec!(2.0)).abs() < dec!(0.001));
3836 assert!((metrics.intercept - dec!(2.0)).abs() < dec!(0.001));
3837 }
3838
3839 #[test]
3843 fn test_risk_metrics_flat_surface_keeps_deterministic_var() {
3844 let points = BTreeSet::from_iter((0..3i64).flat_map(|i| {
3845 (0..3i64).map(move |j| Point3D::new(Decimal::from(i), Decimal::from(j), dec!(5)))
3846 }));
3847 let surface = Surface::new(points);
3848 let metrics = surface.compute_risk_metrics().unwrap();
3849
3850 assert_eq!(metrics.volatility, Decimal::ZERO);
3852 assert_eq!(metrics.value_at_risk, dec!(5));
3854 assert_eq!(metrics.expected_shortfall, Decimal::ZERO);
3857 assert_eq!(metrics.coefficient_of_variation, Decimal::ZERO);
3859 assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
3861 }
3862
3863 #[test]
3866 fn test_risk_metrics_flat_negative_surface_keeps_deterministic_var() {
3867 let points = BTreeSet::from_iter((0..3i64).flat_map(|i| {
3868 (0..3i64).map(move |j| Point3D::new(Decimal::from(i), Decimal::from(j), dec!(-2.5)))
3869 }));
3870 let surface = Surface::new(points);
3871 let metrics = surface.compute_risk_metrics().unwrap();
3872
3873 assert_eq!(metrics.volatility, Decimal::ZERO);
3874 assert_eq!(metrics.value_at_risk, dec!(-2.5));
3875 assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
3876 }
3877}
3878
3879#[cfg(test)]
3880mod tests_trend_metrics {
3881 use super::*;
3882 use optionstratlib_core::assert_decimal_eq;
3883 use rust_decimal_macros::dec;
3884
3885 fn create_linear_surface() -> Surface {
3887 let points = BTreeSet::from_iter(vec![
3888 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3889 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3890 Point3D::new(dec!(2.0), dec!(2.0), dec!(4.0)),
3891 Point3D::new(dec!(3.0), dec!(3.0), dec!(6.0)),
3892 Point3D::new(dec!(4.0), dec!(4.0), dec!(8.0)),
3893 ]);
3894 Surface::new(points)
3895 }
3896
3897 fn create_non_linear_surface() -> Surface {
3899 let points = BTreeSet::from_iter(vec![
3900 Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
3901 Point3D::new(dec!(1.0), dec!(1.0), dec!(3.0)),
3902 Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
3903 Point3D::new(dec!(3.0), dec!(3.0), dec!(5.0)),
3904 Point3D::new(dec!(4.0), dec!(4.0), dec!(4.0)),
3905 ]);
3906 Surface::new(points)
3907 }
3908
3909 #[test]
3910 fn test_compute_trend_metrics_linear_surface() {
3911 let surface = create_linear_surface();
3912 let metrics = surface.compute_trend_metrics().unwrap();
3913
3914 assert_decimal_eq!(metrics.slope, dec!(2.0), dec!(0.001));
3916
3917 assert_decimal_eq!(metrics.intercept, dec!(0.0), dec!(0.001));
3919
3920 assert_decimal_eq!(metrics.r_squared, dec!(1.0), dec!(0.001));
3922
3923 assert_eq!(metrics.moving_average.len(), 4);
3925 }
3926
3927 #[test]
3928 fn test_compute_trend_metrics_non_linear_surface() {
3929 let surface = create_non_linear_surface();
3930 let metrics = surface.compute_trend_metrics().unwrap();
3931
3932 assert!(metrics.r_squared < dec!(1.0));
3934
3935 assert!(metrics.slope != dec!(0.0));
3937 assert!(metrics.intercept != dec!(0.0));
3938 }
3939
3940 #[test]
3941 fn test_moving_average_calculation() {
3942 let surface = create_linear_surface();
3943 let metrics = surface.compute_trend_metrics().unwrap();
3944
3945 let window_sizes = [3, 5, 7];
3947
3948 let surface_points_count = surface.points.len();
3950
3951 let expected_total_points = window_sizes
3952 .iter()
3953 .map(|&window| {
3954 if window > surface_points_count {
3956 0
3957 } else {
3958 surface_points_count
3959 .saturating_sub(window)
3960 .saturating_add(1)
3961 }
3962 })
3963 .sum::<usize>();
3964
3965 assert_eq!(
3967 metrics.moving_average.len(),
3968 expected_total_points,
3969 "Mismatch in moving average points calculation"
3970 );
3971
3972 for point in &metrics.moving_average {
3974 assert!(point.x >= dec!(0.0), "x value should be non-negative");
3975 assert!(point.y >= dec!(0.0), "y value should be non-negative");
3976 }
3977 }
3978
3979 #[test]
3980 fn test_edge_cases() {
3981 let single_point_surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
3983 dec!(1.0),
3984 dec!(1.0),
3985 dec!(1.0),
3986 )]));
3987
3988 let metrics = single_point_surface.compute_trend_metrics();
3989 assert!(metrics.is_ok());
3990
3991 let identical_points_surface = Surface::new(BTreeSet::from_iter(vec![
3993 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3994 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3995 Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3996 ]));
3997
3998 let metrics = identical_points_surface.compute_trend_metrics().unwrap();
3999
4000 assert_decimal_eq!(metrics.r_squared, dec!(1.0), dec!(0.001));
4002 assert_decimal_eq!(metrics.slope, dec!(0.0), dec!(0.001));
4003 }
4004}
4005
4006#[cfg(test)]
4007mod tests_axis_operations {
4008 use super::*;
4009 use rust_decimal_macros::dec;
4010
4011 fn create_test_surface() -> Surface {
4013 let points = BTreeSet::from_iter(vec![
4014 Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
4015 Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
4016 Point3D::new(dec!(0.0), dec!(1.0), dec!(3.0)),
4017 Point3D::new(dec!(1.0), dec!(1.0), dec!(4.0)),
4018 ]);
4019 Surface::new(points)
4020 }
4021
4022 #[test]
4023 fn test_contains_point() {
4024 let surface = create_test_surface();
4025 assert!(surface.contains_point(&Point2D::new(dec!(0.0), dec!(0.0))));
4026 assert!(!surface.contains_point(&Point2D::new(dec!(2.0), dec!(2.0))));
4027 }
4028
4029 #[test]
4030 fn test_get_index_values() {
4031 let surface = create_test_surface();
4032 let indexes = surface.get_index_values();
4033 assert_eq!(indexes.len(), 4);
4034 assert!(indexes.contains(&Point2D::new(dec!(0.0), dec!(0.0))));
4035 assert!(indexes.contains(&Point2D::new(dec!(1.0), dec!(1.0))));
4036 }
4037
4038 #[test]
4039 fn test_get_values() {
4040 let surface = create_test_surface();
4041 let values = surface.get_values(Point2D::new(dec!(0.0), dec!(0.0)));
4042 assert_eq!(values.len(), 1);
4043 assert_eq!(*values[0], dec!(1.0));
4044 }
4045
4046 #[test]
4047 fn test_get_closest_point() {
4048 let surface = create_test_surface();
4049 let point = surface
4050 .get_closest_point(&Point2D::new(dec!(0.5), dec!(0.5)))
4051 .unwrap();
4052 assert_eq!(point.x, dec!(0.0));
4053 assert_eq!(point.y, dec!(0.0));
4054 assert_eq!(point.z, dec!(1.0));
4055 }
4056
4057 #[test]
4058 fn test_get_point() {
4059 let surface = create_test_surface();
4060 let point = surface
4061 .get_point(&Point2D::new(dec!(0.0), dec!(0.0)))
4062 .unwrap();
4063 assert_eq!(point.x, dec!(0.0));
4064 assert_eq!(point.y, dec!(0.0));
4065 assert_eq!(point.z, dec!(1.0));
4066
4067 assert!(
4068 surface
4069 .get_point(&Point2D::new(dec!(2.0), dec!(2.0)))
4070 .is_none()
4071 );
4072 }
4073
4074 #[test]
4084 fn test_merge_indexes() {
4085 let surface1 = create_test_surface();
4086 let surface2 = create_test_surface();
4087 let merged = surface1.merge_indexes(surface2.get_index_values());
4088
4089 assert_eq!(merged.len(), 4);
4090 for expected in [
4091 Point2D::new(dec!(0.0), dec!(0.0)),
4092 Point2D::new(dec!(0.0), dec!(1.0)),
4093 Point2D::new(dec!(1.0), dec!(0.0)),
4094 Point2D::new(dec!(1.0), dec!(1.0)),
4095 ] {
4096 assert!(merged.contains(&expected), "missing index {expected}");
4097 }
4098 }
4099}
4100
4101#[cfg(test)]
4102mod tests_surface_geometric_transformations {
4103 use super::*;
4104 use rust_decimal::MathematicalOps;
4105 use rust_decimal_macros::dec;
4106
4107 fn create_test_surface() -> Surface {
4108 Surface::new(BTreeSet::from_iter(vec![
4109 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4110 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4111 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4112 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
4113 ]))
4114 }
4115
4116 mod test_translate {
4117 use super::*;
4118
4119 #[test]
4120 fn test_translate_positive() {
4121 let surface = create_test_surface();
4122 let result = surface
4123 .translate(vec![&dec!(1.0), &dec!(1.0), &dec!(1.0)])
4124 .unwrap();
4125
4126 let translated_points: Vec<_> = result.points.iter().collect();
4127 assert_eq!(translated_points[0].x, dec!(1.0));
4128 assert_eq!(translated_points[0].y, dec!(1.0));
4129 assert_eq!(translated_points[0].z, dec!(1.0));
4130 }
4131
4132 #[test]
4133 fn test_translate_negative() {
4134 let surface = create_test_surface();
4135 let result = surface
4136 .translate(vec![&dec!(-1.0), &dec!(-1.0), &dec!(-1.0)])
4137 .unwrap();
4138
4139 let translated_points: Vec<_> = result.points.iter().collect();
4140 assert_eq!(translated_points[0].x, dec!(-1.0));
4141 assert_eq!(translated_points[0].y, dec!(-1.0));
4142 assert_eq!(translated_points[0].z, dec!(-1.0));
4143 }
4144
4145 #[test]
4146 fn test_translate_zero() {
4147 let surface = create_test_surface();
4148 let result = surface
4149 .translate(vec![&dec!(0.0), &dec!(0.0), &dec!(0.0)])
4150 .unwrap();
4151 assert_eq!(surface.points, result.points);
4152 }
4153
4154 #[test]
4155 fn test_translate_wrong_dimensions() {
4156 let surface = create_test_surface();
4157 let result = surface.translate(vec![&dec!(1.0), &dec!(1.0)]);
4158 assert!(result.is_err());
4159 }
4160
4161 #[test]
4162 fn test_translate_preserves_distances() {
4163 let surface = create_test_surface();
4164 let result = surface
4165 .translate(vec![&dec!(1.0), &dec!(1.0), &dec!(1.0)])
4166 .unwrap();
4167
4168 let original_points: Vec<_> = surface.points.iter().collect();
4169 let translated_points: Vec<_> = result.points.iter().collect();
4170
4171 let orig_dist = ((original_points[1].x - original_points[0].x).powi(2)
4172 + (original_points[1].y - original_points[0].y).powi(2)
4173 + (original_points[1].z - original_points[0].z).powi(2))
4174 .sqrt();
4175
4176 let trans_dist = ((translated_points[1].x - translated_points[0].x).powi(2)
4177 + (translated_points[1].y - translated_points[0].y).powi(2)
4178 + (translated_points[1].z - translated_points[0].z).powi(2))
4179 .sqrt();
4180
4181 assert_eq!(orig_dist, trans_dist);
4182 }
4183 }
4184
4185 mod test_scale {
4186 use super::*;
4187
4188 #[test]
4189 fn test_scale_uniform() {
4190 let surface = create_test_surface();
4191 let result = surface
4192 .scale(vec![&dec!(2.0), &dec!(2.0), &dec!(2.0)])
4193 .unwrap();
4194 assert_eq!(result[1].x, dec!(0.0));
4195 assert_eq!(result[1].y, dec!(2.0));
4196 assert_eq!(result[1].z, dec!(2.0));
4197 }
4198
4199 #[test]
4200 fn test_scale_non_uniform() {
4201 let surface = create_test_surface();
4202 let result = surface
4203 .scale(vec![&dec!(2.0), &dec!(3.0), &dec!(4.0)])
4204 .unwrap();
4205
4206 assert_eq!(result[0].x, dec!(0.0));
4207 assert_eq!(result[0].y, dec!(0.0));
4208 assert_eq!(result[0].z, dec!(0.0));
4209 assert_eq!(result[1].x, dec!(0.0));
4210 assert_eq!(result[1].y, dec!(3.0));
4211 assert_eq!(result[1].z, dec!(4.0));
4212 assert_eq!(result[2].x, dec!(2.0));
4213 assert_eq!(result[2].y, dec!(0.0));
4214 assert_eq!(result[2].z, dec!(4.0));
4215 assert_eq!(result[2].x, dec!(2.0));
4216 assert_eq!(result[2].y, dec!(0.0));
4217 assert_eq!(result[2].z, dec!(4.0));
4218 }
4219
4220 #[test]
4221 fn test_scale_zero() {
4222 let surface = create_test_surface();
4223 let result = surface
4224 .scale(vec![&dec!(0.0), &dec!(0.0), &dec!(0.0)])
4225 .unwrap();
4226
4227 assert!(
4228 result
4229 .points
4230 .iter()
4231 .all(|p| p.x == dec!(0.0) && p.y == dec!(0.0) && p.z == dec!(0.0))
4232 );
4233 }
4234
4235 #[test]
4236 fn test_scale_wrong_dimensions() {
4237 let surface = create_test_surface();
4238 let result = surface.scale(vec![&dec!(2.0), &dec!(2.0)]);
4239 assert!(result.is_err());
4240 }
4241
4242 #[test]
4243 fn test_scale_negative() {
4244 let surface = create_test_surface();
4245 let result = surface
4246 .scale(vec![&dec!(-1.0), &dec!(-1.0), &dec!(-1.0)])
4247 .unwrap();
4248
4249 let scaled_points: Vec<_> = result.points.iter().collect();
4250 assert_eq!(scaled_points[1].x, dec!(-1.0));
4251 assert_eq!(scaled_points[1].y, dec!(0.0));
4252 assert_eq!(scaled_points[1].z, dec!(-1.0));
4253 }
4254 }
4255
4256 mod test_intersect_with {
4257 use super::*;
4258
4259 #[test]
4260 fn test_surfaces_intersect() {
4261 let surface1 = create_test_surface();
4262 let surface2 = Surface::new(BTreeSet::from_iter(vec![
4263 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4264 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4265 ]));
4266
4267 let intersections = surface1.intersect_with(&surface2).unwrap();
4268 assert_eq!(intersections.len(), 2);
4269 }
4270
4271 #[test]
4272 fn test_no_intersection() {
4273 let surface1 = create_test_surface();
4274 let surface2 = Surface::new(BTreeSet::from_iter(vec![
4275 Point3D::new(dec!(10.0), dec!(10.0), dec!(10.0)),
4276 Point3D::new(dec!(11.0), dec!(11.0), dec!(11.0)),
4277 ]));
4278
4279 let intersections = surface1.intersect_with(&surface2).unwrap();
4280 assert!(intersections.is_empty());
4281 }
4282
4283 #[test]
4284 fn test_multiple_intersections() {
4285 let surface1 = create_test_surface();
4286 let surface2 = create_test_surface();
4287
4288 let intersections = surface1.intersect_with(&surface2).unwrap();
4289 assert_eq!(intersections.len(), surface1.points.len());
4290 }
4291
4292 #[test]
4293 fn test_self_intersection() {
4294 let surface = create_test_surface();
4295 let intersections = surface.intersect_with(&surface).unwrap();
4296 assert_eq!(intersections.len(), surface.points.len());
4297 }
4298
4299 #[test]
4300 fn test_empty_surfaces() {
4301 let surface1 = Surface::new(BTreeSet::new());
4302 let surface2 = Surface::new(BTreeSet::new());
4303
4304 let intersections = surface1.intersect_with(&surface2).unwrap();
4305 assert!(intersections.is_empty());
4306 }
4307 }
4308
4309 mod test_derivative_at {
4310 use super::*;
4311
4312 #[test]
4313 fn test_planar_derivative() {
4314 let surface = Surface::new(BTreeSet::from_iter(vec![
4315 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4316 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4317 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4318 ]));
4319
4320 let derivatives = surface
4321 .derivative_at(&Point3D::new(dec!(0.5), dec!(0.5), dec!(0.5)))
4322 .unwrap();
4323 assert_eq!(derivatives.len(), 2);
4324 assert_eq!(derivatives[0], Decimal::MAX); assert_eq!(derivatives[1], dec!(1.0)); }
4327
4328 #[test]
4329 fn test_non_planar_derivative() {
4330 let surface = create_test_surface();
4331 let derivatives = surface
4332 .derivative_at(&Point3D::new(dec!(0.5), dec!(0.5), dec!(1.0)))
4333 .unwrap();
4334 assert_eq!(derivatives.len(), 2);
4335 }
4336
4337 #[test]
4338 fn test_out_of_range() {
4339 let surface = create_test_surface();
4340 let result = surface.derivative_at(&Point3D::new(dec!(10.0), dec!(10.0), dec!(10.0)));
4341 assert!(result.is_err());
4342 }
4343
4344 #[test]
4345 fn test_at_corner() {
4346 let surface = create_test_surface();
4347 let derivatives = surface
4348 .derivative_at(&Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)))
4349 .unwrap();
4350 assert_eq!(derivatives.len(), 2);
4351 }
4352
4353 #[test]
4354 fn test_single_point_surface() {
4355 let surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
4356 dec!(1.0),
4357 dec!(1.0),
4358 dec!(1.0),
4359 )]));
4360 let result = surface.derivative_at(&Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)));
4361 assert!(result.is_err());
4362 }
4363 }
4364
4365 mod test_extrema {
4366 use super::*;
4367
4368 #[test]
4369 fn test_find_extrema() {
4370 let surface = create_test_surface();
4371 let (min, max) = surface.extrema().unwrap();
4372 assert_eq!(min.z, dec!(0.0));
4373 assert_eq!(max.z, dec!(2.0));
4374 }
4375
4376 #[test]
4377 fn test_empty_surface() {
4378 let surface = Surface::new(BTreeSet::new());
4379 let result = surface.extrema();
4380 assert!(result.is_err());
4381 }
4382
4383 #[test]
4384 fn test_single_point() {
4385 let surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
4386 dec!(1.0),
4387 dec!(1.0),
4388 dec!(1.0),
4389 )]));
4390
4391 let (min, max) = surface.extrema().unwrap();
4392 assert_eq!(min, max);
4393 }
4394
4395 #[test]
4396 fn test_flat_surface() {
4397 let surface = Surface::new(BTreeSet::from_iter(vec![
4398 Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
4399 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4400 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4401 ]));
4402
4403 let (min, max) = surface.extrema().unwrap();
4404 assert_eq!(min.z, max.z);
4405 }
4406
4407 #[test]
4408 fn test_multiple_extrema() {
4409 let surface = Surface::new(BTreeSet::from_iter(vec![
4410 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4411 Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
4412 Point3D::new(dec!(2.0), dec!(2.0), dec!(0.0)),
4413 ]));
4414
4415 let (min, max) = surface.extrema().unwrap();
4416 assert_eq!(min.z, dec!(0.0));
4417 assert_eq!(max.z, dec!(2.0));
4418 }
4419 }
4420
4421 mod test_measure_under {
4422 use super::*;
4423
4424 #[test]
4425 fn test_volume_under_planar() {
4426 let surface = Surface::new(BTreeSet::from_iter(vec![
4427 Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
4428 Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4429 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4430 ]));
4431
4432 let volume = surface.measure_under(&dec!(0.0)).unwrap();
4433 assert_eq!(volume, dec!(0.5)); }
4435
4436 #[test]
4437 fn test_volume_empty_surface() {
4438 let surface = Surface::new(BTreeSet::new());
4439 let volume = surface.measure_under(&dec!(0.0)).unwrap();
4440 assert_eq!(volume, dec!(0.0));
4441 }
4442
4443 #[test]
4444 fn test_volume_single_triangle() {
4445 let surface = Surface::new(BTreeSet::from_iter(vec![
4446 Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4447 Point3D::new(dec!(1.0), dec!(0.0), dec!(0.0)),
4448 Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4449 ]));
4450
4451 let volume = surface.measure_under(&dec!(0.0)).unwrap();
4452 assert!(volume > dec!(0.0));
4453 }
4454
4455 #[test]
4456 fn test_volume_with_base_value() {
4457 let surface = create_test_surface();
4458 let volume1 = surface.measure_under(&dec!(0.0)).unwrap();
4459 let volume2 = surface.measure_under(&dec!(1.0)).unwrap();
4460 assert!(volume1 > volume2);
4461 }
4462
4463 #[test]
4464 fn test_negative_volume() {
4465 let surface = Surface::new(BTreeSet::from_iter(vec![
4466 Point3D::new(dec!(0.0), dec!(0.0), dec!(-1.0)),
4467 Point3D::new(dec!(1.0), dec!(0.0), dec!(-1.0)),
4468 Point3D::new(dec!(0.0), dec!(1.0), dec!(-1.0)),
4469 ]));
4470
4471 let volume = surface.measure_under(&dec!(0.0)).unwrap();
4472 assert!(volume > dec!(0.0));
4473 }
4474 }
4475}
4476
4477#[cfg(test)]
4478mod tests_surface_serde {
4479 use super::*;
4480 use rust_decimal_macros::dec;
4481
4482 fn create_test_surface() -> Surface {
4484 let mut points = BTreeSet::new();
4485 points.insert(Point3D {
4486 x: dec!(1.0),
4487 y: dec!(2.0),
4488 z: dec!(3.0),
4489 });
4490 points.insert(Point3D {
4491 x: dec!(4.0),
4492 y: dec!(5.0),
4493 z: dec!(6.0),
4494 });
4495 points.insert(Point3D {
4496 x: dec!(7.0),
4497 y: dec!(8.0),
4498 z: dec!(9.0),
4499 });
4500
4501 Surface {
4502 points,
4503 x_range: (dec!(1.0), dec!(7.0)),
4504 y_range: (dec!(2.0), dec!(8.0)),
4505 }
4506 }
4507
4508 #[test]
4509 fn test_basic_serialization() {
4510 let surface = create_test_surface();
4511 let serialized = serde_json::to_string(&surface).unwrap();
4512 let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4513
4514 assert_eq!(surface.points, deserialized.points);
4515 assert_eq!(surface.x_range, deserialized.x_range);
4516 assert_eq!(surface.y_range, deserialized.y_range);
4517 }
4518
4519 #[test]
4520 fn test_pretty_print() {
4521 let surface = create_test_surface();
4522 let serialized = serde_json::to_string_pretty(&surface).unwrap();
4523
4524 assert!(serialized.contains('\n'));
4526 assert!(serialized.contains(" "));
4527
4528 let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4530 assert_eq!(surface.points, deserialized.points);
4531 }
4532
4533 #[test]
4534 fn test_empty_surface() {
4535 let surface = Surface {
4536 points: BTreeSet::new(),
4537 x_range: (dec!(0.0), dec!(0.0)),
4538 y_range: (dec!(0.0), dec!(0.0)),
4539 };
4540
4541 let serialized = serde_json::to_string(&surface).unwrap();
4542 let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4543
4544 assert!(deserialized.points.is_empty());
4545 assert_eq!(deserialized.x_range, (dec!(0.0), dec!(0.0)));
4546 assert_eq!(deserialized.y_range, (dec!(0.0), dec!(0.0)));
4547 }
4548
4549 #[test]
4550 fn test_surface_with_negative_values() {
4551 let mut points = BTreeSet::new();
4552 points.insert(Point3D {
4553 x: dec!(-1.0),
4554 y: dec!(-2.0),
4555 z: dec!(-3.0),
4556 });
4557 points.insert(Point3D {
4558 x: dec!(-4.0),
4559 y: dec!(-5.0),
4560 z: dec!(-6.0),
4561 });
4562
4563 let surface = Surface {
4564 points,
4565 x_range: (dec!(-4.0), dec!(-1.0)),
4566 y_range: (dec!(-5.0), dec!(-2.0)),
4567 };
4568
4569 let serialized = serde_json::to_string(&surface).unwrap();
4570 let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4571
4572 assert_eq!(surface.points, deserialized.points);
4573 assert_eq!(surface.x_range, deserialized.x_range);
4574 assert_eq!(surface.y_range, deserialized.y_range);
4575 }
4576
4577 #[test]
4578 fn test_surface_with_high_precision() {
4579 let mut points = BTreeSet::new();
4580 points.insert(Point3D {
4581 x: dec!(1.12345678901234567890),
4582 y: dec!(2.12345678901234567890),
4583 z: dec!(3.12345678901234567890),
4584 });
4585 points.insert(Point3D {
4586 x: dec!(4.12345678901234567890),
4587 y: dec!(5.12345678901234567890),
4588 z: dec!(6.12345678901234567890),
4589 });
4590
4591 let surface = Surface {
4592 points,
4593 x_range: (dec!(1.12345678901234567890), dec!(4.12345678901234567890)),
4594 y_range: (dec!(2.12345678901234567890), dec!(5.12345678901234567890)),
4595 };
4596
4597 let serialized = serde_json::to_string(&surface).unwrap();
4598 let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4599
4600 assert_eq!(surface.points, deserialized.points);
4601 assert_eq!(surface.x_range, deserialized.x_range);
4602 assert_eq!(surface.y_range, deserialized.y_range);
4603 }
4604
4605 #[test]
4606 fn test_invalid_json() {
4607 let json_str = r#"{"points": []}"#;
4609 let result = serde_json::from_str::<Surface>(json_str);
4610 assert!(result.is_err());
4611
4612 let json_str = r#"{"points": [1, 2, 3], "x_range": [0, 1], "y_range": [0, 1]}"#;
4614 let result = serde_json::from_str::<Surface>(json_str);
4615 assert!(result.is_err());
4616
4617 let json_str = r#"{"points": [], "x_range": "invalid", "y_range": [0, 1]}"#;
4619 let result = serde_json::from_str::<Surface>(json_str);
4620 assert!(result.is_err());
4621 }
4622
4623 #[test]
4624 fn test_json_structure() {
4625 let surface = create_test_surface();
4626 let serialized = serde_json::to_string(&surface).unwrap();
4627 let json: serde_json::Value = serde_json::from_str(&serialized).unwrap();
4628
4629 assert!(json.is_object());
4631 assert!(json.get("points").is_some());
4632 assert!(json.get("x_range").is_some());
4633 assert!(json.get("y_range").is_some());
4634
4635 assert!(json.get("points").unwrap().is_array());
4637
4638 let x_range = json.get("x_range").unwrap().as_array().unwrap();
4640 let y_range = json.get("y_range").unwrap().as_array().unwrap();
4641 assert_eq!(x_range.len(), 2);
4642 assert_eq!(y_range.len(), 2);
4643 }
4644
4645 #[test]
4646 fn test_multiple_surfaces() {
4647 let surface1 = create_test_surface();
4648 let mut surface2 = create_test_surface();
4649 surface2.x_range = (dec!(8.0), dec!(14.0));
4650 surface2.y_range = (dec!(9.0), dec!(15.0));
4651
4652 let surfaces = vec![surface1, surface2];
4653 let serialized = serde_json::to_string(&surfaces).unwrap();
4654 let deserialized: Vec<Surface> = serde_json::from_str(&serialized).unwrap();
4655
4656 assert_eq!(surfaces.len(), deserialized.len());
4657 assert_eq!(surfaces[0].points, deserialized[0].points);
4658 assert_eq!(surfaces[1].points, deserialized[1].points);
4659 }
4660
4661 #[test]
4662 fn test_ordering_preservation() {
4663 let surface = create_test_surface();
4664 let serialized = serde_json::to_string(&surface).unwrap();
4665 let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4666
4667 let original_points: Vec<_> = surface.points.into_iter().collect();
4669 let deserialized_points: Vec<_> = deserialized.points.into_iter().collect();
4670
4671 assert_eq!(original_points, deserialized_points);
4673 }
4674
4675 #[test]
4676 fn test_surface_with_extremes() {
4677 let mut points = BTreeSet::new();
4678 points.insert(Point3D {
4679 x: Decimal::MAX,
4680 y: Decimal::MAX,
4681 z: Decimal::MAX,
4682 });
4683 points.insert(Point3D {
4684 x: Decimal::MIN,
4685 y: Decimal::MIN,
4686 z: Decimal::MIN,
4687 });
4688
4689 let surface = Surface {
4690 points,
4691 x_range: (Decimal::MIN, Decimal::MAX),
4692 y_range: (Decimal::MIN, Decimal::MAX),
4693 };
4694
4695 let serialized = serde_json::to_string(&surface).unwrap();
4696 let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4697
4698 assert_eq!(surface.points, deserialized.points);
4699 assert_eq!(surface.x_range, deserialized.x_range);
4700 assert_eq!(surface.y_range, deserialized.y_range);
4701 }
4702
4703 #[test]
4704 fn test_surface_points_array_format() {
4705 let json_str = r#"{
4707 "points": [
4708 [1.0, 2.0, 3.0],
4709 [4.0, 5.0, 6.0]
4710 ],
4711 "x_range": [1.0, 4.0],
4712 "y_range": [2.0, 5.0]
4713 }"#;
4714
4715 let result = serde_json::from_str::<Surface>(json_str);
4716 assert!(result.is_ok());
4717
4718 let surface = result.unwrap();
4719 assert_eq!(surface.points.len(), 2);
4720 }
4721}
4722
4723#[cfg(test)]
4724mod tests_coefficient_of_variation {
4725 use super::*;
4726 use crate::curves::{Curve, Point2D};
4727 use crate::geometrics::MetricsExtractor;
4728
4729 fn grid(z0: i64) -> Surface {
4731 Surface::new(
4732 (0..3i64)
4733 .flat_map(|i| {
4734 (0..3i64).map(move |j| {
4735 Point3D::new(
4736 Decimal::from(i),
4737 Decimal::from(j),
4738 Decimal::from(z0 + i * 3 + j),
4739 )
4740 })
4741 })
4742 .collect(),
4743 )
4744 }
4745
4746 #[test]
4752 fn test_risk_metrics_coefficient_of_variation_matches_the_curve() {
4753 let surface = grid(1).compute_risk_metrics().unwrap();
4754 assert_eq!(
4755 surface.coefficient_of_variation,
4756 dec!(0.5163977794943222513572353866)
4757 );
4758 let curve = Curve::new(
4759 (1..=9i64)
4760 .map(|y| Point2D::new(Decimal::from(y), Decimal::from(y)))
4761 .collect(),
4762 );
4763 assert_eq!(
4764 curve
4765 .compute_risk_metrics()
4766 .unwrap()
4767 .coefficient_of_variation,
4768 surface.coefficient_of_variation
4769 );
4770 }
4771
4772 #[test]
4776 fn test_risk_metrics_match_a_curve_over_the_same_values() {
4777 let surface = grid(1).compute_risk_metrics().unwrap();
4778 let curve = Curve::new(
4779 (1..=9i64)
4780 .map(|y| Point2D::new(Decimal::from(y), Decimal::from(y)))
4781 .collect(),
4782 )
4783 .compute_risk_metrics()
4784 .unwrap();
4785 assert_eq!(surface.volatility, dec!(2.5819888974716112567861769332));
4786 assert_eq!(curve.volatility, surface.volatility);
4787 assert_eq!(curve.value_at_risk, surface.value_at_risk);
4788 assert_eq!(curve.expected_shortfall, surface.expected_shortfall);
4789 assert_eq!(
4790 curve.coefficient_of_variation,
4791 surface.coefficient_of_variation
4792 );
4793 assert_eq!(curve.sharpe_ratio, surface.sharpe_ratio);
4794 }
4795
4796 #[test]
4798 fn test_risk_metrics_zero_mean_is_an_error() {
4799 assert!(matches!(
4800 grid(-4).compute_risk_metrics(),
4801 Err(MetricsError::ZeroMean {
4802 metric: "coefficient of variation"
4803 })
4804 ));
4805 }
4806}
4807
4808#[cfg(test)]
4812mod tests_nearest_matches_sort {
4813 use super::*;
4814
4815 fn sorted_prefix(
4818 surface: &Surface,
4819 x: Decimal,
4820 y: Decimal,
4821 k: usize,
4822 ) -> Result<Vec<&Point3D>, DecimalError> {
4823 let mut all: Vec<&Point3D> = surface.points.iter().collect();
4824 sort_by_distance(&mut all, x, y, "test")?;
4825 all.truncate(k);
4826 Ok(all)
4827 }
4828
4829 fn check(surface: &Surface, x: Decimal, y: Decimal) {
4830 for k in [1, 2, 3, 4, 5, 9, 10] {
4831 let fast = nearest(&surface.points, x, y, k, "test");
4832 let sorted = sorted_prefix(surface, x, y, k);
4833 assert_eq!(
4834 format!("{fast:?}"),
4835 format!("{sorted:?}"),
4836 "k = {k} at ({x}, {y}) on {:?}",
4837 surface.points
4838 );
4839 }
4840 let xy = Point2D::new(x, y);
4841 let filtered: Vec<&Point3D> = surface
4842 .points
4843 .iter()
4844 .filter(|p| p.x == x && p.y == y)
4845 .collect();
4846 assert_eq!(surface.points_at(&xy).collect::<Vec<_>>(), filtered);
4847 assert_eq!(surface.contains_point(&xy), !filtered.is_empty());
4848 assert_eq!(surface.get_point(&xy), filtered.first().copied());
4849 assert_eq!(
4850 surface.get_values(xy),
4851 filtered.iter().map(|p| &p.z).collect::<Vec<_>>()
4852 );
4853 }
4854
4855 fn surface_of(points: &[(i64, i64, i64)]) -> Surface {
4856 Surface::new(
4857 points
4858 .iter()
4859 .map(|&(x, y, z)| {
4860 Point3D::new(Decimal::from(x), Decimal::from(y), Decimal::from(z))
4861 })
4862 .collect(),
4863 )
4864 }
4865
4866 fn probes(surface: &Surface) -> Vec<(Decimal, Decimal)> {
4868 let mut at = vec![
4869 (dec!(-50), dec!(-50)),
4870 (dec!(50), dec!(3)),
4871 (dec!(0.5), dec!(0.5)),
4872 ];
4873 for p in &surface.points {
4874 at.push((p.x, p.y));
4875 at.push((p.x + dec!(0.5), p.y + dec!(0.5)));
4876 at.push((p.x - dec!(0.5), p.y));
4877 }
4878 at
4879 }
4880
4881 #[test]
4882 fn grids_with_ties() {
4883 let mut grid = Vec::new();
4886 for i in 0..6 {
4887 for j in 0..6 {
4888 grid.push((i, j, i * j));
4889 }
4890 }
4891 let surface = surface_of(&grid);
4892 for (x, y) in probes(&surface) {
4893 check(&surface, x, y);
4894 }
4895 check(&surface, dec!(2.5), dec!(2.5));
4896 check(&surface, dec!(3), dec!(2.5));
4897 }
4898
4899 #[test]
4900 fn small_and_stacked_surfaces() {
4901 let cases: Vec<Vec<(i64, i64, i64)>> = vec![
4902 vec![],
4903 vec![(1, 1, 1)],
4904 vec![(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)],
4905 vec![(0, 0, 0), (0, 0, 9), (1, 0, 1), (0, 1, 1), (1, 1, 2)],
4906 vec![(0, 0, 0), (2, 0, 0), (-2, 0, 0), (0, 2, 0), (0, -2, 0)],
4907 ];
4908 for points in cases {
4909 let surface = surface_of(&points);
4910 for (x, y) in probes(&surface) {
4911 check(&surface, x, y);
4912 }
4913 }
4914 }
4915
4916 #[test]
4917 fn random_surfaces() {
4918 let mut state: u64 = 0x9e37_79b9_7f4a_7c15;
4919 let mut next = |bound: u64| {
4920 state ^= state << 13;
4921 state ^= state >> 7;
4922 state ^= state << 17;
4923 state % bound
4924 };
4925 for _ in 0..400 {
4926 let len = next(40) as usize;
4927 let points: Vec<(i64, i64, i64)> = (0..len)
4928 .map(|_| (next(8) as i64 - 2, next(8) as i64 - 2, next(4) as i64))
4929 .collect();
4930 let surface = surface_of(&points);
4931 for (x, y) in probes(&surface) {
4932 check(&surface, x, y);
4933 }
4934 }
4935 }
4936
4937 #[test]
4938 fn distances_that_overflow_report_like_the_sort() {
4939 let far = Decimal::from(10i64.pow(15));
4942 let surface = Surface::new(
4943 [
4944 Point3D::new(dec!(0), dec!(0), dec!(1)),
4945 Point3D::new(dec!(1), dec!(0), dec!(2)),
4946 Point3D::new(dec!(0), dec!(1), dec!(3)),
4947 Point3D::new(dec!(1), far, dec!(4)),
4948 Point3D::new(far, dec!(1), dec!(5)),
4949 ]
4950 .into_iter()
4951 .collect(),
4952 );
4953 assert!(!distances_fit(
4954 &surface.points,
4955 dec!(0.5),
4956 dec!(0.5),
4957 "test"
4958 ));
4959 let fast = nearest(&surface.points, dec!(0.5), dec!(0.5), 3, "test");
4960 assert!(fast.is_err());
4961 check(&surface, dec!(0.5), dec!(0.5));
4962 let wide = Decimal::from(10i64.pow(12));
4964 let surface = Surface::new(
4965 [
4966 Point3D::new(dec!(0), dec!(0), dec!(1)),
4967 Point3D::new(dec!(1), dec!(0), dec!(2)),
4968 Point3D::new(dec!(0), dec!(1), dec!(3)),
4969 Point3D::new(dec!(1), wide, dec!(4)),
4970 Point3D::new(-wide, dec!(1), dec!(5)),
4971 ]
4972 .into_iter()
4973 .collect(),
4974 );
4975 assert!(distances_fit(&surface.points, dec!(0.5), dec!(0.5), "test"));
4976 check(&surface, dec!(0.5), dec!(0.5));
4977 }
4978
4979 #[test]
4980 fn long_mantissas() {
4981 let third = dec!(1) / dec!(3);
4984 let mut points = BTreeSet::new();
4985 for i in 0..5i64 {
4986 for j in 0..5i64 {
4987 let x = third * Decimal::from(i);
4988 let y = third * Decimal::from(j);
4989 points.insert(Point3D::new(x, y, x + y));
4990 }
4991 }
4992 let surface = Surface::new(points);
4993 for (x, y) in probes(&surface) {
4994 check(&surface, x, y);
4995 }
4996 check(&surface, third / dec!(2), third * dec!(1.5));
4997 }
4998}