Skip to main content

numra_dde/
history.rs

1//! History management and interpolation for DDEs.
2//!
3//! Provides efficient storage and interpolation of solution history
4//! for evaluating delayed terms y(t - τ).
5//!
6//! Author: Moussa Leblouba
7//! Date: 2 February 2026
8//! Modified: 2 May 2026
9
10use numra_core::Scalar;
11
12/// Trait for history functions.
13pub trait HistoryFunction<S: Scalar>: Fn(S) -> Vec<S> {}
14impl<S: Scalar, F: Fn(S) -> Vec<S>> HistoryFunction<S> for F {}
15
16/// A single step's data for history interpolation.
17#[derive(Clone, Debug)]
18pub struct HistoryStep<S: Scalar> {
19    /// Time at start of step
20    pub t: S,
21    /// State at start of step
22    pub y: Vec<S>,
23    /// Derivative at start of step
24    pub f: Vec<S>,
25    /// Time at end of step
26    pub t_next: S,
27    /// State at end of step
28    pub y_next: Vec<S>,
29    /// Derivative at end of step
30    pub f_next: Vec<S>,
31}
32
33impl<S: Scalar> HistoryStep<S> {
34    pub fn new(t: S, y: Vec<S>, f: Vec<S>, t_next: S, y_next: Vec<S>, f_next: Vec<S>) -> Self {
35        Self {
36            t,
37            y,
38            f,
39            t_next,
40            y_next,
41            f_next,
42        }
43    }
44
45    /// Step size h = t_next - t.
46    pub fn h(&self) -> S {
47        self.t_next - self.t
48    }
49
50    /// Check if time t is within this step's interval [t, t_next].
51    pub fn contains(&self, t: S) -> bool {
52        t >= self.t && t <= self.t_next
53    }
54}
55
56/// Hermite cubic interpolator for a single step.
57///
58/// Uses the values and derivatives at both endpoints to construct
59/// a cubic polynomial that matches y and y' at t and t_next.
60pub struct HermiteInterpolator;
61
62impl HermiteInterpolator {
63    /// Interpolate at time t within a step.
64    ///
65    /// Uses cubic Hermite interpolation:
66    /// p(θ) = (1-θ)²(1+2θ) y₀ + θ²(3-2θ) y₁ + h θ(1-θ)² f₀ - h θ²(1-θ) f₁
67    ///
68    /// where θ = (t - t₀) / h.
69    pub fn interpolate<S: Scalar>(step: &HistoryStep<S>, t: S) -> Vec<S> {
70        let h = step.h();
71        if h.abs() < S::from_f64(1e-15) {
72            return step.y.clone();
73        }
74
75        let theta = (t - step.t) / h;
76        let theta2 = theta * theta;
77        let theta3 = theta2 * theta;
78
79        // Hermite basis functions
80        let h00 = S::ONE - S::from_f64(3.0) * theta2 + S::from_f64(2.0) * theta3; // 1 - 3θ² + 2θ³
81        let h10 = theta - S::from_f64(2.0) * theta2 + theta3; // θ - 2θ² + θ³
82        let h01 = S::from_f64(3.0) * theta2 - S::from_f64(2.0) * theta3; // 3θ² - 2θ³
83        let h11 = -theta2 + theta3; // -θ² + θ³
84
85        let dim = step.y.len();
86        let mut result = vec![S::ZERO; dim];
87
88        for i in 0..dim {
89            result[i] = h00 * step.y[i]
90                + h10 * h * step.f[i]
91                + h01 * step.y_next[i]
92                + h11 * h * step.f_next[i];
93        }
94
95        result
96    }
97
98    /// Interpolate derivative at time t within a step.
99    pub fn interpolate_derivative<S: Scalar>(step: &HistoryStep<S>, t: S) -> Vec<S> {
100        let h = step.h();
101        if h.abs() < S::from_f64(1e-15) {
102            return step.f.clone();
103        }
104
105        let theta = (t - step.t) / h;
106        let theta2 = theta * theta;
107
108        // Derivatives of Hermite basis functions (divided by h)
109        let dh00 = -S::from_f64(6.0) * theta + S::from_f64(6.0) * theta2;
110        let dh10 = S::ONE - S::from_f64(4.0) * theta + S::from_f64(3.0) * theta2;
111        let dh01 = S::from_f64(6.0) * theta - S::from_f64(6.0) * theta2;
112        let dh11 = -S::from_f64(2.0) * theta + S::from_f64(3.0) * theta2;
113
114        let dim = step.y.len();
115        let mut result = vec![S::ZERO; dim];
116
117        for i in 0..dim {
118            result[i] = (dh00 * step.y[i]
119                + dh10 * h * step.f[i]
120                + dh01 * step.y_next[i]
121                + dh11 * h * step.f_next[i])
122                / h;
123        }
124
125        result
126    }
127}
128
129/// History storage for DDE solver.
130///
131/// Stores computed solution steps and provides interpolation
132/// for accessing y(t - τ) at arbitrary past times.
133pub struct History<S: Scalar, H: Fn(S) -> Vec<S>> {
134    /// Initial history function for t ≤ t0
135    initial_history: H,
136    /// Initial time
137    t0: S,
138    /// Computed solution steps (sorted by time)
139    steps: Vec<HistoryStep<S>>,
140    /// Dimension of the state
141    dim: usize,
142}
143
144impl<S: Scalar, H: Fn(S) -> Vec<S>> History<S, H> {
145    /// Create a new history with initial function.
146    pub fn new(initial_history: H, t0: S, dim: usize) -> Self {
147        Self {
148            initial_history,
149            t0,
150            steps: Vec::new(),
151            dim,
152        }
153    }
154
155    /// Add a completed step to the history.
156    pub fn add_step(&mut self, step: HistoryStep<S>) {
157        self.steps.push(step);
158    }
159
160    /// Get the state at time t using interpolation.
161    pub fn evaluate(&self, t: S) -> Vec<S> {
162        // If t is before t0, use initial history
163        if t <= self.t0 {
164            return (self.initial_history)(t);
165        }
166
167        // Binary search to find the step containing t
168        match self.find_step(t) {
169            Some(idx) => HermiteInterpolator::interpolate(&self.steps[idx], t),
170            None => {
171                // t is after all recorded steps - extrapolate from last
172                if let Some(last) = self.steps.last() {
173                    if t <= last.t_next {
174                        HermiteInterpolator::interpolate(last, t)
175                    } else {
176                        // Beyond computed region - return last known value
177                        last.y_next.clone()
178                    }
179                } else {
180                    // No steps yet - use initial history at t0
181                    (self.initial_history)(self.t0)
182                }
183            }
184        }
185    }
186
187    /// Get the derivative at time t using interpolation.
188    pub fn evaluate_derivative(&self, t: S) -> Vec<S> {
189        if t <= self.t0 {
190            // Can't easily get derivative from arbitrary history function
191            // Use finite difference approximation
192            let eps = S::from_f64(1e-8);
193            let y1 = (self.initial_history)(t - eps);
194            let y2 = (self.initial_history)(t + eps);
195            let mut deriv = vec![S::ZERO; self.dim];
196            for i in 0..self.dim {
197                deriv[i] = (y2[i] - y1[i]) / (S::from_f64(2.0) * eps);
198            }
199            return deriv;
200        }
201
202        match self.find_step(t) {
203            Some(idx) => HermiteInterpolator::interpolate_derivative(&self.steps[idx], t),
204            None => {
205                if let Some(last) = self.steps.last() {
206                    last.f_next.clone()
207                } else {
208                    vec![S::ZERO; self.dim]
209                }
210            }
211        }
212    }
213
214    /// Find the step index containing time t.
215    fn find_step(&self, t: S) -> Option<usize> {
216        if self.steps.is_empty() {
217            return None;
218        }
219
220        // Binary search
221        let mut lo = 0;
222        let mut hi = self.steps.len();
223
224        while lo < hi {
225            let mid = (lo + hi) / 2;
226            if t < self.steps[mid].t {
227                hi = mid;
228            } else if t > self.steps[mid].t_next {
229                lo = mid + 1;
230            } else {
231                return Some(mid);
232            }
233        }
234
235        // Check boundaries
236        if lo < self.steps.len() && self.steps[lo].contains(t) {
237            Some(lo)
238        } else if lo > 0 && self.steps[lo - 1].contains(t) {
239            Some(lo - 1)
240        } else {
241            None
242        }
243    }
244
245    /// Get the current end time of the computed solution.
246    pub fn current_time(&self) -> S {
247        self.steps.last().map(|s| s.t_next).unwrap_or(self.t0)
248    }
249
250    /// Number of stored steps.
251    pub fn n_steps(&self) -> usize {
252        self.steps.len()
253    }
254
255    /// Clear history (keep initial function).
256    pub fn clear(&mut self) {
257        self.steps.clear();
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_hermite_interpolation() {
267        // Test with known polynomial: y = t², y' = 2t
268        // On [0, 1]: y(0)=0, y'(0)=0, y(1)=1, y'(1)=2
269        let step = HistoryStep {
270            t: 0.0,
271            y: vec![0.0],
272            f: vec![0.0],
273            t_next: 1.0,
274            y_next: vec![1.0],
275            f_next: vec![2.0],
276        };
277
278        // Test at midpoint: y(0.5) should be 0.25
279        let y_mid = HermiteInterpolator::interpolate(&step, 0.5);
280        assert!((y_mid[0] - 0.25).abs() < 1e-10);
281
282        // Test at t=0.25: y(0.25) = 0.0625
283        let y_quarter = HermiteInterpolator::interpolate(&step, 0.25);
284        assert!((y_quarter[0] - 0.0625).abs() < 1e-10);
285    }
286
287    #[test]
288    fn test_hermite_derivative() {
289        // Same test: y = t², y' = 2t
290        let step = HistoryStep {
291            t: 0.0,
292            y: vec![0.0],
293            f: vec![0.0],
294            t_next: 1.0,
295            y_next: vec![1.0],
296            f_next: vec![2.0],
297        };
298
299        // Test derivative at midpoint: y'(0.5) = 1.0
300        let f_mid = HermiteInterpolator::interpolate_derivative(&step, 0.5);
301        assert!((f_mid[0] - 1.0).abs() < 1e-10);
302    }
303
304    #[test]
305    fn test_history_initial() {
306        let history_fn = |t: f64| vec![t.sin()];
307        let history = History::new(history_fn, 0.0, 1);
308
309        // Before t0, should use initial history
310        let y = history.evaluate(-1.0);
311        assert!((y[0] - (-1.0_f64).sin()).abs() < 1e-10);
312    }
313
314    #[test]
315    fn test_history_with_steps() {
316        let history_fn = |_t: f64| vec![1.0];
317        let mut history = History::new(history_fn, 0.0, 1);
318
319        // Add a step: y goes from 1 to 2 with derivative going from 1 to 1
320        // This models y = 1 + t approximately
321        history.add_step(HistoryStep {
322            t: 0.0,
323            y: vec![1.0],
324            f: vec![1.0],
325            t_next: 1.0,
326            y_next: vec![2.0],
327            f_next: vec![1.0],
328        });
329
330        // Interpolate at t = 0.5: should be approximately 1.5
331        let y = history.evaluate(0.5);
332        assert!((y[0] - 1.5).abs() < 0.1); // Hermite won't be exact for linear
333
334        // Before t0
335        let y_pre = history.evaluate(-0.5);
336        assert!((y_pre[0] - 1.0).abs() < 1e-10);
337
338        // At exact step boundary
339        let y_end = history.evaluate(1.0);
340        assert!((y_end[0] - 2.0).abs() < 1e-10);
341    }
342
343    #[test]
344    fn test_history_multiple_steps() {
345        let history_fn = |_t: f64| vec![0.0];
346        let mut history = History::new(history_fn, 0.0, 1);
347
348        // Add multiple steps
349        for i in 0..5 {
350            let t = i as f64;
351            let t_next = (i + 1) as f64;
352            history.add_step(HistoryStep {
353                t,
354                y: vec![t],
355                f: vec![1.0],
356                t_next,
357                y_next: vec![t_next],
358                f_next: vec![1.0],
359            });
360        }
361
362        // Test interpolation at various points
363        assert!((history.evaluate(2.5)[0] - 2.5).abs() < 0.1);
364        assert!((history.evaluate(4.0)[0] - 4.0).abs() < 1e-10);
365    }
366}