Skip to main content

numra_dde/
system.rs

1//! DDE system trait and solver infrastructure.
2//!
3//! Author: Moussa Leblouba
4//! Date: 4 February 2026
5//! Modified: 2 May 2026
6
7use numra_core::Scalar;
8
9/// Trait for delay differential equation systems.
10///
11/// Defines a DDE of the form:
12/// ```text
13/// y'(t) = f(t, y(t), y(t - τ₁), y(t - τ₂), ...)
14/// ```
15pub trait DdeSystem<S: Scalar> {
16    /// Dimension of the state space.
17    fn dim(&self) -> usize;
18
19    /// Get the delays τᵢ.
20    ///
21    /// For constant delays, this returns a fixed vector.
22    /// For state-dependent delays, implement `delays_at` instead.
23    fn delays(&self) -> Vec<S>;
24
25    /// Get delays at a specific state (for state-dependent delays).
26    ///
27    /// Default implementation returns constant delays.
28    fn delays_at(&self, _t: S, _y: &[S]) -> Vec<S> {
29        self.delays()
30    }
31
32    /// Number of delays.
33    fn n_delays(&self) -> usize {
34        self.delays().len()
35    }
36
37    /// Evaluate the right-hand side f(t, y(t), y(t-τ₁), ...).
38    ///
39    /// # Arguments
40    /// * `t` - Current time
41    /// * `y` - Current state y(t)
42    /// * `y_delayed` - Delayed states: `y_delayed[i] = y(t - τᵢ)`
43    /// * `dydt` - Output buffer for y'(t)
44    fn rhs(&self, t: S, y: &[S], y_delayed: &[&[S]], dydt: &mut [S]);
45
46    /// Whether delays are state-dependent.
47    fn has_state_dependent_delays(&self) -> bool {
48        false
49    }
50}
51
52/// Options for DDE solvers.
53///
54/// **Divergence from `numra_ode::SolverOptions`** (per Foundation Spec §2.5):
55/// the field set most closely mirrors `SolverOptions` (`rtol`, `atol`, `h0`,
56/// `h_max`, `h_min`, `max_steps`, `t_eval`, `dense_output`) but diverges on
57/// two DDE-specific concerns: (1) `dense_output` defaults to `true` here
58/// because the Method-of-Steps solver evaluates `y(t − τ)` at delay-shifted
59/// times that don't generally land on integration grid points; (2)
60/// `track_discontinuities` + `discontinuity_order` encode the inherent DDE
61/// discontinuity-propagation structure (initial-history non-smoothness
62/// propagates forward at integer multiples of the delays), which has no
63/// ODE analog. See `docs/architecture/foundation-specification.md` §2.5.
64#[derive(Clone, Debug)]
65pub struct DdeOptions<S: Scalar> {
66    /// Relative tolerance
67    pub rtol: S,
68    /// Absolute tolerance
69    pub atol: S,
70    /// Initial step size (None = auto)
71    pub h0: Option<S>,
72    /// Maximum step size
73    pub h_max: S,
74    /// Minimum step size
75    pub h_min: S,
76    /// Maximum number of steps
77    pub max_steps: usize,
78    /// Save solution at these times (None = save all steps)
79    pub t_eval: Option<Vec<S>>,
80    /// Enable dense output (required for accurate history interpolation)
81    pub dense_output: bool,
82    /// Track discontinuities in the solution
83    pub track_discontinuities: bool,
84    /// Track discontinuities up to this derivative order
85    pub discontinuity_order: usize,
86}
87
88impl<S: Scalar> Default for DdeOptions<S> {
89    fn default() -> Self {
90        Self {
91            rtol: S::from_f64(1e-6),
92            atol: S::from_f64(1e-9),
93            h0: None,
94            h_max: S::INFINITY,
95            h_min: S::from_f64(1e-14),
96            max_steps: 100_000,
97            t_eval: None,
98            dense_output: true, // Usually needed for DDEs
99            track_discontinuities: false,
100            discontinuity_order: 0,
101        }
102    }
103}
104
105impl<S: Scalar> DdeOptions<S> {
106    pub fn rtol(mut self, rtol: S) -> Self {
107        self.rtol = rtol;
108        self
109    }
110
111    pub fn atol(mut self, atol: S) -> Self {
112        self.atol = atol;
113        self
114    }
115
116    pub fn h0(mut self, h0: S) -> Self {
117        self.h0 = Some(h0);
118        self
119    }
120
121    pub fn h_max(mut self, h_max: S) -> Self {
122        self.h_max = h_max;
123        self
124    }
125
126    pub fn max_steps(mut self, max_steps: usize) -> Self {
127        self.max_steps = max_steps;
128        self
129    }
130
131    /// Enable discontinuity tracking.
132    ///
133    /// When enabled, the solver will detect and step exactly to
134    /// discontinuity points that propagate from t0 at each delay interval.
135    pub fn track_discontinuities(mut self, track: bool) -> Self {
136        self.track_discontinuities = track;
137        self
138    }
139
140    /// Set the discontinuity order to track.
141    ///
142    /// Discontinuities propagate: the initial discontinuity at t0 propagates
143    /// to t0 + tau for each delay tau. Setting order=3 tracks up to 3 levels
144    /// of propagation (e.g., t0, t0+tau, t0+2*tau, t0+3*tau for single delay).
145    pub fn discontinuity_order(mut self, order: usize) -> Self {
146        self.discontinuity_order = order;
147        self
148    }
149}
150
151/// Statistics from DDE solver.
152#[derive(Clone, Debug, Default)]
153pub struct DdeStats {
154    /// Number of RHS evaluations
155    pub n_eval: usize,
156    /// Number of accepted steps
157    pub n_accept: usize,
158    /// Number of rejected steps
159    pub n_reject: usize,
160    /// Number of discontinuity crossings handled
161    pub n_discontinuities: usize,
162}
163
164/// Result of DDE integration.
165#[derive(Clone, Debug)]
166pub struct DdeResult<S: Scalar> {
167    /// Time points
168    pub t: Vec<S>,
169    /// Solution at each time point (row-major: y[i*dim + j] = y_j(t_i))
170    pub y: Vec<S>,
171    /// Dimension of the system
172    pub dim: usize,
173    /// Solver statistics
174    pub stats: DdeStats,
175    /// Was integration successful?
176    pub success: bool,
177    /// Message (error description if failed)
178    pub message: String,
179}
180
181impl<S: Scalar> DdeResult<S> {
182    pub fn new(t: Vec<S>, y: Vec<S>, dim: usize, stats: DdeStats) -> Self {
183        Self {
184            t,
185            y,
186            dim,
187            stats,
188            success: true,
189            message: String::new(),
190        }
191    }
192
193    pub fn failed(message: String, stats: DdeStats) -> Self {
194        Self {
195            t: Vec::new(),
196            y: Vec::new(),
197            dim: 0,
198            stats,
199            success: false,
200            message,
201        }
202    }
203
204    pub fn len(&self) -> usize {
205        self.t.len()
206    }
207
208    pub fn is_empty(&self) -> bool {
209        self.t.is_empty()
210    }
211
212    pub fn t_final(&self) -> Option<S> {
213        self.t.last().copied()
214    }
215
216    pub fn y_final(&self) -> Option<Vec<S>> {
217        if self.t.is_empty() {
218            None
219        } else {
220            let start = (self.t.len() - 1) * self.dim;
221            Some(self.y[start..start + self.dim].to_vec())
222        }
223    }
224
225    pub fn y_at(&self, i: usize) -> &[S] {
226        let start = i * self.dim;
227        &self.y[start..start + self.dim]
228    }
229
230    pub fn iter(&self) -> impl Iterator<Item = (S, &[S])> {
231        self.t
232            .iter()
233            .enumerate()
234            .map(move |(i, &t)| (t, self.y_at(i)))
235    }
236}
237
238/// Trait for DDE solvers.
239pub trait DdeSolver<S: Scalar> {
240    /// Solve the DDE problem.
241    ///
242    /// # Arguments
243    /// * `system` - The DDE system to solve
244    /// * `t0` - Initial time
245    /// * `tf` - Final time
246    /// * `history` - History function φ(t) for t ≤ t0
247    /// * `options` - Solver options
248    fn solve<Sys, H>(
249        system: &Sys,
250        t0: S,
251        tf: S,
252        history: &H,
253        options: &DdeOptions<S>,
254    ) -> Result<DdeResult<S>, String>
255    where
256        Sys: DdeSystem<S>,
257        H: Fn(S) -> Vec<S>;
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263
264    struct TestDde;
265
266    impl DdeSystem<f64> for TestDde {
267        fn dim(&self) -> usize {
268            1
269        }
270        fn delays(&self) -> Vec<f64> {
271            vec![1.0]
272        }
273        fn rhs(&self, _t: f64, y: &[f64], y_delayed: &[&[f64]], dydt: &mut [f64]) {
274            dydt[0] = -y[0] + y_delayed[0][0];
275        }
276    }
277
278    #[test]
279    fn test_dde_system_trait() {
280        let sys = TestDde;
281        assert_eq!(sys.dim(), 1);
282        assert_eq!(sys.n_delays(), 1);
283        assert_eq!(sys.delays(), vec![1.0]);
284
285        let mut dydt = [0.0];
286        let y_delayed = [&[0.5][..]];
287        sys.rhs(0.0, &[1.0], &y_delayed, &mut dydt);
288        assert!((dydt[0] - (-0.5)).abs() < 1e-10);
289    }
290
291    #[test]
292    fn test_dde_options() {
293        let opts: DdeOptions<f64> = DdeOptions::default().rtol(1e-8).atol(1e-10);
294        assert!((opts.rtol - 1e-8).abs() < 1e-15);
295        assert!((opts.atol - 1e-10).abs() < 1e-15);
296    }
297}