1use crate::boundary2d::BoundaryConditions3D;
11use crate::grid::Grid3D;
12use crate::sparse_assembly::{
13 assemble_laplacian_3d, assemble_operator_3d, Operator3DCoefficients, SparseScalar,
14};
15use numra_linalg::SparseMatrix;
16use numra_ode::OdeSystem;
17
18type ReactionFn<S> = Box<dyn Fn(S, S, S, S, S) -> S + Send + Sync>;
20
21pub struct MOLSystem3D<S: SparseScalar> {
27 grid: Grid3D<S>,
29 operator: SparseMatrix<S>,
31 bc_rhs: Vec<S>,
33 reaction: Option<ReactionFn<S>>,
35}
36
37impl<S: SparseScalar> MOLSystem3D<S> {
38 pub fn heat(grid: Grid3D<S>, alpha: S, bc: &BoundaryConditions3D<S>) -> Self {
40 let coeffs = Operator3DCoefficients::scaled_laplacian(alpha);
41 let (operator, bc_rhs) =
42 assemble_operator_3d(&grid, &coeffs, bc).expect("Failed to assemble 3D operator");
43 Self {
44 grid,
45 operator,
46 bc_rhs,
47 reaction: None,
48 }
49 }
50
51 pub fn laplacian(grid: Grid3D<S>, bc: &BoundaryConditions3D<S>) -> Self {
53 let (operator, bc_rhs) =
54 assemble_laplacian_3d(&grid, bc).expect("Failed to assemble 3D Laplacian");
55 Self {
56 grid,
57 operator,
58 bc_rhs,
59 reaction: None,
60 }
61 }
62
63 pub fn with_operator(
65 grid: Grid3D<S>,
66 coeffs: &Operator3DCoefficients<S>,
67 bc: &BoundaryConditions3D<S>,
68 ) -> Self {
69 let (operator, bc_rhs) =
70 assemble_operator_3d(&grid, coeffs, bc).expect("Failed to assemble 3D operator");
71 Self {
72 grid,
73 operator,
74 bc_rhs,
75 reaction: None,
76 }
77 }
78
79 pub fn with_reaction<F>(mut self, reaction: F) -> Self
83 where
84 F: Fn(S, S, S, S, S) -> S + Send + Sync + 'static,
85 {
86 self.reaction = Some(Box::new(reaction));
87 self
88 }
89
90 pub fn grid(&self) -> &Grid3D<S> {
92 &self.grid
93 }
94
95 pub fn n_interior(&self) -> usize {
97 self.grid.n_interior()
98 }
99
100 pub fn build_full_solution(&self, u_interior: &[S]) -> Vec<S> {
107 let nx = self.grid.x_grid.len();
108 let ny = self.grid.y_grid.len();
109 let nz = self.grid.z_grid.len();
110 let nx_int = self.grid.x_grid.n_interior();
111 let ny_int = self.grid.y_grid.n_interior();
112 let nz_int = self.grid.z_grid.n_interior();
113
114 let mut u_full = vec![S::ZERO; nx * ny * nz];
115
116 for kk in 0..nz_int {
117 for jj in 0..ny_int {
118 for ii in 0..nx_int {
119 let full_idx = self.grid.linear_index(ii + 1, jj + 1, kk + 1);
120 let int_idx = kk * (nx_int * ny_int) + jj * nx_int + ii;
121 u_full[full_idx] = u_interior[int_idx];
122 }
123 }
124 }
125
126 u_full
127 }
128}
129
130impl<S: SparseScalar> OdeSystem<S> for MOLSystem3D<S> {
131 fn dim(&self) -> usize {
132 self.n_interior()
133 }
134
135 fn rhs(&self, t: S, y: &[S], dydt: &mut [S]) {
136 let matvec = self.operator.mul_vec(y).expect("Sparse matvec failed");
138
139 let n = self.n_interior();
140 for i in 0..n {
141 dydt[i] = matvec[i] + self.bc_rhs[i];
142 }
143
144 if let Some(ref reaction) = self.reaction {
146 let nx_int = self.grid.x_grid.n_interior();
147 let ny_int = self.grid.y_grid.n_interior();
148 let nz_int = self.grid.z_grid.n_interior();
149 for kk in 0..nz_int {
150 for jj in 0..ny_int {
151 for ii in 0..nx_int {
152 let idx = kk * (nx_int * ny_int) + jj * nx_int + ii;
153 let x = self.grid.x_grid.points()[ii + 1];
154 let y_coord = self.grid.y_grid.points()[jj + 1];
155 let z_coord = self.grid.z_grid.points()[kk + 1];
156 dydt[idx] = dydt[idx] + reaction(t, x, y_coord, z_coord, y[idx]);
157 }
158 }
159 }
160 }
161 }
162
163 fn jacobian(&self, t: S, y: &[S], jac: &mut [S]) {
180 let n = self.n_interior();
181 let nn = n * n;
182
183 for v in jac.iter_mut().take(nn) {
186 *v = S::ZERO;
187 }
188
189 let col_ptrs = self.operator.col_ptrs();
193 let row_indices = self.operator.row_indices();
194 let values = self.operator.values();
195 for j in 0..n {
196 let start = col_ptrs[j];
197 let end = col_ptrs[j + 1];
198 for idx in start..end {
199 let i = row_indices[idx];
200 jac[i * n + j] = values[idx];
201 }
202 }
203
204 if let Some(ref reaction) = self.reaction {
209 let h_factor = S::EPSILON.sqrt();
210 let nx_int = self.grid.x_grid.n_interior();
211 let ny_int = self.grid.y_grid.n_interior();
212 let nz_int = self.grid.z_grid.n_interior();
213 for kk in 0..nz_int {
214 for jj in 0..ny_int {
215 for ii in 0..nx_int {
216 let idx = kk * (nx_int * ny_int) + jj * nx_int + ii;
217 let x = self.grid.x_grid.points()[ii + 1];
218 let y_coord = self.grid.y_grid.points()[jj + 1];
219 let z_coord = self.grid.z_grid.points()[kk + 1];
220 let u = y[idx];
221 let h = h_factor * (S::ONE + u.abs());
222 let r0 = reaction(t, x, y_coord, z_coord, u);
223 let r1 = reaction(t, x, y_coord, z_coord, u + h);
224 let dr_du = (r1 - r0) / h;
225 jac[idx * n + idx] = jac[idx * n + idx] + dr_du;
226 }
227 }
228 }
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use numra_ode::{DoPri5, Solver, SolverOptions};
237
238 #[test]
239 fn test_mol3d_dim_and_zero_state() {
240 let grid = Grid3D::uniform(0.0, 1.0, 5, 0.0, 1.0, 5, 0.0, 1.0, 5);
242 let bc = BoundaryConditions3D::all_zero_dirichlet();
243 let mol = MOLSystem3D::heat(grid, 0.01_f64, &bc);
244
245 assert_eq!(mol.dim(), 27);
247
248 let u0 = vec![0.0; 27];
249 let options = SolverOptions::default().rtol(1e-6);
250 let result = DoPri5::solve(&mol, 0.0, 0.1, &u0, &options).unwrap();
251 assert!(result.success);
252
253 let y_final = result.y_final().unwrap();
254 for &v in &y_final {
255 assert!(v.abs() < 1e-10, "Expected zero, got {}", v);
256 }
257 }
258
259 #[test]
260 fn test_mol3d_heat_decay() {
261 let alpha = 0.01_f64;
266 let n = 13;
267 let grid = Grid3D::uniform(0.0, 1.0, n, 0.0, 1.0, n, 0.0, 1.0, n);
268 let bc = BoundaryConditions3D::all_zero_dirichlet();
269 let mol = MOLSystem3D::heat(grid.clone(), alpha, &bc);
270
271 let nx_int = n - 2;
272 let ny_int = n - 2;
273 let nz_int = n - 2;
274 let n_int = nx_int * ny_int * nz_int;
275
276 let mut u0 = vec![0.0; n_int];
278 let pi = std::f64::consts::PI;
279 for kk in 0..nz_int {
280 for jj in 0..ny_int {
281 for ii in 0..nx_int {
282 let x = grid.x_grid.points()[ii + 1];
283 let y = grid.y_grid.points()[jj + 1];
284 let z = grid.z_grid.points()[kk + 1];
285 u0[kk * (nx_int * ny_int) + jj * nx_int + ii] =
286 (pi * x).sin() * (pi * y).sin() * (pi * z).sin();
287 }
288 }
289 }
290
291 let t_final = 0.5;
292 let options = SolverOptions::default().rtol(1e-6).atol(1e-9);
293 let result = DoPri5::solve(&mol, 0.0, t_final, &u0, &options).unwrap();
294 assert!(result.success);
295
296 let y_final = result.y_final().unwrap();
297 let decay = (-3.0 * pi * pi * alpha * t_final).exp();
298
299 let mid_i = nx_int / 2;
302 let mid_j = ny_int / 2;
303 let mid_k = nz_int / 2;
304 let idx = mid_k * (nx_int * ny_int) + mid_j * nx_int + mid_i;
305 let x = grid.x_grid.points()[mid_i + 1];
306 let y = grid.y_grid.points()[mid_j + 1];
307 let z = grid.z_grid.points()[mid_k + 1];
308 let exact = (pi * x).sin() * (pi * y).sin() * (pi * z).sin() * decay;
309 let rel_err = (y_final[idx] - exact).abs() / exact.abs();
310 assert!(
311 rel_err < 0.05,
312 "Center: computed={:.6}, exact={:.6}, rel_err={:.4}",
313 y_final[idx],
314 exact,
315 rel_err
316 );
317 }
318
319 #[test]
320 fn test_mol3d_reaction_diffusion() {
321 let d = 0.01_f64;
324 let n = 7;
325 let grid = Grid3D::uniform(0.0, 1.0, n, 0.0, 1.0, n, 0.0, 1.0, n);
326 let bc = BoundaryConditions3D::all_zero_dirichlet();
327 let mol = MOLSystem3D::heat(grid.clone(), d, &bc)
328 .with_reaction(|_t, _x, _y, _z, u| u * (1.0 - u));
329
330 let nx_int = n - 2;
331 let ny_int = n - 2;
332 let nz_int = n - 2;
333 let n_int = nx_int * ny_int * nz_int;
334
335 let mut u0 = vec![0.0; n_int];
337 for kk in 0..nz_int {
338 for jj in 0..ny_int {
339 for ii in 0..nx_int {
340 let x = grid.x_grid.points()[ii + 1];
341 let y = grid.y_grid.points()[jj + 1];
342 let z = grid.z_grid.points()[kk + 1];
343 let r2 = (x - 0.5).powi(2) + (y - 0.5).powi(2) + (z - 0.5).powi(2);
344 if r2 < 0.05 {
345 u0[kk * (nx_int * ny_int) + jj * nx_int + ii] = 0.5;
346 }
347 }
348 }
349 }
350
351 let options = SolverOptions::default().rtol(1e-4);
352 let result = DoPri5::solve(&mol, 0.0, 0.5, &u0, &options).unwrap();
353 assert!(result.success);
354
355 let y_final = result.y_final().unwrap();
356 for &v in &y_final {
357 assert!(
358 (-0.1..=1.1).contains(&v),
359 "Solution out of expected range: {}",
360 v
361 );
362 }
363 }
364
365 #[test]
366 fn test_mol3d_nonzero_dirichlet() {
367 let n = 9;
371 let grid = Grid3D::uniform(0.0, 1.0, n, 0.0, 1.0, n, 0.0, 1.0, n);
372 let bc = BoundaryConditions3D {
373 x_min: crate::boundary::BoxedBC::dirichlet(1.0),
374 x_max: crate::boundary::BoxedBC::dirichlet(0.0),
375 y_min: crate::boundary::BoxedBC::dirichlet(0.0),
376 y_max: crate::boundary::BoxedBC::dirichlet(0.0),
377 z_min: crate::boundary::BoxedBC::dirichlet(0.0),
378 z_max: crate::boundary::BoxedBC::dirichlet(0.0),
379 };
380 let mol = MOLSystem3D::heat(grid, 0.1_f64, &bc);
381
382 let u0 = vec![0.0; mol.dim()];
383 let options = SolverOptions::default().rtol(1e-6);
384 let result = DoPri5::solve(&mol, 0.0, 5.0, &u0, &options).unwrap();
385 assert!(result.success);
386
387 let y_final = result.y_final().unwrap();
388 let nx_int = n - 2;
389 let ny_int = n - 2;
390 let mid_j = ny_int / 2;
391 let mid_k = (n - 2) / 2;
392 let left = y_final[mid_k * (nx_int * ny_int) + mid_j * nx_int];
393 let right = y_final[mid_k * (nx_int * ny_int) + mid_j * nx_int + (nx_int - 1)];
394 assert!(left > 0.3, "Near left face should be warm: {}", left);
395 assert!(right < 0.3, "Near right face should be cool: {}", right);
396 }
397
398 #[test]
399 fn test_mol3d_build_full_solution() {
400 let grid = Grid3D::uniform(0.0, 1.0, 4, 0.0, 1.0, 4, 0.0, 1.0, 4);
401 let bc = BoundaryConditions3D::all_zero_dirichlet();
402 let mol = MOLSystem3D::heat(grid, 0.01_f64, &bc);
403
404 let u_int = vec![1.0; 8]; let u_full = mol.build_full_solution(&u_int);
406 assert_eq!(u_full.len(), 64); let center = mol.grid().linear_index(1, 1, 1);
410 assert!((u_full[center] - 1.0).abs() < 1e-10);
411
412 let corner = mol.grid().linear_index(0, 0, 0);
414 assert!(u_full[corner].abs() < 1e-10);
415 }
416
417 fn fd_jacobian<Sys: numra_ode::OdeSystem<f64>>(sys: &Sys, t: f64, y: &[f64]) -> Vec<f64> {
421 let n = sys.dim();
422 let h_factor = f64::EPSILON.sqrt();
423 let mut jac = vec![0.0; n * n];
424 let mut y_pert = y.to_vec();
425 let mut f0 = vec![0.0; n];
426 let mut f1 = vec![0.0; n];
427 sys.rhs(t, y, &mut f0);
428 for j in 0..n {
429 let yj = y_pert[j];
430 let h = h_factor * (1.0 + yj.abs());
431 y_pert[j] = yj + h;
432 sys.rhs(t, &y_pert, &mut f1);
433 y_pert[j] = yj;
434 for i in 0..n {
435 jac[i * n + j] = (f1[i] - f0[i]) / h;
436 }
437 }
438 jac
439 }
440
441 #[test]
442 fn test_mol3d_jacobian_agrees_with_fd_no_reaction() {
443 let grid = Grid3D::uniform(0.0, 1.0, 5, 0.0, 1.0, 5, 0.0, 1.0, 5);
445 let bc = BoundaryConditions3D::all_zero_dirichlet();
446 let mol = MOLSystem3D::heat(grid, 0.01_f64, &bc);
447 let n = mol.dim();
448 let y: Vec<f64> = (0..n).map(|i| ((i + 1) as f64).sin()).collect();
449
450 let mut jac_analytical = vec![0.0; n * n];
451 OdeSystem::jacobian(&mol, 0.0, &y, &mut jac_analytical);
452 let jac_fd = fd_jacobian(&mol, 0.0, &y);
453
454 for i in 0..n {
455 for j in 0..n {
456 let a = jac_analytical[i * n + j];
457 let f = jac_fd[i * n + j];
458 let tol = 1e-5_f64.max(1e-5 * a.abs());
459 assert!(
460 (a - f).abs() < tol,
461 "Jacobian mismatch at ({},{}): analytical={}, fd={}",
462 i,
463 j,
464 a,
465 f
466 );
467 }
468 }
469 }
470
471 #[test]
472 fn test_mol3d_jacobian_agrees_with_fd_with_reaction() {
473 let grid = Grid3D::uniform(0.0, 1.0, 4, 0.0, 1.0, 4, 0.0, 1.0, 4);
476 let bc = BoundaryConditions3D::all_zero_dirichlet();
477 let mol = MOLSystem3D::heat(grid, 0.05_f64, &bc)
478 .with_reaction(|_t, _x, _y, _z, u: f64| -u * u * u);
479 let n = mol.dim();
480 let y: Vec<f64> = (0..n).map(|i| 0.1 + (i as f64) * 0.01).collect();
481
482 let mut jac_analytical = vec![0.0; n * n];
483 OdeSystem::jacobian(&mol, 0.0, &y, &mut jac_analytical);
484 let jac_fd = fd_jacobian(&mol, 0.0, &y);
485
486 for i in 0..n {
487 for j in 0..n {
488 let a = jac_analytical[i * n + j];
489 let f = jac_fd[i * n + j];
490 assert!(
491 (a - f).abs() < 1e-4,
492 "Jacobian mismatch at ({},{}): analytical={}, fd={}",
493 i,
494 j,
495 a,
496 f
497 );
498 }
499 }
500 }
501
502 #[test]
503 fn test_mol3d_radau5_uses_analytical_jacobian() {
504 use numra_ode::{Radau5, Solver, SolverOptions};
508 let n = 7;
509 let grid = Grid3D::uniform(0.0, 1.0, n, 0.0, 1.0, n, 0.0, 1.0, n);
510 let bc = BoundaryConditions3D::all_zero_dirichlet();
511 let mol = MOLSystem3D::heat(grid.clone(), 0.5_f64, &bc); let nx_int = n - 2;
514 let n_int = nx_int * nx_int * nx_int;
515 let pi = std::f64::consts::PI;
516 let mut u0 = vec![0.0; n_int];
517 for kk in 0..nx_int {
518 for jj in 0..nx_int {
519 for ii in 0..nx_int {
520 let x = grid.x_grid.points()[ii + 1];
521 let yc = grid.y_grid.points()[jj + 1];
522 let zc = grid.z_grid.points()[kk + 1];
523 u0[kk * (nx_int * nx_int) + jj * nx_int + ii] =
524 (pi * x).sin() * (pi * yc).sin() * (pi * zc).sin();
525 }
526 }
527 }
528
529 let options = SolverOptions::default().rtol(1e-5).atol(1e-8);
530 let result = Radau5::solve(&mol, 0.0, 0.02, &u0, &options).unwrap();
531 assert!(result.success);
532
533 let y_final = result.y_final().unwrap();
535 let mid = (nx_int / 2) * (nx_int * nx_int) + (nx_int / 2) * nx_int + (nx_int / 2);
536 let exact = (-3.0 * pi * pi * 0.5_f64 * 0.02).exp();
537 let rel_err = (y_final[mid] - exact).abs() / exact;
540 assert!(
541 rel_err < 0.1,
542 "computed={}, exact={}, rel_err={}",
543 y_final[mid],
544 exact,
545 rel_err
546 );
547 }
548}