scirs2_integrate/pde/method_of_lines/mod3d.rs
1//! Method of Lines for 3D parabolic PDEs
2//!
3//! This module implements the Method of Lines (MOL) approach for solving
4//! 3D parabolic PDEs, such as the 3D heat equation and 3D advection-diffusion.
5
6use scirs2_core::ndarray::{Array1, Array3, Array4, ArrayView1, ArrayView3};
7use std::sync::Arc;
8use std::time::Instant;
9
10use crate::ode::{solve_ivp, ODEOptions};
11use crate::pde::finite_difference::FiniteDifferenceScheme;
12use crate::pde::{
13 BoundaryCondition, BoundaryConditionType, BoundaryLocation, Domain, PDEError, PDEResult,
14 PDESolution, PDESolverInfo,
15};
16
17/// Type alias for 3D coefficient function taking (x, y, z, t, u) and returning a value
18type CoeffFn3D = Arc<dyn Fn(f64, f64, f64, f64, f64) -> f64 + Send + Sync>;
19
20/// Result of 3D method of lines solution
21pub struct MOL3DResult {
22 /// Time points
23 pub t: Array1<f64>,
24
25 /// Solution values, indexed as [time, z, y, x]
26 pub u: Array4<f64>,
27
28 /// ODE solver information
29 pub ode_info: Option<String>,
30
31 /// Computation time
32 pub computation_time: f64,
33}
34
35/// Method of Lines solver for 3D parabolic PDEs
36///
37/// Solves equations of the form:
38/// ∂u/∂t = ∂/∂x(D_x ∂u/∂x) + ∂/∂y(D_y ∂u/∂y) + ∂/∂z(D_z ∂u/∂z) +
39/// v_x ∂u/∂x + v_y ∂u/∂y + v_z ∂u/∂z + f(x,y,z,t,u)
40#[derive(Clone)]
41pub struct MOLParabolicSolver3D {
42 /// Spatial domain
43 domain: Domain,
44
45 /// Time range [t_start, t_end]
46 time_range: [f64; 2],
47
48 /// Diffusion coefficient function D_x(x, y, z, t, u) for ∂²u/∂x²
49 diffusion_x: CoeffFn3D,
50
51 /// Diffusion coefficient function D_y(x, y, z, t, u) for ∂²u/∂y²
52 diffusion_y: CoeffFn3D,
53
54 /// Diffusion coefficient function D_z(x, y, z, t, u) for ∂²u/∂z²
55 diffusion_z: CoeffFn3D,
56
57 /// Advection coefficient function v_x(x, y, z, t, u) for ∂u/∂x
58 advection_x: Option<CoeffFn3D>,
59
60 /// Advection coefficient function v_y(x, y, z, t, u) for ∂u/∂y
61 advection_y: Option<CoeffFn3D>,
62
63 /// Advection coefficient function v_z(x, y, z, t, u) for ∂u/∂z
64 advection_z: Option<CoeffFn3D>,
65
66 /// Reaction term function f(x, y, z, t, u)
67 reaction_term: Option<CoeffFn3D>,
68
69 /// Initial condition function u(x, y, z, 0)
70 initial_condition: Arc<dyn Fn(f64, f64, f64) -> f64 + Send + Sync>,
71
72 /// Boundary conditions
73 boundary_conditions: Vec<BoundaryCondition<f64>>,
74
75 /// Finite difference scheme for spatial discretization
76 fd_scheme: FiniteDifferenceScheme,
77
78 /// Solver options
79 options: super::MOLOptions,
80}
81
82impl MOLParabolicSolver3D {
83 /// Create a new Method of Lines solver for 3D parabolic PDEs
84 #[allow(clippy::too_many_arguments)]
85 pub fn new(
86 domain: Domain,
87 time_range: [f64; 2],
88 diffusion_x: impl Fn(f64, f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
89 diffusion_y: impl Fn(f64, f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
90 diffusion_z: impl Fn(f64, f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
91 initial_condition: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
92 boundary_conditions: Vec<BoundaryCondition<f64>>,
93 options: Option<super::MOLOptions>,
94 ) -> PDEResult<Self> {
95 // Validate the domain
96 if domain.dimensions() != 3 {
97 return Err(PDEError::DomainError(
98 "Domain must be 3-dimensional for 3D parabolic solver".to_string(),
99 ));
100 }
101
102 // Validate time _range
103 if time_range[0] >= time_range[1] {
104 return Err(PDEError::DomainError(
105 "Invalid time _range: start must be less than end".to_string(),
106 ));
107 }
108
109 // Validate boundary _conditions
110 if boundary_conditions.len() != 6 {
111 return Err(PDEError::BoundaryConditions(
112 "3D parabolic PDE requires exactly 6 boundary _conditions (one for each face)"
113 .to_string(),
114 ));
115 }
116
117 // Ensure we have boundary _conditions for all dimensions/faces
118 let has_x_lower = boundary_conditions
119 .iter()
120 .any(|bc| bc.location == BoundaryLocation::Lower && bc.dimension == 0);
121 let has_x_upper = boundary_conditions
122 .iter()
123 .any(|bc| bc.location == BoundaryLocation::Upper && bc.dimension == 0);
124 let has_y_lower = boundary_conditions
125 .iter()
126 .any(|bc| bc.location == BoundaryLocation::Lower && bc.dimension == 1);
127 let has_y_upper = boundary_conditions
128 .iter()
129 .any(|bc| bc.location == BoundaryLocation::Upper && bc.dimension == 1);
130 let has_z_lower = boundary_conditions
131 .iter()
132 .any(|bc| bc.location == BoundaryLocation::Lower && bc.dimension == 2);
133 let has_z_upper = boundary_conditions
134 .iter()
135 .any(|bc| bc.location == BoundaryLocation::Upper && bc.dimension == 2);
136
137 if !has_x_lower
138 || !has_x_upper
139 || !has_y_lower
140 || !has_y_upper
141 || !has_z_lower
142 || !has_z_upper
143 {
144 return Err(PDEError::BoundaryConditions(
145 "3D parabolic PDE requires boundary _conditions for all faces of the domain"
146 .to_string(),
147 ));
148 }
149
150 Ok(MOLParabolicSolver3D {
151 domain,
152 time_range,
153 diffusion_x: Arc::new(diffusion_x),
154 diffusion_y: Arc::new(diffusion_y),
155 diffusion_z: Arc::new(diffusion_z),
156 advection_x: None,
157 advection_y: None,
158 advection_z: None,
159 reaction_term: None,
160 initial_condition: Arc::new(initial_condition),
161 boundary_conditions,
162 fd_scheme: FiniteDifferenceScheme::CentralDifference,
163 options: options.unwrap_or_default(),
164 })
165 }
166
167 /// Add advection terms to the PDE
168 pub fn with_advection(
169 mut self,
170 advection_x: impl Fn(f64, f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
171 advection_y: impl Fn(f64, f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
172 advection_z: impl Fn(f64, f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
173 ) -> Self {
174 self.advection_x = Some(Arc::new(advection_x));
175 self.advection_y = Some(Arc::new(advection_y));
176 self.advection_z = Some(Arc::new(advection_z));
177 self
178 }
179
180 /// Add a reaction term to the PDE
181 pub fn with_reaction(
182 mut self,
183 reaction_term: impl Fn(f64, f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
184 ) -> Self {
185 self.reaction_term = Some(Arc::new(reaction_term));
186 self
187 }
188
189 /// Set the finite difference scheme for spatial discretization
190 pub fn with_fd_scheme(mut self, scheme: FiniteDifferenceScheme) -> Self {
191 self.fd_scheme = scheme;
192 self
193 }
194
195 /// Solve the 3D parabolic PDE
196 pub fn solve(&self) -> PDEResult<MOL3DResult> {
197 let start_time = Instant::now();
198
199 // Generate spatial grids
200 let x_grid = self.domain.grid(0)?;
201 let y_grid = self.domain.grid(1)?;
202 let z_grid = self.domain.grid(2)?;
203 let nx = x_grid.len();
204 let ny = y_grid.len();
205 let nz = z_grid.len();
206 let dx = self.domain.grid_spacing(0)?;
207 let dy = self.domain.grid_spacing(1)?;
208 let dz = self.domain.grid_spacing(2)?;
209
210 // Create initial condition 3D grid and flatten it for the ODE solver
211 let mut u0 = Array3::zeros((nz, ny, nx));
212 for k in 0..nz {
213 for j in 0..ny {
214 for i in 0..nx {
215 u0[[k, j, i]] = (self.initial_condition)(x_grid[i], y_grid[j], z_grid[k]);
216 }
217 }
218 }
219
220 // Flatten the 3D grid into a 1D array for the ODE solver
221 // Note: This is computed but not used, likely for future use
222 let _u0_flat = u0
223 .clone()
224 .into_shape_with_order(nx * ny * nz)
225 .expect("Operation failed");
226
227 // Clone grids for the closure
228 let x_grid_closure = x_grid.clone();
229 let y_grid_closure = y_grid.clone();
230 let z_grid_closure = z_grid.clone();
231
232 // Clone grids for later use outside closure
233 let x_grid_apply = x_grid.clone();
234 let y_grid_apply = y_grid.clone();
235 let z_grid_apply = z_grid.clone();
236
237 // Extract options and other needed values before moving self
238 let ode_options = ODEOptions {
239 method: self.options.ode_method,
240 rtol: self.options.rtol,
241 atol: self.options.atol,
242 max_steps: self.options.max_steps.unwrap_or(500),
243 h0: None,
244 max_step: None,
245 min_step: None,
246 dense_output: true,
247 max_order: None,
248 jac: None,
249 use_banded_jacobian: false,
250 ml: None,
251 mu: None,
252 mass_matrix: None,
253 jacobian_strategy: None,
254 };
255
256 let time_range = self.time_range;
257 let boundary_conditions = self.boundary_conditions.clone();
258
259 // Move self into closure
260 let solver = self;
261
262 // Construct the ODE function that represents the PDE after spatial discretization
263 let ode_func = move |t: f64, u_flat: ArrayView1<f64>| -> Array1<f64> {
264 // Reshape the flattened array back to 3D for easier indexing
265 let u = u_flat
266 .into_shape_with_order((nz, ny, nx))
267 .expect("Operation failed");
268 let mut dudt = Array3::zeros((nz, ny, nx));
269
270 // Apply finite difference approximations for interior points
271 for k in 1..nz - 1 {
272 for j in 1..ny - 1 {
273 for i in 1..nx - 1 {
274 let z = z_grid[k];
275 let y = y_grid[j];
276 let x = x_grid[i];
277 let u_val = u[[k, j, i]];
278
279 // Diffusion terms
280 let d2u_dx2 =
281 (u[[k, j, i + 1]] - 2.0 * u[[k, j, i]] + u[[k, j, i - 1]]) / (dx * dx);
282 let d2u_dy2 =
283 (u[[k, j + 1, i]] - 2.0 * u[[k, j, i]] + u[[k, j - 1, i]]) / (dy * dy);
284 let d2u_dz2 =
285 (u[[k + 1, j, i]] - 2.0 * u[[k, j, i]] + u[[k - 1, j, i]]) / (dz * dz);
286
287 let diffusion_term_x = (solver.diffusion_x)(x, y, z, t, u_val) * d2u_dx2;
288 let diffusion_term_y = (solver.diffusion_y)(x, y, z, t, u_val) * d2u_dy2;
289 let diffusion_term_z = (solver.diffusion_z)(x, y, z, t, u_val) * d2u_dz2;
290
291 // Advection terms
292 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
293 let du_dx = (u[[k, j, i + 1]] - u[[k, j, i - 1]]) / (2.0 * dx);
294 advection_x(x, y, z, t, u_val) * du_dx
295 } else {
296 0.0
297 };
298
299 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
300 let du_dy = (u[[k, j + 1, i]] - u[[k, j - 1, i]]) / (2.0 * dy);
301 advection_y(x, y, z, t, u_val) * du_dy
302 } else {
303 0.0
304 };
305
306 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
307 let du_dz = (u[[k + 1, j, i]] - u[[k - 1, j, i]]) / (2.0 * dz);
308 advection_z(x, y, z, t, u_val) * du_dz
309 } else {
310 0.0
311 };
312
313 // Reaction term
314 let reaction_term = if let Some(reaction) = &solver.reaction_term {
315 reaction(x, y, z, t, u_val)
316 } else {
317 0.0
318 };
319
320 dudt[[k, j, i]] = diffusion_term_x
321 + diffusion_term_y
322 + diffusion_term_z
323 + advection_term_x
324 + advection_term_y
325 + advection_term_z
326 + reaction_term;
327 }
328 }
329 }
330
331 // Apply boundary conditions
332 for bc in &solver.boundary_conditions {
333 match (bc.dimension, bc.location) {
334 // X-direction boundaries
335 (0, BoundaryLocation::Lower) => {
336 // Apply boundary condition at x[0] (left face)
337 apply_boundary_condition_3d(
338 &mut dudt,
339 &u,
340 &x_grid_closure,
341 &y_grid_closure,
342 &z_grid_closure,
343 bc,
344 dx,
345 dy,
346 dz,
347 Some(0),
348 None,
349 None,
350 Some(ny),
351 Some(nz),
352 solver,
353 );
354 }
355 (0, BoundaryLocation::Upper) => {
356 // Apply boundary condition at x[nx-1] (right face)
357 apply_boundary_condition_3d(
358 &mut dudt,
359 &u,
360 &x_grid_closure,
361 &y_grid_closure,
362 &z_grid_closure,
363 bc,
364 dx,
365 dy,
366 dz,
367 Some(nx - 1),
368 None,
369 None,
370 Some(ny),
371 Some(nz),
372 solver,
373 );
374 }
375 // Y-direction boundaries
376 (1, BoundaryLocation::Lower) => {
377 // Apply boundary condition at y[0] (front face)
378 apply_boundary_condition_3d(
379 &mut dudt,
380 &u,
381 &x_grid_closure,
382 &y_grid_closure,
383 &z_grid_closure,
384 bc,
385 dx,
386 dy,
387 dz,
388 None,
389 Some(0),
390 Some(nx),
391 None,
392 Some(nz),
393 solver,
394 );
395 }
396 (1, BoundaryLocation::Upper) => {
397 // Apply boundary condition at y[ny-1] (back face)
398 apply_boundary_condition_3d(
399 &mut dudt,
400 &u,
401 &x_grid_closure,
402 &y_grid_closure,
403 &z_grid_closure,
404 bc,
405 dx,
406 dy,
407 dz,
408 None,
409 Some(ny - 1),
410 Some(nx),
411 None,
412 Some(nz),
413 solver,
414 );
415 }
416 // Z-direction boundaries
417 (2, BoundaryLocation::Lower) => {
418 // Apply boundary condition at z[0] (bottom face)
419 apply_boundary_condition_3d(
420 &mut dudt,
421 &u,
422 &x_grid_closure,
423 &y_grid_closure,
424 &z_grid_closure,
425 bc,
426 dx,
427 dy,
428 dz,
429 None,
430 None,
431 Some(nx),
432 Some(ny),
433 Some(0),
434 solver,
435 );
436 }
437 (2, BoundaryLocation::Upper) => {
438 // Apply boundary condition at z[nz-1] (top face)
439 apply_boundary_condition_3d(
440 &mut dudt,
441 &u,
442 &x_grid_closure,
443 &y_grid_closure,
444 &z_grid_closure,
445 bc,
446 dx,
447 dy,
448 dz,
449 None,
450 None,
451 Some(nx),
452 Some(ny),
453 Some(nz - 1),
454 solver,
455 );
456 }
457 _ => {
458 // Invalid dimension
459 // We'll just ignore this case for now - validation occurs during initialization
460 }
461 }
462 }
463
464 // Flatten the 3D dudt back to 1D for the ODE solver
465 dudt.into_shape_with_order(nx * ny * nz)
466 .expect("Operation failed")
467 };
468
469 // Use the ode_options from earlier
470
471 // Apply Dirichlet boundary conditions to initial condition
472 apply_dirichlet_conditions_to_initial_3d(
473 &mut u0,
474 &boundary_conditions,
475 &x_grid_apply,
476 &y_grid_apply,
477 &z_grid_apply,
478 );
479
480 let u0_flat = u0
481 .into_shape_with_order(nx * ny * nz)
482 .expect("Operation failed");
483
484 // Solve the ODE system
485 let ode_result = solve_ivp(ode_func, time_range, u0_flat, Some(ode_options))?;
486
487 // Extract results
488 let computation_time = start_time.elapsed().as_secs_f64();
489
490 // Reshape the ODE result to match the spatial grid
491 let t = ode_result.t.clone();
492 let nt = t.len();
493
494 // Create a 4D array with dimensions [time, z, y, x]
495 let mut u_4d = Array4::zeros((nt, nz, ny, nx));
496
497 for (time_idx, y_flat) in ode_result.y.iter().enumerate() {
498 let u_3d = y_flat
499 .clone()
500 .into_shape_with_order((nz, ny, nx))
501 .expect("Operation failed");
502 for k in 0..nz {
503 for j in 0..ny {
504 for i in 0..nx {
505 u_4d[[time_idx, k, j, i]] = u_3d[[k, j, i]];
506 }
507 }
508 }
509 }
510
511 let ode_info = Some(format!(
512 "ODE steps: {}, function evaluations: {}, successful steps: {}",
513 ode_result.n_steps, ode_result.n_eval, ode_result.n_accepted,
514 ));
515
516 Ok(MOL3DResult {
517 t: ode_result.t.into(),
518 u: u_4d,
519 ode_info,
520 computation_time,
521 })
522 }
523}
524
525// Helper function to apply boundary conditions in 3D
526#[allow(clippy::too_many_arguments)]
527#[allow(dead_code)]
528fn apply_boundary_condition_3d(
529 dudt: &mut Array3<f64>,
530 u: &ArrayView3<f64>,
531 x_grid: &Array1<f64>,
532 y_grid: &Array1<f64>,
533 z_grid: &Array1<f64>,
534 bc: &BoundaryCondition<f64>,
535 dx: f64,
536 dy: f64,
537 dz: f64,
538 i_fixed: Option<usize>,
539 j_fixed: Option<usize>,
540 i_range: Option<usize>,
541 j_range: Option<usize>,
542 k_fixed: Option<usize>,
543 solver: &MOLParabolicSolver3D,
544) {
545 let nx = x_grid.len();
546 let ny = y_grid.len();
547 let nz = z_grid.len();
548
549 let i_range = i_range.unwrap_or(1);
550 let j_range = j_range.unwrap_or(1);
551 let t = 0.0; // Time is not used here
552
553 match bc.bc_type {
554 BoundaryConditionType::Dirichlet => {
555 // Fixed value: u = bc.value
556 // For Dirichlet, we set dudt = 0 to maintain the _fixed value
557
558 if let Some(i) = i_fixed {
559 // X-direction boundary (left or right face)
560 for k in 0..nz {
561 for j in 0..ny {
562 dudt[[k, j, i]] = 0.0;
563 }
564 }
565 } else if let Some(j) = j_fixed {
566 // Y-direction boundary (front or back face)
567 for k in 0..nz {
568 for i in 0..i_range {
569 dudt[[k, j, i]] = 0.0;
570 }
571 }
572 } else if let Some(k) = k_fixed {
573 // Z-direction boundary (bottom or top face)
574 for j in 0..j_range {
575 for i in 0..i_range {
576 dudt[[k, j, i]] = 0.0;
577 }
578 }
579 }
580 }
581 BoundaryConditionType::Neumann => {
582 // Gradient in the direction normal to the boundary
583
584 if let Some(i) = i_fixed {
585 // X-direction boundary (left or right face)
586 for k in 1..nz - 1 {
587 for j in 1..ny - 1 {
588 let z = z_grid[k];
589 let y = y_grid[j];
590 let x = x_grid[i];
591 let u_val = u[[k, j, i]];
592
593 // Use one-sided difference for the normal direction
594 let du_dn = bc.value; // Given Neumann value
595
596 // For left boundary (i=0), ghost point is at i-1
597 // For right boundary (i=nx-1), ghost point is at i+1
598 let u_ghost = if i == 0 {
599 u[[k, j, i]] - dx * du_dn
600 } else {
601 u[[k, j, i]] + dx * du_dn
602 };
603
604 // Diffusion term in x direction
605 let d2u_dx2 = if i == 0 {
606 (u[[k, j, i + 1]] - 2.0 * u[[k, j, i]] + u_ghost) / (dx * dx)
607 } else {
608 (u_ghost - 2.0 * u[[k, j, i]] + u[[k, j, i - 1]]) / (dx * dx)
609 };
610
611 // Diffusion terms in y and z direction (use central difference)
612 let d2u_dy2 =
613 (u[[k, j + 1, i]] - 2.0 * u[[k, j, i]] + u[[k, j - 1, i]]) / (dy * dy);
614 let d2u_dz2 =
615 (u[[k + 1, j, i]] - 2.0 * u[[k, j, i]] + u[[k - 1, j, i]]) / (dz * dz);
616
617 let diffusion_term_x = (solver.diffusion_x)(x, y, z, t, u_val) * d2u_dx2;
618 let diffusion_term_y = (solver.diffusion_y)(x, y, z, t, u_val) * d2u_dy2;
619 let diffusion_term_z = (solver.diffusion_z)(x, y, z, t, u_val) * d2u_dz2;
620
621 // Advection terms
622 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
623 // Use one-sided difference for advection in x
624 let du_dx = if i == 0 {
625 (u[[k, j, i + 1]] - u[[k, j, i]]) / dx
626 } else {
627 (u[[k, j, i]] - u[[k, j, i - 1]]) / dx
628 };
629 advection_x(x, y, z, t, u_val) * du_dx
630 } else {
631 0.0
632 };
633
634 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
635 // Use central difference for advection in y
636 let du_dy = (u[[k, j + 1, i]] - u[[k, j - 1, i]]) / (2.0 * dy);
637 advection_y(x, y, z, t, u_val) * du_dy
638 } else {
639 0.0
640 };
641
642 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
643 // Use central difference for advection in z
644 let du_dz = (u[[k + 1, j, i]] - u[[k - 1, j, i]]) / (2.0 * dz);
645 advection_z(x, y, z, t, u_val) * du_dz
646 } else {
647 0.0
648 };
649
650 // Reaction term
651 let reaction_term = if let Some(reaction) = &solver.reaction_term {
652 reaction(x, y, z, t, u_val)
653 } else {
654 0.0
655 };
656
657 dudt[[k, j, i]] = diffusion_term_x
658 + diffusion_term_y
659 + diffusion_term_z
660 + advection_term_x
661 + advection_term_y
662 + advection_term_z
663 + reaction_term;
664 }
665 }
666 } else if let Some(j) = j_fixed {
667 // Y-direction boundary (front or back face)
668 for k in 1..nz - 1 {
669 for i in 1..nx - 1 {
670 let z = z_grid[k];
671 let y = y_grid[j];
672 let x = x_grid[i];
673 let u_val = u[[k, j, i]];
674
675 // Use one-sided difference for the normal direction
676 let du_dn = bc.value; // Given Neumann value
677
678 // For front boundary (j=0), ghost point is at j-1
679 // For back boundary (j=ny-1), ghost point is at j+1
680 let u_ghost = if j == 0 {
681 u[[k, j, i]] - dy * du_dn
682 } else {
683 u[[k, j, i]] + dy * du_dn
684 };
685
686 // Diffusion term in y direction
687 let d2u_dy2 = if j == 0 {
688 (u[[k, j + 1, i]] - 2.0 * u[[k, j, i]] + u_ghost) / (dy * dy)
689 } else {
690 (u_ghost - 2.0 * u[[k, j, i]] + u[[k, j - 1, i]]) / (dy * dy)
691 };
692
693 // Diffusion terms in x and z direction (use central difference)
694 let d2u_dx2 =
695 (u[[k, j, i + 1]] - 2.0 * u[[k, j, i]] + u[[k, j, i - 1]]) / (dx * dx);
696 let d2u_dz2 =
697 (u[[k + 1, j, i]] - 2.0 * u[[k, j, i]] + u[[k - 1, j, i]]) / (dz * dz);
698
699 let diffusion_term_x = (solver.diffusion_x)(x, y, z, t, u_val) * d2u_dx2;
700 let diffusion_term_y = (solver.diffusion_y)(x, y, z, t, u_val) * d2u_dy2;
701 let diffusion_term_z = (solver.diffusion_z)(x, y, z, t, u_val) * d2u_dz2;
702
703 // Advection terms
704 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
705 // Use central difference for advection in x
706 let du_dx = (u[[k, j, i + 1]] - u[[k, j, i - 1]]) / (2.0 * dx);
707 advection_x(x, y, z, t, u_val) * du_dx
708 } else {
709 0.0
710 };
711
712 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
713 // Use one-sided difference for advection in y
714 let du_dy = if j == 0 {
715 (u[[k, j + 1, i]] - u[[k, j, i]]) / dy
716 } else {
717 (u[[k, j, i]] - u[[k, j - 1, i]]) / dy
718 };
719 advection_y(x, y, z, t, u_val) * du_dy
720 } else {
721 0.0
722 };
723
724 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
725 // Use central difference for advection in z
726 let du_dz = (u[[k + 1, j, i]] - u[[k - 1, j, i]]) / (2.0 * dz);
727 advection_z(x, y, z, t, u_val) * du_dz
728 } else {
729 0.0
730 };
731
732 // Reaction term
733 let reaction_term = if let Some(reaction) = &solver.reaction_term {
734 reaction(x, y, z, t, u_val)
735 } else {
736 0.0
737 };
738
739 dudt[[k, j, i]] = diffusion_term_x
740 + diffusion_term_y
741 + diffusion_term_z
742 + advection_term_x
743 + advection_term_y
744 + advection_term_z
745 + reaction_term;
746 }
747 }
748 } else if let Some(k) = k_fixed {
749 // Z-direction boundary (bottom or top face)
750 for j in 1..ny - 1 {
751 for i in 1..nx - 1 {
752 let z = z_grid[k];
753 let y = y_grid[j];
754 let x = x_grid[i];
755 let u_val = u[[k, j, i]];
756
757 // Use one-sided difference for the normal direction
758 let du_dn = bc.value; // Given Neumann value
759
760 // For bottom boundary (k=0), ghost point is at k-1
761 // For top boundary (k=nz-1), ghost point is at k+1
762 let u_ghost = if k == 0 {
763 u[[k, j, i]] - dz * du_dn
764 } else {
765 u[[k, j, i]] + dz * du_dn
766 };
767
768 // Diffusion term in z direction
769 let d2u_dz2 = if k == 0 {
770 (u[[k + 1, j, i]] - 2.0 * u[[k, j, i]] + u_ghost) / (dz * dz)
771 } else {
772 (u_ghost - 2.0 * u[[k, j, i]] + u[[k - 1, j, i]]) / (dz * dz)
773 };
774
775 // Diffusion terms in x and y direction (use central difference)
776 let d2u_dx2 =
777 (u[[k, j, i + 1]] - 2.0 * u[[k, j, i]] + u[[k, j, i - 1]]) / (dx * dx);
778 let d2u_dy2 =
779 (u[[k, j + 1, i]] - 2.0 * u[[k, j, i]] + u[[k, j - 1, i]]) / (dy * dy);
780
781 let diffusion_term_x = (solver.diffusion_x)(x, y, z, t, u_val) * d2u_dx2;
782 let diffusion_term_y = (solver.diffusion_y)(x, y, z, t, u_val) * d2u_dy2;
783 let diffusion_term_z = (solver.diffusion_z)(x, y, z, t, u_val) * d2u_dz2;
784
785 // Advection terms
786 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
787 // Use central difference for advection in x
788 let du_dx = (u[[k, j, i + 1]] - u[[k, j, i - 1]]) / (2.0 * dx);
789 advection_x(x, y, z, t, u_val) * du_dx
790 } else {
791 0.0
792 };
793
794 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
795 // Use central difference for advection in y
796 let du_dy = (u[[k, j + 1, i]] - u[[k, j - 1, i]]) / (2.0 * dy);
797 advection_y(x, y, z, t, u_val) * du_dy
798 } else {
799 0.0
800 };
801
802 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
803 // Use one-sided difference for advection in z
804 let du_dz = if k == 0 {
805 (u[[k + 1, j, i]] - u[[k, j, i]]) / dz
806 } else {
807 (u[[k, j, i]] - u[[k - 1, j, i]]) / dz
808 };
809 advection_z(x, y, z, t, u_val) * du_dz
810 } else {
811 0.0
812 };
813
814 // Reaction term
815 let reaction_term = if let Some(reaction) = &solver.reaction_term {
816 reaction(x, y, z, t, u_val)
817 } else {
818 0.0
819 };
820
821 dudt[[k, j, i]] = diffusion_term_x
822 + diffusion_term_y
823 + diffusion_term_z
824 + advection_term_x
825 + advection_term_y
826 + advection_term_z
827 + reaction_term;
828 }
829 }
830 }
831 }
832 BoundaryConditionType::Robin => {
833 // Robin boundary condition: a*u + b*du/dn = c
834 if let Some([a, b, c]) = bc.coefficients {
835 if let Some(i) = i_fixed {
836 // X-direction boundary (left or right face)
837 for k in 1..nz - 1 {
838 for j in 1..ny - 1 {
839 let z = z_grid[k];
840 let y = y_grid[j];
841 let x = x_grid[i];
842 let u_val = u[[k, j, i]];
843
844 // Solve for ghost point value using Robin condition
845 let du_dn = (c - a * u_val) / b;
846
847 // For left boundary (i=0), ghost point is at i-1
848 // For right boundary (i=nx-1), ghost point is at i+1
849 let u_ghost = if i == 0 {
850 u[[k, j, i]] - dx * du_dn
851 } else {
852 u[[k, j, i]] + dx * du_dn
853 };
854
855 // Diffusion term in x direction
856 let d2u_dx2 = if i == 0 {
857 (u[[k, j, i + 1]] - 2.0 * u[[k, j, i]] + u_ghost) / (dx * dx)
858 } else {
859 (u_ghost - 2.0 * u[[k, j, i]] + u[[k, j, i - 1]]) / (dx * dx)
860 };
861
862 // Diffusion terms in y and z direction (use central difference)
863 let d2u_dy2 = (u[[k, j + 1, i]] - 2.0 * u[[k, j, i]]
864 + u[[k, j - 1, i]])
865 / (dy * dy);
866 let d2u_dz2 = (u[[k + 1, j, i]] - 2.0 * u[[k, j, i]]
867 + u[[k - 1, j, i]])
868 / (dz * dz);
869
870 let diffusion_term_x =
871 (solver.diffusion_x)(x, y, z, t, u_val) * d2u_dx2;
872 let diffusion_term_y =
873 (solver.diffusion_y)(x, y, z, t, u_val) * d2u_dy2;
874 let diffusion_term_z =
875 (solver.diffusion_z)(x, y, z, t, u_val) * d2u_dz2;
876
877 // Advection terms
878 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
879 // Use one-sided difference for advection in x
880 let du_dx = if i == 0 {
881 (u[[k, j, i + 1]] - u[[k, j, i]]) / dx
882 } else {
883 (u[[k, j, i]] - u[[k, j, i - 1]]) / dx
884 };
885 advection_x(x, y, z, t, u_val) * du_dx
886 } else {
887 0.0
888 };
889
890 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
891 // Use central difference for advection in y
892 let du_dy = (u[[k, j + 1, i]] - u[[k, j - 1, i]]) / (2.0 * dy);
893 advection_y(x, y, z, t, u_val) * du_dy
894 } else {
895 0.0
896 };
897
898 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
899 // Use central difference for advection in z
900 let du_dz = (u[[k + 1, j, i]] - u[[k - 1, j, i]]) / (2.0 * dz);
901 advection_z(x, y, z, t, u_val) * du_dz
902 } else {
903 0.0
904 };
905
906 // Reaction term
907 let reaction_term = if let Some(reaction) = &solver.reaction_term {
908 reaction(x, y, z, t, u_val)
909 } else {
910 0.0
911 };
912
913 dudt[[k, j, i]] = diffusion_term_x
914 + diffusion_term_y
915 + diffusion_term_z
916 + advection_term_x
917 + advection_term_y
918 + advection_term_z
919 + reaction_term;
920 }
921 }
922 } else if let Some(j) = j_fixed {
923 // Y-direction boundary (front or back face)
924 for k in 1..nz - 1 {
925 for i in 1..nx - 1 {
926 let z = z_grid[k];
927 let y = y_grid[j];
928 let x = x_grid[i];
929 let u_val = u[[k, j, i]];
930
931 // Solve for ghost point value using Robin condition
932 let du_dn = (c - a * u_val) / b;
933
934 // For front boundary (j=0), ghost point is at j-1
935 // For back boundary (j=ny-1), ghost point is at j+1
936 let u_ghost = if j == 0 {
937 u[[k, j, i]] - dy * du_dn
938 } else {
939 u[[k, j, i]] + dy * du_dn
940 };
941
942 // Diffusion term in y direction
943 let d2u_dy2 = if j == 0 {
944 (u[[k, j + 1, i]] - 2.0 * u[[k, j, i]] + u_ghost) / (dy * dy)
945 } else {
946 (u_ghost - 2.0 * u[[k, j, i]] + u[[k, j - 1, i]]) / (dy * dy)
947 };
948
949 // Diffusion terms in x and z direction (use central difference)
950 let d2u_dx2 = (u[[k, j, i + 1]] - 2.0 * u[[k, j, i]]
951 + u[[k, j, i - 1]])
952 / (dx * dx);
953 let d2u_dz2 = (u[[k + 1, j, i]] - 2.0 * u[[k, j, i]]
954 + u[[k - 1, j, i]])
955 / (dz * dz);
956
957 let diffusion_term_x =
958 (solver.diffusion_x)(x, y, z, t, u_val) * d2u_dx2;
959 let diffusion_term_y =
960 (solver.diffusion_y)(x, y, z, t, u_val) * d2u_dy2;
961 let diffusion_term_z =
962 (solver.diffusion_z)(x, y, z, t, u_val) * d2u_dz2;
963
964 // Advection terms
965 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
966 // Use central difference for advection in x
967 let du_dx = (u[[k, j, i + 1]] - u[[k, j, i - 1]]) / (2.0 * dx);
968 advection_x(x, y, z, t, u_val) * du_dx
969 } else {
970 0.0
971 };
972
973 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
974 // Use one-sided difference for advection in y
975 let du_dy = if j == 0 {
976 (u[[k, j + 1, i]] - u[[k, j, i]]) / dy
977 } else {
978 (u[[k, j, i]] - u[[k, j - 1, i]]) / dy
979 };
980 advection_y(x, y, z, t, u_val) * du_dy
981 } else {
982 0.0
983 };
984
985 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
986 // Use central difference for advection in z
987 let du_dz = (u[[k + 1, j, i]] - u[[k - 1, j, i]]) / (2.0 * dz);
988 advection_z(x, y, z, t, u_val) * du_dz
989 } else {
990 0.0
991 };
992
993 // Reaction term
994 let reaction_term = if let Some(reaction) = &solver.reaction_term {
995 reaction(x, y, z, t, u_val)
996 } else {
997 0.0
998 };
999
1000 dudt[[k, j, i]] = diffusion_term_x
1001 + diffusion_term_y
1002 + diffusion_term_z
1003 + advection_term_x
1004 + advection_term_y
1005 + advection_term_z
1006 + reaction_term;
1007 }
1008 }
1009 } else if let Some(k) = k_fixed {
1010 // Z-direction boundary (bottom or top face)
1011 for j in 1..ny - 1 {
1012 for i in 1..nx - 1 {
1013 let z = z_grid[k];
1014 let y = y_grid[j];
1015 let x = x_grid[i];
1016 let u_val = u[[k, j, i]];
1017
1018 // Solve for ghost point value using Robin condition
1019 let du_dn = (c - a * u_val) / b;
1020
1021 // For bottom boundary (k=0), ghost point is at k-1
1022 // For top boundary (k=nz-1), ghost point is at k+1
1023 let u_ghost = if k == 0 {
1024 u[[k, j, i]] - dz * du_dn
1025 } else {
1026 u[[k, j, i]] + dz * du_dn
1027 };
1028
1029 // Diffusion term in z direction
1030 let d2u_dz2 = if k == 0 {
1031 (u[[k + 1, j, i]] - 2.0 * u[[k, j, i]] + u_ghost) / (dz * dz)
1032 } else {
1033 (u_ghost - 2.0 * u[[k, j, i]] + u[[k - 1, j, i]]) / (dz * dz)
1034 };
1035
1036 // Diffusion terms in x and y direction (use central difference)
1037 let d2u_dx2 = (u[[k, j, i + 1]] - 2.0 * u[[k, j, i]]
1038 + u[[k, j, i - 1]])
1039 / (dx * dx);
1040 let d2u_dy2 = (u[[k, j + 1, i]] - 2.0 * u[[k, j, i]]
1041 + u[[k, j - 1, i]])
1042 / (dy * dy);
1043
1044 let diffusion_term_x =
1045 (solver.diffusion_x)(x, y, z, t, u_val) * d2u_dx2;
1046 let diffusion_term_y =
1047 (solver.diffusion_y)(x, y, z, t, u_val) * d2u_dy2;
1048 let diffusion_term_z =
1049 (solver.diffusion_z)(x, y, z, t, u_val) * d2u_dz2;
1050
1051 // Advection terms
1052 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
1053 // Use central difference for advection in x
1054 let du_dx = (u[[k, j, i + 1]] - u[[k, j, i - 1]]) / (2.0 * dx);
1055 advection_x(x, y, z, t, u_val) * du_dx
1056 } else {
1057 0.0
1058 };
1059
1060 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
1061 // Use central difference for advection in y
1062 let du_dy = (u[[k, j + 1, i]] - u[[k, j - 1, i]]) / (2.0 * dy);
1063 advection_y(x, y, z, t, u_val) * du_dy
1064 } else {
1065 0.0
1066 };
1067
1068 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
1069 // Use one-sided difference for advection in z
1070 let du_dz = if k == 0 {
1071 (u[[k + 1, j, i]] - u[[k, j, i]]) / dz
1072 } else {
1073 (u[[k, j, i]] - u[[k - 1, j, i]]) / dz
1074 };
1075 advection_z(x, y, z, t, u_val) * du_dz
1076 } else {
1077 0.0
1078 };
1079
1080 // Reaction term
1081 let reaction_term = if let Some(reaction) = &solver.reaction_term {
1082 reaction(x, y, z, t, u_val)
1083 } else {
1084 0.0
1085 };
1086
1087 dudt[[k, j, i]] = diffusion_term_x
1088 + diffusion_term_y
1089 + diffusion_term_z
1090 + advection_term_x
1091 + advection_term_y
1092 + advection_term_z
1093 + reaction_term;
1094 }
1095 }
1096 }
1097 }
1098 }
1099 BoundaryConditionType::Periodic => {
1100 // Periodic boundary conditions: values and derivatives wrap around
1101 // Handle 3D periodic boundaries with proper edge and corner treatment
1102
1103 if let Some(i) = i_fixed {
1104 // x-direction periodic boundary (left or right face)
1105 for k in 0..nz {
1106 for j in 0..ny {
1107 let z = z_grid[k];
1108 let y = y_grid[j];
1109 let x = x_grid[i];
1110 let u_val = u[[k, j, i]];
1111 let t = 0.0; // Time parameter - would need to be passed in for time-dependent BCs
1112
1113 // Apply the same computation as for interior points but with periodic wrapping
1114 let left_val = if i == 0 {
1115 u[[k, j, nx - 1]]
1116 } else {
1117 u[[k, j, i - 1]]
1118 };
1119 let right_val = if i == nx - 1 {
1120 u[[k, j, 0]]
1121 } else {
1122 u[[k, j, i + 1]]
1123 };
1124 let front_val = if j == 0 {
1125 u[[k, ny - 1, i]]
1126 } else {
1127 u[[k, j - 1, i]]
1128 };
1129 let back_val = if j == ny - 1 {
1130 u[[k, 0, i]]
1131 } else {
1132 u[[k, j + 1, i]]
1133 };
1134 let bottom_val = if k == 0 {
1135 u[[nz - 1, j, i]]
1136 } else {
1137 u[[k - 1, j, i]]
1138 };
1139 let top_val = if k == nz - 1 {
1140 u[[0, j, i]]
1141 } else {
1142 u[[k + 1, j, i]]
1143 };
1144
1145 // Diffusion terms with periodic wrapping
1146 let d_coeff_x = (solver.diffusion_x)(x, y, z, t, u_val);
1147 let d2u_dx2 = (right_val - 2.0 * u_val + left_val) / (dx * dx);
1148 let diffusion_term_x = d_coeff_x * d2u_dx2;
1149
1150 let d_coeff_y = (solver.diffusion_y)(x, y, z, t, u_val);
1151 let d2u_dy2 = (back_val - 2.0 * u_val + front_val) / (dy * dy);
1152 let diffusion_term_y = d_coeff_y * d2u_dy2;
1153
1154 let d_coeff_z = (solver.diffusion_z)(x, y, z, t, u_val);
1155 let d2u_dz2 = (top_val - 2.0 * u_val + bottom_val) / (dz * dz);
1156 let diffusion_term_z = d_coeff_z * d2u_dz2;
1157
1158 // Advection terms with periodic wrapping
1159 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
1160 let a_coeff = advection_x(x, y, z, t, u_val);
1161 let du_dx = (right_val - left_val) / (2.0 * dx);
1162 a_coeff * du_dx
1163 } else {
1164 0.0
1165 };
1166
1167 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
1168 let a_coeff = advection_y(x, y, z, t, u_val);
1169 let du_dy = (back_val - front_val) / (2.0 * dy);
1170 a_coeff * du_dy
1171 } else {
1172 0.0
1173 };
1174
1175 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
1176 let a_coeff = advection_z(x, y, z, t, u_val);
1177 let du_dz = (top_val - bottom_val) / (2.0 * dz);
1178 a_coeff * du_dz
1179 } else {
1180 0.0
1181 };
1182
1183 // Reaction term
1184 let reaction_term = if let Some(reaction) = &solver.reaction_term {
1185 reaction(x, y, z, t, u_val)
1186 } else {
1187 0.0
1188 };
1189
1190 dudt[[k, j, i]] = diffusion_term_x
1191 + diffusion_term_y
1192 + diffusion_term_z
1193 + advection_term_x
1194 + advection_term_y
1195 + advection_term_z
1196 + reaction_term;
1197 }
1198 }
1199 } else if let Some(j) = j_fixed {
1200 // y-direction periodic boundary (front or back face)
1201 for k in 0..nz {
1202 for i in 0..nx {
1203 let z = z_grid[k];
1204 let y = y_grid[j];
1205 let x = x_grid[i];
1206 let u_val = u[[k, j, i]];
1207 let t = 0.0; // Time parameter
1208
1209 // Apply the same computation as for interior points but with periodic wrapping
1210 let left_val = if i == 0 {
1211 u[[k, j, nx - 1]]
1212 } else {
1213 u[[k, j, i - 1]]
1214 };
1215 let right_val = if i == nx - 1 {
1216 u[[k, j, 0]]
1217 } else {
1218 u[[k, j, i + 1]]
1219 };
1220 let front_val = if j == 0 {
1221 u[[k, ny - 1, i]]
1222 } else {
1223 u[[k, j - 1, i]]
1224 };
1225 let back_val = if j == ny - 1 {
1226 u[[k, 0, i]]
1227 } else {
1228 u[[k, j + 1, i]]
1229 };
1230 let bottom_val = if k == 0 {
1231 u[[nz - 1, j, i]]
1232 } else {
1233 u[[k - 1, j, i]]
1234 };
1235 let top_val = if k == nz - 1 {
1236 u[[0, j, i]]
1237 } else {
1238 u[[k + 1, j, i]]
1239 };
1240
1241 // Diffusion terms with periodic wrapping
1242 let d_coeff_x = (solver.diffusion_x)(x, y, z, t, u_val);
1243 let d2u_dx2 = (right_val - 2.0 * u_val + left_val) / (dx * dx);
1244 let diffusion_term_x = d_coeff_x * d2u_dx2;
1245
1246 let d_coeff_y = (solver.diffusion_y)(x, y, z, t, u_val);
1247 let d2u_dy2 = (back_val - 2.0 * u_val + front_val) / (dy * dy);
1248 let diffusion_term_y = d_coeff_y * d2u_dy2;
1249
1250 let d_coeff_z = (solver.diffusion_z)(x, y, z, t, u_val);
1251 let d2u_dz2 = (top_val - 2.0 * u_val + bottom_val) / (dz * dz);
1252 let diffusion_term_z = d_coeff_z * d2u_dz2;
1253
1254 // Advection terms with periodic wrapping
1255 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
1256 let a_coeff = advection_x(x, y, z, t, u_val);
1257 let du_dx = (right_val - left_val) / (2.0 * dx);
1258 a_coeff * du_dx
1259 } else {
1260 0.0
1261 };
1262
1263 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
1264 let a_coeff = advection_y(x, y, z, t, u_val);
1265 let du_dy = (back_val - front_val) / (2.0 * dy);
1266 a_coeff * du_dy
1267 } else {
1268 0.0
1269 };
1270
1271 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
1272 let a_coeff = advection_z(x, y, z, t, u_val);
1273 let du_dz = (top_val - bottom_val) / (2.0 * dz);
1274 a_coeff * du_dz
1275 } else {
1276 0.0
1277 };
1278
1279 // Reaction term
1280 let reaction_term = if let Some(reaction) = &solver.reaction_term {
1281 reaction(x, y, z, t, u_val)
1282 } else {
1283 0.0
1284 };
1285
1286 dudt[[k, j, i]] = diffusion_term_x
1287 + diffusion_term_y
1288 + diffusion_term_z
1289 + advection_term_x
1290 + advection_term_y
1291 + advection_term_z
1292 + reaction_term;
1293 }
1294 }
1295 } else if let Some(k) = k_fixed {
1296 // z-direction periodic boundary (top or bottom face)
1297 for j in 0..ny {
1298 for i in 0..nx {
1299 let z = z_grid[k];
1300 let y = y_grid[j];
1301 let x = x_grid[i];
1302 let u_val = u[[k, j, i]];
1303 let t = 0.0; // Time parameter
1304
1305 // Apply the same computation as for interior points but with periodic wrapping
1306 let left_val = if i == 0 {
1307 u[[k, j, nx - 1]]
1308 } else {
1309 u[[k, j, i - 1]]
1310 };
1311 let right_val = if i == nx - 1 {
1312 u[[k, j, 0]]
1313 } else {
1314 u[[k, j, i + 1]]
1315 };
1316 let front_val = if j == 0 {
1317 u[[k, ny - 1, i]]
1318 } else {
1319 u[[k, j - 1, i]]
1320 };
1321 let back_val = if j == ny - 1 {
1322 u[[k, 0, i]]
1323 } else {
1324 u[[k, j + 1, i]]
1325 };
1326 let bottom_val = if k == 0 {
1327 u[[nz - 1, j, i]]
1328 } else {
1329 u[[k - 1, j, i]]
1330 };
1331 let top_val = if k == nz - 1 {
1332 u[[0, j, i]]
1333 } else {
1334 u[[k + 1, j, i]]
1335 };
1336
1337 // Diffusion terms with periodic wrapping
1338 let d_coeff_x = (solver.diffusion_x)(x, y, z, t, u_val);
1339 let d2u_dx2 = (right_val - 2.0 * u_val + left_val) / (dx * dx);
1340 let diffusion_term_x = d_coeff_x * d2u_dx2;
1341
1342 let d_coeff_y = (solver.diffusion_y)(x, y, z, t, u_val);
1343 let d2u_dy2 = (back_val - 2.0 * u_val + front_val) / (dy * dy);
1344 let diffusion_term_y = d_coeff_y * d2u_dy2;
1345
1346 let d_coeff_z = (solver.diffusion_z)(x, y, z, t, u_val);
1347 let d2u_dz2 = (top_val - 2.0 * u_val + bottom_val) / (dz * dz);
1348 let diffusion_term_z = d_coeff_z * d2u_dz2;
1349
1350 // Advection terms with periodic wrapping
1351 let advection_term_x = if let Some(advection_x) = &solver.advection_x {
1352 let a_coeff = advection_x(x, y, z, t, u_val);
1353 let du_dx = (right_val - left_val) / (2.0 * dx);
1354 a_coeff * du_dx
1355 } else {
1356 0.0
1357 };
1358
1359 let advection_term_y = if let Some(advection_y) = &solver.advection_y {
1360 let a_coeff = advection_y(x, y, z, t, u_val);
1361 let du_dy = (back_val - front_val) / (2.0 * dy);
1362 a_coeff * du_dy
1363 } else {
1364 0.0
1365 };
1366
1367 let advection_term_z = if let Some(advection_z) = &solver.advection_z {
1368 let a_coeff = advection_z(x, y, z, t, u_val);
1369 let du_dz = (top_val - bottom_val) / (2.0 * dz);
1370 a_coeff * du_dz
1371 } else {
1372 0.0
1373 };
1374
1375 // Reaction term
1376 let reaction_term = if let Some(reaction) = &solver.reaction_term {
1377 reaction(x, y, z, t, u_val)
1378 } else {
1379 0.0
1380 };
1381
1382 dudt[[k, j, i]] = diffusion_term_x
1383 + diffusion_term_y
1384 + diffusion_term_z
1385 + advection_term_x
1386 + advection_term_y
1387 + advection_term_z
1388 + reaction_term;
1389 }
1390 }
1391 }
1392 }
1393 }
1394}
1395
1396// Helper function to apply Dirichlet boundary conditions to initial condition
1397#[allow(dead_code)]
1398fn apply_dirichlet_conditions_to_initial_3d(
1399 u0: &mut Array3<f64>,
1400 boundary_conditions: &[BoundaryCondition<f64>],
1401 x_grid: &Array1<f64>,
1402 y_grid: &Array1<f64>,
1403 z_grid: &Array1<f64>,
1404) {
1405 let nx = x_grid.len();
1406 let ny = y_grid.len();
1407 let nz = z_grid.len();
1408
1409 for bc in boundary_conditions {
1410 if bc.bc_type == BoundaryConditionType::Dirichlet {
1411 match (bc.dimension, bc.location) {
1412 (0, BoundaryLocation::Lower) => {
1413 // Left boundary (x = x[0])
1414 for k in 0..nz {
1415 for j in 0..ny {
1416 u0[[k, j, 0]] = bc.value;
1417 }
1418 }
1419 }
1420 (0, BoundaryLocation::Upper) => {
1421 // Right boundary (x = x[nx-1])
1422 for k in 0..nz {
1423 for j in 0..ny {
1424 u0[[k, j, nx - 1]] = bc.value;
1425 }
1426 }
1427 }
1428 (1, BoundaryLocation::Lower) => {
1429 // Front boundary (y = y[0])
1430 for k in 0..nz {
1431 for i in 0..nx {
1432 u0[[k, 0, i]] = bc.value;
1433 }
1434 }
1435 }
1436 (1, BoundaryLocation::Upper) => {
1437 // Back boundary (y = y[ny-1])
1438 for k in 0..nz {
1439 for i in 0..nx {
1440 u0[[k, ny - 1, i]] = bc.value;
1441 }
1442 }
1443 }
1444 (2, BoundaryLocation::Lower) => {
1445 // Bottom boundary (z = z[0])
1446 for j in 0..ny {
1447 for i in 0..nx {
1448 u0[[0, j, i]] = bc.value;
1449 }
1450 }
1451 }
1452 (2, BoundaryLocation::Upper) => {
1453 // Top boundary (z = z[nz-1])
1454 for j in 0..ny {
1455 for i in 0..nx {
1456 u0[[nz - 1, j, i]] = bc.value;
1457 }
1458 }
1459 }
1460 _ => {
1461 // Invalid dimension
1462 // We'll just ignore this case - validation occurs during initialization
1463 }
1464 }
1465 }
1466 }
1467}
1468
1469/// Convert a MOL3DResult to a PDESolution
1470impl From<MOL3DResult> for PDESolution<f64> {
1471 fn from(result: MOL3DResult) -> Self {
1472 let mut grids = Vec::new();
1473
1474 // Add time grid
1475 grids.push(result.t.clone());
1476
1477 // Extract spatial grids from solution shape
1478 let nt = result.t.len();
1479 let nz = result.u.shape()[1];
1480 let ny = result.u.shape()[2];
1481 let nx = result.u.shape()[3];
1482
1483 // Create spatial grids (we don't have the actual grid values, so use linspace)
1484 let z_grid = Array1::linspace(0.0, 1.0, nz);
1485 let y_grid = Array1::linspace(0.0, 1.0, ny);
1486 let x_grid = Array1::linspace(0.0, 1.0, nx);
1487 grids.push(z_grid);
1488 grids.push(y_grid);
1489 grids.push(x_grid);
1490
1491 // Convert the 4D array to a list of 2D arrays
1492 // For PDESolution format, we need to flatten the spatial dimensions
1493 let mut values = Vec::new();
1494 let total_spatial_points = nx * ny * nz;
1495
1496 // Reshape the 4D array (time, z, y, x) to 2D (time, spatial_points)
1497 let u_reshaped = result
1498 .u
1499 .into_shape_with_order((nt, total_spatial_points))
1500 .expect("Operation failed");
1501
1502 // Create a single 2D array with time on one dimension and flattened spatial points on the other
1503 values.push(u_reshaped.t().to_owned());
1504
1505 // Create solver info
1506 let info = PDESolverInfo {
1507 num_iterations: 0, // This information is not available directly
1508 computation_time: result.computation_time,
1509 residual_norm: None,
1510 convergence_history: None,
1511 method: "Method of Lines (3D)".to_string(),
1512 };
1513
1514 PDESolution {
1515 grids,
1516 values,
1517 error_estimate: None,
1518 info,
1519 }
1520 }
1521}