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