Skip to main content

optionstratlib_math/surfaces/
surface.rs

1//! The `Surface` container: a 3D surface defined by an ordered set of
2//! `Point3D`s, with construction from points or a parametric function,
3//! bilinear and other interpolation, arithmetic between surfaces and metric
4//! extraction. Failures are reported as `SurfaceError`. Plotting a surface is
5//! the visualization layer's job (`Plottable` in `optionstratlib-visualization`).
6//!
7
8use 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/// Represents a mathematical surface in 3D space.
37///
38/// # Overview
39/// The `Surface` struct defines a three-dimensional surface composed of a collection
40/// of 3D points. It tracks the range of coordinates in the X and Y dimensions to
41/// establish the boundaries of the surface.
42///
43/// # Fields
44/// - **points**: A sorted collection of `Point3D` objects that define the surface
45///   geometry. `BTreeSet` keeps them ordered and rejects an exact duplicate of
46///   the full `(x, y, z)` triple; it does not stop two heights from sitting
47///   above one xy-coordinate. See [`Surface::new`] for the one-height-per-cell
48///   rule and for why it is a convention rather than an invariant.
49/// - **x_range**: A tuple containing the minimum and maximum x-coordinates of the surface
50///   as `Decimal` values, representing the surface's width boundaries.
51/// - **y_range**: A tuple containing the minimum and maximum y-coordinates of the surface
52///   as `Decimal` values, representing the surface's depth boundaries.
53///
54/// # Examples
55/// ```rust
56/// use rust_decimal_macros::dec;
57/// use std::collections::BTreeSet;
58/// use optionstratlib_math::surfaces::{Surface, Point3D};
59///
60/// // Create some 3D points
61/// let mut points = BTreeSet::new();
62/// points.insert(Point3D { x: dec!(0.0), y: dec!(0.0), z: dec!(1.0) });
63/// points.insert(Point3D { x: dec!(1.0), y: dec!(0.0), z: dec!(2.0) });
64/// points.insert(Point3D { x: dec!(0.0), y: dec!(1.0), z: dec!(1.5) });
65/// points.insert(Point3D { x: dec!(1.0), y: dec!(1.0), z: dec!(2.5) });
66///
67/// // Create a surface with these points
68/// let surface = Surface {
69///     points,
70///     x_range: (dec!(0.0), dec!(1.0)),
71///     y_range: (dec!(0.0), dec!(1.0)),
72/// };
73/// ```
74///
75/// # Usage
76/// `Surface` is primarily used for mathematical modeling, data visualization,
77/// and numerical analysis. It can represent various 3D structures such as
78/// option pricing surfaces, terrain models, or any other data that can be
79/// plotted in three dimensions.
80#[derive(Debug, Clone, Serialize, Deserialize)]
81#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
82#[serde(deny_unknown_fields)]
83pub struct Surface {
84    /// Collection of 3D points defining the surface.
85    ///
86    /// Public, so a struct literal can populate it without passing through
87    /// [`Surface::new`]. No constructor can therefore guarantee one height
88    /// per xy-coordinate.
89    pub points: BTreeSet<Point3D>,
90    /// The minimum and maximum x-coordinates of the surface (min_x, max_x)
91    pub x_range: (Decimal, Decimal),
92    /// The minimum and maximum y-coordinates of the surface (min_y, max_y)
93    pub y_range: (Decimal, Decimal),
94}
95
96impl Surface {
97    /// Creates a new instance from a set of 3D points.
98    ///
99    /// # Parameters
100    /// - `points`: A sorted set of `Point3D` objects that will form this geometric object.
101    ///
102    /// # Returns
103    /// A new instance of the implementing structure with computed x and y ranges.
104    ///
105    /// # Details
106    /// This constructor initializes a geometric object by:
107    /// 1. Computing the minimum and maximum x-coordinate values
108    /// 2. Computing the minimum and maximum y-coordinate values
109    /// 3. Storing the provided points and calculated ranges
110    ///
111    /// The ranges are calculated using the `calculate_range` utility method
112    /// defined in the `GeometricObject` trait.
113    ///
114    /// # Examples
115    /// ```rust
116    /// use std::collections::BTreeSet;
117    /// use rust_decimal_macros::dec;
118    /// use optionstratlib_math::surfaces::{Point3D, Surface};
119    ///
120    /// let mut points = BTreeSet::new();
121    /// points.insert(Point3D { x: dec!(1.0), y: dec!(2.0), z: dec!(3.0) });
122    /// points.insert(Point3D { x: dec!(4.0), y: dec!(5.0), z: dec!(6.0) });
123    ///
124    /// let object = Surface::new(points);
125    /// ```
126    ///
127    /// # One height per xy-coordinate
128    ///
129    /// A `Surface` is a function of its `(x, y)` coordinate: at most one `z`
130    /// above each cell. Every consumer reads it that way.
131    /// [`get_point`](crate::geometrics::AxisOperations::get_point) and
132    /// [`contains_point`](crate::geometrics::AxisOperations::contains_point)
133    /// answer from the first point matching `(x, y)`, and
134    /// `merge_axis_interpolate` resamples both surfaces onto one shared
135    /// xy-grid with one `z` per cell.
136    ///
137    /// Nothing enforces the rule. [`Point3D`] orders on the full
138    /// `(x, y, z)` triple, so `points` may hold several heights above one
139    /// xy-coordinate and `new` stores them all; a surface built that way is
140    /// outside the contract, and `get_point` then silently returns the
141    /// lowest `z` of the stack, the first in `(x, y, z)` order, while
142    /// `contains_point` reports only that something is there. Use
143    /// [`get_values`](crate::geometrics::AxisOperations::get_values) to see
144    /// every `z` above a cell.
145    ///
146    /// Normalizing here would not help: `points` is a `pub` field, so a
147    /// struct literal reaches the same state without going through any
148    /// constructor. Rejecting or collapsing a duplicate can only ever be a
149    /// convenience on this path, never an invariant of the type.
150    ///
151    /// The one projection that is genuinely multi-valued,
152    /// [`Surface::project_onto`], returns a `Vec<Point2D>` for that reason
153    /// and never a [`Curve`](crate::curves::Curve).
154    #[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    /// Projects the surface onto a 2D plane by dropping one coordinate.
166    ///
167    /// The projection is perpendicular to `axis`: the coordinate naming the
168    /// axis is omitted and the remaining two become the abscissa and the
169    /// ordinate of a [`Point2D`].
170    ///
171    /// # Parameters
172    /// - `&self`: Reference to the Surface instance
173    /// - `axis` (`Axis`): The axis perpendicular to the projection plane:
174    ///   - `Axis::X`: Projects onto the YZ plane (x-coordinate is omitted)
175    ///   - `Axis::Y`: Projects onto the XZ plane (y-coordinate is omitted)
176    ///   - `Axis::Z`: Projects onto the XY plane (z-coordinate is omitted)
177    ///
178    /// # Returns
179    /// - `Vec<Point2D>`: exactly one projected point per surface point,
180    ///   sorted ascending by the projected `(x, y)` pair.
181    ///
182    /// # Behavior
183    /// - For `Axis::X`, the returned points carry (y, z) coordinates
184    /// - For `Axis::Y`, the returned points carry (x, z) coordinates
185    /// - For `Axis::Z`, the returned points carry (x, y) coordinates
186    ///
187    /// # Multi-valued projections
188    ///
189    /// Dropping a coordinate from a grid is multi-valued: every row of the
190    /// grid contributes a different height above the same projected abscissa.
191    /// A projection of an `n`-by-`m` grid therefore yields `n * m` points,
192    /// several of which share an abscissa, and two rows that agree on the
193    /// two surviving coordinates project onto the very same point.
194    ///
195    /// That is why this returns a `Vec` and not a
196    /// [`Curve`](crate::curves::Curve). A curve is a function of its
197    /// abscissa (see [`Curve::new`](crate::curves::Curve::new)) and it
198    /// stores its points in a `BTreeSet`, which drops every projected point
199    /// equal to one already collected. The vector keeps all of them:
200    /// `result.len() == self.points.len()`, always.
201    ///
202    /// Callers that need a function of the projected abscissa must aggregate
203    /// the ordinates sharing one themselves. This method does not choose an
204    /// aggregation for them, and building a `Curve` from the result without
205    /// aggregating first is exactly the loss the return type exists to
206    /// prevent.
207    #[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        // Sorting is what the `BTreeSet` behind the old `Curve` return type
219        // provided; only its deduplication is dropped.
220        points.sort();
221        points
222    }
223
224    /// Performs one-dimensional spline interpolation on a collection of points.
225    ///
226    /// This function interpolates a value along a one-dimensional curve defined by a collection
227    /// of points. It uses linear interpolation between adjacent points to estimate the value
228    /// at the target position.
229    ///
230    /// # Parameters
231    /// * `points` - A slice of points of type T
232    /// * `target` - The x-coordinate at which to interpolate
233    /// * `x_selector` - A function that extracts the x-coordinate from a point
234    /// * `z_selector` - A function that extracts the z-coordinate (value) from a point
235    ///
236    /// # Returns
237    /// * `Ok(Decimal)` - The interpolated value at the target position
238    /// * `Err(InterpolationError)` - If interpolation fails (e.g., insufficient points)
239    ///
240    /// # Type Parameters
241    /// * `T` - The type of points, which must implement Clone
242    ///
243    /// # Behavior
244    /// - Points are sorted by their x-coordinate
245    /// - If fewer than 2 points are provided, returns an error
246    /// - If the target is outside the range of x-coordinates, returns the value at the nearest endpoint
247    /// - Otherwise performs linear interpolation between the two points that bracket the target
248    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        // Sort points by x coordinate
259        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        // Ensure we have at least 2 points
267        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        // Handle out-of-range cases
281        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        // Non-empty: `first` above returned otherwise.
287        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        // Find the segment where the target falls. The `target <= first`
299        // branch above already returned, so the first match is never at index
300        // 0 and `index - 1` cannot underflow; the `checked_sub` states it.
301        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        // Get the points for interpolation
325        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        // Linear interpolation
337        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    /// Every point above the xy-coordinate `xy`, in `(x, y, z)` order.
352    ///
353    /// `Point3D` orders on `(x, y, z)`, so the heights above one cell are
354    /// contiguous in the tree and a range bounded by the extreme heights
355    /// reads them in O(log n) instead of filtering every point (#858).
356    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    /// Fetches the point at `index` without going through the panicking
371    /// [`Index`] contract.
372    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    /// Converts the surface points from Decimal to f64 format, with swapped y and z coordinates.
382    ///
383    /// # Returns
384    /// A vector of tuples containing the coordinates of each point in the surface as `(x, z, y)`
385    /// where each coordinate is converted to an `f64` value.
386    ///
387    /// # Details
388    /// - This function is only available on non-WebAssembly targets.
389    /// - The coordinates are returned as `(x, z, y)` tuples, with y and z swapped.
390    /// - If the conversion from `Decimal` to `f64` fails for any coordinate, that value
391    ///   will be replaced with 0.0.
392    ///
393    /// # Example
394    /// ```rust,no_run
395    /// use rust_decimal_macros::dec;
396    /// use std::collections::BTreeSet;
397    /// use optionstratlib_math::surfaces::{Point3D, Surface};
398    ///
399    /// let mut points = BTreeSet::new();
400    /// points.insert(Point3D { x: dec!(1.5), y: dec!(3.0), z: dec!(2.0) });
401    /// points.insert(Point3D { x: dec!(2.5), y: dec!(4.0), z: dec!(3.0) });
402    ///
403    /// let surface = Surface {
404    ///     points,
405    ///     x_range: (dec!(1.0), dec!(3.0)),
406    ///     y_range: (dec!(3.0), dec!(4.0)),
407    /// };
408    ///
409    /// // Will produce: [(1.5, 2.0, 3.0), (2.5, 3.0, 4.0)]
410    /// let points = surface.get_f64_points();
411    /// ```
412    ///
413    /// # Errors
414    ///
415    /// [`SurfaceError::Point3DError`] when a coordinate has no `f64`
416    /// conversion ([`decimal_to_f64`], #828). It used to read as `0.0`.
417    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
430/// Wraps a checked-arithmetic failure in the interpolation variant of the
431/// algorithm that raised it.
432fn interp_err(
433    kind: fn(String) -> InterpolationError,
434) -> impl Fn(DecimalError) -> InterpolationError {
435    move |err| kind(err.to_string())
436}
437
438/// Wraps a checked-arithmetic failure raised while building a surface.
439fn construction_err(err: DecimalError) -> SurfaceError {
440    SurfaceError::ConstructionError(err.to_string())
441}
442
443/// Wraps a checked-arithmetic failure raised while analysing a surface.
444fn analysis_err(err: DecimalError) -> SurfaceError {
445    SurfaceError::AnalysisError(err.to_string())
446}
447
448/// Wraps a checked-arithmetic failure raised while fitting a surface trend.
449fn trend_err(err: DecimalError) -> MetricsError {
450    MetricsError::TrendError(err.to_string())
451}
452
453/// Wraps a checked-arithmetic failure raised while computing surface risk.
454fn risk_err(err: DecimalError) -> MetricsError {
455    MetricsError::RiskError(err.to_string())
456}
457
458/// Squared euclidean distance from `point` to `(x, y)`, computed with checked
459/// arithmetic so a coordinate at the edge of the `Decimal` range reports
460/// instead of aborting.
461fn 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
472/// Orders `points` by their squared distance to `(x, y)`, keeping the stable
473/// ordering `sort_by` produced while moving the arithmetic out of the
474/// comparator, which has no channel for an overflow.
475fn 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/// Position of a point in the tree, relative to the split
493/// [`nearest`] walks outwards from: a point before the split, counted
494/// backwards from it, sorts ahead of every point from the split on, so
495/// ordering on this reproduces the tree's own order.
496#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
497enum TreeOrder {
498    Before(Reverse<usize>),
499    From(usize),
500}
501
502/// Whether [`squared_distance`] succeeds from `(x, y)` to every point of
503/// `points`.
504///
505/// Every step of that distance is monotone in the magnitude of the
506/// coordinate offsets, and those offsets are bounded by the extreme
507/// abscissas (the first and last points of the tree) and the extreme
508/// ordinates (one pass, comparisons only). If the distance at those bounds
509/// fits, every point's does.
510fn 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
528/// The first `k` points of `points` in the order [`sort_by_distance`] leaves
529/// them, ties included, without keying and sorting every point (#858).
530///
531/// The sort is stable over the tree's order, so its first `k` are the `k`
532/// smallest `(squared distance, tree position)` pairs. The tree is sorted by
533/// `x`, so this walks outwards from `x` on each side and stops a side at the
534/// first point whose squared abscissa offset alone exceeds the `k`-th best
535/// distance: that offset never shrinks further out, and a squared distance
536/// is never below its squared abscissa offset, so no point beyond can enter.
537/// The comparison is strict, so a point that ties the `k`-th best on
538/// distance is still keyed and its tree position decides.
539///
540/// When some point's distance would overflow, the sort reports the first
541/// such point in tree order; a walk that stops early cannot, so that case
542/// runs the sort itself and returns the same error.
543fn 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    // Offers `point` to `best`; `false` once no point further out on the
559    // same side can make it.
560    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
599/// Reads one entry of a nearest-neighbour window, naming the slot on the
600/// out-of-bounds path instead of panicking.
601fn 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
614/// Reads one sample of a sorted metric series, naming the statistic on the
615/// out-of-bounds path instead of panicking.
616/// `3 * len / 4`, the index of the third quartile of `len` sorted samples.
617fn 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
638/// Arithmetic mean of a sample. Errors on an empty sample, where the mean is
639/// undefined, and on a sum that leaves the representable range.
640fn 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
645/// Sum of `(value - mean)^order` over a sample: the raw central moment behind
646/// the variance (`order = 2`), skewness (`3`) and kurtosis (`4`).
647fn 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
671/// Implementation of the `GeometricObject` trait for the `Surface` struct.
672///
673/// This implementation provides functionality to create and manipulate 3D surfaces using points
674/// in three-dimensional space. It supports construction from explicit point collections or
675/// through parametric functions.
676///
677/// # Type Parameters
678/// - Uses `Point3D` as the points that form the surface
679/// - Uses `Point2D` as the parametric input for surface generation
680///
681/// # Methods
682/// - `get_points()`: Retrieves all points in the surface
683/// - `from_vector()`: Constructs a surface from a vector of points
684/// - `construct()`: Creates a surface using different construction methods
685///
686/// # Error Handling
687/// Uses `SurfaceError` for various error conditions, including:
688/// - Empty point collections
689/// - Invalid construction parameters
690/// - Errors during parametric function evaluation
691impl GeometricObject<Point3D, Point2D> for Surface {
692    type Error = SurfaceError;
693
694    /// Returns a borrowed reference to all points in the surface as an ordered set
695    ///
696    /// # Returns
697    /// * `BTreeSet<&Point3D>` - A sorted set containing references to all points
698    ///   that define the surface, maintaining the natural ordering of points
699    ///
700    /// # Example
701    /// ```rust
702    /// use optionstratlib_math::surfaces::{Surface, Point3D};
703    /// use std::collections::BTreeSet;
704    /// use rust_decimal_macros::dec;
705    /// use optionstratlib_math::geometrics::GeometricObject;
706    ///
707    /// // Create a surface with some points
708    /// let mut surface = Surface {
709    ///     points: BTreeSet::new(),
710    ///     x_range: (dec!(0), dec!(10)),
711    ///     y_range: (dec!(0), dec!(10)),
712    /// };
713    ///
714    /// // Add points to the surface
715    /// surface.points.insert(Point3D { x: dec!(1.0), y: dec!(2.0), z: dec!(3.0) });
716    /// surface.points.insert(Point3D { x: dec!(4.0), y: dec!(5.0), z: dec!(6.0) });
717    ///
718    /// // Get references to all points in the surface
719    /// let points = surface.get_points();
720    /// assert_eq!(points.len(), 2);
721    /// ```
722    fn get_points(&self) -> BTreeSet<&Point3D> {
723        self.points.iter().collect()
724    }
725
726    /// Creates a new Surface from a vector of points that can be converted into Point3D objects.
727    ///
728    /// This method constructs a Surface by converting each point in the input vector to a Point3D
729    /// and collecting them into an ordered set. It also calculates the x and y coordinate ranges
730    /// of the points to define the surface's boundaries.
731    ///
732    /// # Type Parameters
733    ///
734    /// * `T`: A type that can be converted into Point3D via the Into trait and can be cloned.
735    ///
736    /// # Parameters
737    ///
738    /// * `points`: A vector of objects that can be converted to Point3D.
739    ///
740    /// # Returns
741    ///
742    /// A new Surface instance containing the converted points and their coordinate ranges.
743    ///
744    /// # Example
745    ///
746    /// ```rust
747    /// use optionstratlib_math::surfaces::{Surface, Point3D};
748    /// use optionstratlib_math::geometrics::GeometricObject;
749    /// use rust_decimal_macros::dec;
750    ///
751    /// // Create points data
752    /// let points = vec![
753    ///     Point3D { x: dec!(1.0), y: dec!(2.0), z: dec!(3.0) },
754    ///     Point3D { x: dec!(4.0), y: dec!(5.0), z: dec!(6.0) }
755    /// ];
756    ///
757    /// // Create a surface from the points
758    /// let surface = Surface::from_vector(points);
759    ///
760    /// // The surface will contain both points and have x_range and y_range calculated automatically
761    /// assert_eq!(surface.points.len(), 2);
762    /// assert_eq!(surface.x_range, (dec!(1.0), dec!(4.0)));
763    /// assert_eq!(surface.y_range, (dec!(2.0), dec!(5.0)));
764    /// ```
765    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    /// Constructs a Surface from a given construction method.
780    ///
781    /// This function creates a Surface object from either a set of 3D points or a parametric function.
782    ///
783    /// # Parameters
784    /// * `method` - A construction method that can be converted into a `ConstructionMethod<Point3D, Point2D, SurfaceError>`
785    ///
786    /// # Type Parameters
787    /// * `T` - Type that can be converted into a `ConstructionMethod<Point3D, Point2D, SurfaceError>`
788    ///
789    /// # Returns
790    /// * `Result<Self, Self::Error>` - Either a successfully constructed Surface or an error
791    ///
792    /// # Errors
793    /// * `SurfaceError::Point3DError` - If an empty points array is provided
794    /// * `SurfaceError::ConstructionError` - If invalid parameters are provided or the parametric function fails
795    ///
796    /// # Examples
797    ///
798    /// ## Creating from existing points
799    /// ```rust
800    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
801    /// use std::collections::BTreeSet;
802    /// use optionstratlib_math::geometrics::{ConstructionMethod, GeometricObject};
803    /// use optionstratlib_math::surfaces::{Point3D, Surface};
804    /// let points = BTreeSet::from_iter(vec![
805    ///     Point3D::new(0, 0, 0),
806    ///     Point3D::new(1, 0, 1),
807    ///     Point3D::new(0, 1, 1),
808    /// ]);
809    /// let surface = Surface::construct(ConstructionMethod::FromData { points })?;
810    /// # Ok(())
811    /// # }
812    /// ```
813    ///
814    /// ## Creating from a parametric function
815    /// ```rust,no_run
816    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
817    /// use rust_decimal_macros::dec;
818    /// use optionstratlib_math::curves::Point2D;
819    /// use optionstratlib_math::error::SurfaceError;
820    /// use optionstratlib_math::geometrics::{ConstructionMethod, ConstructionParams, GeometricObject};
821    /// use optionstratlib_math::surfaces::{Point3D, Surface};
822    /// let params = ConstructionParams::D3 {
823    ///     x_start: dec!(-1.0),
824    ///     x_end: dec!(1.0),
825    ///     y_start: dec!(-1.0),
826    ///     y_end: dec!(1.0),
827    ///     x_steps: 20,
828    ///     y_steps: 20,
829    /// };
830    ///
831    /// // Parametric function defining a paraboloid: z = x² + y²
832    /// let f = Box::new(|p: Point2D| -> Result<Point3D, SurfaceError> {
833    ///     Ok(Point3D {
834    ///         x: p.x,
835    ///         y: p.y,
836    ///         z: p.x * p.x + p.y * p.y,
837    ///     })
838    /// });
839    ///
840    /// let surface = Surface::construct(ConstructionMethod::Parametric { f, params })?;
841    /// # Ok(())
842    /// # }
843    /// ```
844    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                // Wrap f in an Arc so it can be shared across threads
887                let f = Arc::new(f);
888                // `f` is the caller's, and may price on a rayon worker.
889                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                            // Each axis ends exactly at its end, and no
899                            // coordinate passes it (#799).
900                            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
926/// Implementation of the `Index` trait for `Surface`, allowing direct indexing access to surface points.
927///
928/// # Overview
929/// This implementation allows you to access individual points in a `Surface` using array-like
930/// indexing notation (e.g., `surface[0]`, `surface[1]`). Points are retrieved in the order they
931/// appear in the underlying `BTreeSet`.
932///
933/// # Panics
934///
935/// Panics if `index >= self.points.len()`. This matches the documented
936/// contract of [`std::ops::Index`] and of `Vec`. `Index` has no error
937/// channel; for a lookup that reports a missing index, use
938/// `get_points().into_iter().nth(index)`.
939///
940/// # Performance
941/// Note that this implementation uses `iter().nth(index)` which has O(n) time complexity
942/// for `BTreeSet`. For frequent access to points by index, consider using a data structure
943/// with O(1) indexing performance.
944impl Index<usize> for Surface {
945    type Output = Point3D;
946
947    /// Retrieves a reference to a point on the surface at the specified index.
948    ///
949    /// This implementation allows using indexing syntax (e.g., `surface[i]`) to access
950    /// individual points that make up the surface.
951    ///
952    /// # Panics
953    ///
954    /// Panics if `index >= self.points.len()`.
955    fn index(&self, index: usize) -> &Self::Output {
956        // INVARIANT: same as `Curve::index` above — `std::ops::Index`
957        // requires returning `&Self::Output` with no signalling channel,
958        // so panic on out-of-bounds is the documented contract.
959        match self.points.iter().nth(index) {
960            Some(p) => p,
961            None => {
962                // INVARIANT: `std::ops::Index` returns `&Self::Output` and has
963                // no fallible channel; the contract mirrors `Vec::index`. No
964                // library code reaches this arm: every internal lookup goes
965                // through `Surface::point_at`.
966                let len = self.points.len();
967                panic!("Surface::index: out of bounds (index = {index}, len = {len})") // scan-banned: allow -- `Index` has no fallible channel and no library call site reaches it
968            }
969        }
970    }
971}
972
973/// Implementation of the `Interpolate` trait for the `Surface` type, enabling
974/// interpolation from 3D surface points to 2D points.
975///
976/// # Overview
977/// This implementation allows a `Surface` object to perform various types of interpolation
978/// (linear, bilinear, cubic, and spline) by projecting 3D points from the surface to 2D points.
979///
980/// # Functionality
981/// By implementing the `Interpolate` trait, `Surface` gains the following capabilities:
982/// - Interpolating between 3D surface points to produce 2D projections
983/// - Finding bracket points for interpolation operations
984/// - Supporting multiple interpolation algorithms through the trait's methods
985///
986/// # Usage Example
987/// ```rust
988/// use rust_decimal_macros::dec;
989/// use optionstratlib_math::surfaces::{Surface, Point3D};
990/// use optionstratlib_math::curves::Point2D;
991/// use optionstratlib_math::geometrics::{Interpolate, InterpolationType};
992///
993/// let surface = Surface::new(Default::default());
994///
995/// // Interpolate a 2D point at a specific position using linear interpolation
996/// let input_point = Point2D { x: dec!(1.5), y: dec!(2.0) };
997/// let result = surface.interpolate(input_point, InterpolationType::Linear);
998/// ```
999///
1000/// # Related Traits
1001/// This implementation relies on the surface also implementing:
1002/// - `LinearInterpolation<Point3D, Point2D>`
1003/// - `BiLinearInterpolation<Point3D, Point2D>`
1004/// - `CubicInterpolation<Point3D, Point2D>`
1005/// - `SplineInterpolation<Point3D, Point2D>`
1006/// - `GeometricObject<Point3D, Point2D>`
1007impl Interpolate<Point3D, Point2D> for Surface {}
1008
1009/// # Linear Interpolation for Surfaces
1010///
1011/// Implementation of the `LinearInterpolation` trait for `Surface` structures, enabling
1012/// interpolation from 2D points to 3D points using barycentric coordinates.
1013///
1014/// ## Overview
1015///
1016/// This implementation allows calculating the height (z-coordinate) of any point within
1017/// the surface's x-y range by using linear interpolation based on the three nearest points
1018/// in the surface. The method employs barycentric coordinate interpolation with triangulation
1019/// of the nearest points.
1020///
1021/// ## Algorithm
1022///
1023/// The interpolation process follows these steps:
1024/// 1. Validate that the input point is within the surface's range
1025/// 2. Check for degenerate cases (all points at same location)
1026/// 3. Check for exact matches with existing points
1027/// 4. Find the three nearest points to the query point
1028/// 5. Calculate barycentric coordinates for the triangle formed by these points
1029/// 6. Interpolate the z-value using the barycentric weights
1030impl LinearInterpolation<Point3D, Point2D> for Surface {
1031    /// ## Parameters
1032    ///
1033    /// * `xy` - A `Point2D` representing the x and y coordinates where interpolation is needed
1034    ///
1035    /// ## Returns
1036    ///
1037    /// * `Result<Point3D, InterpolationError>` - The interpolated 3D point if successful, or an
1038    ///   appropriate error if interpolation cannot be performed
1039    ///
1040    /// ## Errors
1041    ///
1042    /// Returns `InterpolationError::Linear` in the following cases:
1043    /// * When the surface contains only coincident points forming a degenerate triangle
1044    /// * When the query point is outside the surface's x-y range
1045    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        // Check for degenerate triangle before exact match. The tree orders
1073        // on `(x, y, z)`, so every point shares one xy-coordinate exactly
1074        // when the first and the last do.
1075        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        // Check for exact match
1083        if let Some(point) = self.points_at(&xy).next() {
1084            return Ok(*point);
1085        }
1086
1087        // Barycentric interpolation needs a triangle, so a surface with fewer
1088        // than three points has no answer to give.
1089        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            // Three collinear (or coincident) neighbours span no area, so the
1115            // barycentric weights are undefined rather than infinite.
1116            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        // Check if we have enough points
1165        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        // Check if the point is within range
1172        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        // Check for invalid quadrilateral: exactly four heights above `xy`.
1183        // The tree holds no exact duplicate, so those four heights differ.
1184        if self.points_at(&xy).count() == 4 {
1185            return Err(InterpolationError::Bilinear(
1186                "Invalid quadrilateral".to_string(),
1187            ));
1188        }
1189
1190        // For exact matches, return the actual point
1191        if let Some(point) = self.points_at(&xy).next() {
1192            return Ok(*point);
1193        }
1194
1195        // Find the four closest points
1196        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        // Sort points to create a quadrilateral
1208        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        // Get the four points for interpolation
1218        let q11 = nth(&quad_points, 0, InterpolationError::Bilinear)?; // Bottom-left point
1219        let q12 = nth(&quad_points, 1, InterpolationError::Bilinear)?; // Bottom-right point
1220        let q21 = nth(&quad_points, 2, InterpolationError::Bilinear)?; // Top-left point
1221        let q22 = nth(&quad_points, 3, InterpolationError::Bilinear)?; // Top-right point
1222
1223        // Calculate normalized coordinates
1224        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            // The quadrilateral has collapsed onto a line, so the normalized
1228            // coordinates are undefined rather than infinite.
1229            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        // Perform bilinear interpolation
1239        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        // Check if we have enough points for cubic interpolation
1265        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        // Check if the point is within range
1272        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        // Check for exact point match
1283        if let Some(point) = self.points_at(&xy).next() {
1284            return Ok(*point);
1285        }
1286
1287        // Find the 9 closest points for cubic interpolation
1288        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        // Cubic interpolation requires solving a system of equations
1300        // We'll use a weighted cubic interpolation approach
1301
1302        // Calculate weights based on distance
1303        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                    // sqrt only fails for negative input or a result that
1311                    // cannot be represented as Decimal (overflow). Squared
1312                    // distance is always >= 0, so this is the overflow
1313                    // case — drop the point's contribution by giving it
1314                    // zero weight rather than a misleadingly small one.
1315                    warn!(
1316                        "cubic_interpolate: sqrt failed for operand ({sq}); dropping point from weighting"
1317                    );
1318                    weights.push(Decimal::ZERO);
1319                    continue;
1320                }
1321            };
1322            // The 1e-6 floor keeps a coincident neighbour out of a zero divisor.
1323            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        // Weighted cubic interpolation
1331        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            // Cubic weight function
1336            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        // Prevent division by zero
1347        let interpolated_z = if denominator != Decimal::ZERO {
1348            d_div(numerator_z, denominator, op).map_err(interp_err(InterpolationError::Cubic))?
1349        } else {
1350            // Fallback to average if weights are problematic
1351            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        // Check if we have enough points for spline interpolation
1365        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        // Check if the point is within range
1372        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        // Check for exact point match
1383        if let Some(point) = self.points_at(&xy).next() {
1384            return Ok(*point);
1385        }
1386
1387        // Sort points to create a grid-like structure
1388        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        // Group points by x and y coordinates
1398        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        // Prepare data for interpolation
1409        let y_values: Vec<Decimal> = y_groups.keys().cloned().collect();
1410
1411        // Natural cubic spline interpolation
1412        // We'll interpolate in two steps: first along x, then along y
1413
1414        // Interpolate along x for each unique y value
1415        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            // Perform cubic spline interpolation along x for this y
1428            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 no x interpolation points, return error
1437        if interpolated_x_points.is_empty() {
1438            return Err(InterpolationError::Spline(
1439                "Could not interpolate along x-axis".to_string(),
1440            ));
1441        }
1442
1443        // Now interpolate these points along y
1444        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        // Return the final interpolated point
1452        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        // An empty surface has no statistics; report the same zeroed set
1471        // `Curve::compute_basic_metrics` already returns for an empty curve.
1472        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        // Mode calculation using HashMap to count occurrences
1489        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        // A failed square root is an error, not a zero dispersion (#824).
1509        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        // Skewness and kurtosis need a spread to standardise by; mirror the
1523        // zeroed answer `Curve::compute_shape_metrics` returns below two
1524        // samples.
1525        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        // A failed square root is an error, not a unit dispersion (#824).
1542        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        // An empty surface has no quantiles; mirror the zeroed answer
1586        // `Curve::compute_range_metrics` returns for an empty curve.
1587        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        // Handle surfaces with insufficient points
1625        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        // Linear Regression Calculation
1635        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        // Check for identical points to avoid division by zero
1644        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            // All points are the same
1649            (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                // R-squared Calculation
1665                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            // A surface whose abscissas all collapse to one value (a single
1685            // column) leaves the ordinary-least-squares denominator at zero,
1686            // where the slope is undefined rather than infinite.
1687            regression().map_err(trend_err)?
1688        };
1689
1690        // Moving Average Calculation
1691        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        // An empty surface carries no risk; mirror the zeroed answer
1718        // `Curve::compute_risk_metrics` returns for an empty curve.
1719        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        // The population standard deviation, through the helper `Curve`
1732        // uses too, so both report the same volatility for the same values
1733        // (#840).
1734        let volatility = population_std_dev(&z_values, mean, op).map_err(risk_err)?;
1735
1736        // Value at Risk (95% confidence) using parametric method. At zero
1737        // dispersion this is `mean - 1.645 * 0 = mean`, a deterministic level
1738        // rather than an absence of value, so it is computed from the formula
1739        // on every path instead of being short-circuited to zero.
1740        let z_score = dec!(1.645); // 95% confidence interval
1741        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        // Expected Shortfall (Conditional VaR) calculation. An empty tail has
1745        // no conditional mean; report zero as `Curve` does.
1746        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        // The coefficient of variation: the population standard deviation,
1754        // `volatility` here, over the mean, as `Curve` computes it (#824).
1755        let coefficient_of_variation = coefficient_of_variation(volatility, mean, op)?;
1756
1757        // Sharpe Ratio (assuming risk-free rate of 0). A flat surface has no
1758        // dispersion to divide by, which makes this the one field that is
1759        // genuinely undefined at `volatility == 0`; the others keep their
1760        // deterministic limits.
1761        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    /// Merges a collection of surfaces into one, resampled onto a 51 x 51
1781    /// grid over the intersection of their ranges.
1782    ///
1783    /// Each cell of the grid interpolates every operand and combines the
1784    /// heights with `operation`. An empty slice is an error; a single surface
1785    /// is returned as a clone.
1786    ///
1787    /// # Determinism
1788    ///
1789    /// The same operands in the same order give the same result, digit for
1790    /// digit. Within a cell the heights are accumulated in the order they
1791    /// were passed in, never in an order chosen by the thread pool: `Decimal`
1792    /// multiplication rounds once a product needs more than 28 decimal
1793    /// digits and is therefore not associative, so a parallel reducer would
1794    /// take its bracketing from rayon's chunking and could return different
1795    /// last digits on consecutive runs of one binary over one input.
1796    ///
1797    /// `Max` and `Min` still reduce in parallel; they select a height rather
1798    /// than accumulating one, so no rounding enters, and the same caveat
1799    /// about ties between equal `Decimal` values at different scales applies
1800    /// as it does to `Curve::merge`.
1801    ///
1802    /// # Errors
1803    ///
1804    /// - [`SurfaceError::OperationError`] when no surfaces are supplied or
1805    ///   their ranges do not intersect.
1806    /// - The interpolation error of the operand that failed, and any
1807    ///   checked-arithmetic failure raised while combining the heights.
1808    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        // Find intersection of x,y ranges
1821        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        // Validate ranges
1843        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        // Create interpolation grid
1851        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                    // The steps are rounded at 28 places, so on a span with a
1864                    // long mantissa `step * steps` can land past the maximum
1865                    // and fail the interpolation (#795). Clamping moves only
1866                    // such a point, so every grid that fit before is unchanged.
1867                    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                    // Interpolate z values
1877                    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                    // Apply operation
1890                    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                            // Division is neither associative nor commutative,
1904                            // so the divisors are folded in sequence from the
1905                            // first value. A parallel fold produces one partial
1906                            // per chunk and its reducer would have to discard
1907                            // all but one of them, dropping both the trailing
1908                            // divisors and any checked-arithmetic error raised
1909                            // inside a discarded chunk.
1910                            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    /// Reports whether any point sits above the xy-coordinate `x`.
1954    ///
1955    /// Answers on the `(x, y)` pair alone, so it says nothing about how many
1956    /// heights are stacked there. On a surface honouring the
1957    /// one-height-per-cell rule of [`Surface::new`] there is at most one.
1958    fn contains_point(&self, x: &Point2D) -> bool {
1959        self.points_at(x).next().is_some()
1960    }
1961
1962    /// Returns the xy-coordinate of every point, in `(x, y, z)` order.
1963    ///
1964    /// A surface holding several heights above one cell yields that cell
1965    /// once per height; the duplicates are visible here rather than dropped.
1966    fn get_index_values(&self) -> Vec<Point2D> {
1967        self.points.iter().map(|p| Point2D::new(p.x, p.y)).collect()
1968    }
1969
1970    /// Returns every `z` above the xy-coordinate `x`, in ascending order.
1971    ///
1972    /// This is the multi-valued reader: unlike [`Self::get_point`], it never
1973    /// picks a survivor. A surface honouring the one-height-per-cell rule of
1974    /// [`Surface::new`] returns at most one value; anything longer says the
1975    /// surface is not a function of its xy-coordinate.
1976    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        // Compare squared distances directly: ordering is monotonic on
1982        // non-negative inputs, so the sqrt is unnecessary and would
1983        // introduce a fallback that could otherwise distort ordering. The
1984        // distance is folded explicitly because a coordinate at the edge of
1985        // the `Decimal` range overflows, and `min_by` has no channel for that.
1986        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            // `min_by` keeps the first of several equal minima; `<=` here
1991            // preserves that.
1992            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    /// Returns the point above the xy-coordinate `x`, if there is one.
2006    ///
2007    /// Assumes the one-height-per-cell rule of [`Surface::new`]. If the
2008    /// surface breaks it, this returns the *first* match in `(x, y, z)`
2009    /// order, which is the lowest `z` of the stack, and the rest are
2010    /// invisible to the caller. Use [`Self::get_values`] to see all of them.
2011    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    /// Resamples both surfaces onto the shared xy-grid of their merged
2021    /// indices.
2022    ///
2023    /// Each result holds one `z` per cell of that grid, so it is a function
2024    /// of its xy-coordinate whatever the inputs were: where a surface
2025    /// already covers a cell, the point kept is the first match in
2026    /// `(x, y, z)` order, so a stack of heights above one cell collapses to
2027    /// its lowest. Aggregate such a surface before merging if a different
2028    /// height is wanted.
2029    fn merge_axis_interpolate(
2030        &self,
2031        other: &Self,
2032        interpolation: InterpolationType,
2033    ) -> Result<(Self, Self), Self::Error> {
2034        // Get merged unique xy-coordinates
2035        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); // 0.000001 tolerance
2148        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        // Handle surfaces with insufficient points
2172        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        // For surfaces with exactly 2 or 3 points, use a simple approach
2182        if self.points.len() <= 3 {
2183            let p0 = self.point_at(0)?;
2184            let p1 = self.point_at(1)?;
2185
2186            // Ensure points are not identical
2187            if p0 == p1 {
2188                return Err(SurfaceError::invalid_parameters(
2189                    "derivative_at",
2190                    "Points are identical, cannot calculate derivatives",
2191                ));
2192            }
2193
2194            // Calculate derivatives using the first two points. `Decimal::MAX`
2195            // is the pre-existing sentinel for a collapsed axis.
2196            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        // For more complex surfaces, find nearby points
2224        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        // If not enough nearby points, use the entire surface
2238        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        // Ensure we have at least 2 points
2250        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        // Sort and find derivatives
2258        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        // Prevent division by zero
2277        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        // Approximate volume using triangular prisms
2331        let mut volume = Decimal::ZERO;
2332        let points: Vec<_> = self.points.iter().collect();
2333
2334        let op = "Surface::measure_under";
2335
2336        // For each possible triangle in the surface
2337        for window in points.windows(3) {
2338            // Calculate area of triangle
2339            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            // Average height from base_value
2362            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    // Helper function to create test points
2387    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        // Check points are correctly stored
2403        assert_eq!(surface.points, points);
2404
2405        // Check x range calculation
2406        assert_eq!(surface.x_range.0, dec!(0.0));
2407        assert_eq!(surface.x_range.1, dec!(1.0));
2408
2409        // Check y range calculation
2410        assert_eq!(surface.y_range.0, dec!(0.0));
2411        assert_eq!(surface.y_range.1, dec!(1.0));
2412    }
2413
2414    /// Projecting out `x` maps `(x, y, z)` to `(y, z)`, which is multi-valued
2415    /// on a grid: the test surface has two points at `y = 0` and two at
2416    /// `y = 1`, so the projection has two ordinates above abscissa `0` and
2417    /// two above abscissa `1`.
2418    ///
2419    /// All five are asserted by exact contents, not by membership probes.
2420    /// Under the pre-#450 `Point2D` contract, `collect` into the `BTreeSet`
2421    /// of the old `Curve` return type kept only three of them, and the
2422    /// probes passed anyway because `x`-only equality made `(0, 1)` match a
2423    /// probe for `(0, 0)`.
2424    #[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    /// Projecting out `y` maps `(x, y, z)` to `(x, z)`, multi-valued on a
2458    /// grid for the same reason as the X-axis case above: two heights above
2459    /// `x = 0` and two above `x = 1`.
2460    #[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    /// Projecting out `z` maps `(x, y, z)` to `(x, y)`: the xy-footprint of
2480    /// the surface, which on a grid has several ordinates per abscissa by
2481    /// construction.
2482    #[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    /// Two surface points that agree on the two surviving coordinates
2502    /// project onto the very same 2D point. A set keeps one of them; the
2503    /// `Vec` this method returns keeps both, which is the loss the return
2504    /// type exists to prevent.
2505    #[test]
2506    fn test_project_onto_coincident_images_keeps_both_points() {
2507        // `z` depends on `y` alone, so projecting out `x` maps both rows of
2508        // the grid onto the same two points.
2509        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        // The old return type collected into a `BTreeSet`, which is where
2530        // the two lost points went.
2531        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        // Create test points for interpolation
2540        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        // Test interpolation at different points
2547        let test_cases = vec![
2548            (dec!(0.25), dec!(0.5)), // Midpoint
2549            (dec!(0.0), dec!(0.0)),  // Start point
2550            (dec!(1.0), dec!(2.0)),  // End point
2551            (dec!(0.75), dec!(1.5)), // Another point
2552        ];
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            // Allow small deviation due to interpolation
2560            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        // Single point is insufficient for interpolation
2572        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        // Test points outside the point range
2593        let out_of_range_cases = vec![
2594            (dec!(-0.5), dec!(0.0)), // Below minimum
2595            (dec!(1.5), dec!(2.0)),  // Above maximum
2596        ];
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            // Should return endpoints for out-of-range values
2604            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    /// A generator failure owned by the caller, standing in for an error of
2614    /// a higher layer (a `ChainError`, say) that math cannot name.
2615    #[derive(Debug, thiserror::Error)]
2616    #[error("{0}")]
2617    struct GeneratorFailure(&'static str);
2618
2619    // Helper function to create test points
2620    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        // Check x and y ranges
2650        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        // Create a simple parametric function that creates a basic surface
2680        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, // Simple z = x * y surface
2686                ))
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        // Should have (x_steps + 1) * (y_steps + 1) points
2707        assert_eq!(surface.points.len(), 9); // 3x3 grid
2708    }
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        // Use incorrect parameters to trigger error
2718        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        // Parametric function that sometimes fails
2735        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        // The generator's own error survives: still the caller's typed
2759        // error, not a message (roadmap M1-10).
2760        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        // Verify x and y ranges
2777        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        // El valor debe estar entre 0.0 y 1.0
2834        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        // Test interpolation on the edge
2856        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        // Crear una superficie con un gradiente uniforme
2865        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        // Interpolation at any point should maintain the gradient
2874        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        // Verify that the result has the expected precision
2887        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)), // Bottom-left
2899            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)), // Bottom-right
2900            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)), // Top-left
2901            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)), // Top-right
2902        ]);
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        // At the midpoint, we expect the average of surrounding values
2947        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        // Value should be between 0.0 and 1.0
2957        assert!(result.z > dec!(0.0) && result.z < dec!(1.0));
2958    }
2959
2960    /// Four points stacked on one xy-coordinate are an invalid quadrilateral,
2961    /// and `bilinear_interpolate` says so.
2962    ///
2963    /// This test used to assert the *"Need at least four points"* message
2964    /// instead, and it reached it because `BTreeSet::from_iter` sorts and then
2965    /// deduplicates adjacent elements with `PartialEq`, not with `Ord`. With
2966    /// `Point3D` comparing equal on `(x, y)`, these four points collapsed to
2967    /// **one** on the way into the set, so the length guard fired and the
2968    /// invalid-quadrilateral check below it was unreachable. The same four
2969    /// values inserted one by one produced a set of four, because `insert`
2970    /// dispatches on `Ord` — the same container, two different contents,
2971    /// decided by which constructor was used.
2972    ///
2973    /// `PartialEq` now agrees with `Ord`, so the set holds all four and the
2974    /// check the test is named after is the one that runs.
2975    #[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        // Test interpolation at edge
2998        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        // Create a surface with uniform gradient
3007        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        // Verify that the result has the expected precision
3027        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        // Test all four corners
3035        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    /// Helper function to create a test surface with a more complex point distribution
3061    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        // Verify that the interpolated z is between the surrounding points
3121        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        // Test multiple interpolation points
3129        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            // Verify z is within reasonable bounds
3139            assert!(
3140                result.z >= dec!(0.0) && result.z <= dec!(2.0),
3141                "Failed for point {point:?}"
3142            );
3143
3144            // Verify the interpolated point is on the surface
3145            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        // Test interpolation near surface boundaries
3155        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            // Verify z is interpolated correctly
3166            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        // Verify precision and reasonable interpolation
3181        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        // Interpolate the same point multiple times to check consistency
3189        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        // Check that results are very close to each other
3195        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        // Test points very close to existing points
3215        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            // Verify z is interpolated reasonably
3224            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    /// Helper function to create a test surface with a more complex point distribution
3238    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        // Verify that the interpolated z is between the surrounding points
3299        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        // Test multiple interpolation points
3307        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            // Verify z is within reasonable bounds
3317            assert!(
3318                result.z >= dec!(0.0) && result.z <= dec!(2.0),
3319                "Failed for point {point:?}"
3320            );
3321
3322            // Verify the interpolated point is on the surface
3323            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        // Test interpolation near surface boundaries
3333        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            // Verify z is interpolated correctly
3344            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        // Verify precision and reasonable interpolation
3359        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        // Interpolate the same point multiple times to check consistency
3367        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        // Check that results are very close to each other
3373        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        // Test points very close to existing points
3393        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            // Verify z is interpolated reasonably
3402            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        // Create test points for one-dimensional interpolation
3414        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        // Test interpolation at different points
3421        let test_points = vec![
3422            (dec!(0.25), dec!(0.5)), // Midpoint
3423            (dec!(0.0), dec!(0.0)),  // Start point
3424            (dec!(1.0), dec!(2.0)),  // End point
3425            (dec!(0.75), dec!(1.5)), // Another point
3426        ];
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            // Allow small deviation due to interpolation
3434            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        // Test edge cases like very small intervals
3446        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    /// The end of both ranges in [`long_mantissa_surface`]: a 27-place
3485    /// mantissa whose `span / 50` rounds up at 28 places, to
3486    /// `0.2000000000000000000000000004`, so `step * 50` used to land on
3487    /// `10.000000000000000000000000020`, past the maximum (#795).
3488    const LONG_MANTISSA_MAX: Decimal = dec!(10.000000000000000000000000018);
3489
3490    /// The plane `z = x + y` on the grid `{0, 2.5, 5, 7.5, LONG_MANTISSA_MAX}`
3491    /// on both axes.
3492    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        // A 51 x 51 grid from 0 to the maximum on each axis, none past it.
3509        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        // Test point should have z-value of 0 (1.0 - 1.0)
3551        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    /// Division is not associative, so the divisors have to be folded in
3568    /// sequence. Three surfaces at 8, 2 and 2 must give `8 / 2 / 2 = 2`; a
3569    /// reducer that keeps only its left input drops the trailing divisors and
3570    /// lands somewhere between 4 and 8.
3571    #[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    /// Number of times a determinism test repeats the same merge.
3590    ///
3591    /// `Surface::merge` resamples a 51 x 51 grid and runs the fold once per
3592    /// cell, so one call already exercises the fold 2601 times against
3593    /// `Curve::merge`'s 101. That buys the same confidence from far fewer
3594    /// repeats, which matters because each call is three orders of magnitude
3595    /// dearer than a curve merge; see `DETERMINISM_REPEATS` in
3596    /// `curves::curve` for how the rate this is sized against was measured.
3597    const DETERMINISM_REPEATS: usize = 24;
3598
3599    /// The heights of a surface as `(mantissa, scale)` pairs.
3600    ///
3601    /// `Decimal` compares equal across scales, so `dec!(2.0) == dec!(2.00)`
3602    /// and a plain equality check would not notice a fold that moved the
3603    /// scale. The mantissa and the scale together are the stored value.
3604    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    /// Merging three surfaces under `Multiply` gives the same digits on every
3613    /// run, and gives the digits of a left-to-right fold.
3614    ///
3615    /// The three heights are the ones used by the curve test of the same
3616    /// name: their triple product overruns the 28 decimal places a
3617    /// `Decimal` holds and has to round twice, ending `...768908` grouped
3618    /// from the left and `...768907` grouped from the right. The parallel
3619    /// reducer this replaces returned the second.
3620    #[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    /// A divisor that fails the checked division has to surface as an error
3651    /// rather than being discarded together with the fold result that
3652    /// produced it. `1e20 / 1e-20` is `1e40`, well past `Decimal::MAX`, while
3653    /// both operands stay far enough inside the range that the interpolation
3654    /// feeding the fold cannot fail first.
3655    #[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        // Compare some interpolated points
3698        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)); // 1.0 * 1.0 = 1.0
3718    }
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)); // 1.0 / 1.0 = 1.0
3730    }
3731
3732    #[test]
3733    fn test_merge_max() {
3734        let surface1 = create_test_surface(); // z=1.0 everywhere
3735
3736        // Create surface2 with z=2.0 everywhere
3737        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        // Modify one point in surface2 to be lower
3764        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        // We have a linear trend with slope 2.0
3838        assert!((metrics.slope - dec!(2.0)).abs() < dec!(0.001));
3839        assert!((metrics.intercept - dec!(2.0)).abs() < dec!(0.001));
3840    }
3841
3842    /// A flat surface has no uncertainty, but it is not worthless. Only the
3843    /// fields that are genuinely undefined at zero dispersion may be zeroed;
3844    /// the parametric VaR still has its deterministic limit at the mean.
3845    #[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        // Measured, and genuinely zero.
3854        assert_eq!(metrics.volatility, Decimal::ZERO);
3855        // `mean - 1.645 * 0` is the mean, not zero: the surface is worth 5.
3856        assert_eq!(metrics.value_at_risk, dec!(5));
3857        // No sample falls below the VaR, so the conditional mean has an empty
3858        // tail and the function's own empty-tail rule gives zero.
3859        assert_eq!(metrics.expected_shortfall, Decimal::ZERO);
3860        // Zero dispersion over a mean of 5: a zero coefficient of variation.
3861        assert_eq!(metrics.coefficient_of_variation, Decimal::ZERO);
3862        // `mean / 0` is the one genuinely undefined field.
3863        assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
3864    }
3865
3866    /// The same shape with a negative mean: the VaR limit follows the mean
3867    /// wherever it sits, so a sign error cannot hide behind a positive value.
3868    #[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    // Helper function to create a test surface
3889    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    // Helper function to create a non-linear surface
3901    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        // Check slope (should be 2.0 for a perfectly linear surface)
3918        assert_decimal_eq!(metrics.slope, dec!(2.0), dec!(0.001));
3919
3920        // Check intercept (should be close to 0)
3921        assert_decimal_eq!(metrics.intercept, dec!(0.0), dec!(0.001));
3922
3923        // R-squared should be very close to 1 for a perfect linear relationship
3924        assert_decimal_eq!(metrics.r_squared, dec!(1.0), dec!(0.001));
3925
3926        // Check moving average points
3927        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        // R-squared should be less than 1 for a non-perfect linear relationship
3936        assert!(metrics.r_squared < dec!(1.0));
3937
3938        // Slope and intercept will vary based on the non-linear surface
3939        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        // Verify moving average calculation
3949        let window_sizes = [3, 5, 7];
3950
3951        // Calculate total points safely
3952        let surface_points_count = surface.points.len();
3953
3954        let expected_total_points = window_sizes
3955            .iter()
3956            .map(|&window| {
3957                // Safely handle cases where window might be larger than points
3958                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 with more informative message
3969        assert_eq!(
3970            metrics.moving_average.len(),
3971            expected_total_points,
3972            "Mismatch in moving average points calculation"
3973        );
3974
3975        // Verify x and y values in moving average
3976        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        // Surface with a single point
3985        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        // Surface with identical points
3995        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        // For identical points, R-squared should be 1
4004        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    // Create a test Surface with predefined points
4015    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    /// Merging a 2x2 grid with itself yields all four xy-coordinates.
4078    ///
4079    /// This test used to assert `merged.len() == 2`, and it passed. That was
4080    /// not the specification, it was data loss: `merge_indexes` funnels the
4081    /// index values through a `HashSet<Input>`, `Input` is `Point2D` for a
4082    /// surface, and `Point2D` hashed and compared on `x` alone — so both
4083    /// cells of every column collapsed into one and half the grid vanished
4084    /// before `merge_axis_interpolate` ever saw it. `Point2D` now carries
4085    /// both coordinates in `Eq` and `Hash`, so the axis survives the merge.
4086    #[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); // ∂z/∂x
4328            assert_eq!(derivatives[1], dec!(1.0)); // ∂z/∂y
4329        }
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)); // Area of triangle * height
4437        }
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    // Helper function to create a test surface
4486    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        // Verify pretty print format
4528        assert!(serialized.contains('\n'));
4529        assert!(serialized.contains("  "));
4530
4531        // Verify deserialization still works
4532        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        // Missing required fields
4611        let json_str = r#"{"points": []}"#;
4612        let result = serde_json::from_str::<Surface>(json_str);
4613        assert!(result.is_err());
4614
4615        // Invalid points format
4616        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        // Invalid range format
4621        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        // Check structure
4633        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        // Check points is an array
4639        assert!(json.get("points").unwrap().is_array());
4640
4641        // Check ranges are arrays of 2 elements
4642        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        // Convert points to vectors to check ordering
4671        let original_points: Vec<_> = surface.points.into_iter().collect();
4672        let deserialized_points: Vec<_> = deserialized.points.into_iter().collect();
4673
4674        // Check if points maintain their order
4675        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        // Test that points can be deserialized from array format
4709        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    /// A 3 x 3 grid whose `z` values are `z0 + 0..9`.
4733    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    /// `z = 1..9`: mean 5, population standard deviation `sqrt(80 / 12)`,
4750    /// so the coefficient of variation is `0.5163977794943222513572353866`,
4751    /// correct to `Decimal`'s 28 places, where `beta` was a placeholder zero.
4752    /// A curve over the same values gives the same digits: both compute it
4753    /// as `std_dev / mean` (#824).
4754    #[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    /// A curve and a surface over the same values report the same risk
4776    /// metrics, digit for digit: both take the volatility from one helper,
4777    /// the population standard deviation `sqrt(80 / 12)` for `1..9` (#840).
4778    #[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    /// `z = -4..4` has a zero mean: an error, not a zero (#824).
4800    #[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/// The neighbour search of #858 against the full sort it replaced: the same
4812/// points in the same order, ties broken the same way, and the same error
4813/// when a distance overflows.
4814#[cfg(test)]
4815mod tests_nearest_matches_sort {
4816    use super::*;
4817
4818    /// What the interpolators did before #858: key every point, sort, keep
4819    /// the first `k`.
4820    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    /// Every sample, the cell centres around it, and points outside.
4870    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        // On a regular grid most queries have several neighbours at one
4887        // distance, so the tie-break decides which ones are kept.
4888        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        // A distance squared past `Decimal::MAX`: the walk falls back to the
4943        // sort, so the error names the same point and operands.
4944        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        // Large coordinates that still fit take the walk and agree.
4966        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        // Thirds do not terminate, so the squared distances round at the
4985        // 28th place and can tie after rounding.
4986        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}