Skip to main content

scirs2_interpolate/
extrapolation_trait.rs

1//! `Extrapolate` trait — a composable extension point for 1-D interpolators.
2//!
3//! Any type that knows its domain `[x_min, x_max]` and can evaluate an
4//! interior value via `value_at(x)` automatically gets five well-defined
5//! extrapolation modes through the blanket default implementation of
6//! [`Extrapolate::extrapolate`].
7//!
8//! # Extrapolation modes
9//!
10//! | `ExtrapolationBehavior`      | Behaviour outside the domain |
11//! |------------------------|------------------------------|
12//! | `Nearest`              | Clamp `x` to boundary, evaluate |
13//! | `Linear`               | Finite-difference slope at boundary |
14//! | `Polynomial(degree)`   | Neville's algorithm using `degree+1` interior pts |
15//! | `Reflection`           | Mirror `x` about the boundary |
16//! | `Periodic`             | Wrap `x` with period `x_max - x_min` |
17//!
18//! # Example
19//!
20//! ```rust
21//! use scirs2_interpolate::extrapolation_trait::{ExtrapolationBehavior, Extrapolate};
22//!
23//! // A simple linear function on [0, 1].
24//! struct LinearFn;
25//!
26//! impl Extrapolate for LinearFn {
27//!     fn domain(&self) -> (f64, f64) { (0.0, 1.0) }
28//!     fn value_at(&self, x: f64) -> f64 { 2.0 * x }
29//! }
30//!
31//! let f = LinearFn;
32//! let y = f.extrapolate(1.5, &ExtrapolationBehavior::Linear);
33//! assert!((y - 3.0).abs() < 1e-6);
34//! ```
35
36// ─────────────────────────────────────────────────────────────────────────────
37// ExtrapolationBehavior
38// ─────────────────────────────────────────────────────────────────────────────
39
40/// Extrapolation behaviour for queries outside `[x_min, x_max]`.
41///
42/// This enum is deliberately kept simple and orthogonal.  For the richer
43/// physics-informed and ensemble strategies, see [`crate::extrapolation`].
44#[derive(Debug, Clone, PartialEq)]
45pub enum ExtrapolationBehavior {
46    /// Clamp `x` to the nearest boundary value.
47    Nearest,
48    /// Linearly extrapolate using the finite-difference slope at the boundary.
49    ///
50    /// Step size `h = (x_max - x_min) * 1e-6`.
51    Linear,
52    /// Neville polynomial extrapolation of the given degree.
53    ///
54    /// Uses `degree + 1` equally-spaced interior points near the boundary.
55    /// The degree is clamped to 9 (10-point stencil) to avoid blow-up.
56    Polynomial(usize),
57    /// Mirror `x` about the boundary, then evaluate the interior function.
58    Reflection,
59    /// Wrap `x` with period `x_max - x_min`, then evaluate the interior function.
60    Periodic,
61}
62
63// ─────────────────────────────────────────────────────────────────────────────
64// Extrapolate trait
65// ─────────────────────────────────────────────────────────────────────────────
66
67/// Composable extrapolation for 1-D interpolators.
68///
69/// Implementors must provide:
70/// - [`domain`](Extrapolate::domain) — the closed interval `[x_min, x_max]`.
71/// - [`value_at`](Extrapolate::value_at) — interior evaluation (assumed valid
72///   only within the domain; behaviour outside is undefined).
73///
74/// The provided default [`extrapolate`](Extrapolate::extrapolate) method
75/// dispatches to the five modes in [`ExtrapolationBehavior`].
76pub trait Extrapolate {
77    /// Returns `(x_min, x_max)` — the closed interpolation domain.
78    fn domain(&self) -> (f64, f64);
79
80    /// Evaluate the interpolant at `x`.
81    ///
82    /// Callers within this crate guarantee `x_min <= x <= x_max`.
83    fn value_at(&self, x: f64) -> f64;
84
85    /// Evaluate at `x`, applying `mode` for out-of-domain queries.
86    ///
87    /// If `x` is inside the domain the function falls through to `value_at`.
88    fn extrapolate(&self, x: f64, mode: &ExtrapolationBehavior) -> f64 {
89        let (x_min, x_max) = self.domain();
90
91        // In-domain: delegate to interior evaluation.
92        if x >= x_min && x <= x_max {
93            return self.value_at(x);
94        }
95
96        match mode {
97            ExtrapolationBehavior::Nearest => {
98                let clamped = x.max(x_min).min(x_max);
99                self.value_at(clamped)
100            }
101
102            ExtrapolationBehavior::Linear => {
103                let span = x_max - x_min;
104                let h = (span * 1e-6).max(1e-12);
105                if x < x_min {
106                    let slope = (self.value_at(x_min + h) - self.value_at(x_min)) / h;
107                    self.value_at(x_min) + slope * (x - x_min)
108                } else {
109                    let slope = (self.value_at(x_max) - self.value_at(x_max - h)) / h;
110                    self.value_at(x_max) + slope * (x - x_max)
111                }
112            }
113
114            ExtrapolationBehavior::Polynomial(degree) => {
115                // Neville stencil: `n_pts` equally-spaced points taken from
116                // the relevant boundary region (left or right).
117                let n_pts = (*degree + 1).min(10).max(2);
118                let span = x_max - x_min;
119                let stencil_xs: Vec<f64>;
120                let stencil_ys: Vec<f64>;
121                if x < x_min {
122                    let step = span / (n_pts as f64 - 1.0);
123                    stencil_xs = (0..n_pts).map(|i| x_min + i as f64 * step).collect();
124                } else {
125                    let step = span / (n_pts as f64 - 1.0);
126                    stencil_xs = (0..n_pts)
127                        .map(|i| x_max - (n_pts - 1 - i) as f64 * step)
128                        .collect();
129                }
130                stencil_ys = stencil_xs.iter().map(|&xi| self.value_at(xi)).collect();
131                neville_eval(&stencil_xs, &stencil_ys, x)
132            }
133
134            ExtrapolationBehavior::Reflection => {
135                let span = x_max - x_min;
136                let reflected = reflect_into_domain(x, x_min, x_max, span);
137                self.value_at(reflected)
138            }
139
140            ExtrapolationBehavior::Periodic => {
141                let span = x_max - x_min;
142                if span < 1e-30 {
143                    return self.value_at(x_min);
144                }
145                let periodic = ((x - x_min).rem_euclid(span)) + x_min;
146                self.value_at(periodic)
147            }
148        }
149    }
150}
151
152// ─────────────────────────────────────────────────────────────────────────────
153// Helpers
154// ─────────────────────────────────────────────────────────────────────────────
155
156/// Neville's algorithm for polynomial interpolation / extrapolation.
157///
158/// `xs` and `ys` must have the same length ≥ 1.
159/// Returns the value of the unique polynomial passing through all nodes,
160/// evaluated at `x`.
161pub fn neville_eval(xs: &[f64], ys: &[f64], x: f64) -> f64 {
162    let n = xs.len().min(ys.len());
163    if n == 0 {
164        return f64::NAN;
165    }
166    let mut d = ys[..n].to_vec();
167    for k in 1..n {
168        for i in 0..n - k {
169            let den = xs[i] - xs[i + k];
170            if den.abs() > 1e-14 {
171                let num = (x - xs[i + k]) * d[i] - (x - xs[i]) * d[i + 1];
172                d[i] = num / den;
173            }
174            // If denominator is tiny, keep d[i] unchanged (degenerate stencil).
175        }
176    }
177    d[0]
178}
179
180/// Map `x` (possibly outside `[x_min, x_max]`) to a reflected point inside.
181///
182/// The mapping folds `x` with period `2 * span` and mirrors on odd periods.
183pub fn reflect_into_domain(x: f64, x_min: f64, x_max: f64, span: f64) -> f64 {
184    if span < 1e-30 {
185        return x_min;
186    }
187    let norm = (x - x_min) / span;
188    // period = 2: [0,1] is the first half (un-reflected), [1,2] second (reflected)
189    let period = norm.rem_euclid(2.0);
190    let mapped = if period > 1.0 { 2.0 - period } else { period };
191    (mapped * span + x_min).clamp(x_min, x_max)
192}
193
194// ─────────────────────────────────────────────────────────────────────────────
195// Blanket impl for closures
196// ─────────────────────────────────────────────────────────────────────────────
197
198/// Convenience wrapper for using a closure as an `Extrapolate` implementor.
199pub struct ClosureInterpolator<F: Fn(f64) -> f64> {
200    pub x_min: f64,
201    pub x_max: f64,
202    pub f: F,
203}
204
205impl<F: Fn(f64) -> f64> Extrapolate for ClosureInterpolator<F> {
206    fn domain(&self) -> (f64, f64) {
207        (self.x_min, self.x_max)
208    }
209    fn value_at(&self, x: f64) -> f64 {
210        (self.f)(x)
211    }
212}