Skip to main content

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}