Skip to main content

numra_ode/
problem.rs

1//! ODE problem definition.
2//!
3//! This module defines the traits and types for representing ODE initial value problems.
4//! Also supports DAEs (Differential-Algebraic Equations) via mass matrices.
5//!
6//! Author: Moussa Leblouba
7//! Date: 4 February 2026
8//! Modified: 2 May 2026
9
10use numra_core::Scalar;
11
12/// A system of ordinary differential equations.
13///
14/// Represents: dy/dt = f(t, y)
15pub trait OdeSystem<S: Scalar> {
16    /// Dimension of the system.
17    fn dim(&self) -> usize;
18
19    /// Compute the right-hand side: dydt = f(t, y)
20    fn rhs(&self, t: S, y: &[S], dydt: &mut [S]);
21
22    /// Optionally compute the Jacobian: J = ∂f/∂y
23    /// Default implementation uses finite differences.
24    fn jacobian(&self, t: S, y: &[S], jac: &mut [S]) {
25        let n = self.dim();
26        let eps = S::from_f64(1e-8);
27        let mut y_pert = y.to_vec();
28        let mut f0 = vec![S::ZERO; n];
29        let mut f1 = vec![S::ZERO; n];
30
31        self.rhs(t, y, &mut f0);
32
33        for j in 0..n {
34            let yj_save = y_pert[j];
35            let h = eps * (S::ONE + yj_save.abs());
36            y_pert[j] = yj_save + h;
37            self.rhs(t, &y_pert, &mut f1);
38            y_pert[j] = yj_save;
39
40            for i in 0..n {
41                jac[i * n + j] = (f1[i] - f0[i]) / h;
42            }
43        }
44    }
45
46    /// Is the system autonomous? (f does not depend on t explicitly)
47    fn is_autonomous(&self) -> bool {
48        false
49    }
50
51    /// Does this system have a mass matrix?
52    fn has_mass_matrix(&self) -> bool {
53        false
54    }
55
56    /// Get the mass matrix M for the DAE: M * y' = f(t, y)
57    ///
58    /// Default returns identity (standard ODE).
59    /// For DAEs, override to return the (possibly singular) mass matrix.
60    ///
61    /// The matrix is stored in row-major order: mass[i * n + j] = M[i, j]
62    fn mass_matrix(&self, mass: &mut [S]) {
63        let n = self.dim();
64        for i in 0..n {
65            for j in 0..n {
66                mass[i * n + j] = if i == j { S::ONE } else { S::ZERO };
67            }
68        }
69    }
70
71    /// Is the mass matrix singular? (i.e., is this a DAE?)
72    fn is_singular_mass(&self) -> bool {
73        false
74    }
75
76    /// Return indices of algebraic variables (where `M[i,i] = 0`).
77    ///
78    /// Default returns empty (all differential).
79    fn algebraic_indices(&self) -> Vec<usize> {
80        Vec::new()
81    }
82}
83
84/// ODE initial value problem.
85///
86/// Represents: dy/dt = f(t, y), y(t0) = y0, solve from t0 to tf
87pub struct OdeProblem<S: Scalar, F>
88where
89    F: Fn(S, &[S], &mut [S]),
90{
91    /// Right-hand side function
92    pub f: F,
93    /// Initial time
94    pub t0: S,
95    /// Final time
96    pub tf: S,
97    /// Initial state
98    pub y0: Vec<S>,
99}
100
101impl<S: Scalar, F> OdeProblem<S, F>
102where
103    F: Fn(S, &[S], &mut [S]),
104{
105    /// Create a new ODE problem.
106    pub fn new(f: F, t0: S, tf: S, y0: Vec<S>) -> Self {
107        Self { f, t0, tf, y0 }
108    }
109
110    /// Dimension of the system.
111    pub fn dim(&self) -> usize {
112        self.y0.len()
113    }
114
115    /// Time span.
116    pub fn tspan(&self) -> (S, S) {
117        (self.t0, self.tf)
118    }
119
120    /// Integration direction: 1 for forward, -1 for backward.
121    pub fn direction(&self) -> S {
122        if self.tf >= self.t0 {
123            S::ONE
124        } else {
125            -S::ONE
126        }
127    }
128}
129
130impl<S: Scalar, F> OdeSystem<S> for OdeProblem<S, F>
131where
132    F: Fn(S, &[S], &mut [S]),
133{
134    fn dim(&self) -> usize {
135        self.y0.len()
136    }
137
138    fn rhs(&self, t: S, y: &[S], dydt: &mut [S]) {
139        (self.f)(t, y, dydt);
140    }
141}
142
143/// DAE (Differential-Algebraic Equation) problem.
144///
145/// Represents: M * y' = f(t, y)
146///
147/// where M is a (possibly singular) mass matrix.
148/// - When M is nonsingular, this is equivalent to y' = M⁻¹ f(t, y)
149/// - When M is singular (index-1 DAE), some equations are algebraic constraints
150///
151/// # Example: Pendulum DAE
152///
153/// The pendulum in Cartesian coordinates:
154/// ```text
155/// x'' = -λx         (x-component of constraint force)
156/// y'' = -λy - g     (y-component + gravity)
157/// x² + y² = L²      (constraint: pendulum length)
158/// ```
159///
160/// Written as first-order DAE with y = [x, y, vx, vy, λ]:
161/// ```text
162/// x'  = vx           (differential)
163/// y'  = vy           (differential)
164/// vx' = -λx          (differential)
165/// vy' = -λy - g      (differential)
166/// 0   = x² + y² - L² (algebraic)
167/// ```
168pub struct DaeProblem<S: Scalar, F, M>
169where
170    F: Fn(S, &[S], &mut [S]),
171    M: Fn(&mut [S]),
172{
173    /// Right-hand side function
174    pub f: F,
175    /// Mass matrix function
176    pub mass: M,
177    /// Initial time
178    pub t0: S,
179    /// Final time
180    pub tf: S,
181    /// Initial state (must be consistent!)
182    pub y0: Vec<S>,
183    /// Indices of algebraic variables (where `M[i,i] = 0`)
184    pub alg_indices: Vec<usize>,
185}
186
187impl<S: Scalar, F, M> DaeProblem<S, F, M>
188where
189    F: Fn(S, &[S], &mut [S]),
190    M: Fn(&mut [S]),
191{
192    /// Create a new DAE problem.
193    ///
194    /// # Arguments
195    /// * `f` - RHS function
196    /// * `mass` - Mass matrix function (fills row-major array)
197    /// * `t0` - Initial time
198    /// * `tf` - Final time
199    /// * `y0` - Initial state (must satisfy algebraic constraints!)
200    /// * `alg_indices` - Indices of algebraic equations
201    pub fn new(f: F, mass: M, t0: S, tf: S, y0: Vec<S>, alg_indices: Vec<usize>) -> Self {
202        Self {
203            f,
204            mass,
205            t0,
206            tf,
207            y0,
208            alg_indices,
209        }
210    }
211
212    /// Dimension of the system.
213    pub fn dim(&self) -> usize {
214        self.y0.len()
215    }
216}
217
218impl<S: Scalar, F, M> OdeSystem<S> for DaeProblem<S, F, M>
219where
220    F: Fn(S, &[S], &mut [S]),
221    M: Fn(&mut [S]),
222{
223    fn dim(&self) -> usize {
224        self.y0.len()
225    }
226
227    fn rhs(&self, t: S, y: &[S], dydt: &mut [S]) {
228        (self.f)(t, y, dydt);
229    }
230
231    fn has_mass_matrix(&self) -> bool {
232        true
233    }
234
235    fn mass_matrix(&self, mass: &mut [S]) {
236        (self.mass)(mass);
237    }
238
239    fn is_singular_mass(&self) -> bool {
240        !self.alg_indices.is_empty()
241    }
242
243    fn algebraic_indices(&self) -> Vec<usize> {
244        self.alg_indices.clone()
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_ode_problem_creation() {
254        let f = |_t: f64, y: &[f64], dydt: &mut [f64]| {
255            dydt[0] = -y[0];
256        };
257        let problem = OdeProblem::new(f, 0.0, 1.0, vec![1.0]);
258        assert_eq!(problem.dim(), 1);
259        assert_eq!(problem.tspan(), (0.0, 1.0));
260    }
261
262    #[test]
263    fn test_rhs_evaluation() {
264        let f = |_t: f64, y: &[f64], dydt: &mut [f64]| {
265            dydt[0] = -2.0 * y[0];
266        };
267        let problem = OdeProblem::new(f, 0.0, 1.0, vec![1.0]);
268
269        let mut dydt = vec![0.0];
270        problem.rhs(0.0, &[1.0], &mut dydt);
271        assert!((dydt[0] + 2.0).abs() < 1e-10);
272    }
273
274    #[test]
275    fn test_jacobian_finite_diff() {
276        // dy/dt = -2*y, so J = -2
277        let f = |_t: f64, y: &[f64], dydt: &mut [f64]| {
278            dydt[0] = -2.0 * y[0];
279        };
280        let problem = OdeProblem::new(f, 0.0, 1.0, vec![1.0]);
281
282        let mut jac = vec![0.0];
283        problem.jacobian(0.0, &[1.0], &mut jac);
284        assert!((jac[0] + 2.0).abs() < 1e-5);
285    }
286}