Skip to main content

optionstratlib_math/curves/
curve.rs

1/******************************************************************************
2   Author: Joaquín Béjar García
3   Email: jb@taunais.com
4   Date: 9/1/25
5******************************************************************************/
6use crate::curves::Point2D;
7use crate::curves::traits::StatisticalCurve;
8use crate::curves::utils::{detect_peaks_and_valleys, grid_coordinate};
9use crate::error::{CurveError, InterpolationError, MetricsError};
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 optionstratlib_core::error::DecimalError;
18use optionstratlib_core::model::decimal::{
19    d_add, d_div, d_mul, d_product_iter, d_sqrt, d_sub, d_sum_iter,
20};
21use optionstratlib_core::utils::Len;
22use rayon::iter::{IntoParallelIterator, ParallelIterator};
23use rayon::prelude::*;
24use rust_decimal::Decimal;
25use rust_decimal_macros::dec;
26use serde::{Deserialize, Serialize};
27use std::collections::{BTreeSet, btree_set};
28use std::fmt::{Display, Formatter};
29use std::ops::{Bound, Index};
30
31/// Represents a mathematical curve as a collection of 2D points.
32///
33/// # Overview
34/// The `Curve` struct is a fundamental representation of a curve, defined as a series
35/// of points in a two-dimensional Cartesian coordinate system. Each curve is associated
36/// with an `x_range`, specifying the inclusive domain of the curve in terms of its x-coordinates.
37///
38/// This structure supports precise mathematical and computational operations, including
39/// interpolation, analysis, transformations, and intersections. The use of `Decimal`
40/// for coordinates ensures high-precision calculations, making it particularly suitable
41/// for scientific, financial, or mathematical applications.
42///
43/// # Usage
44/// The `Curve` struct acts as the basis for high-level operations provided within
45/// the `crate::curves` module. These include (but are not limited to):
46/// - Generating statistical analyses (`CurveAnalysisResult`)
47/// - Performing curve interpolation
48/// - Logical manipulations, such as merging curves (`MergeOperation`)
49/// - Visualizing graphs or curve plots using libraries like `plotters`
50///
51/// # Example Applications
52/// The `Curve` type fits into mathematical or graphical operations such as:
53/// - Modeling data over a range of x-values
54/// - Comparing curves through transformations or intersections
55/// - Calculating derivatives, integrals, and extrema along the curve
56///
57/// # Constraints
58/// - All points in the `points` vector must lie within the specified `x_range`.
59/// - Methods working with `Curve` data will assume that the `points` vector is ordered
60///   by the `x`-coordinate. Non-ordered inputs may lead to undefined behavior in specific
61///   operations.
62/// - A curve is a function of its abscissa: at most one point per `x`. See
63///   [`Curve::new`] for what breaking that rule does to each consumer, and
64///   for why no constructor can enforce it.
65///
66/// # See Also
67/// - [`Point2D`]: The fundamental data type for representing points in 2D space.
68/// - [`MergeOperation`]: Enum for combining multiple curves.
69#[derive(Debug, Clone, Serialize, Deserialize)]
70#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
71#[serde(deny_unknown_fields)]
72pub struct Curve {
73    /// A ordered set of `Point2D` objects that defines the curve in terms of its x-y plane coordinates.
74    /// Points are stored in a `BTreeSet` which automatically maintains them in sorted order by their
75    /// `(x, y)` pair. The set rejects an exact duplicate of that pair; it does not stop two ordinates
76    /// from sharing an abscissa.
77    ///
78    /// Public, so a struct literal can populate it without passing through
79    /// [`Curve::new`]. No constructor can therefore guarantee one point per
80    /// abscissa.
81    pub points: BTreeSet<Point2D>,
82
83    /// A tuple `(min_x, max_x)` that specifies the minimum and maximum x-coordinate values
84    /// for the curve. Operations performed on the curve should ensure they fall within this range.
85    /// Both values are of type `Decimal` to ensure high precision in boundary calculations.
86    pub x_range: (Decimal, Decimal),
87}
88
89impl Display for Curve {
90    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
91        for point in self.points.iter() {
92            writeln!(f, "{point}")?;
93        }
94        Ok(())
95    }
96}
97
98impl Default for Curve {
99    fn default() -> Self {
100        Curve {
101            points: BTreeSet::new(),
102            x_range: (Decimal::ZERO, Decimal::ZERO),
103        }
104    }
105}
106
107impl Curve {
108    /// Creates a new curve from a vector of points.
109    ///
110    /// This constructor initializes a `Curve` instance using a list of 2D points
111    /// provided as a `BTreeSet<Point2D>`. Additionally, the x-range of the curve is calculated
112    /// and stored. The x-range is determined by evaluating the minimum and maximum
113    /// x-coordinates among the provided points.
114    ///
115    /// # Parameters
116    ///
117    /// - `points` (`BTreeSet<Point2D>`): A vector of points that define the curve in a
118    ///   two-dimensional Cartesian coordinate plane.
119    ///
120    /// # Returns
121    ///
122    /// - `Curve`: A newly instantiated curve containing the provided points and
123    ///   the computed x-range.
124    ///
125    /// # Behavior
126    ///
127    /// - Calculates the x-range of the points using `calculate_range()`.
128    /// - Stores the provided points for later use in curve-related calculations.
129    ///
130    /// # See Also
131    ///
132    /// - [`Point2D`]: The type of points used to define the curve.
133    /// - [`crate::curves::Curve::calculate_range`]: Computes the x-range of a set of points.
134    /// # One point per abscissa
135    ///
136    /// A `Curve` is a function of its abscissa: at most one point per `x`.
137    /// Every consumer reads it that way.
138    /// [`get_point`](crate::geometrics::AxisOperations::get_point) and
139    /// [`contains_point`](crate::geometrics::AxisOperations::contains_point)
140    /// answer from the first point matching `x`,
141    /// [`merge`](crate::geometrics::Arithmetic::merge) resamples every curve
142    /// onto one shared x-grid with one `y` per `x`, and the interpolators in
143    /// [`crate::geometrics`] bracket `x` between two consecutive points and
144    /// divide by the width of that bracket.
145    ///
146    /// Nothing enforces the rule. [`Point2D`] orders on the full `(x, y)`
147    /// pair, so `points` may hold several ordinates for one abscissa and
148    /// `new` stores them all; a curve built that way is outside the contract
149    /// and the consumers behave as follows:
150    ///
151    /// - all four interpolators return
152    ///   [`InterpolationError::DegenerateInterval`] when asked about a
153    ///   repeated abscissa, and pick no survivor: the bracket around it has
154    ///   zero width and the slope across it is undefined;
155    /// - `get_point` and `merge_axis_interpolate` read the first match in
156    ///   `(x, y)` order, which is the lowest ordinate of the stack, and the
157    ///   rest are invisible to the caller. Use
158    ///   [`get_values`](crate::geometrics::AxisOperations::get_values) to
159    ///   see every ordinate at an abscissa.
160    ///
161    /// Normalizing here would not help: `points` is a `pub` field, so a
162    /// struct literal reaches the same state without going through any
163    /// constructor. Rejecting or collapsing a duplicate can only ever be a
164    /// convenience on this path, never an invariant of the type.
165    ///
166    /// A projection of a surface is genuinely multi-valued and is therefore
167    /// not a curve: [`crate::surfaces::Surface::project_onto`] returns a
168    /// `Vec<Point2D>`, and aggregating it to one ordinate per abscissa is
169    /// the caller's job.
170    #[must_use]
171    pub fn new(points: BTreeSet<Point2D>) -> Self {
172        let x_range = Self::calculate_range(points.iter().map(|p| p.x));
173        Curve { points, x_range }
174    }
175
176    /// Returns the sample sitting exactly at the abscissa `x`, if the curve
177    /// has one there.
178    ///
179    /// A curve is a function of its abscissa (see [`Curve::new`]), so at most
180    /// one point can sit at `x`. When several do, the curve has no value at
181    /// `x` and no non-arbitrary way to choose among them exists, so this
182    /// reports [`InterpolationError::DegenerateInterval`] instead of
183    /// returning the first, which would be the lowest ordinate of the stack.
184    /// [`AxisOperations::get_values`] reads all of them.
185    fn exact_point_at(&self, x: Decimal) -> Result<Option<Point2D>, InterpolationError> {
186        let mut at_x = self.points_at(x);
187        let Some(point) = at_x.next() else {
188            return Ok(None);
189        };
190        if at_x.next().is_some() {
191            return Err(InterpolationError::DegenerateInterval);
192        }
193        Ok(Some(*point))
194    }
195
196    /// Every point at the abscissa `x`, in `(x, y)` order.
197    ///
198    /// `Point2D` orders on `(x, y)`, so the points sharing an abscissa are
199    /// contiguous in the tree and a range bounded by the extreme ordinates
200    /// reads them in O(log n) instead of filtering every point (#858).
201    fn points_at(&self, x: Decimal) -> btree_set::Range<'_, Point2D> {
202        self.points
203            .range(Point2D { x, y: Decimal::MIN }..=Point2D { x, y: Decimal::MAX })
204    }
205
206    /// The point immediately after `point` in `(x, y)` order.
207    fn point_after(&self, point: &Point2D) -> Option<&Point2D> {
208        self.points
209            .range((Bound::Excluded(*point), Bound::Unbounded))
210            .next()
211    }
212
213    /// The point immediately before `point` in `(x, y)` order.
214    fn point_before(&self, point: &Point2D) -> Option<&Point2D> {
215        self.points.range(..*point).next_back()
216    }
217
218    /// The two consecutive points
219    /// [`find_bracket_points`](Interpolate::find_bracket_points) selects for
220    /// `x`, read off the tree in O(log n) without collecting the points.
221    ///
222    /// The scan it replaces returns the first window `(i, i + 1)` with
223    /// `x[i] <= x <= x[i + 1]`. With `k` the first point whose abscissa is
224    /// at least `x`, that window is `(k - 1, k)`, or `(0, 1)` when `k` is the
225    /// first point, so the same pair is selected, a stack of ordinates at
226    /// `x` included (#858). The errors are the scan's, in the same order.
227    fn bracket(&self, x: Decimal) -> Result<(&Point2D, &Point2D), InterpolationError> {
228        if self.points.len() < 2 {
229            return Err(InterpolationError::EmptyData);
230        }
231        let (Some(first), Some(last)) = (self.points.first(), self.points.last()) else {
232            return Err(InterpolationError::EmptyData);
233        };
234        if x < first.x || x > last.x {
235            return Err(InterpolationError::OutOfRange {
236                target: x.to_string(),
237            });
238        }
239        let floor = Point2D { x, y: Decimal::MIN };
240        // `x <= last.x`, so some point sits at or after `floor`.
241        let right = self
242            .points
243            .range(floor..)
244            .next()
245            .ok_or(InterpolationError::DegenerateInterval)?;
246        match self.points.range(..floor).next_back() {
247            Some(left) => Ok((left, right)),
248            // `right` is the first point: the window is `(0, 1)`, and the
249            // two-point check above guarantees the second exists.
250            None => self
251                .point_after(right)
252                .map(|second| (right, second))
253                .ok_or(InterpolationError::DegenerateInterval),
254        }
255    }
256
257    /// The four samples [`BiLinearInterpolation::bilinear_interpolate`]
258    /// reads at `x`: the near edge `(i, i + 1)` bracketing `x`, then the far
259    /// edge `(f, f + 1)` with `f = min(i + 2, len - 2)`.
260    ///
261    /// The far edge is `i + 2` exactly when two samples follow the near
262    /// edge, and the curve's last two samples otherwise, so the clamp is
263    /// read off the tree without an index (#858).
264    fn bilinear_cell(&self, x: Decimal) -> Result<[&Point2D; 4], InterpolationError> {
265        let (near_left, near_right) = self.bracket(x)?;
266        let mut beyond = self
267            .points
268            .range((Bound::Excluded(*near_right), Bound::Unbounded));
269        let far = match (beyond.next(), beyond.next()) {
270            (Some(left), Some(right)) => Some((left, right)),
271            _ => {
272                let mut tail = self.points.iter().rev();
273                match (tail.next(), tail.next()) {
274                    (Some(right), Some(left)) => Some((left, right)),
275                    _ => None,
276                }
277            }
278        };
279        let (far_left, far_right) = far.ok_or_else(|| {
280            InterpolationError::Bilinear(format!(
281                "the far edge needs two points, the curve has {}",
282                self.points.len()
283            ))
284        })?;
285        Ok([near_left, near_right, far_left, far_right])
286    }
287
288    /// The four samples [`CubicInterpolation::cubic_interpolate`] reads at
289    /// `x`: with `(i, i + 1)` the bracket, the window starts at 0 for the
290    /// first segment, at `len - 4` for the last one and at `i - 1`
291    /// otherwise.
292    ///
293    /// `i == 0` is a bracket with no sample before it and `i == len - 2` one
294    /// with no sample after it, so the window is read off the tree without
295    /// an index (#858).
296    fn cubic_window(&self, x: Decimal) -> Result<[&Point2D; 4], InterpolationError> {
297        let (left, right) = self.bracket(x)?;
298        let window = match (self.point_before(left), self.point_after(right)) {
299            // `i == 0`: the first four samples.
300            (None, _) => {
301                let mut head = self.points.iter();
302                (head.next(), head.next(), head.next(), head.next())
303            }
304            // `i == len - 2`: the last four samples.
305            (Some(_), None) => {
306                let mut tail = self.points.iter().rev();
307                let (p3, p2, p1, p0) = (tail.next(), tail.next(), tail.next(), tail.next());
308                (p0, p1, p2, p3)
309            }
310            (Some(before), Some(after)) => (Some(before), Some(left), Some(right), Some(after)),
311        };
312        match window {
313            (Some(p0), Some(p1), Some(p2), Some(p3)) => Ok([p0, p1, p2, p3]),
314            _ => Err(InterpolationError::Cubic(format!(
315                "the window needs four points, the curve has {}",
316                self.points.len()
317            ))),
318        }
319    }
320
321    /// The checks [`SplineInterpolation::spline_interpolate`] runs before
322    /// it solves the spline, in its order: at least three points, `x`
323    /// inside the knots, then the exact-sample branch. `Some` is the sample
324    /// sitting at `x`, which is the answer without solving.
325    fn spline_precheck(&self, x: Decimal) -> Result<Option<Point2D>, InterpolationError> {
326        let len = self.points.len();
327
328        // Need at least 3 points for spline interpolation
329        if len < 3 {
330            return Err(InterpolationError::Spline(
331                "Need at least three points for spline interpolation".to_string(),
332            ));
333        }
334
335        // Check if x is within the valid range
336        let (Some(first), Some(last)) = (self.points.first(), self.points.last()) else {
337            return Err(InterpolationError::Spline(format!(
338                "point index 0 is out of bounds for a curve of {len} points"
339            )));
340        };
341        if x < first.x || x > last.x {
342            return Err(InterpolationError::Spline(
343                "x is outside the range of points".to_string(),
344            ));
345        }
346
347        // For exact points, return the actual point value. A stack of
348        // ordinates at `x` has no single value, so it errors out instead of
349        // yielding the lowest of them.
350        self.exact_point_at(x)
351    }
352}
353
354/// A natural cubic spline through the points of a [`Curve`], solved once
355/// and read many times.
356///
357/// [`SplineInterpolation::spline_interpolate`] solves the tridiagonal
358/// system for the spline's second derivatives on every call, so each read
359/// costs O(n) `Decimal` arithmetic. [`CurveSpline::new`] solves it once;
360/// each [`CurveSpline::interpolate`] then locates the segment by binary
361/// search and evaluates one cubic. Use it when one curve is read at many
362/// abscissas: a resampling, a chart, a smile lookup across a chain.
363///
364/// # Same answers as `spline_interpolate`
365///
366/// `spline.interpolate(x)` returns exactly what `curve.spline_interpolate(x)`
367/// returns, to the last digit and including the error and its message: the
368/// same pre-checks run in the same order, the solve and the evaluation are
369/// the same code, and a curve whose system cannot be solved (a repeated
370/// abscissa, an overflowing step) reports that error on every read that
371/// reaches the solve, as `spline_interpolate` does.
372///
373/// The spline borrows the curve, so the curve cannot change while the
374/// solved system is in use.
375///
376/// # Examples
377///
378/// ```rust
379/// use optionstratlib_math::curves::{Curve, CurveSpline, Point2D};
380/// use optionstratlib_math::geometrics::SplineInterpolation;
381/// use rust_decimal_macros::dec;
382///
383/// let curve = Curve::new(
384///     [(0, 0), (1, 1), (2, 4), (3, 9), (4, 16)]
385///         .into_iter()
386///         .map(|(x, y)| Point2D::new(x, y))
387///         .collect(),
388/// );
389/// let spline = CurveSpline::new(&curve);
390/// for x in [dec!(0.5), dec!(1.25), dec!(3.9)] {
391///     assert_eq!(
392///         spline.interpolate(x).unwrap(),
393///         curve.spline_interpolate(x).unwrap()
394///     );
395/// }
396/// assert!(spline.interpolate(dec!(5)).is_err());
397/// ```
398#[derive(Debug, Clone)]
399pub struct CurveSpline<'a> {
400    curve: &'a Curve,
401    knots: Vec<&'a Point2D>,
402    /// The solved second derivatives, `None` when the curve has fewer than
403    /// three points or its system cannot be solved.
404    moments: Option<Vec<Decimal>>,
405}
406
407impl<'a> CurveSpline<'a> {
408    /// Solves the natural cubic spline through `curve`'s points.
409    ///
410    /// Never fails: a curve the spline cannot be built on reports why on
411    /// each [`CurveSpline::interpolate`] call that needs the solve, exactly
412    /// as [`SplineInterpolation::spline_interpolate`] would.
413    #[must_use]
414    pub fn new(curve: &'a Curve) -> Self {
415        let knots: Vec<&'a Point2D> = curve.points.iter().collect();
416        let moments = if knots.len() < 3 {
417            None
418        } else {
419            spline_moments(&knots).ok()
420        };
421        CurveSpline {
422            curve,
423            knots,
424            moments,
425        }
426    }
427
428    /// Reads the spline at `x`.
429    ///
430    /// # Errors
431    ///
432    /// The errors of [`SplineInterpolation::spline_interpolate`] at the same
433    /// `x`, with the same messages:
434    ///
435    /// - [`InterpolationError::Spline`] when the curve has fewer than three
436    ///   points, when `x` is outside its knots, or when a checked step leaves
437    ///   the `Decimal` range;
438    /// - [`InterpolationError::DegenerateInterval`] when several points sit
439    ///   at `x`, or when a repeated abscissa elsewhere collapses a knot
440    ///   interval.
441    pub fn interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
442        if let Some(point) = self.curve.spline_precheck(x)? {
443            return Ok(point);
444        }
445        match &self.moments {
446            Some(moments) => spline_eval(&self.knots, moments, x),
447            None => self.unsolvable(),
448        }
449    }
450
451    /// Re-runs the solve that failed in [`CurveSpline::new`] to return its
452    /// error. The solve is deterministic, so this is the error
453    /// `spline_interpolate` returns.
454    #[cold]
455    fn unsolvable(&self) -> Result<Point2D, InterpolationError> {
456        spline_moments(&self.knots).and_then(|_| {
457            Err(InterpolationError::Spline(
458                "the spline system solved on retry after failing once".to_string(),
459            ))
460        })
461    }
462}
463
464/// Wraps a checked-arithmetic failure raised while building or transforming a
465/// curve.
466fn construction_err(err: DecimalError) -> CurveError {
467    CurveError::ConstructionError(err.to_string())
468}
469
470/// Wraps a checked-arithmetic failure raised while analysing a curve.
471fn analysis_err(err: DecimalError) -> CurveError {
472    CurveError::AnalysisError(err.to_string())
473}
474
475/// Wraps a checked-arithmetic failure in the interpolation variant of the
476/// algorithm that raised it.
477fn interp_err(
478    kind: fn(String) -> InterpolationError,
479) -> impl Fn(DecimalError) -> InterpolationError {
480    move |err| kind(err.to_string())
481}
482
483/// Reads one sample of a sorted metric series, naming the statistic on the
484/// out-of-bounds path instead of panicking.
485/// `3 * len / 4`, the index of the third quartile of `len` sorted samples.
486fn third_quartile_index(len: usize) -> Result<usize, MetricsError> {
487    len.checked_mul(3)
488        .map(|three_len| three_len / 4)
489        .ok_or_else(|| {
490            MetricsError::BasicError(format!("third quartile index of {len} samples overflowed"))
491        })
492}
493
494fn sample_at(
495    values: &[Decimal],
496    index: usize,
497    what: &'static str,
498) -> Result<Decimal, MetricsError> {
499    values.get(index).copied().ok_or_else(|| {
500        MetricsError::BasicError(format!(
501            "{what}: sample {index} is out of bounds for {} values",
502            values.len()
503        ))
504    })
505}
506
507/// Arithmetic mean of a sample. Errors on an empty sample, where the mean is
508/// undefined, and on a sum that leaves the representable range.
509fn mean_of(values: &[Decimal], op: &'static str) -> Result<Decimal, DecimalError> {
510    let sum = d_sum_iter(values.iter().copied(), op)?;
511    d_div(sum, Decimal::from(values.len()), op)
512}
513
514/// Population variance of a sample about `mean`.
515fn variance_of(
516    values: &[Decimal],
517    mean: Decimal,
518    op: &'static str,
519) -> Result<Decimal, DecimalError> {
520    let mut acc = Decimal::ZERO;
521    for &value in values {
522        let centered = d_sub(value, mean, op)?;
523        let squared = powu_checked(centered, 2, op)?;
524        acc = d_add(acc, squared, op)?;
525    }
526    d_div(acc, Decimal::from(values.len()), op)
527}
528
529/// Mean of `(centered / std_dev)^order` over a pre-centered sample: the
530/// standardized moment behind skewness (`order = 3`) and kurtosis
531/// (`order = 4`).
532fn standardized_moment(
533    centered: &[Decimal],
534    std_dev: Decimal,
535    order: u64,
536    op: &'static str,
537) -> Result<Decimal, DecimalError> {
538    let mut acc = Decimal::ZERO;
539    for &value in centered {
540        let z = d_div(value, std_dev, op)?;
541        let moment = powu_checked(z, order, op)?;
542        acc = d_add(acc, moment, op)?;
543    }
544    d_div(acc, Decimal::from(centered.len()), op)
545}
546
547/// Reads one slot of a spline working band, naming the band on the
548/// out-of-bounds path instead of panicking.
549/// Shifts a loop index by `by` positions. The interpolation loops keep every
550/// shifted index inside the curve, so this never fails for them; the checked
551/// form keeps a wrong bound from wrapping or aborting (#788).
552fn shift_index(
553    index: usize,
554    by: isize,
555    kind: fn(String) -> InterpolationError,
556) -> Result<usize, InterpolationError> {
557    index.checked_add_signed(by).ok_or_else(|| {
558        kind(format!(
559            "index {index} shifted by {by} leaves the usize range"
560        ))
561    })
562}
563
564fn band_at(
565    band: &[Decimal],
566    index: usize,
567    name: &'static str,
568) -> Result<Decimal, InterpolationError> {
569    band.get(index).copied().ok_or_else(|| {
570        InterpolationError::Spline(format!(
571            "spline band `{name}` has no slot {index} (len {})",
572            band.len()
573        ))
574    })
575}
576
577/// Mutable counterpart of [`band_at`].
578fn band_at_mut<'a>(
579    band: &'a mut [Decimal],
580    index: usize,
581    name: &'static str,
582) -> Result<&'a mut Decimal, InterpolationError> {
583    let len = band.len();
584    band.get_mut(index).ok_or_else(|| {
585        InterpolationError::Spline(format!(
586            "spline band `{name}` has no slot {index} (len {len})"
587        ))
588    })
589}
590
591/// Evaluates `moment * d^3 / six_h`, the cubic half of a natural-spline
592/// segment, one checked step at a time.
593fn cube_scaled(
594    moment: Decimal,
595    d: Decimal,
596    six_h: Decimal,
597    op: &'static str,
598) -> Result<Decimal, InterpolationError> {
599    let scaled = d_mul(moment, d, op).map_err(interp_err(InterpolationError::Spline))?;
600    let scaled = d_mul(scaled, d, op).map_err(interp_err(InterpolationError::Spline))?;
601    let scaled = d_mul(scaled, d, op).map_err(interp_err(InterpolationError::Spline))?;
602    d_div(scaled, six_h, op).map_err(interp_err(InterpolationError::Spline))
603}
604
605/// Evaluates `(y / h - moment * h / 6) * d`, the linear half of a
606/// natural-spline segment, one checked step at a time.
607fn linear_term(
608    y: Decimal,
609    moment: Decimal,
610    h: Decimal,
611    d: Decimal,
612    op: &'static str,
613) -> Result<Decimal, InterpolationError> {
614    let level = d_div(y, h, op).map_err(interp_err(InterpolationError::Spline))?;
615    let bend = d_mul(moment, h, op).map_err(interp_err(InterpolationError::Spline))?;
616    let bend = d_div(bend, dec!(6), op).map_err(interp_err(InterpolationError::Spline))?;
617    let slope = d_sub(level, bend, op).map_err(interp_err(InterpolationError::Spline))?;
618    d_mul(slope, d, op).map_err(interp_err(InterpolationError::Spline))
619}
620
621impl Len for Curve {
622    fn len(&self) -> usize {
623        self.points.len()
624    }
625
626    fn is_empty(&self) -> bool {
627        self.points.is_empty()
628    }
629}
630
631impl GeometricObject<Point2D, Decimal> for Curve {
632    type Error = CurveError;
633
634    fn get_points(&self) -> BTreeSet<&Point2D> {
635        self.points.iter().collect()
636    }
637
638    /// Builds a curve from a vector of convertible points.
639    ///
640    /// # One point per abscissa
641    ///
642    /// Same rule and same non-enforcement as [`Curve::new`]: a curve is a
643    /// function of its abscissa, and this constructor does not check it.
644    /// `points` is collected into a `BTreeSet`, which drops an exact
645    /// duplicate of an `(x, y)` pair but keeps two ordinates that merely
646    /// share an `x`. Aggregate before calling if the input can carry
647    /// several ordinates per abscissa, as the projection returned by
648    /// [`crate::surfaces::Surface::project_onto`] does.
649    fn from_vector<T>(points: Vec<T>) -> Self
650    where
651        T: Into<Point2D> + Clone,
652    {
653        let points: BTreeSet<Point2D> = points.into_iter().map(|p| p.into()).collect();
654
655        let x_range = Self::calculate_range(points.iter().map(|p| p.x));
656        Curve { points, x_range }
657    }
658
659    fn construct<T>(method: T) -> Result<Self, Self::Error>
660    where
661        Self: Sized,
662        T: Into<ConstructionMethod<Point2D, Decimal, CurveError>>,
663    {
664        let method = method.into();
665        match method {
666            ConstructionMethod::FromData { points } => {
667                if points.is_empty() {
668                    return Err(CurveError::Point2DError {
669                        reason: "Empty points array",
670                    });
671                }
672                Ok(Curve::new(points))
673            }
674            ConstructionMethod::Parametric { f, params } => {
675                let (t_start, t_end, steps) = match params {
676                    ConstructionParams::D2 {
677                        t_start,
678                        t_end,
679                        steps,
680                    } => (t_start, t_end, steps),
681                    _ => {
682                        return Err(CurveError::ConstructionError(
683                            "Invalid parameters".to_string(),
684                        ));
685                    }
686                };
687                if steps == 0 {
688                    return Err(CurveError::ConstructionError(
689                        "Parametric construction needs at least one step".to_string(),
690                    ));
691                }
692                let op = "Curve::construct::step_size";
693                let span = d_sub(t_end, t_start, op).map_err(construction_err)?;
694                let step_size = d_div(span, Decimal::from(steps), op).map_err(construction_err)?;
695
696                let points: Result<BTreeSet<Point2D>, CurveError> = (0..=steps)
697                    .into_par_iter()
698                    .map(|i| {
699                        let offset = d_mul(step_size, Decimal::from(i), "Curve::construct::offset")
700                            .map_err(construction_err)?;
701                        // The last `t` is `t_end` itself, the rest stay on its
702                        // `t_start` side (#799).
703                        let t = grid_coordinate(
704                            d_add(t_start, offset, "Curve::construct::t")
705                                .map_err(construction_err)?,
706                            t_start,
707                            t_end,
708                            i == steps,
709                        );
710                        f(t)
711                    })
712                    .collect();
713
714                points.map(Curve::new)
715            }
716        }
717    }
718}
719
720/// Allows indexed access to the points in a `Curve` using `usize` indices.
721///
722/// # Overview
723/// This implementation provides intuitive, array-like access to the points
724/// within a `Curve`. By using the `Index<usize>` trait, users can directly
725/// reference specific points by their index within the internal `points` collection
726/// without manually iterating or managing indices themselves.
727///
728/// # Behavior
729/// - The `index` method fetches the `Point2D` at the specified position in the order
730///   of the curve's `points` (sorted by the `Point2D` ordering, typically based on the `x` values).
731/// - If the specified index exceeds the range of available points, it triggers a panic
732///   with the message `"Index out of bounds"`.
733///
734/// # Constraints
735/// - The index must be a valid value between `0` and `self.points.len() - 1`.
736/// - The `Curve`'s `points` are internally stored as a `BTreeSet<Point2D>`, so indexing
737///   reflects the natural order of the set, which is determined by the `Ord` trait
738///   implementation for `Point2D`.
739///
740/// # Fields Accessed
741/// - **`points`**: A `BTreeSet` of `Point2D` structs representing the curve's 2D points.
742///
743/// # Panics
744/// This implementation will panic if:
745/// - The index provided is out of bounds (less than `0` or greater than/equal to the number
746///   of points in the curve).
747///
748/// # Use Cases
749/// - Quickly accessing specific points on a curve during visualization, interpolation,
750///   or analysis operations.
751/// - Performing operations that require stepwise access to points, such as
752///   slicing or filtering points along the curve.
753///
754/// # Example
755/// Suppose you have a `Curve` instance `curve` with multiple points:
756/// ```ignore
757/// let point = curve[0]; // Access the first point
758/// ```
759///
760/// # Important Notes
761/// - This indexing implementation provides read-only access (`&Self::Output`).
762/// - Modifying the `points` collection or its contents directly is not allowed through
763///   this implementation, ensuring immutability when using indexed access.
764///
765/// # Type Associations
766/// - **Input**:
767///   - The input type for the `Index` operation is `usize`, the standard for indexing.
768/// - **Output**:
769///   - The output type for the `Index` operation is a reference to `Point2D`,
770///     specifically `&Point2D`.
771///
772/// # Key Implementations
773/// - **`Index<usize>`**: Provides indexing-based access to curve points.
774///
775/// # Panics
776///
777/// Panics if `index >= self.points.len()`, the contract of
778/// [`std::ops::Index`] and of `Vec`. `Index` has no error channel; for a
779/// lookup that reports a missing index, use `get_points().into_iter().nth(index)`.
780impl Index<usize> for Curve {
781    type Output = Point2D;
782
783    /// Fetches the `Point2D` at the specified index.
784    ///
785    /// # Panics
786    ///
787    /// Panics if `index >= self.points.len()`. This matches the
788    /// documented contract of [`std::ops::Index`].
789    fn index(&self, index: usize) -> &Self::Output {
790        // INVARIANT: `std::ops::Index` requires returning `&Self::Output`
791        // by value, so there is no safe way to signal an out-of-bounds
792        // index other than panicking. The contract mirrors `Vec<T>::index`
793        // and is documented above.
794        match self.points.iter().nth(index) {
795            Some(p) => p,
796            None => {
797                // INVARIANT: `std::ops::Index` returns `&Self::Output` and has
798                // no fallible channel; the contract mirrors `Vec::index`. No
799                // library code reaches this arm: no internal lookup indexes
800                // a curve.
801                let len = self.points.len();
802                panic!("Curve::index: out of bounds (index = {index}, len = {len})") // scan-banned: allow -- `Index` has no fallible channel and no library call site reaches it
803            }
804        }
805    }
806}
807
808/// Implementation of the `Interpolate` trait for the `Curve` struct.
809///
810/// This implementation integrates the `get_points` method for the `Curve` structure,
811/// providing access to its internal points. The `Interpolate` trait ensures compatibility
812/// with various interpolation methods such as Linear, BiLinear, Cubic, and Spline
813/// interpolations. By implementing this trait, `Curve` gains the ability to perform
814/// interpolation operations and access bracketing points.
815///
816/// # Traits Involved
817///
818/// The `Interpolate` trait is an aggregation of multiple interpolation-related traits:
819/// - [`LinearInterpolation`]
820/// - [`BiLinearInterpolation`]
821/// - [`CubicInterpolation`]
822/// - [`SplineInterpolation`]
823///
824/// These underlying traits implement specific interpolation algorithms,
825/// enabling `Curve` to support a robust set of interpolation options through the associated methods.
826/// Depending on the use case and provided parameters (e.g., interpolation type and target x-coordinate),
827/// the appropriate algorithm is invoked.
828///
829/// # See Also
830///
831/// - [`Curve`]: The underlying mathematical structure being interpolated.
832/// - [`Point2D`]: The fundamental data type for the curve's points.
833/// - [`Interpolate`]: The trait defining interpolation operations.
834///
835impl Interpolate<Point2D, Decimal> for Curve {
836    /// Same contract and same indices as the trait's default, including the
837    /// degenerate window over a repeated abscissa, without collecting the
838    /// points into a `Vec` first: the pair is found in O(log n) and its
839    /// index by counting the points before it (#858).
840    fn find_bracket_points(&self, x: Decimal) -> Result<(usize, usize), InterpolationError> {
841        let (left, _) = self.bracket(x)?;
842        let i = self.points.range(..*left).count();
843        i.checked_add(1)
844            .map(|j| (i, j))
845            .ok_or(InterpolationError::DegenerateInterval)
846    }
847}
848
849/// Implements the `LinearInterpolation` trait for the `Curve` struct.
850///
851/// This implementation provides linear interpolation functionality for a given set
852/// of points on a curve. The interpolation computes the `y` value corresponding
853/// to a given `x` value using the linear interpolation formula. The method ensures
854/// that the input `x` is within the range of the curve's defined points.
855///
856/// ```text
857/// y = p1.y + (x - p1.x) * (p2.y - p1.y) / (p2.x - p1.x)
858/// ```
859///
860/// # Parameters
861/// - `x`: A `Decimal` representing the `x`-coordinate for which the corresponding
862///   interpolated `y` value is to be computed.
863///
864/// # Returns
865/// - `Ok(Point2D)`: A `Point2D` instance containing the input `x` value and the
866///   interpolated `y` value.
867/// - `Err(CurvesError)`: Returns an error of type `CurvesError::InterpolationError`
868///   in any of the following cases:
869///     - The curve does not have enough points for interpolation.
870///     - The provided `x` value is outside the range of the curve's points.
871///     - Bracketing points for `x` cannot be found.
872///
873/// # Working Mechanism
874/// 1. The method calls `find_bracket_points` (implemented in the `Interpolate` trait)
875///    to locate the index pair `(i, j)` of two points that bracket the `x` value.
876/// 2. From the located points `p1` and `p2`, the method calculates the interpolated
877///    `y` value using the linear interpolation formula.
878/// 3. Finally, a `Point2D` is created and returned with the provided `x` and the computed
879///    `y` value.
880///
881/// # Implementation Details
882/// - The function leverages `Decimal` arithmetic for high precision in calculations.
883/// - It assumes that the provided points on the curve are sorted in ascending order
884///   based on their `x` values.
885///
886/// # Errors
887/// This method returns a `CurvesError` in the following cases:
888/// - **Insufficient Points**: When the curve has fewer than two points.
889/// - **Out-of-Range `x`**: When the input `x` value lies outside the range of the
890///   defined points.
891/// - **No Bracketing Points Found**: When the method fails to find two points
892///   that bracket the given `x`.
893/// - **Degenerate Bracket**: When several points share the requested
894///   abscissa. The curve has no value there and the bracket around it has
895///   zero width, so the method reports
896///   `InterpolationError::DegenerateInterval` instead of picking one of the
897///   ordinates stacked there.
898///
899/// # One point per abscissa
900///
901/// A curve is a function of its abscissa; see [`Curve::new`]. Asking for a
902/// repeated abscissa returns `InterpolationError::DegenerateInterval`; no
903/// ordinate of the stack is chosen.
904///
905/// Away from the stack the method still answers, reading the stack as a
906/// vertical jump: an `x` strictly inside an interval bounded by a repeated
907/// abscissa brackets against the point on its own side, the highest
908/// ordinate when the stack bounds the interval on the left and the lowest
909/// when it bounds it on the right.
910///
911/// # Example (How it works internally)
912/// Suppose the curve is defined by the following points:
913/// - `p1 = (2.0, 4.0)`
914/// - `p2 = (5.0, 10.0)`
915///
916/// Given `x = 3.0`, the method computes:
917/// ```text
918/// y = 4.0 + (3.0 - 2.0) * (10.0 - 4.0) / (5.0 - 2.0)
919///   = 4 + 1 * 6 / 3
920///   = 4 + 2
921///   = 6.0
922/// ```
923/// It will return `Point2D { x: 3.0, y: 6.0 }`.
924///
925/// # See Also
926/// - `find_bracket_points`: Finds two points that bracket a value.
927/// - `Point2D`: Represents points in 2D space.
928/// - `CurvesError`: Represents errors related to curve operations.
929impl LinearInterpolation<Point2D, Decimal> for Curve {
930    /// # Method
931    /// ### `linear_interpolate`
932    ///
933    /// Performs linear interpolation for a given `x` value by finding two consecutive
934    /// points on the curve (`p1` and `p2`) that bracket the provided `x`. The `y` value
935    /// is then calculated using the linear interpolation formula:
936    fn linear_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
937        // A sample sitting exactly at `x` is the answer, and a stack of
938        // ordinates at `x` has no answer. Without this branch the first
939        // bracket containing `x` ends at the lowest of the stack and the
940        // formula reproduces its ordinate, which is a silent pick.
941        if let Some(point) = self.exact_point_at(x)? {
942            return Ok(point);
943        }
944
945        let (p1, p2) = self.bracket(x)?;
946
947        // A curve is meant to be a function of its abscissa, but nothing
948        // enforces it: a `BTreeSet<Point2D>` holds two points sharing an
949        // abscissa, and the slope across a zero-width bracket is undefined
950        // rather than infinite. Report it instead of picking an ordinate.
951        let run = d_sub(p2.x, p1.x, "Curve::linear_interpolate::run")
952            .map_err(interp_err(InterpolationError::Linear))?;
953        if run.is_zero() {
954            return Err(InterpolationError::DegenerateInterval);
955        }
956
957        // Linear interpolation for y value
958        let dx = d_sub(x, p1.x, "Curve::linear_interpolate::dx")
959            .map_err(interp_err(InterpolationError::Linear))?;
960        let rise = d_sub(p2.y, p1.y, "Curve::linear_interpolate::rise")
961            .map_err(interp_err(InterpolationError::Linear))?;
962        let scaled = d_mul(dx, rise, "Curve::linear_interpolate::scaled")
963            .map_err(interp_err(InterpolationError::Linear))?;
964        let ratio = d_div(scaled, run, "Curve::linear_interpolate::ratio")
965            .map_err(interp_err(InterpolationError::Linear))?;
966        let y = d_add(p1.y, ratio, "Curve::linear_interpolate::y")
967            .map_err(interp_err(InterpolationError::Linear))?;
968
969        Ok(Point2D::new(x, y))
970    }
971}
972
973/// Implementation of the `BiLinearInterpolation` trait for the `Curve` struct.
974///
975/// # What it computes
976///
977/// Bilinear interpolation is defined on a cell of a two-dimensional grid: the
978/// answer is the mean of four corner samples, weighted by the query's
979/// fractional position along each axis. A curve has one axis, so the cell is
980/// built out of two of its segments and the second fraction is fixed at one
981/// half:
982///
983/// - the *near edge* is the segment that brackets `x`, from sample `i` to
984///   sample `i + 1`;
985/// - the *far edge* is the segment two positions further along, from sample
986///   `f` to sample `f + 1`;
987/// - the answer is the mean of the two edges read at the same fraction.
988///
989/// ```text
990/// t    = (x - x[i]) / (x[i+1] - x[i])
991/// near = y[i] + t * (y[i+1] - y[i])
992/// far  = y[f] + t * (y[f+1] - y[f])
993/// y    = (near + far) / 2
994/// ```
995///
996/// # Behaviour near the upper boundary
997///
998/// The far edge is `f = i + 2` while such a segment exists. Over the last two
999/// segments of the curve it does not, and `f` is clamped to `len - 2`, the
1000/// curve's last segment. Two consequences a caller can act on:
1001///
1002/// - on the final segment the two edges coincide, `near` equals `far`, and
1003///   the answer is the linear interpolant there. Not always to the last
1004///   digit: this method normalises `x` before scaling by the rise and
1005///   [`LinearInterpolation::linear_interpolate`] scales first, so where the
1006///   fraction along the segment has no finite decimal expansion the two
1007///   round at scale 28 on different quantities and can part company in the
1008///   28th place;
1009/// - on the second-to-last segment the far edge is the segment immediately
1010///   after the near one instead of the one after that.
1011///
1012/// # Why not clamp the window start
1013///
1014/// The obvious repair, and the one
1015/// [`CubicInterpolation::cubic_interpolate`] applies at its own boundary, is
1016/// to slide the whole window back to the last four samples. Do not reach for
1017/// it here. The window start is also the denominator of `t`, so moving it
1018/// back off the segment holding `x` pushes `t` past `1`. Written out, the
1019/// answer weights the four ordinates it reads by
1020///
1021/// ```text
1022/// y = (1 - t)/2 * y[i] + t/2 * y[i+1] + (1 - t)/2 * y[f] + t/2 * y[f+1]
1023/// ```
1024///
1025/// which sums to one for any `t`, but has every weight non-negative only
1026/// while `t` is in `[0, 1]`. Past `1` the two `(1 - t)/2` weights go
1027/// negative, the answer stops being a convex combination of the cell's
1028/// corners, and what is left is an extrapolation of a cell that does not
1029/// contain `x`: on `(0,0), (1,1), (2,4), (3,9)` the window-start clamp
1030/// returns `9.5` at `x = 2.5`, above every ordinate the curve has. Being a
1031/// convex combination of its cell's corners is what makes bilinear
1032/// interpolation bilinear interpolation, so it is the near edge that has to
1033/// stay put and the far edge that gives way. That is why the two boundary
1034/// rules differ.
1035///
1036/// Before this rule existed the last two segments returned
1037/// [`InterpolationError::Bilinear`] because the window ran off the end.
1038///
1039/// # What this is not
1040///
1041/// The far edge sits elsewhere on the curve, so the result does not follow
1042/// the samples between knots and this is not an interpolant through them: on
1043/// `(0,0), (1,1), (2,4), (3,9)` the value at `x = 0.5` is `3.5`, not the
1044/// `0.5` a straight reading of the bracketing segment gives.
1045///
1046/// An `x` landing exactly on a sample returns that sample, which neither
1047/// one-sided limit approaching it matches, so the result jumps at every
1048/// sample. That jump is as old as the method and owes nothing to the
1049/// boundary rule above: on `(0,0), (1,1), (2,4), (3,9), (4,16)` both limits
1050/// at `x = 1` are `5` against a sample of `1`, and both segments meeting
1051/// there are far from the clamp. What the clamp does add is that at the last
1052/// two samples before the end of the curve the two limits no longer agree
1053/// with each other either, because the segments on either side get different
1054/// far edges.
1055///
1056/// Reach for [`LinearInterpolation`] or [`SplineInterpolation`] when a curve
1057/// through the samples is what is wanted.
1058///
1059/// # Parameters
1060///
1061/// - **`x`**: The x-coordinate to interpolate at. Must lie within the range
1062///   of the curve's x-coordinates.
1063///
1064/// # Returns
1065///
1066/// - **`Ok(Point2D)`**: The interpolated point at `x`, both coordinates as
1067///   `Decimal`.
1068/// - **`Err(InterpolationError)`**: See the error list below.
1069///
1070/// # Errors
1071///
1072/// - [`InterpolationError::Bilinear`]: the curve holds fewer than four
1073///   samples, or a checked-arithmetic step left the representable `Decimal`
1074///   range.
1075/// - [`InterpolationError::OutOfRange`]: `x` falls outside the curve's
1076///   x-range.
1077/// - [`InterpolationError::DegenerateInterval`]: several samples share the
1078///   abscissa `x`, so the curve has no value there.
1079///
1080/// # One point per abscissa
1081///
1082/// A curve is a function of its abscissa; see [`Curve::new`]. Asking for a
1083/// repeated abscissa returns [`InterpolationError::DegenerateInterval`], as
1084/// it does for the three sibling algorithms; no ordinate of the stack is
1085/// chosen. A stack elsewhere on the curve does not stop this method
1086/// answering: only the near edge's width is divided by, and a stack sitting
1087/// on the far edge is read for its two ordinates alone.
1088///
1089/// # Related Traits
1090///
1091/// - [`BiLinearInterpolation`]: The trait defining this method.
1092/// - [`Interpolate`]: Ensures compatibility of the curve with multiple interpolation methods.
1093///
1094/// # See Also
1095///
1096/// - [`Curve`]: The overarching structure that represents the curve.
1097/// - [`Point2D`]: The data type used to represent individual points on the curve.
1098/// - [`find_bracket_points`](crate::geometrics::Interpolate::find_bracket_points):
1099///   A helper method used to locate the two points that bracket the given x-coordinate.
1100impl BiLinearInterpolation<Point2D, Decimal> for Curve {
1101    /// Reads the curve at `x` as the mean of the bracketing segment and the
1102    /// segment two positions further along, both at the same fraction.
1103    ///
1104    /// The far segment is clamped to the curve's last one where it would run
1105    /// off the end, which makes the final segment a plain linear
1106    /// interpolation. The impl-level documentation states the rule and its
1107    /// consequences in full.
1108    ///
1109    /// # Errors
1110    ///
1111    /// - [`InterpolationError::Bilinear`] when the curve holds fewer than
1112    ///   four samples, or when a checked-arithmetic step leaves the
1113    ///   representable `Decimal` range.
1114    /// - [`InterpolationError::OutOfRange`] when `x` is outside the curve's
1115    ///   x-range.
1116    /// - [`InterpolationError::DegenerateInterval`] when several samples
1117    ///   share the abscissa `x`.
1118    fn bilinear_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
1119        let len = self.len();
1120
1121        // The cell is four samples wide: two segments, one edge each.
1122        if len < 4 {
1123            return Err(InterpolationError::Bilinear(
1124                "Need at least four points for bilinear interpolation".to_string(),
1125            ));
1126        }
1127
1128        // For exact points, return the actual point value. A stack of
1129        // ordinates at `x` has no single value, so it errors out instead of
1130        // yielding the lowest of them, which is what the three sibling
1131        // algorithms do.
1132        if let Some(point) = self.exact_point_at(x)? {
1133            return Ok(point);
1134        }
1135
1136        // The near edge is the segment bracketing `x`, so `dx` below stays in
1137        // `[0, 1]`. The far edge is the segment two positions on, samples
1138        // `i + 2` and `i + 3`; over the last two segments that one runs past
1139        // the end and is clamped to the curve's last segment, `len - 2`. The
1140        // four-sample check above keeps the clamp inside the curve.
1141        let [p11, p12, p21, p22] = self.bilinear_cell(x)?;
1142
1143        let span = d_sub(p12.x, p11.x, "Curve::bilinear_interpolate::span")
1144            .map_err(interp_err(InterpolationError::Bilinear))?;
1145        if span.is_zero() {
1146            return Err(InterpolationError::DegenerateInterval);
1147        }
1148
1149        // Normalize x to [0,1] interval
1150        let offset = d_sub(x, p11.x, "Curve::bilinear_interpolate::offset")
1151            .map_err(interp_err(InterpolationError::Bilinear))?;
1152        let dx = d_div(offset, span, "Curve::bilinear_interpolate::dx")
1153            .map_err(interp_err(InterpolationError::Bilinear))?;
1154
1155        // Interpolate along the near edge
1156        let bottom_rise = d_sub(p12.y, p11.y, "Curve::bilinear_interpolate::bottom_rise")
1157            .map_err(interp_err(InterpolationError::Bilinear))?;
1158        let bottom_step = d_mul(dx, bottom_rise, "Curve::bilinear_interpolate::bottom_step")
1159            .map_err(interp_err(InterpolationError::Bilinear))?;
1160        let bottom = d_add(p11.y, bottom_step, "Curve::bilinear_interpolate::bottom")
1161            .map_err(interp_err(InterpolationError::Bilinear))?;
1162
1163        // Interpolate along the far edge, at the same fraction
1164        let top_rise = d_sub(p22.y, p21.y, "Curve::bilinear_interpolate::top_rise")
1165            .map_err(interp_err(InterpolationError::Bilinear))?;
1166        let top_step = d_mul(dx, top_rise, "Curve::bilinear_interpolate::top_step")
1167            .map_err(interp_err(InterpolationError::Bilinear))?;
1168        let top = d_add(p21.y, top_step, "Curve::bilinear_interpolate::top")
1169            .map_err(interp_err(InterpolationError::Bilinear))?;
1170
1171        // Mean of the two edges
1172        let edge_gap = d_sub(top, bottom, "Curve::bilinear_interpolate::edge_gap")
1173            .map_err(interp_err(InterpolationError::Bilinear))?;
1174        let half_gap = d_div(edge_gap, dec!(2), "Curve::bilinear_interpolate::half_gap")
1175            .map_err(interp_err(InterpolationError::Bilinear))?;
1176        let y = d_add(bottom, half_gap, "Curve::bilinear_interpolate::y")
1177            .map_err(interp_err(InterpolationError::Bilinear))?;
1178
1179        Ok(Point2D::new(x, y))
1180    }
1181}
1182
1183/// Implements the `CubicInterpolation` trait for the `Curve` struct,
1184/// providing an algorithm for cubic interpolation utilizing a Catmull-Rom spline.
1185///
1186/// # Method: `cubic_interpolate`
1187///
1188/// ## Parameters
1189/// - **`x`**: The x-value at which the interpolation is performed. This value must
1190///   be within the range of x-values in the curve's defined points, and it is passed
1191///   as a `Decimal` to allow for high-precision computation.
1192///
1193/// ## Returns
1194/// - **`Ok(Point2D)`**: Returns a `Point2D` representing the interpolated x and y values.
1195/// - **`Err(CurvesError)`**: Returns an error if:
1196///   - There are fewer than 4 points available for interpolation.
1197///   - The x-value is outside the curve's range, or interpolation fails for any other reason.
1198///
1199/// ## Behavior
1200/// 1. **Point Validation**: Ensures at least four points exist for cubic interpolation,
1201///    as this is a fundamental requirement for computing the Catmull-Rom spline.
1202/// 2. **Exact Match Check**: If the x-value matches an existing point in the curve, the
1203///    method directly returns the corresponding `Point2D` without further computation.
1204/// 3. **Bracket Points**: Determines the bracketing points (4 points total) around the
1205///    provided x-value. Depending on the position of the x-value in the curve, the
1206///    method dynamically adjusts the selected points to ensure they form a proper bracket:
1207///    - If near the start of the curve, uses the first four points.
1208///    - If near the end, uses the last four points.
1209///    - Else, selects points before and after x to define the bracket.
1210/// 4. **Parameter Calculation**: Computes a normalized parameter `t` that represents
1211///    the relative position of the target x-value between `p1` and `p2`.
1212/// 5. **Catmull-Rom Spline**: Performs cubic interpolation using a Catmull-Rom spline,
1213///    a widely used, smooth spline algorithm. The coefficients are calculated based on
1214///    the relative x position and the y-values of the four surrounding points.
1215/// 6. **Interpolation**: Calculates the interpolated y-value using the cubic formula:
1216///    ```text
1217///    y(t) = 0.5 * (
1218///        2 * p1.y + (-p0.y + p2.y) * t
1219///        + (2 * p0.y - 5 * p1.y + 4 * p2.y - p3.y) * t^2
1220///        + (-p0.y + 3 * p1.y - 3 * p2.y + p3.y) * t^3
1221///    )
1222///    ```
1223///    Here, `t` is the normalized x position, and `p0`, `p1`, `p2`, `p3` are the four bracketed points.
1224///
1225/// ## Errors
1226/// - Returns an error of type `CurvesError::InterpolationError` if any issues are encountered,
1227///   such as insufficient points or the inability to locate bracket points.
1228///
1229/// ## Example
1230/// This method is part of the `Curve` struct, which defines a set of points and supports interpolation.
1231/// It is often used in applications requiring smooth manifolds or animations.
1232///
1233/// ## Notes
1234/// - The computed y-value ensures smooth transitions and continuity between interpolated segments.
1235/// - Catmull-Rom splines are particularly effective for creating visually smooth transitions,
1236///   making this method suitable for curves, animations, and numerical analysis.
1237///
1238/// # See Also
1239/// - [`CubicInterpolation`]: The trait defining this method.
1240/// - [`Point2D`]: Represents the points used for interpolation.
1241/// - [`find_bracket_points`](crate::geometrics::Interpolate::find_bracket_points): Determines the bracketing points required for interpolation.
1242impl CubicInterpolation<Point2D, Decimal> for Curve {
1243    /// Performs cubic interpolation on a set of points to estimate the y-coordinate
1244    /// for a given x value using a Catmull-Rom spline.
1245    ///
1246    /// # Parameters
1247    ///
1248    /// - `x`: The x-coordinate for which the interpolation is performed. This value
1249    ///   should lie within the range of the points on the curve.
1250    ///
1251    /// # Returns
1252    ///
1253    /// - `Ok(Point2D)`: A `Point2D` instance representing the interpolated position
1254    ///   `(x, y)`, where `y` is estimated using cubic interpolation.
1255    /// - `Err(CurvesError)`: An error indicating issues with the interpolation process,
1256    ///   such as insufficient points or an out-of-range x value.
1257    ///
1258    /// # Requirements
1259    ///
1260    /// - The number of points in the curve must be at least 4, as cubic interpolation
1261    ///   requires four points for accurate calculations.
1262    /// - The specified `x` value should be inside the range defined by the curve's points.
1263    /// - If the specified x matches an existing point on the curve, the interpolated result
1264    ///   directly returns that exact point.
1265    ///
1266    /// # Functionality
1267    ///
1268    /// This method performs cubic interpolation using the general properties of the
1269    /// Catmull-Rom spline, which is well-suited for smooth curve fitting. It operates as follows:
1270    ///
1271    /// 1. **Exact Point Check**: If the x value matches an existing point, the method
1272    ///    returns that point without further processing.
1273    ///
1274    /// 2. **Bracketing Points Selection**:
1275    ///    - Searches for two points that bracket the given x value (using `find_bracket_points`
1276    ///      from the `Interpolate` trait). The method ensures that there are always enough
1277    ///      points before and after the target x value to perform cubic interpolation.
1278    ///
1279    /// 3. **Point Selection for Interpolation**:
1280    ///    - Depending on the position of the target x value, four points (`p0, p1, p2, p3`)
1281    ///      are selected:
1282    ///        - When `x` is near the start of the points, select the first four.
1283    ///        - When `x` is near the end, select the last four.
1284    ///        - Otherwise, select the two points just before and after the x value and
1285    ///          include an additional adjacent point on either side.
1286    ///
1287    /// 4. **Parameter Calculation**:
1288    ///    - The `t` parameter is derived, representing the normalized position of x
1289    ///      between `p1` and `p2`.
1290    ///
1291    /// 5. **Cubic Interpolation**:
1292    ///    - The interpolated y-coordinate is computed using the Catmull-Rom spline formula,
1293    ///      leveraging the `t`-value and the y-coordinates of the four selected points.
1294    ///
1295    /// # Error Handling
1296    ///
1297    /// This method returns an error in the following circumstances:
1298    /// - If fewer than 4 points are available, it returns a `CurvesError::InterpolationError`
1299    ///   with a corresponding message.
1300    /// - If the bracketing points cannot be identified (e.g., when `x` is outside the
1301    ///   range of points), the appropriate interpolation error is propagated.
1302    /// - If the curve is not a function of its abscissa at the point it is
1303    ///   asked about, it returns `InterpolationError::DegenerateInterval`:
1304    ///   several points at `x` leave the exact-match branch with no single
1305    ///   value to return, and two consecutive points sharing an abscissa
1306    ///   leave the interpolation bracket with zero width. Neither case picks
1307    ///   an ordinate. See [`Curve::new`] for the rule.
1308    ///
1309    /// # Example
1310    ///
1311    /// - Interpolating smoothly along a curve defined by a set of points, avoiding sharp
1312    ///   transitions between segments.
1313    ///
1314    /// - Provides a high degree of precision due to the use of the `Decimal` type for
1315    ///   `x` and `y` calculations.
1316    fn cubic_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
1317        let len = self.len();
1318
1319        // Need at least 4 points for cubic interpolation
1320        if len < 4 {
1321            return Err(InterpolationError::Cubic(
1322                "Need at least four points for cubic interpolation".to_string(),
1323            ));
1324        }
1325
1326        // For exact points, return the actual point value. A stack of
1327        // ordinates at `x` has no single value, so it errors out instead of
1328        // yielding the lowest of them.
1329        if let Some(point) = self.exact_point_at(x)? {
1330            return Ok(point);
1331        }
1332
1333        let [p0, p1, p2, p3] = self.cubic_window(x)?;
1334
1335        let span = d_sub(p2.x, p1.x, "Curve::cubic_interpolate::span")
1336            .map_err(interp_err(InterpolationError::Cubic))?;
1337        if span.is_zero() {
1338            return Err(InterpolationError::DegenerateInterval);
1339        }
1340
1341        // Calculate t parameter (normalized x position between p1 and p2)
1342        let offset = d_sub(x, p1.x, "Curve::cubic_interpolate::offset")
1343            .map_err(interp_err(InterpolationError::Cubic))?;
1344        let t = d_div(offset, span, "Curve::cubic_interpolate::t")
1345            .map_err(interp_err(InterpolationError::Cubic))?;
1346
1347        // Cubic interpolation using Catmull-Rom spline
1348        let t2 = d_mul(t, t, "Curve::cubic_interpolate::t2")
1349            .map_err(interp_err(InterpolationError::Cubic))?;
1350        let t3 = d_mul(t2, t, "Curve::cubic_interpolate::t3")
1351            .map_err(interp_err(InterpolationError::Cubic))?;
1352
1353        let term0 = d_mul(dec!(2), p1.y, "Curve::cubic_interpolate::term0")
1354            .map_err(interp_err(InterpolationError::Cubic))?;
1355
1356        let linear_coeff = d_sub(p2.y, p0.y, "Curve::cubic_interpolate::linear_coeff")
1357            .map_err(interp_err(InterpolationError::Cubic))?;
1358        let term1 = d_mul(linear_coeff, t, "Curve::cubic_interpolate::term1")
1359            .map_err(interp_err(InterpolationError::Cubic))?;
1360
1361        let two_p0 = d_mul(dec!(2), p0.y, "Curve::cubic_interpolate::two_p0")
1362            .map_err(interp_err(InterpolationError::Cubic))?;
1363        let five_p1 = d_mul(dec!(5), p1.y, "Curve::cubic_interpolate::five_p1")
1364            .map_err(interp_err(InterpolationError::Cubic))?;
1365        let four_p2 = d_mul(dec!(4), p2.y, "Curve::cubic_interpolate::four_p2")
1366            .map_err(interp_err(InterpolationError::Cubic))?;
1367        let quad_coeff = d_sub(two_p0, five_p1, "Curve::cubic_interpolate::quad_coeff_a")
1368            .map_err(interp_err(InterpolationError::Cubic))?;
1369        let quad_coeff = d_add(
1370            quad_coeff,
1371            four_p2,
1372            "Curve::cubic_interpolate::quad_coeff_b",
1373        )
1374        .map_err(interp_err(InterpolationError::Cubic))?;
1375        let quad_coeff = d_sub(quad_coeff, p3.y, "Curve::cubic_interpolate::quad_coeff_c")
1376            .map_err(interp_err(InterpolationError::Cubic))?;
1377        let term2 = d_mul(quad_coeff, t2, "Curve::cubic_interpolate::term2")
1378            .map_err(interp_err(InterpolationError::Cubic))?;
1379
1380        let three_p1 = d_mul(dec!(3), p1.y, "Curve::cubic_interpolate::three_p1")
1381            .map_err(interp_err(InterpolationError::Cubic))?;
1382        let three_p2 = d_mul(dec!(3), p2.y, "Curve::cubic_interpolate::three_p2")
1383            .map_err(interp_err(InterpolationError::Cubic))?;
1384        let cubic_coeff = d_sub(three_p1, p0.y, "Curve::cubic_interpolate::cubic_coeff_a")
1385            .map_err(interp_err(InterpolationError::Cubic))?;
1386        let cubic_coeff = d_sub(
1387            cubic_coeff,
1388            three_p2,
1389            "Curve::cubic_interpolate::cubic_coeff_b",
1390        )
1391        .map_err(interp_err(InterpolationError::Cubic))?;
1392        let cubic_coeff = d_add(cubic_coeff, p3.y, "Curve::cubic_interpolate::cubic_coeff_c")
1393            .map_err(interp_err(InterpolationError::Cubic))?;
1394        let term3 = d_mul(cubic_coeff, t3, "Curve::cubic_interpolate::term3")
1395            .map_err(interp_err(InterpolationError::Cubic))?;
1396
1397        let sum = d_sum_iter(
1398            [term0, term1, term2, term3],
1399            "Curve::cubic_interpolate::sum",
1400        )
1401        .map_err(interp_err(InterpolationError::Cubic))?;
1402        let y = d_mul(dec!(0.5), sum, "Curve::cubic_interpolate::y")
1403            .map_err(interp_err(InterpolationError::Cubic))?;
1404
1405        Ok(Point2D::new(x, y))
1406    }
1407}
1408
1409/// Implements the `SplineInterpolation` trait for the `Curve` struct, providing functionality
1410/// to perform cubic spline interpolation.
1411///
1412/// # Overview
1413/// This method calculates the interpolated `y` value for a given `x` value by using cubic
1414/// spline interpolation on the points in the `Curve`. The method ensures a smooth transition
1415/// between points by computing second derivatives of the curve at each point, and uses those
1416/// derivatives in the spline interpolation formula.
1417///
1418/// # Parameters
1419/// - `x`: The x-coordinate at which the curve should be interpolated. This value is passed as
1420///   a `Decimal` for precise calculations.
1421///
1422/// # Returns
1423/// - On success, returns a `Point2D` instance representing the interpolated point.
1424/// - On error, returns a `CurvesError` indicating the reason for failure (e.g., insufficient points
1425///   or an out-of-range x-coordinate).
1426///
1427/// # Errors
1428/// - Returns `CurvesError::InterpolationError` with an appropriate error message in the following cases:
1429///   - If the curve contains fewer than three points, as spline interpolation requires at least three points.
1430///   - If the given `x` value lies outside the range of x-coordinates spanned by the points in the curve.
1431///   - If a valid segment for interpolation cannot be located.
1432///
1433/// # Details
1434/// 1. **Validation**:
1435///    - Ensures that there are at least three points in the curve for spline interpolation.
1436///    - Validates that the provided `x` value is within the range of `x` values of the curve.
1437/// 2. **Exact Match**: If the `x` value matches the x-coordinate of an existing point, the corresponding
1438///    `Point2D` is returned immediately.
1439/// 3. **Second Derivative Calculation**:
1440///    - Uses a tridiagonal matrix to compute the second derivatives at each point. This step
1441///      involves setting up the system of equations based on the boundary conditions (natural spline)
1442///      and solving it using the Thomas algorithm.
1443/// 4. **Segment Identification**:
1444///    - Determines the segment (interval between two consecutive points) in which the provided `x` value lies.
1445/// 5. **Interpolation**:
1446///    - Computes the interpolated y-coordinate using the cubic spline formula, which is based on
1447///      the second derivatives and the positions of the surrounding points.
1448///
1449/// # Implementation Notes
1450/// - This implementation uses `Decimal` from the `rust_decimal` crate to ensure high precision
1451///   in calculations, making it suitable for scientific and financial applications.
1452/// - The Thomas algorithm is employed to solve the tridiagonal matrix system efficiently.
1453/// - The method assumes natural spline boundary conditions, where the second derivatives at the
1454///   endpoints are set to zero, ensuring a smooth and continuous curve shape.
1455///
1456/// # Example Usage
1457/// Refer to the documentation for how to use the `SplineInterpolation` trait, as examples
1458/// are not provided inline with this implementation.
1459///
1460/// # See Also
1461/// - [`SplineInterpolation`]: The trait definition for spline interpolation.
1462/// - [`Point2D`]: Represents a point in 2D space.
1463/// - [`Curve`]: Represents a mathematical curve made up of points for interpolation.
1464/// - [`CurveError`]: Enumerates possible errors during curve operations.
1465impl SplineInterpolation<Point2D, Decimal> for Curve {
1466    /// Performs cubic spline interpolation for a given x-coordinate and returns the interpolated
1467    /// `Point2D` value. This function computes the second derivatives of the curve points, solves
1468    /// a tridiagonal system to derive the interpolation parameters, and evaluates the spline
1469    /// function for the provided `x` value.
1470    ///
1471    /// # Parameters
1472    ///
1473    /// - `x`:
1474    ///   - The x-coordinate at which the interpolation is to be performed.
1475    ///   - Must be of type `Decimal`.
1476    ///
1477    /// # Returns
1478    ///
1479    /// - `Ok(Point2D)`:
1480    ///   - The `Point2D` instance representing the interpolated point at the given `x` value.
1481    ///   - The interpolated `y` value is calculated based on the cubic spline interpolation algorithm.
1482    ///
1483    /// - `Err(CurvesError)`:
1484    ///   - Returned when an error occurs during the interpolation process, such as:
1485    ///     - Insufficient points provided (less than 3 points).
1486    ///     - The given `x` is outside the valid range of the points.
1487    ///     - Unable to determine the correct segment for interpolation.
1488    ///
1489    /// # Errors
1490    ///
1491    /// - `CurvesError::InterpolationError`:
1492    ///   - Occurs under the following conditions:
1493    ///     - **"Need at least three points for spline interpolation"**:
1494    ///       Requires at least 3 points to perform cubic spline interpolation.
1495    ///     - **"x is outside the range of points"**:
1496    ///       The provided `x` value lies outside the domain of the curve points.
1497    ///     - **"Could not find valid segment for interpolation"**:
1498    ///       Spline interpolation fails due to an invalid segment determination.
1499    ///
1500    /// # Pre-conditions
1501    ///
1502    /// - The curve must contain at least three points for cubic spline interpolation.
1503    /// - The `x` value must fall within the range of the curve's x-coordinates.
1504    ///
1505    /// # Implementation Details
1506    ///
1507    /// - **Inputs**:
1508    ///   - Uses the `get_points` method of the curve to retrieve the list of `Point2D` instances
1509    ///     that define the interpolation curve.
1510    ///   - Computes the second derivatives (`m`) for each point using the Thomas algorithm to solve
1511    ///     a tridiagonal system.
1512    /// - **Boundary Conditions**:
1513    ///   - Natural spline boundary conditions are used, with the second derivatives on the boundary
1514    ///     set to zero.
1515    /// - **Interpolation**:
1516    ///   - Determines the segment `[x_i, x_{i+1}]` to which the input `x` belongs.
1517    ///   - Uses the cubic spline equation to calculate the interpolated `y` value.
1518    ///
1519    /// # Mathematical Formulation
1520    ///
1521    /// Let `x_i`, `x_{i+1}`, `y_i`, `y_{i+1}` refer to the points of the segment where `x` lies.
1522    /// The cubic spline function at `x` is computed as follows:
1523    ///
1524    /// ```text
1525    /// S(x) = m_i * (x_{i+1} - x)^3 / (6 * h)
1526    ///      + m_{i+1} * (x - x_i)^3 / (6 * h)
1527    ///      + (y_i / h - h * m_i / 6) * (x_{i+1} - x)
1528    ///      + (y_{i+1} / h - h * m_{i+1} / 6) * (x - x_i)
1529    /// ```
1530    ///
1531    /// Where:
1532    /// - `m_i`, `m_{i+1}` are the second derivatives at `x_i` and `x_{i+1}`.
1533    /// - `h = x_{i+1} - x_i` is the distance between the two points.
1534    /// - `(x_{i+1} - x)` and `(x - x_i)` are the relative distances within the segment.
1535    ///
1536    /// # Example Usages (Non-code)
1537    ///
1538    /// This method is typically used for high-precision curve fitting or graphical rendering where
1539    /// smooth transitions between points are essential. Common applications include:
1540    /// - Signal processing.
1541    /// - Data interpolation for missing values.
1542    /// - Smooth graphical representations of mathematical functions.
1543    ///
1544    /// # Related Types
1545    ///
1546    /// - [`Point2D`]: Represents a 2D point and is used as input/output
1547    ///   for this function.
1548    /// - [`CurveError`] Represents any error encountered during
1549    ///   interpolation.
1550    ///
1551    /// # Performance
1552    ///
1553    /// - The function operates with `O(n)` complexity, where `n` is the number of points. The
1554    ///   tridiagonal system is solved efficiently using the Thomas algorithm.
1555    /// - Every call solves the system again. To read one curve at many
1556    ///   abscissas, solve it once with [`CurveSpline`], which returns the
1557    ///   same answers.
1558    ///
1559    /// # Notes
1560    ///
1561    /// - Natural spline interpolation may introduce minor deviations beyond the range of existing
1562    ///   data points due to its boundary conditions. For strictly constrained results, consider
1563    ///   alternative interpolation methods, such as linear or cubic Hermite interpolation.
1564    /// - The knots must be a function of their abscissa. Several points at
1565    ///   the requested `x` leave the exact-match branch with no single value
1566    ///   to return, and any repeated abscissa collapses a knot interval, so
1567    ///   both return `InterpolationError::DegenerateInterval` rather than
1568    ///   picking an ordinate. See [`Curve::new`] for the rule.
1569    fn spline_interpolate(&self, x: Decimal) -> Result<Point2D, InterpolationError> {
1570        if let Some(point) = self.spline_precheck(x)? {
1571            return Ok(point);
1572        }
1573        let pts: Vec<&Point2D> = self.points.iter().collect();
1574        let m = spline_moments(&pts)?;
1575        spline_eval(&pts, &m, x)
1576    }
1577}
1578
1579/// The second derivatives of the natural cubic spline through `pts`, by the
1580/// Thomas algorithm. They depend on the knots alone, so
1581/// [`CurveSpline`] solves them once per curve.
1582///
1583/// Errors as [`SplineInterpolation::spline_interpolate`] does once its
1584/// pre-checks pass: [`InterpolationError::DegenerateInterval`] for a
1585/// repeated abscissa, [`InterpolationError::Spline`] when a checked step
1586/// leaves the `Decimal` range.
1587fn spline_moments(pts: &[&Point2D]) -> Result<Vec<Decimal>, InterpolationError> {
1588    let n = pts.len();
1589    let spline = InterpolationError::Spline;
1590    let last_index = shift_index(n, -1, spline)?;
1591    let at = |index: usize| -> Result<&Point2D, InterpolationError> {
1592        pts.get(index).copied().ok_or_else(|| {
1593            InterpolationError::Spline(format!(
1594                "point index {index} is out of bounds for a curve of {n} points"
1595            ))
1596        })
1597    };
1598
1599    // Calculate second derivatives. The three interior bands and the
1600    // right-hand side are built in order, with the natural-spline
1601    // boundary rows (`b[0] = b[n-1] = 1`) pushed at the ends.
1602    let mut a = vec![Decimal::ZERO];
1603    let mut b = vec![Decimal::ONE];
1604    let mut c = vec![Decimal::ZERO];
1605    let mut r = vec![Decimal::ZERO];
1606
1607    // Fill the matrices
1608    for i in 1..last_index {
1609        let prev = at(shift_index(i, -1, spline)?)?;
1610        let curr = at(i)?;
1611        let next = at(shift_index(i, 1, spline)?)?;
1612
1613        let hi = d_sub(curr.x, prev.x, "Curve::spline_interpolate::hi")
1614            .map_err(interp_err(InterpolationError::Spline))?;
1615        let hi1 = d_sub(next.x, curr.x, "Curve::spline_interpolate::hi1")
1616            .map_err(interp_err(InterpolationError::Spline))?;
1617        // A repeated abscissa collapses a knot interval; the second
1618        // derivative there is undefined rather than infinite.
1619        if hi.is_zero() || hi1.is_zero() {
1620            return Err(InterpolationError::DegenerateInterval);
1621        }
1622
1623        let band = d_add(hi, hi1, "Curve::spline_interpolate::band")
1624            .map_err(interp_err(InterpolationError::Spline))?;
1625        let diag = d_mul(dec!(2), band, "Curve::spline_interpolate::diag")
1626            .map_err(interp_err(InterpolationError::Spline))?;
1627
1628        let rise_next = d_sub(next.y, curr.y, "Curve::spline_interpolate::rise_next")
1629            .map_err(interp_err(InterpolationError::Spline))?;
1630        let slope_next = d_div(rise_next, hi1, "Curve::spline_interpolate::slope_next")
1631            .map_err(interp_err(InterpolationError::Spline))?;
1632        let rise_prev = d_sub(curr.y, prev.y, "Curve::spline_interpolate::rise_prev")
1633            .map_err(interp_err(InterpolationError::Spline))?;
1634        let slope_prev = d_div(rise_prev, hi, "Curve::spline_interpolate::slope_prev")
1635            .map_err(interp_err(InterpolationError::Spline))?;
1636        let curvature = d_sub(
1637            slope_next,
1638            slope_prev,
1639            "Curve::spline_interpolate::curvature",
1640        )
1641        .map_err(interp_err(InterpolationError::Spline))?;
1642        let rhs = d_mul(dec!(6), curvature, "Curve::spline_interpolate::rhs")
1643            .map_err(interp_err(InterpolationError::Spline))?;
1644
1645        a.push(hi);
1646        b.push(diag);
1647        c.push(hi1);
1648        r.push(rhs);
1649    }
1650
1651    // Add boundary conditions (natural spline)
1652    a.push(Decimal::ZERO);
1653    b.push(Decimal::ONE);
1654    c.push(Decimal::ZERO);
1655    r.push(Decimal::ZERO);
1656
1657    // Solve tridiagonal system using Thomas algorithm
1658    let mut m = vec![Decimal::ZERO; n];
1659
1660    for i in 1..last_index {
1661        let prev = shift_index(i, -1, spline)?;
1662        let a_i = band_at(&a, i, "a")?;
1663        let b_prev = band_at(&b, prev, "b")?;
1664        if b_prev.is_zero() {
1665            return Err(InterpolationError::DegenerateInterval);
1666        }
1667        let w = d_div(a_i, b_prev, "Curve::spline_interpolate::w")
1668            .map_err(interp_err(InterpolationError::Spline))?;
1669
1670        let c_prev = band_at(&c, prev, "c")?;
1671        let wc = d_mul(w, c_prev, "Curve::spline_interpolate::wc")
1672            .map_err(interp_err(InterpolationError::Spline))?;
1673        let b_i = band_at(&b, i, "b")?;
1674        *band_at_mut(&mut b, i, "b")? = d_sub(b_i, wc, "Curve::spline_interpolate::b_i")
1675            .map_err(interp_err(InterpolationError::Spline))?;
1676
1677        let r_prev = band_at(&r, prev, "r")?;
1678        let wr = d_mul(w, r_prev, "Curve::spline_interpolate::wr")
1679            .map_err(interp_err(InterpolationError::Spline))?;
1680        let r_i = band_at(&r, i, "r")?;
1681        *band_at_mut(&mut r, i, "r")? = d_sub(r_i, wr, "Curve::spline_interpolate::r_i")
1682            .map_err(interp_err(InterpolationError::Spline))?;
1683    }
1684
1685    let b_last = band_at(&b, last_index, "b")?;
1686    if b_last.is_zero() {
1687        return Err(InterpolationError::DegenerateInterval);
1688    }
1689    let r_last = band_at(&r, last_index, "r")?;
1690    *band_at_mut(&mut m, last_index, "m")? =
1691        d_div(r_last, b_last, "Curve::spline_interpolate::m_last")
1692            .map_err(interp_err(InterpolationError::Spline))?;
1693    for i in (1..last_index).rev() {
1694        let c_i = band_at(&c, i, "c")?;
1695        let m_next = band_at(&m, shift_index(i, 1, spline)?, "m")?;
1696        let cm = d_mul(c_i, m_next, "Curve::spline_interpolate::cm")
1697            .map_err(interp_err(InterpolationError::Spline))?;
1698        let r_i = band_at(&r, i, "r")?;
1699        let numerator = d_sub(r_i, cm, "Curve::spline_interpolate::m_numerator")
1700            .map_err(interp_err(InterpolationError::Spline))?;
1701        let b_i = band_at(&b, i, "b")?;
1702        if b_i.is_zero() {
1703            return Err(InterpolationError::DegenerateInterval);
1704        }
1705        *band_at_mut(&mut m, i, "m")? = d_div(numerator, b_i, "Curve::spline_interpolate::m_i")
1706            .map_err(interp_err(InterpolationError::Spline))?;
1707    }
1708
1709    Ok(m)
1710}
1711
1712/// Evaluates the spline with second derivatives `m` through `pts` at `x`,
1713/// which the pre-checks keep inside the knots.
1714fn spline_eval(pts: &[&Point2D], m: &[Decimal], x: Decimal) -> Result<Point2D, InterpolationError> {
1715    let n = pts.len();
1716    let spline = InterpolationError::Spline;
1717    let at = |index: usize| -> Result<&Point2D, InterpolationError> {
1718        pts.get(index).copied().ok_or_else(|| {
1719            InterpolationError::Spline(format!(
1720                "point index {index} is out of bounds for a curve of {n} points"
1721            ))
1722        })
1723    };
1724
1725    // Find segment for interpolation: the first `[x_i, x_{i+1}]` holding
1726    // `x`. With `k` the first knot at or after `x` that is `k - 1`, or 0
1727    // when `k` is the first knot, so a binary search finds the window a
1728    // scan from the left would (#858).
1729    let first_at_or_after = pts.partition_point(|p| p.x < x);
1730    let segment = shift_index(first_at_or_after.max(1), -1, spline)?;
1731    if segment >= shift_index(n, -1, spline)? {
1732        return Err(InterpolationError::Spline(
1733            "Could not find valid segment for interpolation".to_string(),
1734        ));
1735    }
1736
1737    // Calculate interpolated value
1738    let left = at(segment)?;
1739    let right = at(shift_index(segment, 1, spline)?)?;
1740    let h = d_sub(right.x, left.x, "Curve::spline_interpolate::h")
1741        .map_err(interp_err(InterpolationError::Spline))?;
1742    if h.is_zero() {
1743        return Err(InterpolationError::DegenerateInterval);
1744    }
1745    let dx = d_sub(right.x, x, "Curve::spline_interpolate::dx")
1746        .map_err(interp_err(InterpolationError::Spline))?;
1747    let dx1 = d_sub(x, left.x, "Curve::spline_interpolate::dx1")
1748        .map_err(interp_err(InterpolationError::Spline))?;
1749
1750    let m_left = band_at(m, segment, "m")?;
1751    let m_right = band_at(m, shift_index(segment, 1, spline)?, "m")?;
1752    let six_h = d_mul(dec!(6), h, "Curve::spline_interpolate::six_h")
1753        .map_err(interp_err(InterpolationError::Spline))?;
1754
1755    let left_cube = cube_scaled(m_left, dx, six_h, "Curve::spline_interpolate::left_cube")?;
1756    let right_cube = cube_scaled(m_right, dx1, six_h, "Curve::spline_interpolate::right_cube")?;
1757    let left_linear = linear_term(left.y, m_left, h, dx, "Curve::spline_interpolate::left")?;
1758    let right_linear = linear_term(right.y, m_right, h, dx1, "Curve::spline_interpolate::right")?;
1759
1760    let y = d_sum_iter(
1761        [left_cube, right_cube, left_linear, right_linear],
1762        "Curve::spline_interpolate::y",
1763    )
1764    .map_err(interp_err(InterpolationError::Spline))?;
1765
1766    Ok(Point2D::new(x, y))
1767}
1768
1769impl StatisticalCurve for Curve {
1770    fn get_x_values(&self) -> Vec<Decimal> {
1771        self.points.iter().map(|p| p.x).collect()
1772    }
1773}
1774
1775/// A default implementation for the `Curve` type using a provided default strategy.
1776///
1777/// This implementation provides a basic approach to computing curve metrics
1778/// by using interpolation and statistical methods available in the standard
1779/// curve analysis library.
1780///
1781/// # Note
1782/// This is a minimal implementation that may need to be customized or enhanced
1783/// based on specific requirements or domain-specific analysis needs.
1784impl MetricsExtractor for Curve {
1785    fn compute_basic_metrics(&self) -> Result<BasicMetrics, MetricsError> {
1786        let y_values: Vec<Decimal> = self.points.iter().map(|p| p.y).collect();
1787
1788        // Handle empty curve
1789        if y_values.is_empty() {
1790            return Ok(BasicMetrics {
1791                mean: Decimal::ZERO,
1792                median: Decimal::ZERO,
1793                mode: Decimal::ZERO,
1794                std_dev: Decimal::ZERO,
1795            });
1796        }
1797
1798        // Mean
1799        let mean = mean_of(&y_values, "Curve::compute_basic_metrics::mean")
1800            .map_err(|e| MetricsError::BasicError(e.to_string()))?;
1801
1802        // Median
1803        let mut sorted_values = y_values.clone();
1804        sorted_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1805        let mid = sorted_values.len() / 2;
1806        let median = if sorted_values.len().is_multiple_of(2) {
1807            let below = mid.checked_sub(1).ok_or_else(|| {
1808                MetricsError::BasicError(
1809                    "median: empty sample after the emptiness guard".to_string(),
1810                )
1811            })?;
1812            let lower = sample_at(&sorted_values, below, "median")?;
1813            let upper = sample_at(&sorted_values, mid, "median")?;
1814            let pair = d_add(lower, upper, "Curve::compute_basic_metrics::median_pair")
1815                .map_err(|e| MetricsError::BasicError(e.to_string()))?;
1816            d_div(pair, Decimal::TWO, "Curve::compute_basic_metrics::median")
1817                .map_err(|e| MetricsError::BasicError(e.to_string()))?
1818        } else {
1819            sample_at(&sorted_values, mid, "median")?
1820        };
1821
1822        // Mode (most frequent value)
1823        let mode = {
1824            let mut freq_map = std::collections::HashMap::new();
1825            for &val in &y_values {
1826                let count = freq_map.entry(val).or_insert(0usize);
1827                *count = count.checked_add(1).ok_or_else(|| {
1828                    MetricsError::BasicError("mode: frequency count overflowed".to_string())
1829                })?;
1830            }
1831            freq_map
1832                .into_iter()
1833                .max_by_key(|&(_, count)| count)
1834                .map(|(val, _)| val)
1835                .unwrap_or(Decimal::ZERO)
1836        };
1837
1838        // Standard Deviation
1839        let variance = variance_of(&y_values, mean, "Curve::compute_basic_metrics::variance")
1840            .map_err(|e| MetricsError::BasicError(e.to_string()))?;
1841        // A failed square root is an error, not a zero dispersion (#824).
1842        let std_dev = d_sqrt(variance, "Curve::compute_basic_metrics::std_dev")
1843            .map_err(|e| MetricsError::BasicError(e.to_string()))?;
1844
1845        Ok(BasicMetrics {
1846            mean,
1847            median,
1848            mode,
1849            std_dev,
1850        })
1851    }
1852
1853    fn compute_shape_metrics(&self) -> Result<ShapeMetrics, MetricsError> {
1854        let y_values: Vec<Decimal> = self.points.iter().map(|p| p.y).collect();
1855
1856        // Handle empty or single-point curve
1857        if y_values.len() < 2 {
1858            return Ok(ShapeMetrics {
1859                skewness: Decimal::ZERO,
1860                kurtosis: Decimal::ZERO,
1861                peaks: vec![],
1862                valleys: vec![],
1863                inflection_points: vec![],
1864            });
1865        }
1866
1867        // Mean and Standard Deviation
1868        let mean = mean_of(&y_values, "Curve::compute_shape_metrics::mean")
1869            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1870
1871        // Compute centered and scaled values
1872        let centered_values: Vec<Decimal> = y_values
1873            .iter()
1874            .map(|&x| d_sub(x, mean, "Curve::compute_shape_metrics::centered"))
1875            .collect::<Result<Vec<_>, _>>()
1876            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1877
1878        // Compute variance
1879        let variance = variance_of(&y_values, mean, "Curve::compute_shape_metrics::variance")
1880            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1881        // A failed square root is an error, not a unit dispersion (#824).
1882        let std_dev = d_sqrt(variance, "Curve::compute_shape_metrics::std_dev")
1883            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1884        if std_dev.is_zero() || std_dev < dec!(1e-9) {
1885            return Err(MetricsError::ShapeError(format!(
1886                "standard deviation ({std_dev}) is too small to compute skewness/kurtosis; the curve is degenerate"
1887            )));
1888        }
1889
1890        // Skewness calculation (Fisher-Pearson standardized moment)
1891        let skewness = standardized_moment(
1892            &centered_values,
1893            std_dev,
1894            3,
1895            "Curve::compute_shape_metrics::skewness",
1896        )
1897        .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1898
1899        // Kurtosis calculation (Fisher's definition - adjust to excess kurtosis)
1900        let raw_kurtosis = standardized_moment(
1901            &centered_values,
1902            std_dev,
1903            4,
1904            "Curve::compute_shape_metrics::kurtosis",
1905        )
1906        .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1907        let kurtosis = d_sub(
1908            raw_kurtosis,
1909            Decimal::from(3),
1910            "Curve::compute_shape_metrics::excess_kurtosis",
1911        )
1912        .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1913
1914        // Peaks and Valleys detection
1915        let (peaks, valleys) = detect_peaks_and_valleys(&self.points, dec!(0.1), 2)
1916            .map_err(|e| MetricsError::ShapeError(e.to_string()))?;
1917
1918        Ok(ShapeMetrics {
1919            skewness,
1920            kurtosis,
1921            peaks,
1922            valleys,
1923            inflection_points: vec![],
1924        })
1925    }
1926
1927    fn compute_range_metrics(&self) -> Result<RangeMetrics, MetricsError> {
1928        // Handle empty curve
1929        if self.points.is_empty() {
1930            return Ok(RangeMetrics {
1931                min: Point2D::new(Decimal::ZERO, Decimal::ZERO),
1932                max: Point2D::new(Decimal::ZERO, Decimal::ZERO),
1933                range: Decimal::ZERO,
1934                quartiles: (Decimal::ZERO, Decimal::ZERO, Decimal::ZERO),
1935                interquartile_range: Decimal::ZERO,
1936            });
1937        }
1938
1939        let mut y_values: Vec<Decimal> = self.points.iter().map(|p| p.y).collect();
1940        y_values.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
1941
1942        let len = y_values.len();
1943        let min_point = self
1944            .points
1945            .iter()
1946            .min_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
1947            .cloned()
1948            .ok_or_else(|| MetricsError::BasicError("empty curve in min_point".to_string()))?;
1949        let max_point = self
1950            .points
1951            .iter()
1952            .max_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
1953            .cloned()
1954            .ok_or_else(|| MetricsError::BasicError("empty curve in max_point".to_string()))?;
1955
1956        let range = d_sub(
1957            max_point.y,
1958            min_point.y,
1959            "Curve::compute_range_metrics::range",
1960        )
1961        .map_err(|e| MetricsError::RangeError(e.to_string()))?;
1962
1963        // Quartiles
1964        let q1 = sample_at(&y_values, len / 4, "first quartile")?;
1965        let q2 = sample_at(&y_values, len / 2, "median")?;
1966        let q3 = sample_at(&y_values, third_quartile_index(len)?, "third quartile")?;
1967
1968        let interquartile_range = d_sub(q3, q1, "Curve::compute_range_metrics::iqr")
1969            .map_err(|e| MetricsError::RangeError(e.to_string()))?;
1970
1971        Ok(RangeMetrics {
1972            min: min_point,
1973            max: max_point,
1974            range,
1975            quartiles: (q1, q2, q3),
1976            interquartile_range,
1977        })
1978    }
1979
1980    fn compute_trend_metrics(&self) -> Result<TrendMetrics, MetricsError> {
1981        let points: Vec<Point2D> = self.points.clone().into_iter().collect();
1982
1983        // Handle insufficient points
1984        if points.len() < 2 {
1985            return Ok(TrendMetrics {
1986                slope: Decimal::ZERO,
1987                intercept: Decimal::ZERO,
1988                r_squared: Decimal::ZERO,
1989                moving_average: vec![],
1990            });
1991        }
1992
1993        // Linear Regression Calculation
1994        let n = Decimal::from(points.len());
1995        let x_vals: Vec<Decimal> = points.iter().map(|p| p.x).collect();
1996        let y_vals: Vec<Decimal> = points.iter().map(|p| p.y).collect();
1997
1998        let trend = || -> Result<(Decimal, Decimal, Decimal), DecimalError> {
1999            let op = "Curve::compute_trend_metrics";
2000            let sum_x = d_sum_iter(x_vals.iter().copied(), op)?;
2001            let sum_y = d_sum_iter(y_vals.iter().copied(), op)?;
2002            let mut sum_xy = Decimal::ZERO;
2003            let mut sum_xx = Decimal::ZERO;
2004            for (x, y) in x_vals.iter().zip(&y_vals) {
2005                sum_xy = d_add(sum_xy, d_mul(*x, *y, op)?, op)?;
2006                sum_xx = d_add(sum_xx, d_mul(*x, *x, op)?, op)?;
2007            }
2008
2009            let numerator = d_sub(d_mul(n, sum_xy, op)?, d_mul(sum_x, sum_y, op)?, op)?;
2010            let denominator = d_sub(d_mul(n, sum_xx, op)?, d_mul(sum_x, sum_x, op)?, op)?;
2011            let slope = d_div(numerator, denominator, op)?;
2012            let intercept = d_div(d_sub(sum_y, d_mul(slope, sum_x, op)?, op)?, n, op)?;
2013
2014            // R-squared Calculation
2015            let mean_y = d_div(sum_y, n, op)?;
2016            let mut sst = Decimal::ZERO;
2017            for y in &y_vals {
2018                let centered = d_sub(*y, mean_y, op)?;
2019                sst = d_add(sst, powu_checked(centered, 2, op)?, op)?;
2020            }
2021
2022            let mut ssr = Decimal::ZERO;
2023            for (y, x) in y_vals.iter().zip(&x_vals) {
2024                let y_predicted = d_add(d_mul(slope, *x, op)?, intercept, op)?;
2025                let residual = d_sub(*y, y_predicted, op)?;
2026                ssr = d_add(ssr, powu_checked(residual, 2, op)?, op)?;
2027            }
2028
2029            let r_squared = if sst == Decimal::ZERO {
2030                Decimal::ONE
2031            } else {
2032                d_sub(Decimal::ONE, d_div(ssr, sst, op)?, op)?
2033            };
2034
2035            Ok((slope, intercept, r_squared))
2036        };
2037
2038        // A sample whose abscissas all collapse to one value (or whose squares
2039        // underflow the `Decimal` scale) leaves the ordinary-least-squares
2040        // denominator at zero, where the slope is undefined rather than
2041        // infinite.
2042        let (slope, intercept, r_squared) =
2043            trend().map_err(|e| MetricsError::TrendError(e.to_string()))?;
2044
2045        // Moving Average Calculation
2046        let window_sizes = [3, 5, 7];
2047        let mut moving_average: Vec<Point2D> = Vec::new();
2048        for window in window_sizes {
2049            if window > points.len() {
2050                continue;
2051            }
2052            for window_points in points.windows(window) {
2053                let xs: Vec<Decimal> = window_points.iter().map(|p| p.x).collect();
2054                let ys: Vec<Decimal> = window_points.iter().map(|p| p.y).collect();
2055                let avg_x = mean_of(&xs, "Curve::compute_trend_metrics::moving_average_x")
2056                    .map_err(|e| MetricsError::TrendError(e.to_string()))?;
2057                let avg_y = mean_of(&ys, "Curve::compute_trend_metrics::moving_average_y")
2058                    .map_err(|e| MetricsError::TrendError(e.to_string()))?;
2059                moving_average.push(Point2D::new(avg_x, avg_y));
2060            }
2061        }
2062
2063        Ok(TrendMetrics {
2064            slope,
2065            intercept,
2066            r_squared,
2067            moving_average,
2068        })
2069    }
2070
2071    fn compute_risk_metrics(&self) -> Result<RiskMetrics, MetricsError> {
2072        let y_values: Vec<Decimal> = self.points.iter().map(|p| p.y).collect();
2073
2074        if y_values.is_empty() {
2075            return Ok(RiskMetrics {
2076                volatility: Decimal::ZERO,
2077                value_at_risk: Decimal::ZERO,
2078                expected_shortfall: Decimal::ZERO,
2079                coefficient_of_variation: Decimal::ZERO,
2080                sharpe_ratio: Decimal::ZERO,
2081            });
2082        }
2083
2084        let op = "Curve::compute_risk_metrics";
2085        let mean = mean_of(&y_values, op).map_err(|e| MetricsError::RiskError(e.to_string()))?;
2086        // The population standard deviation `sqrt(S / n)`, through the
2087        // helper `Surface` uses too, so both report the same volatility for
2088        // the same values (#840). It was `S / sqrt(n)`, the sum of squared
2089        // deviations `S` over `sqrt(n)`: not a standard deviation, and
2090        // `sqrt(S)` times this one.
2091        let volatility = population_std_dev(&y_values, mean, op)
2092            .map_err(|e| MetricsError::RiskError(e.to_string()))?;
2093
2094        // Value at Risk (95% confidence) using parametric method. At zero
2095        // dispersion this is `mean - 1.645 * 0 = mean`, a deterministic level
2096        // rather than an absence of value, so it is computed from the formula
2097        // on every path instead of being short-circuited to zero.
2098        let z_score = dec!(1.645);
2099        let scaled_vol =
2100            d_mul(z_score, volatility, op).map_err(|e| MetricsError::RiskError(e.to_string()))?;
2101        let var =
2102            d_sub(mean, scaled_vol, op).map_err(|e| MetricsError::RiskError(e.to_string()))?;
2103
2104        let tail: Vec<Decimal> = y_values.iter().copied().filter(|&x| x < var).collect();
2105        let expected_shortfall = if tail.is_empty() {
2106            Decimal::ZERO
2107        } else {
2108            mean_of(&tail, op).map_err(|e| MetricsError::RiskError(e.to_string()))?
2109        };
2110
2111        // The coefficient of variation: the population standard deviation,
2112        // `volatility` here, over the mean, as `Surface` computes it (#824).
2113        let coefficient_of_variation = coefficient_of_variation(volatility, mean, op)?;
2114
2115        // Sharpe Ratio (assuming risk-free rate of 0). A flat curve has no
2116        // dispersion to divide by, which makes this the one field that is
2117        // genuinely undefined at `volatility == 0`; the others keep their
2118        // deterministic limits.
2119        let sharpe_ratio = if volatility.is_zero() {
2120            Decimal::ZERO
2121        } else {
2122            d_div(mean, volatility, op).map_err(|e| MetricsError::RiskError(e.to_string()))?
2123        };
2124
2125        Ok(RiskMetrics {
2126            volatility,
2127            value_at_risk: var,
2128            expected_shortfall,
2129            coefficient_of_variation,
2130            sharpe_ratio,
2131        })
2132    }
2133}
2134
2135/// Implements the `CurveArithmetic` trait for the `Curve` type, providing
2136/// functionality for merging multiple curves using a specified mathematical
2137/// operation and performing arithmetic operations between two curves.
2138impl Arithmetic<Curve> for Curve {
2139    type Error = CurveError;
2140
2141    /// Merges a collection of curves into a single curve based on the specified
2142    /// mathematical operation.
2143    ///
2144    /// # Parameters
2145    ///
2146    /// - `curves` (`&[&Curve]`): A slice of references to the curves to be merged.
2147    ///   Each curve must have defined x-ranges and interpolation capabilities.
2148    /// - `operation` (`MergeOperation`): The arithmetic operation to perform on the
2149    ///   interpolated y-values for the provided curves. Operations include addition,
2150    ///   subtraction, multiplication, division, and aggregation (e.g., max, min).
2151    ///
2152    /// # Returns
2153    ///
2154    /// - `Ok(Curve)`: Returns a new curve resulting from the merging operation.
2155    /// - `Err(CurvesError)`: If input parameters are invalid or the merge operation
2156    ///   encounters an error (e.g., incompatible x-ranges or interpolation failure),
2157    ///   an error is returned.
2158    ///
2159    /// # Behavior
2160    ///
2161    /// 1. **Parameter Validation**:
2162    ///   - Verifies that at least one curve is provided in the `curves` parameter.
2163    ///   - Returns an error if no curves are included or if x-ranges are incompatible.
2164    ///
2165    /// 2. **Cloning Single Curve**:
2166    ///   - If only one curve is provided, its clone is returned as the result without
2167    ///     performing any further calculations.
2168    ///
2169    /// 3. **Range Computation**:
2170    ///   - Computes the intersection of x-ranges across the curves by finding the
2171    ///     maximum lower bound (`min_x`) and minimum upper bound (`max_x`).
2172    ///   - If the x-range intersection is invalid (i.e., `min_x >= max_x`), an error
2173    ///     is returned.
2174    ///
2175    /// 4. **Interpolation and Arithmetic**:
2176    ///   - Divides the x-range into `steps` equally spaced intervals (default: 100).
2177    ///   - Interpolates the y-values for all curves at each x-value in the range.
2178    ///   - Applies the specified `operation` to the aggregated y-values at each x-point.
2179    ///
2180    /// 5. **Parallel Processing**:
2181    ///   - Uses parallel iteration over the grid to interpolate the operands
2182    ///     efficiently, leveraging the Rayon library. The grid points are
2183    ///     independent of one another and are collected back in x order.
2184    ///
2185    /// 6. **Error Handling**:
2186    ///   - Any errors during interpolation or arithmetic operations are propagated
2187    ///     back to the caller.
2188    ///
2189    /// # Determinism
2190    ///
2191    /// The same operands in the same order give the same result, digit for
2192    /// digit, with the one exception noted for `Max` and `Min` below. Within
2193    /// a grid point the operands are accumulated in the order they were
2194    /// passed in, never in an order chosen by the thread pool.
2195    ///
2196    /// This matters for `Multiply` and `Divide` in particular. `Decimal`
2197    /// multiplication rounds once a product needs more than 28 decimal
2198    /// digits, so it is not associative, and a parallel reducer would take
2199    /// its bracketing from whatever chunking rayon picked on the day: three
2200    /// or more curves could then merge to different last digits on
2201    /// consecutive runs of one binary over one input. `Add` and `Subtract`
2202    /// fold in order for the same reason.
2203    ///
2204    /// `Max` and `Min` still reduce in parallel. They select an operand
2205    /// instead of accumulating one, so no rounding enters and the extremum
2206    /// is the same whatever the grouping. What is not pinned is which of
2207    /// several operands that compare equal is the one returned, and two
2208    /// `Decimal` values comparing equal can still carry different scales, so
2209    /// a tie between `2.0` and `2.00` is the one case where the stored digits
2210    /// are left to the reducer.
2211    ///
2212    /// # Errors
2213    ///
2214    /// - **Invalid Parameter** (`CurvesError`): Returned when no curves are provided
2215    ///   or x-ranges are incompatible.
2216    /// - **Interpolation Failure** (`CurvesError`): Raised if interpolation fails
2217    ///   for a specific curve or x-value.
2218    ///
2219    /// # Example Use Case
2220    ///
2221    /// This function enables combining multiple curves for tasks such as:
2222    /// - Summing y-values across different curves to compute a composite curve.
2223    /// - Finding the maximum/minimum y-value at each x-point for a collection of curves.
2224    ///
2225    /// # One point per abscissa
2226    ///
2227    /// This assumes the one-point-per-abscissa rule of [`Curve::new`] and
2228    /// re-establishes it in the result: the merged curve is sampled on an
2229    /// evenly spaced grid over the common x-range, with one `y` per grid
2230    /// point, so the original abscissae do not survive the merge.
2231    ///
2232    /// An input carrying several ordinates at one abscissa never has one of
2233    /// them silently chosen. Each sample is produced by cubic interpolation,
2234    /// which reports [`InterpolationError::DegenerateInterval`], wrapped in
2235    /// a [`CurveError`], whenever the sample lands on the repeated abscissa
2236    /// or brackets across it. A sample far enough from the stack is
2237    /// unaffected and reads the ordinate on its own side, so a merge of such
2238    /// a curve can still succeed on a grid that misses the stack. Aggregate
2239    /// the curve to one ordinate per abscissa before merging it.
2240    fn merge(curves: &[&Curve], operation: MergeOperation) -> Result<Curve, CurveError> {
2241        if curves.is_empty() {
2242            return Err(CurveError::invalid_parameters(
2243                "merge_curves",
2244                "No curves provided for merging",
2245            ));
2246        }
2247
2248        // If only one curve, return a clone
2249        if let [only] = curves {
2250            return Ok((*only).clone());
2251        }
2252
2253        // Find the intersection of x-ranges
2254        let min_x = curves
2255            .iter()
2256            .map(|c| c.x_range.0)
2257            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
2258            .unwrap_or(Decimal::ZERO);
2259
2260        let max_x = curves
2261            .iter()
2262            .map(|c| c.x_range.1)
2263            .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
2264            .unwrap_or(Decimal::ZERO);
2265
2266        // Check if ranges are compatible
2267        if min_x >= max_x {
2268            return Err(CurveError::invalid_parameters(
2269                "merge_curves",
2270                "Curves have incompatible x-ranges",
2271            ));
2272        }
2273
2274        // Determine number of interpolation steps
2275        let steps = 100; // Configurable number of interpolation points
2276        let op = "Curve::merge";
2277        let span = d_sub(max_x, min_x, op).map_err(construction_err)?;
2278        let step_size = d_div(span, Decimal::from(steps), op).map_err(construction_err)?;
2279
2280        // Interpolate and perform operation using parallel iterator
2281        let result_points: Result<Vec<Point2D>, CurveError> = (0..=steps)
2282            .into_par_iter()
2283            .map(|i| {
2284                let offset = d_mul(step_size, Decimal::from(i), op).map_err(construction_err)?;
2285                // `step_size` is rounded at 28 places, so on a span with a
2286                // long mantissa `step_size * steps` can land past `max_x`
2287                // and fail the interpolation (#795). Clamping moves only
2288                // such a point, so every grid that fit before is unchanged.
2289                let x = d_add(min_x, offset, op)
2290                    .map_err(construction_err)?
2291                    .min(max_x);
2292
2293                // Interpolate y values for each curve
2294                let y_values: Result<Vec<Decimal>, CurveError> = curves
2295                    .iter()
2296                    .map(|curve| {
2297                        curve
2298                            .interpolate(x, InterpolationType::Cubic)
2299                            .map(|point| point.y)
2300                            .map_err(CurveError::from)
2301                    })
2302                    .collect();
2303
2304                let y_values = y_values?;
2305
2306                // Perform the specified operation on interpolated y values
2307                let result_y: Decimal = match operation {
2308                    MergeOperation::Add => {
2309                        d_sum_iter(y_values.iter().copied(), op).map_err(construction_err)?
2310                    }
2311                    MergeOperation::Subtract => {
2312                        let signed = y_values
2313                            .iter()
2314                            .enumerate()
2315                            .map(|(i, &val)| if i == 0 { val } else { -val });
2316                        d_sum_iter(signed, op).map_err(construction_err)?
2317                    }
2318                    MergeOperation::Multiply => {
2319                        d_product_iter(y_values.iter().copied(), op).map_err(construction_err)?
2320                    }
2321                    MergeOperation::Divide => {
2322                        // Division is neither associative nor commutative, so
2323                        // every element after the first enters as its
2324                        // reciprocal and the whole set is folded in sequence
2325                        // from the leftmost value, leaving no partial to
2326                        // discard. The fold is sequential for the same reason
2327                        // `Multiply` is: `Decimal` multiplication rounds at 28
2328                        // digits and is not associative either, so a parallel
2329                        // reducer would take its bracketing from rayon's
2330                        // chunking and move the last digits with it.
2331                        y_values.iter().copied().enumerate().try_fold(
2332                            Decimal::ONE,
2333                            |acc, (i, val)| {
2334                                // `Decimal::MAX` is the pre-existing sentinel
2335                                // for a zero divisor; only the arithmetic is
2336                                // made checked.
2337                                let factor = if i == 0 {
2338                                    val
2339                                } else if val == Decimal::ZERO {
2340                                    Decimal::MAX
2341                                } else {
2342                                    d_div(Decimal::ONE, val, op).map_err(construction_err)?
2343                                };
2344                                d_mul(acc, factor, op).map_err(construction_err)
2345                            },
2346                        )?
2347                    }
2348                    MergeOperation::Max => y_values
2349                        .par_iter()
2350                        .cloned()
2351                        .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
2352                        .unwrap_or(Decimal::ZERO),
2353                    MergeOperation::Min => y_values
2354                        .par_iter()
2355                        .cloned()
2356                        .min_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
2357                        .unwrap_or(Decimal::ZERO),
2358                };
2359
2360                Ok(Point2D::new(x, result_y))
2361            })
2362            .collect();
2363
2364        // Handle potential errors during parallel processing
2365        let result_points = result_points?;
2366
2367        Ok(Curve::from_vector(result_points))
2368    }
2369
2370    /// Combines the current `Curve` instance with another curve using a mathematical
2371    /// operation, resulting in a new curve.
2372    ///
2373    /// # Parameters
2374    ///
2375    /// - `self` (`&Self`): A reference to the current curve instance.
2376    /// - `other` (`&Curve`): A reference to the second curve for the arithmetic operation.
2377    /// - `operation` (`MergeOperation`): The operation to apply when merging the curves.
2378    ///
2379    /// # Returns
2380    ///
2381    /// - `Ok(Curve)`: Returns a new curve that represents the result of the operation.
2382    /// - `Err(CurvesError)`: If the merge operation fails (e.g., incompatible x-ranges or
2383    ///   interpolation errors), an error is returned.
2384    ///
2385    /// # Behavior
2386    ///
2387    /// This function is a convenience wrapper around `merge_curves` that operates
2388    /// specifically on two curves. It passes `self` and `other` as an array to
2389    /// `merge_curves` and applies the desired operation.
2390    ///
2391    /// # Errors
2392    ///
2393    /// - Inherits all errors returned by the `merge_curves` method, including parameter
2394    ///   validation and interpolation errors.
2395    ///
2396    /// # Examples
2397    ///
2398    /// Use this method to easily perform arithmetic operations between two curves,
2399    /// such as summing their y-values or finding their pointwise maximum.
2400    fn merge_with(&self, other: &Curve, operation: MergeOperation) -> Result<Curve, CurveError> {
2401        Self::merge(&[self, other], operation)
2402    }
2403}
2404
2405impl AxisOperations<Point2D, Decimal> for Curve {
2406    type Error = CurveError;
2407
2408    /// Reports whether the curve has a point at the abscissa `x`.
2409    ///
2410    /// Answers on `x` alone, so it says nothing about how many ordinates sit
2411    /// there. On a curve honouring the one-point-per-abscissa rule of
2412    /// [`Curve::new`] there is at most one.
2413    fn contains_point(&self, x: &Decimal) -> bool {
2414        self.points_at(*x).next().is_some()
2415    }
2416
2417    /// Returns the abscissa of every point, in `(x, y)` order.
2418    ///
2419    /// A curve holding several ordinates at one abscissa yields that
2420    /// abscissa once per ordinate; the duplicates are visible here rather
2421    /// than dropped.
2422    fn get_index_values(&self) -> Vec<Decimal> {
2423        self.points.iter().map(|p| p.x).collect()
2424    }
2425
2426    /// Returns every ordinate at the abscissa `x`, in ascending order.
2427    ///
2428    /// This is the multi-valued reader: unlike [`Self::get_point`], it never
2429    /// picks a survivor. A curve honouring the one-point-per-abscissa rule
2430    /// of [`Curve::new`] returns at most one value; anything longer says the
2431    /// curve is not a function of its abscissa.
2432    fn get_values(&self, x: Decimal) -> Vec<&Decimal> {
2433        self.points_at(x).map(|p| &p.y).collect()
2434    }
2435
2436    fn get_closest_point(&self, x: &Decimal) -> Result<&Point2D, Self::Error> {
2437        // The distance is folded explicitly rather than computed inside a
2438        // comparator: `a.x - x` overflows for abscissas at opposite ends of
2439        // the `Decimal` range, and `min_by` has no channel for that.
2440        let mut closest: Option<(&Point2D, Decimal)> = None;
2441        for point in &self.points {
2442            let distance = d_sub(point.x, *x, "Curve::get_closest_point")
2443                .map_err(analysis_err)?
2444                .abs();
2445            // `min_by` keeps the first of several equal minima; `<=` here
2446            // preserves that.
2447            match closest {
2448                Some((_, best)) if best <= distance => {}
2449                _ => closest = Some((point, distance)),
2450            }
2451        }
2452
2453        closest
2454            .map(|(point, _)| point)
2455            .ok_or(CurveError::Point2DError {
2456                reason: "No points available",
2457            })
2458    }
2459
2460    /// Returns the point at the abscissa `x`, if there is one.
2461    ///
2462    /// Assumes the one-point-per-abscissa rule of [`Curve::new`]. If the
2463    /// curve breaks it, this returns the *first* match in `(x, y)` order,
2464    /// which is the lowest ordinate of the stack, and the rest are invisible
2465    /// to the caller. Use [`Self::get_values`] to see all of them.
2466    fn get_point(&self, x: &Decimal) -> Option<&Point2D> {
2467        self.points_at(*x).next()
2468    }
2469}
2470
2471/// [`GeometricTransformations::intersect_with`] for curves, comparing every
2472/// pair of samples: the reference the windowed walk reproduces, kept for the
2473/// case where an abscissa difference can overflow, where the error has to
2474/// name the first such pair in `(p1, p2)` order.
2475fn intersect_all_pairs(
2476    curve: &Curve,
2477    other: &Curve,
2478    epsilon: Decimal,
2479) -> Result<Vec<Point2D>, CurveError> {
2480    let mut intersections = Vec::new();
2481    for p1 in &curve.points {
2482        for p2 in &other.points {
2483            // Find points with small distance between them
2484            let dx = d_sub(p1.x, p2.x, "Curve::intersect_with::dx")
2485                .map_err(analysis_err)?
2486                .abs();
2487            if dx >= epsilon {
2488                continue;
2489            }
2490            let dy = d_sub(p1.y, p2.y, "Curve::intersect_with::dy")
2491                .map_err(analysis_err)?
2492                .abs();
2493            if dy < epsilon {
2494                intersections.push(*p1);
2495            }
2496        }
2497    }
2498    Ok(intersections)
2499}
2500
2501/// Reads `curve` at `x` for a resampling. A spline is solved into `spline`
2502/// on the first call and reused after, with the answers
2503/// [`Interpolate::interpolate`] gives.
2504fn resample<'a>(
2505    curve: &'a Curve,
2506    spline: &mut Option<CurveSpline<'a>>,
2507    x: Decimal,
2508    interpolation: InterpolationType,
2509) -> Result<Point2D, InterpolationError> {
2510    if interpolation == InterpolationType::Spline {
2511        spline
2512            .get_or_insert_with(|| CurveSpline::new(curve))
2513            .interpolate(x)
2514    } else {
2515        curve.interpolate(x, interpolation)
2516    }
2517}
2518
2519impl MergeAxisInterpolate<Point2D, Decimal> for Curve
2520where
2521    Self: Sized,
2522{
2523    /// Resamples both curves onto the shared x-grid of their merged indices.
2524    ///
2525    /// Each result holds one point per abscissa of that grid, so it is a
2526    /// function of its abscissa whatever the inputs were: where a curve
2527    /// already has a point at an abscissa, the one kept is the first match
2528    /// in `(x, y)` order, so a stack of ordinates collapses to its lowest,
2529    /// and elsewhere the point is interpolated. Aggregate such a curve
2530    /// before merging if a different ordinate is wanted.
2531    fn merge_axis_interpolate(
2532        &self,
2533        other: &Self,
2534        interpolation: InterpolationType,
2535    ) -> Result<(Self, Self), Self::Error> {
2536        // Get merged unique x-coordinates
2537        let merged_x_values = self.merge_axis_index(other);
2538
2539        // Sort the merged x values
2540        let mut sorted_x_values: Vec<Decimal> = merged_x_values.into_iter().collect();
2541        sorted_x_values.sort();
2542
2543        let mut interpolated_self_points = BTreeSet::new();
2544        let mut interpolated_other_points = BTreeSet::new();
2545
2546        // A spline is solved once per curve, on the first abscissa that
2547        // needs it, instead of once per resampled abscissa (#858).
2548        let mut self_spline: Option<CurveSpline<'_>> = None;
2549        let mut other_spline: Option<CurveSpline<'_>> = None;
2550
2551        for x in &sorted_x_values {
2552            if self.contains_point(x) {
2553                let pt = self.get_point(x).ok_or_else(|| {
2554                    CurveError::InterpolationError(format!(
2555                        "missing self point at x={x} despite contains_point()"
2556                    ))
2557                })?;
2558                interpolated_self_points.insert(*pt);
2559            } else {
2560                let interpolated_point = resample(self, &mut self_spline, *x, interpolation)?;
2561                interpolated_self_points.insert(interpolated_point);
2562            }
2563            if other.contains_point(x) {
2564                let pt = other.get_point(x).ok_or_else(|| {
2565                    CurveError::InterpolationError(format!(
2566                        "missing other point at x={x} despite contains_point()"
2567                    ))
2568                })?;
2569                interpolated_other_points.insert(*pt);
2570            } else {
2571                let interpolated_point = resample(other, &mut other_spline, *x, interpolation)?;
2572                interpolated_other_points.insert(interpolated_point);
2573            }
2574        }
2575        Ok((
2576            Curve::new(interpolated_self_points),
2577            Curve::new(interpolated_other_points),
2578        ))
2579    }
2580}
2581
2582impl GeometricTransformations<Point2D> for Curve {
2583    type Error = CurveError;
2584
2585    fn translate(&self, deltas: Vec<&Decimal>) -> Result<Self, Self::Error> {
2586        if deltas.len() != 2 {
2587            return Err(CurveError::invalid_parameters(
2588                "translate",
2589                "Expected 2 deltas for 2D translation",
2590            ));
2591        }
2592
2593        let (Some(dx), Some(dy)) = (deltas.first(), deltas.get(1)) else {
2594            return Err(CurveError::invalid_parameters(
2595                "translate",
2596                "Expected 2 deltas for 2D translation",
2597            ));
2598        };
2599
2600        let translated_points = self
2601            .points
2602            .iter()
2603            .map(|point| {
2604                let x = d_add(point.x, **dx, "Curve::translate::x")?;
2605                let y = d_add(point.y, **dy, "Curve::translate::y")?;
2606                Ok(Point2D::new(x, y))
2607            })
2608            .collect::<Result<BTreeSet<Point2D>, DecimalError>>()
2609            .map_err(construction_err)?;
2610
2611        Ok(Curve::new(translated_points))
2612    }
2613
2614    fn scale(&self, factors: Vec<&Decimal>) -> Result<Self, Self::Error> {
2615        if factors.len() != 2 {
2616            return Err(CurveError::invalid_parameters(
2617                "scale",
2618                "Expected 2 factors for 2D scaling",
2619            ));
2620        }
2621
2622        let (Some(fx), Some(fy)) = (factors.first(), factors.get(1)) else {
2623            return Err(CurveError::invalid_parameters(
2624                "scale",
2625                "Expected 2 factors for 2D scaling",
2626            ));
2627        };
2628
2629        let scaled_points = self
2630            .points
2631            .iter()
2632            .map(|point| {
2633                let x = d_mul(point.x, **fx, "Curve::scale::x")?;
2634                let y = d_mul(point.y, **fy, "Curve::scale::y")?;
2635                Ok(Point2D::new(x, y))
2636            })
2637            .collect::<Result<BTreeSet<Point2D>, DecimalError>>()
2638            .map_err(construction_err)?;
2639
2640        Ok(Curve::new(scaled_points))
2641    }
2642
2643    /// Reports the samples the two curves share.
2644    ///
2645    /// # What it computes
2646    ///
2647    /// Every pair of samples `(p1, p2)`, `p1` from `self` and `p2` from
2648    /// `other`, whose abscissas differ by less than `1e-6` and whose
2649    /// ordinates differ by less than `1e-6`, contributes `p1` to the result.
2650    /// The result is in `(p1, p2)` order: by `self`'s `(x, y)` order, then by
2651    /// `other`'s for one `p1`.
2652    ///
2653    /// # What it does not compute
2654    ///
2655    /// Only coinciding samples are compared, never the segments between
2656    /// them. Two curves that cross between their samples are not reported,
2657    /// so `(0, 0), (2, 2)` and `(0, 2), (2, 0)`, which cross at `(1, 1)`,
2658    /// return nothing. A sample of `self` that coincides with several samples
2659    /// of `other` appears once per match. Whether a segment-crossing variant
2660    /// should exist is an open owner decision (#858); this method documents
2661    /// the current behaviour.
2662    ///
2663    /// # Cost
2664    ///
2665    /// Both point sets are sorted by abscissa, so the samples of `other`
2666    /// within `1e-6` of `p1` form a window that only slides forward as `p1`
2667    /// advances. The walk is O(n + m) plus one step per matching pair, with
2668    /// no allocation beyond the result (#858). It visits the pairs the
2669    /// all-pairs comparison tested on ordinates, in the same order, so the
2670    /// result and any error are the same. When some abscissa difference
2671    /// could overflow, the all-pairs comparison runs instead, so the error
2672    /// names the same pair.
2673    ///
2674    /// # Errors
2675    ///
2676    /// [`CurveError::AnalysisError`] when a coordinate difference leaves the
2677    /// `Decimal` range.
2678    fn intersect_with(&self, other: &Self) -> Result<Vec<Point2D>, Self::Error> {
2679        let epsilon = Decimal::new(1, 6);
2680        let fits = match (
2681            self.points.first(),
2682            self.points.last(),
2683            other.points.first(),
2684            other.points.last(),
2685        ) {
2686            // Each difference `p1.x - p2.x` lies between these two.
2687            (Some(first), Some(last), Some(other_first), Some(other_last)) => {
2688                d_sub(first.x, other_last.x, "Curve::intersect_with::dx").is_ok()
2689                    && d_sub(last.x, other_first.x, "Curve::intersect_with::dx").is_ok()
2690            }
2691            // An empty curve shares nothing; both paths return no point.
2692            _ => true,
2693        };
2694        if !fits {
2695            return intersect_all_pairs(self, other, epsilon);
2696        }
2697
2698        let mut intersections = Vec::new();
2699        // The samples of `other` not yet `epsilon` or more to the left of
2700        // the current `p1`. A sample dropped here is that far left of every
2701        // later `p1` too, whose abscissas are no smaller.
2702        let mut window = other.points.iter();
2703        for p1 in &self.points {
2704            while let Some(p2) = window.clone().next() {
2705                let dx = d_sub(p1.x, p2.x, "Curve::intersect_with::dx").map_err(analysis_err)?;
2706                if dx < epsilon {
2707                    break;
2708                }
2709                window.next();
2710            }
2711            for p2 in window.clone() {
2712                let dx = d_sub(p1.x, p2.x, "Curve::intersect_with::dx")
2713                    .map_err(analysis_err)?
2714                    .abs();
2715                // Past the window: `p2` is `epsilon` or more to the right,
2716                // and so is every sample after it.
2717                if dx >= epsilon {
2718                    break;
2719                }
2720                let dy = d_sub(p1.y, p2.y, "Curve::intersect_with::dy")
2721                    .map_err(analysis_err)?
2722                    .abs();
2723                if dy < epsilon {
2724                    intersections.push(*p1);
2725                }
2726            }
2727        }
2728
2729        Ok(intersections)
2730    }
2731
2732    fn derivative_at(&self, point: &Point2D) -> Result<Vec<Decimal>, Self::Error> {
2733        let (p0, p1) = self.bracket(point.x)?;
2734
2735        let op = "Curve::derivative_at";
2736        let rise = d_sub(p1.y, p0.y, op).map_err(analysis_err)?;
2737        let x1_sq = d_mul(p1.x, p1.x, op).map_err(analysis_err)?;
2738        let x0_sq = d_mul(p0.x, p0.x, op).map_err(analysis_err)?;
2739        let run = d_sub(x1_sq, x0_sq, op).map_err(analysis_err)?;
2740        if run.is_zero() {
2741            // The parabola fitted through the bracket is degenerate when the
2742            // two abscissas have the same square, so its coefficient is
2743            // undefined rather than infinite.
2744            return Err(CurveError::AnalysisError(
2745                "derivative_at: bracketing abscissas have equal squares".to_string(),
2746            ));
2747        }
2748
2749        let a = d_div(rise, run, op).map_err(analysis_err)?;
2750        let slope = d_mul(dec!(2.0), a, op).map_err(analysis_err)?;
2751        let derivative = d_mul(slope, point.x, op).map_err(analysis_err)?;
2752
2753        Ok(vec![derivative])
2754    }
2755
2756    fn extrema(&self) -> Result<(Point2D, Point2D), Self::Error> {
2757        if self.points.is_empty() {
2758            return Err(CurveError::invalid_parameters(
2759                "extrema",
2760                "Curve has no points",
2761            ));
2762        }
2763
2764        let min_point = self
2765            .points
2766            .iter()
2767            .min_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
2768            .cloned()
2769            .ok_or_else(|| {
2770                CurveError::AnalysisError("extrema: empty curve in min_by".to_string())
2771            })?;
2772
2773        let max_point = self
2774            .points
2775            .iter()
2776            .max_by(|a, b| a.y.partial_cmp(&b.y).unwrap_or(std::cmp::Ordering::Equal))
2777            .cloned()
2778            .ok_or_else(|| {
2779                CurveError::AnalysisError("extrema: empty curve in max_by".to_string())
2780            })?;
2781
2782        Ok((min_point, max_point))
2783    }
2784
2785    fn measure_under(&self, base_value: &Decimal) -> Result<Decimal, Self::Error> {
2786        if self.points.len() < 2 {
2787            return Ok(Decimal::ZERO);
2788        }
2789
2790        let mut area = Decimal::ZERO;
2791        let points: Vec<_> = self.points.iter().collect();
2792
2793        let op = "Curve::measure_under";
2794
2795        // Approximate area using trapezoidal rule
2796        for pair in points.windows(2) {
2797            let (Some(left), Some(right)) = (pair.first(), pair.get(1)) else {
2798                return Err(CurveError::AnalysisError(
2799                    "measure_under: trapezoid window is shorter than two points".to_string(),
2800                ));
2801            };
2802            let width = d_sub(right.x, left.x, op).map_err(analysis_err)?;
2803            let left_height = d_sub(left.y, *base_value, op).map_err(analysis_err)?;
2804            let right_height = d_sub(right.y, *base_value, op).map_err(analysis_err)?;
2805            let sum_heights = d_add(left_height, right_height, op).map_err(analysis_err)?;
2806            let height = d_div(sum_heights, Decimal::TWO, op).map_err(analysis_err)?;
2807            let slice = d_mul(width, height, op).map_err(analysis_err)?;
2808            area = d_add(area, slice, op).map_err(analysis_err)?;
2809        }
2810
2811        Ok(area.abs())
2812    }
2813}
2814
2815#[cfg(test)]
2816mod tests_curves {
2817    use super::*;
2818
2819    use crate::curves::utils::{create_constant_curve, create_linear_curve};
2820    use Decimal;
2821    use optionstratlib_core::{model::Positive, pos_or_panic};
2822    use rust_decimal_macros::dec;
2823
2824    #[test]
2825    fn test_new_with_decimal() {
2826        let x = dec!(1.5);
2827        let y = dec!(2.5);
2828        let point = Point2D::new(x, y);
2829        assert_eq!(point.x, dec!(1.5));
2830        assert_eq!(point.y, dec!(2.5));
2831    }
2832
2833    #[test]
2834    fn test_new_with_positive() {
2835        let x = pos_or_panic!(1.5_f64);
2836        let y = pos_or_panic!(2.5_f64);
2837        let point = Point2D::new(x, y);
2838        assert_eq!(point.x, dec!(1.5));
2839        assert_eq!(point.y, dec!(2.5));
2840    }
2841
2842    #[test]
2843    fn test_to_tuple_with_decimal() {
2844        let point = Point2D::new(dec!(1.5), dec!(2.5));
2845        let tuple: (Decimal, Decimal) = point.to_tuple().unwrap();
2846        assert_eq!(tuple, (dec!(1.5), dec!(2.5)));
2847    }
2848
2849    #[test]
2850    fn test_to_tuple_with_positive() {
2851        let point = Point2D::new(dec!(1.5), dec!(2.5));
2852        let tuple: (Positive, Positive) = point.to_tuple().unwrap();
2853        assert_eq!(tuple, (pos_or_panic!(1.5), pos_or_panic!(2.5)));
2854    }
2855
2856    #[test]
2857    fn test_from_tuple_with_decimal() {
2858        let x = dec!(1.5);
2859        let y = dec!(2.5);
2860        let point = Point2D::from_tuple(x, y).unwrap();
2861        assert_eq!(point, Point2D::new(dec!(1.5), dec!(2.5)));
2862    }
2863
2864    #[test]
2865    fn test_from_tuple_with_positive() {
2866        let x = pos_or_panic!(1.5_f64);
2867        let y = pos_or_panic!(2.5_f64);
2868        let point = Point2D::from_tuple(x, y).unwrap();
2869        assert_eq!(point, Point2D::new(dec!(1.5), dec!(2.5)));
2870    }
2871
2872    #[test]
2873    fn test_new_with_mixed_types() {
2874        let x = dec!(1.5);
2875        let y = pos_or_panic!(2.5_f64);
2876        let point = Point2D::new(x, y);
2877        assert_eq!(point.x, dec!(1.5));
2878        assert_eq!(point.y, dec!(2.5));
2879    }
2880
2881    #[test]
2882    fn test_create_constant_curve() {
2883        let curve = create_constant_curve(dec!(1.0), dec!(2.0), dec!(5.0)).unwrap();
2884        assert_eq!(curve.get_points().len(), 11);
2885        for point in curve.get_points() {
2886            assert_eq!(point.y, dec!(5.0));
2887        }
2888    }
2889
2890    #[test]
2891    fn test_create_linear_curve() {
2892        let curve = create_linear_curve(dec!(1.0), dec!(2.0), dec!(2.0)).unwrap();
2893        assert_eq!(curve.get_points().len(), 11);
2894        let mut slope = dec!(2.0);
2895        for point in curve.get_points() {
2896            assert_eq!(point.y, slope);
2897            slope += dec!(0.2);
2898        }
2899    }
2900}
2901
2902#[cfg(test)]
2903mod tests_linear_interpolate {
2904    use super::*;
2905    use crate::geometrics::InterpolationType;
2906    use rust_decimal_macros::dec;
2907
2908    #[test]
2909    fn test_linear_interpolation_exact_points() {
2910        let curve = Curve::new(BTreeSet::from_iter(vec![
2911            Point2D::new(Decimal::ZERO, Decimal::ZERO),
2912            Point2D::new(Decimal::ONE, Decimal::TWO),
2913        ]));
2914
2915        // Test exact input points
2916        let p0 = curve
2917            .interpolate(Decimal::ZERO, InterpolationType::Linear)
2918            .unwrap();
2919        assert_eq!(p0.x, Decimal::ZERO);
2920        assert_eq!(p0.y, Decimal::ZERO);
2921
2922        let p1 = curve
2923            .interpolate(Decimal::ONE, InterpolationType::Linear)
2924            .unwrap();
2925        assert_eq!(p1.x, Decimal::ONE);
2926        assert_eq!(p1.y, Decimal::TWO);
2927    }
2928
2929    #[test]
2930    fn test_linear_interpolation_midpoint() {
2931        let curve = Curve::new(BTreeSet::from_iter(vec![
2932            Point2D::new(Decimal::ZERO, Decimal::ZERO),
2933            Point2D::new(Decimal::ONE, Decimal::TWO),
2934        ]));
2935
2936        // Test midpoint
2937        let mid = curve
2938            .interpolate(dec!(0.5), InterpolationType::Linear)
2939            .unwrap();
2940        assert_eq!(mid.x, dec!(0.5));
2941        assert_eq!(mid.y, dec!(1.0));
2942    }
2943
2944    #[test]
2945    fn test_linear_interpolation_quarter_points() {
2946        let curve = Curve::new(BTreeSet::from_iter(vec![
2947            Point2D::new(Decimal::ZERO, Decimal::ZERO),
2948            Point2D::new(Decimal::ONE, Decimal::TWO),
2949        ]));
2950
2951        // Test at x = 0.25
2952        let p25 = curve
2953            .interpolate(dec!(0.25), InterpolationType::Linear)
2954            .unwrap();
2955        assert_eq!(p25.x, dec!(0.25));
2956        assert_eq!(p25.y, dec!(0.5));
2957
2958        // Test at x = 0.75
2959        let p75 = curve
2960            .interpolate(dec!(0.75), InterpolationType::Linear)
2961            .unwrap();
2962        assert_eq!(p75.x, dec!(0.75));
2963        assert_eq!(p75.y, dec!(1.5));
2964    }
2965
2966    #[test]
2967    fn test_linear_interpolation_out_of_range() {
2968        let curve = Curve::new(BTreeSet::from_iter(vec![
2969            Point2D::new(dec!(0.0), dec!(0.0)),
2970            Point2D::new(dec!(1.0), dec!(2.0)),
2971        ]));
2972
2973        assert!(
2974            curve
2975                .interpolate(dec!(-0.1), InterpolationType::Linear)
2976                .is_err()
2977        );
2978        assert!(
2979            curve
2980                .interpolate(dec!(1.1), InterpolationType::Linear)
2981                .is_err()
2982        );
2983    }
2984
2985    #[test]
2986    fn test_linear_interpolation_insufficient_points() {
2987        let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
2988            dec!(0.0),
2989            dec!(0.0),
2990        )]));
2991
2992        assert!(
2993            curve
2994                .interpolate(dec!(0.5), InterpolationType::Linear)
2995                .is_err()
2996        );
2997    }
2998
2999    #[test]
3000    fn test_linear_interpolation_non_monotonic() {
3001        let curve = Curve::new(BTreeSet::from_iter(vec![
3002            Point2D::new(dec!(0.0), dec!(0.0)),
3003            Point2D::new(dec!(1.0), dec!(2.0)),
3004            Point2D::new(dec!(2.0), dec!(1.0)),
3005        ]));
3006
3007        let p15 = curve
3008            .interpolate(dec!(1.5), InterpolationType::Linear)
3009            .unwrap();
3010        assert_eq!(p15.x, dec!(1.5));
3011        assert_eq!(p15.y, dec!(1.5));
3012    }
3013}
3014
3015#[cfg(test)]
3016mod tests_bilinear_interpolate {
3017    use super::*;
3018    use crate::geometrics::InterpolationType;
3019    use rust_decimal_macros::dec;
3020
3021    #[test]
3022    fn test_bilinear_interpolation() {
3023        let curve = Curve::new(BTreeSet::from_iter(vec![
3024            Point2D::new(Decimal::ZERO, Decimal::ZERO),
3025            Point2D::new(Decimal::TWO, Decimal::ONE),
3026            Point2D::new(Decimal::TEN, Decimal::ONE),
3027            Point2D::new(Decimal::ONE, Decimal::TWO),
3028        ]));
3029
3030        // Test exact points
3031        let corner = curve
3032            .interpolate(Decimal::ZERO, InterpolationType::Bilinear)
3033            .unwrap();
3034        assert_eq!(corner.x, Decimal::ZERO);
3035        assert_eq!(corner.y, Decimal::ZERO);
3036
3037        // Test midpoint interpolation
3038        let mid = curve
3039            .interpolate(dec!(0.5), InterpolationType::Bilinear)
3040            .unwrap();
3041        assert_eq!(mid.x, dec!(0.5));
3042        assert_eq!(mid.y, dec!(1.0));
3043    }
3044
3045    #[test]
3046    fn test_bilinear_interpolation_out_of_range() {
3047        let curve = Curve::new(BTreeSet::from_iter(vec![
3048            Point2D::new(dec!(0.0), dec!(0.0)),
3049            Point2D::new(dec!(1.0), dec!(1.0)),
3050            Point2D::new(dec!(0.0), dec!(1.0)),
3051            Point2D::new(dec!(1.0), dec!(2.0)),
3052        ]));
3053
3054        assert!(
3055            curve
3056                .interpolate(dec!(-0.5), InterpolationType::Bilinear)
3057                .is_err()
3058        );
3059        assert!(
3060            curve
3061                .interpolate(dec!(1.5), InterpolationType::Bilinear)
3062                .is_err()
3063        );
3064    }
3065
3066    #[test]
3067    fn test_bilinear_interpolation_insufficient_points() {
3068        let curve = Curve::new(BTreeSet::from_iter(vec![
3069            Point2D::new(dec!(0.0), dec!(0.0)),
3070            Point2D::new(dec!(1.0), dec!(1.0)),
3071            Point2D::new(dec!(2.0), dec!(2.0)),
3072        ]));
3073
3074        assert!(
3075            curve
3076                .interpolate(dec!(0.5), InterpolationType::Bilinear)
3077                .is_err()
3078        );
3079    }
3080
3081    #[test]
3082    fn test_bilinear_interpolation_quarter_points() {
3083        let curve = Curve::new(BTreeSet::from_iter(vec![
3084            Point2D::new(Decimal::ZERO, Decimal::ZERO), // p11 (0,0)
3085            Point2D::new(Decimal::ONE, Decimal::ONE),   // p12 (1,1)
3086            Point2D::new(Decimal::TWO, Decimal::ONE),   // p21 (0,1)
3087            Point2D::new(Decimal::TEN, Decimal::TWO),   // p22 (1,2)
3088        ]));
3089
3090        // At x = 0.25:
3091        // Bottom edge: 0.25 * (1 - 0) = 0.25
3092        // Top edge: 0.25 * (2 - 1) + 1 = 1.25
3093        // Result: 0.25 + (1.25 - 0.25)/2 = 0.75
3094        let p25 = curve
3095            .interpolate(dec!(0.25), InterpolationType::Bilinear)
3096            .unwrap();
3097        assert_eq!(p25.x, dec!(0.25));
3098        assert_eq!(p25.y, dec!(0.75));
3099
3100        // At x = 0.75:
3101        // Bottom edge: 0.75 * (1 - 0) = 0.75
3102        // Top edge: 0.75 * (2 - 1) + 1 = 1.75
3103        // Result: 0.75 + (1.75 - 0.75)/2 = 1.25
3104        let p75 = curve
3105            .interpolate(dec!(0.75), InterpolationType::Bilinear)
3106            .unwrap();
3107        assert_eq!(p75.x, dec!(0.75));
3108        assert_eq!(p75.y, dec!(1.25));
3109    }
3110
3111    #[test]
3112    fn test_bilinear_interpolation_boundaries() {
3113        let curve = Curve::new(BTreeSet::from_iter(vec![
3114            Point2D::new(dec!(0.0), dec!(0.0)),
3115            Point2D::new(dec!(1.0), dec!(1.0)),
3116            Point2D::new(dec!(0.0), dec!(1.0)),
3117            Point2D::new(dec!(1.0), dec!(2.0)),
3118        ]));
3119
3120        assert!(
3121            curve
3122                .interpolate(dec!(-0.1), InterpolationType::Bilinear)
3123                .is_err()
3124        );
3125        assert!(
3126            curve
3127                .interpolate(dec!(1.1), InterpolationType::Bilinear)
3128                .is_err()
3129        );
3130    }
3131
3132    #[test]
3133    fn test_out_of_range() {
3134        let curve = Curve::new(BTreeSet::from_iter(vec![
3135            Point2D::new(Decimal::ZERO, Decimal::ZERO),
3136            Point2D::new(Decimal::ONE, Decimal::ONE),
3137            Point2D::new(Decimal::ZERO, Decimal::ONE),
3138            Point2D::new(Decimal::ONE, Decimal::TWO),
3139        ]));
3140
3141        assert!(
3142            curve
3143                .interpolate(dec!(-1), InterpolationType::Bilinear)
3144                .is_err()
3145        );
3146        assert!(
3147            curve
3148                .interpolate(Decimal::TWO, InterpolationType::Bilinear)
3149                .is_err()
3150        );
3151    }
3152
3153    /// The three curves the reference table was built on. `quadratic_*` are
3154    /// the samples of `y = x^2`; `irregular` has neither uniform spacing nor
3155    /// a monotone ordinate, so the far edge of a cell is a different width
3156    /// from the near one.
3157    fn quadratic_four() -> Curve {
3158        Curve::new(BTreeSet::from_iter(vec![
3159            Point2D::new(dec!(0), dec!(0)),
3160            Point2D::new(dec!(1), dec!(1)),
3161            Point2D::new(dec!(2), dec!(4)),
3162            Point2D::new(dec!(3), dec!(9)),
3163        ]))
3164    }
3165
3166    fn quadratic_five() -> Curve {
3167        Curve::new(BTreeSet::from_iter(vec![
3168            Point2D::new(dec!(0), dec!(0)),
3169            Point2D::new(dec!(1), dec!(1)),
3170            Point2D::new(dec!(2), dec!(4)),
3171            Point2D::new(dec!(3), dec!(9)),
3172            Point2D::new(dec!(4), dec!(16)),
3173        ]))
3174    }
3175
3176    fn irregular() -> Curve {
3177        Curve::new(BTreeSet::from_iter(vec![
3178            Point2D::new(dec!(0), dec!(1)),
3179            Point2D::new(dec!(0.5), dec!(2.25)),
3180            Point2D::new(dec!(2), dec!(-1.5)),
3181            Point2D::new(dec!(3.25), dec!(4)),
3182            Point2D::new(dec!(5), dec!(0.75)),
3183        ]))
3184    }
3185
3186    /// The two abscissas that used to abort and then, after #446, returned
3187    /// `InterpolationError::Bilinear`. Reference values computed in exact
3188    /// rational arithmetic from the rule stated on the impl: mean of the
3189    /// bracketing segment and the segment two on, the second clamped to the
3190    /// curve's last.
3191    ///
3192    /// `x = 1.5`: near edge `(1,1)-(2,4)` gives `2.5`, far edge clamped to
3193    /// `(2,4)-(3,9)` gives `6.5`, mean `4.5`.
3194    /// `x = 2.5`: both edges are `(2,4)-(3,9)`, mean of `6.5` with itself.
3195    #[test]
3196    fn test_bilinear_interpolate_four_point_upper_segments_match_reference() {
3197        let curve = quadratic_four();
3198
3199        let mid = curve.bilinear_interpolate(dec!(1.5)).unwrap();
3200        assert_eq!(mid.x, dec!(1.5));
3201        assert_eq!(mid.y, dec!(4.5));
3202
3203        let last = curve.bilinear_interpolate(dec!(2.5)).unwrap();
3204        assert_eq!(last.x, dec!(2.5));
3205        assert_eq!(last.y, dec!(6.5));
3206    }
3207
3208    /// On a five-point curve the clamp bites on the last two segments only.
3209    /// `x = 2.5`: near `(2,4)-(3,9)` gives `6.5`, far clamped to
3210    /// `(3,9)-(4,16)` gives `12.5`, mean `9.5`.
3211    /// `x = 3.5`: both edges are `(3,9)-(4,16)`, mean of `12.5` with itself.
3212    #[test]
3213    fn test_bilinear_interpolate_five_point_upper_segments_match_reference() {
3214        let curve = quadratic_five();
3215
3216        let second_to_last = curve.bilinear_interpolate(dec!(2.5)).unwrap();
3217        assert_eq!(second_to_last.y, dec!(9.5));
3218
3219        let last = curve.bilinear_interpolate(dec!(3.5)).unwrap();
3220        assert_eq!(last.y, dec!(12.5));
3221    }
3222
3223    /// The clamp only reaches the last two segments, so every abscissa the
3224    /// method already answered keeps its answer. These are the values the
3225    /// pre-#451 code produced.
3226    #[test]
3227    fn test_bilinear_interpolate_interior_is_unchanged_by_the_clamp() {
3228        let four = quadratic_four();
3229        assert_eq!(four.bilinear_interpolate(dec!(0.5)).unwrap().y, dec!(3.5));
3230
3231        let five = quadratic_five();
3232        assert_eq!(five.bilinear_interpolate(dec!(0.5)).unwrap().y, dec!(3.5));
3233        assert_eq!(five.bilinear_interpolate(dec!(1.5)).unwrap().y, dec!(7.5));
3234    }
3235
3236    /// On the final segment the two edges of the cell are the same segment,
3237    /// so the mean of the two collapses to the linear interpolant.
3238    #[test]
3239    fn test_bilinear_interpolate_final_segment_equals_linear() {
3240        for (curve, x) in [(quadratic_four(), dec!(2.5)), (quadratic_five(), dec!(3.5))] {
3241            let bilinear = curve.bilinear_interpolate(x).unwrap();
3242            let linear = curve.linear_interpolate(x).unwrap();
3243            assert_eq!(bilinear.y, linear.y);
3244        }
3245    }
3246
3247    /// The same identity where the fraction along the segment has no finite
3248    /// decimal expansion. The two methods divide at different points of the
3249    /// chain, `bilinear` normalising `x` before scaling by the rise and
3250    /// `linear` scaling first, so each rounds at scale 28 on a different
3251    /// quantity and the two answers can part company in the last place.
3252    #[test]
3253    fn test_bilinear_interpolate_final_segment_equals_linear_to_the_last_place() {
3254        let curve = irregular();
3255        let bilinear = curve.bilinear_interpolate(dec!(4)).unwrap().y;
3256        let linear = curve.linear_interpolate(dec!(4)).unwrap().y;
3257
3258        assert_ne!(bilinear, linear);
3259        assert!((bilinear - linear).abs() < dec!(0.0000000000000000000000001));
3260    }
3261
3262    /// Reference values for the irregular curve, exact rational arithmetic:
3263    ///
3264    /// ```text
3265    /// x = 0.25  near seg 0, far seg 2  ->  1.4375
3266    /// x = 1     near seg 1, far seg 3  ->  47/24     = 1.95833...
3267    /// x = 2.5   near seg 2, far seg 3  ->  1.7
3268    /// x = 4     near seg 3, far seg 3  ->  73/28     = 2.60714...
3269    /// ```
3270    ///
3271    /// `x = 1` and `x = 4` land on a fraction with no finite decimal
3272    /// expansion, so `d_div` rounds it at scale 28 and the result carries the
3273    /// rounding forward; the bound below is far above the observed error of
3274    /// roughly `1.7e-28` and far below anything a caller could care about.
3275    #[test]
3276    fn test_bilinear_interpolate_irregular_curve_matches_reference() {
3277        const TOLERANCE: Decimal = dec!(0.0000000000000000000000001);
3278        let curve = irregular();
3279
3280        assert_eq!(
3281            curve.bilinear_interpolate(dec!(0.25)).unwrap().y,
3282            dec!(1.4375)
3283        );
3284        assert_eq!(curve.bilinear_interpolate(dec!(2.5)).unwrap().y, dec!(1.7));
3285
3286        let recurring = curve.bilinear_interpolate(dec!(1)).unwrap().y;
3287        assert!((recurring - dec!(1.9583333333333333333333333333)).abs() < TOLERANCE);
3288
3289        let last = curve.bilinear_interpolate(dec!(4)).unwrap().y;
3290        assert!((last - dec!(2.6071428571428571428571428571)).abs() < TOLERANCE);
3291    }
3292
3293    /// An abscissa carrying a sample returns that sample, on both ends of the
3294    /// curve and in the middle.
3295    #[test]
3296    fn test_bilinear_interpolate_sample_abscissa_returns_the_sample() {
3297        let curve = quadratic_four();
3298        for (x, y) in [
3299            (dec!(0), dec!(0)),
3300            (dec!(1), dec!(1)),
3301            (dec!(2), dec!(4)),
3302            (dec!(3), dec!(9)),
3303        ] {
3304            let point = curve.bilinear_interpolate(x).unwrap();
3305            assert_eq!(point.x, x);
3306            assert_eq!(point.y, y);
3307        }
3308    }
3309
3310    /// Several ordinates at the queried abscissa leave the curve with no
3311    /// value there, and the exact-match branch now reports that instead of
3312    /// returning the lowest of them, which is what the three sibling
3313    /// algorithms do.
3314    #[test]
3315    fn test_bilinear_interpolate_stacked_abscissa_is_degenerate() {
3316        let curve = Curve::new(BTreeSet::from_iter(vec![
3317            Point2D::new(dec!(0), dec!(0)),
3318            Point2D::new(dec!(1), dec!(1)),
3319            Point2D::new(dec!(1), dec!(7)),
3320            Point2D::new(dec!(2), dec!(4)),
3321            Point2D::new(dec!(3), dec!(9)),
3322        ]));
3323
3324        let err = curve.bilinear_interpolate(dec!(1)).unwrap_err();
3325        assert!(matches!(err, InterpolationError::DegenerateInterval));
3326    }
3327
3328    /// The jump at a sample predates the boundary rule. At `x = 1` both
3329    /// segments meeting there take their far edge unclamped, so all three
3330    /// values below are the ones the pre-#451 code produced: the two
3331    /// one-sided limits agree with each other on `(4 + 16) / 2` and neither
3332    /// agrees with the sample.
3333    #[test]
3334    fn test_bilinear_interpolate_jump_at_a_sample_predates_the_clamp() {
3335        const EPSILON: Decimal = dec!(0.0000000001);
3336        const SLACK: Decimal = dec!(0.00000001);
3337        let curve = quadratic_five();
3338
3339        let from_left = curve.bilinear_interpolate(dec!(1) - EPSILON).unwrap().y;
3340        assert!((from_left - dec!(5)).abs() < SLACK);
3341
3342        let from_right = curve.bilinear_interpolate(dec!(1) + EPSILON).unwrap().y;
3343        assert!((from_right - dec!(5)).abs() < SLACK);
3344
3345        assert_eq!(curve.bilinear_interpolate(dec!(1)).unwrap().y, dec!(1));
3346    }
3347
3348    /// The clamp gives the segments on either side of `x = 2` different far
3349    /// edges, so the one-sided limits there disagree, and neither matches the
3350    /// sample. Approaching from the left the far edge is the segment
3351    /// `(3,9)-(4,16)` and the limit is the mean of `4` and `16`; from the
3352    /// right it is clamped back to that same segment read at fraction zero,
3353    /// and the limit is the mean of `4` and `9`. The sample itself is `4`.
3354    /// The doc comment on the impl calls this jump out; the test pins it.
3355    #[test]
3356    fn test_bilinear_interpolate_jumps_where_the_clamp_starts() {
3357        const EPSILON: Decimal = dec!(0.0000000001);
3358        const SLACK: Decimal = dec!(0.00000001);
3359        let curve = quadratic_five();
3360
3361        let from_left = curve.bilinear_interpolate(dec!(2) - EPSILON).unwrap().y;
3362        assert!((from_left - dec!(10)).abs() < SLACK);
3363
3364        let from_right = curve.bilinear_interpolate(dec!(2) + EPSILON).unwrap().y;
3365        assert!((from_right - dec!(6.5)).abs() < SLACK);
3366
3367        assert_eq!(curve.bilinear_interpolate(dec!(2)).unwrap().y, dec!(4));
3368    }
3369
3370    /// The near edge is never clamped, so the fraction along it stays in
3371    /// `[0, 1]` and the answer is a convex combination of four sample
3372    /// ordinates: it cannot leave the range the curve's ordinates span. This
3373    /// is what a clamp of the whole window to the last four samples would
3374    /// give up.
3375    #[test]
3376    fn test_bilinear_interpolate_stays_within_the_ordinate_range() {
3377        for curve in [quadratic_four(), quadratic_five(), irregular()] {
3378            let ordinates: Vec<Decimal> = curve.points.iter().map(|p| p.y).collect();
3379            let low = ordinates.iter().copied().fold(Decimal::MAX, Decimal::min);
3380            let high = ordinates.iter().copied().fold(Decimal::MIN, Decimal::max);
3381
3382            let first = curve.points.iter().next().unwrap().x;
3383            let last = curve.points.iter().next_back().unwrap().x;
3384            let step = (last - first) / dec!(64);
3385
3386            let mut x = first;
3387            while x <= last {
3388                let y = curve.bilinear_interpolate(x).unwrap().y;
3389                assert!(y >= low, "{y} below the sample range at x = {x}");
3390                assert!(y <= high, "{y} above the sample range at x = {x}");
3391                x += step;
3392            }
3393        }
3394    }
3395}
3396
3397#[cfg(test)]
3398mod tests_cubic_interpolate {
3399    use super::*;
3400    use crate::geometrics::InterpolationType;
3401    use rust_decimal_macros::dec;
3402    use tracing::info;
3403
3404    #[test]
3405    fn test_cubic_interpolation_exact_points() {
3406        let curve = Curve::new(BTreeSet::from_iter(vec![
3407            Point2D::new(dec!(0.0), dec!(0.0)),
3408            Point2D::new(dec!(1.0), dec!(1.0)),
3409            Point2D::new(dec!(2.0), dec!(4.0)),
3410            Point2D::new(dec!(3.0), dec!(9.0)),
3411        ]));
3412
3413        // Test exact points
3414        let p1 = curve
3415            .interpolate(dec!(1.0), InterpolationType::Cubic)
3416            .unwrap();
3417        assert_eq!(p1.x, dec!(1.0));
3418        assert_eq!(p1.y, dec!(1.0));
3419    }
3420
3421    #[test]
3422    fn test_cubic_interpolation_midpoints() {
3423        let curve = Curve::new(BTreeSet::from_iter(vec![
3424            Point2D::new(dec!(0.0), dec!(0.0)),
3425            Point2D::new(dec!(1.0), dec!(1.0)),
3426            Point2D::new(dec!(2.0), dec!(4.0)),
3427            Point2D::new(dec!(3.0), dec!(9.0)),
3428        ]));
3429
3430        // Test midpoint interpolation
3431        let mid = curve
3432            .interpolate(dec!(1.5), InterpolationType::Cubic)
3433            .unwrap();
3434        assert_eq!(mid.x, dec!(1.5));
3435        // y value should be continuous and smooth
3436        assert!(mid.y > dec!(1.0) && mid.y < dec!(4.0));
3437    }
3438
3439    #[test]
3440    fn test_cubic_interpolation_insufficient_points() {
3441        let curve = Curve::new(BTreeSet::from_iter(vec![
3442            Point2D::new(dec!(0.0), dec!(0.0)),
3443            Point2D::new(dec!(1.0), dec!(1.0)),
3444            Point2D::new(dec!(2.0), dec!(4.0)),
3445        ]));
3446
3447        assert!(
3448            curve
3449                .interpolate(dec!(1.5), InterpolationType::Cubic)
3450                .is_err()
3451        );
3452    }
3453
3454    #[test]
3455    fn test_cubic_interpolation_out_of_range() {
3456        let curve = Curve::new(BTreeSet::from_iter(vec![
3457            Point2D::new(dec!(0.0), dec!(0.0)),
3458            Point2D::new(dec!(1.0), dec!(1.0)),
3459            Point2D::new(dec!(2.0), dec!(4.0)),
3460            Point2D::new(dec!(3.0), dec!(9.0)),
3461        ]));
3462
3463        assert!(
3464            curve
3465                .interpolate(dec!(-0.5), InterpolationType::Cubic)
3466                .is_err()
3467        );
3468        assert!(
3469            curve
3470                .interpolate(dec!(3.5), InterpolationType::Cubic)
3471                .is_err()
3472        );
3473    }
3474
3475    #[test]
3476    fn test_cubic_interpolation_monotonicity() {
3477        let curve = Curve::new(BTreeSet::from_iter(vec![
3478            Point2D::new(dec!(0.0), dec!(0.0)),
3479            Point2D::new(dec!(1.0), dec!(1.0)),
3480            Point2D::new(dec!(2.0), dec!(4.0)),
3481            Point2D::new(dec!(3.0), dec!(9.0)),
3482        ]));
3483
3484        let p1 = curve
3485            .interpolate(dec!(0.5), InterpolationType::Cubic)
3486            .unwrap();
3487        let p2 = curve
3488            .interpolate(dec!(1.5), InterpolationType::Cubic)
3489            .unwrap();
3490        let p3 = curve
3491            .interpolate(dec!(2.5), InterpolationType::Cubic)
3492            .unwrap();
3493
3494        assert!(p1.y < p2.y);
3495        assert!(p2.y < p3.y);
3496        info!("p1: {:?}, p2: {:?}, p3: {:?}", p1, p2, p3);
3497    }
3498}
3499
3500#[cfg(test)]
3501mod tests_spline_interpolate {
3502    use super::*;
3503    use crate::geometrics::InterpolationType;
3504    use rust_decimal_macros::dec;
3505
3506    #[test]
3507    fn test_spline_interpolation_exact_points() {
3508        let curve = Curve::new(BTreeSet::from_iter(vec![
3509            Point2D::new(dec!(0.0), dec!(0.0)),
3510            Point2D::new(dec!(1.0), dec!(1.0)),
3511            Point2D::new(dec!(2.0), dec!(4.0)),
3512            Point2D::new(dec!(3.0), dec!(9.0)),
3513        ]));
3514
3515        let p1 = curve
3516            .interpolate(dec!(1.0), InterpolationType::Spline)
3517            .unwrap();
3518        assert_eq!(p1.x, dec!(1.0));
3519        assert_eq!(p1.y, dec!(1.0));
3520    }
3521
3522    #[test]
3523    fn test_spline_interpolation_midpoints() {
3524        let curve = Curve::new(BTreeSet::from_iter(vec![
3525            Point2D::new(dec!(0.0), dec!(0.0)),
3526            Point2D::new(dec!(1.0), dec!(1.0)),
3527            Point2D::new(dec!(2.0), dec!(4.0)),
3528            Point2D::new(dec!(3.0), dec!(9.0)),
3529        ]));
3530
3531        let mid = curve
3532            .interpolate(dec!(1.5), InterpolationType::Spline)
3533            .unwrap();
3534        assert_eq!(mid.x, dec!(1.5));
3535        // Value should be continuous and between the points
3536        assert!(mid.y > dec!(1.0) && mid.y < dec!(4.0));
3537    }
3538
3539    #[test]
3540    fn test_spline_interpolation_insufficient_points() {
3541        let curve = Curve::new(BTreeSet::from_iter(vec![
3542            Point2D::new(dec!(0.0), dec!(0.0)),
3543            Point2D::new(dec!(1.0), dec!(1.0)),
3544        ]));
3545
3546        assert!(
3547            curve
3548                .interpolate(dec!(0.5), InterpolationType::Spline)
3549                .is_err()
3550        );
3551    }
3552
3553    #[test]
3554    fn test_spline_interpolation_out_of_range() {
3555        let curve = Curve::new(BTreeSet::from_iter(vec![
3556            Point2D::new(dec!(0.0), dec!(0.0)),
3557            Point2D::new(dec!(1.0), dec!(1.0)),
3558            Point2D::new(dec!(2.0), dec!(4.0)),
3559        ]));
3560
3561        assert!(
3562            curve
3563                .interpolate(dec!(-0.5), InterpolationType::Spline)
3564                .is_err()
3565        );
3566        assert!(
3567            curve
3568                .interpolate(dec!(2.5), InterpolationType::Spline)
3569                .is_err()
3570        );
3571    }
3572
3573    #[test]
3574    fn test_spline_interpolation_smoothness() {
3575        let curve = Curve::new(BTreeSet::from_iter(vec![
3576            Point2D::new(dec!(0.0), dec!(0.0)),
3577            Point2D::new(dec!(1.0), dec!(1.0)),
3578            Point2D::new(dec!(2.0), dec!(4.0)),
3579            Point2D::new(dec!(3.0), dec!(9.0)),
3580        ]));
3581
3582        // Test points close together to verify smoothness
3583        let p1 = curve
3584            .interpolate(dec!(1.48), InterpolationType::Spline)
3585            .unwrap();
3586        let p2 = curve
3587            .interpolate(dec!(1.49), InterpolationType::Spline)
3588            .unwrap();
3589        let p3 = curve
3590            .interpolate(dec!(1.50), InterpolationType::Spline)
3591            .unwrap();
3592        let p4 = curve
3593            .interpolate(dec!(1.51), InterpolationType::Spline)
3594            .unwrap();
3595        let p5 = curve
3596            .interpolate(dec!(1.52), InterpolationType::Spline)
3597            .unwrap();
3598
3599        // Verify monotonicity and smooth transitions
3600        assert!(p1.y < p2.y);
3601        assert!(p2.y < p3.y);
3602        assert!(p3.y < p4.y);
3603        assert!(p4.y < p5.y);
3604
3605        // Verify that the changes are smooth (second differences are small)
3606        let d1 = p2.y - p1.y;
3607        let d2 = p3.y - p2.y;
3608        let d3 = p4.y - p3.y;
3609        let d4 = p5.y - p4.y;
3610
3611        // Second differences should be small
3612        assert!((d2 - d1).abs() < dec!(0.001));
3613        assert!((d3 - d2).abs() < dec!(0.001));
3614        assert!((d4 - d3).abs() < dec!(0.001));
3615    }
3616}
3617
3618#[cfg(test)]
3619mod tests_curve_arithmetic {
3620    use super::*;
3621    use crate::curves::utils::{create_constant_curve, create_linear_curve};
3622    use crate::geometrics::InterpolationType;
3623
3624    #[test]
3625    fn test_merge_curves_add() {
3626        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(1.0)).unwrap();
3627        let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
3628
3629        let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Add).unwrap();
3630
3631        // Check result at some sample points
3632        let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
3633        for x in &test_points {
3634            let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y
3635                + curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
3636
3637            let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
3638            assert!(
3639                (result_point.y - expected_y).abs() < dec!(0.001),
3640                "Failed at x = {}, expected {}, got {}",
3641                x,
3642                expected_y,
3643                result_point.y
3644            );
3645        }
3646    }
3647
3648    #[test]
3649    fn test_merge_curves_subtract() {
3650        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
3651        let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(1.0)).unwrap();
3652        let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Subtract).unwrap();
3653        // Check result at some sample points
3654        let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
3655        for x in &test_points {
3656            let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y
3657                - curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
3658
3659            let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
3660            assert!(
3661                (result_point.y - expected_y).abs() < dec!(0.001),
3662                "Failed at x = {}, expected {}, got {}",
3663                x,
3664                expected_y,
3665                result_point.y
3666            );
3667        }
3668    }
3669
3670    #[test]
3671    fn test_merge_curves_multiply() {
3672        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
3673        let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
3674
3675        let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Multiply).unwrap();
3676
3677        // Check result at some sample points
3678        let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
3679        for x in &test_points {
3680            let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y
3681                * curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
3682
3683            let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
3684            assert!(
3685                (result_point.y - expected_y).abs() < dec!(0.001),
3686                "Failed at x = {}, expected {}, got {}",
3687                x,
3688                expected_y,
3689                result_point.y
3690            );
3691        }
3692    }
3693
3694    #[test]
3695    fn test_merge_curves_divide() {
3696        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(6.0)).unwrap();
3697        let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
3698        let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Divide).unwrap();
3699
3700        // Check result at some sample points
3701        let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
3702        for x in &test_points {
3703            let y2 = curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
3704
3705            // Skip points where interpolation results in zero
3706            if y2 == Decimal::ZERO {
3707                continue;
3708            }
3709
3710            let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y / y2;
3711
3712            let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
3713            assert!(
3714                (result_point.y - expected_y).abs() < dec!(0.001),
3715                "Failed at x = {}, expected {}, got {}",
3716                x,
3717                expected_y,
3718                result_point.y
3719            );
3720        }
3721    }
3722
3723    /// Division is not associative, so every divisor has to reach the result.
3724    /// `Curve::merge` reaches them by turning each element after the first
3725    /// into its reciprocal and folding the whole set with a combining
3726    /// reducer, so no partial is discarded. Three curves at 8, 2 and 2 must
3727    /// give `8 / 2 / 2 = 2`, never `8 / 2` and never 8.
3728    #[test]
3729    fn test_merge_curves_divide_folds_every_divisor() {
3730        let eight = create_constant_curve(dec!(0.0), dec!(10.0), dec!(8.0)).unwrap();
3731        let two_a = create_constant_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
3732        let two_b = create_constant_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
3733
3734        let result = Curve::merge(&[&eight, &two_a, &two_b], MergeOperation::Divide).unwrap();
3735
3736        for x in [dec!(0.0), dec!(5.0), dec!(10.0)] {
3737            let y = result.interpolate(x, InterpolationType::Cubic).unwrap().y;
3738            assert!(
3739                (y - dec!(2)).abs() < dec!(0.0001),
3740                "expected 8 / 2 / 2 = 2 at x = {x}, got {y}"
3741            );
3742        }
3743    }
3744
3745    /// Number of times a single-pool determinism test repeats the same merge.
3746    ///
3747    /// This is the weaker of the two guards here, and the count says so.
3748    /// Repeating on one pool only reaches the work-stealing component of the
3749    /// old defect: measured against the reducer this replaces, a forty-operand
3750    /// merge returned 52 distinct results in 2000 runs on a two-thread pool,
3751    /// 809 in 2000 on four threads and 1996 in 2000 on eight, but exactly one
3752    /// result in 2000 on one thread and one in 2000 on sixteen. Taking the
3753    /// weakest rate that did flap, about 2.6% per run, 512 repeats leave
3754    /// roughly one chance in a million of missing it.
3755    ///
3756    /// At the three operands the issue names, the old reducer was stable on
3757    /// every pool size probed: 5000 runs, one result. Repetition alone would
3758    /// not have caught it there at any count, which is why
3759    /// [`test_merge_curves_multiply_is_reproducible`] also pins the exact
3760    /// product, and why the property that actually broke is pinned by
3761    /// [`test_merge_curves_multiply_is_reproducible_across_pool_sizes`].
3762    const DETERMINISM_REPEATS: usize = 512;
3763
3764    /// The ordinates of a curve as `(mantissa, scale)` pairs.
3765    ///
3766    /// `Decimal` compares equal across scales, so `dec!(2.0) == dec!(2.00)`
3767    /// and a plain equality check would not notice a fold that moved the
3768    /// scale. The mantissa and the scale together are the stored value.
3769    fn ordinate_fingerprint(curve: &Curve) -> Vec<(i128, u32)> {
3770        curve
3771            .points
3772            .iter()
3773            .map(|point| (point.y.mantissa(), point.y.scale()))
3774            .collect()
3775    }
3776
3777    /// Rayon pool sizes a cross-pool determinism test merges on.
3778    ///
3779    /// Rayon sets the splitting threshold of a parallel reduction to the
3780    /// length of the input divided by eight times the number of threads in
3781    /// the pool, so the pool size is one of the inputs to the grouping the
3782    /// old reducer chose. One, two and four are the sizes at which the
3783    /// threshold lands above one for a forty-operand merge, and eight and
3784    /// sixteen bracket the count of a typical machine.
3785    const POOL_SIZES: [usize; 5] = [1, 2, 4, 8, 16];
3786
3787    /// Merges per pool size in a cross-pool determinism test.
3788    ///
3789    /// The pool size is the variable that moved the result deterministically;
3790    /// the repeats on top of it catch the work-stealing component, which on
3791    /// eight threads made 1996 of 2000 runs of the old reducer distinct from
3792    /// one another. Four is enough at that rate for the odds of missing it to
3793    /// be negligible, and keeps the whole test inside a second.
3794    const RUNS_PER_POOL: usize = 4;
3795
3796    /// Number of operands a cross-pool determinism test merges.
3797    ///
3798    /// Forty rather than three. At three operands the splitting threshold is
3799    /// one whatever the pool size, so rayon built the same balanced tree
3800    /// everywhere and the old reducer looked stable; the divergence needs an
3801    /// input long enough for the threshold to exceed one on a small pool.
3802    const CROSS_POOL_OPERANDS: u32 = 40;
3803
3804    /// Builds `CROSS_POOL_OPERANDS` constant curves just under one.
3805    ///
3806    /// Each ordinate is `1 - 1/n` for a distinct `n`, which fills the 28-place
3807    /// scale so the running product rounds at every step, and stays near one
3808    /// so forty of them neither overflow nor collapse toward zero.
3809    fn cross_pool_operands() -> Vec<Curve> {
3810        (0..CROSS_POOL_OPERANDS)
3811            .map(|i| {
3812                let value = Decimal::ONE - Decimal::ONE / Decimal::from(1000 + i * 7);
3813                create_constant_curve(dec!(0), dec!(10), value)
3814                    .expect("constant curve fixture must build")
3815            })
3816            .collect()
3817    }
3818
3819    /// Asserts that merging `curves` under `operation` gives one set of digits
3820    /// across every pool size in [`POOL_SIZES`].
3821    fn assert_reproducible_across_pools(curves: &[&Curve], operation: MergeOperation) {
3822        let mut reference: Option<Vec<(i128, u32)>> = None;
3823
3824        for threads in POOL_SIZES {
3825            let pool = rayon::ThreadPoolBuilder::new()
3826                .num_threads(threads)
3827                .build()
3828                .expect("rayon must build a pool of a fixed size");
3829
3830            for run in 0..RUNS_PER_POOL {
3831                let merged = pool
3832                    .install(|| Curve::merge(curves, operation))
3833                    .expect("the fixture merge must succeed");
3834                let fingerprint = ordinate_fingerprint(&merged);
3835
3836                match &reference {
3837                    None => reference = Some(fingerprint),
3838                    Some(reference) => assert_eq!(
3839                        &fingerprint, reference,
3840                        "run {run} on a {threads}-thread pool produced different digits \
3841                         from the first merge, under {operation:?}"
3842                    ),
3843                }
3844            }
3845        }
3846    }
3847
3848    /// The same operands in the same order give the same digits whatever the
3849    /// size of the rayon pool the merge runs on.
3850    ///
3851    /// This is the defect of #453 at its root, and the property repetition on
3852    /// one pool does not reach. Rayon takes the grouping of a parallel
3853    /// reduction from a threshold keyed to the pool size, so the old reducer
3854    /// bracketed a forty-curve product one way on one thread and another way
3855    /// on sixteen: 2000 runs on each, one result each, and the two results
3856    /// disagreed. Two services on differently sized machines would have
3857    /// disagreed on one input while each looked perfectly stable where it
3858    /// ran, which is harder to notice than run-to-run noise.
3859    ///
3860    /// The pool is built explicitly rather than read from the environment
3861    /// because `RAYON_NUM_THREADS` is consulted once, when the global pool is
3862    /// first used, and a test cannot rely on being the first to touch it.
3863    /// `rayon` is already a direct dependency, so this needs nothing new.
3864    #[test]
3865    fn test_merge_curves_multiply_is_reproducible_across_pool_sizes() {
3866        let curves = cross_pool_operands();
3867        let refs: Vec<&Curve> = curves.iter().collect();
3868        assert_reproducible_across_pools(&refs, MergeOperation::Multiply);
3869    }
3870
3871    /// `Divide` folds the reciprocals of its divisors into the leftmost value
3872    /// with the same multiplication, so it took its bracketing from the same
3873    /// splitter and moves under the same fix.
3874    ///
3875    /// `Surface`'s `Divide` was made sequential in #452; `Curve`'s was not,
3876    /// and was still a parallel product of reciprocals until #453.
3877    #[test]
3878    fn test_merge_curves_divide_is_reproducible_across_pool_sizes() {
3879        let curves = cross_pool_operands();
3880        let refs: Vec<&Curve> = curves.iter().collect();
3881        assert_reproducible_across_pools(&refs, MergeOperation::Divide);
3882    }
3883
3884    /// Merging three curves under `Multiply` gives the same digits on every
3885    /// run, and gives the digits of a left-to-right fold.
3886    ///
3887    /// The three ordinates are picked so the triple product overruns the 28
3888    /// significant digits a `Decimal` holds and has to round twice: they
3889    /// multiply to `...768908` grouped from the left and to `...768907`
3890    /// grouped from the right, so the bracketing shows up in the result
3891    /// instead of being hidden by exact arithmetic. Most triples do not
3892    /// behave that way, which is why these are not arbitrary: over a sweep of
3893    /// 205,379 reciprocal triples only 628, 0.31%, regrouped to a different
3894    /// last digit.
3895    ///
3896    /// The exact product is what fails the old reducer here: it returned the
3897    /// right-grouped value on every pool size probed, so at three operands
3898    /// the repetition is a guard against a future regression rather than a
3899    /// reproduction of this one. See [`DETERMINISM_REPEATS`] for the count,
3900    /// and [`test_merge_curves_multiply_is_reproducible_across_pool_sizes`]
3901    /// for the property that did break.
3902    #[test]
3903    fn test_merge_curves_multiply_is_reproducible() {
3904        let a =
3905            create_constant_curve(dec!(0), dec!(10), dec!(0.010989010989010989010989011)).unwrap();
3906        let b =
3907            create_constant_curve(dec!(0), dec!(10), dec!(0.010752688172043010752688172)).unwrap();
3908        let c =
3909            create_constant_curve(dec!(0), dec!(10), dec!(0.0094339622641509433962264151)).unwrap();
3910
3911        let expected = dec!(0.0000011147302687168785768908);
3912        let first = Curve::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
3913        for point in first.points.iter() {
3914            assert_eq!(
3915                (point.y.mantissa(), point.y.scale()),
3916                (expected.mantissa(), expected.scale()),
3917                "expected the left-to-right product {expected} at x = {}, got {}",
3918                point.x,
3919                point.y
3920            );
3921        }
3922
3923        let reference = ordinate_fingerprint(&first);
3924        for run in 1..DETERMINISM_REPEATS {
3925            let again = Curve::merge(&[&a, &b, &c], MergeOperation::Multiply).unwrap();
3926            assert_eq!(
3927                ordinate_fingerprint(&again),
3928                reference,
3929                "run {run} of {DETERMINISM_REPEATS} produced different digits from the first"
3930            );
3931        }
3932    }
3933
3934    /// `Divide` folds the reciprocals of every divisor into the leftmost
3935    /// value, so it rounds as often as `Multiply` does and needs the same
3936    /// fixed order.
3937    ///
3938    /// The divisors are chosen so their reciprocals fill the 28-place scale;
3939    /// the quotient is not pinned to a literal here because the operand
3940    /// ordering is already pinned by
3941    /// [`test_merge_curves_multiply_is_reproducible`], and reproducing the
3942    /// reciprocal rounding rule inside the assertion would only restate the
3943    /// implementation. What is asserted is the property the issue names:
3944    /// same operands, same digits, every run.
3945    #[test]
3946    fn test_merge_curves_divide_is_reproducible() {
3947        let a = create_constant_curve(dec!(0), dec!(10), dec!(97)).unwrap();
3948        let b = create_constant_curve(dec!(0), dec!(10), dec!(91)).unwrap();
3949        let c = create_constant_curve(dec!(0), dec!(10), dec!(93)).unwrap();
3950
3951        let first = Curve::merge(&[&a, &b, &c], MergeOperation::Divide).unwrap();
3952        let reference = ordinate_fingerprint(&first);
3953        for run in 1..DETERMINISM_REPEATS {
3954            let again = Curve::merge(&[&a, &b, &c], MergeOperation::Divide).unwrap();
3955            assert_eq!(
3956                ordinate_fingerprint(&again),
3957                reference,
3958                "run {run} of {DETERMINISM_REPEATS} produced different digits from the first"
3959            );
3960        }
3961    }
3962
3963    #[test]
3964    fn test_merge_curves_max() {
3965        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
3966        let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
3967
3968        let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Max).unwrap();
3969
3970        // Check result at some sample points
3971        let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
3972        for x in &test_points {
3973            let y1 = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y;
3974            let y2 = curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
3975            let expected_y = y1.max(y2);
3976
3977            let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
3978            assert!(
3979                (result_point.y - expected_y).abs() < dec!(0.001),
3980                "Failed at x = {}, expected {}, got {}",
3981                x,
3982                expected_y,
3983                result_point.y
3984            );
3985        }
3986    }
3987
3988    #[test]
3989    fn test_merge_curves_min() {
3990        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
3991        let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
3992
3993        let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Min).unwrap();
3994
3995        // Check result at some sample points
3996        let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
3997        for x in &test_points {
3998            let y1 = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y;
3999            let y2 = curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y;
4000            let expected_y = y1.min(y2);
4001
4002            let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
4003            assert!(
4004                (result_point.y - expected_y).abs() < dec!(0.001),
4005                "Failed at x = {}, expected {}, got {}",
4006                x,
4007                expected_y,
4008                result_point.y
4009            );
4010        }
4011    }
4012
4013    #[test]
4014    fn test_merge_with_single_operation() {
4015        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
4016        let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
4017
4018        let result = curve1.merge_with(&curve2, MergeOperation::Add).unwrap();
4019
4020        // Verify that merge_with is equivalent to merge_curves with two curves
4021        let merged_result = Curve::merge(&[&curve1, &curve2], MergeOperation::Add).unwrap();
4022
4023        // Compare points of both results
4024        assert_eq!(result.points.len(), merged_result.points.len());
4025
4026        for i in 0..result.points.len() {
4027            assert!((result[i].x - merged_result[i].x).abs() < dec!(0.001));
4028            assert!((result[i].y - merged_result[i].y).abs() < dec!(0.001));
4029        }
4030    }
4031
4032    /// The line `y = x` sampled at `x = (10 / 511) * i`, `i = 0..=511`. The
4033    /// last abscissa is `10.000000000000000000000000016`: the span has a
4034    /// 27-place mantissa, so `span / 100` rounds up and `step * 100` used to
4035    /// land on `10.000000000000000000000000020`, past `max_x` (#795).
4036    fn long_mantissa_curve() -> Curve {
4037        let step = dec!(10) / dec!(511);
4038        let points: Vec<Point2D> = (0..=511i64)
4039            .map(|i| {
4040                let x = step * Decimal::from(i);
4041                Point2D::new(x, x)
4042            })
4043            .collect();
4044        Curve::from_vector(points.iter().collect())
4045    }
4046
4047    #[test]
4048    fn test_merge_curves_long_mantissa_range_ends_on_max_x() {
4049        let curve = long_mantissa_curve();
4050        let max_x = curve.x_range.1;
4051        assert_eq!(max_x, dec!(10.000000000000000000000000016));
4052
4053        let result = curve.merge_with(&curve, MergeOperation::Add).unwrap();
4054
4055        // 101 grid points from 0 to max_x, none past it; the last is max_x.
4056        assert_eq!(result.points.len(), 101);
4057        assert!(result.points.iter().all(|p| p.x <= max_x));
4058        assert_eq!(result.x_range, (Decimal::ZERO, max_x));
4059        // `y = 2x` on the merged line.
4060        let last = result.points.last().unwrap();
4061        assert!((last.y - dec!(2) * max_x).abs() < dec!(0.0000000001));
4062        let mid = result
4063            .interpolate(dec!(5), InterpolationType::Linear)
4064            .unwrap();
4065        assert!((mid.y - dec!(10)).abs() < dec!(0.0000000001));
4066    }
4067
4068    #[test]
4069    fn test_merge_curves_error_handling() {
4070        // Test with empty slice
4071        let result = Curve::merge(&[], MergeOperation::Add);
4072        assert!(result.is_err());
4073
4074        // Test with curves of incompatible ranges
4075        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(1.0)).unwrap();
4076        let curve2 = create_linear_curve(dec!(5.0), dec!(15.0), dec!(2.0)).unwrap();
4077
4078        // Verify that the merge operation works even with partially overlapping ranges
4079        let result = Curve::merge(&[&curve1, &curve2], MergeOperation::Add);
4080        assert!(result.is_ok());
4081    }
4082
4083    #[test]
4084    fn test_merge_multiple_curves() {
4085        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(1.0)).unwrap();
4086        let curve2 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(2.0)).unwrap();
4087        let curve3 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(3.0)).unwrap();
4088
4089        let result = Curve::merge(&[&curve1, &curve2, &curve3], MergeOperation::Add).unwrap();
4090
4091        // Check result at some sample points
4092        let test_points = [dec!(0.0), dec!(5.0), dec!(10.0)];
4093        for x in &test_points {
4094            let expected_y = curve1.interpolate(*x, InterpolationType::Cubic).unwrap().y
4095                + curve2.interpolate(*x, InterpolationType::Cubic).unwrap().y
4096                + curve3.interpolate(*x, InterpolationType::Cubic).unwrap().y;
4097
4098            let result_point = result.interpolate(*x, InterpolationType::Cubic).unwrap();
4099            assert!(
4100                (result_point.y - expected_y).abs() < dec!(0.001),
4101                "Failed at x = {}, expected {}, got {}",
4102                x,
4103                expected_y,
4104                result_point.y
4105            );
4106        }
4107    }
4108}
4109
4110#[cfg(test)]
4111mod tests_extended {
4112    use super::*;
4113    use crate::error::CurveError::OperationError;
4114    use crate::geometrics::{ConstructionMethod, ConstructionParams};
4115    use optionstratlib_core::error::OperationErrorKind;
4116
4117    /// A generator failure owned by the caller, standing in for an error of
4118    /// a higher layer (a `ChainError`, say) that math cannot name.
4119    #[derive(Debug, thiserror::Error)]
4120    #[error("{0}")]
4121    struct GeneratorFailure(&'static str);
4122
4123    #[test]
4124    fn test_construct_from_data_empty() {
4125        let result = Curve::construct(ConstructionMethod::FromData {
4126            points: BTreeSet::new(),
4127        });
4128        assert!(result.is_err());
4129        let error = result.unwrap_err();
4130        match error {
4131            CurveError::Point2DError { reason } => {
4132                assert_eq!(reason, "Empty points array");
4133            }
4134            _ => {
4135                panic!("Unexpected error type");
4136            }
4137        }
4138    }
4139
4140    #[test]
4141    fn test_construct_parametric_valid() {
4142        let f = |t: Decimal| Ok(Point2D::new(t, t * dec!(2.0)));
4143        let params = ConstructionParams::D2 {
4144            t_start: Decimal::ZERO,
4145            t_end: dec!(10.0),
4146            steps: 10,
4147        };
4148        let result = Curve::construct(ConstructionMethod::Parametric {
4149            f: Box::new(f),
4150            params,
4151        });
4152        assert!(result.is_ok());
4153    }
4154
4155    #[test]
4156    fn test_construct_parametric_invalid_function() {
4157        let f = |_t: Decimal| -> Result<Point2D, CurveError> {
4158            Err(CurveError::generator(GeneratorFailure(
4159                "Function evaluation failed",
4160            )))
4161        };
4162        let params = ConstructionParams::D2 {
4163            t_start: Decimal::ZERO,
4164            t_end: dec!(10.0),
4165            steps: 10,
4166        };
4167        let result = Curve::construct(ConstructionMethod::Parametric {
4168            f: Box::new(f),
4169            params,
4170        });
4171        assert!(result.is_err());
4172        let error = result.unwrap_err();
4173        // The generator's own error survives the construction boundary: it is
4174        // still the caller's typed error, not a message (roadmap M1-10).
4175        match error {
4176            CurveError::Generator(source) => {
4177                let cause = source
4178                    .downcast_ref::<GeneratorFailure>()
4179                    .expect("the generator's own error must survive boxing");
4180                assert!(cause.to_string().contains("Function evaluation failed"));
4181            }
4182            _ => {
4183                panic!("Unexpected error type");
4184            }
4185        }
4186    }
4187
4188    #[test]
4189    fn test_segment_not_found_error() {
4190        let segment: Option<Point2D> = None;
4191        let result: Result<Point2D, CurveError> = segment.ok_or_else(|| {
4192            CurveError::ConstructionError(
4193                "Could not find valid segment for interpolation".to_string(),
4194            )
4195        });
4196        assert!(result.is_err());
4197        let error = result.unwrap_err();
4198        match error {
4199            CurveError::ConstructionError(reason) => {
4200                assert_eq!(reason, "Could not find valid segment for interpolation");
4201            }
4202            _ => {
4203                panic!("Unexpected error type");
4204            }
4205        }
4206    }
4207
4208    #[test]
4209    fn test_compute_basic_metrics_placeholder() {
4210        let curve = Curve {
4211            points: BTreeSet::new(),
4212            x_range: (Default::default(), Default::default()),
4213        };
4214        let metrics = curve.compute_basic_metrics();
4215        assert!(metrics.is_ok());
4216        let metrics = metrics.unwrap();
4217        assert_eq!(metrics.mean, Decimal::ZERO);
4218    }
4219
4220    #[test]
4221    fn test_single_curve_return() {
4222        let curve = Curve {
4223            points: BTreeSet::new(),
4224            x_range: (Default::default(), Default::default()),
4225        };
4226        let result = if vec![curve.clone()].len() == 1 {
4227            Ok(curve.clone())
4228        } else {
4229            Err(CurveError::invalid_parameters(
4230                "merge_curves",
4231                "Invalid state",
4232            ))
4233        };
4234        assert!(result.is_ok());
4235    }
4236
4237    #[test]
4238    fn test_merge_curves_invalid_x_range() {
4239        let min_x = dec!(10.0);
4240        let max_x = dec!(5.0);
4241        let result = if min_x >= max_x {
4242            Err(CurveError::invalid_parameters(
4243                "merge_curves",
4244                "Curves have incompatible x-ranges",
4245            ))
4246        } else {
4247            Ok(())
4248        };
4249        assert!(result.is_err());
4250        let error = result.unwrap_err();
4251        match error {
4252            OperationError(OperationErrorKind::InvalidParameters { operation, reason }) => {
4253                assert_eq!(operation, "merge_curves");
4254                assert_eq!(reason, "Curves have incompatible x-ranges");
4255            }
4256            _ => {
4257                panic!("Unexpected error type");
4258            }
4259        }
4260    }
4261}
4262
4263#[cfg(test)]
4264mod tests_curve_metrics {
4265    use super::*;
4266    use optionstratlib_core::assert_decimal_eq;
4267    use rust_decimal_macros::dec;
4268    use std::collections::BTreeSet;
4269
4270    // Helper function to create test curves
4271    fn create_linear_curve() -> Curve {
4272        let points = BTreeSet::from_iter(vec![
4273            Point2D::new(dec!(0.0), dec!(0.0)),
4274            Point2D::new(dec!(1.0), dec!(2.0)),
4275            Point2D::new(dec!(2.0), dec!(4.0)),
4276            Point2D::new(dec!(3.0), dec!(6.0)),
4277            Point2D::new(dec!(4.0), dec!(8.0)),
4278        ]);
4279        Curve::new(points)
4280    }
4281
4282    fn create_non_linear_curve() -> Curve {
4283        Curve {
4284            points: (0..=20)
4285                .map(|x| Point2D {
4286                    x: Decimal::from(x),
4287                    y: Decimal::from(x * x % 7), // Ejemplo no lineal
4288                })
4289                .collect(),
4290            x_range: (Default::default(), Default::default()),
4291        }
4292    }
4293
4294    fn create_constant_curve() -> Curve {
4295        let points = BTreeSet::from_iter(vec![
4296            Point2D::new(dec!(0.0), dec!(5.0)),
4297            Point2D::new(dec!(1.0), dec!(5.0)),
4298            Point2D::new(dec!(2.0), dec!(5.0)),
4299        ]);
4300        Curve::new(points)
4301    }
4302
4303    #[test]
4304    fn test_basic_metrics() {
4305        // Linear curve
4306        let linear_curve = create_linear_curve();
4307        let basic_metrics = linear_curve.compute_basic_metrics().unwrap();
4308
4309        // Expected values for linear curve
4310        assert_decimal_eq!(basic_metrics.mean, dec!(4.0), dec!(0.001));
4311        assert_decimal_eq!(basic_metrics.median, dec!(4.0), dec!(0.001));
4312        assert_decimal_eq!(basic_metrics.std_dev, dec!(2.82842712), dec!(0.001));
4313
4314        // Constant curve
4315        let constant_curve = create_constant_curve();
4316        let constant_metrics = constant_curve.compute_basic_metrics().unwrap();
4317
4318        assert_decimal_eq!(constant_metrics.mean, dec!(5.0), dec!(0.001));
4319        assert_decimal_eq!(constant_metrics.median, dec!(5.0), dec!(0.001));
4320        assert_decimal_eq!(constant_metrics.std_dev, dec!(0.0), dec!(0.001));
4321    }
4322
4323    #[test]
4324    fn test_shape_metrics() {
4325        // Linear curve
4326        let linear_curve = create_linear_curve();
4327        let shape_metrics = linear_curve.compute_shape_metrics().unwrap();
4328
4329        // More lenient check for linear curve
4330        assert!(
4331            shape_metrics.skewness.abs() < dec!(0.5),
4332            "Skewness for linear curve should be very close to 0, got {}",
4333            shape_metrics.skewness
4334        );
4335
4336        // Allow a wider range for kurtosis of a linear curve
4337        assert!(
4338            shape_metrics.kurtosis.abs() < dec!(2.0),
4339            "Kurtosis for linear curve should be close to 0, got {}",
4340            shape_metrics.kurtosis
4341        );
4342
4343        // Non-linear curve
4344        let non_linear_curve = create_non_linear_curve();
4345        let non_linear_metrics = non_linear_curve.compute_shape_metrics().unwrap();
4346
4347        // More nuanced checks for non-linear curve
4348        assert!(
4349            non_linear_metrics.skewness.abs() > dec!(0.003),
4350            "Non-linear curve should have significant skewness, got {}",
4351            non_linear_metrics.skewness
4352        );
4353
4354        // Ensure the non-linear curve has a meaningfully different kurtosis
4355        assert!(
4356            non_linear_metrics.kurtosis.abs() > dec!(1.0),
4357            "Non-linear curve should have significant kurtosis, got {}",
4358            non_linear_metrics.kurtosis
4359        );
4360
4361        // Check peaks and valleys
4362        assert!(
4363            !non_linear_metrics.peaks.is_empty(),
4364            "Peaks should be detected"
4365        );
4366        assert!(
4367            !non_linear_metrics.valleys.is_empty(),
4368            "Valleys should be detected"
4369        );
4370    }
4371
4372    #[test]
4373    fn test_range_metrics() {
4374        // Linear curve
4375        let linear_curve = create_linear_curve();
4376        let range_metrics = linear_curve.compute_range_metrics().unwrap();
4377
4378        assert_decimal_eq!(range_metrics.min.y, dec!(0.0), dec!(0.001));
4379        assert_decimal_eq!(range_metrics.max.y, dec!(8.0), dec!(0.001));
4380        assert_decimal_eq!(range_metrics.range, dec!(8.0), dec!(0.001));
4381
4382        // Constant curve
4383        let constant_curve = create_constant_curve();
4384        let constant_range_metrics = constant_curve.compute_range_metrics().unwrap();
4385
4386        assert_decimal_eq!(constant_range_metrics.min.y, dec!(5.0), dec!(0.001));
4387        assert_decimal_eq!(constant_range_metrics.max.y, dec!(5.0), dec!(0.001));
4388        assert_decimal_eq!(constant_range_metrics.range, dec!(0.0), dec!(0.001));
4389    }
4390
4391    #[test]
4392    fn test_trend_metrics() {
4393        // Linear curve
4394        let linear_curve = create_linear_curve();
4395        let trend_metrics = linear_curve.compute_trend_metrics().unwrap();
4396
4397        // Expected values for a perfectly linear curve
4398        assert_decimal_eq!(trend_metrics.slope, dec!(2.0), dec!(0.001));
4399        assert_decimal_eq!(trend_metrics.intercept, dec!(0.0), dec!(0.001));
4400        assert_decimal_eq!(trend_metrics.r_squared, dec!(1.0), dec!(0.001));
4401
4402        // Non-linear curve
4403        let non_linear_curve = create_non_linear_curve();
4404        let non_linear_trend_metrics = non_linear_curve.compute_trend_metrics().unwrap();
4405
4406        // R-squared should be less than 1
4407        assert!(non_linear_trend_metrics.r_squared < dec!(1.0));
4408
4409        // Moving average should exist
4410        assert!(!non_linear_trend_metrics.moving_average.is_empty());
4411    }
4412
4413    #[test]
4414    fn test_constant_curve_risk_metrics() {
4415        let constant_curve = create_constant_curve();
4416        let risk_metrics = constant_curve.compute_risk_metrics().unwrap();
4417
4418        assert_eq!(risk_metrics.volatility, dec!(0.0));
4419        assert_eq!(risk_metrics.coefficient_of_variation, dec!(0.0));
4420        assert_eq!(risk_metrics.sharpe_ratio, dec!(0.0));
4421    }
4422
4423    /// A flat curve has no uncertainty, but it is not worthless. Only the
4424    /// fields that are genuinely undefined at zero dispersion may be zeroed;
4425    /// the parametric VaR still has its deterministic limit at the mean.
4426    #[test]
4427    fn test_risk_metrics_flat_curve_keeps_deterministic_var() {
4428        let curve = create_constant_curve();
4429        let metrics = curve.compute_risk_metrics().unwrap();
4430
4431        // Measured, and genuinely zero.
4432        assert_eq!(metrics.volatility, Decimal::ZERO);
4433        // `mean - 1.645 * 0` is the mean, not zero: the curve is worth 5.
4434        assert_eq!(metrics.value_at_risk, dec!(5));
4435        // No sample falls below the VaR, so the conditional mean has an empty
4436        // tail and the function's own empty-tail rule gives zero.
4437        assert_eq!(metrics.expected_shortfall, Decimal::ZERO);
4438        // Zero dispersion over a mean of 5: a zero coefficient of variation.
4439        assert_eq!(metrics.coefficient_of_variation, Decimal::ZERO);
4440        // `mean / 0` is the one genuinely undefined field.
4441        assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
4442    }
4443
4444    /// The same shape with a negative mean: the VaR limit follows the mean
4445    /// wherever it sits, so a sign error cannot hide behind a positive value.
4446    #[test]
4447    fn test_risk_metrics_flat_negative_curve_keeps_deterministic_var() {
4448        let points = BTreeSet::from_iter(vec![
4449            Point2D::new(dec!(0.0), dec!(-2.5)),
4450            Point2D::new(dec!(1.0), dec!(-2.5)),
4451            Point2D::new(dec!(2.0), dec!(-2.5)),
4452        ]);
4453        let metrics = Curve::new(points).compute_risk_metrics().unwrap();
4454
4455        assert_eq!(metrics.volatility, Decimal::ZERO);
4456        assert_eq!(metrics.value_at_risk, dec!(-2.5));
4457        assert_eq!(metrics.sharpe_ratio, Decimal::ZERO);
4458    }
4459
4460    #[test]
4461    fn test_risk_metrics() {
4462        // Curva lineal
4463        let linear_curve = create_linear_curve();
4464        let risk_metrics = linear_curve.compute_risk_metrics().unwrap();
4465
4466        assert!(
4467            risk_metrics.volatility > dec!(0.0),
4468            "Volatility debe ser mayor a cero."
4469        );
4470        assert!(
4471            risk_metrics.value_at_risk != dec!(0.0),
4472            "Value at Risk no debe ser cero."
4473        );
4474        assert!(
4475            risk_metrics.coefficient_of_variation != dec!(0.0),
4476            "the coefficient of variation must not be zero"
4477        );
4478    }
4479
4480    #[test]
4481    fn test_risk_metrics_bis() {
4482        // Linear curve
4483        let linear_curve = create_linear_curve();
4484        let risk_metrics = linear_curve.compute_risk_metrics().unwrap();
4485
4486        // Volatility and risk metrics should be non-zero
4487        assert!(risk_metrics.volatility > dec!(0.0));
4488        assert!(risk_metrics.value_at_risk != dec!(0.0));
4489        assert!(risk_metrics.coefficient_of_variation != dec!(0.0));
4490
4491        // Constant curve
4492        let constant_curve = create_constant_curve();
4493        let constant_risk_metrics = constant_curve.compute_risk_metrics().unwrap();
4494
4495        // Volatility should be zero for a constant curve
4496        assert_decimal_eq!(constant_risk_metrics.volatility, dec!(0.0), dec!(0.001));
4497    }
4498
4499    #[test]
4500    fn test_edge_cases() {
4501        // Empty curve
4502        let empty_curve = Curve::new(BTreeSet::new());
4503
4504        assert!(empty_curve.compute_basic_metrics().is_ok());
4505        assert!(empty_curve.compute_shape_metrics().is_ok());
4506        assert!(empty_curve.compute_range_metrics().is_ok());
4507        assert!(empty_curve.compute_trend_metrics().is_ok());
4508        assert!(empty_curve.compute_risk_metrics().is_ok());
4509
4510        // Single point curve
4511        let single_point_curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
4512            dec!(1.0),
4513            dec!(1.0),
4514        )]));
4515
4516        assert!(single_point_curve.compute_basic_metrics().is_ok());
4517        assert!(single_point_curve.compute_shape_metrics().is_ok());
4518        assert!(single_point_curve.compute_range_metrics().is_ok());
4519        assert!(single_point_curve.compute_trend_metrics().is_ok());
4520        assert!(single_point_curve.compute_risk_metrics().is_ok());
4521    }
4522}
4523
4524#[cfg(test)]
4525mod tests_merge_axis_interpolate {
4526    use super::*;
4527    use crate::curves::utils::create_linear_curve;
4528    use crate::geometrics::InterpolationType;
4529    use rust_decimal_macros::dec;
4530
4531    #[test]
4532    fn test_merge_axis_interpolate_linear() {
4533        // Create two curves with different x ranges and points
4534        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(0.5)).unwrap();
4535        let curve2 = create_linear_curve(dec!(4.0), dec!(20.0), dec!(1.0)).unwrap();
4536
4537        // Merge and interpolate using linear interpolation
4538        let result = curve1.merge_axis_interpolate(&curve2, InterpolationType::Linear);
4539
4540        assert!(result.is_ok());
4541        let (interpolated_curve1, interpolated_curve2) = result.unwrap();
4542
4543        // Verify that both interpolated curves have the same x range
4544        assert_eq!(interpolated_curve1.x_range.0, interpolated_curve2.x_range.0);
4545        assert_eq!(interpolated_curve1.x_range.1, interpolated_curve2.x_range.1);
4546
4547        // Verify number of points (should cover full merged x range)
4548        assert_eq!(interpolated_curve1.points.len(), 10);
4549        assert_eq!(interpolated_curve2.points.len(), 10);
4550        assert_eq!(interpolated_curve1.x_range, interpolated_curve2.x_range);
4551        assert_eq!(
4552            interpolated_curve1.get_index_values(),
4553            interpolated_curve2.get_index_values()
4554        );
4555    }
4556
4557    #[test]
4558    fn test_merge_axis_interpolate_cubic() {
4559        // Create two curves with different x ranges and points
4560        let curve1 = create_linear_curve(dec!(0.0), dec!(10.0), dec!(0.5)).unwrap();
4561        let curve2 = create_linear_curve(dec!(4.0), dec!(20.0), dec!(1.0)).unwrap();
4562
4563        // Merge and interpolate using cubic interpolation
4564        let result = curve1.merge_axis_interpolate(&curve2, InterpolationType::Cubic);
4565
4566        assert!(result.is_ok());
4567        let (interpolated_curve1, interpolated_curve2) = result.unwrap();
4568
4569        // Verify that both interpolated curves have the same x range
4570        assert_eq!(interpolated_curve1.x_range.0, interpolated_curve2.x_range.0);
4571        assert_eq!(interpolated_curve1.x_range.1, interpolated_curve2.x_range.1);
4572
4573        // Verify number of points (should cover full merged x range)
4574        assert_eq!(interpolated_curve1.points.len(), 10);
4575        assert_eq!(interpolated_curve2.points.len(), 10);
4576        assert_eq!(interpolated_curve1.x_range, interpolated_curve2.x_range);
4577        assert_eq!(
4578            interpolated_curve1.get_index_values(),
4579            interpolated_curve2.get_index_values()
4580        );
4581    }
4582}
4583
4584#[cfg(test)]
4585mod tests_geometric_transformations {
4586    use super::*;
4587    use rust_decimal_macros::dec;
4588
4589    fn create_test_curve() -> Curve {
4590        Curve::new(BTreeSet::from_iter(vec![
4591            Point2D::new(dec!(0.0), dec!(0.0)),
4592            Point2D::new(dec!(1.0), dec!(1.0)),
4593            Point2D::new(dec!(2.0), dec!(4.0)),
4594            Point2D::new(dec!(3.0), dec!(9.0)),
4595        ]))
4596    }
4597
4598    mod test_translate {
4599        use super::*;
4600
4601        #[test]
4602        fn test_translate_positive() {
4603            let curve = create_test_curve();
4604            let result = curve.translate(vec![&dec!(2.0), &dec!(3.0)]).unwrap();
4605
4606            let translated_points: Vec<_> = result.points.iter().collect();
4607            assert_eq!(translated_points[0].x, dec!(2.0));
4608            assert_eq!(translated_points[0].y, dec!(3.0));
4609
4610            assert_eq!(translated_points[1].x, dec!(3.0));
4611            assert_eq!(translated_points[1].y, dec!(4.0));
4612
4613            assert_eq!(translated_points[2].x, dec!(4.0));
4614            assert_eq!(translated_points[2].y, dec!(7.0));
4615
4616            assert_eq!(translated_points[3].x, dec!(5.0));
4617            assert_eq!(translated_points[3].y, dec!(12.0));
4618        }
4619
4620        #[test]
4621        fn test_translate_negative() {
4622            let curve = create_test_curve();
4623            let result = curve.translate(vec![&dec!(-1.0), &dec!(-2.0)]).unwrap();
4624
4625            let translated_points: Vec<_> = result.points.iter().collect();
4626            assert_eq!(translated_points[0].x, dec!(-1.0));
4627            assert_eq!(translated_points[0].y, dec!(-2.0));
4628        }
4629
4630        #[test]
4631        fn test_translate_zero() {
4632            let curve = create_test_curve();
4633            let result = curve.translate(vec![&dec!(0.0), &dec!(0.0)]).unwrap();
4634            assert_eq!(curve.points, result.points);
4635        }
4636
4637        #[test]
4638        fn test_translate_wrong_dimensions() {
4639            let curve = create_test_curve();
4640            let result = curve.translate(vec![&dec!(1.0)]);
4641            assert!(result.is_err());
4642        }
4643
4644        #[test]
4645        fn test_translate_preserves_shape() {
4646            let curve = create_test_curve();
4647            let result = curve.translate(vec![&dec!(1.0), &dec!(1.0)]).unwrap();
4648
4649            let original_diffs: Vec<Decimal> = curve
4650                .points
4651                .iter()
4652                .zip(curve.points.iter().skip(1))
4653                .map(|(a, b)| b.y - a.y)
4654                .collect();
4655
4656            let translated_diffs: Vec<Decimal> = result
4657                .points
4658                .iter()
4659                .zip(result.points.iter().skip(1))
4660                .map(|(a, b)| b.y - a.y)
4661                .collect();
4662
4663            assert_eq!(original_diffs, translated_diffs);
4664        }
4665    }
4666
4667    mod test_scale {
4668        use super::*;
4669
4670        #[test]
4671        fn test_scale_uniform() {
4672            let curve = create_test_curve();
4673            let result = curve.scale(vec![&dec!(2.0), &dec!(2.0)]).unwrap();
4674
4675            let scaled_points: Vec<_> = result.points.iter().collect();
4676
4677            assert_eq!(scaled_points[0].x, dec!(0.0));
4678            assert_eq!(scaled_points[0].y, dec!(0.0));
4679
4680            assert_eq!(scaled_points[1].x, dec!(2.0));
4681            assert_eq!(scaled_points[1].y, dec!(2.0));
4682
4683            assert_eq!(scaled_points[2].x, dec!(4.0));
4684            assert_eq!(scaled_points[2].y, dec!(8.0));
4685
4686            assert_eq!(scaled_points[3].x, dec!(6.0));
4687            assert_eq!(scaled_points[3].y, dec!(18.0));
4688        }
4689
4690        #[test]
4691        fn test_scale_non_uniform() {
4692            let curve = create_test_curve();
4693            let result = curve.scale(vec![&dec!(2.0), &dec!(3.0)]).unwrap();
4694
4695            let scaled_points: Vec<_> = result.points.iter().collect();
4696            assert_eq!(scaled_points[1].x, dec!(2.0));
4697            assert_eq!(scaled_points[1].y, dec!(3.0));
4698        }
4699
4700        #[test]
4701        fn test_scale_zero() {
4702            let curve = create_test_curve();
4703            let result = curve.scale(vec![&dec!(0.0), &dec!(0.0)]).unwrap();
4704
4705            assert!(
4706                result
4707                    .points
4708                    .iter()
4709                    .all(|p| p.x == dec!(0.0) && p.y == dec!(0.0))
4710            );
4711        }
4712
4713        #[test]
4714        fn test_scale_wrong_dimensions() {
4715            let curve = create_test_curve();
4716            let result = curve.scale(vec![&dec!(2.0)]);
4717            assert!(result.is_err());
4718        }
4719
4720        #[test]
4721        fn test_scale_negative() {
4722            let curve = create_test_curve();
4723            let result = curve.scale(vec![&dec!(-1.0), &dec!(-1.0)]).unwrap();
4724
4725            assert_eq!(result[1].x, dec!(-2.0));
4726            assert_eq!(result[1].y, dec!(-4.0));
4727
4728            assert_eq!(result[3].x, dec!(0.0));
4729            assert_eq!(result[3].y, dec!(0.0));
4730        }
4731    }
4732
4733    mod test_intersect_with {
4734        use super::*;
4735
4736        #[test]
4737        fn test_curves_intersect() {
4738            let curve1 = create_test_curve();
4739            let curve2 = Curve::new(BTreeSet::from_iter(vec![
4740                Point2D::new(dec!(0.0), dec!(0.0)),
4741                Point2D::new(dec!(1.0), dec!(2.0)),
4742            ]));
4743
4744            let intersections = curve1.intersect_with(&curve2).unwrap();
4745            assert_eq!(intersections.len(), 1);
4746        }
4747
4748        #[test]
4749        fn test_no_intersection() {
4750            let curve1 = create_test_curve();
4751            let curve2 = Curve::new(BTreeSet::from_iter(vec![
4752                Point2D::new(dec!(10.0), dec!(10.0)),
4753                Point2D::new(dec!(11.0), dec!(11.0)),
4754            ]));
4755
4756            let intersections = curve1.intersect_with(&curve2).unwrap();
4757            assert!(intersections.is_empty());
4758        }
4759
4760        #[test]
4761        fn test_multiple_intersections() {
4762            let curve1 = create_test_curve();
4763            let curve2 = create_test_curve();
4764
4765            let intersections = curve1.intersect_with(&curve2).unwrap();
4766            assert_eq!(intersections.len(), curve1.points.len());
4767        }
4768
4769        #[test]
4770        fn test_self_intersection() {
4771            let curve = create_test_curve();
4772            let intersections = curve.intersect_with(&curve).unwrap();
4773            assert_eq!(intersections.len(), curve.points.len());
4774        }
4775
4776        #[test]
4777        fn test_empty_curves() {
4778            let curve1 = Curve::new(BTreeSet::new());
4779            let curve2 = Curve::new(BTreeSet::new());
4780
4781            let intersections = curve1.intersect_with(&curve2).unwrap();
4782            assert!(intersections.is_empty());
4783        }
4784    }
4785
4786    mod test_derivative_at {
4787        use super::*;
4788
4789        #[test]
4790        fn test_linear_derivative() {
4791            let curve = Curve::new(BTreeSet::from_iter(vec![
4792                Point2D::new(dec!(0.0), dec!(0.0)),
4793                Point2D::new(dec!(1.0), dec!(1.0)),
4794            ]));
4795
4796            let derivative = curve
4797                .derivative_at(&Point2D::new(dec!(0.5), dec!(0.5)))
4798                .unwrap();
4799            assert_eq!(derivative[0], dec!(1.0));
4800        }
4801
4802        #[test]
4803        fn test_quadratic_derivative() {
4804            let curve = create_test_curve();
4805            let derivative = curve
4806                .derivative_at(&Point2D::new(dec!(1.0), dec!(1.0)))
4807                .unwrap();
4808            assert_eq!(derivative[0], dec!(2.0));
4809            let derivative2 = curve
4810                .derivative_at(&Point2D::new(dec!(2.0), dec!(4.0)))
4811                .unwrap();
4812            assert_eq!(derivative2[0], dec!(4.0));
4813        }
4814
4815        #[test]
4816        fn test_out_of_range() {
4817            let curve = create_test_curve();
4818            let result = curve.derivative_at(&Point2D::new(dec!(10.0), dec!(0.0)));
4819            assert!(result.is_err());
4820        }
4821
4822        #[test]
4823        fn test_at_endpoint() {
4824            let curve = create_test_curve();
4825            let derivative = curve
4826                .derivative_at(&Point2D::new(dec!(0.0), dec!(0.0)))
4827                .unwrap();
4828            assert!(derivative[0] == dec!(0.0));
4829        }
4830
4831        #[test]
4832        fn test_vertical_line() {
4833            let curve = Curve::new(BTreeSet::from_iter(vec![
4834                Point2D::new(dec!(1.0), dec!(0.0)),
4835                Point2D::new(dec!(1.0), dec!(1.0)),
4836            ]));
4837
4838            let result = curve.derivative_at(&Point2D::new(dec!(1.0), dec!(0.5)));
4839            assert!(result.is_err());
4840        }
4841    }
4842
4843    mod test_extrema {
4844        use super::*;
4845
4846        #[test]
4847        fn test_find_extrema() {
4848            let curve = create_test_curve();
4849            let (min, max) = curve.extrema().unwrap();
4850            assert_eq!(min.y, dec!(0.0));
4851            assert_eq!(max.y, dec!(9.0));
4852        }
4853
4854        #[test]
4855        fn test_empty_curve() {
4856            let curve = Curve::new(BTreeSet::new());
4857            let result = curve.extrema();
4858            assert!(result.is_err());
4859        }
4860
4861        #[test]
4862        fn test_single_point() {
4863            let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
4864                dec!(1.0),
4865                dec!(1.0),
4866            )]));
4867
4868            let (min, max) = curve.extrema().unwrap();
4869            assert_eq!(min, max);
4870        }
4871
4872        #[test]
4873        fn test_flat_curve() {
4874            let curve = Curve::new(BTreeSet::from_iter(vec![
4875                Point2D::new(dec!(0.0), dec!(1.0)),
4876                Point2D::new(dec!(1.0), dec!(1.0)),
4877            ]));
4878
4879            let (min, max) = curve.extrema().unwrap();
4880            assert_eq!(min.y, max.y);
4881        }
4882
4883        #[test]
4884        fn test_multiple_extrema() {
4885            let curve = Curve::new(BTreeSet::from_iter(vec![
4886                Point2D::new(dec!(0.0), dec!(0.0)),
4887                Point2D::new(dec!(1.0), dec!(1.0)),
4888                Point2D::new(dec!(2.0), dec!(0.0)),
4889            ]));
4890
4891            let (min, max) = curve.extrema().unwrap();
4892            assert_eq!(min.y, dec!(0.0));
4893            assert_eq!(max.y, dec!(1.0));
4894        }
4895    }
4896
4897    mod test_measure_under {
4898        use super::*;
4899
4900        #[test]
4901        fn test_area_under_linear() {
4902            let curve = Curve::new(BTreeSet::from_iter(vec![
4903                Point2D::new(dec!(0.0), dec!(0.0)),
4904                Point2D::new(dec!(1.0), dec!(1.0)),
4905            ]));
4906
4907            let area = curve.measure_under(&dec!(0.0)).unwrap();
4908            assert_eq!(area, dec!(0.5));
4909        }
4910
4911        #[test]
4912        fn test_area_empty_curve() {
4913            let curve = Curve::new(BTreeSet::new());
4914            let area = curve.measure_under(&dec!(0.0)).unwrap();
4915            assert_eq!(area, dec!(0.0));
4916        }
4917
4918        #[test]
4919        fn test_area_single_point() {
4920            let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
4921                dec!(1.0),
4922                dec!(1.0),
4923            )]));
4924
4925            let area = curve.measure_under(&dec!(0.0)).unwrap();
4926            assert_eq!(area, dec!(0.0));
4927        }
4928
4929        #[test]
4930        fn test_area_with_base_value() {
4931            let curve = create_test_curve();
4932            let area1 = curve.measure_under(&dec!(0.0)).unwrap();
4933            let area2 = curve.measure_under(&dec!(1.0)).unwrap();
4934            assert!(area1 > area2);
4935        }
4936
4937        #[test]
4938        fn test_negative_area() {
4939            let curve = Curve::new(BTreeSet::from_iter(vec![
4940                Point2D::new(dec!(0.0), dec!(-1.0)),
4941                Point2D::new(dec!(1.0), dec!(-2.0)),
4942            ]));
4943
4944            let area = curve.measure_under(&dec!(0.0)).unwrap();
4945            assert!(area > dec!(0.0));
4946        }
4947    }
4948}
4949
4950#[cfg(test)]
4951mod tests_curve_serde {
4952    use super::*;
4953    use rust_decimal_macros::dec;
4954
4955    // Helper function to create a test curve
4956    fn create_test_curve() -> Curve {
4957        let mut points = BTreeSet::new();
4958        points.insert(Point2D {
4959            x: dec!(1.0),
4960            y: dec!(2.0),
4961        });
4962        points.insert(Point2D {
4963            x: dec!(3.0),
4964            y: dec!(4.0),
4965        });
4966        points.insert(Point2D {
4967            x: dec!(5.0),
4968            y: dec!(6.0),
4969        });
4970
4971        Curve {
4972            points,
4973            x_range: (dec!(1.0), dec!(5.0)),
4974        }
4975    }
4976
4977    #[test]
4978    fn test_basic_serialization() {
4979        let curve = create_test_curve();
4980        let serialized = serde_json::to_string(&curve).unwrap();
4981        let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
4982
4983        assert_eq!(curve.points, deserialized.points);
4984        assert_eq!(curve.x_range, deserialized.x_range);
4985    }
4986
4987    #[test]
4988    fn test_pretty_print() {
4989        let curve = create_test_curve();
4990        let serialized = serde_json::to_string_pretty(&curve).unwrap();
4991
4992        // Verify pretty print format
4993        assert!(serialized.contains('\n'));
4994        assert!(serialized.contains("  "));
4995
4996        // Verify deserialization still works
4997        let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
4998        assert_eq!(curve.points, deserialized.points);
4999    }
5000
5001    #[test]
5002    fn test_empty_curve() {
5003        let curve = Curve {
5004            points: BTreeSet::new(),
5005            x_range: (dec!(0.0), dec!(0.0)),
5006        };
5007
5008        let serialized = serde_json::to_string(&curve).unwrap();
5009        let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
5010
5011        assert!(deserialized.points.is_empty());
5012        assert_eq!(deserialized.x_range, (dec!(0.0), dec!(0.0)));
5013    }
5014
5015    #[test]
5016    fn test_curve_with_negative_values() {
5017        let mut points = BTreeSet::new();
5018        points.insert(Point2D {
5019            x: dec!(-1.0),
5020            y: dec!(-2.0),
5021        });
5022        points.insert(Point2D {
5023            x: dec!(-3.0),
5024            y: dec!(-4.0),
5025        });
5026
5027        let curve = Curve {
5028            points,
5029            x_range: (dec!(-3.0), dec!(-1.0)),
5030        };
5031
5032        let serialized = serde_json::to_string(&curve).unwrap();
5033        let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
5034
5035        assert_eq!(curve.points, deserialized.points);
5036        assert_eq!(curve.x_range, deserialized.x_range);
5037    }
5038
5039    #[test]
5040    fn test_curve_with_high_precision() {
5041        let mut points = BTreeSet::new();
5042        points.insert(Point2D {
5043            x: dec!(1.12345678901234567890),
5044            y: dec!(2.12345678901234567890),
5045        });
5046        points.insert(Point2D {
5047            x: dec!(3.12345678901234567890),
5048            y: dec!(4.12345678901234567890),
5049        });
5050
5051        let curve = Curve {
5052            points,
5053            x_range: (dec!(1.12345678901234567890), dec!(3.12345678901234567890)),
5054        };
5055
5056        let serialized = serde_json::to_string(&curve).unwrap();
5057        let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
5058
5059        assert_eq!(curve.points, deserialized.points);
5060        assert_eq!(curve.x_range, deserialized.x_range);
5061    }
5062
5063    #[test]
5064    fn test_invalid_json() {
5065        // Missing required fields
5066        let json_str = r#"{"points": []}"#;
5067        let result = serde_json::from_str::<Curve>(json_str);
5068        assert!(result.is_err());
5069
5070        // Invalid points format
5071        let json_str = r#"{"points": [1, 2, 3], "x_range": [0, 1]}"#;
5072        let result = serde_json::from_str::<Curve>(json_str);
5073        assert!(result.is_err());
5074
5075        // Invalid x_range format
5076        let json_str = r#"{"points": [], "x_range": "invalid"}"#;
5077        let result = serde_json::from_str::<Curve>(json_str);
5078        assert!(result.is_err());
5079    }
5080
5081    #[test]
5082    fn test_json_structure() {
5083        let curve = create_test_curve();
5084        let serialized = serde_json::to_string(&curve).unwrap();
5085        let json: serde_json::Value = serde_json::from_str(&serialized).unwrap();
5086
5087        // Check structure
5088        assert!(json.is_object());
5089        assert!(json.get("points").is_some());
5090        assert!(json.get("x_range").is_some());
5091
5092        // Check points is an array
5093        assert!(json.get("points").unwrap().is_array());
5094
5095        // Check x_range is an array of 2 elements
5096        let x_range = json.get("x_range").unwrap().as_array().unwrap();
5097        assert_eq!(x_range.len(), 2);
5098    }
5099
5100    #[test]
5101    fn test_multiple_curves() {
5102        let curve1 = create_test_curve();
5103        let mut curve2 = create_test_curve();
5104        curve2.x_range = (dec!(6.0), dec!(10.0));
5105
5106        let curves = vec![curve1, curve2];
5107        let serialized = serde_json::to_string(&curves).unwrap();
5108        let deserialized: Vec<Curve> = serde_json::from_str(&serialized).unwrap();
5109
5110        assert_eq!(curves.len(), deserialized.len());
5111        assert_eq!(curves[0].points, deserialized[0].points);
5112        assert_eq!(curves[1].points, deserialized[1].points);
5113    }
5114
5115    #[test]
5116    fn test_ordering_preservation() {
5117        let curve = create_test_curve();
5118        let serialized = serde_json::to_string(&curve).unwrap();
5119        let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
5120
5121        // Convert points to vectors to check ordering
5122        let original_points: Vec<_> = curve.points.into_iter().collect();
5123        let deserialized_points: Vec<_> = deserialized.points.into_iter().collect();
5124
5125        // Check if points maintain their order
5126        assert_eq!(original_points, deserialized_points);
5127    }
5128
5129    #[test]
5130    fn test_curve_with_extremes() {
5131        let mut points = BTreeSet::new();
5132        points.insert(Point2D {
5133            x: Decimal::MAX,
5134            y: Decimal::MAX,
5135        });
5136        points.insert(Point2D {
5137            x: Decimal::MIN,
5138            y: Decimal::MIN,
5139        });
5140
5141        let curve = Curve {
5142            points,
5143            x_range: (Decimal::MIN, Decimal::MAX),
5144        };
5145
5146        let serialized = serde_json::to_string(&curve).unwrap();
5147        let deserialized: Curve = serde_json::from_str(&serialized).unwrap();
5148
5149        assert_eq!(curve.points, deserialized.points);
5150        assert_eq!(curve.x_range, deserialized.x_range);
5151    }
5152}
5153
5154#[cfg(test)]
5155mod tests_curve_display_and_default {
5156    use crate::curves::{Curve, Point2D};
5157    use rust_decimal::Decimal;
5158    use rust_decimal_macros::dec;
5159    use std::collections::BTreeSet;
5160
5161    #[test]
5162    fn test_curve_display() {
5163        let point1 = Point2D::new(dec!(1.0), dec!(2.0));
5164        let point2 = Point2D::new(dec!(3.0), dec!(4.0));
5165
5166        let mut points = BTreeSet::new();
5167        points.insert(point1);
5168        points.insert(point2);
5169
5170        let curve = Curve::new(points);
5171
5172        let display_string = format!("{curve}");
5173        assert!(display_string.contains("(x: 1.0, y: 2.0)"));
5174        assert!(display_string.contains("(x: 3.0, y: 4.0)"));
5175    }
5176
5177    #[test]
5178    fn test_curve_default() {
5179        // Test the Default implementation (line 83, 85-86)
5180        let curve = Curve::default();
5181        assert!(curve.points.is_empty());
5182        assert_eq!(curve.x_range, (Decimal::ZERO, Decimal::ZERO));
5183    }
5184}
5185
5186#[cfg(test)]
5187mod tests_curve_len_and_geometric {
5188    use crate::curves::{Curve, Point2D};
5189    use crate::error::CurveError;
5190    use crate::geometrics::{ConstructionMethod, ConstructionParams, GeometricObject};
5191    use optionstratlib_core::utils::Len;
5192    use rust_decimal_macros::dec;
5193    use std::collections::BTreeSet;
5194
5195    #[test]
5196    fn test_curve_len() {
5197        // Test Len implementation (lines 129-130)
5198        let curve = Curve::default();
5199        assert_eq!(curve.len(), 0);
5200        assert!(curve.is_empty());
5201
5202        let mut points = BTreeSet::new();
5203        points.insert(Point2D::new(dec!(1.0), dec!(2.0)));
5204        let curve_with_point = Curve::new(points);
5205        assert_eq!(curve_with_point.len(), 1);
5206        assert!(!curve_with_point.is_empty());
5207    }
5208
5209    #[test]
5210    fn test_curve_get_points() {
5211        // Test GeometricObject.get_points (line 164)
5212        let mut points = BTreeSet::new();
5213        points.insert(Point2D::new(dec!(1.0), dec!(2.0)));
5214        points.insert(Point2D::new(dec!(3.0), dec!(4.0)));
5215
5216        let curve = Curve::new(points);
5217        let retrieved_points = curve.get_points();
5218
5219        assert_eq!(retrieved_points.len(), 2);
5220        assert!(
5221            retrieved_points
5222                .iter()
5223                .any(|p| p.x == dec!(1.0) && p.y == dec!(2.0))
5224        );
5225        assert!(
5226            retrieved_points
5227                .iter()
5228                .any(|p| p.x == dec!(3.0) && p.y == dec!(4.0))
5229        );
5230    }
5231
5232    #[test]
5233    fn test_construct_method_error() {
5234        // Test ConstructionMethod errors (lines 168-175, 179, 181, 189)
5235        let result = Curve::construct(ConstructionMethod::Parametric {
5236            f: Box::new(|_| Err(CurveError::generator(std::io::Error::other("Test error")))),
5237            params: ConstructionParams::D2 {
5238                t_start: dec!(0.0),
5239                t_end: dec!(1.0),
5240                steps: 10,
5241            },
5242        });
5243
5244        assert!(result.is_err());
5245        match result {
5246            Err(CurveError::Generator(source)) => {
5247                assert!(source.to_string().contains("Test error"));
5248            }
5249            _ => panic!("Expected Generator"),
5250        }
5251
5252        // Test invalid params
5253        let result = Curve::construct(ConstructionMethod::Parametric {
5254            f: Box::new(|t| Ok(Point2D::new(t, t * dec!(2.0)))),
5255            params: ConstructionParams::D3 {
5256                x_start: dec!(0.0),
5257                x_end: dec!(1.0),
5258                y_start: dec!(0.0),
5259                y_end: dec!(1.0),
5260                x_steps: 10,
5261                y_steps: 10,
5262            },
5263        });
5264
5265        assert!(result.is_err());
5266        match result {
5267            Err(CurveError::ConstructionError(msg)) => {
5268                assert_eq!(msg, "Invalid parameters");
5269            }
5270            _ => panic!("Expected ConstructionError"),
5271        }
5272    }
5273}
5274
5275#[cfg(test)]
5276mod tests_interpolation_edge_cases {
5277    use crate::curves::{Curve, Point2D};
5278    use crate::geometrics::{AxisOperations, Interpolate, InterpolationType};
5279    use rust_decimal_macros::dec;
5280    use std::collections::BTreeSet;
5281    use tracing::info;
5282
5283    #[test]
5284    fn test_cubic_interpolation_edge_cases() {
5285        // Test edge cases for cubic interpolation (line 924)
5286        let curve = Curve::new(BTreeSet::from_iter(vec![
5287            Point2D::new(dec!(0.0), dec!(0.0)),
5288            Point2D::new(dec!(1.0), dec!(1.0)),
5289            Point2D::new(dec!(2.0), dec!(4.0)),
5290            Point2D::new(dec!(3.0), dec!(9.0)),
5291        ]));
5292
5293        // Test interpolation at the start of the curve
5294        let start_result = curve.interpolate(dec!(0.25), InterpolationType::Cubic);
5295        assert!(start_result.is_ok());
5296
5297        // Test interpolation at the end of the curve
5298        let end_result = curve.interpolate(dec!(2.75), InterpolationType::Cubic);
5299        assert!(end_result.is_ok());
5300    }
5301
5302    #[test]
5303    fn test_spline_interpolation_edge_cases() {
5304        // Test edge cases for spline interpolation (lines 942-943, 1037)
5305        let curve = Curve::new(BTreeSet::from_iter(vec![
5306            Point2D::new(dec!(0.0), dec!(0.0)),
5307            Point2D::new(dec!(1.0), dec!(1.0)),
5308            Point2D::new(dec!(2.0), dec!(4.0)),
5309            Point2D::new(dec!(3.0), dec!(9.0)),
5310        ]));
5311
5312        // Test with exact match to a point
5313        let exact_result = curve.interpolate(dec!(1.0), InterpolationType::Spline);
5314        assert!(exact_result.is_ok());
5315        let exact_point = exact_result.unwrap();
5316        assert_eq!(exact_point.x, dec!(1.0));
5317        assert_eq!(exact_point.y, dec!(1.0));
5318
5319        // Test spline system solving at the midpoint
5320        let mid_result = curve.interpolate(dec!(1.5), InterpolationType::Spline);
5321        assert!(mid_result.is_ok());
5322    }
5323
5324    #[test]
5325    fn test_axis_operations() {
5326        // Test AxisOperations (line 1155)
5327        let curve = Curve::new(BTreeSet::from_iter(vec![
5328            Point2D::new(dec!(0.0), dec!(0.0)),
5329            Point2D::new(dec!(1.0), dec!(1.0)),
5330            Point2D::new(dec!(2.0), dec!(4.0)),
5331        ]));
5332
5333        // Test get_index_values
5334        let indices = curve.get_index_values();
5335        assert_eq!(indices, vec![dec!(0.0), dec!(1.0), dec!(2.0)]);
5336
5337        // Test get_values
5338        let values = curve.get_values(dec!(1.0));
5339        assert_eq!(values.len(), 1);
5340        assert_eq!(*values[0], dec!(1.0));
5341
5342        // Test get_closest_point
5343        let closest = curve.get_closest_point(&dec!(0.9)).unwrap();
5344        assert_eq!(closest.x, dec!(1.0));
5345        assert_eq!(closest.y, dec!(1.0));
5346
5347        info!("{:?}", curve);
5348        // Test get_point
5349        let point = curve.get_point(&dec!(1.0));
5350        assert!(point.is_some());
5351        assert_eq!(point.unwrap().y, dec!(1.0));
5352
5353        // Test get_point with non-existent x value
5354        let non_existent = curve.get_point(&dec!(1.5));
5355        assert!(non_existent.is_none());
5356    }
5357}
5358
5359#[cfg(test)]
5360mod tests_axis_merge_and_transformations {
5361    use super::*;
5362    use rust_decimal_macros::dec;
5363
5364    #[test]
5365    fn test_merge_axis_index() {
5366        // Test merge_axis_index (lines 1227-1228, 1236)
5367        let curve1 = Curve::new(BTreeSet::from_iter(vec![
5368            Point2D::new(dec!(0.0), dec!(0.0)),
5369            Point2D::new(dec!(1.0), dec!(1.0)),
5370        ]));
5371
5372        let curve2 = Curve::new(BTreeSet::from_iter(vec![
5373            Point2D::new(dec!(1.0), dec!(2.0)),
5374            Point2D::new(dec!(2.0), dec!(4.0)),
5375        ]));
5376
5377        // Get merged x-values
5378        let merged = curve1.merge_axis_index(&curve2);
5379        assert_eq!(merged.len(), 1);
5380        assert!(merged.contains(&dec!(1.0)));
5381    }
5382
5383    #[test]
5384    fn test_translate_with_negative_values() {
5385        // Test translate with negative values (line 1326)
5386        let curve = Curve::new(BTreeSet::from_iter(vec![
5387            Point2D::new(dec!(0.0), dec!(0.0)),
5388            Point2D::new(dec!(1.0), dec!(1.0)),
5389        ]));
5390
5391        let result = curve.translate(vec![&dec!(-2.0), &dec!(-3.0)]).unwrap();
5392
5393        assert_eq!(result.points.len(), 2);
5394
5395        let translated_points: Vec<_> = result.points.iter().collect();
5396        assert_eq!(translated_points[0].x, dec!(-2.0));
5397        assert_eq!(translated_points[0].y, dec!(-3.0));
5398        assert_eq!(translated_points[1].x, dec!(-1.0));
5399        assert_eq!(translated_points[1].y, dec!(-2.0));
5400    }
5401
5402    #[test]
5403    fn test_intersect_with_empty_curves() {
5404        // Test intersect_with empty curves (lines 1344-1346)
5405        let curve1 = Curve::new(BTreeSet::new());
5406        let curve2 = Curve::new(BTreeSet::new());
5407
5408        let result = curve1.intersect_with(&curve2).unwrap();
5409        assert!(result.is_empty());
5410    }
5411
5412    #[test]
5413    fn test_derivative_edge_cases() {
5414        // Test derivative_at edge cases (lines 1467-1468, 1470-1471)
5415        let curve = Curve::new(BTreeSet::from_iter(vec![
5416            Point2D::new(dec!(0.0), dec!(0.0)),
5417            Point2D::new(dec!(1.0), dec!(1.0)),
5418        ]));
5419
5420        // Test derivative at endpoint
5421        let result = curve
5422            .derivative_at(&Point2D::new(dec!(0.0), dec!(0.0)))
5423            .unwrap();
5424        assert_eq!(result[0], dec!(0.0));
5425
5426        // Test derivative at midpoint
5427        let result = curve
5428            .derivative_at(&Point2D::new(dec!(0.5), dec!(0.5)))
5429            .unwrap();
5430        assert_eq!(result[0], dec!(1.0));
5431    }
5432
5433    #[test]
5434    fn test_derivative_vertical_line() {
5435        // Test derivative for vertical line (lines 1475-1476)
5436        let curve = Curve::new(BTreeSet::from_iter(vec![
5437            Point2D::new(dec!(1.0), dec!(0.0)),
5438            Point2D::new(dec!(1.0), dec!(1.0)),
5439        ]));
5440
5441        // This should return an error as the derivative is undefined for a vertical line
5442        let result = curve.derivative_at(&Point2D::new(dec!(1.0), dec!(0.5)));
5443        assert!(result.is_err());
5444    }
5445
5446    #[test]
5447    fn test_extrema_single_point() {
5448        // Test extrema with single point (lines 1478-1481)
5449        let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
5450            dec!(1.0),
5451            dec!(1.0),
5452        )]));
5453
5454        let (min, max) = curve.extrema().unwrap();
5455        assert_eq!(min, max);
5456        assert_eq!(min.x, dec!(1.0));
5457        assert_eq!(min.y, dec!(1.0));
5458    }
5459
5460    #[test]
5461    fn test_extrema_flat_curve() {
5462        // Test extrema with flat curve (lines 1483-1484)
5463        let curve = Curve::new(BTreeSet::from_iter(vec![
5464            Point2D::new(dec!(0.0), dec!(1.0)),
5465            Point2D::new(dec!(1.0), dec!(1.0)),
5466        ]));
5467
5468        let (min, max) = curve.extrema().unwrap();
5469        assert_eq!(min.y, max.y);
5470        assert_eq!(min.y, dec!(1.0));
5471    }
5472
5473    #[test]
5474    fn test_measure_under_single_point() {
5475        // Test measure_under with single point (lines 1488-1490)
5476        let curve = Curve::new(BTreeSet::from_iter(vec![Point2D::new(
5477            dec!(1.0),
5478            dec!(1.0),
5479        )]));
5480
5481        let area = curve.measure_under(&dec!(0.0)).unwrap();
5482        assert_eq!(area, dec!(0.0));
5483    }
5484
5485    #[test]
5486    fn test_measure_under_negative_area() {
5487        // Test measure_under with negative area (line 1492)
5488        let curve = Curve::new(BTreeSet::from_iter(vec![
5489            Point2D::new(dec!(0.0), dec!(-1.0)),
5490            Point2D::new(dec!(1.0), dec!(-2.0)),
5491        ]));
5492
5493        let area = curve.measure_under(&dec!(0.0)).unwrap();
5494        assert!(area > dec!(0.0));
5495        assert_eq!(area, dec!(1.5)); // |0.5 * 1 * (-1.5)| = 0.75 * 2 = 1.5
5496    }
5497
5498    #[test]
5499    fn test_extrema_multiple_extrema() {
5500        // Test extrema with multiple local extrema (lines 1518, 1524)
5501        let curve = Curve::new(BTreeSet::from_iter(vec![
5502            Point2D::new(dec!(0.0), dec!(0.0)),
5503            Point2D::new(dec!(1.0), dec!(2.0)),
5504            Point2D::new(dec!(2.0), dec!(1.0)),
5505            Point2D::new(dec!(3.0), dec!(3.0)),
5506        ]));
5507
5508        let (min, max) = curve.extrema().unwrap();
5509        assert_eq!(min.y, dec!(0.0));
5510        assert_eq!(max.y, dec!(3.0));
5511    }
5512}
5513
5514/// A `Curve` is a function of its abscissa, and nothing enforces it. These
5515/// tests pin what each consumer does when the rule is broken, so that no
5516/// consumer picks a survivor from a stack of ordinates without saying so.
5517#[cfg(test)]
5518mod tests_duplicate_abscissa {
5519    use crate::curves::{Curve, Point2D};
5520    use crate::error::InterpolationError;
5521    use crate::geometrics::{AxisOperations, Interpolate, InterpolationType};
5522    use rust_decimal_macros::dec;
5523    use std::collections::BTreeSet;
5524
5525    /// Four abscissae, with two ordinates stacked on `x = 1`: enough points
5526    /// for every interpolator, and the stack sits in the interior.
5527    fn stacked_curve() -> Curve {
5528        Curve::new(BTreeSet::from_iter(vec![
5529            Point2D::new(dec!(0.0), dec!(0.0)),
5530            Point2D::new(dec!(1.0), dec!(1.0)),
5531            Point2D::new(dec!(1.0), dec!(3.0)),
5532            Point2D::new(dec!(2.0), dec!(2.0)),
5533            Point2D::new(dec!(3.0), dec!(5.0)),
5534        ]))
5535    }
5536
5537    #[test]
5538    fn test_curve_new_stacked_abscissa_keeps_both_points() {
5539        let curve = stacked_curve();
5540
5541        // `Point2D` orders and compares on the full pair, so both survive.
5542        assert_eq!(curve.points.len(), 5);
5543        assert_eq!(curve.x_range, (dec!(0.0), dec!(3.0)));
5544    }
5545
5546    #[test]
5547    fn test_curve_get_values_stacked_abscissa_returns_every_ordinate() {
5548        let curve = stacked_curve();
5549
5550        let values: Vec<_> = curve.get_values(dec!(1.0)).into_iter().copied().collect();
5551        assert_eq!(values, vec![dec!(1.0), dec!(3.0)]);
5552    }
5553
5554    /// `get_point` is documented as reading the first match. This pins that
5555    /// it is the lowest ordinate, so the doc is checkable.
5556    #[test]
5557    fn test_curve_get_point_stacked_abscissa_returns_lowest_ordinate() {
5558        let curve = stacked_curve();
5559
5560        let point = curve.get_point(&dec!(1.0)).expect("x = 1 is present");
5561        assert_eq!(*point, Point2D::new(dec!(1.0), dec!(1.0)));
5562        assert!(curve.contains_point(&dec!(1.0)));
5563    }
5564
5565    #[test]
5566    fn test_curve_linear_interpolate_stacked_abscissa_is_degenerate() {
5567        let curve = stacked_curve();
5568
5569        assert!(matches!(
5570            curve.interpolate(dec!(1.0), InterpolationType::Linear),
5571            Err(InterpolationError::DegenerateInterval)
5572        ));
5573    }
5574
5575    #[test]
5576    fn test_curve_cubic_interpolate_stacked_abscissa_is_degenerate() {
5577        let curve = stacked_curve();
5578
5579        assert!(matches!(
5580            curve.interpolate(dec!(1.0), InterpolationType::Cubic),
5581            Err(InterpolationError::DegenerateInterval)
5582        ));
5583    }
5584
5585    #[test]
5586    fn test_curve_spline_interpolate_stacked_abscissa_is_degenerate() {
5587        let curve = stacked_curve();
5588
5589        assert!(matches!(
5590            curve.interpolate(dec!(1.0), InterpolationType::Spline),
5591            Err(InterpolationError::DegenerateInterval)
5592        ));
5593    }
5594
5595    /// Away from the stack the curve is still a function, and linear
5596    /// interpolation reads the ordinate on the same side of the jump.
5597    #[test]
5598    fn test_curve_linear_interpolate_away_from_stack_reads_its_own_side() {
5599        let curve = stacked_curve();
5600
5601        let left = curve
5602            .interpolate(dec!(0.5), InterpolationType::Linear)
5603            .expect("0.5 brackets between (0, 0) and the lowest ordinate at x = 1");
5604        assert_eq!(left, Point2D::new(dec!(0.5), dec!(0.5)));
5605
5606        let right = curve
5607            .interpolate(dec!(1.5), InterpolationType::Linear)
5608            .expect("1.5 brackets between the highest ordinate at x = 1 and (2, 2)");
5609        assert_eq!(right, Point2D::new(dec!(1.5), dec!(2.5)));
5610    }
5611
5612    /// A curve honouring the rule is unaffected by the exact-match guard:
5613    /// asking for a stored abscissa returns the stored point.
5614    #[test]
5615    fn test_curve_interpolate_single_ordinate_returns_the_stored_point() {
5616        let curve = Curve::new(BTreeSet::from_iter(vec![
5617            Point2D::new(dec!(0.0), dec!(0.0)),
5618            Point2D::new(dec!(1.0), dec!(1.0)),
5619            Point2D::new(dec!(2.0), dec!(4.0)),
5620            Point2D::new(dec!(3.0), dec!(9.0)),
5621        ]));
5622
5623        for interpolation in [
5624            InterpolationType::Linear,
5625            InterpolationType::Cubic,
5626            InterpolationType::Spline,
5627        ] {
5628            let point = curve
5629                .interpolate(dec!(2.0), interpolation)
5630                .expect("x = 2 is a stored abscissa");
5631            assert_eq!(point, Point2D::new(dec!(2.0), dec!(4.0)));
5632        }
5633    }
5634}
5635
5636#[cfg(test)]
5637mod tests_coefficient_of_variation {
5638    use super::*;
5639    use crate::error::MetricsError;
5640    use crate::geometrics::MetricsExtractor;
5641
5642    fn line(values: impl IntoIterator<Item = i64>) -> Curve {
5643        Curve::new(
5644            values
5645                .into_iter()
5646                .enumerate()
5647                .map(|(x, y)| Point2D::new(Decimal::from(x), Decimal::from(y)))
5648                .collect(),
5649        )
5650    }
5651
5652    /// `y = 1..5`: mean 3, population standard deviation `sqrt(2)`, so the
5653    /// coefficient of variation is `sqrt(2) / 3`, here to the last of
5654    /// `Decimal`'s 28 places. As `beta` the field held `volatility / mean`,
5655    /// `(10 / sqrt(5)) / 3 = 1.4907119849998597976061157791`, which divided
5656    /// the sum of squared deviations by `sqrt(n)` instead of taking a
5657    /// standard deviation (#824).
5658    #[test]
5659    fn test_risk_metrics_coefficient_of_variation_is_std_dev_over_mean() {
5660        let curve = line(1..=5);
5661        let metrics = curve.compute_risk_metrics().unwrap();
5662        assert_eq!(
5663            metrics.coefficient_of_variation,
5664            dec!(0.4714045207910316829338962414)
5665        );
5666        let basic = curve.compute_basic_metrics().unwrap();
5667        assert_eq!(
5668            metrics.coefficient_of_variation,
5669            d_div(basic.std_dev, basic.mean, "test").unwrap()
5670        );
5671    }
5672
5673    /// The coefficient of variation is signed as the mean.
5674    #[test]
5675    fn test_risk_metrics_coefficient_of_variation_follows_the_sign_of_the_mean() {
5676        let metrics = line((1..=5).map(|y| -y)).compute_risk_metrics().unwrap();
5677        assert_eq!(
5678            metrics.coefficient_of_variation,
5679            dec!(-0.4714045207910316829338962414)
5680        );
5681    }
5682
5683    /// `y = -1, 0, 1` has a zero mean, where `std_dev / mean` is undefined:
5684    /// an error, not the zero `beta` reported (#824).
5685    #[test]
5686    fn test_risk_metrics_zero_mean_is_an_error() {
5687        assert!(matches!(
5688            line([-1, 0, 1]).compute_risk_metrics(),
5689            Err(MetricsError::ZeroMean {
5690                metric: "coefficient of variation"
5691            })
5692        ));
5693    }
5694}
5695
5696#[cfg(test)]
5697mod tests_risk_volatility {
5698    use super::*;
5699    use crate::geometrics::MetricsExtractor;
5700
5701    fn line(values: impl IntoIterator<Item = i64>) -> Curve {
5702        Curve::new(
5703            values
5704                .into_iter()
5705                .enumerate()
5706                .map(|(x, y)| Point2D::new(Decimal::from(x), Decimal::from(y)))
5707                .collect(),
5708        )
5709    }
5710
5711    /// `y = 1..5`: the risk volatility is the population standard deviation
5712    /// `sqrt(S / n) = sqrt(10 / 5) = sqrt(2)`, and the VaR and Sharpe ratio
5713    /// follow from it, each to the last of `Decimal`'s 28 places of a 50-digit
5714    /// reference. With `S / sqrt(n)` they were `4.4721359549995793928183473373`,
5715    /// `-4.3566636459743081011861813699` and `0.6708203932499369089227521006`
5716    /// (#840).
5717    #[test]
5718    fn test_risk_metrics_volatility_is_the_population_std_dev() {
5719        let metrics = line(1..=5).compute_risk_metrics().unwrap();
5720        assert_eq!(metrics.volatility, dec!(1.4142135623730950488016887242));
5721        assert_eq!(metrics.value_at_risk, dec!(0.6736186898962586447212220487));
5722        assert_eq!(metrics.expected_shortfall, Decimal::ZERO);
5723        assert_eq!(
5724            metrics.coefficient_of_variation,
5725            dec!(0.4714045207910316829338962414)
5726        );
5727        assert_eq!(metrics.sharpe_ratio, dec!(2.1213203435596425732025330863));
5728    }
5729
5730    /// One dip in ten, `y = 1, 10 x 9`: mean 9.1, volatility 2.7, so the VaR
5731    /// `9.1 - 1.645 * 2.7 = 4.6585` sits above the dip and the expected
5732    /// shortfall is its value, 1. The old `S / sqrt(n) = 23.05...` put the VaR
5733    /// at `-28.82...`, below every sample, so the shortfall read 0 (#840).
5734    #[test]
5735    fn test_risk_metrics_expected_shortfall_sees_the_tail() {
5736        let metrics = line([1, 10, 10, 10, 10, 10, 10, 10, 10, 10])
5737            .compute_risk_metrics()
5738            .unwrap();
5739        assert_eq!(metrics.volatility, dec!(2.7));
5740        assert_eq!(metrics.value_at_risk, dec!(4.6585));
5741        assert_eq!(metrics.expected_shortfall, dec!(1));
5742        assert_eq!(metrics.sharpe_ratio, dec!(3.3703703703703703703703703704));
5743    }
5744
5745    /// The risk volatility is the `std_dev` of the basic metrics.
5746    #[test]
5747    fn test_risk_metrics_volatility_matches_basic_std_dev() {
5748        let curve = Curve::new(
5749            (0..=20i64)
5750                .map(|x| Point2D::new(Decimal::from(x), Decimal::from(x * x % 7)))
5751                .collect(),
5752        );
5753        assert_eq!(
5754            curve.compute_risk_metrics().unwrap().volatility,
5755            curve.compute_basic_metrics().unwrap().std_dev
5756        );
5757    }
5758}
5759
5760/// The O(log n) bracketing of #858 against the scan it replaced: the same
5761/// indices, the same points and the same errors, on curves with stacked
5762/// abscissas, at every sample, between samples, at the ends and outside.
5763#[cfg(test)]
5764mod tests_bracket_matches_scan {
5765    use super::*;
5766
5767    /// The scan `Interpolate::find_bracket_points` ran before #858.
5768    fn scan_bracket(curve: &Curve, x: Decimal) -> Result<(usize, usize), InterpolationError> {
5769        let points: Vec<&Point2D> = curve.points.iter().collect();
5770        if points.len() < 2 {
5771            return Err(InterpolationError::EmptyData);
5772        }
5773        let (first, last) = (points[0], points[points.len() - 1]);
5774        if x < first.x || x > last.x {
5775            return Err(InterpolationError::OutOfRange {
5776                target: x.to_string(),
5777            });
5778        }
5779        for (i, pair) in points.windows(2).enumerate() {
5780            if pair[0].x <= x && x <= pair[1].x {
5781                return Ok((i, i + 1));
5782            }
5783        }
5784        Err(InterpolationError::DegenerateInterval)
5785    }
5786
5787    /// The bilinear cell the index arithmetic selected before #858.
5788    fn scan_bilinear_cell(curve: &Curve, i: usize) -> [usize; 4] {
5789        let far = (i + 2).min(curve.len() - 2);
5790        [i, i + 1, far, far + 1]
5791    }
5792
5793    /// The cubic window the index arithmetic selected before #858.
5794    fn scan_cubic_window(curve: &Curve, i: usize) -> [usize; 4] {
5795        let len = curve.len();
5796        let first = if i == 0 {
5797            0
5798        } else if i == len - 2 {
5799            len - 4
5800        } else {
5801            i - 1
5802        };
5803        [first, first + 1, first + 2, first + 3]
5804    }
5805
5806    fn nth(curve: &Curve, index: usize) -> &Point2D {
5807        curve.points.iter().nth(index).unwrap()
5808    }
5809
5810    fn same_points(curve: &Curve, got: [&Point2D; 4], want: [usize; 4]) -> bool {
5811        got.iter()
5812            .zip(want)
5813            .all(|(point, index)| *point == nth(curve, index))
5814    }
5815
5816    /// Checks every reader #858 touched against its scan at `x`.
5817    fn check(curve: &Curve, x: Decimal) {
5818        let scanned = scan_bracket(curve, x);
5819        let fast = curve.find_bracket_points(x);
5820        assert_eq!(
5821            format!("{fast:?}"),
5822            format!("{scanned:?}"),
5823            "find_bracket_points at {x} on {curve:?}"
5824        );
5825        let pair = curve.bracket(x);
5826        match &scanned {
5827            Ok((i, j)) => {
5828                let (left, right) = pair.unwrap();
5829                assert_eq!((left, right), (nth(curve, *i), nth(curve, *j)));
5830                if curve.len() >= 4 {
5831                    let cell = curve.bilinear_cell(x).unwrap();
5832                    assert!(same_points(curve, cell, scan_bilinear_cell(curve, *i)));
5833                    let window = curve.cubic_window(x).unwrap();
5834                    assert!(same_points(curve, window, scan_cubic_window(curve, *i)));
5835                }
5836            }
5837            Err(e) => assert_eq!(format!("{:?}", pair.unwrap_err()), format!("{e:?}")),
5838        }
5839
5840        let filtered: Vec<&Point2D> = curve.points.iter().filter(|p| p.x == x).collect();
5841        assert_eq!(curve.points_at(x).collect::<Vec<_>>(), filtered);
5842        assert_eq!(curve.contains_point(&x), !filtered.is_empty());
5843        assert_eq!(curve.get_point(&x), filtered.first().copied());
5844        assert_eq!(
5845            curve.get_values(x),
5846            filtered.iter().map(|p| &p.y).collect::<Vec<_>>()
5847        );
5848        let exact = match filtered[..] {
5849            [] => Ok(None),
5850            [only] => Ok(Some(*only)),
5851            _ => Err(InterpolationError::DegenerateInterval),
5852        };
5853        assert_eq!(
5854            format!("{:?}", curve.exact_point_at(x)),
5855            format!("{exact:?}")
5856        );
5857    }
5858
5859    /// Every sample, every midpoint, both ends and beyond them.
5860    fn probes(curve: &Curve) -> Vec<Decimal> {
5861        let mut xs = vec![dec!(-1000), dec!(1000)];
5862        let abscissas: Vec<Decimal> = curve.points.iter().map(|p| p.x).collect();
5863        for pair in abscissas.windows(2) {
5864            xs.push((pair[0] + pair[1]) / dec!(2));
5865        }
5866        for x in abscissas {
5867            xs.extend([x, x - dec!(0.001), x + dec!(0.001)]);
5868        }
5869        xs
5870    }
5871
5872    fn curve_of(points: &[(i64, i64)]) -> Curve {
5873        Curve::new(
5874            points
5875                .iter()
5876                .map(|&(x, y)| Point2D::new(Decimal::from(x), Decimal::from(y)))
5877                .collect(),
5878        )
5879    }
5880
5881    #[test]
5882    fn edge_curves() {
5883        let cases: Vec<Vec<(i64, i64)>> = vec![
5884            vec![],
5885            vec![(1, 1)],
5886            vec![(1, 1), (1, 2)],
5887            vec![(0, 0), (1, 1)],
5888            vec![(0, 0), (1, 1), (2, 4), (3, 9)],
5889            vec![(0, 0), (0, 5), (1, 1), (2, 4), (3, 9)],
5890            vec![(0, 0), (1, 1), (1, 3), (1, 7), (2, 4), (3, 9)],
5891            vec![(0, 0), (1, 1), (2, 4), (3, 9), (3, 10)],
5892            vec![(0, 0), (0, 1), (0, 2), (0, 3)],
5893            vec![(-3, 2), (-1, 0), (4, 4), (5, -1), (9, 3), (12, 0)],
5894        ];
5895        for points in cases {
5896            let curve = curve_of(&points);
5897            for x in probes(&curve) {
5898                check(&curve, x);
5899            }
5900        }
5901    }
5902
5903    #[test]
5904    fn decimal_scales_compare_by_value() {
5905        // `1.0` and `1` are the same abscissa to both the scan and the tree.
5906        let curve = Curve::new(
5907            [
5908                Point2D::new(dec!(0), dec!(0)),
5909                Point2D::new(dec!(1.0), dec!(1)),
5910                Point2D::new(dec!(2.00), dec!(4)),
5911                Point2D::new(dec!(3), dec!(9)),
5912            ]
5913            .into_iter()
5914            .collect(),
5915        );
5916        for x in [
5917            dec!(1),
5918            dec!(1.000),
5919            dec!(2),
5920            dec!(2.0),
5921            dec!(0.0),
5922            dec!(3.00),
5923        ] {
5924            check(&curve, x);
5925        }
5926    }
5927
5928    #[test]
5929    fn random_curves_with_stacked_abscissas() {
5930        // A fixed-seed generator: small integer coordinates make stacks of
5931        // ordinates at one abscissa common.
5932        let mut state: u64 = 0x2545_f491_4f6c_dd1d;
5933        let mut next = |bound: u64| {
5934            state ^= state << 13;
5935            state ^= state >> 7;
5936            state ^= state << 17;
5937            state % bound
5938        };
5939        for _ in 0..2_000 {
5940            let len = next(14) as usize;
5941            let points: Vec<(i64, i64)> = (0..len)
5942                .map(|_| (next(10) as i64 - 3, next(6) as i64 - 2))
5943                .collect();
5944            let curve = curve_of(&points);
5945            for x in probes(&curve) {
5946                check(&curve, x);
5947            }
5948        }
5949    }
5950}
5951
5952/// [`CurveSpline`] and the refactored `spline_interpolate` of #858 against
5953/// the `spline_interpolate` they replace, kept here verbatim as
5954/// `legacy_spline`: the same point or the same error at every probe.
5955#[cfg(test)]
5956mod tests_spline_matches_legacy {
5957    use super::*;
5958
5959    fn legacy_point_at(
5960        curve: &Curve,
5961        index: usize,
5962        kind: fn(String) -> InterpolationError,
5963    ) -> Result<&Point2D, InterpolationError> {
5964        curve.points.iter().nth(index).ok_or_else(|| {
5965            kind(format!(
5966                "point index {index} is out of bounds for a curve of {} points",
5967                curve.points.len()
5968            ))
5969        })
5970    }
5971
5972    fn legacy_exact_point_at(
5973        curve: &Curve,
5974        x: Decimal,
5975    ) -> Result<Option<Point2D>, InterpolationError> {
5976        let mut at_x = curve.points.iter().filter(|p| p.x == x);
5977        let Some(point) = at_x.next() else {
5978            return Ok(None);
5979        };
5980        if at_x.next().is_some() {
5981            return Err(InterpolationError::DegenerateInterval);
5982        }
5983        Ok(Some(*point))
5984    }
5985
5986    /// `Curve::spline_interpolate` as it was before #858.
5987    #[rustfmt::skip]
5988fn legacy_spline(curve: &Curve, x: Decimal) -> Result<Point2D, InterpolationError> {
5989    let len = curve.len();
5990
5991    // Need at least 3 points for spline interpolation
5992    if len < 3 {
5993        return Err(InterpolationError::Spline(
5994            "Need at least three points for spline interpolation".to_string(),
5995        ));
5996    }
5997
5998    // Check if x is within the valid range
5999    let first = legacy_point_at(curve, 0, InterpolationError::Spline)?;
6000    let spline = InterpolationError::Spline;
6001    let last_index = shift_index(len, -1, spline)?;
6002    let last = legacy_point_at(curve, last_index, spline)?;
6003    if x < first.x || x > last.x {
6004        return Err(InterpolationError::Spline(
6005            "x is outside the range of points".to_string(),
6006        ));
6007    }
6008
6009    // For exact points, return the actual point value. A stack of
6010    // ordinates at `x` has no single value, so it errors out instead of
6011    // yielding the lowest of them.
6012    if let Some(point) = legacy_exact_point_at(curve, x)? {
6013        return Ok(point);
6014    }
6015
6016    let n = len;
6017    let pts: Vec<&Point2D> = curve.points.iter().collect();
6018    let at = |index: usize| -> Result<&Point2D, InterpolationError> {
6019        pts.get(index).copied().ok_or_else(|| {
6020            InterpolationError::Spline(format!(
6021                "point index {index} is out of bounds for a curve of {n} points"
6022            ))
6023        })
6024    };
6025
6026    // Calculate second derivatives. The three interior bands and the
6027    // right-hand side are built in order, with the natural-spline
6028    // boundary rows (`b[0] = b[n-1] = 1`) pushed at the ends.
6029    let mut a = vec![Decimal::ZERO];
6030    let mut b = vec![Decimal::ONE];
6031    let mut c = vec![Decimal::ZERO];
6032    let mut r = vec![Decimal::ZERO];
6033
6034    // Fill the matrices
6035    for i in 1..last_index {
6036        let prev = at(shift_index(i, -1, spline)?)?;
6037        let curr = at(i)?;
6038        let next = at(shift_index(i, 1, spline)?)?;
6039
6040        let hi = d_sub(curr.x, prev.x, "Curve::spline_interpolate::hi")
6041            .map_err(interp_err(InterpolationError::Spline))?;
6042        let hi1 = d_sub(next.x, curr.x, "Curve::spline_interpolate::hi1")
6043            .map_err(interp_err(InterpolationError::Spline))?;
6044        // A repeated abscissa collapses a knot interval; the second
6045        // derivative there is undefined rather than infinite.
6046        if hi.is_zero() || hi1.is_zero() {
6047            return Err(InterpolationError::DegenerateInterval);
6048        }
6049
6050        let band = d_add(hi, hi1, "Curve::spline_interpolate::band")
6051            .map_err(interp_err(InterpolationError::Spline))?;
6052        let diag = d_mul(dec!(2), band, "Curve::spline_interpolate::diag")
6053            .map_err(interp_err(InterpolationError::Spline))?;
6054
6055        let rise_next = d_sub(next.y, curr.y, "Curve::spline_interpolate::rise_next")
6056            .map_err(interp_err(InterpolationError::Spline))?;
6057        let slope_next = d_div(rise_next, hi1, "Curve::spline_interpolate::slope_next")
6058            .map_err(interp_err(InterpolationError::Spline))?;
6059        let rise_prev = d_sub(curr.y, prev.y, "Curve::spline_interpolate::rise_prev")
6060            .map_err(interp_err(InterpolationError::Spline))?;
6061        let slope_prev = d_div(rise_prev, hi, "Curve::spline_interpolate::slope_prev")
6062            .map_err(interp_err(InterpolationError::Spline))?;
6063        let curvature = d_sub(
6064            slope_next,
6065            slope_prev,
6066            "Curve::spline_interpolate::curvature",
6067        )
6068        .map_err(interp_err(InterpolationError::Spline))?;
6069        let rhs = d_mul(dec!(6), curvature, "Curve::spline_interpolate::rhs")
6070            .map_err(interp_err(InterpolationError::Spline))?;
6071
6072        a.push(hi);
6073        b.push(diag);
6074        c.push(hi1);
6075        r.push(rhs);
6076    }
6077
6078    // Add boundary conditions (natural spline)
6079    a.push(Decimal::ZERO);
6080    b.push(Decimal::ONE);
6081    c.push(Decimal::ZERO);
6082    r.push(Decimal::ZERO);
6083
6084    // Solve tridiagonal system using Thomas algorithm
6085    let mut m = vec![Decimal::ZERO; n];
6086
6087    for i in 1..last_index {
6088        let prev = shift_index(i, -1, spline)?;
6089        let a_i = band_at(&a, i, "a")?;
6090        let b_prev = band_at(&b, prev, "b")?;
6091        if b_prev.is_zero() {
6092            return Err(InterpolationError::DegenerateInterval);
6093        }
6094        let w = d_div(a_i, b_prev, "Curve::spline_interpolate::w")
6095            .map_err(interp_err(InterpolationError::Spline))?;
6096
6097        let c_prev = band_at(&c, prev, "c")?;
6098        let wc = d_mul(w, c_prev, "Curve::spline_interpolate::wc")
6099            .map_err(interp_err(InterpolationError::Spline))?;
6100        let b_i = band_at(&b, i, "b")?;
6101        *band_at_mut(&mut b, i, "b")? = d_sub(b_i, wc, "Curve::spline_interpolate::b_i")
6102            .map_err(interp_err(InterpolationError::Spline))?;
6103
6104        let r_prev = band_at(&r, prev, "r")?;
6105        let wr = d_mul(w, r_prev, "Curve::spline_interpolate::wr")
6106            .map_err(interp_err(InterpolationError::Spline))?;
6107        let r_i = band_at(&r, i, "r")?;
6108        *band_at_mut(&mut r, i, "r")? = d_sub(r_i, wr, "Curve::spline_interpolate::r_i")
6109            .map_err(interp_err(InterpolationError::Spline))?;
6110    }
6111
6112    let b_last = band_at(&b, last_index, "b")?;
6113    if b_last.is_zero() {
6114        return Err(InterpolationError::DegenerateInterval);
6115    }
6116    let r_last = band_at(&r, last_index, "r")?;
6117    *band_at_mut(&mut m, last_index, "m")? =
6118        d_div(r_last, b_last, "Curve::spline_interpolate::m_last")
6119            .map_err(interp_err(InterpolationError::Spline))?;
6120    for i in (1..last_index).rev() {
6121        let c_i = band_at(&c, i, "c")?;
6122        let m_next = band_at(&m, shift_index(i, 1, spline)?, "m")?;
6123        let cm = d_mul(c_i, m_next, "Curve::spline_interpolate::cm")
6124            .map_err(interp_err(InterpolationError::Spline))?;
6125        let r_i = band_at(&r, i, "r")?;
6126        let numerator = d_sub(r_i, cm, "Curve::spline_interpolate::m_numerator")
6127            .map_err(interp_err(InterpolationError::Spline))?;
6128        let b_i = band_at(&b, i, "b")?;
6129        if b_i.is_zero() {
6130            return Err(InterpolationError::DegenerateInterval);
6131        }
6132        *band_at_mut(&mut m, i, "m")? = d_div(numerator, b_i, "Curve::spline_interpolate::m_i")
6133            .map_err(interp_err(InterpolationError::Spline))?;
6134    }
6135
6136    // Find segment for interpolation
6137    let mut segment = None;
6138    for i in 0..last_index {
6139        if at(i)?.x <= x && x <= at(shift_index(i, 1, spline)?)?.x {
6140            segment = Some(i);
6141            break;
6142        }
6143    }
6144
6145    let segment = segment.ok_or_else(|| {
6146        InterpolationError::Spline("Could not find valid segment for interpolation".to_string())
6147    })?;
6148
6149    // Calculate interpolated value
6150    let left = at(segment)?;
6151    let right = at(shift_index(segment, 1, spline)?)?;
6152    let h = d_sub(right.x, left.x, "Curve::spline_interpolate::h")
6153        .map_err(interp_err(InterpolationError::Spline))?;
6154    if h.is_zero() {
6155        return Err(InterpolationError::DegenerateInterval);
6156    }
6157    let dx = d_sub(right.x, x, "Curve::spline_interpolate::dx")
6158        .map_err(interp_err(InterpolationError::Spline))?;
6159    let dx1 = d_sub(x, left.x, "Curve::spline_interpolate::dx1")
6160        .map_err(interp_err(InterpolationError::Spline))?;
6161
6162    let m_left = band_at(&m, segment, "m")?;
6163    let m_right = band_at(&m, shift_index(segment, 1, spline)?, "m")?;
6164    let six_h = d_mul(dec!(6), h, "Curve::spline_interpolate::six_h")
6165        .map_err(interp_err(InterpolationError::Spline))?;
6166
6167    let left_cube = cube_scaled(m_left, dx, six_h, "Curve::spline_interpolate::left_cube")?;
6168    let right_cube = cube_scaled(m_right, dx1, six_h, "Curve::spline_interpolate::right_cube")?;
6169    let left_linear = linear_term(left.y, m_left, h, dx, "Curve::spline_interpolate::left")?;
6170    let right_linear =
6171        linear_term(right.y, m_right, h, dx1, "Curve::spline_interpolate::right")?;
6172
6173    let y = d_sum_iter(
6174        [left_cube, right_cube, left_linear, right_linear],
6175        "Curve::spline_interpolate::y",
6176    )
6177    .map_err(interp_err(InterpolationError::Spline))?;
6178
6179    Ok(Point2D::new(x, y))
6180}
6181
6182    fn check(curve: &Curve) {
6183        let spline = CurveSpline::new(curve);
6184        let mut xs = vec![dec!(-1000), dec!(1000), dec!(0.123456789)];
6185        let knots: Vec<Decimal> = curve.points.iter().map(|p| p.x).collect();
6186        for pair in knots.windows(2) {
6187            xs.push((pair[0] + pair[1]) / dec!(2));
6188            xs.push(pair[0] + (pair[1] - pair[0]) / dec!(3));
6189        }
6190        for x in knots {
6191            xs.extend([x, x - dec!(0.001), x + dec!(0.001)]);
6192        }
6193        for x in xs {
6194            let legacy = format!("{:?}", legacy_spline(curve, x));
6195            assert_eq!(
6196                format!("{:?}", curve.spline_interpolate(x)),
6197                legacy,
6198                "spline_interpolate at {x} on {curve:?}"
6199            );
6200            assert_eq!(
6201                format!("{:?}", spline.interpolate(x)),
6202                legacy,
6203                "CurveSpline at {x} on {curve:?}"
6204            );
6205        }
6206    }
6207
6208    fn curve_of(points: &[(Decimal, Decimal)]) -> Curve {
6209        Curve::new(points.iter().map(|&(x, y)| Point2D::new(x, y)).collect())
6210    }
6211
6212    #[test]
6213    fn edge_curves() {
6214        let d = |v: i64| Decimal::from(v);
6215        let cases: Vec<Vec<(Decimal, Decimal)>> = vec![
6216            vec![],
6217            vec![(d(1), d(1))],
6218            vec![(d(0), d(0)), (d(1), d(1))],
6219            vec![(d(0), d(0)), (d(1), d(1)), (d(2), d(4))],
6220            vec![(d(0), d(0)), (d(1), d(1)), (d(1), d(3)), (d(2), d(4))],
6221            vec![(d(0), d(0)), (d(0), d(2)), (d(1), d(1)), (d(2), d(4))],
6222            vec![(d(0), d(0)), (d(1), d(1)), (d(2), d(4)), (d(2), d(5))],
6223            // Long mantissas: the solve rounds at the 28th place.
6224            (0..9)
6225                .map(|i| {
6226                    let x = dec!(10) / dec!(7) * d(i);
6227                    (x, (x - dec!(5)) / dec!(7) * (x - dec!(5)) / dec!(3))
6228                })
6229                .collect(),
6230            // Uneven knots, including a close pair.
6231            vec![
6232                (d(-3), d(2)),
6233                (d(-1), d(0)),
6234                (dec!(-0.999), d(1)),
6235                (d(4), d(4)),
6236                (d(9), d(-3)),
6237            ],
6238        ];
6239        for points in cases {
6240            check(&curve_of(&points));
6241        }
6242    }
6243
6244    #[test]
6245    fn overflowing_solve_reports_the_same_error() {
6246        let curve = curve_of(&[
6247            (dec!(0), dec!(0)),
6248            (dec!(0.0000000000000000000000000001), Decimal::MAX),
6249            (dec!(1), Decimal::MIN),
6250            (dec!(2), dec!(1)),
6251        ]);
6252        assert!(legacy_spline(&curve, dec!(0.5)).is_err());
6253        check(&curve);
6254    }
6255
6256    #[test]
6257    fn random_curves() {
6258        let mut state: u64 = 0xd1b5_4a32_d192_ed03;
6259        let mut next = |bound: u64| {
6260            state ^= state << 13;
6261            state ^= state >> 7;
6262            state ^= state << 17;
6263            state % bound
6264        };
6265        for _ in 0..300 {
6266            let len = next(16) as usize;
6267            let points: Vec<(Decimal, Decimal)> = (0..len)
6268                .map(|_| {
6269                    (
6270                        Decimal::new(next(400) as i64 - 100, 1),
6271                        Decimal::new(next(20_000) as i64 - 10_000, 3),
6272                    )
6273                })
6274                .collect();
6275            check(&curve_of(&points));
6276        }
6277    }
6278
6279    #[test]
6280    fn merge_resampling_matches_per_read_splines() {
6281        // The merge solves each spline once; reading every resampled
6282        // abscissa through `spline_interpolate` gives the same curves.
6283        let a = curve_of(
6284            &(0..12)
6285                .map(|i| (Decimal::from(i), Decimal::from(i * i % 7)))
6286                .collect::<Vec<_>>(),
6287        );
6288        let b = curve_of(
6289            &(0..9)
6290                .map(|i| {
6291                    (
6292                        dec!(0.5) + Decimal::from(i) * dec!(1.3),
6293                        Decimal::from(i % 4),
6294                    )
6295                })
6296                .collect::<Vec<_>>(),
6297        );
6298        let (ra, rb) = a
6299            .merge_axis_interpolate(&b, InterpolationType::Spline)
6300            .unwrap();
6301        for (resampled, source) in [(&ra, &a), (&rb, &b)] {
6302            for p in &resampled.points {
6303                let want = match source.get_point(&p.x) {
6304                    Some(point) => *point,
6305                    None => legacy_spline(source, p.x).unwrap(),
6306                };
6307                assert_eq!(*p, want);
6308            }
6309        }
6310    }
6311}
6312
6313/// The windowed `intersect_with` of #858 against the all-pairs comparison it
6314/// replaced, `intersect_all_pairs`: the same points in the same order and the
6315/// same error.
6316#[cfg(test)]
6317mod tests_intersect_matches_all_pairs {
6318    use super::*;
6319
6320    fn check(a: &Curve, b: &Curve) {
6321        for (left, right) in [(a, b), (b, a), (a, a)] {
6322            let fast = left.intersect_with(right);
6323            let all = intersect_all_pairs(left, right, Decimal::new(1, 6));
6324            assert_eq!(
6325                format!("{fast:?}"),
6326                format!("{all:?}"),
6327                "{left:?} with {right:?}"
6328            );
6329        }
6330    }
6331
6332    fn curve_of(points: &[(Decimal, Decimal)]) -> Curve {
6333        Curve::new(points.iter().map(|&(x, y)| Point2D::new(x, y)).collect())
6334    }
6335
6336    #[test]
6337    fn samples_within_tolerance_are_matched_in_pair_order() {
6338        let tiny = dec!(0.0000005);
6339        let a = curve_of(&[
6340            (dec!(0), dec!(1)),
6341            (dec!(1), dec!(2)),
6342            (dec!(1), dec!(5)),
6343            (dec!(2), dec!(3)),
6344        ]);
6345        let b = curve_of(&[
6346            (dec!(0) + tiny, dec!(1) - tiny),
6347            (dec!(1) - tiny, dec!(2)),
6348            (dec!(1), dec!(2) + tiny),
6349            (dec!(1) + dec!(0.000001), dec!(5)),
6350            (dec!(2.000001), dec!(3)),
6351        ]);
6352        check(&a, &b);
6353        let found = match a.intersect_with(&b) {
6354            Ok(found) => found,
6355            Err(e) => panic!("intersect_with failed: {e:?}"),
6356        };
6357        // `(1, 2)` matches two samples of `b`; `(1, 5)` and `(2, 3)` sit
6358        // exactly `1e-6` away, which is not within tolerance.
6359        assert_eq!(
6360            found,
6361            vec![
6362                Point2D::new(dec!(0), dec!(1)),
6363                Point2D::new(dec!(1), dec!(2)),
6364                Point2D::new(dec!(1), dec!(2)),
6365            ]
6366        );
6367    }
6368
6369    #[test]
6370    fn crossings_between_samples_are_not_reported() {
6371        let rising = curve_of(&[(dec!(0), dec!(0)), (dec!(2), dec!(2))]);
6372        let falling = curve_of(&[(dec!(0), dec!(2)), (dec!(2), dec!(0))]);
6373        assert!(matches!(rising.intersect_with(&falling), Ok(found) if found.is_empty()));
6374    }
6375
6376    #[test]
6377    fn empty_and_disjoint_curves() {
6378        let empty = Curve::default();
6379        let a = curve_of(&[(dec!(0), dec!(0)), (dec!(1), dec!(1))]);
6380        let far = curve_of(&[(dec!(10), dec!(0)), (dec!(11), dec!(1))]);
6381        check(&empty, &a);
6382        check(&a, &far);
6383        check(&empty, &empty);
6384    }
6385
6386    #[test]
6387    fn overflowing_abscissa_difference_reports_the_same_pair() {
6388        let a = curve_of(&[
6389            (dec!(-70000000000000000000000000000), dec!(0)),
6390            (dec!(0), dec!(0)),
6391        ]);
6392        let b = curve_of(&[
6393            (dec!(0), dec!(0)),
6394            (dec!(70000000000000000000000000000), dec!(1)),
6395        ]);
6396        assert!(a.intersect_with(&b).is_err());
6397        check(&a, &b);
6398        // Ordinates that overflow only matter on a pair within tolerance.
6399        let c = curve_of(&[
6400            (dec!(0), dec!(-70000000000000000000000000000)),
6401            (dec!(1), dec!(0)),
6402        ]);
6403        let d = curve_of(&[
6404            (dec!(0), dec!(70000000000000000000000000000)),
6405            (dec!(1), dec!(0)),
6406        ]);
6407        assert!(c.intersect_with(&d).is_err());
6408        check(&c, &d);
6409    }
6410
6411    #[test]
6412    fn random_curves_with_stacks_and_near_misses() {
6413        let mut state: u64 = 0xa076_1d64_78bd_642f;
6414        let mut next = |bound: u64| {
6415            state ^= state << 13;
6416            state ^= state >> 7;
6417            state ^= state << 17;
6418            state % bound
6419        };
6420        // Abscissas on a 5e-7 grid, so neighbours fall inside, on and
6421        // outside the 1e-6 tolerance.
6422        let curve = |next: &mut dyn FnMut(u64) -> u64| {
6423            let len = next(30) as usize;
6424            let points: Vec<(Decimal, Decimal)> = (0..len)
6425                .map(|_| {
6426                    (
6427                        Decimal::new(next(12) as i64, 7) * dec!(5),
6428                        Decimal::new(next(4) as i64, 7) * dec!(5),
6429                    )
6430                })
6431                .collect();
6432            curve_of(&points)
6433        };
6434        for _ in 0..1_000 {
6435            let a = curve(&mut next);
6436            let b = curve(&mut next);
6437            check(&a, &b);
6438        }
6439    }
6440}