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