scirs2_integrate/pde/implicit/mod.rs
1//! Implicit methods for solving PDEs
2//!
3//! This module provides implementations of implicit time-stepping schemes for
4//! solving partial differential equations (PDEs). Implicit methods are generally
5//! more stable than explicit methods and can handle stiff problems more efficiently.
6//!
7//! ## Supported Methods
8//!
9//! * Crank-Nicolson method: A second-order implicit method that is A-stable
10//! * Backward Euler method: A first-order fully implicit method that is L-stable
11//! * Trapezoidal rule: A second-order implicit method
12//! * Alternating Direction Implicit (ADI) method: An efficient operator splitting method for
13//! multi-dimensional problems
14
15use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
16use std::time::Instant;
17
18use crate::pde::finite_difference::FiniteDifferenceScheme;
19use crate::pde::{
20 BoundaryCondition, BoundaryConditionType, BoundaryLocation, Domain, PDEError, PDEResult,
21 PDESolution, PDESolverInfo,
22};
23
24/// Type alias for coefficient functions in PDEs
25type CoefficientFunction = Box<dyn Fn(f64, f64, f64) -> f64 + Send + Sync>;
26
27/// Type alias for initial condition function
28type InitialCondition = Box<dyn Fn(f64) -> f64 + Send + Sync>;
29
30// Re-export ADI implementation
31mod adi;
32pub use adi::{ADIResult, ADI2D};
33
34/// Available implicit time-stepping schemes
35#[derive(Debug, Clone, Copy, PartialEq)]
36pub enum ImplicitMethod {
37 /// Backward Euler method (first-order, L-stable)
38 BackwardEuler,
39
40 /// Crank-Nicolson method (second-order, A-stable)
41 CrankNicolson,
42
43 /// Trapezoidal rule (second-order, A-stable)
44 TrapezoidalRule,
45
46 /// Alternating Direction Implicit method (efficient for multi-dimensional problems)
47 ADI,
48}
49
50/// Options for implicit PDE solvers
51#[derive(Debug, Clone)]
52pub struct ImplicitOptions {
53 /// Implicit time-stepping method to use
54 pub method: ImplicitMethod,
55
56 /// Tolerance for iterative solvers
57 pub tolerance: f64,
58
59 /// Maximum number of iterations for linear system solver
60 pub max_iterations: usize,
61
62 /// Fixed time step (if None, adaptive time-stepping will be used)
63 pub dt: Option<f64>,
64
65 /// Minimum time step for adaptive time-stepping
66 pub min_dt: Option<f64>,
67
68 /// Maximum time step for adaptive time-stepping
69 pub max_dt: Option<f64>,
70
71 /// Time steps to save (if None, all steps will be saved)
72 pub save_every: Option<usize>,
73
74 /// Print detailed progress information
75 pub verbose: bool,
76}
77
78impl Default for ImplicitOptions {
79 fn default() -> Self {
80 ImplicitOptions {
81 method: ImplicitMethod::CrankNicolson,
82 tolerance: 1e-6,
83 max_iterations: 100,
84 dt: Some(0.01),
85 min_dt: Some(1e-6),
86 max_dt: Some(0.1),
87 save_every: None,
88 verbose: false,
89 }
90 }
91}
92
93/// Result of implicit method solution
94pub struct ImplicitResult {
95 /// Time points
96 pub t: Array1<f64>,
97
98 /// Solution values, indexed as [time, space...]
99 pub u: Vec<Array2<f64>>,
100
101 /// Solver information
102 pub info: Option<String>,
103
104 /// Computation time
105 pub computation_time: f64,
106
107 /// Number of time steps
108 pub num_steps: usize,
109
110 /// Number of linear system solves
111 pub num_linear_solves: usize,
112}
113
114/// Crank-Nicolson solver for 1D parabolic PDEs
115///
116/// This solver uses the Crank-Nicolson method, which is a second-order
117/// implicit method that is unconditionally stable for the heat equation.
118pub struct CrankNicolson1D {
119 /// Spatial domain
120 domain: Domain,
121
122 /// Time range [t_start, t_end]
123 time_range: [f64; 2],
124
125 /// Diffusion coefficient function D(x, t, u) for ∂²u/∂x²
126 diffusion_coeff: CoefficientFunction,
127
128 /// Advection coefficient function v(x, t, u) for ∂u/∂x
129 advection_coeff: Option<CoefficientFunction>,
130
131 /// Reaction term function f(x, t, u)
132 reaction_term: Option<CoefficientFunction>,
133
134 /// Initial condition function u(x, 0)
135 initial_condition: InitialCondition,
136
137 /// Boundary conditions
138 boundary_conditions: Vec<BoundaryCondition<f64>>,
139
140 /// Finite difference scheme for spatial discretization
141 fd_scheme: FiniteDifferenceScheme,
142
143 /// Solver options
144 options: ImplicitOptions,
145}
146
147impl CrankNicolson1D {
148 /// Create a new Crank-Nicolson solver for 1D parabolic PDEs
149 pub fn new(
150 domain: Domain,
151 time_range: [f64; 2],
152 diffusion_coeff: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
153 initial_condition: impl Fn(f64) -> f64 + Send + Sync + 'static,
154 boundary_conditions: Vec<BoundaryCondition<f64>>,
155 options: Option<ImplicitOptions>,
156 ) -> PDEResult<Self> {
157 // Validate the domain
158 if domain.dimensions() != 1 {
159 return Err(PDEError::DomainError(
160 "Domain must be 1-dimensional for 1D Crank-Nicolson solver".to_string(),
161 ));
162 }
163
164 // Validate time _range
165 if time_range[0] >= time_range[1] {
166 return Err(PDEError::DomainError(
167 "Invalid time _range: start must be less than end".to_string(),
168 ));
169 }
170
171 // Validate boundary _conditions
172 if boundary_conditions.len() != 2 {
173 return Err(PDEError::BoundaryConditions(
174 "1D parabolic PDE requires exactly 2 boundary _conditions".to_string(),
175 ));
176 }
177
178 // Ensure we have both lower and upper boundary _conditions
179 let has_lower = boundary_conditions
180 .iter()
181 .any(|bc| bc.location == BoundaryLocation::Lower);
182 let has_upper = boundary_conditions
183 .iter()
184 .any(|bc| bc.location == BoundaryLocation::Upper);
185
186 if !has_lower || !has_upper {
187 return Err(PDEError::BoundaryConditions(
188 "1D parabolic PDE requires both lower and upper boundary _conditions".to_string(),
189 ));
190 }
191
192 let mut options = options.unwrap_or_default();
193 options.method = ImplicitMethod::CrankNicolson;
194
195 Ok(CrankNicolson1D {
196 domain,
197 time_range,
198 diffusion_coeff: Box::new(diffusion_coeff),
199 advection_coeff: None,
200 reaction_term: None,
201 initial_condition: Box::new(initial_condition),
202 boundary_conditions,
203 fd_scheme: FiniteDifferenceScheme::CentralDifference,
204 options,
205 })
206 }
207
208 /// Add an advection term to the PDE
209 pub fn with_advection(
210 mut self,
211 advection_coeff: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
212 ) -> Self {
213 self.advection_coeff = Some(Box::new(advection_coeff));
214 self
215 }
216
217 /// Add a reaction term to the PDE
218 pub fn with_reaction(
219 mut self,
220 reaction_term: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
221 ) -> Self {
222 self.reaction_term = Some(Box::new(reaction_term));
223 self
224 }
225
226 /// Set the finite difference scheme for spatial discretization
227 pub fn with_fd_scheme(mut self, scheme: FiniteDifferenceScheme) -> Self {
228 self.fd_scheme = scheme;
229 self
230 }
231
232 /// Solve the PDE using the Crank-Nicolson method
233 pub fn solve(&self) -> PDEResult<ImplicitResult> {
234 let start_time = Instant::now();
235
236 // Generate spatial grid
237 let x_grid = self.domain.grid(0)?;
238 let nx = x_grid.len();
239 let dx = self.domain.grid_spacing(0)?;
240
241 // Time step
242 let dt = self.options.dt.unwrap_or(0.01);
243
244 // Calculate number of time steps
245 let t_start = self.time_range[0];
246 let t_end = self.time_range[1];
247 let num_steps = ((t_end - t_start) / dt).ceil() as usize;
248
249 // Initialize time array
250 let mut t_values = Vec::with_capacity(num_steps + 1);
251 t_values.push(t_start);
252
253 // Initialize solution arrays
254 let mut u_current = Array1::zeros(nx);
255
256 // Apply initial condition
257 for (i, &x) in x_grid.iter().enumerate() {
258 u_current[i] = (self.initial_condition)(x);
259 }
260
261 // Apply Dirichlet boundary conditions to initial condition
262 apply_dirichlet_conditions_to_initial_1d(&mut u_current, &self.boundary_conditions);
263
264 // Store solutions
265 let save_every = self.options.save_every.unwrap_or(1);
266 let mut solutions = Vec::with_capacity((num_steps + 1) / save_every + 1);
267 solutions.push(
268 u_current
269 .clone()
270 .into_shape_with_order((nx, 1))
271 .expect("Operation failed"),
272 );
273
274 // Initialize matrices for Crank-Nicolson method
275 let mut a_matrix = Array2::zeros((nx, nx));
276 let mut b_matrix = Array2::zeros((nx, nx));
277
278 // Track solver statistics
279 let mut num_linear_solves = 0;
280
281 // Time-stepping loop
282 for step in 0..num_steps {
283 let t_current = t_start + step as f64 * dt;
284 let t_next = t_current + dt;
285
286 // Set up coefficient matrices based on PDE terms and boundary conditions
287 self.setup_coefficient_matrices(
288 &mut a_matrix,
289 &mut b_matrix,
290 &x_grid,
291 dx,
292 dt,
293 t_current,
294 );
295
296 // Right-hand side vector
297 let rhs = b_matrix.dot(&u_current);
298
299 // Solve the linear system: A * u_{n+1} = B * u_n
300 let u_next = self.solve_linear_system(&a_matrix, &rhs.view())?;
301 num_linear_solves += 1;
302
303 // Update current solution and time
304 u_current = u_next;
305 t_values.push(t_next);
306
307 // Save solution if needed
308 if (step + 1) % save_every == 0 || step == num_steps - 1 {
309 solutions.push(
310 u_current
311 .clone()
312 .into_shape_with_order((nx, 1))
313 .expect("Operation failed"),
314 );
315 }
316
317 // Print progress if verbose
318 if self.options.verbose && (step + 1) % 10 == 0 {
319 println!(
320 "Step {}/{} completed, t = {:.4}",
321 step + 1,
322 num_steps,
323 t_next
324 );
325 }
326 }
327
328 // Convert time values to Array1
329 let t_array = Array1::from_vec(t_values);
330
331 // Compute solution time
332 let computation_time = start_time.elapsed().as_secs_f64();
333
334 // Create result
335 let info = Some(format!(
336 "Time steps: {num_steps}, Linear system solves: {num_linear_solves}"
337 ));
338
339 Ok(ImplicitResult {
340 t: t_array,
341 u: solutions,
342 info,
343 computation_time,
344 num_steps,
345 num_linear_solves,
346 })
347 }
348
349 /// Set up coefficient matrices for the Crank-Nicolson method
350 fn setup_coefficient_matrices(
351 &self,
352 a_matrix: &mut Array2<f64>,
353 b_matrix: &mut Array2<f64>,
354 x_grid: &Array1<f64>,
355 dx: f64,
356 dt: f64,
357 t: f64,
358 ) {
359 let nx = x_grid.len();
360
361 // Clear matrices
362 a_matrix.fill(0.0);
363 b_matrix.fill(0.0);
364
365 // Set up implicit (A) and explicit (B) matrices for interior points
366 for i in 1..nx - 1 {
367 let x = x_grid[i];
368 let u_val = 0.0; // Used for linearization around previous state if needed
369
370 // Diffusion coefficient at the current point
371 let d = (self.diffusion_coeff)(x, t, u_val);
372
373 // Crank-Nicolson coefficients for diffusion term
374 let r = 0.5 * d * dt / (dx * dx);
375
376 // Implicit part (left-hand side)
377 a_matrix[[i, i - 1]] = -r; // Coefficient for u_{i-1}^{n+1}
378 a_matrix[[i, i]] = 1.0 + 2.0 * r; // Coefficient for u_{i}^{n+1}
379 a_matrix[[i, i + 1]] = -r; // Coefficient for u_{i+1}^{n+1}
380
381 // Explicit part (right-hand side)
382 b_matrix[[i, i - 1]] = r; // Coefficient for u_{i-1}^{n}
383 b_matrix[[i, i]] = 1.0 - 2.0 * r; // Coefficient for u_{i}^{n}
384 b_matrix[[i, i + 1]] = r; // Coefficient for u_{i+1}^{n}
385
386 // Add advection term if present
387 if let Some(advection) = &self.advection_coeff {
388 let v = advection(x, t, u_val);
389
390 // Coefficient for advection term (central difference)
391 let c = 0.25 * v * dt / dx;
392
393 // Implicit part
394 a_matrix[[i, i - 1]] -= c; // Additional term for u_{i-1}^{n+1}
395 a_matrix[[i, i + 1]] += c; // Additional term for u_{i+1}^{n+1}
396
397 // Explicit part
398 b_matrix[[i, i - 1]] -= c; // Additional term for u_{i-1}^{n}
399 b_matrix[[i, i + 1]] += c; // Additional term for u_{i+1}^{n}
400 }
401
402 // Add reaction term if present
403 if let Some(reaction) = &self.reaction_term {
404 // For linear reaction terms of the form R(u) = ku
405 // For nonlinear terms, would need to linearize around previous state
406 let k = reaction(x, t, u_val);
407
408 // Coefficient for reaction term
409 let s = 0.5 * k * dt;
410
411 // Implicit part
412 a_matrix[[i, i]] += s; // Additional term for u_{i}^{n+1}
413
414 // Explicit part
415 b_matrix[[i, i]] += s; // Additional term for u_{i}^{n}
416 }
417 }
418
419 // Apply boundary conditions
420 for bc in &self.boundary_conditions {
421 match bc.location {
422 BoundaryLocation::Lower => {
423 // Apply boundary condition at x[0]
424 match bc.bc_type {
425 BoundaryConditionType::Dirichlet => {
426 // u(a, t) = bc.value
427 // Set the row to enforce u_0^{n+1} = bc.value
428 for j in 0..nx {
429 a_matrix[[0, j]] = 0.0;
430 b_matrix[[0, j]] = 0.0;
431 }
432 a_matrix[[0, 0]] = 1.0;
433 b_matrix[[0, 0]] = 0.0;
434
435 // Add boundary value to RHS
436 b_matrix[[0, 0]] = bc.value;
437 }
438 BoundaryConditionType::Neumann => {
439 // du/dx(a, t) = bc.value
440 // Use second-order one-sided difference:
441 // (-3u_0 + 4u_1 - u_2)/(2dx) = bc.value
442
443 for j in 0..nx {
444 a_matrix[[0, j]] = 0.0;
445 b_matrix[[0, j]] = 0.0;
446 }
447
448 // Implicit part
449 a_matrix[[0, 0]] = -3.0;
450 a_matrix[[0, 1]] = 4.0;
451 a_matrix[[0, 2]] = -1.0;
452
453 // RHS is the boundary value scaled by 2*dx
454 b_matrix[[0, 0]] = 2.0 * dx * bc.value;
455 }
456 BoundaryConditionType::Robin => {
457 // a*u + b*du/dx = c
458 if let Some([a_val, b_val, c_val]) = bc.coefficients {
459 // Use second-order one-sided difference for the derivative:
460 // (-3u_0 + 4u_1 - u_2)/(2dx)
461
462 for j in 0..nx {
463 a_matrix[[0, j]] = 0.0;
464 b_matrix[[0, j]] = 0.0;
465 }
466
467 // Implicit part: a*u_0 + b*(-3u_0 + 4u_1 - u_2)/(2dx) = c
468 a_matrix[[0, 0]] = a_val - 3.0 * b_val / (2.0 * dx);
469 a_matrix[[0, 1]] = 4.0 * b_val / (2.0 * dx);
470 a_matrix[[0, 2]] = -b_val / (2.0 * dx);
471
472 // RHS is the boundary value c
473 b_matrix[[0, 0]] = c_val;
474 }
475 }
476 BoundaryConditionType::Periodic => {
477 // For periodic BCs, special handling at both boundaries together is needed
478 // (see below)
479 }
480 }
481 }
482 BoundaryLocation::Upper => {
483 // Apply boundary condition at x[nx-1]
484 let i = nx - 1;
485
486 match bc.bc_type {
487 BoundaryConditionType::Dirichlet => {
488 // u(b, t) = bc.value
489 // Set the row to enforce u_{nx-1}^{n+1} = bc.value
490 for j in 0..nx {
491 a_matrix[[i, j]] = 0.0;
492 b_matrix[[i, j]] = 0.0;
493 }
494 a_matrix[[i, i]] = 1.0;
495 b_matrix[[i, i]] = 0.0;
496
497 // Add boundary value to RHS
498 b_matrix[[i, i]] = bc.value;
499 }
500 BoundaryConditionType::Neumann => {
501 // du/dx(b, t) = bc.value
502 // Use second-order one-sided difference:
503 // (3u_{nx-1} - 4u_{nx-2} + u_{nx-3})/(2dx) = bc.value
504
505 for j in 0..nx {
506 a_matrix[[i, j]] = 0.0;
507 b_matrix[[i, j]] = 0.0;
508 }
509
510 // Implicit part
511 a_matrix[[i, i]] = 3.0;
512 a_matrix[[i, i - 1]] = -4.0;
513 a_matrix[[i, i - 2]] = 1.0;
514
515 // RHS is the boundary value scaled by 2*dx
516 b_matrix[[i, i]] = 2.0 * dx * bc.value;
517 }
518 BoundaryConditionType::Robin => {
519 // a*u + b*du/dx = c
520 if let Some([a_val, b_val, c_val]) = bc.coefficients {
521 // Use second-order one-sided difference for the derivative:
522 // (3u_{nx-1} - 4u_{nx-2} + u_{nx-3})/(2dx)
523
524 for j in 0..nx {
525 a_matrix[[i, j]] = 0.0;
526 b_matrix[[i, j]] = 0.0;
527 }
528
529 // Implicit part: a*u_{nx-1} + b*(3u_{nx-1} - 4u_{nx-2} + u_{nx-3})/(2dx) = c
530 a_matrix[[i, i]] = a_val + 3.0 * b_val / (2.0 * dx);
531 a_matrix[[i, i - 1]] = -4.0 * b_val / (2.0 * dx);
532 a_matrix[[i, i - 2]] = b_val / (2.0 * dx);
533
534 // RHS is the boundary value c
535 b_matrix[[i, i]] = c_val;
536 }
537 }
538 BoundaryConditionType::Periodic => {
539 // Handle periodic boundary conditions
540 // We need to make u_0 = u_{nx-1} and ensure the fluxes match at the boundaries
541
542 // First, clear the boundary rows
543 for j in 0..nx {
544 a_matrix[[0, j]] = 0.0;
545 a_matrix[[i, j]] = 0.0;
546 b_matrix[[0, j]] = 0.0;
547 b_matrix[[i, j]] = 0.0;
548 }
549
550 // For the lower boundary (i=0), set up equation connecting to upper boundary
551 // Use periodic stencil for diffusion
552 let x = x_grid[0];
553 let u_val = 0.0; // Used for linearization if needed
554 let d = (self.diffusion_coeff)(x, t, u_val);
555 let r = 0.5 * d * dt / (dx * dx);
556
557 a_matrix[[0, i]] = -r; // Coefficient for u_{nx-1}^{n+1}
558 a_matrix[[0, 0]] = 1.0 + 2.0 * r; // Coefficient for u_{0}^{n+1}
559 a_matrix[[0, 1]] = -r; // Coefficient for u_{1}^{n+1}
560
561 b_matrix[[0, i]] = r; // Coefficient for u_{nx-1}^{n}
562 b_matrix[[0, 0]] = 1.0 - 2.0 * r; // Coefficient for u_{0}^{n}
563 b_matrix[[0, 1]] = r; // Coefficient for u_{1}^{n}
564
565 // For the upper boundary (i=nx-1), set up equation connecting to lower boundary
566 let x = x_grid[i];
567 let d = (self.diffusion_coeff)(x, t, u_val);
568 let r = 0.5 * d * dt / (dx * dx);
569
570 a_matrix[[i, i - 1]] = -r; // Coefficient for u_{nx-2}^{n+1}
571 a_matrix[[i, i]] = 1.0 + 2.0 * r; // Coefficient for u_{nx-1}^{n+1}
572 a_matrix[[i, 0]] = -r; // Coefficient for u_{0}^{n+1}
573
574 b_matrix[[i, i - 1]] = r; // Coefficient for u_{nx-2}^{n}
575 b_matrix[[i, i]] = 1.0 - 2.0 * r; // Coefficient for u_{nx-1}^{n}
576 b_matrix[[i, 0]] = r; // Coefficient for u_{0}^{n}
577
578 // Add advection term if present
579 if let Some(advection) = &self.advection_coeff {
580 // Lower boundary
581 let v = advection(x_grid[0], t, u_val);
582 let c = 0.25 * v * dt / dx;
583
584 a_matrix[[0, i]] -= c; // Additional term for u_{nx-1}^{n+1}
585 a_matrix[[0, 1]] += c; // Additional term for u_{1}^{n+1}
586
587 b_matrix[[0, i]] -= c; // Additional term for u_{nx-1}^{n}
588 b_matrix[[0, 1]] += c; // Additional term for u_{1}^{n}
589
590 // Upper boundary
591 let v = advection(x_grid[i], t, u_val);
592 let c = 0.25 * v * dt / dx;
593
594 a_matrix[[i, i - 1]] -= c; // Additional term for u_{nx-2}^{n+1}
595 a_matrix[[i, 0]] += c; // Additional term for u_{0}^{n+1}
596
597 b_matrix[[i, i - 1]] -= c; // Additional term for u_{nx-2}^{n}
598 b_matrix[[i, 0]] += c; // Additional term for u_{0}^{n}
599 }
600 }
601 }
602 }
603 }
604 }
605
606 // Special case for periodic boundary conditions
607 // If we have both lower and upper periodic boundary conditions,
608 // we need to make sure they're consistent
609 let has_periodic_lower = self.boundary_conditions.iter().any(|bc| {
610 bc.location == BoundaryLocation::Lower && bc.bc_type == BoundaryConditionType::Periodic
611 });
612
613 let has_periodic_upper = self.boundary_conditions.iter().any(|bc| {
614 bc.location == BoundaryLocation::Upper && bc.bc_type == BoundaryConditionType::Periodic
615 });
616
617 if has_periodic_lower && has_periodic_upper {
618 // Already handled in the boundary condition loop
619 }
620 }
621
622 /// Solve the linear system Ax = b
623 fn solve_linear_system(&self, a: &Array2<f64>, b: &ArrayView1<f64>) -> PDEResult<Array1<f64>> {
624 let n = b.len();
625
626 // Simple tridiagonal solver for Crank-Nicolson matrices
627 // For a general solver, use a specialized linear algebra library
628
629 // Check if the matrix is tridiagonal
630 let is_tridiagonal = a
631 .indexed_iter()
632 .filter(|((i, j), &val)| val != 0.0 && (*i as isize - *j as isize).abs() > 1)
633 .count()
634 == 0;
635
636 if is_tridiagonal {
637 // Extract the tridiagonal elements
638 let mut lower = Array1::zeros(n - 1);
639 let mut diagonal = Array1::zeros(n);
640 let mut upper = Array1::zeros(n - 1);
641
642 for i in 0..n {
643 diagonal[i] = a[[i, i]];
644 if i < n - 1 {
645 upper[i] = a[[i, i + 1]];
646 }
647 if i > 0 {
648 lower[i - 1] = a[[i, i - 1]];
649 }
650 }
651
652 // Solve tridiagonal system using Thomas algorithm
653 let mut solution = Array1::zeros(n);
654 let mut temp_diag = diagonal.clone();
655 let mut temp_rhs = b.to_owned();
656
657 // Forward sweep
658 for i in 1..n {
659 let w = lower[i - 1] / temp_diag[i - 1];
660 temp_diag[i] -= w * upper[i - 1];
661 temp_rhs[i] -= w * temp_rhs[i - 1];
662 }
663
664 // Backward sweep
665 solution[n - 1] = temp_rhs[n - 1] / temp_diag[n - 1];
666 for i in (0..n - 1).rev() {
667 solution[i] = (temp_rhs[i] - upper[i] * solution[i + 1]) / temp_diag[i];
668 }
669
670 Ok(solution)
671 } else {
672 // General case: Gaussian elimination with partial pivoting
673 // For a real implementation, use a specialized linear algebra library
674
675 // Create copies of A and b
676 let mut a_copy = a.clone();
677 let mut b_copy = b.to_owned();
678
679 // Forward elimination
680 for i in 0..n {
681 // Find pivot
682 let mut max_val = a_copy[[i, i]].abs();
683 let mut max_row = i;
684
685 for k in i + 1..n {
686 if a_copy[[k, i]].abs() > max_val {
687 max_val = a_copy[[k, i]].abs();
688 max_row = k;
689 }
690 }
691
692 // Check if matrix is singular
693 if max_val < 1e-10 {
694 return Err(PDEError::Other(
695 "Matrix is singular or nearly singular".to_string(),
696 ));
697 }
698
699 // Swap rows if necessary
700 if max_row != i {
701 for j in i..n {
702 let temp = a_copy[[i, j]];
703 a_copy[[i, j]] = a_copy[[max_row, j]];
704 a_copy[[max_row, j]] = temp;
705 }
706
707 let temp = b_copy[i];
708 b_copy[i] = b_copy[max_row];
709 b_copy[max_row] = temp;
710 }
711
712 // Eliminate below
713 for k in i + 1..n {
714 let factor = a_copy[[k, i]] / a_copy[[i, i]];
715
716 for j in i..n {
717 a_copy[[k, j]] -= factor * a_copy[[i, j]];
718 }
719
720 b_copy[k] -= factor * b_copy[i];
721 }
722 }
723
724 // Back substitution
725 let mut x = Array1::zeros(n);
726 for i in (0..n).rev() {
727 let mut sum = 0.0;
728 for j in i + 1..n {
729 sum += a_copy[[i, j]] * x[j];
730 }
731
732 x[i] = (b_copy[i] - sum) / a_copy[[i, i]];
733 }
734
735 Ok(x)
736 }
737 }
738}
739
740/// Backward Euler solver for 1D parabolic PDEs
741///
742/// This solver uses the Backward Euler method, which is a first-order
743/// fully implicit method that is L-stable and well-suited for stiff problems.
744pub struct BackwardEuler1D {
745 /// Spatial domain
746 domain: Domain,
747
748 /// Time range [t_start, t_end]
749 time_range: [f64; 2],
750
751 /// Diffusion coefficient function D(x, t, u) for ∂²u/∂x²
752 diffusion_coeff: CoefficientFunction,
753
754 /// Advection coefficient function v(x, t, u) for ∂u/∂x
755 advection_coeff: Option<CoefficientFunction>,
756
757 /// Reaction term function f(x, t, u)
758 reaction_term: Option<CoefficientFunction>,
759
760 /// Initial condition function u(x, 0)
761 initial_condition: InitialCondition,
762
763 /// Boundary conditions
764 boundary_conditions: Vec<BoundaryCondition<f64>>,
765
766 /// Finite difference scheme for spatial discretization
767 fd_scheme: FiniteDifferenceScheme,
768
769 /// Solver options
770 options: ImplicitOptions,
771}
772
773impl BackwardEuler1D {
774 /// Create a new Backward Euler solver for 1D parabolic PDEs
775 pub fn new(
776 domain: Domain,
777 time_range: [f64; 2],
778 diffusion_coeff: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
779 initial_condition: impl Fn(f64) -> f64 + Send + Sync + 'static,
780 boundary_conditions: Vec<BoundaryCondition<f64>>,
781 options: Option<ImplicitOptions>,
782 ) -> PDEResult<Self> {
783 // Validate the domain
784 if domain.dimensions() != 1 {
785 return Err(PDEError::DomainError(
786 "Domain must be 1-dimensional for 1D Backward Euler solver".to_string(),
787 ));
788 }
789
790 // Validate time _range
791 if time_range[0] >= time_range[1] {
792 return Err(PDEError::DomainError(
793 "Invalid time _range: start must be less than end".to_string(),
794 ));
795 }
796
797 // Validate boundary _conditions
798 if boundary_conditions.len() != 2 {
799 return Err(PDEError::BoundaryConditions(
800 "1D parabolic PDE requires exactly 2 boundary _conditions".to_string(),
801 ));
802 }
803
804 // Ensure we have both lower and upper boundary _conditions
805 let has_lower = boundary_conditions
806 .iter()
807 .any(|bc| bc.location == BoundaryLocation::Lower);
808 let has_upper = boundary_conditions
809 .iter()
810 .any(|bc| bc.location == BoundaryLocation::Upper);
811
812 if !has_lower || !has_upper {
813 return Err(PDEError::BoundaryConditions(
814 "1D parabolic PDE requires both lower and upper boundary _conditions".to_string(),
815 ));
816 }
817
818 let mut options = options.unwrap_or_default();
819 options.method = ImplicitMethod::BackwardEuler;
820
821 Ok(BackwardEuler1D {
822 domain,
823 time_range,
824 diffusion_coeff: Box::new(diffusion_coeff),
825 advection_coeff: None,
826 reaction_term: None,
827 initial_condition: Box::new(initial_condition),
828 boundary_conditions,
829 fd_scheme: FiniteDifferenceScheme::CentralDifference,
830 options,
831 })
832 }
833
834 /// Add an advection term to the PDE
835 pub fn with_advection(
836 mut self,
837 advection_coeff: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
838 ) -> Self {
839 self.advection_coeff = Some(Box::new(advection_coeff));
840 self
841 }
842
843 /// Add a reaction term to the PDE
844 pub fn with_reaction(
845 mut self,
846 reaction_term: impl Fn(f64, f64, f64) -> f64 + Send + Sync + 'static,
847 ) -> Self {
848 self.reaction_term = Some(Box::new(reaction_term));
849 self
850 }
851
852 /// Set the finite difference scheme for spatial discretization
853 pub fn with_fd_scheme(mut self, scheme: FiniteDifferenceScheme) -> Self {
854 self.fd_scheme = scheme;
855 self
856 }
857
858 /// Solve the PDE using the Backward Euler method
859 pub fn solve(&self) -> PDEResult<ImplicitResult> {
860 let start_time = Instant::now();
861
862 // Generate spatial grid
863 let x_grid = self.domain.grid(0)?;
864 let nx = x_grid.len();
865 let dx = self.domain.grid_spacing(0)?;
866
867 // Time step
868 let dt = self.options.dt.unwrap_or(0.01);
869
870 // Calculate number of time steps
871 let t_start = self.time_range[0];
872 let t_end = self.time_range[1];
873 let num_steps = ((t_end - t_start) / dt).ceil() as usize;
874
875 // Initialize time array
876 let mut t_values = Vec::with_capacity(num_steps + 1);
877 t_values.push(t_start);
878
879 // Initialize solution arrays
880 let mut u_current = Array1::zeros(nx);
881
882 // Apply initial condition
883 for (i, &x) in x_grid.iter().enumerate() {
884 u_current[i] = (self.initial_condition)(x);
885 }
886
887 // Apply Dirichlet boundary conditions to initial condition
888 apply_dirichlet_conditions_to_initial_1d(&mut u_current, &self.boundary_conditions);
889
890 // Store solutions
891 let save_every = self.options.save_every.unwrap_or(1);
892 let mut solutions = Vec::with_capacity((num_steps + 1) / save_every + 1);
893 solutions.push(
894 u_current
895 .clone()
896 .into_shape_with_order((nx, 1))
897 .expect("Operation failed"),
898 );
899
900 // Initialize coefficient matrix for Backward Euler method
901 let mut a_matrix = Array2::zeros((nx, nx));
902
903 // Track solver statistics
904 let mut num_linear_solves = 0;
905
906 // Time-stepping loop
907 for step in 0..num_steps {
908 let t_current = t_start + step as f64 * dt;
909 let t_next = t_current + dt;
910
911 // Set up coefficient matrix based on PDE terms and boundary conditions
912 self.setup_coefficient_matrix(
913 &mut a_matrix,
914 &x_grid,
915 dx,
916 dt,
917 t_next, // Use t_{n+1} for fully implicit scheme
918 );
919
920 // Right-hand side vector is just the current solution
921 let rhs = u_current.clone();
922
923 // Solve the linear system: A * u_{n+1} = u_n
924 let u_next = self.solve_linear_system(&a_matrix, &rhs.view())?;
925 num_linear_solves += 1;
926
927 // Update current solution and time
928 u_current = u_next;
929 t_values.push(t_next);
930
931 // Save solution if needed
932 if (step + 1) % save_every == 0 || step == num_steps - 1 {
933 solutions.push(
934 u_current
935 .clone()
936 .into_shape_with_order((nx, 1))
937 .expect("Operation failed"),
938 );
939 }
940
941 // Print progress if verbose
942 if self.options.verbose && (step + 1) % 10 == 0 {
943 println!(
944 "Step {}/{} completed, t = {:.4}",
945 step + 1,
946 num_steps,
947 t_next
948 );
949 }
950 }
951
952 // Convert time values to Array1
953 let t_array = Array1::from_vec(t_values);
954
955 // Compute solution time
956 let computation_time = start_time.elapsed().as_secs_f64();
957
958 // Create result
959 let info = Some(format!(
960 "Time steps: {num_steps}, Linear system solves: {num_linear_solves}"
961 ));
962
963 Ok(ImplicitResult {
964 t: t_array,
965 u: solutions,
966 info,
967 computation_time,
968 num_steps,
969 num_linear_solves,
970 })
971 }
972
973 /// Set up coefficient matrix for the Backward Euler method
974 fn setup_coefficient_matrix(
975 &self,
976 a_matrix: &mut Array2<f64>,
977 x_grid: &Array1<f64>,
978 dx: f64,
979 dt: f64,
980 t: f64,
981 ) {
982 let nx = x_grid.len();
983
984 // Clear _matrix
985 a_matrix.fill(0.0);
986
987 // Set up implicit _matrix for interior points
988 for i in 1..nx - 1 {
989 let x = x_grid[i];
990 let u_val = 0.0; // Used for linearization around previous state if needed
991
992 // Diffusion coefficient at the current point
993 let d = (self.diffusion_coeff)(x, t, u_val);
994
995 // Backward Euler coefficients for diffusion term
996 let r = d * dt / (dx * dx);
997
998 // Coefficient _matrix for implicit scheme
999 a_matrix[[i, i - 1]] = -r; // Coefficient for u_{i-1}^{n+1}
1000 a_matrix[[i, i]] = 1.0 + 2.0 * r; // Coefficient for u_{i}^{n+1}
1001 a_matrix[[i, i + 1]] = -r; // Coefficient for u_{i+1}^{n+1}
1002
1003 // Add advection term if present
1004 if let Some(advection) = &self.advection_coeff {
1005 let v = advection(x, t, u_val);
1006
1007 // Coefficient for advection term (upwind for stability)
1008 if v > 0.0 {
1009 // Use backward difference for positive velocity
1010 let c = v * dt / dx;
1011 a_matrix[[i, i]] += c;
1012 a_matrix[[i, i - 1]] -= c;
1013 } else {
1014 // Use forward difference for negative velocity
1015 let c = -v * dt / dx;
1016 a_matrix[[i, i]] += c;
1017 a_matrix[[i, i + 1]] -= c;
1018 }
1019 }
1020
1021 // Add reaction term if present
1022 if let Some(reaction) = &self.reaction_term {
1023 // For linear reaction terms of the form R(u) = ku
1024 // For nonlinear terms, would need to linearize around previous state
1025 let k = reaction(x, t, u_val);
1026
1027 // Coefficient for reaction term
1028 let s = k * dt;
1029 a_matrix[[i, i]] += s; // Additional term for u_{i}^{n+1}
1030 }
1031 }
1032
1033 // Apply boundary conditions
1034 for bc in &self.boundary_conditions {
1035 match bc.location {
1036 BoundaryLocation::Lower => {
1037 // Apply boundary condition at x[0]
1038 match bc.bc_type {
1039 BoundaryConditionType::Dirichlet => {
1040 // u(a, t) = bc.value
1041 // Replace the equation with u_0^{n+1} = bc.value
1042 for j in 0..nx {
1043 a_matrix[[0, j]] = 0.0;
1044 }
1045 a_matrix[[0, 0]] = 1.0;
1046
1047 // Right-hand side will have the boundary value
1048 // For Backward Euler, we directly modify the solution vector
1049 // to enforce the boundary condition
1050 }
1051 BoundaryConditionType::Neumann => {
1052 // du/dx(a, t) = bc.value
1053 // Use second-order one-sided difference:
1054 // (-3u_0 + 4u_1 - u_2)/(2dx) = bc.value
1055
1056 for j in 0..nx {
1057 a_matrix[[0, j]] = 0.0;
1058 }
1059
1060 a_matrix[[0, 0]] = -3.0;
1061 a_matrix[[0, 1]] = 4.0;
1062 a_matrix[[0, 2]] = -1.0;
1063
1064 // Right-hand side will have the boundary value scaled by 2*dx
1065 // For Backward Euler, we directly modify the solution vector
1066 }
1067 BoundaryConditionType::Robin => {
1068 // a*u + b*du/dx = c
1069 if let Some([a_val, b_val, c_val]) = bc.coefficients {
1070 // Use second-order one-sided difference for the derivative:
1071 // (-3u_0 + 4u_1 - u_2)/(2dx)
1072
1073 for j in 0..nx {
1074 a_matrix[[0, j]] = 0.0;
1075 }
1076
1077 a_matrix[[0, 0]] = a_val - 3.0 * b_val / (2.0 * dx);
1078 a_matrix[[0, 1]] = 4.0 * b_val / (2.0 * dx);
1079 a_matrix[[0, 2]] = -b_val / (2.0 * dx);
1080
1081 // Right-hand side will have the boundary value c
1082 // For Backward Euler, we directly modify the solution vector
1083 }
1084 }
1085 BoundaryConditionType::Periodic => {
1086 // For periodic BCs, special handling at both boundaries together is needed
1087 // (see below)
1088 }
1089 }
1090 }
1091 BoundaryLocation::Upper => {
1092 // Apply boundary condition at x[nx-1]
1093 let i = nx - 1;
1094
1095 match bc.bc_type {
1096 BoundaryConditionType::Dirichlet => {
1097 // u(b, t) = bc.value
1098 // Replace the equation with u_{nx-1}^{n+1} = bc.value
1099 for j in 0..nx {
1100 a_matrix[[i, j]] = 0.0;
1101 }
1102 a_matrix[[i, i]] = 1.0;
1103
1104 // Right-hand side will have the boundary value
1105 // For Backward Euler, we directly modify the solution vector
1106 }
1107 BoundaryConditionType::Neumann => {
1108 // du/dx(b, t) = bc.value
1109 // Use second-order one-sided difference:
1110 // (3u_{nx-1} - 4u_{nx-2} + u_{nx-3})/(2dx) = bc.value
1111
1112 for j in 0..nx {
1113 a_matrix[[i, j]] = 0.0;
1114 }
1115
1116 a_matrix[[i, i]] = 3.0;
1117 a_matrix[[i, i - 1]] = -4.0;
1118 a_matrix[[i, i - 2]] = 1.0;
1119
1120 // Right-hand side will have the boundary value scaled by 2*dx
1121 // For Backward Euler, we directly modify the solution vector
1122 }
1123 BoundaryConditionType::Robin => {
1124 // a*u + b*du/dx = c
1125 if let Some([a_val, b_val, c_val]) = bc.coefficients {
1126 // Use second-order one-sided difference for the derivative:
1127 // (3u_{nx-1} - 4u_{nx-2} + u_{nx-3})/(2dx)
1128
1129 for j in 0..nx {
1130 a_matrix[[i, j]] = 0.0;
1131 }
1132
1133 a_matrix[[i, i]] = a_val + 3.0 * b_val / (2.0 * dx);
1134 a_matrix[[i, i - 1]] = -4.0 * b_val / (2.0 * dx);
1135 a_matrix[[i, i - 2]] = b_val / (2.0 * dx);
1136
1137 // Right-hand side will have the boundary value c
1138 // For Backward Euler, we directly modify the solution vector
1139 }
1140 }
1141 BoundaryConditionType::Periodic => {
1142 // Handle periodic boundary conditions
1143 // We need to make u_0 = u_{nx-1} and ensure the fluxes match at the boundaries
1144
1145 // First, clear the boundary rows
1146 for j in 0..nx {
1147 a_matrix[[0, j]] = 0.0;
1148 a_matrix[[i, j]] = 0.0;
1149 }
1150
1151 // For the lower boundary (i=0), set up equation connecting to upper boundary
1152 // Use periodic stencil for diffusion
1153 let x = x_grid[0];
1154 let u_val = 0.0; // Used for linearization if needed
1155 let d = (self.diffusion_coeff)(x, t, u_val);
1156 let r = d * dt / (dx * dx);
1157
1158 a_matrix[[0, i]] = -r; // Coefficient for u_{nx-1}^{n+1}
1159 a_matrix[[0, 0]] = 1.0 + 2.0 * r; // Coefficient for u_{0}^{n+1}
1160 a_matrix[[0, 1]] = -r; // Coefficient for u_{1}^{n+1}
1161
1162 // For the upper boundary (i=nx-1), set up equation connecting to lower boundary
1163 let x = x_grid[i];
1164 let d = (self.diffusion_coeff)(x, t, u_val);
1165 let r = d * dt / (dx * dx);
1166
1167 a_matrix[[i, i - 1]] = -r; // Coefficient for u_{nx-2}^{n+1}
1168 a_matrix[[i, i]] = 1.0 + 2.0 * r; // Coefficient for u_{nx-1}^{n+1}
1169 a_matrix[[i, 0]] = -r; // Coefficient for u_{0}^{n+1}
1170
1171 // Add advection term if present
1172 if let Some(advection) = &self.advection_coeff {
1173 // Lower boundary
1174 let v = advection(x_grid[0], t, u_val);
1175
1176 if v > 0.0 {
1177 // Use backward difference for positive velocity
1178 let c = v * dt / dx;
1179 a_matrix[[0, 0]] += c;
1180 a_matrix[[0, i]] -= c;
1181 } else {
1182 // Use forward difference for negative velocity
1183 let c = -v * dt / dx;
1184 a_matrix[[0, 0]] += c;
1185 a_matrix[[0, 1]] -= c;
1186 }
1187
1188 // Upper boundary
1189 let v = advection(x_grid[i], t, u_val);
1190
1191 if v > 0.0 {
1192 // Use backward difference for positive velocity
1193 let c = v * dt / dx;
1194 a_matrix[[i, i]] += c;
1195 a_matrix[[i, i - 1]] -= c;
1196 } else {
1197 // Use forward difference for negative velocity
1198 let c = -v * dt / dx;
1199 a_matrix[[i, i]] += c;
1200 a_matrix[[i, 0]] -= c;
1201 }
1202 }
1203 }
1204 }
1205 }
1206 }
1207 }
1208
1209 // Special handling for Dirichlet boundary conditions
1210 // For Backward Euler, we need to modify the RHS vector with the boundary values
1211 // This is done in the solve method when applying the boundary conditions to the solution
1212 }
1213
1214 /// Solve the linear system Ax = b
1215 fn solve_linear_system(&self, a: &Array2<f64>, b: &ArrayView1<f64>) -> PDEResult<Array1<f64>> {
1216 let n = b.len();
1217
1218 // Simple tridiagonal solver for Backward Euler matrices
1219 // For a general solver, use a specialized linear algebra library
1220
1221 // Check if the matrix is tridiagonal
1222 let is_tridiagonal = a
1223 .indexed_iter()
1224 .filter(|((i, j), &val)| val != 0.0 && (*i as isize - *j as isize).abs() > 1)
1225 .count()
1226 == 0;
1227
1228 if is_tridiagonal {
1229 // Extract the tridiagonal elements
1230 let mut lower = Array1::zeros(n - 1);
1231 let mut diagonal = Array1::zeros(n);
1232 let mut upper = Array1::zeros(n - 1);
1233
1234 for i in 0..n {
1235 diagonal[i] = a[[i, i]];
1236 if i < n - 1 {
1237 upper[i] = a[[i, i + 1]];
1238 }
1239 if i > 0 {
1240 lower[i - 1] = a[[i, i - 1]];
1241 }
1242 }
1243
1244 // Solve tridiagonal system using Thomas algorithm
1245 let mut solution = Array1::zeros(n);
1246 let mut temp_diag = diagonal.clone();
1247 let mut temp_rhs = b.to_owned();
1248
1249 // Forward sweep
1250 for i in 1..n {
1251 let w = lower[i - 1] / temp_diag[i - 1];
1252 temp_diag[i] -= w * upper[i - 1];
1253 temp_rhs[i] -= w * temp_rhs[i - 1];
1254 }
1255
1256 // Backward sweep
1257 solution[n - 1] = temp_rhs[n - 1] / temp_diag[n - 1];
1258 for i in (0..n - 1).rev() {
1259 solution[i] = (temp_rhs[i] - upper[i] * solution[i + 1]) / temp_diag[i];
1260 }
1261
1262 // Apply Dirichlet boundary conditions directly
1263 for bc in &self.boundary_conditions {
1264 if bc.bc_type == BoundaryConditionType::Dirichlet {
1265 match bc.location {
1266 BoundaryLocation::Lower => solution[0] = bc.value,
1267 BoundaryLocation::Upper => solution[n - 1] = bc.value,
1268 }
1269 }
1270 }
1271
1272 Ok(solution)
1273 } else {
1274 // General case: Gaussian elimination with partial pivoting
1275 // For a real implementation, use a specialized linear algebra library
1276
1277 // Create copies of A and b
1278 let mut a_copy = a.clone();
1279 let mut b_copy = b.to_owned();
1280
1281 // Forward elimination
1282 for i in 0..n {
1283 // Find pivot
1284 let mut max_val = a_copy[[i, i]].abs();
1285 let mut max_row = i;
1286
1287 for k in i + 1..n {
1288 if a_copy[[k, i]].abs() > max_val {
1289 max_val = a_copy[[k, i]].abs();
1290 max_row = k;
1291 }
1292 }
1293
1294 // Check if matrix is singular
1295 if max_val < 1e-10 {
1296 return Err(PDEError::Other(
1297 "Matrix is singular or nearly singular".to_string(),
1298 ));
1299 }
1300
1301 // Swap rows if necessary
1302 if max_row != i {
1303 for j in i..n {
1304 let temp = a_copy[[i, j]];
1305 a_copy[[i, j]] = a_copy[[max_row, j]];
1306 a_copy[[max_row, j]] = temp;
1307 }
1308
1309 let temp = b_copy[i];
1310 b_copy[i] = b_copy[max_row];
1311 b_copy[max_row] = temp;
1312 }
1313
1314 // Eliminate below
1315 for k in i + 1..n {
1316 let factor = a_copy[[k, i]] / a_copy[[i, i]];
1317
1318 for j in i..n {
1319 a_copy[[k, j]] -= factor * a_copy[[i, j]];
1320 }
1321
1322 b_copy[k] -= factor * b_copy[i];
1323 }
1324 }
1325
1326 // Back substitution
1327 let mut x = Array1::zeros(n);
1328 for i in (0..n).rev() {
1329 let mut sum = 0.0;
1330 for j in i + 1..n {
1331 sum += a_copy[[i, j]] * x[j];
1332 }
1333
1334 x[i] = (b_copy[i] - sum) / a_copy[[i, i]];
1335 }
1336
1337 // Apply Dirichlet boundary conditions directly
1338 for bc in &self.boundary_conditions {
1339 if bc.bc_type == BoundaryConditionType::Dirichlet {
1340 match bc.location {
1341 BoundaryLocation::Lower => x[0] = bc.value,
1342 BoundaryLocation::Upper => x[n - 1] = bc.value,
1343 }
1344 }
1345 }
1346
1347 Ok(x)
1348 }
1349 }
1350}
1351
1352/// Helper function to apply Dirichlet boundary conditions to initial condition
1353#[allow(dead_code)]
1354fn apply_dirichlet_conditions_to_initial_1d(
1355 u0: &mut Array1<f64>,
1356 boundary_conditions: &[BoundaryCondition<f64>],
1357) {
1358 let nx = u0.len();
1359
1360 for bc in boundary_conditions {
1361 if bc.bc_type == BoundaryConditionType::Dirichlet {
1362 match bc.location {
1363 BoundaryLocation::Lower => {
1364 // Lower boundary (x = x[0])
1365 u0[0] = bc.value;
1366 }
1367 BoundaryLocation::Upper => {
1368 // Upper boundary (x = x[nx-1])
1369 u0[nx - 1] = bc.value;
1370 }
1371 }
1372 }
1373 }
1374}
1375
1376/// Convert an ImplicitResult to a PDESolution
1377impl From<ImplicitResult> for PDESolution<f64> {
1378 fn from(result: ImplicitResult) -> Self {
1379 let mut grids = Vec::new();
1380
1381 // Add time grid
1382 grids.push(result.t.clone());
1383
1384 // Extract spatial grid from solution shape
1385 let nx = result.u[0].shape()[0];
1386
1387 // Create spatial grid (we don't have the actual grid values, so use linspace)
1388 let x_grid = Array1::linspace(0.0, 1.0, nx);
1389 grids.push(x_grid);
1390
1391 // Create solver info
1392 let info = PDESolverInfo {
1393 num_iterations: result.num_linear_solves,
1394 computation_time: result.computation_time,
1395 residual_norm: None,
1396 convergence_history: None,
1397 method: "Implicit Method".to_string(),
1398 };
1399
1400 PDESolution {
1401 grids,
1402 values: result.u,
1403 error_estimate: None,
1404 info,
1405 }
1406 }
1407}