Skip to main content

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}