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}