scirs2_integrate/pde/method_of_lines/
mod2d.rs

1//! Method of Lines for 2D parabolic PDEs
2//!
3//! This module implements the Method of Lines (MOL) approach for solving
4//! 2D parabolic PDEs, such as the 2D heat equation and 2D advection-diffusion.
5
6use scirs2_core::ndarray::{s, Array1, Array2, Array3, ArrayView1, ArrayView2};
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 2D coefficient function taking (x, y, t, u) and returning a value
18type CoeffFn2D = Arc<dyn Fn(f64, f64, f64, f64) -> f64 + Send + Sync>;
19
20/// Result of 2D method of lines solution
21pub struct MOL2DResult {
22    /// Time points
23    pub t: Array1<f64>,
24
25    /// Solution values, indexed as [time, y, x]
26    pub u: Array3<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 2D parabolic PDEs
36///
37/// Solves equations of the form:
38/// ∂u/∂t = ∂/∂x(D_x(x,y,t,u) ∂u/∂x) + ∂/∂y(D_y(x,y,t,u) ∂u/∂y) + v_x(x,y,t,u) ∂u/∂x + v_y(x,y,t,u) ∂u/∂y + f(x,y,t,u)
39#[derive(Clone)]
40pub struct MOLParabolicSolver2D {
41    /// Spatial domain
42    domain: Domain,
43
44    /// Time range [t_start, t_end]
45    time_range: [f64; 2],
46
47    /// Diffusion coefficient function D_x(x, y, t, u) for ∂²u/∂x²
48    diffusion_x: CoeffFn2D,
49
50    /// Diffusion coefficient function D_y(x, y, t, u) for ∂²u/∂y²
51    diffusion_y: CoeffFn2D,
52
53    /// Advection coefficient function v_x(x, y, t, u) for ∂u/∂x
54    advection_x: Option<CoeffFn2D>,
55
56    /// Advection coefficient function v_y(x, y, t, u) for ∂u/∂y
57    advection_y: Option<CoeffFn2D>,
58
59    /// Reaction term function f(x, y, t, u)
60    reaction_term: Option<CoeffFn2D>,
61
62    /// Initial condition function u(x, y, 0)
63    initial_condition: Arc<dyn Fn(f64, f64) -> f64 + Send + Sync>,
64
65    /// Boundary conditions
66    boundary_conditions: Vec<BoundaryCondition<f64>>,
67
68    /// Finite difference scheme for spatial discretization
69    fd_scheme: FiniteDifferenceScheme,
70
71    /// Solver options
72    options: super::MOLOptions,
73}
74
75impl MOLParabolicSolver2D {
76    /// Create a new Method of Lines solver for 2D parabolic PDEs
77    #[allow(clippy::too_many_arguments)]
78    pub fn new(
79        domain: Domain,
80        time_range: [f64; 2],
81        diffusion_x: impl Fn(f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
82        diffusion_y: impl Fn(f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
83        initial_condition: impl Fn(f64, f64) -> f64 + Send + Sync + 'static,
84        boundary_conditions: Vec<BoundaryCondition<f64>>,
85        options: Option<super::MOLOptions>,
86    ) -> PDEResult<Self> {
87        // Validate the domain
88        if domain.dimensions() != 2 {
89            return Err(PDEError::DomainError(
90                "Domain must be 2-dimensional for 2D parabolic solver".to_string(),
91            ));
92        }
93
94        // Validate time _range
95        if time_range[0] >= time_range[1] {
96            return Err(PDEError::DomainError(
97                "Invalid time _range: start must be less than end".to_string(),
98            ));
99        }
100
101        // Validate boundary _conditions
102        if boundary_conditions.len() != 4 {
103            return Err(PDEError::BoundaryConditions(
104                "2D parabolic PDE requires exactly 4 boundary _conditions (one for each edge)"
105                    .to_string(),
106            ));
107        }
108
109        // Ensure we have boundary _conditions for all dimensions/edges
110        let has_x_lower = boundary_conditions
111            .iter()
112            .any(|bc| bc.location == BoundaryLocation::Lower && bc.dimension == 0);
113        let has_x_upper = boundary_conditions
114            .iter()
115            .any(|bc| bc.location == BoundaryLocation::Upper && bc.dimension == 0);
116        let has_y_lower = boundary_conditions
117            .iter()
118            .any(|bc| bc.location == BoundaryLocation::Lower && bc.dimension == 1);
119        let has_y_upper = boundary_conditions
120            .iter()
121            .any(|bc| bc.location == BoundaryLocation::Upper && bc.dimension == 1);
122
123        if !has_x_lower || !has_x_upper || !has_y_lower || !has_y_upper {
124            return Err(PDEError::BoundaryConditions(
125                "2D parabolic PDE requires boundary _conditions for all edges of the domain"
126                    .to_string(),
127            ));
128        }
129
130        Ok(MOLParabolicSolver2D {
131            domain,
132            time_range,
133            diffusion_x: Arc::new(diffusion_x),
134            diffusion_y: Arc::new(diffusion_y),
135            advection_x: None,
136            advection_y: None,
137            reaction_term: None,
138            initial_condition: Arc::new(initial_condition),
139            boundary_conditions,
140            fd_scheme: FiniteDifferenceScheme::CentralDifference,
141            options: options.unwrap_or_default(),
142        })
143    }
144
145    /// Add advection terms to the PDE
146    pub fn with_advection(
147        mut self,
148        advection_x: impl Fn(f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
149        advection_y: impl Fn(f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
150    ) -> Self {
151        self.advection_x = Some(Arc::new(advection_x));
152        self.advection_y = Some(Arc::new(advection_y));
153        self
154    }
155
156    /// Add a reaction term to the PDE
157    pub fn with_reaction(
158        mut self,
159        reaction_term: impl Fn(f64, f64, f64, f64) -> f64 + Send + Sync + 'static,
160    ) -> Self {
161        self.reaction_term = Some(Arc::new(reaction_term));
162        self
163    }
164
165    /// Set the finite difference scheme for spatial discretization
166    pub fn with_fd_scheme(mut self, scheme: FiniteDifferenceScheme) -> Self {
167        self.fd_scheme = scheme;
168        self
169    }
170
171    /// Solve the 2D parabolic PDE
172    pub fn solve(&self) -> PDEResult<MOL2DResult> {
173        let start_time = Instant::now();
174
175        // Generate spatial grids
176        let x_grid = self.domain.grid(0)?;
177        let y_grid = self.domain.grid(1)?;
178        let nx = x_grid.len();
179        let ny = y_grid.len();
180        let dx = self.domain.grid_spacing(0)?;
181        let dy = self.domain.grid_spacing(1)?;
182
183        // Create initial condition 2D grid and flatten it for the ODE solver
184        let mut u0 = Array2::zeros((ny, nx));
185        for j in 0..ny {
186            for i in 0..nx {
187                u0[[j, i]] = (self.initial_condition)(x_grid[i], y_grid[j]);
188            }
189        }
190
191        // Flatten the 2D grid into a 1D array for the ODE solver
192        let _u0_flat = u0.clone().into_shape_with_order(nx * ny).unwrap();
193
194        // Clone grids for the closure
195        let x_grid_closure = x_grid.clone();
196        let y_grid_closure = y_grid.clone();
197
198        // Clone grids for later use
199        let x_grid_apply = x_grid.clone();
200        let y_grid_apply = y_grid.clone();
201
202        // Extract options before moving self
203        let ode_options = ODEOptions {
204            method: self.options.ode_method,
205            rtol: self.options.rtol,
206            atol: self.options.atol,
207            max_steps: self.options.max_steps.unwrap_or(500),
208            h0: None,
209            max_step: None,
210            min_step: None,
211            dense_output: true,
212            max_order: None,
213            jac: None,
214            use_banded_jacobian: false,
215            ml: None,
216            mu: None,
217            mass_matrix: None,
218            jacobian_strategy: None,
219        };
220
221        let time_range = self.time_range;
222        let boundary_conditions = self.boundary_conditions.clone();
223
224        // Move self into closure
225        let solver = self;
226
227        // Construct the ODE function that represents the PDE after spatial discretization
228        let ode_func = move |t: f64, u_flat: ArrayView1<f64>| -> Array1<f64> {
229            // Reshape the flattened array back to 2D for easier indexing
230            let u = u_flat.into_shape_with_order((ny, nx)).unwrap();
231            let mut dudt = Array2::zeros((ny, nx));
232
233            // Apply finite difference approximations for interior points
234            for j in 1..ny - 1 {
235                for i in 1..nx - 1 {
236                    let y = y_grid[j];
237                    let x = x_grid[i];
238                    let u_val = u[[j, i]];
239
240                    // Diffusion terms
241                    let d2u_dx2 = (u[[j, i + 1]] - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx);
242                    let d2u_dy2 = (u[[j + 1, i]] - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy);
243
244                    let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
245                    let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
246
247                    // Advection terms
248                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
249                        let du_dx = (u[[j, i + 1]] - u[[j, i - 1]]) / (2.0 * dx);
250                        advection_x(x, y, t, u_val) * du_dx
251                    } else {
252                        0.0
253                    };
254
255                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
256                        let du_dy = (u[[j + 1, i]] - u[[j - 1, i]]) / (2.0 * dy);
257                        advection_y(x, y, t, u_val) * du_dy
258                    } else {
259                        0.0
260                    };
261
262                    // Reaction term
263                    let reaction_val = if let Some(reaction) = &solver.reaction_term {
264                        reaction(x, y, t, u_val)
265                    } else {
266                        0.0
267                    };
268
269                    dudt[[j, i]] = diffusion_term_x
270                        + diffusion_term_y
271                        + advection_term_x
272                        + advection_term_y
273                        + reaction_val;
274                }
275            }
276
277            // Apply boundary conditions
278            for bc in &solver.boundary_conditions {
279                match (bc.dimension, bc.location) {
280                    // X-direction boundaries
281                    (0, BoundaryLocation::Lower) => {
282                        // Apply boundary condition at x[0] (left edge)
283                        apply_boundary_condition_2d(
284                            &mut dudt,
285                            &u,
286                            &x_grid_closure,
287                            &y_grid_closure,
288                            bc,
289                            dx,
290                            dy,
291                            Some(0),
292                            None,
293                            None,
294                            Some(ny),
295                            solver,
296                        );
297                    }
298                    (0, BoundaryLocation::Upper) => {
299                        // Apply boundary condition at x[nx-1] (right edge)
300                        apply_boundary_condition_2d(
301                            &mut dudt,
302                            &u,
303                            &x_grid_closure,
304                            &y_grid_closure,
305                            bc,
306                            dx,
307                            dy,
308                            Some(nx - 1),
309                            None,
310                            None,
311                            Some(ny),
312                            solver,
313                        );
314                    }
315                    // Y-direction boundaries
316                    (1, BoundaryLocation::Lower) => {
317                        // Apply boundary condition at y[0] (bottom edge)
318                        apply_boundary_condition_2d(
319                            &mut dudt,
320                            &u,
321                            &x_grid_closure,
322                            &y_grid_closure,
323                            bc,
324                            dx,
325                            dy,
326                            None,
327                            Some(0),
328                            Some(nx),
329                            None,
330                            solver,
331                        );
332                    }
333                    (1, BoundaryLocation::Upper) => {
334                        // Apply boundary condition at y[ny-1] (top edge)
335                        apply_boundary_condition_2d(
336                            &mut dudt,
337                            &u,
338                            &x_grid_closure,
339                            &y_grid_closure,
340                            bc,
341                            dx,
342                            dy,
343                            None,
344                            Some(ny - 1),
345                            Some(nx),
346                            None,
347                            solver,
348                        );
349                    }
350                    _ => {
351                        // Invalid dimension
352                        // We'll just ignore this case for now - validation occurs during initialization
353                    }
354                }
355            }
356
357            // Flatten the 2D dudt back to 1D for the ODE solver
358            dudt.into_shape_with_order(nx * ny).unwrap()
359        };
360
361        // Apply Dirichlet boundary conditions to initial condition
362        apply_dirichlet_conditions_to_initial(
363            &mut u0,
364            &boundary_conditions,
365            &x_grid_apply,
366            &y_grid_apply,
367        );
368
369        let u0_flat = u0.clone().into_shape_with_order(nx * ny).unwrap();
370
371        // Solve the ODE system
372        let ode_result = solve_ivp(ode_func, time_range, u0_flat, Some(ode_options))?;
373
374        // Extract results
375        let computation_time = start_time.elapsed().as_secs_f64();
376
377        // Reshape the ODE result to match the spatial grid
378        let t = ode_result.t.clone();
379        let nt = t.len();
380
381        // Create a 3D array with dimensions [time, y, x]
382        let mut u_3d = Array3::zeros((nt, ny, nx));
383
384        for (time_idx, y_flat) in ode_result.y.iter().enumerate() {
385            let u_2d = y_flat.clone().into_shape_with_order((ny, nx)).unwrap();
386            for j in 0..ny {
387                for i in 0..nx {
388                    u_3d[[time_idx, j, i]] = u_2d[[j, i]];
389                }
390            }
391        }
392
393        let ode_info = Some(format!(
394            "ODE steps: {}, function evaluations: {}, successful steps: {}",
395            ode_result.n_steps, ode_result.n_eval, ode_result.n_accepted,
396        ));
397
398        Ok(MOL2DResult {
399            t: ode_result.t.into(),
400            u: u_3d,
401            ode_info,
402            computation_time,
403        })
404    }
405}
406
407// Helper function to apply boundary conditions in 2D
408#[allow(clippy::too_many_arguments)]
409#[allow(dead_code)]
410fn apply_boundary_condition_2d(
411    dudt: &mut Array2<f64>,
412    u: &ArrayView2<f64>,
413    x_grid: &Array1<f64>,
414    y_grid: &Array1<f64>,
415    bc: &BoundaryCondition<f64>,
416    dx: f64,
417    dy: f64,
418    i_fixed: Option<usize>,
419    j_fixed: Option<usize>,
420    i_range: Option<usize>,
421    j_range: Option<usize>,
422    solver: &MOLParabolicSolver2D,
423) {
424    let nx = x_grid.len();
425    let ny = y_grid.len();
426
427    let i_range = i_range.unwrap_or(1);
428    let j_range = j_range.unwrap_or(1);
429
430    match bc.bc_type {
431        BoundaryConditionType::Dirichlet => {
432            // Fixed value: u = bc.value
433            // For Dirichlet, we set dudt = 0 to maintain the _fixed value
434            if let Some(i) = i_fixed {
435                for j in 0..j_range {
436                    dudt[[j, i]] = 0.0;
437                }
438            } else if let Some(j) = j_fixed {
439                for i in 0..i_range {
440                    dudt[[j, i]] = 0.0;
441                }
442            }
443        }
444        BoundaryConditionType::Neumann => {
445            // Gradient in the direction normal to the boundary
446            if let Some(i) = i_fixed {
447                // x-direction boundary (left or right)
448                for j in 1..ny - 1 {
449                    let y = y_grid[j];
450                    let x = x_grid[i];
451                    let u_val = u[[j, i]];
452                    let t = 0.0; // Time is not used here
453
454                    // Use one-sided difference for the normal direction
455                    let du_dn = bc.value; // Given Neumann value
456
457                    // For left boundary (i=0), ghost point is at i-1
458                    // For right boundary (i=nx-1), ghost point is at i+1
459                    let u_ghost = if i == 0 {
460                        u[[j, i]] - dx * du_dn
461                    } else {
462                        u[[j, i]] + dx * du_dn
463                    };
464
465                    // Diffusion term in x direction
466                    let d2u_dx2 = if i == 0 {
467                        (u[[j, i + 1]] - 2.0 * u[[j, i]] + u_ghost) / (dx * dx)
468                    } else {
469                        (u_ghost - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx)
470                    };
471
472                    // Diffusion term in y direction (use central difference)
473                    let d2u_dy2 = (u[[j + 1, i]] - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy);
474
475                    let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
476                    let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
477
478                    // Advection terms
479                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
480                        // Use one-sided difference for advection in x
481                        let du_dx = if i == 0 {
482                            (u[[j, i + 1]] - u[[j, i]]) / dx
483                        } else {
484                            (u[[j, i]] - u[[j, i - 1]]) / dx
485                        };
486                        advection_x(x, y, t, u_val) * du_dx
487                    } else {
488                        0.0
489                    };
490
491                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
492                        // Use central difference for advection in y
493                        let du_dy = (u[[j + 1, i]] - u[[j - 1, i]]) / (2.0 * dy);
494                        advection_y(x, y, t, u_val) * du_dy
495                    } else {
496                        0.0
497                    };
498
499                    // Reaction term
500                    let reaction_term = if let Some(reaction) = &solver.reaction_term {
501                        reaction(x, y, t, u_val)
502                    } else {
503                        0.0
504                    };
505
506                    dudt[[j, i]] = diffusion_term_x
507                        + diffusion_term_y
508                        + advection_term_x
509                        + advection_term_y
510                        + reaction_term;
511                }
512            } else if let Some(j) = j_fixed {
513                // y-direction boundary (bottom or top)
514                for i in 1..nx - 1 {
515                    let y = y_grid[j];
516                    let x = x_grid[i];
517                    let u_val = u[[j, i]];
518                    let t = 0.0; // Time is not used here
519
520                    // Use one-sided difference for the normal direction
521                    let du_dn = bc.value; // Given Neumann value
522
523                    // For bottom boundary (j=0), ghost point is at j-1
524                    // For top boundary (j=ny-1), ghost point is at j+1
525                    let u_ghost = if j == 0 {
526                        u[[j, i]] - dy * du_dn
527                    } else {
528                        u[[j, i]] + dy * du_dn
529                    };
530
531                    // Diffusion term in y direction
532                    let d2u_dy2 = if j == 0 {
533                        (u[[j + 1, i]] - 2.0 * u[[j, i]] + u_ghost) / (dy * dy)
534                    } else {
535                        (u_ghost - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy)
536                    };
537
538                    // Diffusion term in x direction (use central difference)
539                    let d2u_dx2 = (u[[j, i + 1]] - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx);
540
541                    let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
542                    let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
543
544                    // Advection terms
545                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
546                        // Use central difference for advection in x
547                        let du_dx = (u[[j, i + 1]] - u[[j, i - 1]]) / (2.0 * dx);
548                        advection_x(x, y, t, u_val) * du_dx
549                    } else {
550                        0.0
551                    };
552
553                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
554                        // Use one-sided difference for advection in y
555                        let du_dy = if j == 0 {
556                            (u[[j + 1, i]] - u[[j, i]]) / dy
557                        } else {
558                            (u[[j, i]] - u[[j - 1, i]]) / dy
559                        };
560                        advection_y(x, y, t, u_val) * du_dy
561                    } else {
562                        0.0
563                    };
564
565                    // Reaction term
566                    let reaction_term = if let Some(reaction) = &solver.reaction_term {
567                        reaction(x, y, t, u_val)
568                    } else {
569                        0.0
570                    };
571
572                    dudt[[j, i]] = diffusion_term_x
573                        + diffusion_term_y
574                        + advection_term_x
575                        + advection_term_y
576                        + reaction_term;
577                }
578            }
579        }
580        BoundaryConditionType::Robin => {
581            // Robin boundary condition: a*u + b*du/dn = c
582            if let Some([a, b, c]) = bc.coefficients {
583                if let Some(i) = i_fixed {
584                    // x-direction boundary (left or right)
585                    for j in 1..ny - 1 {
586                        let y = y_grid[j];
587                        let x = x_grid[i];
588                        let u_val = u[[j, i]];
589                        let t = 0.0; // Time is not used here
590
591                        // Solve for ghost point value using Robin condition
592                        let du_dn = (c - a * u_val) / b;
593
594                        // For left boundary (i=0), ghost point is at i-1
595                        // For right boundary (i=nx-1), ghost point is at i+1
596                        let u_ghost = if i == 0 {
597                            u[[j, i]] - dx * du_dn
598                        } else {
599                            u[[j, i]] + dx * du_dn
600                        };
601
602                        // Diffusion term in x direction
603                        let d2u_dx2 = if i == 0 {
604                            (u[[j, i + 1]] - 2.0 * u[[j, i]] + u_ghost) / (dx * dx)
605                        } else {
606                            (u_ghost - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx)
607                        };
608
609                        // Diffusion term in y direction (use central difference)
610                        let d2u_dy2 = (u[[j + 1, i]] - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy);
611
612                        let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
613                        let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
614
615                        // Advection terms
616                        let advection_term_x = if let Some(advection_x) = &solver.advection_x {
617                            // Use one-sided difference for advection in x
618                            let du_dx = if i == 0 {
619                                (u[[j, i + 1]] - u[[j, i]]) / dx
620                            } else {
621                                (u[[j, i]] - u[[j, i - 1]]) / dx
622                            };
623                            advection_x(x, y, t, u_val) * du_dx
624                        } else {
625                            0.0
626                        };
627
628                        let advection_term_y = if let Some(advection_y) = &solver.advection_y {
629                            // Use central difference for advection in y
630                            let du_dy = (u[[j + 1, i]] - u[[j - 1, i]]) / (2.0 * dy);
631                            advection_y(x, y, t, u_val) * du_dy
632                        } else {
633                            0.0
634                        };
635
636                        // Reaction term
637                        let reaction_term = if let Some(reaction) = &solver.reaction_term {
638                            reaction(x, y, t, u_val)
639                        } else {
640                            0.0
641                        };
642
643                        dudt[[j, i]] = diffusion_term_x
644                            + diffusion_term_y
645                            + advection_term_x
646                            + advection_term_y
647                            + reaction_term;
648                    }
649                } else if let Some(j) = j_fixed {
650                    // y-direction boundary (bottom or top)
651                    for i in 1..nx - 1 {
652                        let y = y_grid[j];
653                        let x = x_grid[i];
654                        let u_val = u[[j, i]];
655                        let t = 0.0; // Time is not used here
656
657                        // Solve for ghost point value using Robin condition
658                        let du_dn = (c - a * u_val) / b;
659
660                        // For bottom boundary (j=0), ghost point is at j-1
661                        // For top boundary (j=ny-1), ghost point is at j+1
662                        let u_ghost = if j == 0 {
663                            u[[j, i]] - dy * du_dn
664                        } else {
665                            u[[j, i]] + dy * du_dn
666                        };
667
668                        // Diffusion term in y direction
669                        let d2u_dy2 = if j == 0 {
670                            (u[[j + 1, i]] - 2.0 * u[[j, i]] + u_ghost) / (dy * dy)
671                        } else {
672                            (u_ghost - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy)
673                        };
674
675                        // Diffusion term in x direction (use central difference)
676                        let d2u_dx2 = (u[[j, i + 1]] - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx);
677
678                        let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
679                        let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
680
681                        // Advection terms
682                        let advection_term_x = if let Some(advection_x) = &solver.advection_x {
683                            // Use central difference for advection in x
684                            let du_dx = (u[[j, i + 1]] - u[[j, i - 1]]) / (2.0 * dx);
685                            advection_x(x, y, t, u_val) * du_dx
686                        } else {
687                            0.0
688                        };
689
690                        let advection_term_y = if let Some(advection_y) = &solver.advection_y {
691                            // Use one-sided difference for advection in y
692                            let du_dy = if j == 0 {
693                                (u[[j + 1, i]] - u[[j, i]]) / dy
694                            } else {
695                                (u[[j, i]] - u[[j - 1, i]]) / dy
696                            };
697                            advection_y(x, y, t, u_val) * du_dy
698                        } else {
699                            0.0
700                        };
701
702                        // Reaction term
703                        let reaction_term = if let Some(reaction) = &solver.reaction_term {
704                            reaction(x, y, t, u_val)
705                        } else {
706                            0.0
707                        };
708
709                        dudt[[j, i]] = diffusion_term_x
710                            + diffusion_term_y
711                            + advection_term_x
712                            + advection_term_y
713                            + reaction_term;
714                    }
715                }
716            }
717        }
718        BoundaryConditionType::Periodic => {
719            // Periodic boundary conditions: values and derivatives wrap around
720            // Handle 2D periodic boundaries with proper corner treatment
721
722            if let Some(i) = i_fixed {
723                // x-direction periodic boundary (left or right)
724                let _opposite_i = if i == 0 { nx - 1 } else { 0 };
725
726                for j in 0..ny {
727                    let y = y_grid[j];
728                    let x = x_grid[i];
729                    let u_val = u[[j, i]];
730                    let t = 0.0; // Time parameter - would need to be passed in for time-dependent BCs
731
732                    // Apply the same computation as for interior points but with periodic wrapping
733                    let left_val = if i == 0 {
734                        u[[j, nx - 1]]
735                    } else {
736                        u[[j, i - 1]]
737                    };
738                    let right_val = if i == nx - 1 {
739                        u[[j, 0]]
740                    } else {
741                        u[[j, i + 1]]
742                    };
743                    let top_val = if j == ny - 1 {
744                        u[[0, i]]
745                    } else {
746                        u[[j + 1, i]]
747                    };
748                    let bottom_val = if j == 0 {
749                        u[[ny - 1, i]]
750                    } else {
751                        u[[j - 1, i]]
752                    };
753
754                    // Diffusion terms with periodic wrapping
755                    let d_coeff_x = (solver.diffusion_x)(x, y, t, u_val);
756                    let d2u_dx2 = (right_val - 2.0 * u_val + left_val) / (dx * dx);
757                    let diffusion_term_x = d_coeff_x * d2u_dx2;
758
759                    let d_coeff_y = (solver.diffusion_y)(x, y, t, u_val);
760                    let d2u_dy2 = (top_val - 2.0 * u_val + bottom_val) / (dy * dy);
761                    let diffusion_term_y = d_coeff_y * d2u_dy2;
762
763                    // Advection terms with periodic wrapping
764                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
765                        let a_coeff = advection_x(x, y, t, u_val);
766                        let du_dx = (right_val - left_val) / (2.0 * dx);
767                        a_coeff * du_dx
768                    } else {
769                        0.0
770                    };
771
772                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
773                        let a_coeff = advection_y(x, y, t, u_val);
774                        let du_dy = (top_val - bottom_val) / (2.0 * dy);
775                        a_coeff * du_dy
776                    } else {
777                        0.0
778                    };
779
780                    // Reaction term
781                    let reaction_term = if let Some(reaction) = &solver.reaction_term {
782                        reaction(x, y, t, u_val)
783                    } else {
784                        0.0
785                    };
786
787                    dudt[[j, i]] = diffusion_term_x
788                        + diffusion_term_y
789                        + advection_term_x
790                        + advection_term_y
791                        + reaction_term;
792                }
793            } else if let Some(j) = j_fixed {
794                // y-direction periodic boundary (top or bottom)
795                let _opposite_j = if j == 0 { ny - 1 } else { 0 };
796
797                for i in 0..nx {
798                    let y = y_grid[j];
799                    let x = x_grid[i];
800                    let u_val = u[[j, i]];
801                    let t = 0.0; // Time parameter
802
803                    // Apply the same computation as for interior points but with periodic wrapping
804                    let left_val = if i == 0 {
805                        u[[j, nx - 1]]
806                    } else {
807                        u[[j, i - 1]]
808                    };
809                    let right_val = if i == nx - 1 {
810                        u[[j, 0]]
811                    } else {
812                        u[[j, i + 1]]
813                    };
814                    let top_val = if j == ny - 1 {
815                        u[[0, i]]
816                    } else {
817                        u[[j + 1, i]]
818                    };
819                    let bottom_val = if j == 0 {
820                        u[[ny - 1, i]]
821                    } else {
822                        u[[j - 1, i]]
823                    };
824
825                    // Diffusion terms with periodic wrapping
826                    let d_coeff_x = (solver.diffusion_x)(x, y, t, u_val);
827                    let d2u_dx2 = (right_val - 2.0 * u_val + left_val) / (dx * dx);
828                    let diffusion_term_x = d_coeff_x * d2u_dx2;
829
830                    let d_coeff_y = (solver.diffusion_y)(x, y, t, u_val);
831                    let d2u_dy2 = (top_val - 2.0 * u_val + bottom_val) / (dy * dy);
832                    let diffusion_term_y = d_coeff_y * d2u_dy2;
833
834                    // Advection terms with periodic wrapping
835                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
836                        let a_coeff = advection_x(x, y, t, u_val);
837                        let du_dx = (right_val - left_val) / (2.0 * dx);
838                        a_coeff * du_dx
839                    } else {
840                        0.0
841                    };
842
843                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
844                        let a_coeff = advection_y(x, y, t, u_val);
845                        let du_dy = (top_val - bottom_val) / (2.0 * dy);
846                        a_coeff * du_dy
847                    } else {
848                        0.0
849                    };
850
851                    // Reaction term
852                    let reaction_term = if let Some(reaction) = &solver.reaction_term {
853                        reaction(x, y, t, u_val)
854                    } else {
855                        0.0
856                    };
857
858                    dudt[[j, i]] = diffusion_term_x
859                        + diffusion_term_y
860                        + advection_term_x
861                        + advection_term_y
862                        + reaction_term;
863                }
864            }
865        }
866    }
867}
868
869// Helper function to apply Dirichlet boundary conditions to initial condition
870#[allow(dead_code)]
871fn apply_dirichlet_conditions_to_initial(
872    u0: &mut Array2<f64>,
873    boundary_conditions: &[BoundaryCondition<f64>],
874    x_grid: &Array1<f64>,
875    y_grid: &Array1<f64>,
876) {
877    let nx = x_grid.len();
878    let ny = y_grid.len();
879
880    for bc in boundary_conditions {
881        if bc.bc_type == BoundaryConditionType::Dirichlet {
882            match (bc.dimension, bc.location) {
883                (0, BoundaryLocation::Lower) => {
884                    // Left boundary (x = x[0])
885                    for j in 0..ny {
886                        u0[[j, 0]] = bc.value;
887                    }
888                }
889                (0, BoundaryLocation::Upper) => {
890                    // Right boundary (x = x[nx-1])
891                    for j in 0..ny {
892                        u0[[j, nx - 1]] = bc.value;
893                    }
894                }
895                (1, BoundaryLocation::Lower) => {
896                    // Bottom boundary (y = y[0])
897                    for i in 0..nx {
898                        u0[[0, i]] = bc.value;
899                    }
900                }
901                (1, BoundaryLocation::Upper) => {
902                    // Top boundary (y = y[ny-1])
903                    for i in 0..nx {
904                        u0[[ny - 1, i]] = bc.value;
905                    }
906                }
907                _ => {
908                    // Invalid dimension
909                    // We'll just ignore this case - validation occurs during initialization
910                }
911            }
912        }
913    }
914}
915
916/// Convert a MOL2DResult to a PDESolution
917impl From<MOL2DResult> for PDESolution<f64> {
918    fn from(result: MOL2DResult) -> Self {
919        let mut grids = Vec::new();
920
921        // Add time grid
922        grids.push(result.t.clone());
923
924        // Extract spatial grids from solution shape
925        let nt = result.t.len();
926        let ny = result.u.shape()[1];
927        let nx = result.u.shape()[2];
928
929        // Create spatial grids (we don't have the actual grid values, so use linspace)
930        let y_grid = Array1::linspace(0.0, 1.0, ny);
931        let x_grid = Array1::linspace(0.0, 1.0, nx);
932        grids.push(y_grid);
933        grids.push(x_grid);
934
935        // Convert the 3D array to 2D arrays, one per time step
936        let mut values = Vec::new();
937        for t_idx in 0..nt {
938            let time_slice = result.u.slice(s![t_idx, .., ..]).to_owned();
939            values.push(time_slice);
940        }
941
942        // Create solver info
943        let info = PDESolverInfo {
944            num_iterations: 0, // This information is not available directly
945            computation_time: result.computation_time,
946            residual_norm: None,
947            convergence_history: None,
948            method: "Method of Lines (2D)".to_string(),
949        };
950
951        PDESolution {
952            grids,
953            values,
954            error_estimate: None,
955            info,
956        }
957    }
958}