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}