Skip to main content

numra_ode/
dae_init.rs

1//! Consistent initialization for DAEs.
2//!
3//! For differential-algebraic equations (DAEs), the initial conditions must satisfy
4//! the algebraic constraints. This module provides functions to compute consistent
5//! initial conditions for index-1 DAEs.
6//!
7//! # Background
8//!
9//! A DAE in semi-explicit form is:
10//! ```text
11//! y₁' = f₁(t, y₁, y₂)    (differential equations)
12//! 0   = g(t, y₁, y₂)      (algebraic constraints)
13//! ```
14//!
15//! Given initial values (y₁⁰, y₂⁰) where y₂⁰ may not satisfy the constraint g = 0,
16//! we solve for y₂ such that g(t₀, y₁⁰, y₂) = 0.
17//!
18//! # Example
19//!
20//! ```rust
21//! use numra_ode::{DaeProblem, compute_consistent_initial};
22//!
23//! // Constraint: y₂ = y₁²
24//! let dae = DaeProblem::new(
25//!     |_t, y: &[f64], dydt: &mut [f64]| {
26//!         dydt[0] = -y[0];                // y₁' = -y₁
27//!         dydt[1] = y[1] - y[0] * y[0];   // 0 = y₂ - y₁² (residual)
28//!     },
29//!     |mass: &mut [f64]| {
30//!         mass[0] = 1.0; mass[1] = 0.0;
31//!         mass[2] = 0.0; mass[3] = 0.0;   // M[1,1] = 0 (algebraic)
32//!     },
33//!     0.0, 1.0,
34//!     vec![2.0, 0.0],  // Inconsistent: y₂ should be 4
35//!     vec![1],         // Index 1 is algebraic
36//! );
37//!
38//! let y0 = compute_consistent_initial(&dae, 0.0, &[2.0, 0.0]).unwrap();
39//! assert!((y0[1] - 4.0).abs() < 1e-10);  // Now consistent
40//! ```
41//!
42//! Author: Moussa Leblouba
43//! Date: 4 February 2026
44//! Modified: 2 May 2026
45
46use crate::OdeSystem;
47use faer::{ComplexField, Conjugate, SimpleEntity};
48use numra_core::Scalar;
49use numra_linalg::{DenseMatrix, LUFactorization};
50
51/// Compute consistent initial conditions for an index-1 DAE.
52///
53/// Given y0 with potentially inconsistent algebraic variables,
54/// solve the algebraic constraints g(t0, y) = 0 to find consistent values.
55///
56/// Uses default tolerance of 1e-10 and max 50 iterations.
57pub fn compute_consistent_initial<S, Sys>(system: &Sys, t0: S, y0: &[S]) -> Result<Vec<S>, String>
58where
59    S: Scalar + SimpleEntity + Conjugate<Canonical = S> + ComplexField,
60    Sys: OdeSystem<S>,
61{
62    compute_consistent_initial_tol(system, t0, y0, S::from_f64(1e-10), 50)
63}
64
65/// Compute consistent initial conditions with user-specified tolerance.
66///
67/// # Algorithm
68///
69/// Uses Newton iteration with a full Jacobian on the algebraic variable subset:
70/// 1. Keep differential variables fixed
71/// 2. Evaluate residual g(t₀, y) on algebraic equations
72/// 3. Build full Jacobian ∂g/∂y_alg via finite differences
73/// 4. Solve J_alg * Δy_alg = -g using LU factorization
74/// 5. Update y_alg += Δy_alg
75/// 6. Repeat until convergence
76///
77/// This handles coupled algebraic constraints correctly (unlike a diagonal
78/// Jacobian approach which fails when constraints share variables).
79pub fn compute_consistent_initial_tol<S, Sys>(
80    system: &Sys,
81    t0: S,
82    y0: &[S],
83    tol: S,
84    max_iter: usize,
85) -> Result<Vec<S>, String>
86where
87    S: Scalar + SimpleEntity + Conjugate<Canonical = S> + ComplexField,
88    Sys: OdeSystem<S>,
89{
90    // If not a DAE (no singular mass matrix), y0 is already consistent
91    if !system.is_singular_mass() {
92        return Ok(y0.to_vec());
93    }
94
95    let alg_indices = system.algebraic_indices();
96    if alg_indices.is_empty() {
97        return Ok(y0.to_vec());
98    }
99
100    let dim = y0.len();
101    let n_alg = alg_indices.len();
102    let mut y = y0.to_vec();
103    let mut f = vec![S::ZERO; dim];
104    let mut f_pert = vec![S::ZERO; dim];
105    let h_factor = S::EPSILON.sqrt();
106
107    for iter in 0..max_iter {
108        // Evaluate the residual f(t, y)
109        system.rhs(t0, &y, &mut f);
110
111        // Check convergence: max residual on algebraic equations
112        let mut max_residual = S::ZERO;
113        for &i in &alg_indices {
114            max_residual = max_residual.max(f[i].abs());
115        }
116
117        if max_residual < tol {
118            return Ok(y);
119        }
120
121        // Build full Jacobian on the algebraic variable subset
122        // J[row, col] = ∂f[alg_indices[row]] / ∂y[alg_indices[col]]
123        let mut jac_data = vec![S::ZERO; n_alg * n_alg];
124
125        for (col, &j) in alg_indices.iter().enumerate() {
126            let y_orig = y[j];
127            let h = h_factor * (S::ONE + y_orig.abs());
128            y[j] = y_orig + h;
129            system.rhs(t0, &y, &mut f_pert);
130            y[j] = y_orig;
131
132            for (row, &i) in alg_indices.iter().enumerate() {
133                jac_data[row * n_alg + col] = (f_pert[i] - f[i]) / h;
134            }
135        }
136
137        // Build RHS: -g (negative residual on algebraic equations)
138        let mut rhs_alg = vec![S::ZERO; n_alg];
139        for (row, &i) in alg_indices.iter().enumerate() {
140            rhs_alg[row] = -f[i];
141        }
142
143        // Solve J_alg * delta = -g using LU factorization
144        let jac_mat = DenseMatrix::from_row_major(n_alg, n_alg, &jac_data);
145        let lu = LUFactorization::new(&jac_mat).map_err(|e| {
146            format!(
147                "DAE Jacobian factorization failed at iteration {}: {}. \
148                 System may be index-2 or higher.",
149                iter, e
150            )
151        })?;
152
153        let delta = lu.solve(&rhs_alg).map_err(|e| {
154            format!(
155                "DAE Jacobian solve failed at iteration {}: {}. \
156                 Jacobian may be singular; system may be index-2 or higher.",
157                iter, e
158            )
159        })?;
160
161        // Update algebraic variables
162        for (idx, &j) in alg_indices.iter().enumerate() {
163            y[j] = y[j] + delta[idx];
164        }
165    }
166
167    Err(format!(
168        "Failed to find consistent initial conditions after {} iterations \
169         (residual still > {})",
170        max_iter,
171        tol.to_f64()
172    ))
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178    use crate::DaeProblem;
179
180    #[test]
181    fn test_dae_consistent_init() {
182        // Simple algebraic constraint: y2 = y1^2
183        let dae = DaeProblem::new(
184            |_t, y: &[f64], dydt: &mut [f64]| {
185                dydt[0] = -y[0];
186                dydt[1] = y[1] - y[0] * y[0];
187            },
188            |mass: &mut [f64]| {
189                mass[0] = 1.0;
190                mass[1] = 0.0;
191                mass[2] = 0.0;
192                mass[3] = 0.0;
193            },
194            0.0,
195            1.0,
196            vec![2.0, 0.0],
197            vec![1],
198        );
199
200        let y0 = compute_consistent_initial(&dae, 0.0, &[2.0, 0.0]).unwrap();
201
202        let constraint = y0[1] - y0[0].powi(2);
203        assert!(
204            constraint.abs() < 1e-10,
205            "Constraint not satisfied: {}",
206            constraint
207        );
208        assert!((y0[0] - 2.0).abs() < 1e-10, "y1 should be unchanged");
209        assert!(
210            (y0[1] - 4.0).abs() < 1e-10,
211            "y2 should be 4.0, got {}",
212            y0[1]
213        );
214    }
215
216    #[test]
217    fn test_dae_consistent_init_already_consistent() {
218        let dae = DaeProblem::new(
219            |_t, y: &[f64], dydt: &mut [f64]| {
220                dydt[0] = -y[0];
221                dydt[1] = y[1] - y[0] * y[0];
222            },
223            |mass: &mut [f64]| {
224                mass[0] = 1.0;
225                mass[1] = 0.0;
226                mass[2] = 0.0;
227                mass[3] = 0.0;
228            },
229            0.0,
230            1.0,
231            vec![2.0, 4.0],
232            vec![1],
233        );
234
235        let y0 = compute_consistent_initial(&dae, 0.0, &[2.0, 4.0]).unwrap();
236        assert!((y0[0] - 2.0).abs() < 1e-10);
237        assert!((y0[1] - 4.0).abs() < 1e-10);
238    }
239
240    #[test]
241    fn test_dae_consistent_init_ode_passthrough() {
242        use crate::OdeProblem;
243
244        let ode = OdeProblem::new(
245            |_t, y: &[f64], dydt: &mut [f64]| {
246                dydt[0] = -y[0];
247            },
248            0.0,
249            1.0,
250            vec![1.0],
251        );
252
253        let y0 = compute_consistent_initial(&ode, 0.0, &[5.0]).unwrap();
254        assert!((y0[0] - 5.0).abs() < 1e-10);
255    }
256
257    #[test]
258    fn test_dae_consistent_init_multiple_algebraic() {
259        // y1' = -y1
260        // 0 = y2 - y1^2  (algebraic)
261        // 0 = y3 - y1^3  (algebraic)
262        let dae = DaeProblem::new(
263            |_t, y: &[f64], dydt: &mut [f64]| {
264                dydt[0] = -y[0];
265                dydt[1] = y[1] - y[0] * y[0];
266                dydt[2] = y[2] - y[0].powi(3);
267            },
268            |mass: &mut [f64]| {
269                mass[0] = 1.0;
270                mass[1] = 0.0;
271                mass[2] = 0.0;
272                mass[3] = 0.0;
273                mass[4] = 0.0;
274                mass[5] = 0.0;
275                mass[6] = 0.0;
276                mass[7] = 0.0;
277                mass[8] = 0.0;
278            },
279            0.0,
280            1.0,
281            vec![3.0, 0.0, 0.0],
282            vec![1, 2],
283        );
284
285        let y0 = compute_consistent_initial(&dae, 0.0, &[3.0, 0.0, 0.0]).unwrap();
286
287        assert!((y0[0] - 3.0).abs() < 1e-10, "y1 unchanged");
288        assert!((y0[1] - 9.0).abs() < 1e-10, "y2 = y1^2 = 9");
289        assert!((y0[2] - 27.0).abs() < 1e-10, "y3 = y1^3 = 27");
290    }
291
292    #[test]
293    fn test_dae_consistent_init_nonlinear_constraint() {
294        let dae = DaeProblem::new(
295            |_t, y: &[f64], dydt: &mut [f64]| {
296                dydt[0] = 1.0;
297                dydt[1] = y[1] - y[0].sin();
298            },
299            |mass: &mut [f64]| {
300                mass[0] = 1.0;
301                mass[1] = 0.0;
302                mass[2] = 0.0;
303                mass[3] = 0.0;
304            },
305            0.0,
306            1.0,
307            vec![1.0, 0.0],
308            vec![1],
309        );
310
311        let y0 = compute_consistent_initial(&dae, 0.0, &[1.0, 0.0]).unwrap();
312        let expected_y2 = 1.0_f64.sin();
313        assert!((y0[0] - 1.0).abs() < 1e-10, "y1 unchanged");
314        assert!(
315            (y0[1] - expected_y2).abs() < 1e-10,
316            "y2 = sin(y1), got {} expected {}",
317            y0[1],
318            expected_y2
319        );
320    }
321
322    #[test]
323    fn test_dae_consistent_init_with_tolerance() {
324        let dae = DaeProblem::new(
325            |_t, y: &[f64], dydt: &mut [f64]| {
326                dydt[0] = -y[0];
327                dydt[1] = y[1] - y[0] * y[0];
328            },
329            |mass: &mut [f64]| {
330                mass[0] = 1.0;
331                mass[1] = 0.0;
332                mass[2] = 0.0;
333                mass[3] = 0.0;
334            },
335            0.0,
336            1.0,
337            vec![2.0, 0.0],
338            vec![1],
339        );
340
341        let y0 = compute_consistent_initial_tol(&dae, 0.0, &[2.0, 0.0], 1e-12, 100).unwrap();
342        let constraint = y0[1] - y0[0].powi(2);
343        assert!(constraint.abs() < 1e-12, "Should satisfy tighter tolerance");
344    }
345
346    #[test]
347    fn test_dae_init_coupled_constraints() {
348        // DAE with two coupled algebraic constraints:
349        // y1' = -y1
350        // 0 = y2 + y3 - 1    (constraint 1)
351        // 0 = y2 - y3        (constraint 2)
352        // Solution: y2 = y3 = 0.5
353        // A diagonal Jacobian cannot solve this because d(g1)/d(y2) and d(g1)/d(y3) both matter.
354        let dae = DaeProblem::new(
355            |_t, y: &[f64], dydt: &mut [f64]| {
356                dydt[0] = -y[0];
357                dydt[1] = y[1] + y[2] - 1.0; // g1 = y2 + y3 - 1
358                dydt[2] = y[1] - y[2]; // g2 = y2 - y3
359            },
360            |mass: &mut [f64]| {
361                mass[0] = 1.0;
362                mass[1] = 0.0;
363                mass[2] = 0.0;
364                mass[3] = 0.0;
365                mass[4] = 0.0;
366                mass[5] = 0.0;
367                mass[6] = 0.0;
368                mass[7] = 0.0;
369                mass[8] = 0.0;
370            },
371            0.0,
372            1.0,
373            vec![1.0, 0.7, 0.2],
374            vec![1, 2],
375        );
376
377        let y0 = compute_consistent_initial(&dae, 0.0, &[1.0, 0.7, 0.2]).unwrap();
378
379        assert!((y0[0] - 1.0).abs() < 1e-10, "y1 unchanged");
380        assert!(
381            (y0[1] - 0.5).abs() < 1e-8,
382            "y2 should be 0.5, got {}",
383            y0[1]
384        );
385        assert!(
386            (y0[2] - 0.5).abs() < 1e-8,
387            "y3 should be 0.5, got {}",
388            y0[2]
389        );
390
391        // Verify constraints
392        let g1 = y0[1] + y0[2] - 1.0;
393        let g2 = y0[1] - y0[2];
394        assert!(g1.abs() < 1e-10, "Constraint 1 violated: {}", g1);
395        assert!(g2.abs() < 1e-10, "Constraint 2 violated: {}", g2);
396    }
397}