scirs2_integrate/pde/method_of_lines/
mod.rs

1//! Method of Lines (MOL) approach for solving PDEs
2//!
3//! The Method of Lines transforms PDEs into systems of ODEs by discretizing
4//! all but one dimension (typically space), leaving a system of ODEs in the
5//! remaining dimension (typically time).
6//!
7//! This approach allows using well-established ODE solvers for time integration
8//! after the spatial discretization is performed.
9//!
10//! ## Supported PDE Types and Dimensions
11//!
12//! - 1D Parabolic PDEs (heat equation, advection-diffusion)
13//! - 2D Parabolic PDEs (2D heat equation, 2D advection-diffusion)
14//! - 3D Parabolic PDEs (3D heat equation, 3D advection-diffusion)
15//! - 1D Hyperbolic PDEs (wave equation)
16
17pub mod mod2d;
18pub use mod2d::{MOL2DResult, MOLParabolicSolver2D};
19
20pub mod mod3d;
21pub use mod3d::{MOL3DResult, MOLParabolicSolver3D};
22
23pub mod hyperbolic;
24pub use hyperbolic::{MOLHyperbolicResult, MOLWaveEquation1D};
25
26use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
27use std::sync::Arc;
28use std::time::Instant;
29
30use crate::ode::{solve_ivp, ODEMethod, ODEOptions};
31use crate::pde::finite_difference::FiniteDifferenceScheme;
32use crate::pde::{BoundaryCondition, Domain, PDEError, PDEResult, PDESolution, PDESolverInfo};
33
34/// Type alias for 1D coefficient function taking (x, t, u) and returning a value
35type CoeffFn1D = Arc<dyn Fn(f64, f64, f64) -> f64 + Send + Sync>;
36
37/// Options for the Method of Lines PDE solver
38#[derive(Debug, Clone)]
39pub struct MOLOptions {
40    /// ODE solver method to use for time integration
41    pub ode_method: ODEMethod,
42
43    /// Absolute tolerance for the ODE solver
44    pub atol: f64,
45
46    /// Relative tolerance for the ODE solver
47    pub rtol: f64,
48
49    /// Maximum number of ODE steps
50    pub max_steps: Option<usize>,
51
52    /// Print detailed progress information
53    pub verbose: bool,
54}
55
56impl Default for MOLOptions {
57    fn default() -> Self {
58        MOLOptions {
59            ode_method: ODEMethod::RK45,
60            atol: 1e-6,
61            rtol: 1e-3,
62            max_steps: None,
63            verbose: false,
64        }
65    }
66}
67
68/// Result of method of lines solution
69pub struct MOLResult {
70    /// Time points
71    pub t: Array1<f64>,
72
73    /// Solution values, indexed as [time, space, variable]
74    pub u: Vec<Array2<f64>>,
75
76    /// ODE solver information
77    pub ode_info: Option<String>,
78
79    /// Computation time
80    pub computation_time: f64,
81}
82
83/// Method of Lines solver for 1D parabolic PDEs (e.g., heat equation)
84///
85/// Solves equations of the form: u/t = �(ab(x, t, u) u/x) + f(x, t, u)
86#[derive(Clone)]
87pub struct MOLParabolicSolver1D {
88    /// Spatial domain
89    domain: Domain,
90
91    /// Time range [t_start, t_end]
92    time_range: [f64; 2],
93
94    /// Diffusion coefficient function a(x, t, u) for �u/x�
95    diffusion_coeff: CoeffFn1D,
96
97    /// Advection coefficient function b(x, t, u) for u/x
98    advection_coeff: Option<CoeffFn1D>,
99
100    /// Reaction term function f(x, t, u)
101    reaction_term: Option<CoeffFn1D>,
102
103    /// Initial condition function u(x, 0)
104    initial_condition: Arc<dyn Fn(f64) -> f64 + Send + Sync>,
105
106    /// Boundary conditions
107    boundary_conditions: Vec<BoundaryCondition<f64>>,
108
109    /// Finite difference scheme for spatial discretization
110    fd_scheme: FiniteDifferenceScheme,
111
112    /// Solver options
113    options: MOLOptions,
114}
115
116impl MOLParabolicSolver1D {
117    /// Create a new Method of Lines solver for 1D parabolic PDEs
118    pub fn new(
119        domain: Domain,
120        time_range: [f64; 2],
121        diffusion_coeff: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
122        initial_condition: impl Fn(f64) -> f64 + Send + Sync + 'static,
123        boundary_conditions: Vec<BoundaryCondition<f64>>,
124        options: Option<MOLOptions>,
125    ) -> PDEResult<Self> {
126        // Validate the domain
127        if domain.dimensions() != 1 {
128            return Err(PDEError::DomainError(
129                "Domain must be 1-dimensional for 1D parabolic solver".to_string(),
130            ));
131        }
132
133        // Validate time _range
134        if time_range[0] >= time_range[1] {
135            return Err(PDEError::DomainError(
136                "Invalid time _range: start must be less than end".to_string(),
137            ));
138        }
139
140        // Validate boundary _conditions
141        if boundary_conditions.len() != 2 {
142            return Err(PDEError::BoundaryConditions(
143                "1D parabolic PDE requires exactly 2 boundary _conditions".to_string(),
144            ));
145        }
146
147        // Ensure we have both lower and upper boundary _conditions
148        let has_lower = boundary_conditions
149            .iter()
150            .any(|bc| bc.location == crate::pde::BoundaryLocation::Lower);
151        let has_upper = boundary_conditions
152            .iter()
153            .any(|bc| bc.location == crate::pde::BoundaryLocation::Upper);
154
155        if !has_lower || !has_upper {
156            return Err(PDEError::BoundaryConditions(
157                "1D parabolic PDE requires both lower and upper boundary _conditions".to_string(),
158            ));
159        }
160
161        Ok(MOLParabolicSolver1D {
162            domain,
163            time_range,
164            diffusion_coeff: Arc::new(diffusion_coeff),
165            advection_coeff: None,
166            reaction_term: None,
167            initial_condition: Arc::new(initial_condition),
168            boundary_conditions,
169            fd_scheme: FiniteDifferenceScheme::CentralDifference,
170            options: options.unwrap_or_default(),
171        })
172    }
173
174    /// Add an advection term to the PDE
175    pub fn with_advection(
176        mut self,
177        advection_coeff: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
178    ) -> Self {
179        self.advection_coeff = Some(Arc::new(advection_coeff));
180        self
181    }
182
183    /// Add a reaction term to the PDE
184    pub fn with_reaction(
185        mut self,
186        reaction_term: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
187    ) -> Self {
188        self.reaction_term = Some(Arc::new(reaction_term));
189        self
190    }
191
192    /// Set the finite difference scheme for spatial discretization
193    pub fn with_fd_scheme(mut self, scheme: FiniteDifferenceScheme) -> Self {
194        self.fd_scheme = scheme;
195        self
196    }
197
198    /// Solve the PDE
199    pub fn solve(&self) -> PDEResult<MOLResult> {
200        let start_time = Instant::now();
201
202        // Generate spatial grid
203        let x_grid = self.domain.grid(0)?;
204        let nx = x_grid.len();
205        let dx = self.domain.grid_spacing(0)?;
206
207        // Create initial condition vector
208        let mut u0 = Array1::zeros(nx);
209        for (i, &x) in x_grid.iter().enumerate() {
210            u0[i] = (self.initial_condition)(x);
211        }
212
213        // Extract data before moving self
214        let _fd_scheme = self.fd_scheme;
215        let x_grid = x_grid.clone();
216        let time_range = self.time_range;
217        let boundary_conditions = self.boundary_conditions.clone();
218
219        // Extract options before moving self
220        let ode_options = ODEOptions {
221            method: self.options.ode_method,
222            rtol: self.options.rtol,
223            atol: self.options.atol,
224            max_steps: self.options.max_steps.unwrap_or(500),
225            h0: None,
226            max_step: None,
227            min_step: None,
228            dense_output: true,
229            max_order: None,
230            jac: None,
231            use_banded_jacobian: false,
232            ml: None,
233            mu: None,
234            mass_matrix: None,
235            jacobian_strategy: None,
236        };
237
238        // Move self into closure
239        let solver = self;
240
241        // Construct the ODE function that represents the PDE after spatial discretization
242        let ode_func = move |t: f64, u: ArrayView1<f64>| -> Array1<f64> {
243            let mut dudt = Array1::zeros(nx);
244
245            // Apply finite difference approximations for interior points
246            for i in 1..nx - 1 {
247                let x = x_grid[i];
248                let u_i = u[i];
249
250                // Second derivative term (diffusion)
251                let d2u_dx2 = (u[i + 1] - 2.0 * u[i] + u[i - 1]) / (dx * dx);
252                let diffusion_term = (solver.diffusion_coeff)(x, t, u_i) * d2u_dx2;
253
254                // First derivative term (advection)
255                let advection_term = if let Some(advection) = &solver.advection_coeff {
256                    let du_dx = (u[i + 1] - u[i - 1]) / (2.0 * dx);
257                    advection(x, t, u_i) * du_dx
258                } else {
259                    0.0
260                };
261
262                // Reaction term
263                let reaction_term = if let Some(reaction) = &solver.reaction_term {
264                    reaction(x, t, u_i)
265                } else {
266                    0.0
267                };
268
269                dudt[i] = diffusion_term + advection_term + reaction_term;
270            }
271
272            // Apply boundary conditions
273            for bc in &solver.boundary_conditions {
274                match bc.location {
275                    crate::pde::BoundaryLocation::Lower => {
276                        // Apply boundary condition at x[0]
277                        match bc.bc_type {
278                            crate::pde::BoundaryConditionType::Dirichlet => {
279                                // Fixed value: u(x_0, t) = bc.value
280                                // For Dirichlet, we set dudt[0] = 0 to maintain the fixed value
281                                dudt[0] = 0.0;
282                            }
283                            crate::pde::BoundaryConditionType::Neumann => {
284                                // Fixed gradient: u/x|_{x_0} = bc.value
285                                // Use one-sided difference to approximate second derivative
286                                let du_dx = bc.value; // Given Neumann value
287                                let u_ghost = u[0] - dx * du_dx; // Ghost point value
288
289                                // Now use central difference for diffusion term
290                                let d2u_dx2 = (u[1] - 2.0 * u[0] + u_ghost) / (dx * dx);
291                                let diffusion_term =
292                                    (solver.diffusion_coeff)(x_grid[0], t, u[0]) * d2u_dx2;
293
294                                // Other terms
295                                let advection_term =
296                                    if let Some(advection) = &solver.advection_coeff {
297                                        let du_dx_forward = (u[1] - u[0]) / dx;
298                                        advection(x_grid[0], t, u[0]) * du_dx_forward
299                                    } else {
300                                        0.0
301                                    };
302
303                                let reaction_term = if let Some(reaction) = &solver.reaction_term {
304                                    reaction(x_grid[0], t, u[0])
305                                } else {
306                                    0.0
307                                };
308
309                                dudt[0] = diffusion_term + advection_term + reaction_term;
310                            }
311                            crate::pde::BoundaryConditionType::Robin => {
312                                // Robin boundary condition: a*u + b*du/dx = c
313                                if let Some([a, b, c]) = bc.coefficients {
314                                    // Solve for ghost point value using Robin condition
315                                    let du_dx = (c - a * u[0]) / b;
316                                    let u_ghost = u[0] - dx * du_dx;
317
318                                    // Use central difference with ghost point
319                                    let d2u_dx2 = (u[1] - 2.0 * u[0] + u_ghost) / (dx * dx);
320                                    let diffusion_term =
321                                        (solver.diffusion_coeff)(x_grid[0], t, u[0]) * d2u_dx2;
322
323                                    // Other terms
324                                    let advection_term =
325                                        if let Some(advection) = &solver.advection_coeff {
326                                            let du_dx_forward = (u[1] - u[0]) / dx;
327                                            advection(x_grid[0], t, u[0]) * du_dx_forward
328                                        } else {
329                                            0.0
330                                        };
331
332                                    let reaction_term =
333                                        if let Some(reaction) = &solver.reaction_term {
334                                            reaction(x_grid[0], t, u[0])
335                                        } else {
336                                            0.0
337                                        };
338
339                                    dudt[0] = diffusion_term + advection_term + reaction_term;
340                                }
341                            }
342                            crate::pde::BoundaryConditionType::Periodic => {
343                                // Periodic boundary: u(x_0, t) = u(x_n, t)
344                                // Use values from the other end of the domain
345                                let d2u_dx2 = (u[1] - 2.0 * u[0] + u[nx - 1]) / (dx * dx);
346                                let diffusion_term =
347                                    (solver.diffusion_coeff)(x_grid[0], t, u[0]) * d2u_dx2;
348
349                                // Other terms
350                                let advection_term =
351                                    if let Some(advection) = &solver.advection_coeff {
352                                        let du_dx = (u[1] - u[nx - 1]) / (2.0 * dx);
353                                        advection(x_grid[0], t, u[0]) * du_dx
354                                    } else {
355                                        0.0
356                                    };
357
358                                let reaction_term = if let Some(reaction) = &solver.reaction_term {
359                                    reaction(x_grid[0], t, u[0])
360                                } else {
361                                    0.0
362                                };
363
364                                dudt[0] = diffusion_term + advection_term + reaction_term;
365                            }
366                        }
367                    }
368                    crate::pde::BoundaryLocation::Upper => {
369                        // Apply boundary condition at x[nx-1]
370                        match bc.bc_type {
371                            crate::pde::BoundaryConditionType::Dirichlet => {
372                                // Fixed value: u(x_n, t) = bc.value
373                                // For Dirichlet, we set dudt[nx-1] = 0 to maintain the fixed value
374                                dudt[nx - 1] = 0.0;
375                            }
376                            crate::pde::BoundaryConditionType::Neumann => {
377                                // Fixed gradient: u/x|_{x_n} = bc.value
378                                // Use one-sided difference to approximate second derivative
379                                let du_dx = bc.value; // Given Neumann value
380                                let u_ghost = u[nx - 1] + dx * du_dx; // Ghost point value
381
382                                // Now use central difference for diffusion term
383                                let d2u_dx2 = (u_ghost - 2.0 * u[nx - 1] + u[nx - 2]) / (dx * dx);
384                                let diffusion_term =
385                                    (solver.diffusion_coeff)(x_grid[nx - 1], t, u[nx - 1])
386                                        * d2u_dx2;
387
388                                // Other terms
389                                let advection_term =
390                                    if let Some(advection) = &solver.advection_coeff {
391                                        let du_dx_backward = (u[nx - 1] - u[nx - 2]) / dx;
392                                        advection(x_grid[nx - 1], t, u[nx - 1]) * du_dx_backward
393                                    } else {
394                                        0.0
395                                    };
396
397                                let reaction_term = if let Some(reaction) = &solver.reaction_term {
398                                    reaction(x_grid[nx - 1], t, u[nx - 1])
399                                } else {
400                                    0.0
401                                };
402
403                                dudt[nx - 1] = diffusion_term + advection_term + reaction_term;
404                            }
405                            crate::pde::BoundaryConditionType::Robin => {
406                                // Robin boundary condition: a*u + b*du/dx = c
407                                if let Some([a, b, c]) = bc.coefficients {
408                                    // Solve for ghost point value using Robin condition
409                                    let du_dx = (c - a * u[nx - 1]) / b;
410                                    let u_ghost = u[nx - 1] + dx * du_dx;
411
412                                    // Use central difference with ghost point
413                                    let d2u_dx2 =
414                                        (u_ghost - 2.0 * u[nx - 1] + u[nx - 2]) / (dx * dx);
415                                    let diffusion_term =
416                                        (solver.diffusion_coeff)(x_grid[nx - 1], t, u[nx - 1])
417                                            * d2u_dx2;
418
419                                    // Other terms
420                                    let advection_term =
421                                        if let Some(advection) = &solver.advection_coeff {
422                                            let du_dx_backward = (u[nx - 1] - u[nx - 2]) / dx;
423                                            advection(x_grid[nx - 1], t, u[nx - 1]) * du_dx_backward
424                                        } else {
425                                            0.0
426                                        };
427
428                                    let reaction_term =
429                                        if let Some(reaction) = &solver.reaction_term {
430                                            reaction(x_grid[nx - 1], t, u[nx - 1])
431                                        } else {
432                                            0.0
433                                        };
434
435                                    dudt[nx - 1] = diffusion_term + advection_term + reaction_term;
436                                }
437                            }
438                            crate::pde::BoundaryConditionType::Periodic => {
439                                // Periodic boundary: u(x_n, t) = u(x_0, t)
440                                // Use values from the other end of the domain
441                                let d2u_dx2 = (u[0] - 2.0 * u[nx - 1] + u[nx - 2]) / (dx * dx);
442                                let diffusion_term =
443                                    (solver.diffusion_coeff)(x_grid[nx - 1], t, u[nx - 1])
444                                        * d2u_dx2;
445
446                                // Other terms
447                                let advection_term =
448                                    if let Some(advection) = &solver.advection_coeff {
449                                        let du_dx = (u[0] - u[nx - 2]) / (2.0 * dx);
450                                        advection(x_grid[nx - 1], t, u[nx - 1]) * du_dx
451                                    } else {
452                                        0.0
453                                    };
454
455                                let reaction_term = if let Some(reaction) = &solver.reaction_term {
456                                    reaction(x_grid[nx - 1], t, u[nx - 1])
457                                } else {
458                                    0.0
459                                };
460
461                                dudt[nx - 1] = diffusion_term + advection_term + reaction_term;
462                            }
463                        }
464                    }
465                }
466            }
467
468            dudt
469        };
470
471        // Set up ODE solver options
472        let ode_options = ode_options;
473
474        // Apply Dirichlet boundary conditions to initial condition
475        for bc in &boundary_conditions {
476            if bc.bc_type == crate::pde::BoundaryConditionType::Dirichlet {
477                match bc.location {
478                    crate::pde::BoundaryLocation::Lower => u0[0] = bc.value,
479                    crate::pde::BoundaryLocation::Upper => u0[nx - 1] = bc.value,
480                }
481            }
482        }
483
484        // Solve the ODE system
485        let ode_result = solve_ivp(ode_func, time_range, u0, 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 mut u = Vec::new();
493
494        // Create a single 2D array with dimensions [time, space]
495        let u_2d =
496            Array2::from_shape_vec((t.len(), nx), ode_result.y.into_iter().flatten().collect())
497                .map_err(|e| PDEError::Other(format!("Error reshaping result: {e}")))?;
498
499        u.push(u_2d);
500
501        let ode_info = Some(format!(
502            "ODE steps: {}, function evaluations: {}, successful steps: {}",
503            ode_result.n_steps, ode_result.n_eval, ode_result.n_accepted,
504        ));
505
506        Ok(MOLResult {
507            t: ode_result.t.into(),
508            u,
509            ode_info,
510            computation_time,
511        })
512    }
513}
514
515/// Convert a MOLResult to a PDESolution
516impl From<MOLResult> for PDESolution<f64> {
517    fn from(result: MOLResult) -> Self {
518        let mut grids = Vec::new();
519
520        // Add time grid
521        grids.push(result.t.clone());
522
523        // Extract spatial grid from solution
524        let nx = result.u[0].shape()[1];
525        // Note: For a proper implementation, the spatial grid should be provided
526        let spatial_grid = Array1::linspace(0.0, 1.0, nx);
527        grids.push(spatial_grid);
528
529        // Create solver info
530        let info = PDESolverInfo {
531            num_iterations: 0, // This information is not available directly
532            computation_time: result.computation_time,
533            residual_norm: None,
534            convergence_history: None,
535            method: "Method of Lines".to_string(),
536        };
537
538        PDESolution {
539            grids,
540            values: result.u,
541            error_estimate: None,
542            info,
543        }
544    }
545}