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;
24
25use rayon::iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator};
26use rust_decimal::Decimal;
27use rust_decimal_macros::dec;
28use serde::{Deserialize, Serialize};
29use std::cmp::Reverse;
30use std::collections::{BTreeSet, btree_set};
31use std::ops::Index;
32use std::sync::Arc;
33use tracing::warn;
34
35/// Represents a mathematical surface in 3D space.
36///
37/// # Overview
38/// The `Surface` struct defines a three-dimensional surface composed of a collection
39/// of 3D points. It tracks the range of coordinates in the X and Y dimensions to
40/// establish the boundaries of the surface.
41///
42/// # Fields
43/// - **points**: A sorted collection of `Point3D` objects that define the surface
44///   geometry. `BTreeSet` keeps them ordered and rejects an exact duplicate of
45///   the full `(x, y, z)` triple; it does not stop two heights from sitting
46///   above one xy-coordinate. See [`Surface::new`] for the one-height-per-cell
47///   rule and for why it is a convention rather than an invariant.
48/// - **x_range**: A tuple containing the minimum and maximum x-coordinates of the surface
49///   as `Decimal` values, representing the surface's width boundaries.
50/// - **y_range**: A tuple containing the minimum and maximum y-coordinates of the surface
51///   as `Decimal` values, representing the surface's depth boundaries.
52///
53/// # Examples
54/// ```rust
55/// use rust_decimal_macros::dec;
56/// use std::collections::BTreeSet;
57/// use optionstratlib_math::surfaces::{Surface, Point3D};
58///
59/// // Create some 3D points
60/// let mut points = BTreeSet::new();
61/// points.insert(Point3D { x: dec!(0.0), y: dec!(0.0), z: dec!(1.0) });
62/// points.insert(Point3D { x: dec!(1.0), y: dec!(0.0), z: dec!(2.0) });
63/// points.insert(Point3D { x: dec!(0.0), y: dec!(1.0), z: dec!(1.5) });
64/// points.insert(Point3D { x: dec!(1.0), y: dec!(1.0), z: dec!(2.5) });
65///
66/// // Create a surface with these points
67/// let surface = Surface {
68///     points,
69///     x_range: (dec!(0.0), dec!(1.0)),
70///     y_range: (dec!(0.0), dec!(1.0)),
71/// };
72/// ```
73///
74/// # Usage
75/// `Surface` is primarily used for mathematical modeling, data visualization,
76/// and numerical analysis. It can represent various 3D structures such as
77/// option pricing surfaces, terrain models, or any other data that can be
78/// plotted in three dimensions.
79#[derive(Debug, Clone, Serialize, Deserialize)]
80#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
81#[serde(deny_unknown_fields)]
82pub struct Surface {
83    /// Collection of 3D points defining the surface.
84    ///
85    /// Public, so a struct literal can populate it without passing through
86    /// [`Surface::new`]. No constructor can therefore guarantee one height
87    /// per xy-coordinate.
88    pub points: BTreeSet<Point3D>,
89    /// The minimum and maximum x-coordinates of the surface (min_x, max_x)
90    pub x_range: (Decimal, Decimal),
91    /// The minimum and maximum y-coordinates of the surface (min_y, max_y)
92    pub y_range: (Decimal, Decimal),
93}
94
95impl Surface {
96    /// Creates a new instance from a set of 3D points.
97    ///
98    /// # Parameters
99    /// - `points`: A sorted set of `Point3D` objects that will form this geometric object.
100    ///
101    /// # Returns
102    /// A new instance of the implementing structure with computed x and y ranges.
103    ///
104    /// # Details
105    /// This constructor initializes a geometric object by:
106    /// 1. Computing the minimum and maximum x-coordinate values
107    /// 2. Computing the minimum and maximum y-coordinate values
108    /// 3. Storing the provided points and calculated ranges
109    ///
110    /// The ranges are calculated using the `calculate_range` utility method
111    /// defined in the `GeometricObject` trait.
112    ///
113    /// # Examples
114    /// ```rust
115    /// use std::collections::BTreeSet;
116    /// use rust_decimal_macros::dec;
117    /// use optionstratlib_math::surfaces::{Point3D, Surface};
118    ///
119    /// let mut points = BTreeSet::new();
120    /// points.insert(Point3D { x: dec!(1.0), y: dec!(2.0), z: dec!(3.0) });
121    /// points.insert(Point3D { x: dec!(4.0), y: dec!(5.0), z: dec!(6.0) });
122    ///
123    /// let object = Surface::new(points);
124    /// ```
125    ///
126    /// # One height per xy-coordinate
127    ///
128    /// A `Surface` is a function of its `(x, y)` coordinate: at most one `z`
129    /// above each cell. Every consumer reads it that way.
130    /// [`get_point`](crate::geometrics::AxisOperations::get_point) and
131    /// [`contains_point`](crate::geometrics::AxisOperations::contains_point)
132    /// answer from the first point matching `(x, y)`, and
133    /// `merge_axis_interpolate` resamples both surfaces onto one shared
134    /// xy-grid with one `z` per cell.
135    ///
136    /// Nothing enforces the rule. [`Point3D`] orders on the full
137    /// `(x, y, z)` triple, so `points` may hold several heights above one
138    /// xy-coordinate and `new` stores them all; a surface built that way is
139    /// outside the contract, and `get_point` then silently returns the
140    /// lowest `z` of the stack, the first in `(x, y, z)` order, while
141    /// `contains_point` reports only that something is there. Use
142    /// [`get_values`](crate::geometrics::AxisOperations::get_values) to see
143    /// every `z` above a cell.
144    ///
145    /// Normalizing here would not help: `points` is a `pub` field, so a
146    /// struct literal reaches the same state without going through any
147    /// constructor. Rejecting or collapsing a duplicate can only ever be a
148    /// convenience on this path, never an invariant of the type.
149    ///
150    /// The one projection that is genuinely multi-valued,
151    /// [`Surface::project_onto`], returns a `Vec<Point2D>` for that reason
152    /// and never a [`Curve`](crate::curves::Curve).
153    #[must_use]
154    pub fn new(points: BTreeSet<Point3D>) -> Self {
155        let x_range = Self::calculate_range(points.iter().map(|p| p.x));
156        let y_range = Self::calculate_range(points.iter().map(|p| p.y));
157        Self {
158            points,
159            x_range,
160            y_range,
161        }
162    }
163
164    /// Projects the surface onto a 2D plane by dropping one coordinate.
165    ///
166    /// The projection is perpendicular to `axis`: the coordinate naming the
167    /// axis is omitted and the remaining two become the abscissa and the
168    /// ordinate of a [`Point2D`].
169    ///
170    /// # Parameters
171    /// - `&self`: Reference to the Surface instance
172    /// - `axis` (`Axis`): The axis perpendicular to the projection plane:
173    ///   - `Axis::X`: Projects onto the YZ plane (x-coordinate is omitted)
174    ///   - `Axis::Y`: Projects onto the XZ plane (y-coordinate is omitted)
175    ///   - `Axis::Z`: Projects onto the XY plane (z-coordinate is omitted)
176    ///
177    /// # Returns
178    /// - `Vec<Point2D>`: exactly one projected point per surface point,
179    ///   sorted ascending by the projected `(x, y)` pair.
180    ///
181    /// # Behavior
182    /// - For `Axis::X`, the returned points carry (y, z) coordinates
183    /// - For `Axis::Y`, the returned points carry (x, z) coordinates
184    /// - For `Axis::Z`, the returned points carry (x, y) coordinates
185    ///
186    /// # Multi-valued projections
187    ///
188    /// Dropping a coordinate from a grid is multi-valued: every row of the
189    /// grid contributes a different height above the same projected abscissa.
190    /// A projection of an `n`-by-`m` grid therefore yields `n * m` points,
191    /// several of which share an abscissa, and two rows that agree on the
192    /// two surviving coordinates project onto the very same point.
193    ///
194    /// That is why this returns a `Vec` and not a
195    /// [`Curve`](crate::curves::Curve). A curve is a function of its
196    /// abscissa (see [`Curve::new`](crate::curves::Curve::new)) and it
197    /// stores its points in a `BTreeSet`, which drops every projected point
198    /// equal to one already collected. The vector keeps all of them:
199    /// `result.len() == self.points.len()`, always.
200    ///
201    /// Callers that need a function of the projected abscissa must aggregate
202    /// the ordinates sharing one themselves. This method does not choose an
203    /// aggregation for them, and building a `Curve` from the result without
204    /// aggregating first is exactly the loss the return type exists to
205    /// prevent.
206    #[must_use]
207    pub fn project_onto(&self, axis: Axis) -> Vec<Point2D> {
208        let mut points: Vec<Point2D> = self
209            .points
210            .iter()
211            .map(|p| match axis {
212                Axis::X => Point2D::new(p.y, p.z),
213                Axis::Y => Point2D::new(p.x, p.z),
214                Axis::Z => Point2D::new(p.x, p.y),
215            })
216            .collect();
217        // Sorting is what the `BTreeSet` behind the old `Curve` return type
218        // provided; only its deduplication is dropped.
219        points.sort();
220        points
221    }
222
223    /// Performs one-dimensional spline interpolation on a collection of points.
224    ///
225    /// This function interpolates a value along a one-dimensional curve defined by a collection
226    /// of points. It uses linear interpolation between adjacent points to estimate the value
227    /// at the target position.
228    ///
229    /// # Parameters
230    /// * `points` - A slice of points of type T
231    /// * `target` - The x-coordinate at which to interpolate
232    /// * `x_selector` - A function that extracts the x-coordinate from a point
233    /// * `z_selector` - A function that extracts the z-coordinate (value) from a point
234    ///
235    /// # Returns
236    /// * `Ok(Decimal)` - The interpolated value at the target position
237    /// * `Err(InterpolationError)` - If interpolation fails (e.g., insufficient points)
238    ///
239    /// # Type Parameters
240    /// * `T` - The type of points, which must implement Clone
241    ///
242    /// # Behavior
243    /// - Points are sorted by their x-coordinate
244    /// - If fewer than 2 points are provided, returns an error
245    /// - If the target is outside the range of x-coordinates, returns the value at the nearest endpoint
246    /// - Otherwise performs linear interpolation between the two points that bracket the target
247    fn one_dimensional_spline_interpolation<T>(
248        &self,
249        points: &[T],
250        target: Decimal,
251        x_selector: fn(&T) -> Decimal,
252        z_selector: fn(&T) -> Decimal,
253    ) -> Result<Decimal, InterpolationError>
254    where
255        T: Clone,
256    {
257        // Sort points by x coordinate
258        let mut sorted_points = points.to_vec();
259        sorted_points.sort_by(|a, b| {
260            x_selector(a)
261                .partial_cmp(&x_selector(b))
262                .unwrap_or(std::cmp::Ordering::Equal)
263        });
264
265        // Ensure we have at least 2 points
266        if sorted_points.len() < 2 {
267            return Err(InterpolationError::Spline(
268                "Insufficient points for interpolation".to_string(),
269            ));
270        }
271
272        let missing = |index: usize| {
273            InterpolationError::Spline(format!(
274                "spline knot {index} is out of bounds for {} samples",
275                sorted_points.len()
276            ))
277        };
278
279        // Handle out-of-range cases
280        let first = sorted_points.first().ok_or_else(|| missing(0))?;
281        if target <= x_selector(first) {
282            return Ok(z_selector(first));
283        }
284
285        // Non-empty: `first` above returned otherwise.
286        let last_index = sorted_points
287            .len()
288            .checked_sub(1)
289            .ok_or_else(|| missing(0))?;
290        let last = sorted_points
291            .get(last_index)
292            .ok_or_else(|| missing(last_index))?;
293        if target >= x_selector(last) {
294            return Ok(z_selector(last));
295        }
296
297        // Find the segment where the target falls. The `target <= first`
298        // branch above already returned, so the first match is never at index
299        // 0 and `index - 1` cannot underflow; the `checked_sub` states it.
300        let (left_index, right_index) = match sorted_points
301            .iter()
302            .enumerate()
303            .find(|(_, p)| x_selector(p) > target)
304        {
305            Some((index, _)) => (
306                index.checked_sub(1).ok_or_else(|| {
307                    InterpolationError::Spline(
308                        "spline bracket starts before the first knot".to_string(),
309                    )
310                })?,
311                index,
312            ),
313            None => (
314                last_index.checked_sub(1).ok_or_else(|| {
315                    InterpolationError::Spline(
316                        "spline bracket needs at least two knots".to_string(),
317                    )
318                })?,
319                last_index,
320            ),
321        };
322
323        // Get the points for interpolation
324        let left = sorted_points
325            .get(left_index)
326            .ok_or_else(|| missing(left_index))?;
327        let right = sorted_points
328            .get(right_index)
329            .ok_or_else(|| missing(right_index))?;
330        let x0 = x_selector(left);
331        let x1 = x_selector(right);
332        let z0 = z_selector(left);
333        let z1 = z_selector(right);
334
335        // Linear interpolation
336        let op = "Surface::one_dimensional_spline_interpolation";
337        let run = d_sub(x1, x0, op).map_err(interp_err(InterpolationError::Spline))?;
338        if run.is_zero() {
339            return Err(InterpolationError::DegenerateInterval);
340        }
341        let rise = d_sub(z1, z0, op).map_err(interp_err(InterpolationError::Spline))?;
342        let offset = d_sub(target, x0, op).map_err(interp_err(InterpolationError::Spline))?;
343        let ratio = d_div(offset, run, op).map_err(interp_err(InterpolationError::Spline))?;
344        let step = d_mul(rise, ratio, op).map_err(interp_err(InterpolationError::Spline))?;
345        let interpolated_z = d_add(z0, step, op).map_err(interp_err(InterpolationError::Spline))?;
346
347        Ok(interpolated_z)
348    }
349
350    /// Every point above the xy-coordinate `xy`, in `(x, y, z)` order.
351    ///
352    /// `Point3D` orders on `(x, y, z)`, so the heights above one cell are
353    /// contiguous in the tree and a range bounded by the extreme heights
354    /// reads them in O(log n) instead of filtering every point (#858).
355    fn points_at(&self, xy: &Point2D) -> btree_set::Range<'_, Point3D> {
356        self.points.range(
357            Point3D {
358                x: xy.x,
359                y: xy.y,
360                z: Decimal::MIN,
361            }..=Point3D {
362                x: xy.x,
363                y: xy.y,
364                z: Decimal::MAX,
365            },
366        )
367    }
368
369    /// Fetches the point at `index` without going through the panicking
370    /// [`Index`] contract.
371    fn point_at(&self, index: usize) -> Result<&Point3D, SurfaceError> {
372        self.points.iter().nth(index).ok_or_else(|| {
373            SurfaceError::AnalysisError(format!(
374                "point index {index} is out of bounds for a surface of {} points",
375                self.points.len()
376            ))
377        })
378    }
379
380    /// Converts the surface points from Decimal to f64 format, with swapped y and z coordinates.
381    ///
382    /// # Returns
383    /// A vector of tuples containing the coordinates of each point in the surface as `(x, z, y)`
384    /// where each coordinate is converted to an `f64` value.
385    ///
386    /// # Details
387    /// - This function is only available on non-WebAssembly targets.
388    /// - The coordinates are returned as `(x, z, y)` tuples, with y and z swapped.
389    /// - If the conversion from `Decimal` to `f64` fails for any coordinate, that value
390    ///   will be replaced with 0.0.
391    ///
392    /// # Example
393    /// ```rust,no_run
394    /// use rust_decimal_macros::dec;
395    /// use std::collections::BTreeSet;
396    /// use optionstratlib_math::surfaces::{Point3D, Surface};
397    ///
398    /// let mut points = BTreeSet::new();
399    /// points.insert(Point3D { x: dec!(1.5), y: dec!(3.0), z: dec!(2.0) });
400    /// points.insert(Point3D { x: dec!(2.5), y: dec!(4.0), z: dec!(3.0) });
401    ///
402    /// let surface = Surface {
403    ///     points,
404    ///     x_range: (dec!(1.0), dec!(3.0)),
405    ///     y_range: (dec!(3.0), dec!(4.0)),
406    /// };
407    ///
408    /// // Will produce: [(1.5, 2.0, 3.0), (2.5, 3.0, 4.0)]
409    /// let points = surface.get_f64_points();
410    /// ```
411    ///
412    /// # Errors
413    ///
414    /// [`SurfaceError::Point3DError`] when a coordinate has no `f64`
415    /// conversion ([`decimal_to_f64`], #828). It used to read as `0.0`.
416    pub fn get_f64_points(&self) -> Result<Vec<(f64, f64, f64)>, SurfaceError> {
417        let to_f64 = |value: Decimal| {
418            decimal_to_f64(value).map_err(|_| SurfaceError::Point3DError {
419                reason: "Error converting Decimal to f64",
420            })
421        };
422        self.points
423            .iter()
424            .map(|p| Ok((to_f64(p.x)?, to_f64(p.z)?, to_f64(p.y)?)))
425            .collect()
426    }
427}
428
429/// Wraps a checked-arithmetic failure in the interpolation variant of the
430/// algorithm that raised it.
431fn interp_err(
432    kind: fn(String) -> InterpolationError,
433) -> impl Fn(DecimalError) -> InterpolationError {
434    move |err| kind(err.to_string())
435}
436
437/// Wraps a checked-arithmetic failure raised while building a surface.
438fn construction_err(err: DecimalError) -> SurfaceError {
439    SurfaceError::ConstructionError(err.to_string())
440}
441
442/// Wraps a checked-arithmetic failure raised while analysing a surface.
443fn analysis_err(err: DecimalError) -> SurfaceError {
444    SurfaceError::AnalysisError(err.to_string())
445}
446
447/// Wraps a checked-arithmetic failure raised while fitting a surface trend.
448fn trend_err(err: DecimalError) -> MetricsError {
449    MetricsError::TrendError(err.to_string())
450}
451
452/// Wraps a checked-arithmetic failure raised while computing surface risk.
453fn risk_err(err: DecimalError) -> MetricsError {
454    MetricsError::RiskError(err.to_string())
455}
456
457/// Squared euclidean distance from `point` to `(x, y)`, computed with checked
458/// arithmetic so a coordinate at the edge of the `Decimal` range reports
459/// instead of aborting.
460fn squared_distance(
461    point: &Point3D,
462    x: Decimal,
463    y: Decimal,
464    op: &'static str,
465) -> Result<Decimal, DecimalError> {
466    let dx = d_sub(point.x, x, op)?;
467    let dy = d_sub(point.y, y, op)?;
468    d_add(powu_checked(dx, 2, op)?, powu_checked(dy, 2, op)?, op)
469}
470
471/// Orders `points` by their squared distance to `(x, y)`, keeping the stable
472/// ordering `sort_by` produced while moving the arithmetic out of the
473/// comparator, which has no channel for an overflow.
474fn sort_by_distance<'a>(
475    points: &mut [&'a Point3D],
476    x: Decimal,
477    y: Decimal,
478    op: &'static str,
479) -> Result<(), DecimalError> {
480    let mut keyed: Vec<(Decimal, &'a Point3D)> = Vec::with_capacity(points.len());
481    for point in points.iter() {
482        keyed.push((squared_distance(point, x, y, op)?, point));
483    }
484    keyed.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
485    for (slot, (_, point)) in points.iter_mut().zip(keyed) {
486        *slot = point;
487    }
488    Ok(())
489}
490
491/// Position of a point in the tree, relative to the split
492/// [`nearest`] walks outwards from: a point before the split, counted
493/// backwards from it, sorts ahead of every point from the split on, so
494/// ordering on this reproduces the tree's own order.
495#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
496enum TreeOrder {
497    Before(Reverse<usize>),
498    From(usize),
499}
500
501/// Whether [`squared_distance`] succeeds from `(x, y)` to every point of
502/// `points`.
503///
504/// Every step of that distance is monotone in the magnitude of the
505/// coordinate offsets, and those offsets are bounded by the extreme
506/// abscissas (the first and last points of the tree) and the extreme
507/// ordinates (one pass, comparisons only). If the distance at those bounds
508/// fits, every point's does.
509fn distances_fit(points: &BTreeSet<Point3D>, x: Decimal, y: Decimal, op: &'static str) -> bool {
510    let (Some(first), Some(last)) = (points.first(), points.last()) else {
511        return true;
512    };
513    let (y_min, y_max) = points
514        .iter()
515        .fold((first.y, first.y), |(lo, hi), p| (lo.min(p.y), hi.max(p.y)));
516    let widest = |lo: Decimal, hi: Decimal, at: Decimal| -> Option<Decimal> {
517        let below = d_sub(lo, at, op).ok()?.abs();
518        let above = d_sub(hi, at, op).ok()?.abs();
519        powu_checked(below.max(above), 2, op).ok()
520    };
521    match (widest(first.x, last.x, x), widest(y_min, y_max, y)) {
522        (Some(dx2), Some(dy2)) => d_add(dx2, dy2, op).is_ok(),
523        _ => false,
524    }
525}
526
527/// The first `k` points of `points` in the order [`sort_by_distance`] leaves
528/// them, ties included, without keying and sorting every point (#858).
529///
530/// The sort is stable over the tree's order, so its first `k` are the `k`
531/// smallest `(squared distance, tree position)` pairs. The tree is sorted by
532/// `x`, so this walks outwards from `x` on each side and stops a side at the
533/// first point whose squared abscissa offset alone exceeds the `k`-th best
534/// distance: that offset never shrinks further out, and a squared distance
535/// is never below its squared abscissa offset, so no point beyond can enter.
536/// The comparison is strict, so a point that ties the `k`-th best on
537/// distance is still keyed and its tree position decides.
538///
539/// When some point's distance would overflow, the sort reports the first
540/// such point in tree order; a walk that stops early cannot, so that case
541/// runs the sort itself and returns the same error.
542fn nearest<'a>(
543    points: &'a BTreeSet<Point3D>,
544    x: Decimal,
545    y: Decimal,
546    k: usize,
547    op: &'static str,
548) -> Result<Vec<&'a Point3D>, DecimalError> {
549    if !distances_fit(points, x, y, op) {
550        let mut all: Vec<&Point3D> = points.iter().collect();
551        sort_by_distance(&mut all, x, y, op)?;
552        all.truncate(k);
553        return Ok(all);
554    }
555
556    let mut best: Vec<(Decimal, TreeOrder, &'a Point3D)> = Vec::with_capacity(k);
557    // Offers `point` to `best`; `false` once no point further out on the
558    // same side can make it.
559    let mut offer = |point: &'a Point3D, order: TreeOrder| -> Result<bool, DecimalError> {
560        let full = best.len() >= k;
561        if full {
562            let reach = powu_checked(d_sub(point.x, x, op)?, 2, op)?;
563            match best.last() {
564                Some(&(kth, _, _)) if reach > kth => return Ok(false),
565                None => return Ok(false),
566                Some(_) => {}
567            }
568        }
569        let distance = squared_distance(point, x, y, op)?;
570        let at = best.partition_point(|&(d, o, _)| (d, o) < (distance, order));
571        if at < k {
572            if full {
573                best.pop();
574            }
575            best.insert(at, (distance, order, point));
576        }
577        Ok(true)
578    };
579
580    let split = Point3D {
581        x,
582        y: Decimal::MIN,
583        z: Decimal::MIN,
584    };
585    for (steps, point) in points.range(split..).enumerate() {
586        if !offer(point, TreeOrder::From(steps))? {
587            break;
588        }
589    }
590    for (steps, point) in points.range(..split).rev().enumerate() {
591        if !offer(point, TreeOrder::Before(Reverse(steps)))? {
592            break;
593        }
594    }
595    Ok(best.into_iter().map(|(_, _, point)| point).collect())
596}
597
598/// Reads one entry of a nearest-neighbour window, naming the slot on the
599/// out-of-bounds path instead of panicking.
600fn nth<'a>(
601    points: &[&'a Point3D],
602    index: usize,
603    kind: fn(String) -> InterpolationError,
604) -> Result<&'a Point3D, InterpolationError> {
605    points.get(index).copied().ok_or_else(|| {
606        kind(format!(
607            "neighbour {index} is out of bounds for a window of {} points",
608            points.len()
609        ))
610    })
611}
612
613/// Reads one sample of a sorted metric series, naming the statistic on the
614/// out-of-bounds path instead of panicking.
615/// `3 * len / 4`, the index of the third quartile of `len` sorted samples.
616fn third_quartile_index(len: usize) -> Result<usize, MetricsError> {
617    len.checked_mul(3)
618        .map(|three_len| three_len / 4)
619        .ok_or_else(|| {
620            MetricsError::BasicError(format!("third quartile index of {len} samples overflowed"))
621        })
622}
623
624fn sample_at(
625    values: &[Decimal],
626    index: usize,
627    what: &'static str,
628) -> Result<Decimal, MetricsError> {
629    values.get(index).copied().ok_or_else(|| {
630        MetricsError::BasicError(format!(
631            "{what}: sample {index} is out of bounds for {} values",
632            values.len()
633        ))
634    })
635}
636
637/// Arithmetic mean of a sample. Errors on an empty sample, where the mean is
638/// undefined, and on a sum that leaves the representable range.
639fn mean_of(values: &[Decimal], op: &'static str) -> Result<Decimal, DecimalError> {
640    let sum = d_sum_iter(values.iter().copied(), op)?;
641    d_div(sum, Decimal::from(values.len()), op)
642}
643
644/// Sum of `(value - mean)^order` over a sample: the raw central moment behind
645/// the variance (`order = 2`), skewness (`3`) and kurtosis (`4`).
646fn central_moment(
647    values: &[Decimal],
648    mean: Decimal,
649    order: u64,
650    op: &'static str,
651) -> Result<Decimal, DecimalError> {
652    let mut acc = Decimal::ZERO;
653    for &value in values {
654        let centered = d_sub(value, mean, op)?;
655        acc = d_add(acc, powu_checked(centered, order, op)?, op)?;
656    }
657    Ok(acc)
658}
659
660impl Default for Surface {
661    fn default() -> Self {
662        Self {
663            points: BTreeSet::new(),
664            x_range: (Decimal::ZERO, Decimal::ZERO),
665            y_range: (Decimal::ZERO, Decimal::ZERO),
666        }
667    }
668}
669
670/// Implementation of the `GeometricObject` trait for the `Surface` struct.
671///
672/// This implementation provides functionality to create and manipulate 3D surfaces using points
673/// in three-dimensional space. It supports construction from explicit point collections or
674/// through parametric functions.
675///
676/// # Type Parameters
677/// - Uses `Point3D` as the points that form the surface
678/// - Uses `Point2D` as the parametric input for surface generation
679///
680/// # Methods
681/// - `get_points()`: Retrieves all points in the surface
682/// - `from_vector()`: Constructs a surface from a vector of points
683/// - `construct()`: Creates a surface using different construction methods
684///
685/// # Error Handling
686/// Uses `SurfaceError` for various error conditions, including:
687/// - Empty point collections
688/// - Invalid construction parameters
689/// - Errors during parametric function evaluation
690impl GeometricObject<Point3D, Point2D> for Surface {
691    type Error = SurfaceError;
692
693    /// Returns a borrowed reference to all points in the surface as an ordered set
694    ///
695    /// # Returns
696    /// * `BTreeSet<&Point3D>` - A sorted set containing references to all points
697    ///   that define the surface, maintaining the natural ordering of points
698    ///
699    /// # Example
700    /// ```rust
701    /// use optionstratlib_math::surfaces::{Surface, Point3D};
702    /// use std::collections::BTreeSet;
703    /// use rust_decimal_macros::dec;
704    /// use optionstratlib_math::geometrics::GeometricObject;
705    ///
706    /// // Create a surface with some points
707    /// let mut surface = Surface {
708    ///     points: BTreeSet::new(),
709    ///     x_range: (dec!(0), dec!(10)),
710    ///     y_range: (dec!(0), dec!(10)),
711    /// };
712    ///
713    /// // Add points to the surface
714    /// surface.points.insert(Point3D { x: dec!(1.0), y: dec!(2.0), z: dec!(3.0) });
715    /// surface.points.insert(Point3D { x: dec!(4.0), y: dec!(5.0), z: dec!(6.0) });
716    ///
717    /// // Get references to all points in the surface
718    /// let points = surface.get_points();
719    /// assert_eq!(points.len(), 2);
720    /// ```
721    fn get_points(&self) -> BTreeSet<&Point3D> {
722        self.points.iter().collect()
723    }
724
725    /// Creates a new Surface from a vector of points that can be converted into Point3D objects.
726    ///
727    /// This method constructs a Surface by converting each point in the input vector to a Point3D
728    /// and collecting them into an ordered set. It also calculates the x and y coordinate ranges
729    /// of the points to define the surface's boundaries.
730    ///
731    /// # Type Parameters
732    ///
733    /// * `T`: A type that can be converted into Point3D via the Into trait and can be cloned.
734    ///
735    /// # Parameters
736    ///
737    /// * `points`: A vector of objects that can be converted to Point3D.
738    ///
739    /// # Returns
740    ///
741    /// A new Surface instance containing the converted points and their coordinate ranges.
742    ///
743    /// # Example
744    ///
745    /// ```rust
746    /// use optionstratlib_math::surfaces::{Surface, Point3D};
747    /// use optionstratlib_math::geometrics::GeometricObject;
748    /// use rust_decimal_macros::dec;
749    ///
750    /// // Create points data
751    /// let points = vec![
752    ///     Point3D { x: dec!(1.0), y: dec!(2.0), z: dec!(3.0) },
753    ///     Point3D { x: dec!(4.0), y: dec!(5.0), z: dec!(6.0) }
754    /// ];
755    ///
756    /// // Create a surface from the points
757    /// let surface = Surface::from_vector(points);
758    ///
759    /// // The surface will contain both points and have x_range and y_range calculated automatically
760    /// assert_eq!(surface.points.len(), 2);
761    /// assert_eq!(surface.x_range, (dec!(1.0), dec!(4.0)));
762    /// assert_eq!(surface.y_range, (dec!(2.0), dec!(5.0)));
763    /// ```
764    fn from_vector<T>(points: Vec<T>) -> Self
765    where
766        T: Into<Point3D> + Clone,
767    {
768        let points: BTreeSet<Point3D> = points.into_iter().map(|p| p.into()).collect();
769        let x_range = Self::calculate_range(points.iter().map(|p| p.x));
770        let y_range = Self::calculate_range(points.iter().map(|p| p.y));
771        Surface {
772            points,
773            x_range,
774            y_range,
775        }
776    }
777
778    /// Constructs a Surface from a given construction method.
779    ///
780    /// This function creates a Surface object from either a set of 3D points or a parametric function.
781    ///
782    /// # Parameters
783    /// * `method` - A construction method that can be converted into a `ConstructionMethod<Point3D, Point2D, SurfaceError>`
784    ///
785    /// # Type Parameters
786    /// * `T` - Type that can be converted into a `ConstructionMethod<Point3D, Point2D, SurfaceError>`
787    ///
788    /// # Returns
789    /// * `Result<Self, Self::Error>` - Either a successfully constructed Surface or an error
790    ///
791    /// # Errors
792    /// * `SurfaceError::Point3DError` - If an empty points array is provided
793    /// * `SurfaceError::ConstructionError` - If invalid parameters are provided or the parametric function fails
794    ///
795    /// # Examples
796    ///
797    /// ## Creating from existing points
798    /// ```rust
799    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
800    /// use std::collections::BTreeSet;
801    /// use optionstratlib_math::geometrics::{ConstructionMethod, GeometricObject};
802    /// use optionstratlib_math::surfaces::{Point3D, Surface};
803    /// let points = BTreeSet::from_iter(vec![
804    ///     Point3D::new(0, 0, 0),
805    ///     Point3D::new(1, 0, 1),
806    ///     Point3D::new(0, 1, 1),
807    /// ]);
808    /// let surface = Surface::construct(ConstructionMethod::FromData { points })?;
809    /// # Ok(())
810    /// # }
811    /// ```
812    ///
813    /// ## Creating from a parametric function
814    /// ```rust,no_run
815    /// # fn run() -> Result<(), Box<dyn std::error::Error>> {
816    /// use rust_decimal_macros::dec;
817    /// use optionstratlib_math::curves::Point2D;
818    /// use optionstratlib_math::error::SurfaceError;
819    /// use optionstratlib_math::geometrics::{ConstructionMethod, ConstructionParams, GeometricObject};
820    /// use optionstratlib_math::surfaces::{Point3D, Surface};
821    /// let params = ConstructionParams::D3 {
822    ///     x_start: dec!(-1.0),
823    ///     x_end: dec!(1.0),
824    ///     y_start: dec!(-1.0),
825    ///     y_end: dec!(1.0),
826    ///     x_steps: 20,
827    ///     y_steps: 20,
828    /// };
829    ///
830    /// // Parametric function defining a paraboloid: z = x² + y²
831    /// let f = Box::new(|p: Point2D| -> Result<Point3D, SurfaceError> {
832    ///     Ok(Point3D {
833    ///         x: p.x,
834    ///         y: p.y,
835    ///         z: p.x * p.x + p.y * p.y,
836    ///     })
837    /// });
838    ///
839    /// let surface = Surface::construct(ConstructionMethod::Parametric { f, params })?;
840    /// # Ok(())
841    /// # }
842    /// ```
843    fn construct<T>(method: T) -> Result<Self, Self::Error>
844    where
845        Self: Sized,
846        T: Into<ConstructionMethod<Point3D, Point2D, SurfaceError>>,
847    {
848        let method = method.into();
849        match method {
850            ConstructionMethod::FromData { points } => {
851                if points.is_empty() {
852                    return Err(SurfaceError::Point3DError {
853                        reason: "Empty points array",
854                    });
855                }
856                Ok(Surface::new(points))
857            }
858            ConstructionMethod::Parametric { f, params } => {
859                let (x_start, x_end, y_start, y_end, x_steps, y_steps) = match params {
860                    ConstructionParams::D3 {
861                        x_start,
862                        x_end,
863                        y_start,
864                        y_end,
865                        x_steps,
866                        y_steps,
867                    } => (x_start, x_end, y_start, y_end, x_steps, y_steps),
868                    _ => {
869                        return Err(SurfaceError::ConstructionError(
870                            "Invalid parameters".to_string(),
871                        ));
872                    }
873                };
874                if x_steps == 0 || y_steps == 0 {
875                    return Err(SurfaceError::ConstructionError(
876                        "Parametric construction needs at least one step on each axis".to_string(),
877                    ));
878                }
879                let op = "Surface::construct::step";
880                let x_span = d_sub(x_end, x_start, op).map_err(construction_err)?;
881                let x_step = d_div(x_span, Decimal::from(x_steps), op).map_err(construction_err)?;
882                let y_span = d_sub(y_end, y_start, op).map_err(construction_err)?;
883                let y_step = d_div(y_span, Decimal::from(y_steps), op).map_err(construction_err)?;
884
885                // Wrap f in an Arc so it can be shared across threads
886                let f = Arc::new(f);
887
888                let points: Result<BTreeSet<Point3D>, SurfaceError> = (0..=x_steps)
889                    .into_par_iter()
890                    .flat_map(|i| {
891                        let f = Arc::clone(&f);
892                        (0..=y_steps).into_par_iter().map(move |j| {
893                            let x_offset =
894                                d_mul(x_step, Decimal::from(i), op).map_err(construction_err)?;
895                            // Each axis ends exactly at its end, and no
896                            // coordinate passes it (#799).
897                            let x = grid_coordinate(
898                                d_add(x_start, x_offset, op).map_err(construction_err)?,
899                                x_start,
900                                x_end,
901                                i == x_steps,
902                            );
903                            let y_offset =
904                                d_mul(y_step, Decimal::from(j), op).map_err(construction_err)?;
905                            let y = grid_coordinate(
906                                d_add(y_start, y_offset, op).map_err(construction_err)?,
907                                y_start,
908                                y_end,
909                                j == y_steps,
910                            );
911                            let t = Point2D::new(x, y);
912                            f(t)
913                        })
914                    })
915                    .collect();
916
917                points.map(Surface::new)
918            }
919        }
920    }
921}
922
923/// Implementation of the `Index` trait for `Surface`, allowing direct indexing access to surface points.
924///
925/// # Overview
926/// This implementation allows you to access individual points in a `Surface` using array-like
927/// indexing notation (e.g., `surface[0]`, `surface[1]`). Points are retrieved in the order they
928/// appear in the underlying `BTreeSet`.
929///
930/// # Panics
931///
932/// Panics if `index >= self.points.len()`. This matches the documented
933/// contract of [`std::ops::Index`] and of `Vec`. `Index` has no error
934/// channel; for a lookup that reports a missing index, use
935/// `get_points().into_iter().nth(index)`.
936///
937/// # Performance
938/// Note that this implementation uses `iter().nth(index)` which has O(n) time complexity
939/// for `BTreeSet`. For frequent access to points by index, consider using a data structure
940/// with O(1) indexing performance.
941impl Index<usize> for Surface {
942    type Output = Point3D;
943
944    /// Retrieves a reference to a point on the surface at the specified index.
945    ///
946    /// This implementation allows using indexing syntax (e.g., `surface[i]`) to access
947    /// individual points that make up the surface.
948    ///
949    /// # Panics
950    ///
951    /// Panics if `index >= self.points.len()`.
952    fn index(&self, index: usize) -> &Self::Output {
953        // INVARIANT: same as `Curve::index` above — `std::ops::Index`
954        // requires returning `&Self::Output` with no signalling channel,
955        // so panic on out-of-bounds is the documented contract.
956        match self.points.iter().nth(index) {
957            Some(p) => p,
958            None => {
959                // INVARIANT: `std::ops::Index` returns `&Self::Output` and has
960                // no fallible channel; the contract mirrors `Vec::index`. No
961                // library code reaches this arm: every internal lookup goes
962                // through `Surface::point_at`.
963                let len = self.points.len();
964                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
965            }
966        }
967    }
968}
969
970/// Implementation of the `Interpolate` trait for the `Surface` type, enabling
971/// interpolation from 3D surface points to 2D points.
972///
973/// # Overview
974/// This implementation allows a `Surface` object to perform various types of interpolation
975/// (linear, bilinear, cubic, and spline) by projecting 3D points from the surface to 2D points.
976///
977/// # Functionality
978/// By implementing the `Interpolate` trait, `Surface` gains the following capabilities:
979/// - Interpolating between 3D surface points to produce 2D projections
980/// - Finding bracket points for interpolation operations
981/// - Supporting multiple interpolation algorithms through the trait's methods
982///
983/// # Usage Example
984/// ```rust
985/// use rust_decimal_macros::dec;
986/// use optionstratlib_math::surfaces::{Surface, Point3D};
987/// use optionstratlib_math::curves::Point2D;
988/// use optionstratlib_math::geometrics::{Interpolate, InterpolationType};
989///
990/// let surface = Surface::new(Default::default());
991///
992/// // Interpolate a 2D point at a specific position using linear interpolation
993/// let input_point = Point2D { x: dec!(1.5), y: dec!(2.0) };
994/// let result = surface.interpolate(input_point, InterpolationType::Linear);
995/// ```
996///
997/// # Related Traits
998/// This implementation relies on the surface also implementing:
999/// - `LinearInterpolation<Point3D, Point2D>`
1000/// - `BiLinearInterpolation<Point3D, Point2D>`
1001/// - `CubicInterpolation<Point3D, Point2D>`
1002/// - `SplineInterpolation<Point3D, Point2D>`
1003/// - `GeometricObject<Point3D, Point2D>`
1004impl Interpolate<Point3D, Point2D> for Surface {}
1005
1006/// # Linear Interpolation for Surfaces
1007///
1008/// Implementation of the `LinearInterpolation` trait for `Surface` structures, enabling
1009/// interpolation from 2D points to 3D points using barycentric coordinates.
1010///
1011/// ## Overview
1012///
1013/// This implementation allows calculating the height (z-coordinate) of any point within
1014/// the surface's x-y range by using linear interpolation based on the three nearest points
1015/// in the surface. The method employs barycentric coordinate interpolation with triangulation
1016/// of the nearest points.
1017///
1018/// ## Algorithm
1019///
1020/// The interpolation process follows these steps:
1021/// 1. Validate that the input point is within the surface's range
1022/// 2. Check for degenerate cases (all points at same location)
1023/// 3. Check for exact matches with existing points
1024/// 4. Find the three nearest points to the query point
1025/// 5. Calculate barycentric coordinates for the triangle formed by these points
1026/// 6. Interpolate the z-value using the barycentric weights
1027impl LinearInterpolation<Point3D, Point2D> for Surface {
1028    /// ## Parameters
1029    ///
1030    /// * `xy` - A `Point2D` representing the x and y coordinates where interpolation is needed
1031    ///
1032    /// ## Returns
1033    ///
1034    /// * `Result<Point3D, InterpolationError>` - The interpolated 3D point if successful, or an
1035    ///   appropriate error if interpolation cannot be performed
1036    ///
1037    /// ## Errors
1038    ///
1039    /// Returns `InterpolationError::Linear` in the following cases:
1040    /// * When the surface contains only coincident points forming a degenerate triangle
1041    /// * When the query point is outside the surface's x-y range
1042    fn linear_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
1043        let first = match self.points.iter().next() {
1044            Some(p) => p,
1045            None => {
1046                return Err(InterpolationError::Linear(
1047                    "No points in the surface".to_string(),
1048                ));
1049            }
1050        };
1051        let all_same_xy = self.points.iter().all(|p| p.x == first.x && p.y == first.y);
1052
1053        if all_same_xy && (first.x == xy.x && first.y == xy.y) {
1054            return Err(InterpolationError::Linear(
1055                "Degenerate triangle detected".to_string(),
1056            ));
1057        }
1058
1059        if xy.x < self.x_range.0
1060            || xy.x > self.x_range.1
1061            || xy.y < self.y_range.0
1062            || xy.y > self.y_range.1
1063        {
1064            return Err(InterpolationError::Linear(
1065                "Point is outside the surface's range".to_string(),
1066            ));
1067        }
1068
1069        // Check for degenerate triangle before exact match. The tree orders
1070        // on `(x, y, z)`, so every point shares one xy-coordinate exactly
1071        // when the first and the last do.
1072        let last = self.points.last().unwrap_or(first);
1073        if (first.x, first.y) == (last.x, last.y) {
1074            return Err(InterpolationError::Linear(
1075                "Degenerate triangle detected".to_string(),
1076            ));
1077        }
1078
1079        // Check for exact match
1080        if let Some(point) = self.points_at(&xy).next() {
1081            return Ok(*point);
1082        }
1083
1084        // Barycentric interpolation needs a triangle, so a surface with fewer
1085        // than three points has no answer to give.
1086        if self.points.len() < 3 {
1087            return Err(InterpolationError::Linear(
1088                "Need at least three points for linear interpolation".to_string(),
1089            ));
1090        }
1091
1092        let op = "Surface::linear_interpolate";
1093        let nearest_points = nearest(&self.points, xy.x, xy.y, 3, op)
1094            .map_err(interp_err(InterpolationError::Linear))?;
1095
1096        let p1 = nth(&nearest_points, 0, InterpolationError::Linear)?;
1097        let p2 = nth(&nearest_points, 1, InterpolationError::Linear)?;
1098        let p3 = nth(&nearest_points, 2, InterpolationError::Linear)?;
1099
1100        let y23 = d_sub(p2.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
1101        let x13 = d_sub(p1.x, p3.x, op).map_err(interp_err(InterpolationError::Linear))?;
1102        let x32 = d_sub(p3.x, p2.x, op).map_err(interp_err(InterpolationError::Linear))?;
1103        let y13 = d_sub(p1.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
1104        let denominator = d_add(
1105            d_mul(y23, x13, op).map_err(interp_err(InterpolationError::Linear))?,
1106            d_mul(x32, y13, op).map_err(interp_err(InterpolationError::Linear))?,
1107            op,
1108        )
1109        .map_err(interp_err(InterpolationError::Linear))?;
1110        if denominator.is_zero() {
1111            // Three collinear (or coincident) neighbours span no area, so the
1112            // barycentric weights are undefined rather than infinite.
1113            return Err(InterpolationError::Linear(
1114                "Degenerate triangle detected: the three nearest points are collinear".to_string(),
1115            ));
1116        }
1117
1118        let qx3 = d_sub(xy.x, p3.x, op).map_err(interp_err(InterpolationError::Linear))?;
1119        let qy3 = d_sub(xy.y, p3.y, op).map_err(interp_err(InterpolationError::Linear))?;
1120        let y31 = d_sub(p3.y, p1.y, op).map_err(interp_err(InterpolationError::Linear))?;
1121
1122        let w1_num = d_add(
1123            d_mul(y23, qx3, op).map_err(interp_err(InterpolationError::Linear))?,
1124            d_mul(x32, qy3, op).map_err(interp_err(InterpolationError::Linear))?,
1125            op,
1126        )
1127        .map_err(interp_err(InterpolationError::Linear))?;
1128        let w1 = d_div(w1_num, denominator, op).map_err(interp_err(InterpolationError::Linear))?;
1129
1130        let w2_num = d_add(
1131            d_mul(y31, qx3, op).map_err(interp_err(InterpolationError::Linear))?,
1132            d_mul(x13, qy3, op).map_err(interp_err(InterpolationError::Linear))?,
1133            op,
1134        )
1135        .map_err(interp_err(InterpolationError::Linear))?;
1136        let w2 = d_div(w2_num, denominator, op).map_err(interp_err(InterpolationError::Linear))?;
1137
1138        let w3 = d_sub(
1139            d_sub(Decimal::ONE, w1, op).map_err(interp_err(InterpolationError::Linear))?,
1140            w2,
1141            op,
1142        )
1143        .map_err(interp_err(InterpolationError::Linear))?;
1144
1145        let z = d_sum_iter(
1146            [
1147                d_mul(w1, p1.z, op).map_err(interp_err(InterpolationError::Linear))?,
1148                d_mul(w2, p2.z, op).map_err(interp_err(InterpolationError::Linear))?,
1149                d_mul(w3, p3.z, op).map_err(interp_err(InterpolationError::Linear))?,
1150            ],
1151            op,
1152        )
1153        .map_err(interp_err(InterpolationError::Linear))?;
1154
1155        Ok(Point3D::new(xy.x, xy.y, z))
1156    }
1157}
1158
1159impl BiLinearInterpolation<Point3D, Point2D> for Surface {
1160    fn bilinear_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
1161        // Check if we have enough points
1162        if self.points.len() < 4 {
1163            return Err(InterpolationError::Bilinear(
1164                "Need at least four points for bilinear interpolation".to_string(),
1165            ));
1166        }
1167
1168        // Check if the point is within range
1169        if xy.x < self.x_range.0
1170            || xy.x > self.x_range.1
1171            || xy.y < self.y_range.0
1172            || xy.y > self.y_range.1
1173        {
1174            return Err(InterpolationError::Bilinear(
1175                "Point is outside the surface's range".to_string(),
1176            ));
1177        }
1178
1179        // Check for invalid quadrilateral: exactly four heights above `xy`.
1180        // The tree holds no exact duplicate, so those four heights differ.
1181        if self.points_at(&xy).count() == 4 {
1182            return Err(InterpolationError::Bilinear(
1183                "Invalid quadrilateral".to_string(),
1184            ));
1185        }
1186
1187        // For exact matches, return the actual point
1188        if let Some(point) = self.points_at(&xy).next() {
1189            return Ok(*point);
1190        }
1191
1192        // Find the four closest points
1193        let op = "Surface::bilinear_interpolate";
1194        let sorted_points = nearest(&self.points, xy.x, xy.y, 4, op)
1195            .map_err(interp_err(InterpolationError::Bilinear))?;
1196
1197        let closest_points = sorted_points.get(0..4).ok_or_else(|| {
1198            InterpolationError::Bilinear(format!(
1199                "need four neighbours, found {}",
1200                sorted_points.len()
1201            ))
1202        })?;
1203
1204        // Sort points to create a quadrilateral
1205        let mut quad_points: Vec<&Point3D> = closest_points.to_vec();
1206        quad_points.sort_by(|a, b| {
1207            let a_key = (a.y, a.x);
1208            let b_key = (b.y, b.x);
1209            a_key
1210                .partial_cmp(&b_key)
1211                .unwrap_or(std::cmp::Ordering::Equal)
1212        });
1213
1214        // Get the four points for interpolation
1215        let q11 = nth(&quad_points, 0, InterpolationError::Bilinear)?; // Bottom-left point
1216        let q12 = nth(&quad_points, 1, InterpolationError::Bilinear)?; // Bottom-right point
1217        let q21 = nth(&quad_points, 2, InterpolationError::Bilinear)?; // Top-left point
1218        let q22 = nth(&quad_points, 3, InterpolationError::Bilinear)?; // Top-right point
1219
1220        // Calculate normalized coordinates
1221        let x_span = d_sub(q12.x, q11.x, op).map_err(interp_err(InterpolationError::Bilinear))?;
1222        let y_span = d_sub(q21.y, q11.y, op).map_err(interp_err(InterpolationError::Bilinear))?;
1223        if x_span.is_zero() || y_span.is_zero() {
1224            // The quadrilateral has collapsed onto a line, so the normalized
1225            // coordinates are undefined rather than infinite.
1226            return Err(InterpolationError::DegenerateInterval);
1227        }
1228        let x_offset = d_sub(xy.x, q11.x, op).map_err(interp_err(InterpolationError::Bilinear))?;
1229        let x_ratio =
1230            d_div(x_offset, x_span, op).map_err(interp_err(InterpolationError::Bilinear))?;
1231        let y_offset = d_sub(xy.y, q11.y, op).map_err(interp_err(InterpolationError::Bilinear))?;
1232        let y_ratio =
1233            d_div(y_offset, y_span, op).map_err(interp_err(InterpolationError::Bilinear))?;
1234
1235        // Perform bilinear interpolation
1236        let inv_x =
1237            d_sub(Decimal::ONE, x_ratio, op).map_err(interp_err(InterpolationError::Bilinear))?;
1238        let inv_y =
1239            d_sub(Decimal::ONE, y_ratio, op).map_err(interp_err(InterpolationError::Bilinear))?;
1240        let corner = |a: Decimal, b: Decimal, z: Decimal| -> Result<Decimal, InterpolationError> {
1241            let weight = d_mul(a, b, op).map_err(interp_err(InterpolationError::Bilinear))?;
1242            d_mul(weight, z, op).map_err(interp_err(InterpolationError::Bilinear))
1243        };
1244        let z = d_sum_iter(
1245            [
1246                corner(inv_x, inv_y, q11.z)?,
1247                corner(x_ratio, inv_y, q12.z)?,
1248                corner(inv_x, y_ratio, q21.z)?,
1249                corner(x_ratio, y_ratio, q22.z)?,
1250            ],
1251            op,
1252        )
1253        .map_err(interp_err(InterpolationError::Bilinear))?;
1254
1255        Ok(Point3D::new(xy.x, xy.y, z))
1256    }
1257}
1258
1259impl CubicInterpolation<Point3D, Point2D> for Surface {
1260    fn cubic_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
1261        // Check if we have enough points for cubic interpolation
1262        if self.points.len() < 9 {
1263            return Err(InterpolationError::Cubic(
1264                "Need at least nine points for cubic interpolation".to_string(),
1265            ));
1266        }
1267
1268        // Check if the point is within range
1269        if xy.x < self.x_range.0
1270            || xy.x > self.x_range.1
1271            || xy.y < self.y_range.0
1272            || xy.y > self.y_range.1
1273        {
1274            return Err(InterpolationError::Cubic(
1275                "Point is outside the surface's range".to_string(),
1276            ));
1277        }
1278
1279        // Check for exact point match
1280        if let Some(point) = self.points_at(&xy).next() {
1281            return Ok(*point);
1282        }
1283
1284        // Find the 9 closest points for cubic interpolation
1285        let op = "Surface::cubic_interpolate";
1286        let sorted_points = nearest(&self.points, xy.x, xy.y, 9, op)
1287            .map_err(interp_err(InterpolationError::Cubic))?;
1288
1289        let closest_points = sorted_points.get(0..9).ok_or_else(|| {
1290            InterpolationError::Cubic(format!(
1291                "need nine neighbours, found {}",
1292                sorted_points.len()
1293            ))
1294        })?;
1295
1296        // Cubic interpolation requires solving a system of equations
1297        // We'll use a weighted cubic interpolation approach
1298
1299        // Calculate weights based on distance
1300        let mut weights: Vec<Decimal> = Vec::with_capacity(closest_points.len());
1301        for &point in closest_points {
1302            let sq = squared_distance(point, xy.x, xy.y, op)
1303                .map_err(interp_err(InterpolationError::Cubic))?;
1304            let dist = match d_sqrt(sq, op) {
1305                Ok(d) => d,
1306                Err(_) => {
1307                    // sqrt only fails for negative input or a result that
1308                    // cannot be represented as Decimal (overflow). Squared
1309                    // distance is always >= 0, so this is the overflow
1310                    // case — drop the point's contribution by giving it
1311                    // zero weight rather than a misleadingly small one.
1312                    warn!(
1313                        "cubic_interpolate: sqrt failed for operand ({sq}); dropping point from weighting"
1314                    );
1315                    weights.push(Decimal::ZERO);
1316                    continue;
1317                }
1318            };
1319            // The 1e-6 floor keeps a coincident neighbour out of a zero divisor.
1320            let shifted = d_add(dist, Decimal::new(1, 6), op)
1321                .map_err(interp_err(InterpolationError::Cubic))?;
1322            weights.push(
1323                d_div(Decimal::ONE, shifted, op).map_err(interp_err(InterpolationError::Cubic))?,
1324            );
1325        }
1326
1327        // Weighted cubic interpolation
1328        let mut numerator_z = Decimal::ZERO;
1329        let mut denominator = Decimal::ZERO;
1330
1331        for (&point, &weight) in closest_points.iter().zip(weights.iter()) {
1332            // Cubic weight function
1333            let cubic_weight =
1334                powu_checked(weight, 3, op).map_err(interp_err(InterpolationError::Cubic))?;
1335            let contribution =
1336                d_mul(point.z, cubic_weight, op).map_err(interp_err(InterpolationError::Cubic))?;
1337            numerator_z = d_add(numerator_z, contribution, op)
1338                .map_err(interp_err(InterpolationError::Cubic))?;
1339            denominator = d_add(denominator, cubic_weight, op)
1340                .map_err(interp_err(InterpolationError::Cubic))?;
1341        }
1342
1343        // Prevent division by zero
1344        let interpolated_z = if denominator != Decimal::ZERO {
1345            d_div(numerator_z, denominator, op).map_err(interp_err(InterpolationError::Cubic))?
1346        } else {
1347            // Fallback to average if weights are problematic
1348            let zs: Vec<Decimal> = closest_points.iter().map(|p| p.z).collect();
1349            let sum = d_sum_iter(zs.iter().copied(), op)
1350                .map_err(interp_err(InterpolationError::Cubic))?;
1351            d_div(sum, Decimal::from(closest_points.len()), op)
1352                .map_err(interp_err(InterpolationError::Cubic))?
1353        };
1354
1355        Ok(Point3D::new(xy.x, xy.y, interpolated_z))
1356    }
1357}
1358
1359impl SplineInterpolation<Point3D, Point2D> for Surface {
1360    fn spline_interpolate(&self, xy: Point2D) -> Result<Point3D, InterpolationError> {
1361        // Check if we have enough points for spline interpolation
1362        if self.points.len() < 9 {
1363            return Err(InterpolationError::Spline(
1364                "Need at least nine points for spline interpolation".to_string(),
1365            ));
1366        }
1367
1368        // Check if the point is within range
1369        if xy.x < self.x_range.0
1370            || xy.x > self.x_range.1
1371            || xy.y < self.y_range.0
1372            || xy.y > self.y_range.1
1373        {
1374            return Err(InterpolationError::Spline(
1375                "Point is outside the surface's range".to_string(),
1376            ));
1377        }
1378
1379        // Check for exact point match
1380        if let Some(point) = self.points_at(&xy).next() {
1381            return Ok(*point);
1382        }
1383
1384        // Sort points to create a grid-like structure
1385        let mut sorted_points: Vec<&Point3D> = self.points.iter().collect();
1386        sorted_points.sort_by(|a, b| {
1387            let a_key = (a.x, a.y);
1388            let b_key = (b.x, b.y);
1389            a_key
1390                .partial_cmp(&b_key)
1391                .unwrap_or(std::cmp::Ordering::Equal)
1392        });
1393
1394        // Group points by x and y coordinates
1395        let mut x_groups: std::collections::HashMap<Decimal, Vec<&Point3D>> =
1396            std::collections::HashMap::new();
1397        let mut y_groups: std::collections::HashMap<Decimal, Vec<&Point3D>> =
1398            std::collections::HashMap::new();
1399
1400        for &point in &sorted_points {
1401            x_groups.entry(point.x).or_default().push(point);
1402            y_groups.entry(point.y).or_default().push(point);
1403        }
1404
1405        // Prepare data for interpolation
1406        let y_values: Vec<Decimal> = y_groups.keys().cloned().collect();
1407
1408        // Natural cubic spline interpolation
1409        // We'll interpolate in two steps: first along x, then along y
1410
1411        // Interpolate along x for each unique y value
1412        let mut interpolated_x_points: Vec<Point3D> = Vec::new();
1413        for &y in &y_values {
1414            let y_points: Vec<&Point3D> = sorted_points
1415                .iter()
1416                .filter(|&&p| p.y == y)
1417                .cloned()
1418                .collect();
1419
1420            if y_points.len() < 2 {
1421                continue;
1422            }
1423
1424            // Perform cubic spline interpolation along x for this y
1425            let x_interpolated =
1426                self.one_dimensional_spline_interpolation(&y_points, xy.x, |p| p.x, |p| p.z);
1427
1428            if let Ok(z) = x_interpolated {
1429                interpolated_x_points.push(Point3D::new(xy.x, y, z));
1430            }
1431        }
1432
1433        // If no x interpolation points, return error
1434        if interpolated_x_points.is_empty() {
1435            return Err(InterpolationError::Spline(
1436                "Could not interpolate along x-axis".to_string(),
1437            ));
1438        }
1439
1440        // Now interpolate these points along y
1441        let y_interpolated = self.one_dimensional_spline_interpolation(
1442            &interpolated_x_points,
1443            xy.y,
1444            |p| p.y,
1445            |p| p.z,
1446        );
1447
1448        // Return the final interpolated point
1449        y_interpolated.map(|z| Point3D::new(xy.x, xy.y, z))
1450    }
1451}
1452
1453impl Len for Surface {
1454    fn len(&self) -> usize {
1455        self.points.len()
1456    }
1457
1458    fn is_empty(&self) -> bool {
1459        self.points.is_empty()
1460    }
1461}
1462
1463impl MetricsExtractor for Surface {
1464    fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
1465        let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
1466
1467        // An empty surface has no statistics; report the same zeroed set
1468        // `Curve::compute_basic_metrics` already returns for an empty curve.
1469        if z_values.is_empty() {
1470            return Ok(BasicMetrics {
1471                mean: Decimal::ZERO,
1472                median: Decimal::ZERO,
1473                mode: Decimal::ZERO,
1474                std_dev: Decimal::ZERO,
1475            });
1476        }
1477
1478        let op = "Surface::compute_basic_metrics";
1479        let mean = mean_of(&z_values, op).map_err(|e| MetricsError::BasicError(e.to_string()))?;
1480
1481        let mut sorted = z_values.clone();
1482        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1483        let median = sample_at(&sorted, sorted.len() / 2, "median")?;
1484
1485        // Mode calculation using HashMap to count occurrences
1486        let mode = {
1487            let mut freq_map = std::collections::HashMap::new();
1488            for &val in &z_values {
1489                let count = freq_map.entry(val).or_insert(0usize);
1490                *count = count.checked_add(1).ok_or_else(|| {
1491                    MetricsError::BasicError("mode: frequency count overflowed".to_string())
1492                })?;
1493            }
1494            freq_map
1495                .into_iter()
1496                .max_by_key(|&(_, count)| count)
1497                .map(|(val, _)| val)
1498                .unwrap_or(Decimal::ZERO)
1499        };
1500
1501        let sum_sq = central_moment(&z_values, mean, 2, op)
1502            .map_err(|e| MetricsError::BasicError(e.to_string()))?;
1503        let variance = d_div(sum_sq, Decimal::from(z_values.len()), op)
1504            .map_err(|e| MetricsError::BasicError(e.to_string()))?;
1505        // A failed square root is an error, not a zero dispersion (#824).
1506        let std_dev = d_sqrt(variance, op).map_err(|e| MetricsError::BasicError(e.to_string()))?;
1507
1508        Ok(BasicMetrics {
1509            mean,
1510            median,
1511            mode,
1512            std_dev,
1513        })
1514    }
1515
1516    fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
1517        let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
1518
1519        // Skewness and kurtosis need a spread to standardise by; mirror the
1520        // zeroed answer `Curve::compute_shape_metrics` returns below two
1521        // samples.
1522        if z_values.len() < 2 {
1523            return Ok(ShapeMetrics {
1524                skewness: Decimal::ZERO,
1525                kurtosis: Decimal::ZERO,
1526                peaks: vec![],
1527                valleys: vec![],
1528                inflection_points: vec![],
1529            });
1530        }
1531
1532        let op = "Surface::compute_shape_metrics";
1533        let mean = mean_of(&z_values, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1534        let sum_sq = central_moment(&z_values, mean, 2, op)
1535            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1536        let variance = d_div(sum_sq, Decimal::from(z_values.len()), op)
1537            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1538        // A failed square root is an error, not a unit dispersion (#824).
1539        let std_dev = d_sqrt(variance, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1540        if std_dev.is_zero() {
1541            return Err(MetricsError::ShapeError(format!(
1542                "standard deviation ({std_dev}) is too small to compute skewness/kurtosis; the surface is degenerate"
1543            )));
1544        }
1545
1546        let n = Decimal::from(z_values.len());
1547
1548        let skew_num = central_moment(&z_values, mean, 3, op)
1549            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1550        let skew_den = d_mul(
1551            n,
1552            powu_checked(std_dev, 3, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?,
1553            op,
1554        )
1555        .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1556        let skewness =
1557            d_div(skew_num, skew_den, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1558
1559        let kurt_num = central_moment(&z_values, mean, 4, op)
1560            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1561        let kurt_den = d_mul(
1562            n,
1563            powu_checked(std_dev, 4, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?,
1564            op,
1565        )
1566        .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1567        let kurtosis =
1568            d_div(kurt_num, kurt_den, op).map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1569
1570        Ok(ShapeMetrics {
1571            skewness,
1572            kurtosis,
1573            peaks: vec![],
1574            valleys: vec![],
1575            inflection_points: vec![],
1576        })
1577    }
1578
1579    fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
1580        let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
1581
1582        // An empty surface has no quantiles; mirror the zeroed answer
1583        // `Curve::compute_range_metrics` returns for an empty curve.
1584        if z_values.is_empty() {
1585            return Ok(RangeMetrics {
1586                min: Point2D::new(Decimal::ZERO, Decimal::ZERO),
1587                max: Point2D::new(Decimal::ZERO, Decimal::ZERO),
1588                range: Decimal::ZERO,
1589                quartiles: (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
1590                interquartile_range: Decimal::ZERO,
1591            });
1592        }
1593
1594        let mut sorted = z_values.clone();
1595        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1596
1597        let min = sorted.first().copied().unwrap_or(Decimal::ZERO);
1598        let max = sorted.last().copied().unwrap_or(Decimal::ZERO);
1599
1600        let op = "Surface::compute_range_metrics";
1601        let len = sorted.len();
1602        let q1 = sample_at(&sorted, len / 4, "first quartile")?;
1603        let q2 = sample_at(&sorted, len / 2, "median")?;
1604        let q3 = sample_at(&sorted, third_quartile_index(len)?, "third quartile")?;
1605
1606        let range = d_sub(max, min, op).map_err(|e| MetricsError::RangeError(e.to_string()))?;
1607        let iqr = d_sub(q3, q1, op).map_err(|e| MetricsError::RangeError(e.to_string()))?;
1608
1609        Ok(RangeMetrics {
1610            min: Point2D::new(Decimal::ZERO, min),
1611            max: Point2D::new(Decimal::ZERO, max),
1612            range,
1613            quartiles: (q1, q2, q3),
1614            interquartile_range: iqr,
1615        })
1616    }
1617
1618    fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
1619        let points: Vec<Point2D> = self.points.iter().map(|p| Point2D::new(p.x, p.z)).collect();
1620
1621        // Handle surfaces with insufficient points
1622        if points.len() < 2 {
1623            return Ok(TrendMetrics {
1624                slope: Decimal::ZERO,
1625                intercept: Decimal::ZERO,
1626                r_squared: Decimal::ONE,
1627                moving_average: vec![],
1628            });
1629        }
1630
1631        // Linear Regression Calculation
1632        let n = Decimal::from(points.len());
1633        let x_vals: Vec<Decimal> = points.iter().map(|p| p.x).collect();
1634        let z_vals: Vec<Decimal> = points.iter().map(|p| p.y).collect();
1635
1636        let op = "Surface::compute_trend_metrics";
1637        let sum_x = d_sum_iter(x_vals.iter().copied(), op).map_err(trend_err)?;
1638        let sum_z = d_sum_iter(z_vals.iter().copied(), op).map_err(trend_err)?;
1639
1640        // Check for identical points to avoid division by zero
1641        let first_z = sample_at(&z_vals, 0, "trend baseline")?;
1642        let is_identical_points = z_vals.iter().all(|&z| z == first_z);
1643
1644        let (slope, intercept, r_squared) = if is_identical_points {
1645            // All points are the same
1646            (Decimal::ZERO, first_z, Decimal::ONE)
1647        } else {
1648            let regression = || -> Result<(Decimal, Decimal, Decimal), DecimalError> {
1649                let mut sum_xz = Decimal::ZERO;
1650                let mut sum_xx = Decimal::ZERO;
1651                for (x, z) in x_vals.iter().zip(&z_vals) {
1652                    sum_xz = d_add(sum_xz, d_mul(*x, *z, op)?, op)?;
1653                    sum_xx = d_add(sum_xx, d_mul(*x, *x, op)?, op)?;
1654                }
1655
1656                let numerator = d_sub(d_mul(n, sum_xz, op)?, d_mul(sum_x, sum_z, op)?, op)?;
1657                let denominator = d_sub(d_mul(n, sum_xx, op)?, d_mul(sum_x, sum_x, op)?, op)?;
1658                let slope = d_div(numerator, denominator, op)?;
1659                let intercept = d_div(d_sub(sum_z, d_mul(slope, sum_x, op)?, op)?, n, op)?;
1660
1661                // R-squared Calculation
1662                let mean_z = d_div(sum_z, n, op)?;
1663                let sst = central_moment(&z_vals, mean_z, 2, op)?;
1664
1665                let mut ssr = Decimal::ZERO;
1666                for (z, x) in z_vals.iter().zip(&x_vals) {
1667                    let z_predicted = d_add(d_mul(slope, *x, op)?, intercept, op)?;
1668                    let residual = d_sub(*z, z_predicted, op)?;
1669                    ssr = d_add(ssr, powu_checked(residual, 2, op)?, op)?;
1670                }
1671
1672                let r_squared = if sst == Decimal::ZERO {
1673                    Decimal::ONE
1674                } else {
1675                    d_sub(Decimal::ONE, d_div(ssr, sst, op)?, op)?
1676                };
1677
1678                Ok((slope, intercept, r_squared))
1679            };
1680
1681            // A surface whose abscissas all collapse to one value (a single
1682            // column) leaves the ordinary-least-squares denominator at zero,
1683            // where the slope is undefined rather than infinite.
1684            regression().map_err(trend_err)?
1685        };
1686
1687        // Moving Average Calculation
1688        let window_sizes = [3, 5, 7];
1689        let mut moving_average: Vec<Point2D> = Vec::new();
1690        for window in window_sizes {
1691            if window > points.len() {
1692                continue;
1693            }
1694            for window_points in points.windows(window) {
1695                let xs: Vec<Decimal> = window_points.iter().map(|p| p.x).collect();
1696                let ys: Vec<Decimal> = window_points.iter().map(|p| p.y).collect();
1697                let avg_x = mean_of(&xs, op).map_err(trend_err)?;
1698                let avg_y = mean_of(&ys, op).map_err(trend_err)?;
1699                moving_average.push(Point2D::new(avg_x, avg_y));
1700            }
1701        }
1702
1703        Ok(TrendMetrics {
1704            slope,
1705            intercept,
1706            r_squared,
1707            moving_average,
1708        })
1709    }
1710
1711    fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
1712        let z_values: Vec<Decimal> = self.points.iter().map(|p| p.z).collect();
1713
1714        // An empty surface carries no risk; mirror the zeroed answer
1715        // `Curve::compute_risk_metrics` returns for an empty curve.
1716        if z_values.is_empty() {
1717            return Ok(RiskMetrics {
1718                volatility: Decimal::ZERO,
1719                value_at_risk: Decimal::ZERO,
1720                expected_shortfall: Decimal::ZERO,
1721                coefficient_of_variation: Decimal::ZERO,
1722                sharpe_ratio: Decimal::ZERO,
1723            });
1724        }
1725
1726        let op = "Surface::compute_risk_metrics";
1727        let mean = mean_of(&z_values, op).map_err(risk_err)?;
1728        // The population standard deviation, through the helper `Curve`
1729        // uses too, so both report the same volatility for the same values
1730        // (#840).
1731        let volatility = population_std_dev(&z_values, mean, op).map_err(risk_err)?;
1732
1733        // Value at Risk (95% confidence) using parametric method. At zero
1734        // dispersion this is `mean - 1.645 * 0 = mean`, a deterministic level
1735        // rather than an absence of value, so it is computed from the formula
1736        // on every path instead of being short-circuited to zero.
1737        let z_score = dec!(1.645); // 95% confidence interval
1738        let scaled_vol = d_mul(z_score, volatility, op).map_err(risk_err)?;
1739        let var = d_sub(mean, scaled_vol, op).map_err(risk_err)?;
1740
1741        // Expected Shortfall (Conditional VaR) calculation. An empty tail has
1742        // no conditional mean; report zero as `Curve` does.
1743        let tail: Vec<Decimal> = z_values.iter().copied().filter(|&x| x < var).collect();
1744        let expected_shortfall = if tail.is_empty() {
1745            Decimal::ZERO
1746        } else {
1747            mean_of(&tail, op).map_err(risk_err)?
1748        };
1749
1750        // The coefficient of variation: the population standard deviation,
1751        // `volatility` here, over the mean, as `Curve` computes it (#824).
1752        let coefficient_of_variation = coefficient_of_variation(volatility, mean, op)?;
1753
1754        // Sharpe Ratio (assuming risk-free rate of 0). A flat surface has no
1755        // dispersion to divide by, which makes this the one field that is
1756        // genuinely undefined at `volatility == 0`; the others keep their
1757        // deterministic limits.
1758        let sharpe_ratio = if volatility.is_zero() {
1759            Decimal::ZERO
1760        } else {
1761            d_div(mean, volatility, op).map_err(risk_err)?
1762        };
1763
1764        Ok(RiskMetrics {
1765            volatility,
1766            value_at_risk: var,
1767            expected_shortfall,
1768            coefficient_of_variation,
1769            sharpe_ratio,
1770        })
1771    }
1772}
1773
1774impl Arithmetic<Surface> for Surface {
1775    type Error = SurfaceError;
1776
1777    /// Merges a collection of surfaces into one, resampled onto a 51 x 51
1778    /// grid over the intersection of their ranges.
1779    ///
1780    /// Each cell of the grid interpolates every operand and combines the
1781    /// heights with `operation`. An empty slice is an error; a single surface
1782    /// is returned as a clone.
1783    ///
1784    /// # Determinism
1785    ///
1786    /// The same operands in the same order give the same result, digit for
1787    /// digit. Within a cell the heights are accumulated in the order they
1788    /// were passed in, never in an order chosen by the thread pool: `Decimal`
1789    /// multiplication rounds once a product needs more than 28 decimal
1790    /// digits and is therefore not associative, so a parallel reducer would
1791    /// take its bracketing from rayon's chunking and could return different
1792    /// last digits on consecutive runs of one binary over one input.
1793    ///
1794    /// `Max` and `Min` still reduce in parallel; they select a height rather
1795    /// than accumulating one, so no rounding enters, and the same caveat
1796    /// about ties between equal `Decimal` values at different scales applies
1797    /// as it does to `Curve::merge`.
1798    ///
1799    /// # Errors
1800    ///
1801    /// - [`SurfaceError::OperationError`] when no surfaces are supplied or
1802    ///   their ranges do not intersect.
1803    /// - The interpolation error of the operand that failed, and any
1804    ///   checked-arithmetic failure raised while combining the heights.
1805    fn merge(surfaces: &[&Surface], operation: MergeOperation) -> Result<Surface, Self::Error> {
1806        if surfaces.is_empty() {
1807            return Err(SurfaceError::invalid_parameters(
1808                "merge_surfaces",
1809                "No surfaces provided for merging",
1810            ));
1811        }
1812
1813        if let [only] = surfaces {
1814            return Ok((*only).clone());
1815        }
1816
1817        // Find intersection of x,y ranges
1818        let min_x = surfaces
1819            .iter()
1820            .map(|s| s.x_range.0)
1821            .max()
1822            .unwrap_or(Decimal::ZERO);
1823        let max_x = surfaces
1824            .iter()
1825            .map(|s| s.x_range.1)
1826            .min()
1827            .unwrap_or(Decimal::ZERO);
1828        let min_y = surfaces
1829            .iter()
1830            .map(|s| s.y_range.0)
1831            .max()
1832            .unwrap_or(Decimal::ZERO);
1833        let max_y = surfaces
1834            .iter()
1835            .map(|s| s.y_range.1)
1836            .min()
1837            .unwrap_or(Decimal::ZERO);
1838
1839        // Validate ranges
1840        if min_x >= max_x || min_y >= max_y {
1841            return Err(SurfaceError::invalid_parameters(
1842                "merge_surfaces",
1843                "Surfaces have incompatible ranges",
1844            ));
1845        }
1846
1847        // Create interpolation grid
1848        let steps = 50;
1849        let op = "Surface::merge";
1850        let x_span = d_sub(max_x, min_x, op).map_err(construction_err)?;
1851        let x_step = d_div(x_span, Decimal::from(steps), op).map_err(construction_err)?;
1852        let y_span = d_sub(max_y, min_y, op).map_err(construction_err)?;
1853        let y_step = d_div(y_span, Decimal::from(steps), op).map_err(construction_err)?;
1854
1855        let result_points: Result<Vec<Point3D>, SurfaceError> = (0..=steps)
1856            .into_par_iter()
1857            .flat_map(|i| {
1858                (0..=steps).into_par_iter().map(move |j| {
1859                    let x_offset = d_mul(x_step, Decimal::from(i), op).map_err(construction_err)?;
1860                    // The steps are rounded at 28 places, so on a span with a
1861                    // long mantissa `step * steps` can land past the maximum
1862                    // and fail the interpolation (#795). Clamping moves only
1863                    // such a point, so every grid that fit before is unchanged.
1864                    let x = d_add(min_x, x_offset, op)
1865                        .map_err(construction_err)?
1866                        .min(max_x);
1867                    let y_offset = d_mul(y_step, Decimal::from(j), op).map_err(construction_err)?;
1868                    let y = d_add(min_y, y_offset, op)
1869                        .map_err(construction_err)?
1870                        .min(max_y);
1871                    let point = Point2D::new(x, y);
1872
1873                    // Interpolate z values
1874                    let z_values: Result<Vec<Decimal>, SurfaceError> = surfaces
1875                        .iter()
1876                        .map(|surface| {
1877                            surface
1878                                .interpolate(point, InterpolationType::Cubic)
1879                                .map(|point3d| point3d.z)
1880                                .map_err(SurfaceError::from)
1881                        })
1882                        .collect();
1883
1884                    let z_values = z_values?;
1885
1886                    // Apply operation
1887                    let result_z = match operation {
1888                        MergeOperation::Add => {
1889                            d_sum_iter(z_values.iter().copied(), op).map_err(construction_err)?
1890                        }
1891                        MergeOperation::Subtract => {
1892                            let first = z_values.first().cloned().unwrap_or(Decimal::ZERO);
1893                            let remaining_sum = d_sum_iter(z_values.iter().skip(1).copied(), op)
1894                                .map_err(construction_err)?;
1895                            d_sub(first, remaining_sum, op).map_err(construction_err)?
1896                        }
1897                        MergeOperation::Multiply => d_product_iter(z_values.iter().copied(), op)
1898                            .map_err(construction_err)?,
1899                        MergeOperation::Divide => {
1900                            // Division is neither associative nor commutative,
1901                            // so the divisors are folded in sequence from the
1902                            // first value. A parallel fold produces one partial
1903                            // per chunk and its reducer would have to discard
1904                            // all but one of them, dropping both the trailing
1905                            // divisors and any checked-arithmetic error raised
1906                            // inside a discarded chunk.
1907                            let mut divisors = z_values.iter().copied();
1908                            let first = divisors.next().unwrap_or(Decimal::ONE);
1909                            divisors.try_fold(first, |acc, val| {
1910                                if val == Decimal::ZERO {
1911                                    Ok(acc)
1912                                } else {
1913                                    d_div(acc, val, op).map_err(construction_err)
1914                                }
1915                            })?
1916                        }
1917                        MergeOperation::Max => z_values
1918                            .par_iter()
1919                            .cloned()
1920                            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1921                            .unwrap_or(Decimal::ZERO),
1922                        MergeOperation::Min => z_values
1923                            .par_iter()
1924                            .cloned()
1925                            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1926                            .unwrap_or(Decimal::ZERO),
1927                    };
1928
1929                    Ok(Point3D::new(x, y, result_z))
1930                })
1931            })
1932            .collect();
1933
1934        let result_points = result_points?;
1935        Ok(Surface::from_vector(result_points))
1936    }
1937
1938    fn merge_with(
1939        &self,
1940        other: &Surface,
1941        operation: MergeOperation,
1942    ) -> Result<Surface, Self::Error> {
1943        Self::merge(&[self, other], operation)
1944    }
1945}
1946
1947impl AxisOperations<Point3D, Point2D> for Surface {
1948    type Error = SurfaceError;
1949
1950    /// Reports whether any point sits above the xy-coordinate `x`.
1951    ///
1952    /// Answers on the `(x, y)` pair alone, so it says nothing about how many
1953    /// heights are stacked there. On a surface honouring the
1954    /// one-height-per-cell rule of [`Surface::new`] there is at most one.
1955    fn contains_point(&self, x: &Point2D) -> bool {
1956        self.points_at(x).next().is_some()
1957    }
1958
1959    /// Returns the xy-coordinate of every point, in `(x, y, z)` order.
1960    ///
1961    /// A surface holding several heights above one cell yields that cell
1962    /// once per height; the duplicates are visible here rather than dropped.
1963    fn get_index_values(&self) -> Vec<Point2D> {
1964        self.points.iter().map(|p| Point2D::new(p.x, p.y)).collect()
1965    }
1966
1967    /// Returns every `z` above the xy-coordinate `x`, in ascending order.
1968    ///
1969    /// This is the multi-valued reader: unlike [`Self::get_point`], it never
1970    /// picks a survivor. A surface honouring the one-height-per-cell rule of
1971    /// [`Surface::new`] returns at most one value; anything longer says the
1972    /// surface is not a function of its xy-coordinate.
1973    fn get_values(&self, x: Point2D) -> Vec<&Decimal> {
1974        self.points_at(&x).map(|p| &p.z).collect()
1975    }
1976
1977    fn get_closest_point(&self, x: &Point2D) -> Result<&Point3D, Self::Error> {
1978        // Compare squared distances directly: ordering is monotonic on
1979        // non-negative inputs, so the sqrt is unnecessary and would
1980        // introduce a fallback that could otherwise distort ordering. The
1981        // distance is folded explicitly because a coordinate at the edge of
1982        // the `Decimal` range overflows, and `min_by` has no channel for that.
1983        let mut closest: Option<(&Point3D, Decimal)> = None;
1984        for point in &self.points {
1985            let squared = squared_distance(point, x.x, x.y, "Surface::get_closest_point")
1986                .map_err(analysis_err)?;
1987            // `min_by` keeps the first of several equal minima; `<=` here
1988            // preserves that.
1989            match closest {
1990                Some((_, best)) if best <= squared => {}
1991                _ => closest = Some((point, squared)),
1992            }
1993        }
1994
1995        closest
1996            .map(|(point, _)| point)
1997            .ok_or(SurfaceError::Point3DError {
1998                reason: "No points found",
1999            })
2000    }
2001
2002    /// Returns the point above the xy-coordinate `x`, if there is one.
2003    ///
2004    /// Assumes the one-height-per-cell rule of [`Surface::new`]. If the
2005    /// surface breaks it, this returns the *first* match in `(x, y, z)`
2006    /// order, which is the lowest `z` of the stack, and the rest are
2007    /// invisible to the caller. Use [`Self::get_values`] to see all of them.
2008    fn get_point(&self, x: &Point2D) -> Option<&Point3D> {
2009        self.points_at(x).next()
2010    }
2011}
2012
2013impl MergeAxisInterpolate<Point3D, Point2D> for Surface
2014where
2015    Self: Sized,
2016{
2017    /// Resamples both surfaces onto the shared xy-grid of their merged
2018    /// indices.
2019    ///
2020    /// Each result holds one `z` per cell of that grid, so it is a function
2021    /// of its xy-coordinate whatever the inputs were: where a surface
2022    /// already covers a cell, the point kept is the first match in
2023    /// `(x, y, z)` order, so a stack of heights above one cell collapses to
2024    /// its lowest. Aggregate such a surface before merging if a different
2025    /// height is wanted.
2026    fn merge_axis_interpolate(
2027        &self,
2028        other: &Self,
2029        interpolation: InterpolationType,
2030    ) -> Result<(Self, Self), Self::Error> {
2031        // Get merged unique xy-coordinates
2032        let merged_xy_values = self.merge_axis_index(other);
2033
2034        let mut interpolated_self_points = BTreeSet::new();
2035        let mut interpolated_other_points = BTreeSet::new();
2036
2037        for xy in &merged_xy_values {
2038            if self.contains_point(xy) {
2039                let pt = self
2040                    .points_at(xy)
2041                    .next()
2042                    .ok_or_else(|| {
2043                        SurfaceError::AnalysisError(format!(
2044                            "merge_axis_interpolate: missing self point at ({},{}) despite contains_point()",
2045                            xy.x, xy.y
2046                        ))
2047                    })?;
2048                interpolated_self_points.insert(*pt);
2049            } else {
2050                let interpolated_point = self.interpolate(*xy, interpolation)?;
2051                interpolated_self_points.insert(interpolated_point);
2052            }
2053
2054            if other.contains_point(xy) {
2055                let pt = other
2056                    .points_at(xy)
2057                    .next()
2058                    .ok_or_else(|| {
2059                        SurfaceError::AnalysisError(format!(
2060                            "merge_axis_interpolate: missing other point at ({},{}) despite contains_point()",
2061                            xy.x, xy.y
2062                        ))
2063                    })?;
2064                interpolated_other_points.insert(*pt);
2065            } else {
2066                let interpolated_point = other.interpolate(*xy, interpolation)?;
2067                interpolated_other_points.insert(interpolated_point);
2068            }
2069        }
2070
2071        Ok((
2072            Surface::new(interpolated_self_points),
2073            Surface::new(interpolated_other_points),
2074        ))
2075    }
2076}
2077
2078impl GeometricTransformations<Point3D> for Surface {
2079    type Error = SurfaceError;
2080
2081    fn translate(&self, deltas: Vec<&Decimal>) -> Result<Self, Self::Error> {
2082        if deltas.len() != 3 {
2083            return Err(SurfaceError::invalid_parameters(
2084                "translate",
2085                "Expected 3 deltas for 3D translation",
2086            ));
2087        }
2088
2089        let (Some(dx), Some(dy), Some(dz)) = (deltas.first(), deltas.get(1), deltas.get(2)) else {
2090            return Err(SurfaceError::invalid_parameters(
2091                "translate",
2092                "Expected 3 deltas for 3D translation",
2093            ));
2094        };
2095
2096        let translated_points = self
2097            .points
2098            .iter()
2099            .map(|point| {
2100                let x = d_add(point.x, **dx, "Surface::translate::x")?;
2101                let y = d_add(point.y, **dy, "Surface::translate::y")?;
2102                let z = d_add(point.z, **dz, "Surface::translate::z")?;
2103                Ok(Point3D::new(x, y, z))
2104            })
2105            .collect::<Result<BTreeSet<Point3D>, DecimalError>>()
2106            .map_err(construction_err)?;
2107
2108        Ok(Surface::new(translated_points))
2109    }
2110
2111    fn scale(&self, factors: Vec<&Decimal>) -> Result<Self, Self::Error> {
2112        if factors.len() != 3 {
2113            return Err(SurfaceError::invalid_parameters(
2114                "scale",
2115                "Expected 3 factors for 3D scaling",
2116            ));
2117        }
2118
2119        let (Some(fx), Some(fy), Some(fz)) = (factors.first(), factors.get(1), factors.get(2))
2120        else {
2121            return Err(SurfaceError::invalid_parameters(
2122                "scale",
2123                "Expected 3 factors for 3D scaling",
2124            ));
2125        };
2126
2127        let scaled_points = self
2128            .points
2129            .iter()
2130            .map(|point| {
2131                let x = d_mul(point.x, **fx, "Surface::scale::x")?;
2132                let y = d_mul(point.y, **fy, "Surface::scale::y")?;
2133                let z = d_mul(point.z, **fz, "Surface::scale::z")?;
2134                Ok(Point3D::new(x, y, z))
2135            })
2136            .collect::<Result<BTreeSet<Point3D>, DecimalError>>()
2137            .map_err(construction_err)?;
2138
2139        Ok(Surface::new(scaled_points))
2140    }
2141
2142    fn intersect_with(&self, other: &Self) -> Result<Vec<Point3D>, Self::Error> {
2143        let mut intersections = Vec::new();
2144        let epsilon = Decimal::new(1, 6); // 0.000001 tolerance
2145        let op = "Surface::intersect_with";
2146
2147        for p1 in self.points.iter() {
2148            for p2 in other.points.iter() {
2149                let dx = d_sub(p1.x, p2.x, op).map_err(analysis_err)?.abs();
2150                if dx >= epsilon {
2151                    continue;
2152                }
2153                let dy = d_sub(p1.y, p2.y, op).map_err(analysis_err)?.abs();
2154                if dy >= epsilon {
2155                    continue;
2156                }
2157                let dz = d_sub(p1.z, p2.z, op).map_err(analysis_err)?.abs();
2158                if dz < epsilon {
2159                    intersections.push(*p1);
2160                }
2161            }
2162        }
2163
2164        Ok(intersections)
2165    }
2166
2167    fn derivative_at(&self, point: &Point3D) -> Result<Vec<Decimal>, Self::Error> {
2168        // Handle surfaces with insufficient points
2169        if self.points.len() < 2 {
2170            return Err(SurfaceError::invalid_parameters(
2171                "derivative_at",
2172                "Surface needs at least 2 points for derivative calculation",
2173            ));
2174        }
2175
2176        let op = "Surface::derivative_at";
2177
2178        // For surfaces with exactly 2 or 3 points, use a simple approach
2179        if self.points.len() <= 3 {
2180            let p0 = self.point_at(0)?;
2181            let p1 = self.point_at(1)?;
2182
2183            // Ensure points are not identical
2184            if p0 == p1 {
2185                return Err(SurfaceError::invalid_parameters(
2186                    "derivative_at",
2187                    "Points are identical, cannot calculate derivatives",
2188                ));
2189            }
2190
2191            // Calculate derivatives using the first two points. `Decimal::MAX`
2192            // is the pre-existing sentinel for a collapsed axis.
2193            let rise = d_sub(p1.z, p0.z, op).map_err(analysis_err)?;
2194            let run_x = d_sub(p1.x, p0.x, op).map_err(analysis_err)?;
2195            let dx = if run_x == Decimal::ZERO {
2196                Decimal::MAX
2197            } else {
2198                d_div(rise, run_x, op).map_err(analysis_err)?
2199            };
2200
2201            let run_y = d_sub(p1.y, p0.y, op).map_err(analysis_err)?;
2202            let dy = if run_y == Decimal::ZERO {
2203                Decimal::MAX
2204            } else {
2205                d_div(rise, run_y, op).map_err(analysis_err)?
2206            };
2207
2208            return Ok(vec![dx, dy]);
2209        }
2210
2211        if !(self.x_range.0..=self.x_range.1).contains(&point.x)
2212            || !(self.y_range.0..=self.y_range.1).contains(&point.y)
2213        {
2214            return Err(SurfaceError::invalid_parameters(
2215                "derivative_at",
2216                "Point is outside the surface's range",
2217            ));
2218        }
2219
2220        // For more complex surfaces, find nearby points
2221        let tolerance = dec!(0.5);
2222
2223        let mut x_points: BTreeSet<Point3D> = BTreeSet::new();
2224        let mut y_points: BTreeSet<Point3D> = BTreeSet::new();
2225        for candidate in self.get_points() {
2226            if d_sub(candidate.x, point.x, op).map_err(analysis_err)?.abs() < tolerance {
2227                x_points.insert(*candidate);
2228            }
2229            if d_sub(candidate.y, point.y, op).map_err(analysis_err)?.abs() < tolerance {
2230                y_points.insert(*candidate);
2231            }
2232        }
2233
2234        // If not enough nearby points, use the entire surface
2235        let x_candidates = if x_points.len() < 2 {
2236            &self.points
2237        } else {
2238            &x_points
2239        };
2240        let y_candidates = if y_points.len() < 2 {
2241            &self.points
2242        } else {
2243            &y_points
2244        };
2245
2246        // Ensure we have at least 2 points
2247        if x_candidates.len() < 2 || y_candidates.len() < 2 {
2248            return Err(SurfaceError::invalid_parameters(
2249                "derivative_at",
2250                "Could not find suitable points for derivative calculation",
2251            ));
2252        }
2253
2254        // Sort and find derivatives
2255        let mut x_sorted: Vec<_> = x_candidates.iter().collect();
2256        x_sorted.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
2257
2258        let mut y_sorted: Vec<_> = y_candidates.iter().collect();
2259        y_sorted.sort_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal));
2260
2261        let missing = || {
2262            SurfaceError::AnalysisError(
2263                "derivative_at: fewer than two candidates after filtering".to_string(),
2264            )
2265        };
2266        let (Some(x0), Some(x1)) = (x_sorted.first(), x_sorted.get(1)) else {
2267            return Err(missing());
2268        };
2269        let (Some(y0), Some(y1)) = (y_sorted.first(), y_sorted.get(1)) else {
2270            return Err(missing());
2271        };
2272
2273        // Prevent division by zero
2274        let dx = if x0.x == x1.x {
2275            Decimal::ZERO
2276        } else {
2277            let rise = d_sub(x1.z, x0.z, op).map_err(analysis_err)?;
2278            let run = d_sub(x1.x, x0.x, op).map_err(analysis_err)?;
2279            d_div(rise, run, op).map_err(analysis_err)?
2280        };
2281
2282        let dy = if y0.y == y1.y {
2283            Decimal::ZERO
2284        } else {
2285            let rise = d_sub(y1.z, y0.z, op).map_err(analysis_err)?;
2286            let run = d_sub(y1.y, y0.y, op).map_err(analysis_err)?;
2287            d_div(rise, run, op).map_err(analysis_err)?
2288        };
2289
2290        Ok(vec![dx, dy])
2291    }
2292
2293    fn extrema(&self) -> Result<(Point3D, Point3D), Self::Error> {
2294        if self.points.is_empty() {
2295            return Err(SurfaceError::invalid_parameters(
2296                "extrema",
2297                "Surface has no points",
2298            ));
2299        }
2300
2301        let min_point = self
2302            .points
2303            .iter()
2304            .min_by(|a, b| a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
2305            .cloned()
2306            .ok_or_else(|| {
2307                SurfaceError::AnalysisError("extrema: empty point set in min_by".to_string())
2308            })?;
2309
2310        let max_point = self
2311            .points
2312            .iter()
2313            .max_by(|a, b| a.z.partial_cmp(&b.z).unwrap_or(std::cmp::Ordering::Equal))
2314            .cloned()
2315            .ok_or_else(|| {
2316                SurfaceError::AnalysisError("extrema: empty point set in max_by".to_string())
2317            })?;
2318
2319        Ok((min_point, max_point))
2320    }
2321
2322    fn measure_under(&self, base_value: &Decimal) -> Result<Decimal, Self::Error> {
2323        if self.points.len() < 3 {
2324            return Ok(Decimal::ZERO);
2325        }
2326
2327        // Approximate volume using triangular prisms
2328        let mut volume = Decimal::ZERO;
2329        let points: Vec<_> = self.points.iter().collect();
2330
2331        let op = "Surface::measure_under";
2332
2333        // For each possible triangle in the surface
2334        for window in points.windows(3) {
2335            // Calculate area of triangle
2336            let (Some(p1), Some(p2), Some(p3)) = (window.first(), window.get(1), window.get(2))
2337            else {
2338                return Err(SurfaceError::AnalysisError(
2339                    "measure_under: triangle window is shorter than three points".to_string(),
2340                ));
2341            };
2342
2343            let cross_a = d_mul(
2344                d_sub(p2.x, p1.x, op).map_err(analysis_err)?,
2345                d_sub(p3.y, p1.y, op).map_err(analysis_err)?,
2346                op,
2347            )
2348            .map_err(analysis_err)?;
2349            let cross_b = d_mul(
2350                d_sub(p3.x, p1.x, op).map_err(analysis_err)?,
2351                d_sub(p2.y, p1.y, op).map_err(analysis_err)?,
2352                op,
2353            )
2354            .map_err(analysis_err)?;
2355            let cross = d_sub(cross_a, cross_b, op).map_err(analysis_err)?;
2356            let area = d_div(cross.abs(), dec!(2), op).map_err(analysis_err)?;
2357
2358            // Average height from base_value
2359            let heights = d_sum_iter(
2360                [
2361                    d_sub(p1.z, *base_value, op).map_err(analysis_err)?,
2362                    d_sub(p2.z, *base_value, op).map_err(analysis_err)?,
2363                    d_sub(p3.z, *base_value, op).map_err(analysis_err)?,
2364                ],
2365                op,
2366            )
2367            .map_err(analysis_err)?;
2368            let avg_height = d_div(heights, dec!(3), op).map_err(analysis_err)?;
2369
2370            let prism = d_mul(area, avg_height, op).map_err(analysis_err)?;
2371            volume = d_add(volume, prism, op).map_err(analysis_err)?;
2372        }
2373
2374        Ok(volume.abs())
2375    }
2376}
2377
2378#[cfg(test)]
2379mod tests_surface_basic {
2380    use super::*;
2381    use rust_decimal_macros::dec;
2382
2383    // Helper function to create test points
2384    fn create_test_points() -> BTreeSet<Point3D> {
2385        BTreeSet::from_iter(vec![
2386            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2387            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
2388            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
2389            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2390            Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
2391        ])
2392    }
2393
2394    #[test]
2395    fn test_surface_new() {
2396        let points = create_test_points();
2397        let surface = Surface::new(points.clone());
2398
2399        // Check points are correctly stored
2400        assert_eq!(surface.points, points);
2401
2402        // Check x range calculation
2403        assert_eq!(surface.x_range.0, dec!(0.0));
2404        assert_eq!(surface.x_range.1, dec!(1.0));
2405
2406        // Check y range calculation
2407        assert_eq!(surface.y_range.0, dec!(0.0));
2408        assert_eq!(surface.y_range.1, dec!(1.0));
2409    }
2410
2411    /// Projecting out `x` maps `(x, y, z)` to `(y, z)`, which is multi-valued
2412    /// on a grid: the test surface has two points at `y = 0` and two at
2413    /// `y = 1`, so the projection has two ordinates above abscissa `0` and
2414    /// two above abscissa `1`.
2415    ///
2416    /// All five are asserted by exact contents, not by membership probes.
2417    /// Under the pre-#450 `Point2D` contract, `collect` into the `BTreeSet`
2418    /// of the old `Curve` return type kept only three of them, and the
2419    /// probes passed anyway because `x`-only equality made `(0, 1)` match a
2420    /// probe for `(0, 0)`.
2421    #[test]
2422    fn test_project_onto_x_axis_keeps_every_projected_point() {
2423        let points = create_test_points();
2424        let surface = Surface::new(points);
2425        let projection = surface.project_onto(Axis::X);
2426
2427        assert_eq!(
2428            projection,
2429            vec![
2430                Point2D::new(dec!(0.0), dec!(0.0)),
2431                Point2D::new(dec!(0.0), dec!(1.0)),
2432                Point2D::new(dec!(0.5), dec!(1.5)),
2433                Point2D::new(dec!(1.0), dec!(1.0)),
2434                Point2D::new(dec!(1.0), dec!(2.0)),
2435            ]
2436        );
2437        assert_eq!(projection.len(), surface.points.len());
2438
2439        let points = match surface.get_f64_points() {
2440            Ok(points) => points,
2441            Err(e) => panic!("finite coordinates convert: {e:?}"),
2442        };
2443        assert_eq!(points.len(), 5);
2444        assert_eq!(points[0].0, 0.0);
2445        assert_eq!(points[0].1, 0.0);
2446        assert_eq!(points[0].2, 0.0);
2447
2448        let default = Surface::default();
2449        assert_eq!(default.points.len(), 0);
2450        assert_eq!(default.x_range, (Decimal::ZERO, Decimal::ZERO));
2451        assert_eq!(default.y_range, (Decimal::ZERO, Decimal::ZERO));
2452    }
2453
2454    /// Projecting out `y` maps `(x, y, z)` to `(x, z)`, multi-valued on a
2455    /// grid for the same reason as the X-axis case above: two heights above
2456    /// `x = 0` and two above `x = 1`.
2457    #[test]
2458    fn test_project_onto_y_axis_keeps_every_projected_point() {
2459        let points = create_test_points();
2460        let surface = Surface::new(points);
2461        let projection = surface.project_onto(Axis::Y);
2462
2463        assert_eq!(
2464            projection,
2465            vec![
2466                Point2D::new(dec!(0.0), dec!(0.0)),
2467                Point2D::new(dec!(0.0), dec!(1.0)),
2468                Point2D::new(dec!(0.5), dec!(1.5)),
2469                Point2D::new(dec!(1.0), dec!(1.0)),
2470                Point2D::new(dec!(1.0), dec!(2.0)),
2471            ]
2472        );
2473        assert_eq!(projection.len(), surface.points.len());
2474    }
2475
2476    /// Projecting out `z` maps `(x, y, z)` to `(x, y)`: the xy-footprint of
2477    /// the surface, which on a grid has several ordinates per abscissa by
2478    /// construction.
2479    #[test]
2480    fn test_project_onto_z_axis_keeps_every_projected_point() {
2481        let points = create_test_points();
2482        let surface = Surface::new(points);
2483        let projection = surface.project_onto(Axis::Z);
2484
2485        assert_eq!(
2486            projection,
2487            vec![
2488                Point2D::new(dec!(0.0), dec!(0.0)),
2489                Point2D::new(dec!(0.0), dec!(1.0)),
2490                Point2D::new(dec!(0.5), dec!(0.5)),
2491                Point2D::new(dec!(1.0), dec!(0.0)),
2492                Point2D::new(dec!(1.0), dec!(1.0)),
2493            ]
2494        );
2495        assert_eq!(projection.len(), surface.points.len());
2496    }
2497
2498    /// Two surface points that agree on the two surviving coordinates
2499    /// project onto the very same 2D point. A set keeps one of them; the
2500    /// `Vec` this method returns keeps both, which is the loss the return
2501    /// type exists to prevent.
2502    #[test]
2503    fn test_project_onto_coincident_images_keeps_both_points() {
2504        // `z` depends on `y` alone, so projecting out `x` maps both rows of
2505        // the grid onto the same two points.
2506        let surface = Surface::new(BTreeSet::from_iter(vec![
2507            Point3D::new(dec!(0.0), dec!(0.0), dec!(7.0)),
2508            Point3D::new(dec!(0.0), dec!(1.0), dec!(9.0)),
2509            Point3D::new(dec!(1.0), dec!(0.0), dec!(7.0)),
2510            Point3D::new(dec!(1.0), dec!(1.0), dec!(9.0)),
2511        ]));
2512
2513        let projection = surface.project_onto(Axis::X);
2514
2515        assert_eq!(projection.len(), 4);
2516        assert_eq!(
2517            projection,
2518            vec![
2519                Point2D::new(dec!(0.0), dec!(7.0)),
2520                Point2D::new(dec!(0.0), dec!(7.0)),
2521                Point2D::new(dec!(1.0), dec!(9.0)),
2522                Point2D::new(dec!(1.0), dec!(9.0)),
2523            ]
2524        );
2525
2526        // The old return type collected into a `BTreeSet`, which is where
2527        // the two lost points went.
2528        let as_set: BTreeSet<Point2D> = projection.into_iter().collect();
2529        assert_eq!(as_set.len(), 2);
2530    }
2531
2532    #[test]
2533    fn test_one_dimensional_spline_interpolation_basic() {
2534        let surface = Surface::new(create_test_points());
2535
2536        // Create test points for interpolation
2537        let test_points = vec![
2538            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2539            Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
2540            Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
2541        ];
2542
2543        // Test interpolation at different points
2544        let test_cases = vec![
2545            (dec!(0.25), dec!(0.5)), // Midpoint
2546            (dec!(0.0), dec!(0.0)),  // Start point
2547            (dec!(1.0), dec!(2.0)),  // End point
2548            (dec!(0.75), dec!(1.5)), // Another point
2549        ];
2550
2551        for (target, expected) in test_cases {
2552            let result = surface
2553                .one_dimensional_spline_interpolation(&test_points, target, |p| p.x, |p| p.z)
2554                .unwrap();
2555
2556            // Allow small deviation due to interpolation
2557            assert!(
2558                (result - expected).abs() < dec!(0.1),
2559                "Failed for target {target}, expected {expected}, got {result}"
2560            );
2561        }
2562    }
2563
2564    #[test]
2565    fn test_one_dimensional_spline_interpolation_insufficient_points() {
2566        let surface = Surface::new(create_test_points());
2567
2568        // Single point is insufficient for interpolation
2569        let test_points = vec![Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0))];
2570
2571        let result =
2572            surface.one_dimensional_spline_interpolation(&test_points, dec!(0.5), |p| p.x, |p| p.z);
2573
2574        assert!(matches!(
2575            result,
2576            Err(InterpolationError::Spline(msg)) if msg.contains("Insufficient points")
2577        ));
2578    }
2579
2580    #[test]
2581    fn test_one_dimensional_spline_interpolation_out_of_range() {
2582        let surface = Surface::new(create_test_points());
2583
2584        let test_points = vec![
2585            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2586            Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
2587        ];
2588
2589        // Test points outside the point range
2590        let out_of_range_cases = vec![
2591            (dec!(-0.5), dec!(0.0)), // Below minimum
2592            (dec!(1.5), dec!(2.0)),  // Above maximum
2593        ];
2594
2595        for (target, expected) in out_of_range_cases {
2596            let result = surface
2597                .one_dimensional_spline_interpolation(&test_points, target, |p| p.x, |p| p.z)
2598                .unwrap();
2599
2600            // Should return endpoints for out-of-range values
2601            assert_eq!(result, expected, "Failed for out-of-range target {target}");
2602        }
2603    }
2604}
2605
2606#[cfg(test)]
2607mod tests_surface_geometric_object {
2608    use super::*;
2609    use rust_decimal_macros::dec;
2610    /// A generator failure owned by the caller, standing in for an error of
2611    /// a higher layer (a `ChainError`, say) that math cannot name.
2612    #[derive(Debug, thiserror::Error)]
2613    #[error("{0}")]
2614    struct GeneratorFailure(&'static str);
2615
2616    // Helper function to create test points
2617    fn create_test_points() -> Vec<Point3D> {
2618        vec![
2619            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2620            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
2621            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
2622            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2623        ]
2624    }
2625
2626    #[test]
2627    fn test_get_points() {
2628        let points = create_test_points();
2629        let surface = Surface::from_vector(points.clone());
2630
2631        let retrieved_points: Vec<&Point3D> = surface.get_points().into_iter().collect();
2632
2633        assert_eq!(retrieved_points.len(), points.len());
2634        for point in &points {
2635            assert!(retrieved_points.contains(&point));
2636        }
2637    }
2638
2639    #[test]
2640    fn test_from_vector() {
2641        let points = create_test_points();
2642        let surface = Surface::from_vector(points.clone());
2643
2644        assert_eq!(surface.points.len(), points.len());
2645
2646        // Check x and y ranges
2647        assert_eq!(surface.x_range.0, dec!(0.0));
2648        assert_eq!(surface.x_range.1, dec!(1.0));
2649        assert_eq!(surface.y_range.0, dec!(0.0));
2650        assert_eq!(surface.y_range.1, dec!(1.0));
2651    }
2652
2653    #[test]
2654    fn test_construct_from_data() {
2655        let points = BTreeSet::from_iter(create_test_points());
2656        let result = Surface::construct(ConstructionMethod::FromData { points });
2657
2658        assert!(result.is_ok());
2659        let surface = result.unwrap();
2660        assert_eq!(surface.points.len(), 4);
2661    }
2662
2663    #[test]
2664    fn test_construct_from_data_empty() {
2665        let points: BTreeSet<Point3D> = BTreeSet::new();
2666        let result = Surface::construct(ConstructionMethod::FromData { points });
2667
2668        assert!(matches!(
2669            result,
2670            Err(SurfaceError::Point3DError { reason: _ })
2671        ));
2672    }
2673
2674    #[test]
2675    fn test_construct_parametric() {
2676        // Create a simple parametric function that creates a basic surface
2677        let parametric_func: Box<dyn Fn(Point2D) -> Result<Point3D, SurfaceError> + Send + Sync> =
2678            Box::new(move |t: Point2D| -> Result<Point3D, SurfaceError> {
2679                Ok(Point3D::new(
2680                    t.x,
2681                    t.y,
2682                    t.x * t.y, // Simple z = x * y surface
2683                ))
2684            });
2685
2686        let params = ConstructionParams::D3 {
2687            x_start: dec!(0.0),
2688            x_end: dec!(1.0),
2689            y_start: dec!(0.0),
2690            y_end: dec!(1.0),
2691            x_steps: 2,
2692            y_steps: 2,
2693        };
2694
2695        let result = Surface::construct(ConstructionMethod::Parametric {
2696            f: parametric_func,
2697            params,
2698        });
2699
2700        assert!(result.is_ok());
2701        let surface = result.unwrap();
2702
2703        // Should have (x_steps + 1) * (y_steps + 1) points
2704        assert_eq!(surface.points.len(), 9); // 3x3 grid
2705    }
2706
2707    #[test]
2708    fn test_construct_parametric_invalid_params() {
2709        let parametric_func: Box<dyn Fn(Point2D) -> Result<Point3D, SurfaceError> + Send + Sync> =
2710            Box::new(move |_: Point2D| -> Result<Point3D, SurfaceError> {
2711                Ok(Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)))
2712            });
2713
2714        // Use incorrect parameters to trigger error
2715        let params = ConstructionParams::D2 {
2716            t_start: Decimal::ZERO,
2717            t_end: Decimal::ONE,
2718            steps: 2,
2719        };
2720
2721        let result = Surface::construct(ConstructionMethod::Parametric {
2722            f: parametric_func,
2723            params,
2724        });
2725
2726        assert!(matches!(result, Err(SurfaceError::ConstructionError(_))));
2727    }
2728
2729    #[test]
2730    fn test_construct_parametric_error_handling() {
2731        // Parametric function that sometimes fails
2732        let parametric_func: Box<dyn Fn(Point2D) -> Result<Point3D, SurfaceError> + Send + Sync> =
2733            Box::new(move |t: Point2D| -> Result<Point3D, SurfaceError> {
2734                if t.x > dec!(0.5) && t.y > dec!(0.5) {
2735                    Err(SurfaceError::generator(GeneratorFailure("Test error")))
2736                } else {
2737                    Ok(Point3D::new(t.x, t.y, t.x * t.y))
2738                }
2739            });
2740
2741        let params = ConstructionParams::D3 {
2742            x_start: dec!(0.0),
2743            x_end: dec!(1.0),
2744            y_start: dec!(0.0),
2745            y_end: dec!(1.0),
2746            x_steps: 2,
2747            y_steps: 2,
2748        };
2749
2750        let result = Surface::construct(ConstructionMethod::Parametric {
2751            f: parametric_func,
2752            params,
2753        });
2754
2755        // The generator's own error survives: still the caller's typed
2756        // error, not a message (roadmap M1-10).
2757        match result {
2758            Err(SurfaceError::Generator(source)) => {
2759                let cause = source
2760                    .downcast_ref::<GeneratorFailure>()
2761                    .expect("the generator's own error must survive boxing");
2762                assert!(cause.to_string().contains("Test error"));
2763            }
2764            other => panic!("expected SurfaceError::Generator, got {other:?}"),
2765        }
2766    }
2767
2768    #[test]
2769    fn test_range_calculation() {
2770        let points = create_test_points();
2771        let surface = Surface::from_vector(points);
2772
2773        // Verify x and y ranges
2774        assert_eq!(surface.x_range.0, dec!(0.0));
2775        assert_eq!(surface.x_range.1, dec!(1.0));
2776        assert_eq!(surface.y_range.0, dec!(0.0));
2777        assert_eq!(surface.y_range.1, dec!(1.0));
2778    }
2779}
2780
2781#[cfg(test)]
2782mod tests_surface_linear_interpolation {
2783    use super::*;
2784    use rust_decimal_macros::dec;
2785
2786    fn create_test_surface() -> Surface {
2787        let points = BTreeSet::from_iter(vec![
2788            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2789            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
2790            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
2791            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2792        ]);
2793        Surface::new(points)
2794    }
2795
2796    #[test]
2797    fn test_point_out_of_range() {
2798        let surface = create_test_surface();
2799        let result = surface.linear_interpolate(Point2D::new(dec!(-1.0), dec!(0.5)));
2800        assert!(matches!(
2801            result,
2802            Err(InterpolationError::Linear(msg)) if msg.contains("outside the surface's range")
2803        ));
2804    }
2805
2806    #[test]
2807    fn test_exact_point_match() {
2808        let surface = create_test_surface();
2809        let result = surface
2810            .linear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
2811            .unwrap();
2812        assert_eq!(result.z, dec!(0.0));
2813    }
2814
2815    #[test]
2816    fn test_midpoint_interpolation() {
2817        let surface = create_test_surface();
2818        let result = surface
2819            .linear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
2820            .unwrap();
2821        assert_eq!(result.z, dec!(1.0));
2822    }
2823
2824    #[test]
2825    fn test_quarter_point_interpolation() {
2826        let surface = create_test_surface();
2827        let result = surface
2828            .linear_interpolate(Point2D::new(dec!(0.25), dec!(0.25)))
2829            .unwrap();
2830        // El valor debe estar entre 0.0 y 1.0
2831        assert!(result.z > dec!(0.0) && result.z < dec!(1.0));
2832    }
2833
2834    #[test]
2835    fn test_degenerate_triangle() {
2836        let points = BTreeSet::from_iter(vec![
2837            Point3D::new(dec!(1.0), dec!(1.0), dec!(0.0)),
2838            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
2839            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2840        ]);
2841        let surface = Surface::new(points);
2842        let result = surface.linear_interpolate(Point2D::new(dec!(1.0), dec!(1.0)));
2843        assert!(matches!(
2844            result,
2845            Err(InterpolationError::Linear(msg)) if msg.contains("Degenerate triangle")
2846        ));
2847    }
2848
2849    #[test]
2850    fn test_boundary_interpolation() {
2851        let surface = create_test_surface();
2852        // Test interpolation on the edge
2853        let result = surface
2854            .linear_interpolate(Point2D::new(dec!(0.0), dec!(0.5)))
2855            .unwrap();
2856        assert_eq!(result.z, dec!(0.5));
2857    }
2858
2859    #[test]
2860    fn test_uniform_gradient() {
2861        // Crear una superficie con un gradiente uniforme
2862        let points = BTreeSet::from_iter(vec![
2863            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2864            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
2865            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
2866            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
2867        ]);
2868        let surface = Surface::new(points);
2869
2870        // Interpolation at any point should maintain the gradient
2871        let result = surface
2872            .linear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
2873            .unwrap();
2874        assert_eq!(result.z, dec!(1.0));
2875    }
2876
2877    #[test]
2878    fn test_interpolation_precision() {
2879        let surface = create_test_surface();
2880        let result = surface
2881            .linear_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
2882            .unwrap();
2883        // Verify that the result has the expected precision
2884        assert!(result.z >= dec!(0.0) && result.z <= dec!(2.0));
2885    }
2886}
2887
2888#[cfg(test)]
2889mod tests_surface_bilinear_interpolation {
2890    use super::*;
2891    use rust_decimal_macros::dec;
2892
2893    fn create_test_surface() -> Surface {
2894        let points = BTreeSet::from_iter(vec![
2895            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)), // Bottom-left
2896            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)), // Bottom-right
2897            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)), // Top-left
2898            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)), // Top-right
2899        ]);
2900        Surface::new(points)
2901    }
2902
2903    #[test]
2904    fn test_insufficient_points() {
2905        let points = BTreeSet::from_iter(vec![
2906            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2907            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
2908            Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
2909        ]);
2910        let surface = Surface::new(points);
2911        let result = surface.bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
2912        assert!(matches!(
2913            result,
2914            Err(InterpolationError::Bilinear(msg)) if msg.contains("Need at least four points")
2915        ));
2916    }
2917
2918    #[test]
2919    fn test_point_out_of_range() {
2920        let surface = create_test_surface();
2921        let result = surface.bilinear_interpolate(Point2D::new(dec!(-1.0), dec!(0.5)));
2922        assert!(matches!(
2923            result,
2924            Err(InterpolationError::Bilinear(msg)) if msg.contains("outside the surface's range")
2925        ));
2926    }
2927
2928    #[test]
2929    fn test_exact_point_match() {
2930        let surface = create_test_surface();
2931        let result = surface
2932            .bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
2933            .unwrap();
2934        assert_eq!(result.z, dec!(0.0));
2935    }
2936
2937    #[test]
2938    fn test_midpoint_interpolation() {
2939        let surface = create_test_surface();
2940        let result = surface
2941            .bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
2942            .unwrap();
2943        // At the midpoint, we expect the average of surrounding values
2944        assert_eq!(result.z, dec!(1.0));
2945    }
2946
2947    #[test]
2948    fn test_quarter_point_interpolation() {
2949        let surface = create_test_surface();
2950        let result = surface
2951            .bilinear_interpolate(Point2D::new(dec!(0.25), dec!(0.25)))
2952            .unwrap();
2953        // Value should be between 0.0 and 1.0
2954        assert!(result.z > dec!(0.0) && result.z < dec!(1.0));
2955    }
2956
2957    /// Four points stacked on one xy-coordinate are an invalid quadrilateral,
2958    /// and `bilinear_interpolate` says so.
2959    ///
2960    /// This test used to assert the *"Need at least four points"* message
2961    /// instead, and it reached it because `BTreeSet::from_iter` sorts and then
2962    /// deduplicates adjacent elements with `PartialEq`, not with `Ord`. With
2963    /// `Point3D` comparing equal on `(x, y)`, these four points collapsed to
2964    /// **one** on the way into the set, so the length guard fired and the
2965    /// invalid-quadrilateral check below it was unreachable. The same four
2966    /// values inserted one by one produced a set of four, because `insert`
2967    /// dispatches on `Ord` — the same container, two different contents,
2968    /// decided by which constructor was used.
2969    ///
2970    /// `PartialEq` now agrees with `Ord`, so the set holds all four and the
2971    /// check the test is named after is the one that runs.
2972    #[test]
2973    fn test_invalid_quadrilateral() {
2974        let points = BTreeSet::from_iter(vec![
2975            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
2976            Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
2977            Point3D::new(dec!(0.0), dec!(0.0), dec!(2.0)),
2978            Point3D::new(dec!(0.0), dec!(0.0), dec!(3.0)),
2979        ]);
2980        assert_eq!(points.len(), 4, "all four heights are distinct points");
2981
2982        let surface = Surface::new(points);
2983        let result = surface.bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)));
2984        assert!(result.is_err());
2985        assert!(matches!(
2986            result,
2987            Err(InterpolationError::Bilinear(msg)) if msg.contains("Invalid quadrilateral")
2988        ));
2989    }
2990
2991    #[test]
2992    fn test_boundary_interpolation() {
2993        let surface = create_test_surface();
2994        // Test interpolation at edge
2995        let result = surface
2996            .bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.5)))
2997            .unwrap();
2998        assert_eq!(result.z, dec!(0.5));
2999    }
3000
3001    #[test]
3002    fn test_uniform_gradient() {
3003        // Create a surface with uniform gradient
3004        let points = BTreeSet::from_iter(vec![
3005            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3006            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
3007            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
3008            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3009        ]);
3010        let surface = Surface::new(points);
3011        let result = surface
3012            .bilinear_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
3013            .unwrap();
3014        assert_eq!(result.z, dec!(1.0));
3015    }
3016
3017    #[test]
3018    fn test_interpolation_precision() {
3019        let surface = create_test_surface();
3020        let result = surface
3021            .bilinear_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
3022            .unwrap();
3023        // Verify that the result has the expected precision
3024        assert!(result.z >= dec!(0.0) && result.z <= dec!(2.0));
3025    }
3026
3027    #[test]
3028    fn test_corners_interpolation() {
3029        let surface = create_test_surface();
3030
3031        // Test all four corners
3032        let bl = surface
3033            .bilinear_interpolate(Point2D::new(dec!(0.0), dec!(0.0)))
3034            .unwrap();
3035        let br = surface
3036            .bilinear_interpolate(Point2D::new(dec!(1.0), dec!(0.0)))
3037            .unwrap();
3038        let tl = surface
3039            .bilinear_interpolate(Point2D::new(dec!(0.0), dec!(1.0)))
3040            .unwrap();
3041        let tr = surface
3042            .bilinear_interpolate(Point2D::new(dec!(1.0), dec!(1.0)))
3043            .unwrap();
3044
3045        assert_eq!(bl.z, dec!(0.0));
3046        assert_eq!(br.z, dec!(1.0));
3047        assert_eq!(tl.z, dec!(1.0));
3048        assert_eq!(tr.z, dec!(2.0));
3049    }
3050}
3051
3052#[cfg(test)]
3053mod tests_surface_cubic_interpolation {
3054    use super::*;
3055    use rust_decimal_macros::dec;
3056
3057    /// Helper function to create a test surface with a more complex point distribution
3058    fn create_test_surface() -> Surface {
3059        let points = BTreeSet::from_iter(vec![
3060            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3061            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
3062            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
3063            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3064            Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
3065            Point3D::new(dec!(0.2), dec!(0.8), dec!(0.7)),
3066            Point3D::new(dec!(0.8), dec!(0.2), dec!(0.7)),
3067            Point3D::new(dec!(0.3), dec!(0.3), dec!(0.3)),
3068            Point3D::new(dec!(0.7), dec!(0.7), dec!(1.7)),
3069        ]);
3070        Surface::new(points)
3071    }
3072
3073    #[test]
3074    fn test_insufficient_points() {
3075        let points = BTreeSet::from_iter(vec![
3076            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3077            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3078            Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
3079        ]);
3080        let surface = Surface::new(points);
3081        let result = surface.cubic_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
3082
3083        assert!(matches!(
3084            result,
3085            Err(InterpolationError::Cubic(msg)) if msg.contains("Need at least nine points")
3086        ));
3087    }
3088
3089    #[test]
3090    fn test_point_out_of_range() {
3091        let surface = create_test_surface();
3092        let result = surface.cubic_interpolate(Point2D::new(dec!(2.0), dec!(2.0)));
3093
3094        assert!(matches!(
3095            result,
3096            Err(InterpolationError::Cubic(msg)) if msg.contains("outside the surface's range")
3097        ));
3098    }
3099
3100    #[test]
3101    fn test_exact_point_match() {
3102        let surface = create_test_surface();
3103        let result = surface
3104            .cubic_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
3105            .unwrap();
3106
3107        assert_eq!(result.z, dec!(1.5));
3108    }
3109
3110    #[test]
3111    fn test_midpoint_interpolation() {
3112        let surface = create_test_surface();
3113        let result = surface
3114            .cubic_interpolate(Point2D::new(dec!(0.4), dec!(0.4)))
3115            .unwrap();
3116
3117        // Verify that the interpolated z is between the surrounding points
3118        assert!(result.z > dec!(0.3) && result.z < dec!(1.5));
3119    }
3120
3121    #[test]
3122    fn test_interpolation_consistency() {
3123        let surface = create_test_surface();
3124
3125        // Test multiple interpolation points
3126        let test_points = vec![
3127            Point2D::new(dec!(0.2), dec!(0.2)),
3128            Point2D::new(dec!(0.6), dec!(0.6)),
3129            Point2D::new(dec!(0.8), dec!(0.3)),
3130        ];
3131
3132        for point in test_points {
3133            let result = surface.cubic_interpolate(point).unwrap();
3134
3135            // Verify z is within reasonable bounds
3136            assert!(
3137                result.z >= dec!(0.0) && result.z <= dec!(2.0),
3138                "Failed for point {point:?}"
3139            );
3140
3141            // Verify the interpolated point is on the surface
3142            assert_eq!(result.x, point.x);
3143            assert_eq!(result.y, point.y);
3144        }
3145    }
3146
3147    #[test]
3148    fn test_boundary_interpolation() {
3149        let surface = create_test_surface();
3150
3151        // Test interpolation near surface boundaries
3152        let boundary_points = vec![
3153            Point2D::new(dec!(0.0), dec!(0.5)),
3154            Point2D::new(dec!(0.5), dec!(0.0)),
3155            Point2D::new(dec!(1.0), dec!(0.5)),
3156            Point2D::new(dec!(0.5), dec!(1.0)),
3157        ];
3158
3159        for point in boundary_points {
3160            let result = surface.cubic_interpolate(point).unwrap();
3161
3162            // Verify z is interpolated correctly
3163            assert!(
3164                result.z > dec!(0.0) && result.z < dec!(2.0),
3165                "Failed for boundary point {point:?}"
3166            );
3167        }
3168    }
3169
3170    #[test]
3171    fn test_interpolation_precision() {
3172        let surface = create_test_surface();
3173        let result = surface
3174            .cubic_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
3175            .unwrap();
3176
3177        // Verify precision and reasonable interpolation
3178        assert!(result.z > dec!(0.0) && result.z < dec!(2.0));
3179    }
3180
3181    #[test]
3182    fn test_repeated_interpolation() {
3183        let surface = create_test_surface();
3184
3185        // Interpolate the same point multiple times to check consistency
3186        let point = Point2D::new(dec!(0.4), dec!(0.4));
3187        let results: Vec<Decimal> = (0..5)
3188            .map(|_| surface.cubic_interpolate(point).unwrap().z)
3189            .collect();
3190
3191        // Check that results are very close to each other
3192        let max_diff = results
3193            .iter()
3194            .max_by(|a, b| a.partial_cmp(b).unwrap())
3195            .unwrap()
3196            - results
3197                .iter()
3198                .min_by(|a, b| a.partial_cmp(b).unwrap())
3199                .unwrap();
3200
3201        assert!(
3202            max_diff < dec!(0.001),
3203            "Interpolation results should be consistent"
3204        );
3205    }
3206
3207    #[test]
3208    fn test_extreme_point_locations() {
3209        let surface = create_test_surface();
3210
3211        // Test points very close to existing points
3212        let extreme_points = vec![
3213            Point2D::new(dec!(0.001), dec!(0.001)),
3214            Point2D::new(dec!(0.999), dec!(0.999)),
3215        ];
3216
3217        for point in extreme_points {
3218            let result = surface.cubic_interpolate(point).unwrap();
3219
3220            // Verify z is interpolated reasonably
3221            assert!(
3222                result.z >= dec!(0.0) && result.z <= dec!(2.0),
3223                "Failed for extreme point {point:?}"
3224            );
3225        }
3226    }
3227}
3228
3229#[cfg(test)]
3230mod tests_surface_spline_interpolation {
3231    use super::*;
3232    use rust_decimal_macros::dec;
3233
3234    /// Helper function to create a test surface with a more complex point distribution
3235    fn create_test_surface() -> Surface {
3236        let points = BTreeSet::from_iter(vec![
3237            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3238            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
3239            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
3240            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3241            Point3D::new(dec!(0.5), dec!(0.5), dec!(1.5)),
3242            Point3D::new(dec!(0.2), dec!(0.8), dec!(0.7)),
3243            Point3D::new(dec!(0.8), dec!(0.2), dec!(0.7)),
3244            Point3D::new(dec!(0.3), dec!(0.3), dec!(0.3)),
3245            Point3D::new(dec!(0.7), dec!(0.7), dec!(1.7)),
3246            Point3D::new(dec!(0.4), dec!(0.6), dec!(1.1)),
3247            Point3D::new(dec!(0.6), dec!(0.4), dec!(1.2)),
3248        ]);
3249        Surface::new(points)
3250    }
3251
3252    #[test]
3253    fn test_insufficient_points() {
3254        let points = BTreeSet::from_iter(vec![
3255            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3256            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3257        ]);
3258        let surface = Surface::new(points);
3259        let result = surface.spline_interpolate(Point2D::new(dec!(0.5), dec!(0.5)));
3260
3261        assert!(matches!(
3262            result,
3263            Err(InterpolationError::Spline(msg)) if msg.contains("Need at least nine points")
3264        ));
3265    }
3266
3267    #[test]
3268    fn test_point_out_of_range() {
3269        let surface = create_test_surface();
3270        let result = surface.spline_interpolate(Point2D::new(dec!(2.0), dec!(2.0)));
3271
3272        assert!(matches!(
3273            result,
3274            Err(InterpolationError::Spline(msg)) if msg.contains("outside the surface's range")
3275        ));
3276    }
3277
3278    #[test]
3279    fn test_exact_point_match() {
3280        let surface = create_test_surface();
3281        let result = surface
3282            .spline_interpolate(Point2D::new(dec!(0.5), dec!(0.5)))
3283            .unwrap();
3284
3285        assert_eq!(result.z, dec!(1.5));
3286    }
3287
3288    #[test]
3289    fn test_midpoint_interpolation() {
3290        let surface = create_test_surface();
3291        let result = surface
3292            .spline_interpolate(Point2D::new(dec!(0.4), dec!(0.4)))
3293            .unwrap();
3294
3295        // Verify that the interpolated z is between the surrounding points
3296        assert!(result.z > dec!(0.3) && result.z < dec!(1.5));
3297    }
3298
3299    #[test]
3300    fn test_interpolation_consistency() {
3301        let surface = create_test_surface();
3302
3303        // Test multiple interpolation points
3304        let test_points = vec![
3305            Point2D::new(dec!(0.2), dec!(0.2)),
3306            Point2D::new(dec!(0.6), dec!(0.6)),
3307            Point2D::new(dec!(0.8), dec!(0.3)),
3308        ];
3309
3310        for point in test_points {
3311            let result = surface.spline_interpolate(point).unwrap();
3312
3313            // Verify z is within reasonable bounds
3314            assert!(
3315                result.z >= dec!(0.0) && result.z <= dec!(2.0),
3316                "Failed for point {point:?}"
3317            );
3318
3319            // Verify the interpolated point is on the surface
3320            assert_eq!(result.x, point.x);
3321            assert_eq!(result.y, point.y);
3322        }
3323    }
3324
3325    #[test]
3326    fn test_boundary_interpolation() {
3327        let surface = create_test_surface();
3328
3329        // Test interpolation near surface boundaries
3330        let boundary_points = vec![
3331            Point2D::new(dec!(0.0), dec!(0.5)),
3332            Point2D::new(dec!(0.5), dec!(0.0)),
3333            Point2D::new(dec!(1.0), dec!(0.5)),
3334            Point2D::new(dec!(0.5), dec!(1.0)),
3335        ];
3336
3337        for point in boundary_points {
3338            let result = surface.spline_interpolate(point).unwrap();
3339
3340            // Verify z is interpolated correctly
3341            assert!(
3342                result.z > dec!(0.0) && result.z < dec!(2.0),
3343                "Failed for boundary point {point:?}"
3344            );
3345        }
3346    }
3347
3348    #[test]
3349    fn test_interpolation_precision() {
3350        let surface = create_test_surface();
3351        let result = surface
3352            .spline_interpolate(Point2D::new(dec!(0.333333), dec!(0.333333)))
3353            .unwrap();
3354
3355        // Verify precision and reasonable interpolation
3356        assert!(result.z > dec!(0.0) && result.z < dec!(2.0));
3357    }
3358
3359    #[test]
3360    fn test_repeated_interpolation() {
3361        let surface = create_test_surface();
3362
3363        // Interpolate the same point multiple times to check consistency
3364        let point = Point2D::new(dec!(0.4), dec!(0.4));
3365        let results: Vec<Decimal> = (0..5)
3366            .map(|_| surface.spline_interpolate(point).unwrap().z)
3367            .collect();
3368
3369        // Check that results are very close to each other
3370        let max_diff = results
3371            .iter()
3372            .max_by(|a, b| a.partial_cmp(b).unwrap())
3373            .unwrap()
3374            - results
3375                .iter()
3376                .min_by(|a, b| a.partial_cmp(b).unwrap())
3377                .unwrap();
3378
3379        assert!(
3380            max_diff < dec!(0.001),
3381            "Interpolation results should be consistent"
3382        );
3383    }
3384
3385    #[test]
3386    fn test_extreme_point_locations() {
3387        let surface = create_test_surface();
3388
3389        // Test points very close to existing points
3390        let extreme_points = vec![
3391            Point2D::new(dec!(0.001), dec!(0.001)),
3392            Point2D::new(dec!(0.999), dec!(0.999)),
3393        ];
3394
3395        for point in extreme_points {
3396            let result = surface.spline_interpolate(point).unwrap();
3397
3398            // Verify z is interpolated reasonably
3399            assert!(
3400                result.z >= dec!(0.0) && result.z <= dec!(2.0),
3401                "Failed for extreme point {point:?}"
3402            );
3403        }
3404    }
3405
3406    #[test]
3407    fn test_one_dimensional_spline_interpolation() {
3408        let surface = create_test_surface();
3409
3410        // Create test points for one-dimensional interpolation
3411        let points = vec![
3412            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3413            Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
3414            Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
3415        ];
3416
3417        // Test interpolation at different points
3418        let test_points = vec![
3419            (dec!(0.25), dec!(0.5)), // Midpoint
3420            (dec!(0.0), dec!(0.0)),  // Start point
3421            (dec!(1.0), dec!(2.0)),  // End point
3422            (dec!(0.75), dec!(1.5)), // Another point
3423        ];
3424
3425        for (target, expected) in test_points {
3426            let result = surface
3427                .one_dimensional_spline_interpolation(&points, target, |p| p.x, |p| p.z)
3428                .unwrap();
3429
3430            // Allow small deviation due to interpolation
3431            assert!(
3432                (result - expected).abs() < dec!(0.1),
3433                "Failed for target {target}, expected {expected}, got {result}"
3434            );
3435        }
3436    }
3437
3438    #[test]
3439    fn test_interpolation_edge_cases() {
3440        let surface = create_test_surface();
3441
3442        // Test edge cases like very small intervals
3443        let edge_points = vec![
3444            Point2D::new(dec!(0.001), dec!(0.001)),
3445            Point2D::new(dec!(0.999), dec!(0.999)),
3446            Point2D::new(dec!(0.5), dec!(0.5)),
3447        ];
3448
3449        for point in edge_points {
3450            let result = surface.spline_interpolate(point);
3451            assert!(result.is_ok(), "Failed for point {point:?}");
3452
3453            let interpolated_point = result.unwrap();
3454            assert_eq!(interpolated_point.x, point.x);
3455            assert_eq!(interpolated_point.y, point.y);
3456        }
3457    }
3458}
3459
3460#[cfg(test)]
3461mod tests_surface_arithmetic {
3462    use super::*;
3463    use optionstratlib_core::error::OperationErrorKind;
3464    use rust_decimal_macros::dec;
3465
3466    fn create_test_surface() -> Surface {
3467        let points = BTreeSet::from_iter(vec![
3468            Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
3469            Point3D::new(dec!(0.5), dec!(0.0), dec!(1.0)),
3470            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
3471            Point3D::new(dec!(0.0), dec!(0.5), dec!(1.0)),
3472            Point3D::new(dec!(0.5), dec!(0.5), dec!(1.0)),
3473            Point3D::new(dec!(1.0), dec!(0.5), dec!(1.0)),
3474            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
3475            Point3D::new(dec!(0.5), dec!(1.0), dec!(1.0)),
3476            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3477        ]);
3478        Surface::new(points)
3479    }
3480
3481    /// The end of both ranges in [`long_mantissa_surface`]: a 27-place
3482    /// mantissa whose `span / 50` rounds up at 28 places, to
3483    /// `0.2000000000000000000000000004`, so `step * 50` used to land on
3484    /// `10.000000000000000000000000020`, past the maximum (#795).
3485    const LONG_MANTISSA_MAX: Decimal = dec!(10.000000000000000000000000018);
3486
3487    /// The plane `z = x + y` on the grid `{0, 2.5, 5, 7.5, LONG_MANTISSA_MAX}`
3488    /// on both axes.
3489    fn long_mantissa_surface() -> Surface {
3490        let axis = [dec!(0), dec!(2.5), dec!(5), dec!(7.5), LONG_MANTISSA_MAX];
3491        let points = axis
3492            .iter()
3493            .flat_map(|&x| axis.iter().map(move |&y| Point3D::new(x, y, x + y)))
3494            .collect();
3495        Surface::new(points)
3496    }
3497
3498    #[test]
3499    fn test_merge_long_mantissa_ranges_end_on_the_maximum() {
3500        let surface = long_mantissa_surface();
3501        let max = LONG_MANTISSA_MAX;
3502
3503        let result = surface.merge_with(&surface, MergeOperation::Add).unwrap();
3504
3505        // A 51 x 51 grid from 0 to the maximum on each axis, none past it.
3506        assert_eq!(result.points.len(), 51 * 51);
3507        assert!(result.points.iter().all(|p| p.x <= max && p.y <= max));
3508        assert_eq!(result.x_range, (Decimal::ZERO, max));
3509        assert_eq!(result.y_range, (Decimal::ZERO, max));
3510    }
3511
3512    #[test]
3513    fn test_merge_empty_surfaces() {
3514        let result = Surface::merge(&[], MergeOperation::Add);
3515        assert!(matches!(
3516            result,
3517            Err(SurfaceError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
3518            if operation == "merge_surfaces" && reason.contains("No surfaces")
3519        ));
3520    }
3521
3522    #[test]
3523    fn test_merge_single_surface() {
3524        let surface = create_test_surface();
3525        let result = Surface::merge(&[&surface], MergeOperation::Add).unwrap();
3526        assert_eq!(result.points.len(), surface.points.len());
3527    }
3528
3529    #[test]
3530    fn test_merge_add() {
3531        let surface1 = create_test_surface();
3532        let surface2 = create_test_surface();
3533        let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Add).unwrap();
3534
3535        let mid_point = result
3536            .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3537            .unwrap();
3538        assert_eq!(mid_point.z, dec!(2.0));
3539    }
3540
3541    #[test]
3542    fn test_merge_subtract() {
3543        let surface1 = create_test_surface();
3544        let surface2 = create_test_surface();
3545        let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Subtract).unwrap();
3546
3547        // Test point should have z-value of 0 (1.0 - 1.0)
3548        let mid_point = result
3549            .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3550            .unwrap();
3551        assert_eq!(mid_point.z, dec!(0.0));
3552    }
3553
3554    fn constant_surface(height: Decimal) -> Surface {
3555        let points =
3556            BTreeSet::from_iter([dec!(0.0), dec!(0.5), dec!(1.0)].into_iter().flat_map(|x| {
3557                [dec!(0.0), dec!(0.5), dec!(1.0)]
3558                    .into_iter()
3559                    .map(move |y| Point3D::new(x, y, height))
3560            }));
3561        Surface::new(points)
3562    }
3563
3564    /// Division is not associative, so the divisors have to be folded in
3565    /// sequence. Three surfaces at 8, 2 and 2 must give `8 / 2 / 2 = 2`; a
3566    /// reducer that keeps only its left input drops the trailing divisors and
3567    /// lands somewhere between 4 and 8.
3568    #[test]
3569    fn test_merge_divide_folds_every_divisor() {
3570        let eight = constant_surface(dec!(8));
3571        let two_a = constant_surface(dec!(2));
3572        let two_b = constant_surface(dec!(2));
3573
3574        let result = Surface::merge(&[&eight, &two_a, &two_b], MergeOperation::Divide).unwrap();
3575
3576        let mid_point = result
3577            .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3578            .unwrap();
3579        assert!(
3580            (mid_point.z - dec!(2)).abs() < dec!(0.0001),
3581            "expected 8 / 2 / 2 = 2, got {}",
3582            mid_point.z
3583        );
3584    }
3585
3586    /// Number of times a determinism test repeats the same merge.
3587    ///
3588    /// `Surface::merge` resamples a 51 x 51 grid and runs the fold once per
3589    /// cell, so one call already exercises the fold 2601 times against
3590    /// `Curve::merge`'s 101. That buys the same confidence from far fewer
3591    /// repeats, which matters because each call is three orders of magnitude
3592    /// dearer than a curve merge; see `DETERMINISM_REPEATS` in
3593    /// `curves::curve` for how the rate this is sized against was measured.
3594    const DETERMINISM_REPEATS: usize = 24;
3595
3596    /// The heights of a surface as `(mantissa, scale)` pairs.
3597    ///
3598    /// `Decimal` compares equal across scales, so `dec!(2.0) == dec!(2.00)`
3599    /// and a plain equality check would not notice a fold that moved the
3600    /// scale. The mantissa and the scale together are the stored value.
3601    fn height_fingerprint(surface: &Surface) -> Vec<(i128, u32)> {
3602        surface
3603            .points
3604            .iter()
3605            .map(|point| (point.z.mantissa(), point.z.scale()))
3606            .collect()
3607    }
3608
3609    /// Merging three surfaces under `Multiply` gives the same digits on every
3610    /// run, and gives the digits of a left-to-right fold.
3611    ///
3612    /// The three heights are the ones used by the curve test of the same
3613    /// name: their triple product overruns the 28 decimal places a
3614    /// `Decimal` holds and has to round twice, ending `...768908` grouped
3615    /// from the left and `...768907` grouped from the right. The parallel
3616    /// reducer this replaces returned the second.
3617    #[test]
3618    fn test_merge_surfaces_multiply_is_reproducible() {
3619        let a = constant_surface(dec!(0.010989010989010989010989011));
3620        let b = constant_surface(dec!(0.010752688172043010752688172));
3621        let c = constant_surface(dec!(0.0094339622641509433962264151));
3622
3623        let expected = dec!(0.0000011147302687168785768908);
3624        let first = Surface::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
3625        for point in first.points.iter() {
3626            assert_eq!(
3627                (point.z.mantissa(), point.z.scale()),
3628                (expected.mantissa(), expected.scale()),
3629                "expected the left-to-right product {expected} at ({}, {}), got {}",
3630                point.x,
3631                point.y,
3632                point.z
3633            );
3634        }
3635
3636        let reference = height_fingerprint(&first);
3637        for run in 1..DETERMINISM_REPEATS {
3638            let again = Surface::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
3639            assert_eq!(
3640                height_fingerprint(&again),
3641                reference,
3642                "run {run} of {DETERMINISM_REPEATS} produced different digits from the first"
3643            );
3644        }
3645    }
3646
3647    /// A divisor that fails the checked division has to surface as an error
3648    /// rather than being discarded together with the fold result that
3649    /// produced it. `1e20 / 1e-20` is `1e40`, well past `Decimal::MAX`, while
3650    /// both operands stay far enough inside the range that the interpolation
3651    /// feeding the fold cannot fail first.
3652    #[test]
3653    fn test_merge_divide_reports_overflow_instead_of_dropping_it() {
3654        let tiny = constant_surface(Decimal::new(1, 20));
3655        let big = constant_surface(dec!(100000000000000000000));
3656
3657        let result = Surface::merge(&[&big, &tiny], MergeOperation::Divide);
3658        assert!(
3659            matches!(result, Err(SurfaceError::ConstructionError(_))),
3660            "a quotient outside the Decimal range must be reported, got {result:?}"
3661        );
3662    }
3663
3664    #[test]
3665    fn test_incompatible_ranges() {
3666        let surface1 = Surface::new(BTreeSet::from_iter(vec![
3667            Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
3668            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3669        ]));
3670
3671        let surface2 = Surface::new(BTreeSet::from_iter(vec![
3672            Point3D::new(dec!(2.0), dec!(2.0), dec!(1.0)),
3673            Point3D::new(dec!(3.0), dec!(3.0), dec!(1.0)),
3674        ]));
3675
3676        let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Add);
3677        assert!(matches!(
3678            result,
3679            Err(SurfaceError::OperationError(OperationErrorKind::InvalidParameters { operation, reason }))
3680            if operation == "merge_surfaces" && reason.contains("incompatible ranges")
3681        ));
3682    }
3683
3684    #[test]
3685    fn test_merge_with() {
3686        let surface1 = create_test_surface();
3687        let surface2 = create_test_surface();
3688
3689        let result1 = surface1.merge_with(&surface2, MergeOperation::Add).unwrap();
3690        let result2 = Surface::merge(&[&surface1, &surface2], MergeOperation::Add).unwrap();
3691
3692        assert_eq!(result1.points.len(), result2.points.len());
3693
3694        // Compare some interpolated points
3695        let test_point = Point2D::new(dec!(0.5), dec!(0.5));
3696        let z1 = result1
3697            .interpolate(test_point, InterpolationType::Cubic)
3698            .unwrap();
3699        let z2 = result2
3700            .interpolate(test_point, InterpolationType::Cubic)
3701            .unwrap();
3702        assert_eq!(z1.z, z2.z);
3703    }
3704
3705    #[test]
3706    fn test_merge_multiply() {
3707        let surface1 = create_test_surface();
3708        let surface2 = create_test_surface();
3709        let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Multiply).unwrap();
3710
3711        let mid_point = result
3712            .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3713            .unwrap();
3714        assert_eq!(mid_point.z, dec!(1.0)); // 1.0 * 1.0 = 1.0
3715    }
3716
3717    #[test]
3718    fn test_merge_divide() {
3719        let surface1 = create_test_surface();
3720        let surface2 = create_test_surface();
3721        let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Divide).unwrap();
3722
3723        let mid_point = result
3724            .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3725            .unwrap();
3726        assert_eq!(mid_point.z, dec!(1.0)); // 1.0 / 1.0 = 1.0
3727    }
3728
3729    #[test]
3730    fn test_merge_max() {
3731        let surface1 = create_test_surface(); // z=1.0 everywhere
3732
3733        // Create surface2 with z=2.0 everywhere
3734        let points2 = BTreeSet::from_iter(vec![
3735            Point3D::new(dec!(0.0), dec!(0.0), dec!(2.0)),
3736            Point3D::new(dec!(0.5), dec!(0.0), dec!(2.0)),
3737            Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
3738            Point3D::new(dec!(0.0), dec!(0.5), dec!(2.0)),
3739            Point3D::new(dec!(0.5), dec!(0.5), dec!(2.0)),
3740            Point3D::new(dec!(1.0), dec!(0.5), dec!(2.0)),
3741            Point3D::new(dec!(0.0), dec!(1.0), dec!(2.0)),
3742            Point3D::new(dec!(0.5), dec!(1.0), dec!(2.0)),
3743            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3744        ]);
3745        let surface2 = Surface::new(points2);
3746
3747        let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Max).unwrap();
3748
3749        let mid_point = result
3750            .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3751            .unwrap();
3752        assert_eq!(mid_point.z, dec!(2.0));
3753    }
3754
3755    #[test]
3756    fn test_merge_min() {
3757        let surface1 = create_test_surface();
3758        let mut surface2 = create_test_surface();
3759
3760        // Modify one point in surface2 to be lower
3761        surface2
3762            .points
3763            .insert(Point3D::new(dec!(0.5), dec!(0.5), dec!(0.5)));
3764
3765        let result = Surface::merge(&[&surface1, &surface2], MergeOperation::Min).unwrap();
3766
3767        let mid_point = result
3768            .interpolate(Point2D::new(dec!(0.5), dec!(0.5)), InterpolationType::Cubic)
3769            .unwrap();
3770        assert_eq!(mid_point.z, dec!(0.5));
3771    }
3772}
3773
3774#[cfg(test)]
3775mod tests_metrics {
3776    use super::*;
3777    use rust_decimal_macros::dec;
3778
3779    fn create_test_surface() -> Surface {
3780        let points = BTreeSet::from_iter(vec![
3781            Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
3782            Point3D::new(dec!(0.5), dec!(0.0), dec!(2.0)),
3783            Point3D::new(dec!(1.0), dec!(0.0), dec!(3.0)),
3784            Point3D::new(dec!(0.0), dec!(0.5), dec!(2.0)),
3785            Point3D::new(dec!(0.5), dec!(0.5), dec!(3.0)),
3786            Point3D::new(dec!(1.0), dec!(0.5), dec!(4.0)),
3787            Point3D::new(dec!(0.0), dec!(1.0), dec!(3.0)),
3788            Point3D::new(dec!(0.5), dec!(1.0), dec!(4.0)),
3789            Point3D::new(dec!(1.0), dec!(1.0), dec!(5.0)),
3790        ]);
3791        Surface::new(points)
3792    }
3793
3794    #[test]
3795    fn test_basic_metrics() {
3796        let surface = create_test_surface();
3797        let metrics = surface.compute_basic_metrics().unwrap();
3798
3799        assert_eq!(metrics.mean, dec!(3.0));
3800        assert_eq!(metrics.median, dec!(3.0));
3801        assert_eq!(metrics.std_dev, dec!(1.1547005383792515290182975610));
3802    }
3803
3804    #[test]
3805    fn test_shape_metrics() {
3806        let surface = create_test_surface();
3807        let metrics = surface.compute_shape_metrics().unwrap();
3808
3809        assert!(metrics.skewness.abs() < dec!(0.001));
3810        assert!((metrics.kurtosis - dec!(2.25)).abs() < dec!(0.001));
3811    }
3812
3813    #[test]
3814    fn test_range_metrics() {
3815        let surface = create_test_surface();
3816        let metrics = surface.compute_range_metrics().unwrap();
3817
3818        assert_eq!(metrics.min.y, dec!(1.0));
3819        assert_eq!(metrics.max.y, dec!(5.0));
3820        assert_eq!(metrics.range, dec!(4.0));
3821
3822        let (q1, q2, q3) = metrics.quartiles;
3823        assert_eq!(q1, dec!(2.0));
3824        assert_eq!(q2, dec!(3.0));
3825        assert_eq!(q3, dec!(4.0));
3826        assert_eq!(metrics.interquartile_range, dec!(2.0));
3827    }
3828
3829    #[test]
3830    fn test_trend_metrics() {
3831        let surface = create_test_surface();
3832        let metrics = surface.compute_trend_metrics().unwrap();
3833
3834        // We have a linear trend with slope 2.0
3835        assert!((metrics.slope - dec!(2.0)).abs() < dec!(0.001));
3836        assert!((metrics.intercept - dec!(2.0)).abs() < dec!(0.001));
3837    }
3838
3839    /// A flat surface has no uncertainty, but it is not worthless. Only the
3840    /// fields that are genuinely undefined at zero dispersion may be zeroed;
3841    /// the parametric VaR still has its deterministic limit at the mean.
3842    #[test]
3843    fn test_risk_metrics_flat_surface_keeps_deterministic_var() {
3844        let points = BTreeSet::from_iter((0..3i64).flat_map(|i| {
3845            (0..3i64).map(move |j| Point3D::new(Decimal::from(i), Decimal::from(j), dec!(5)))
3846        }));
3847        let surface = Surface::new(points);
3848        let metrics = surface.compute_risk_metrics().unwrap();
3849
3850        // Measured, and genuinely zero.
3851        assert_eq!(metrics.volatility, Decimal::ZERO);
3852        // `mean - 1.645 * 0` is the mean, not zero: the surface is worth 5.
3853        assert_eq!(metrics.value_at_risk, dec!(5));
3854        // No sample falls below the VaR, so the conditional mean has an empty
3855        // tail and the function's own empty-tail rule gives zero.
3856        assert_eq!(metrics.expected_shortfall, Decimal::ZERO);
3857        // Zero dispersion over a mean of 5: a zero coefficient of variation.
3858        assert_eq!(metrics.coefficient_of_variation, Decimal::ZERO);
3859        // `mean / 0` is the one genuinely undefined field.
3860        assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
3861    }
3862
3863    /// The same shape with a negative mean: the VaR limit follows the mean
3864    /// wherever it sits, so a sign error cannot hide behind a positive value.
3865    #[test]
3866    fn test_risk_metrics_flat_negative_surface_keeps_deterministic_var() {
3867        let points = BTreeSet::from_iter((0..3i64).flat_map(|i| {
3868            (0..3i64).map(move |j| Point3D::new(Decimal::from(i), Decimal::from(j), dec!(-2.5)))
3869        }));
3870        let surface = Surface::new(points);
3871        let metrics = surface.compute_risk_metrics().unwrap();
3872
3873        assert_eq!(metrics.volatility, Decimal::ZERO);
3874        assert_eq!(metrics.value_at_risk, dec!(-2.5));
3875        assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
3876    }
3877}
3878
3879#[cfg(test)]
3880mod tests_trend_metrics {
3881    use super::*;
3882    use optionstratlib_core::assert_decimal_eq;
3883    use rust_decimal_macros::dec;
3884
3885    // Helper function to create a test surface
3886    fn create_linear_surface() -> Surface {
3887        let points = BTreeSet::from_iter(vec![
3888            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
3889            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
3890            Point3D::new(dec!(2.0), dec!(2.0), dec!(4.0)),
3891            Point3D::new(dec!(3.0), dec!(3.0), dec!(6.0)),
3892            Point3D::new(dec!(4.0), dec!(4.0), dec!(8.0)),
3893        ]);
3894        Surface::new(points)
3895    }
3896
3897    // Helper function to create a non-linear surface
3898    fn create_non_linear_surface() -> Surface {
3899        let points = BTreeSet::from_iter(vec![
3900            Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
3901            Point3D::new(dec!(1.0), dec!(1.0), dec!(3.0)),
3902            Point3D::new(dec!(2.0), dec!(2.0), dec!(2.0)),
3903            Point3D::new(dec!(3.0), dec!(3.0), dec!(5.0)),
3904            Point3D::new(dec!(4.0), dec!(4.0), dec!(4.0)),
3905        ]);
3906        Surface::new(points)
3907    }
3908
3909    #[test]
3910    fn test_compute_trend_metrics_linear_surface() {
3911        let surface = create_linear_surface();
3912        let metrics = surface.compute_trend_metrics().unwrap();
3913
3914        // Check slope (should be 2.0 for a perfectly linear surface)
3915        assert_decimal_eq!(metrics.slope, dec!(2.0), dec!(0.001));
3916
3917        // Check intercept (should be close to 0)
3918        assert_decimal_eq!(metrics.intercept, dec!(0.0), dec!(0.001));
3919
3920        // R-squared should be very close to 1 for a perfect linear relationship
3921        assert_decimal_eq!(metrics.r_squared, dec!(1.0), dec!(0.001));
3922
3923        // Check moving average points
3924        assert_eq!(metrics.moving_average.len(), 4);
3925    }
3926
3927    #[test]
3928    fn test_compute_trend_metrics_non_linear_surface() {
3929        let surface = create_non_linear_surface();
3930        let metrics = surface.compute_trend_metrics().unwrap();
3931
3932        // R-squared should be less than 1 for a non-perfect linear relationship
3933        assert!(metrics.r_squared < dec!(1.0));
3934
3935        // Slope and intercept will vary based on the non-linear surface
3936        assert!(metrics.slope != dec!(0.0));
3937        assert!(metrics.intercept != dec!(0.0));
3938    }
3939
3940    #[test]
3941    fn test_moving_average_calculation() {
3942        let surface = create_linear_surface();
3943        let metrics = surface.compute_trend_metrics().unwrap();
3944
3945        // Verify moving average calculation
3946        let window_sizes = [3, 5, 7];
3947
3948        // Calculate total points safely
3949        let surface_points_count = surface.points.len();
3950
3951        let expected_total_points = window_sizes
3952            .iter()
3953            .map(|&window| {
3954                // Safely handle cases where window might be larger than points
3955                if window > surface_points_count {
3956                    0
3957                } else {
3958                    surface_points_count
3959                        .saturating_sub(window)
3960                        .saturating_add(1)
3961                }
3962            })
3963            .sum::<usize>();
3964
3965        // Assert with more informative message
3966        assert_eq!(
3967            metrics.moving_average.len(),
3968            expected_total_points,
3969            "Mismatch in moving average points calculation"
3970        );
3971
3972        // Verify x and y values in moving average
3973        for point in &metrics.moving_average {
3974            assert!(point.x >= dec!(0.0), "x value should be non-negative");
3975            assert!(point.y >= dec!(0.0), "y value should be non-negative");
3976        }
3977    }
3978
3979    #[test]
3980    fn test_edge_cases() {
3981        // Surface with a single point
3982        let single_point_surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
3983            dec!(1.0),
3984            dec!(1.0),
3985            dec!(1.0),
3986        )]));
3987
3988        let metrics = single_point_surface.compute_trend_metrics();
3989        assert!(metrics.is_ok());
3990
3991        // Surface with identical points
3992        let identical_points_surface = Surface::new(BTreeSet::from_iter(vec![
3993            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3994            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3995            Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)),
3996        ]));
3997
3998        let metrics = identical_points_surface.compute_trend_metrics().unwrap();
3999
4000        // For identical points, R-squared should be 1
4001        assert_decimal_eq!(metrics.r_squared, dec!(1.0), dec!(0.001));
4002        assert_decimal_eq!(metrics.slope, dec!(0.0), dec!(0.001));
4003    }
4004}
4005
4006#[cfg(test)]
4007mod tests_axis_operations {
4008    use super::*;
4009    use rust_decimal_macros::dec;
4010
4011    // Create a test Surface with predefined points
4012    fn create_test_surface() -> Surface {
4013        let points = BTreeSet::from_iter(vec![
4014            Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
4015            Point3D::new(dec!(1.0), dec!(0.0), dec!(2.0)),
4016            Point3D::new(dec!(0.0), dec!(1.0), dec!(3.0)),
4017            Point3D::new(dec!(1.0), dec!(1.0), dec!(4.0)),
4018        ]);
4019        Surface::new(points)
4020    }
4021
4022    #[test]
4023    fn test_contains_point() {
4024        let surface = create_test_surface();
4025        assert!(surface.contains_point(&Point2D::new(dec!(0.0), dec!(0.0))));
4026        assert!(!surface.contains_point(&Point2D::new(dec!(2.0), dec!(2.0))));
4027    }
4028
4029    #[test]
4030    fn test_get_index_values() {
4031        let surface = create_test_surface();
4032        let indexes = surface.get_index_values();
4033        assert_eq!(indexes.len(), 4);
4034        assert!(indexes.contains(&Point2D::new(dec!(0.0), dec!(0.0))));
4035        assert!(indexes.contains(&Point2D::new(dec!(1.0), dec!(1.0))));
4036    }
4037
4038    #[test]
4039    fn test_get_values() {
4040        let surface = create_test_surface();
4041        let values = surface.get_values(Point2D::new(dec!(0.0), dec!(0.0)));
4042        assert_eq!(values.len(), 1);
4043        assert_eq!(*values[0], dec!(1.0));
4044    }
4045
4046    #[test]
4047    fn test_get_closest_point() {
4048        let surface = create_test_surface();
4049        let point = surface
4050            .get_closest_point(&Point2D::new(dec!(0.5), dec!(0.5)))
4051            .unwrap();
4052        assert_eq!(point.x, dec!(0.0));
4053        assert_eq!(point.y, dec!(0.0));
4054        assert_eq!(point.z, dec!(1.0));
4055    }
4056
4057    #[test]
4058    fn test_get_point() {
4059        let surface = create_test_surface();
4060        let point = surface
4061            .get_point(&Point2D::new(dec!(0.0), dec!(0.0)))
4062            .unwrap();
4063        assert_eq!(point.x, dec!(0.0));
4064        assert_eq!(point.y, dec!(0.0));
4065        assert_eq!(point.z, dec!(1.0));
4066
4067        assert!(
4068            surface
4069                .get_point(&Point2D::new(dec!(2.0), dec!(2.0)))
4070                .is_none()
4071        );
4072    }
4073
4074    /// Merging a 2x2 grid with itself yields all four xy-coordinates.
4075    ///
4076    /// This test used to assert `merged.len() == 2`, and it passed. That was
4077    /// not the specification, it was data loss: `merge_indexes` funnels the
4078    /// index values through a `HashSet<Input>`, `Input` is `Point2D` for a
4079    /// surface, and `Point2D` hashed and compared on `x` alone — so both
4080    /// cells of every column collapsed into one and half the grid vanished
4081    /// before `merge_axis_interpolate` ever saw it. `Point2D` now carries
4082    /// both coordinates in `Eq` and `Hash`, so the axis survives the merge.
4083    #[test]
4084    fn test_merge_indexes() {
4085        let surface1 = create_test_surface();
4086        let surface2 = create_test_surface();
4087        let merged = surface1.merge_indexes(surface2.get_index_values());
4088
4089        assert_eq!(merged.len(), 4);
4090        for expected in [
4091            Point2D::new(dec!(0.0), dec!(0.0)),
4092            Point2D::new(dec!(0.0), dec!(1.0)),
4093            Point2D::new(dec!(1.0), dec!(0.0)),
4094            Point2D::new(dec!(1.0), dec!(1.0)),
4095        ] {
4096            assert!(merged.contains(&expected), "missing index {expected}");
4097        }
4098    }
4099}
4100
4101#[cfg(test)]
4102mod tests_surface_geometric_transformations {
4103    use super::*;
4104    use rust_decimal::MathematicalOps;
4105    use rust_decimal_macros::dec;
4106
4107    fn create_test_surface() -> Surface {
4108        Surface::new(BTreeSet::from_iter(vec![
4109            Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4110            Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4111            Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4112            Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
4113        ]))
4114    }
4115
4116    mod test_translate {
4117        use super::*;
4118
4119        #[test]
4120        fn test_translate_positive() {
4121            let surface = create_test_surface();
4122            let result = surface
4123                .translate(vec![&dec!(1.0), &dec!(1.0), &dec!(1.0)])
4124                .unwrap();
4125
4126            let translated_points: Vec<_> = result.points.iter().collect();
4127            assert_eq!(translated_points[0].x, dec!(1.0));
4128            assert_eq!(translated_points[0].y, dec!(1.0));
4129            assert_eq!(translated_points[0].z, dec!(1.0));
4130        }
4131
4132        #[test]
4133        fn test_translate_negative() {
4134            let surface = create_test_surface();
4135            let result = surface
4136                .translate(vec![&dec!(-1.0), &dec!(-1.0), &dec!(-1.0)])
4137                .unwrap();
4138
4139            let translated_points: Vec<_> = result.points.iter().collect();
4140            assert_eq!(translated_points[0].x, dec!(-1.0));
4141            assert_eq!(translated_points[0].y, dec!(-1.0));
4142            assert_eq!(translated_points[0].z, dec!(-1.0));
4143        }
4144
4145        #[test]
4146        fn test_translate_zero() {
4147            let surface = create_test_surface();
4148            let result = surface
4149                .translate(vec![&dec!(0.0), &dec!(0.0), &dec!(0.0)])
4150                .unwrap();
4151            assert_eq!(surface.points, result.points);
4152        }
4153
4154        #[test]
4155        fn test_translate_wrong_dimensions() {
4156            let surface = create_test_surface();
4157            let result = surface.translate(vec![&dec!(1.0), &dec!(1.0)]);
4158            assert!(result.is_err());
4159        }
4160
4161        #[test]
4162        fn test_translate_preserves_distances() {
4163            let surface = create_test_surface();
4164            let result = surface
4165                .translate(vec![&dec!(1.0), &dec!(1.0), &dec!(1.0)])
4166                .unwrap();
4167
4168            let original_points: Vec<_> = surface.points.iter().collect();
4169            let translated_points: Vec<_> = result.points.iter().collect();
4170
4171            let orig_dist = ((original_points[1].x - original_points[0].x).powi(2)
4172                + (original_points[1].y - original_points[0].y).powi(2)
4173                + (original_points[1].z - original_points[0].z).powi(2))
4174            .sqrt();
4175
4176            let trans_dist = ((translated_points[1].x - translated_points[0].x).powi(2)
4177                + (translated_points[1].y - translated_points[0].y).powi(2)
4178                + (translated_points[1].z - translated_points[0].z).powi(2))
4179            .sqrt();
4180
4181            assert_eq!(orig_dist, trans_dist);
4182        }
4183    }
4184
4185    mod test_scale {
4186        use super::*;
4187
4188        #[test]
4189        fn test_scale_uniform() {
4190            let surface = create_test_surface();
4191            let result = surface
4192                .scale(vec![&dec!(2.0), &dec!(2.0), &dec!(2.0)])
4193                .unwrap();
4194            assert_eq!(result[1].x, dec!(0.0));
4195            assert_eq!(result[1].y, dec!(2.0));
4196            assert_eq!(result[1].z, dec!(2.0));
4197        }
4198
4199        #[test]
4200        fn test_scale_non_uniform() {
4201            let surface = create_test_surface();
4202            let result = surface
4203                .scale(vec![&dec!(2.0), &dec!(3.0), &dec!(4.0)])
4204                .unwrap();
4205
4206            assert_eq!(result[0].x, dec!(0.0));
4207            assert_eq!(result[0].y, dec!(0.0));
4208            assert_eq!(result[0].z, dec!(0.0));
4209            assert_eq!(result[1].x, dec!(0.0));
4210            assert_eq!(result[1].y, dec!(3.0));
4211            assert_eq!(result[1].z, dec!(4.0));
4212            assert_eq!(result[2].x, dec!(2.0));
4213            assert_eq!(result[2].y, dec!(0.0));
4214            assert_eq!(result[2].z, dec!(4.0));
4215            assert_eq!(result[2].x, dec!(2.0));
4216            assert_eq!(result[2].y, dec!(0.0));
4217            assert_eq!(result[2].z, dec!(4.0));
4218        }
4219
4220        #[test]
4221        fn test_scale_zero() {
4222            let surface = create_test_surface();
4223            let result = surface
4224                .scale(vec![&dec!(0.0), &dec!(0.0), &dec!(0.0)])
4225                .unwrap();
4226
4227            assert!(
4228                result
4229                    .points
4230                    .iter()
4231                    .all(|p| p.x == dec!(0.0) && p.y == dec!(0.0) && p.z == dec!(0.0))
4232            );
4233        }
4234
4235        #[test]
4236        fn test_scale_wrong_dimensions() {
4237            let surface = create_test_surface();
4238            let result = surface.scale(vec![&dec!(2.0), &dec!(2.0)]);
4239            assert!(result.is_err());
4240        }
4241
4242        #[test]
4243        fn test_scale_negative() {
4244            let surface = create_test_surface();
4245            let result = surface
4246                .scale(vec![&dec!(-1.0), &dec!(-1.0), &dec!(-1.0)])
4247                .unwrap();
4248
4249            let scaled_points: Vec<_> = result.points.iter().collect();
4250            assert_eq!(scaled_points[1].x, dec!(-1.0));
4251            assert_eq!(scaled_points[1].y, dec!(0.0));
4252            assert_eq!(scaled_points[1].z, dec!(-1.0));
4253        }
4254    }
4255
4256    mod test_intersect_with {
4257        use super::*;
4258
4259        #[test]
4260        fn test_surfaces_intersect() {
4261            let surface1 = create_test_surface();
4262            let surface2 = Surface::new(BTreeSet::from_iter(vec![
4263                Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4264                Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4265            ]));
4266
4267            let intersections = surface1.intersect_with(&surface2).unwrap();
4268            assert_eq!(intersections.len(), 2);
4269        }
4270
4271        #[test]
4272        fn test_no_intersection() {
4273            let surface1 = create_test_surface();
4274            let surface2 = Surface::new(BTreeSet::from_iter(vec![
4275                Point3D::new(dec!(10.0), dec!(10.0), dec!(10.0)),
4276                Point3D::new(dec!(11.0), dec!(11.0), dec!(11.0)),
4277            ]));
4278
4279            let intersections = surface1.intersect_with(&surface2).unwrap();
4280            assert!(intersections.is_empty());
4281        }
4282
4283        #[test]
4284        fn test_multiple_intersections() {
4285            let surface1 = create_test_surface();
4286            let surface2 = create_test_surface();
4287
4288            let intersections = surface1.intersect_with(&surface2).unwrap();
4289            assert_eq!(intersections.len(), surface1.points.len());
4290        }
4291
4292        #[test]
4293        fn test_self_intersection() {
4294            let surface = create_test_surface();
4295            let intersections = surface.intersect_with(&surface).unwrap();
4296            assert_eq!(intersections.len(), surface.points.len());
4297        }
4298
4299        #[test]
4300        fn test_empty_surfaces() {
4301            let surface1 = Surface::new(BTreeSet::new());
4302            let surface2 = Surface::new(BTreeSet::new());
4303
4304            let intersections = surface1.intersect_with(&surface2).unwrap();
4305            assert!(intersections.is_empty());
4306        }
4307    }
4308
4309    mod test_derivative_at {
4310        use super::*;
4311
4312        #[test]
4313        fn test_planar_derivative() {
4314            let surface = Surface::new(BTreeSet::from_iter(vec![
4315                Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4316                Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4317                Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4318            ]));
4319
4320            let derivatives = surface
4321                .derivative_at(&Point3D::new(dec!(0.5), dec!(0.5), dec!(0.5)))
4322                .unwrap();
4323            assert_eq!(derivatives.len(), 2);
4324            assert_eq!(derivatives[0], Decimal::MAX); // ∂z/∂x
4325            assert_eq!(derivatives[1], dec!(1.0)); // ∂z/∂y
4326        }
4327
4328        #[test]
4329        fn test_non_planar_derivative() {
4330            let surface = create_test_surface();
4331            let derivatives = surface
4332                .derivative_at(&Point3D::new(dec!(0.5), dec!(0.5), dec!(1.0)))
4333                .unwrap();
4334            assert_eq!(derivatives.len(), 2);
4335        }
4336
4337        #[test]
4338        fn test_out_of_range() {
4339            let surface = create_test_surface();
4340            let result = surface.derivative_at(&Point3D::new(dec!(10.0), dec!(10.0), dec!(10.0)));
4341            assert!(result.is_err());
4342        }
4343
4344        #[test]
4345        fn test_at_corner() {
4346            let surface = create_test_surface();
4347            let derivatives = surface
4348                .derivative_at(&Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)))
4349                .unwrap();
4350            assert_eq!(derivatives.len(), 2);
4351        }
4352
4353        #[test]
4354        fn test_single_point_surface() {
4355            let surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
4356                dec!(1.0),
4357                dec!(1.0),
4358                dec!(1.0),
4359            )]));
4360            let result = surface.derivative_at(&Point3D::new(dec!(1.0), dec!(1.0), dec!(1.0)));
4361            assert!(result.is_err());
4362        }
4363    }
4364
4365    mod test_extrema {
4366        use super::*;
4367
4368        #[test]
4369        fn test_find_extrema() {
4370            let surface = create_test_surface();
4371            let (min, max) = surface.extrema().unwrap();
4372            assert_eq!(min.z, dec!(0.0));
4373            assert_eq!(max.z, dec!(2.0));
4374        }
4375
4376        #[test]
4377        fn test_empty_surface() {
4378            let surface = Surface::new(BTreeSet::new());
4379            let result = surface.extrema();
4380            assert!(result.is_err());
4381        }
4382
4383        #[test]
4384        fn test_single_point() {
4385            let surface = Surface::new(BTreeSet::from_iter(vec![Point3D::new(
4386                dec!(1.0),
4387                dec!(1.0),
4388                dec!(1.0),
4389            )]));
4390
4391            let (min, max) = surface.extrema().unwrap();
4392            assert_eq!(min, max);
4393        }
4394
4395        #[test]
4396        fn test_flat_surface() {
4397            let surface = Surface::new(BTreeSet::from_iter(vec![
4398                Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
4399                Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4400                Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4401            ]));
4402
4403            let (min, max) = surface.extrema().unwrap();
4404            assert_eq!(min.z, max.z);
4405        }
4406
4407        #[test]
4408        fn test_multiple_extrema() {
4409            let surface = Surface::new(BTreeSet::from_iter(vec![
4410                Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4411                Point3D::new(dec!(1.0), dec!(1.0), dec!(2.0)),
4412                Point3D::new(dec!(2.0), dec!(2.0), dec!(0.0)),
4413            ]));
4414
4415            let (min, max) = surface.extrema().unwrap();
4416            assert_eq!(min.z, dec!(0.0));
4417            assert_eq!(max.z, dec!(2.0));
4418        }
4419    }
4420
4421    mod test_measure_under {
4422        use super::*;
4423
4424        #[test]
4425        fn test_volume_under_planar() {
4426            let surface = Surface::new(BTreeSet::from_iter(vec![
4427                Point3D::new(dec!(0.0), dec!(0.0), dec!(1.0)),
4428                Point3D::new(dec!(1.0), dec!(0.0), dec!(1.0)),
4429                Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4430            ]));
4431
4432            let volume = surface.measure_under(&dec!(0.0)).unwrap();
4433            assert_eq!(volume, dec!(0.5)); // Area of triangle * height
4434        }
4435
4436        #[test]
4437        fn test_volume_empty_surface() {
4438            let surface = Surface::new(BTreeSet::new());
4439            let volume = surface.measure_under(&dec!(0.0)).unwrap();
4440            assert_eq!(volume, dec!(0.0));
4441        }
4442
4443        #[test]
4444        fn test_volume_single_triangle() {
4445            let surface = Surface::new(BTreeSet::from_iter(vec![
4446                Point3D::new(dec!(0.0), dec!(0.0), dec!(0.0)),
4447                Point3D::new(dec!(1.0), dec!(0.0), dec!(0.0)),
4448                Point3D::new(dec!(0.0), dec!(1.0), dec!(1.0)),
4449            ]));
4450
4451            let volume = surface.measure_under(&dec!(0.0)).unwrap();
4452            assert!(volume > dec!(0.0));
4453        }
4454
4455        #[test]
4456        fn test_volume_with_base_value() {
4457            let surface = create_test_surface();
4458            let volume1 = surface.measure_under(&dec!(0.0)).unwrap();
4459            let volume2 = surface.measure_under(&dec!(1.0)).unwrap();
4460            assert!(volume1 > volume2);
4461        }
4462
4463        #[test]
4464        fn test_negative_volume() {
4465            let surface = Surface::new(BTreeSet::from_iter(vec![
4466                Point3D::new(dec!(0.0), dec!(0.0), dec!(-1.0)),
4467                Point3D::new(dec!(1.0), dec!(0.0), dec!(-1.0)),
4468                Point3D::new(dec!(0.0), dec!(1.0), dec!(-1.0)),
4469            ]));
4470
4471            let volume = surface.measure_under(&dec!(0.0)).unwrap();
4472            assert!(volume > dec!(0.0));
4473        }
4474    }
4475}
4476
4477#[cfg(test)]
4478mod tests_surface_serde {
4479    use super::*;
4480    use rust_decimal_macros::dec;
4481
4482    // Helper function to create a test surface
4483    fn create_test_surface() -> Surface {
4484        let mut points = BTreeSet::new();
4485        points.insert(Point3D {
4486            x: dec!(1.0),
4487            y: dec!(2.0),
4488            z: dec!(3.0),
4489        });
4490        points.insert(Point3D {
4491            x: dec!(4.0),
4492            y: dec!(5.0),
4493            z: dec!(6.0),
4494        });
4495        points.insert(Point3D {
4496            x: dec!(7.0),
4497            y: dec!(8.0),
4498            z: dec!(9.0),
4499        });
4500
4501        Surface {
4502            points,
4503            x_range: (dec!(1.0), dec!(7.0)),
4504            y_range: (dec!(2.0), dec!(8.0)),
4505        }
4506    }
4507
4508    #[test]
4509    fn test_basic_serialization() {
4510        let surface = create_test_surface();
4511        let serialized = serde_json::to_string(&surface).unwrap();
4512        let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4513
4514        assert_eq!(surface.points, deserialized.points);
4515        assert_eq!(surface.x_range, deserialized.x_range);
4516        assert_eq!(surface.y_range, deserialized.y_range);
4517    }
4518
4519    #[test]
4520    fn test_pretty_print() {
4521        let surface = create_test_surface();
4522        let serialized = serde_json::to_string_pretty(&surface).unwrap();
4523
4524        // Verify pretty print format
4525        assert!(serialized.contains('\n'));
4526        assert!(serialized.contains("  "));
4527
4528        // Verify deserialization still works
4529        let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4530        assert_eq!(surface.points, deserialized.points);
4531    }
4532
4533    #[test]
4534    fn test_empty_surface() {
4535        let surface = Surface {
4536            points: BTreeSet::new(),
4537            x_range: (dec!(0.0), dec!(0.0)),
4538            y_range: (dec!(0.0), dec!(0.0)),
4539        };
4540
4541        let serialized = serde_json::to_string(&surface).unwrap();
4542        let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4543
4544        assert!(deserialized.points.is_empty());
4545        assert_eq!(deserialized.x_range, (dec!(0.0), dec!(0.0)));
4546        assert_eq!(deserialized.y_range, (dec!(0.0), dec!(0.0)));
4547    }
4548
4549    #[test]
4550    fn test_surface_with_negative_values() {
4551        let mut points = BTreeSet::new();
4552        points.insert(Point3D {
4553            x: dec!(-1.0),
4554            y: dec!(-2.0),
4555            z: dec!(-3.0),
4556        });
4557        points.insert(Point3D {
4558            x: dec!(-4.0),
4559            y: dec!(-5.0),
4560            z: dec!(-6.0),
4561        });
4562
4563        let surface = Surface {
4564            points,
4565            x_range: (dec!(-4.0), dec!(-1.0)),
4566            y_range: (dec!(-5.0), dec!(-2.0)),
4567        };
4568
4569        let serialized = serde_json::to_string(&surface).unwrap();
4570        let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4571
4572        assert_eq!(surface.points, deserialized.points);
4573        assert_eq!(surface.x_range, deserialized.x_range);
4574        assert_eq!(surface.y_range, deserialized.y_range);
4575    }
4576
4577    #[test]
4578    fn test_surface_with_high_precision() {
4579        let mut points = BTreeSet::new();
4580        points.insert(Point3D {
4581            x: dec!(1.12345678901234567890),
4582            y: dec!(2.12345678901234567890),
4583            z: dec!(3.12345678901234567890),
4584        });
4585        points.insert(Point3D {
4586            x: dec!(4.12345678901234567890),
4587            y: dec!(5.12345678901234567890),
4588            z: dec!(6.12345678901234567890),
4589        });
4590
4591        let surface = Surface {
4592            points,
4593            x_range: (dec!(1.12345678901234567890), dec!(4.12345678901234567890)),
4594            y_range: (dec!(2.12345678901234567890), dec!(5.12345678901234567890)),
4595        };
4596
4597        let serialized = serde_json::to_string(&surface).unwrap();
4598        let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4599
4600        assert_eq!(surface.points, deserialized.points);
4601        assert_eq!(surface.x_range, deserialized.x_range);
4602        assert_eq!(surface.y_range, deserialized.y_range);
4603    }
4604
4605    #[test]
4606    fn test_invalid_json() {
4607        // Missing required fields
4608        let json_str = r#"{"points": []}"#;
4609        let result = serde_json::from_str::<Surface>(json_str);
4610        assert!(result.is_err());
4611
4612        // Invalid points format
4613        let json_str = r#"{"points": [1, 2, 3], "x_range": [0, 1], "y_range": [0, 1]}"#;
4614        let result = serde_json::from_str::<Surface>(json_str);
4615        assert!(result.is_err());
4616
4617        // Invalid range format
4618        let json_str = r#"{"points": [], "x_range": "invalid", "y_range": [0, 1]}"#;
4619        let result = serde_json::from_str::<Surface>(json_str);
4620        assert!(result.is_err());
4621    }
4622
4623    #[test]
4624    fn test_json_structure() {
4625        let surface = create_test_surface();
4626        let serialized = serde_json::to_string(&surface).unwrap();
4627        let json: serde_json::Value = serde_json::from_str(&serialized).unwrap();
4628
4629        // Check structure
4630        assert!(json.is_object());
4631        assert!(json.get("points").is_some());
4632        assert!(json.get("x_range").is_some());
4633        assert!(json.get("y_range").is_some());
4634
4635        // Check points is an array
4636        assert!(json.get("points").unwrap().is_array());
4637
4638        // Check ranges are arrays of 2 elements
4639        let x_range = json.get("x_range").unwrap().as_array().unwrap();
4640        let y_range = json.get("y_range").unwrap().as_array().unwrap();
4641        assert_eq!(x_range.len(), 2);
4642        assert_eq!(y_range.len(), 2);
4643    }
4644
4645    #[test]
4646    fn test_multiple_surfaces() {
4647        let surface1 = create_test_surface();
4648        let mut surface2 = create_test_surface();
4649        surface2.x_range = (dec!(8.0), dec!(14.0));
4650        surface2.y_range = (dec!(9.0), dec!(15.0));
4651
4652        let surfaces = vec![surface1, surface2];
4653        let serialized = serde_json::to_string(&surfaces).unwrap();
4654        let deserialized: Vec<Surface> = serde_json::from_str(&serialized).unwrap();
4655
4656        assert_eq!(surfaces.len(), deserialized.len());
4657        assert_eq!(surfaces[0].points, deserialized[0].points);
4658        assert_eq!(surfaces[1].points, deserialized[1].points);
4659    }
4660
4661    #[test]
4662    fn test_ordering_preservation() {
4663        let surface = create_test_surface();
4664        let serialized = serde_json::to_string(&surface).unwrap();
4665        let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4666
4667        // Convert points to vectors to check ordering
4668        let original_points: Vec<_> = surface.points.into_iter().collect();
4669        let deserialized_points: Vec<_> = deserialized.points.into_iter().collect();
4670
4671        // Check if points maintain their order
4672        assert_eq!(original_points, deserialized_points);
4673    }
4674
4675    #[test]
4676    fn test_surface_with_extremes() {
4677        let mut points = BTreeSet::new();
4678        points.insert(Point3D {
4679            x: Decimal::MAX,
4680            y: Decimal::MAX,
4681            z: Decimal::MAX,
4682        });
4683        points.insert(Point3D {
4684            x: Decimal::MIN,
4685            y: Decimal::MIN,
4686            z: Decimal::MIN,
4687        });
4688
4689        let surface = Surface {
4690            points,
4691            x_range: (Decimal::MIN, Decimal::MAX),
4692            y_range: (Decimal::MIN, Decimal::MAX),
4693        };
4694
4695        let serialized = serde_json::to_string(&surface).unwrap();
4696        let deserialized: Surface = serde_json::from_str(&serialized).unwrap();
4697
4698        assert_eq!(surface.points, deserialized.points);
4699        assert_eq!(surface.x_range, deserialized.x_range);
4700        assert_eq!(surface.y_range, deserialized.y_range);
4701    }
4702
4703    #[test]
4704    fn test_surface_points_array_format() {
4705        // Test that points can be deserialized from array format
4706        let json_str = r#"{
4707            "points": [
4708                [1.0, 2.0, 3.0],
4709                [4.0, 5.0, 6.0]
4710            ],
4711            "x_range": [1.0, 4.0],
4712            "y_range": [2.0, 5.0]
4713        }"#;
4714
4715        let result = serde_json::from_str::<Surface>(json_str);
4716        assert!(result.is_ok());
4717
4718        let surface = result.unwrap();
4719        assert_eq!(surface.points.len(), 2);
4720    }
4721}
4722
4723#[cfg(test)]
4724mod tests_coefficient_of_variation {
4725    use super::*;
4726    use crate::curves::{Curve, Point2D};
4727    use crate::geometrics::MetricsExtractor;
4728
4729    /// A 3 x 3 grid whose `z` values are `z0 + 0..9`.
4730    fn grid(z0: i64) -> Surface {
4731        Surface::new(
4732            (0..3i64)
4733                .flat_map(|i| {
4734                    (0..3i64).map(move |j| {
4735                        Point3D::new(
4736                            Decimal::from(i),
4737                            Decimal::from(j),
4738                            Decimal::from(z0 + i * 3 + j),
4739                        )
4740                    })
4741                })
4742                .collect(),
4743        )
4744    }
4745
4746    /// `z = 1..9`: mean 5, population standard deviation `sqrt(80 / 12)`,
4747    /// so the coefficient of variation is `0.5163977794943222513572353866`,
4748    /// correct to `Decimal`'s 28 places, where `beta` was a placeholder zero.
4749    /// A curve over the same values gives the same digits: both compute it
4750    /// as `std_dev / mean` (#824).
4751    #[test]
4752    fn test_risk_metrics_coefficient_of_variation_matches_the_curve() {
4753        let surface = grid(1).compute_risk_metrics().unwrap();
4754        assert_eq!(
4755            surface.coefficient_of_variation,
4756            dec!(0.5163977794943222513572353866)
4757        );
4758        let curve = Curve::new(
4759            (1..=9i64)
4760                .map(|y| Point2D::new(Decimal::from(y), Decimal::from(y)))
4761                .collect(),
4762        );
4763        assert_eq!(
4764            curve
4765                .compute_risk_metrics()
4766                .unwrap()
4767                .coefficient_of_variation,
4768            surface.coefficient_of_variation
4769        );
4770    }
4771
4772    /// A curve and a surface over the same values report the same risk
4773    /// metrics, digit for digit: both take the volatility from one helper,
4774    /// the population standard deviation `sqrt(80 / 12)` for `1..9` (#840).
4775    #[test]
4776    fn test_risk_metrics_match_a_curve_over_the_same_values() {
4777        let surface = grid(1).compute_risk_metrics().unwrap();
4778        let curve = Curve::new(
4779            (1..=9i64)
4780                .map(|y| Point2D::new(Decimal::from(y), Decimal::from(y)))
4781                .collect(),
4782        )
4783        .compute_risk_metrics()
4784        .unwrap();
4785        assert_eq!(surface.volatility, dec!(2.5819888974716112567861769332));
4786        assert_eq!(curve.volatility, surface.volatility);
4787        assert_eq!(curve.value_at_risk, surface.value_at_risk);
4788        assert_eq!(curve.expected_shortfall, surface.expected_shortfall);
4789        assert_eq!(
4790            curve.coefficient_of_variation,
4791            surface.coefficient_of_variation
4792        );
4793        assert_eq!(curve.sharpe_ratio, surface.sharpe_ratio);
4794    }
4795
4796    /// `z = -4..4` has a zero mean: an error, not a zero (#824).
4797    #[test]
4798    fn test_risk_metrics_zero_mean_is_an_error() {
4799        assert!(matches!(
4800            grid(-4).compute_risk_metrics(),
4801            Err(MetricsError::ZeroMean {
4802                metric: "coefficient of variation"
4803            })
4804        ));
4805    }
4806}
4807
4808/// The neighbour search of #858 against the full sort it replaced: the same
4809/// points in the same order, ties broken the same way, and the same error
4810/// when a distance overflows.
4811#[cfg(test)]
4812mod tests_nearest_matches_sort {
4813    use super::*;
4814
4815    /// What the interpolators did before #858: key every point, sort, keep
4816    /// the first `k`.
4817    fn sorted_prefix(
4818        surface: &Surface,
4819        x: Decimal,
4820        y: Decimal,
4821        k: usize,
4822    ) -> Result<Vec<&Point3D>, DecimalError> {
4823        let mut all: Vec<&Point3D> = surface.points.iter().collect();
4824        sort_by_distance(&mut all, x, y, "test")?;
4825        all.truncate(k);
4826        Ok(all)
4827    }
4828
4829    fn check(surface: &Surface, x: Decimal, y: Decimal) {
4830        for k in [1, 2, 3, 4, 5, 9, 10] {
4831            let fast = nearest(&surface.points, x, y, k, "test");
4832            let sorted = sorted_prefix(surface, x, y, k);
4833            assert_eq!(
4834                format!("{fast:?}"),
4835                format!("{sorted:?}"),
4836                "k = {k} at ({x}, {y}) on {:?}",
4837                surface.points
4838            );
4839        }
4840        let xy = Point2D::new(x, y);
4841        let filtered: Vec<&Point3D> = surface
4842            .points
4843            .iter()
4844            .filter(|p| p.x == x && p.y == y)
4845            .collect();
4846        assert_eq!(surface.points_at(&xy).collect::<Vec<_>>(), filtered);
4847        assert_eq!(surface.contains_point(&xy), !filtered.is_empty());
4848        assert_eq!(surface.get_point(&xy), filtered.first().copied());
4849        assert_eq!(
4850            surface.get_values(xy),
4851            filtered.iter().map(|p| &p.z).collect::<Vec<_>>()
4852        );
4853    }
4854
4855    fn surface_of(points: &[(i64, i64, i64)]) -> Surface {
4856        Surface::new(
4857            points
4858                .iter()
4859                .map(|&(x, y, z)| {
4860                    Point3D::new(Decimal::from(x), Decimal::from(y), Decimal::from(z))
4861                })
4862                .collect(),
4863        )
4864    }
4865
4866    /// Every sample, the cell centres around it, and points outside.
4867    fn probes(surface: &Surface) -> Vec<(Decimal, Decimal)> {
4868        let mut at = vec![
4869            (dec!(-50), dec!(-50)),
4870            (dec!(50), dec!(3)),
4871            (dec!(0.5), dec!(0.5)),
4872        ];
4873        for p in &surface.points {
4874            at.push((p.x, p.y));
4875            at.push((p.x + dec!(0.5), p.y + dec!(0.5)));
4876            at.push((p.x - dec!(0.5), p.y));
4877        }
4878        at
4879    }
4880
4881    #[test]
4882    fn grids_with_ties() {
4883        // On a regular grid most queries have several neighbours at one
4884        // distance, so the tie-break decides which ones are kept.
4885        let mut grid = Vec::new();
4886        for i in 0..6 {
4887            for j in 0..6 {
4888                grid.push((i, j, i * j));
4889            }
4890        }
4891        let surface = surface_of(&grid);
4892        for (x, y) in probes(&surface) {
4893            check(&surface, x, y);
4894        }
4895        check(&surface, dec!(2.5), dec!(2.5));
4896        check(&surface, dec!(3), dec!(2.5));
4897    }
4898
4899    #[test]
4900    fn small_and_stacked_surfaces() {
4901        let cases: Vec<Vec<(i64, i64, i64)>> = vec![
4902            vec![],
4903            vec![(1, 1, 1)],
4904            vec![(1, 1, 1), (1, 1, 2), (1, 1, 3), (1, 1, 4)],
4905            vec![(0, 0, 0), (0, 0, 9), (1, 0, 1), (0, 1, 1), (1, 1, 2)],
4906            vec![(0, 0, 0), (2, 0, 0), (-2, 0, 0), (0, 2, 0), (0, -2, 0)],
4907        ];
4908        for points in cases {
4909            let surface = surface_of(&points);
4910            for (x, y) in probes(&surface) {
4911                check(&surface, x, y);
4912            }
4913        }
4914    }
4915
4916    #[test]
4917    fn random_surfaces() {
4918        let mut state: u64 = 0x9e37_79b9_7f4a_7c15;
4919        let mut next = |bound: u64| {
4920            state ^= state << 13;
4921            state ^= state >> 7;
4922            state ^= state << 17;
4923            state % bound
4924        };
4925        for _ in 0..400 {
4926            let len = next(40) as usize;
4927            let points: Vec<(i64, i64, i64)> = (0..len)
4928                .map(|_| (next(8) as i64 - 2, next(8) as i64 - 2, next(4) as i64))
4929                .collect();
4930            let surface = surface_of(&points);
4931            for (x, y) in probes(&surface) {
4932                check(&surface, x, y);
4933            }
4934        }
4935    }
4936
4937    #[test]
4938    fn distances_that_overflow_report_like_the_sort() {
4939        // A distance squared past `Decimal::MAX`: the walk falls back to the
4940        // sort, so the error names the same point and operands.
4941        let far = Decimal::from(10i64.pow(15));
4942        let surface = Surface::new(
4943            [
4944                Point3D::new(dec!(0), dec!(0), dec!(1)),
4945                Point3D::new(dec!(1), dec!(0), dec!(2)),
4946                Point3D::new(dec!(0), dec!(1), dec!(3)),
4947                Point3D::new(dec!(1), far, dec!(4)),
4948                Point3D::new(far, dec!(1), dec!(5)),
4949            ]
4950            .into_iter()
4951            .collect(),
4952        );
4953        assert!(!distances_fit(
4954            &surface.points,
4955            dec!(0.5),
4956            dec!(0.5),
4957            "test"
4958        ));
4959        let fast = nearest(&surface.points, dec!(0.5), dec!(0.5), 3, "test");
4960        assert!(fast.is_err());
4961        check(&surface, dec!(0.5), dec!(0.5));
4962        // Large coordinates that still fit take the walk and agree.
4963        let wide = Decimal::from(10i64.pow(12));
4964        let surface = Surface::new(
4965            [
4966                Point3D::new(dec!(0), dec!(0), dec!(1)),
4967                Point3D::new(dec!(1), dec!(0), dec!(2)),
4968                Point3D::new(dec!(0), dec!(1), dec!(3)),
4969                Point3D::new(dec!(1), wide, dec!(4)),
4970                Point3D::new(-wide, dec!(1), dec!(5)),
4971            ]
4972            .into_iter()
4973            .collect(),
4974        );
4975        assert!(distances_fit(&surface.points, dec!(0.5), dec!(0.5), "test"));
4976        check(&surface, dec!(0.5), dec!(0.5));
4977    }
4978
4979    #[test]
4980    fn long_mantissas() {
4981        // Thirds do not terminate, so the squared distances round at the
4982        // 28th place and can tie after rounding.
4983        let third = dec!(1) / dec!(3);
4984        let mut points = BTreeSet::new();
4985        for i in 0..5i64 {
4986            for j in 0..5i64 {
4987                let x = third * Decimal::from(i);
4988                let y = third * Decimal::from(j);
4989                points.insert(Point3D::new(x, y, x + y));
4990            }
4991        }
4992        let surface = Surface::new(points);
4993        for (x, y) in probes(&surface) {
4994            check(&surface, x, y);
4995        }
4996        check(&surface, third / dec!(2), third * dec!(1.5));
4997    }
4998}