1use crate::OdeSystem;
47use faer::{ComplexField, Conjugate, SimpleEntity};
48use numra_core::Scalar;
49use numra_linalg::{DenseMatrix, LUFactorization};
50
51pub 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
65pub 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 !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 system.rhs(t0, &y, &mut f);
110
111 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 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 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 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 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 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 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 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; dydt[2] = y[1] - y[2]; },
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 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}