Skip to main content

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
193            .clone()
194            .into_shape_with_order(nx * ny)
195            .expect("Operation failed");
196
197        // Clone grids for the closure
198        let x_grid_closure = x_grid.clone();
199        let y_grid_closure = y_grid.clone();
200
201        // Clone grids for later use
202        let x_grid_apply = x_grid.clone();
203        let y_grid_apply = y_grid.clone();
204
205        // Extract options before moving self
206        let ode_options = ODEOptions {
207            method: self.options.ode_method,
208            rtol: self.options.rtol,
209            atol: self.options.atol,
210            max_steps: self.options.max_steps.unwrap_or(500),
211            h0: None,
212            max_step: None,
213            min_step: None,
214            dense_output: true,
215            max_order: None,
216            jac: None,
217            use_banded_jacobian: false,
218            ml: None,
219            mu: None,
220            mass_matrix: None,
221            jacobian_strategy: None,
222        };
223
224        let time_range = self.time_range;
225        let boundary_conditions = self.boundary_conditions.clone();
226
227        // Move self into closure
228        let solver = self;
229
230        // Construct the ODE function that represents the PDE after spatial discretization
231        let ode_func = move |t: f64, u_flat: ArrayView1<f64>| -> Array1<f64> {
232            // Reshape the flattened array back to 2D for easier indexing
233            let u = u_flat
234                .into_shape_with_order((ny, nx))
235                .expect("Operation failed");
236            let mut dudt = Array2::zeros((ny, nx));
237
238            // Apply finite difference approximations for interior points
239            for j in 1..ny - 1 {
240                for i in 1..nx - 1 {
241                    let y = y_grid[j];
242                    let x = x_grid[i];
243                    let u_val = u[[j, i]];
244
245                    // Diffusion terms
246                    let d2u_dx2 = (u[[j, i + 1]] - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx);
247                    let d2u_dy2 = (u[[j + 1, i]] - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy);
248
249                    let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
250                    let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
251
252                    // Advection terms
253                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
254                        let du_dx = (u[[j, i + 1]] - u[[j, i - 1]]) / (2.0 * dx);
255                        advection_x(x, y, t, u_val) * du_dx
256                    } else {
257                        0.0
258                    };
259
260                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
261                        let du_dy = (u[[j + 1, i]] - u[[j - 1, i]]) / (2.0 * dy);
262                        advection_y(x, y, t, u_val) * du_dy
263                    } else {
264                        0.0
265                    };
266
267                    // Reaction term
268                    let reaction_val = if let Some(reaction) = &solver.reaction_term {
269                        reaction(x, y, t, u_val)
270                    } else {
271                        0.0
272                    };
273
274                    dudt[[j, i]] = diffusion_term_x
275                        + diffusion_term_y
276                        + advection_term_x
277                        + advection_term_y
278                        + reaction_val;
279                }
280            }
281
282            // Apply boundary conditions
283            for bc in &solver.boundary_conditions {
284                match (bc.dimension, bc.location) {
285                    // X-direction boundaries
286                    (0, BoundaryLocation::Lower) => {
287                        // Apply boundary condition at x[0] (left edge)
288                        apply_boundary_condition_2d(
289                            &mut dudt,
290                            &u,
291                            &x_grid_closure,
292                            &y_grid_closure,
293                            bc,
294                            dx,
295                            dy,
296                            Some(0),
297                            None,
298                            None,
299                            Some(ny),
300                            solver,
301                        );
302                    }
303                    (0, BoundaryLocation::Upper) => {
304                        // Apply boundary condition at x[nx-1] (right edge)
305                        apply_boundary_condition_2d(
306                            &mut dudt,
307                            &u,
308                            &x_grid_closure,
309                            &y_grid_closure,
310                            bc,
311                            dx,
312                            dy,
313                            Some(nx - 1),
314                            None,
315                            None,
316                            Some(ny),
317                            solver,
318                        );
319                    }
320                    // Y-direction boundaries
321                    (1, BoundaryLocation::Lower) => {
322                        // Apply boundary condition at y[0] (bottom edge)
323                        apply_boundary_condition_2d(
324                            &mut dudt,
325                            &u,
326                            &x_grid_closure,
327                            &y_grid_closure,
328                            bc,
329                            dx,
330                            dy,
331                            None,
332                            Some(0),
333                            Some(nx),
334                            None,
335                            solver,
336                        );
337                    }
338                    (1, BoundaryLocation::Upper) => {
339                        // Apply boundary condition at y[ny-1] (top edge)
340                        apply_boundary_condition_2d(
341                            &mut dudt,
342                            &u,
343                            &x_grid_closure,
344                            &y_grid_closure,
345                            bc,
346                            dx,
347                            dy,
348                            None,
349                            Some(ny - 1),
350                            Some(nx),
351                            None,
352                            solver,
353                        );
354                    }
355                    _ => {
356                        // Invalid dimension
357                        // We'll just ignore this case for now - validation occurs during initialization
358                    }
359                }
360            }
361
362            // Flatten the 2D dudt back to 1D for the ODE solver
363            dudt.into_shape_with_order(nx * ny)
364                .expect("Operation failed")
365        };
366
367        // Apply Dirichlet boundary conditions to initial condition
368        apply_dirichlet_conditions_to_initial(
369            &mut u0,
370            &boundary_conditions,
371            &x_grid_apply,
372            &y_grid_apply,
373        );
374
375        let u0_flat = u0
376            .clone()
377            .into_shape_with_order(nx * ny)
378            .expect("Operation failed");
379
380        // Solve the ODE system
381        let ode_result = solve_ivp(ode_func, time_range, u0_flat, Some(ode_options))?;
382
383        // Extract results
384        let computation_time = start_time.elapsed().as_secs_f64();
385
386        // Reshape the ODE result to match the spatial grid
387        let t = ode_result.t.clone();
388        let nt = t.len();
389
390        // Create a 3D array with dimensions [time, y, x]
391        let mut u_3d = Array3::zeros((nt, ny, nx));
392
393        for (time_idx, y_flat) in ode_result.y.iter().enumerate() {
394            let u_2d = y_flat
395                .clone()
396                .into_shape_with_order((ny, nx))
397                .expect("Operation failed");
398            for j in 0..ny {
399                for i in 0..nx {
400                    u_3d[[time_idx, j, i]] = u_2d[[j, i]];
401                }
402            }
403        }
404
405        let ode_info = Some(format!(
406            "ODE steps: {}, function evaluations: {}, successful steps: {}",
407            ode_result.n_steps, ode_result.n_eval, ode_result.n_accepted,
408        ));
409
410        Ok(MOL2DResult {
411            t: ode_result.t.into(),
412            u: u_3d,
413            ode_info,
414            computation_time,
415        })
416    }
417}
418
419// Helper function to apply boundary conditions in 2D
420#[allow(clippy::too_many_arguments)]
421#[allow(dead_code)]
422fn apply_boundary_condition_2d(
423    dudt: &mut Array2<f64>,
424    u: &ArrayView2<f64>,
425    x_grid: &Array1<f64>,
426    y_grid: &Array1<f64>,
427    bc: &BoundaryCondition<f64>,
428    dx: f64,
429    dy: f64,
430    i_fixed: Option<usize>,
431    j_fixed: Option<usize>,
432    i_range: Option<usize>,
433    j_range: Option<usize>,
434    solver: &MOLParabolicSolver2D,
435) {
436    let nx = x_grid.len();
437    let ny = y_grid.len();
438
439    let i_range = i_range.unwrap_or(1);
440    let j_range = j_range.unwrap_or(1);
441
442    match bc.bc_type {
443        BoundaryConditionType::Dirichlet => {
444            // Fixed value: u = bc.value
445            // For Dirichlet, we set dudt = 0 to maintain the _fixed value
446            if let Some(i) = i_fixed {
447                for j in 0..j_range {
448                    dudt[[j, i]] = 0.0;
449                }
450            } else if let Some(j) = j_fixed {
451                for i in 0..i_range {
452                    dudt[[j, i]] = 0.0;
453                }
454            }
455        }
456        BoundaryConditionType::Neumann => {
457            // Gradient in the direction normal to the boundary
458            if let Some(i) = i_fixed {
459                // x-direction boundary (left or right)
460                for j in 1..ny - 1 {
461                    let y = y_grid[j];
462                    let x = x_grid[i];
463                    let u_val = u[[j, i]];
464                    let t = 0.0; // Time is not used here
465
466                    // Use one-sided difference for the normal direction
467                    let du_dn = bc.value; // Given Neumann value
468
469                    // For left boundary (i=0), ghost point is at i-1
470                    // For right boundary (i=nx-1), ghost point is at i+1
471                    let u_ghost = if i == 0 {
472                        u[[j, i]] - dx * du_dn
473                    } else {
474                        u[[j, i]] + dx * du_dn
475                    };
476
477                    // Diffusion term in x direction
478                    let d2u_dx2 = if i == 0 {
479                        (u[[j, i + 1]] - 2.0 * u[[j, i]] + u_ghost) / (dx * dx)
480                    } else {
481                        (u_ghost - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx)
482                    };
483
484                    // Diffusion term in y direction (use central difference)
485                    let d2u_dy2 = (u[[j + 1, i]] - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy);
486
487                    let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
488                    let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
489
490                    // Advection terms
491                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
492                        // Use one-sided difference for advection in x
493                        let du_dx = if i == 0 {
494                            (u[[j, i + 1]] - u[[j, i]]) / dx
495                        } else {
496                            (u[[j, i]] - u[[j, i - 1]]) / dx
497                        };
498                        advection_x(x, y, t, u_val) * du_dx
499                    } else {
500                        0.0
501                    };
502
503                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
504                        // Use central difference for advection in y
505                        let du_dy = (u[[j + 1, i]] - u[[j - 1, i]]) / (2.0 * dy);
506                        advection_y(x, y, t, u_val) * du_dy
507                    } else {
508                        0.0
509                    };
510
511                    // Reaction term
512                    let reaction_term = if let Some(reaction) = &solver.reaction_term {
513                        reaction(x, y, t, u_val)
514                    } else {
515                        0.0
516                    };
517
518                    dudt[[j, i]] = diffusion_term_x
519                        + diffusion_term_y
520                        + advection_term_x
521                        + advection_term_y
522                        + reaction_term;
523                }
524            } else if let Some(j) = j_fixed {
525                // y-direction boundary (bottom or top)
526                for i in 1..nx - 1 {
527                    let y = y_grid[j];
528                    let x = x_grid[i];
529                    let u_val = u[[j, i]];
530                    let t = 0.0; // Time is not used here
531
532                    // Use one-sided difference for the normal direction
533                    let du_dn = bc.value; // Given Neumann value
534
535                    // For bottom boundary (j=0), ghost point is at j-1
536                    // For top boundary (j=ny-1), ghost point is at j+1
537                    let u_ghost = if j == 0 {
538                        u[[j, i]] - dy * du_dn
539                    } else {
540                        u[[j, i]] + dy * du_dn
541                    };
542
543                    // Diffusion term in y direction
544                    let d2u_dy2 = if j == 0 {
545                        (u[[j + 1, i]] - 2.0 * u[[j, i]] + u_ghost) / (dy * dy)
546                    } else {
547                        (u_ghost - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy)
548                    };
549
550                    // Diffusion term in x direction (use central difference)
551                    let d2u_dx2 = (u[[j, i + 1]] - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx);
552
553                    let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
554                    let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
555
556                    // Advection terms
557                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
558                        // Use central difference for advection in x
559                        let du_dx = (u[[j, i + 1]] - u[[j, i - 1]]) / (2.0 * dx);
560                        advection_x(x, y, t, u_val) * du_dx
561                    } else {
562                        0.0
563                    };
564
565                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
566                        // Use one-sided difference for advection in y
567                        let du_dy = if j == 0 {
568                            (u[[j + 1, i]] - u[[j, i]]) / dy
569                        } else {
570                            (u[[j, i]] - u[[j - 1, i]]) / dy
571                        };
572                        advection_y(x, y, t, u_val) * du_dy
573                    } else {
574                        0.0
575                    };
576
577                    // Reaction term
578                    let reaction_term = if let Some(reaction) = &solver.reaction_term {
579                        reaction(x, y, t, u_val)
580                    } else {
581                        0.0
582                    };
583
584                    dudt[[j, i]] = diffusion_term_x
585                        + diffusion_term_y
586                        + advection_term_x
587                        + advection_term_y
588                        + reaction_term;
589                }
590            }
591        }
592        BoundaryConditionType::Robin => {
593            // Robin boundary condition: a*u + b*du/dn = c
594            if let Some([a, b, c]) = bc.coefficients {
595                if let Some(i) = i_fixed {
596                    // x-direction boundary (left or right)
597                    for j in 1..ny - 1 {
598                        let y = y_grid[j];
599                        let x = x_grid[i];
600                        let u_val = u[[j, i]];
601                        let t = 0.0; // Time is not used here
602
603                        // Solve for ghost point value using Robin condition
604                        let du_dn = (c - a * u_val) / b;
605
606                        // For left boundary (i=0), ghost point is at i-1
607                        // For right boundary (i=nx-1), ghost point is at i+1
608                        let u_ghost = if i == 0 {
609                            u[[j, i]] - dx * du_dn
610                        } else {
611                            u[[j, i]] + dx * du_dn
612                        };
613
614                        // Diffusion term in x direction
615                        let d2u_dx2 = if i == 0 {
616                            (u[[j, i + 1]] - 2.0 * u[[j, i]] + u_ghost) / (dx * dx)
617                        } else {
618                            (u_ghost - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx)
619                        };
620
621                        // Diffusion term in y direction (use central difference)
622                        let d2u_dy2 = (u[[j + 1, i]] - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy);
623
624                        let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
625                        let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
626
627                        // Advection terms
628                        let advection_term_x = if let Some(advection_x) = &solver.advection_x {
629                            // Use one-sided difference for advection in x
630                            let du_dx = if i == 0 {
631                                (u[[j, i + 1]] - u[[j, i]]) / dx
632                            } else {
633                                (u[[j, i]] - u[[j, i - 1]]) / dx
634                            };
635                            advection_x(x, y, t, u_val) * du_dx
636                        } else {
637                            0.0
638                        };
639
640                        let advection_term_y = if let Some(advection_y) = &solver.advection_y {
641                            // Use central difference for advection in y
642                            let du_dy = (u[[j + 1, i]] - u[[j - 1, i]]) / (2.0 * dy);
643                            advection_y(x, y, t, u_val) * du_dy
644                        } else {
645                            0.0
646                        };
647
648                        // Reaction term
649                        let reaction_term = if let Some(reaction) = &solver.reaction_term {
650                            reaction(x, y, t, u_val)
651                        } else {
652                            0.0
653                        };
654
655                        dudt[[j, i]] = diffusion_term_x
656                            + diffusion_term_y
657                            + advection_term_x
658                            + advection_term_y
659                            + reaction_term;
660                    }
661                } else if let Some(j) = j_fixed {
662                    // y-direction boundary (bottom or top)
663                    for i in 1..nx - 1 {
664                        let y = y_grid[j];
665                        let x = x_grid[i];
666                        let u_val = u[[j, i]];
667                        let t = 0.0; // Time is not used here
668
669                        // Solve for ghost point value using Robin condition
670                        let du_dn = (c - a * u_val) / b;
671
672                        // For bottom boundary (j=0), ghost point is at j-1
673                        // For top boundary (j=ny-1), ghost point is at j+1
674                        let u_ghost = if j == 0 {
675                            u[[j, i]] - dy * du_dn
676                        } else {
677                            u[[j, i]] + dy * du_dn
678                        };
679
680                        // Diffusion term in y direction
681                        let d2u_dy2 = if j == 0 {
682                            (u[[j + 1, i]] - 2.0 * u[[j, i]] + u_ghost) / (dy * dy)
683                        } else {
684                            (u_ghost - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy)
685                        };
686
687                        // Diffusion term in x direction (use central difference)
688                        let d2u_dx2 = (u[[j, i + 1]] - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx);
689
690                        let diffusion_term_x = (solver.diffusion_x)(x, y, t, u_val) * d2u_dx2;
691                        let diffusion_term_y = (solver.diffusion_y)(x, y, t, u_val) * d2u_dy2;
692
693                        // Advection terms
694                        let advection_term_x = if let Some(advection_x) = &solver.advection_x {
695                            // Use central difference for advection in x
696                            let du_dx = (u[[j, i + 1]] - u[[j, i - 1]]) / (2.0 * dx);
697                            advection_x(x, y, t, u_val) * du_dx
698                        } else {
699                            0.0
700                        };
701
702                        let advection_term_y = if let Some(advection_y) = &solver.advection_y {
703                            // Use one-sided difference for advection in y
704                            let du_dy = if j == 0 {
705                                (u[[j + 1, i]] - u[[j, i]]) / dy
706                            } else {
707                                (u[[j, i]] - u[[j - 1, i]]) / dy
708                            };
709                            advection_y(x, y, t, u_val) * du_dy
710                        } else {
711                            0.0
712                        };
713
714                        // Reaction term
715                        let reaction_term = if let Some(reaction) = &solver.reaction_term {
716                            reaction(x, y, t, u_val)
717                        } else {
718                            0.0
719                        };
720
721                        dudt[[j, i]] = diffusion_term_x
722                            + diffusion_term_y
723                            + advection_term_x
724                            + advection_term_y
725                            + reaction_term;
726                    }
727                }
728            }
729        }
730        BoundaryConditionType::Periodic => {
731            // Periodic boundary conditions: values and derivatives wrap around
732            // Handle 2D periodic boundaries with proper corner treatment
733
734            if let Some(i) = i_fixed {
735                // x-direction periodic boundary (left or right)
736                let _opposite_i = if i == 0 { nx - 1 } else { 0 };
737
738                for j in 0..ny {
739                    let y = y_grid[j];
740                    let x = x_grid[i];
741                    let u_val = u[[j, i]];
742                    let t = 0.0; // Time parameter - would need to be passed in for time-dependent BCs
743
744                    // Apply the same computation as for interior points but with periodic wrapping
745                    let left_val = if i == 0 {
746                        u[[j, nx - 1]]
747                    } else {
748                        u[[j, i - 1]]
749                    };
750                    let right_val = if i == nx - 1 {
751                        u[[j, 0]]
752                    } else {
753                        u[[j, i + 1]]
754                    };
755                    let top_val = if j == ny - 1 {
756                        u[[0, i]]
757                    } else {
758                        u[[j + 1, i]]
759                    };
760                    let bottom_val = if j == 0 {
761                        u[[ny - 1, i]]
762                    } else {
763                        u[[j - 1, i]]
764                    };
765
766                    // Diffusion terms with periodic wrapping
767                    let d_coeff_x = (solver.diffusion_x)(x, y, t, u_val);
768                    let d2u_dx2 = (right_val - 2.0 * u_val + left_val) / (dx * dx);
769                    let diffusion_term_x = d_coeff_x * d2u_dx2;
770
771                    let d_coeff_y = (solver.diffusion_y)(x, y, t, u_val);
772                    let d2u_dy2 = (top_val - 2.0 * u_val + bottom_val) / (dy * dy);
773                    let diffusion_term_y = d_coeff_y * d2u_dy2;
774
775                    // Advection terms with periodic wrapping
776                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
777                        let a_coeff = advection_x(x, y, t, u_val);
778                        let du_dx = (right_val - left_val) / (2.0 * dx);
779                        a_coeff * du_dx
780                    } else {
781                        0.0
782                    };
783
784                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
785                        let a_coeff = advection_y(x, y, t, u_val);
786                        let du_dy = (top_val - bottom_val) / (2.0 * dy);
787                        a_coeff * du_dy
788                    } else {
789                        0.0
790                    };
791
792                    // Reaction term
793                    let reaction_term = if let Some(reaction) = &solver.reaction_term {
794                        reaction(x, y, t, u_val)
795                    } else {
796                        0.0
797                    };
798
799                    dudt[[j, i]] = diffusion_term_x
800                        + diffusion_term_y
801                        + advection_term_x
802                        + advection_term_y
803                        + reaction_term;
804                }
805            } else if let Some(j) = j_fixed {
806                // y-direction periodic boundary (top or bottom)
807                let _opposite_j = if j == 0 { ny - 1 } else { 0 };
808
809                for i in 0..nx {
810                    let y = y_grid[j];
811                    let x = x_grid[i];
812                    let u_val = u[[j, i]];
813                    let t = 0.0; // Time parameter
814
815                    // Apply the same computation as for interior points but with periodic wrapping
816                    let left_val = if i == 0 {
817                        u[[j, nx - 1]]
818                    } else {
819                        u[[j, i - 1]]
820                    };
821                    let right_val = if i == nx - 1 {
822                        u[[j, 0]]
823                    } else {
824                        u[[j, i + 1]]
825                    };
826                    let top_val = if j == ny - 1 {
827                        u[[0, i]]
828                    } else {
829                        u[[j + 1, i]]
830                    };
831                    let bottom_val = if j == 0 {
832                        u[[ny - 1, i]]
833                    } else {
834                        u[[j - 1, i]]
835                    };
836
837                    // Diffusion terms with periodic wrapping
838                    let d_coeff_x = (solver.diffusion_x)(x, y, t, u_val);
839                    let d2u_dx2 = (right_val - 2.0 * u_val + left_val) / (dx * dx);
840                    let diffusion_term_x = d_coeff_x * d2u_dx2;
841
842                    let d_coeff_y = (solver.diffusion_y)(x, y, t, u_val);
843                    let d2u_dy2 = (top_val - 2.0 * u_val + bottom_val) / (dy * dy);
844                    let diffusion_term_y = d_coeff_y * d2u_dy2;
845
846                    // Advection terms with periodic wrapping
847                    let advection_term_x = if let Some(advection_x) = &solver.advection_x {
848                        let a_coeff = advection_x(x, y, t, u_val);
849                        let du_dx = (right_val - left_val) / (2.0 * dx);
850                        a_coeff * du_dx
851                    } else {
852                        0.0
853                    };
854
855                    let advection_term_y = if let Some(advection_y) = &solver.advection_y {
856                        let a_coeff = advection_y(x, y, t, u_val);
857                        let du_dy = (top_val - bottom_val) / (2.0 * dy);
858                        a_coeff * du_dy
859                    } else {
860                        0.0
861                    };
862
863                    // Reaction term
864                    let reaction_term = if let Some(reaction) = &solver.reaction_term {
865                        reaction(x, y, t, u_val)
866                    } else {
867                        0.0
868                    };
869
870                    dudt[[j, i]] = diffusion_term_x
871                        + diffusion_term_y
872                        + advection_term_x
873                        + advection_term_y
874                        + reaction_term;
875                }
876            }
877        }
878    }
879}
880
881// Helper function to apply Dirichlet boundary conditions to initial condition
882#[allow(dead_code)]
883fn apply_dirichlet_conditions_to_initial(
884    u0: &mut Array2<f64>,
885    boundary_conditions: &[BoundaryCondition<f64>],
886    x_grid: &Array1<f64>,
887    y_grid: &Array1<f64>,
888) {
889    let nx = x_grid.len();
890    let ny = y_grid.len();
891
892    for bc in boundary_conditions {
893        if bc.bc_type == BoundaryConditionType::Dirichlet {
894            match (bc.dimension, bc.location) {
895                (0, BoundaryLocation::Lower) => {
896                    // Left boundary (x = x[0])
897                    for j in 0..ny {
898                        u0[[j, 0]] = bc.value;
899                    }
900                }
901                (0, BoundaryLocation::Upper) => {
902                    // Right boundary (x = x[nx-1])
903                    for j in 0..ny {
904                        u0[[j, nx - 1]] = bc.value;
905                    }
906                }
907                (1, BoundaryLocation::Lower) => {
908                    // Bottom boundary (y = y[0])
909                    for i in 0..nx {
910                        u0[[0, i]] = bc.value;
911                    }
912                }
913                (1, BoundaryLocation::Upper) => {
914                    // Top boundary (y = y[ny-1])
915                    for i in 0..nx {
916                        u0[[ny - 1, i]] = bc.value;
917                    }
918                }
919                _ => {
920                    // Invalid dimension
921                    // We'll just ignore this case - validation occurs during initialization
922                }
923            }
924        }
925    }
926}
927
928/// Convert a MOL2DResult to a PDESolution
929impl From<MOL2DResult> for PDESolution<f64> {
930    fn from(result: MOL2DResult) -> Self {
931        let mut grids = Vec::new();
932
933        // Add time grid
934        grids.push(result.t.clone());
935
936        // Extract spatial grids from solution shape
937        let nt = result.t.len();
938        let ny = result.u.shape()[1];
939        let nx = result.u.shape()[2];
940
941        // Create spatial grids (we don't have the actual grid values, so use linspace)
942        let y_grid = Array1::linspace(0.0, 1.0, ny);
943        let x_grid = Array1::linspace(0.0, 1.0, nx);
944        grids.push(y_grid);
945        grids.push(x_grid);
946
947        // Convert the 3D array to 2D arrays, one per time step
948        let mut values = Vec::new();
949        for t_idx in 0..nt {
950            let time_slice = result.u.slice(s![t_idx, .., ..]).to_owned();
951            values.push(time_slice);
952        }
953
954        // Create solver info
955        let info = PDESolverInfo {
956            num_iterations: 0, // This information is not available directly
957            computation_time: result.computation_time,
958            residual_norm: None,
959            convergence_history: None,
960            method: "Method of Lines (2D)".to_string(),
961        };
962
963        PDESolution {
964            grids,
965            values,
966            error_estimate: None,
967            info,
968        }
969    }
970}