scirs2_integrate/pde/elliptic/
mod.rs

1//! Elliptic PDE solvers
2//!
3//! This module provides solvers for elliptic partial differential equations,
4//! such as Poisson's equation and Laplace's equation. These are steady-state
5//! problems that don't involve time derivatives.
6//!
7//! Supported equation types:
8//! - Poisson's equation: ∇²u = f(x, y)
9//! - Laplace's equation: ∇²u = 0
10
11use scirs2_core::ndarray::{Array1, Array2};
12use std::time::Instant;
13
14use crate::pde::finite_difference::FiniteDifferenceScheme;
15use crate::pde::{
16    BoundaryCondition, BoundaryConditionType, BoundaryLocation, Domain, PDEError, PDEResult,
17    PDESolution, PDESolverInfo,
18};
19
20/// Result of elliptic PDE solution
21pub struct EllipticResult {
22    /// Solution values
23    pub u: Array2<f64>,
24
25    /// Residual norm
26    pub residual_norm: f64,
27
28    /// Number of iterations performed
29    pub num_iterations: usize,
30
31    /// Computation time
32    pub computation_time: f64,
33
34    /// Convergence history
35    pub convergence_history: Option<Vec<f64>>,
36}
37
38/// Options for elliptic PDE solvers
39#[derive(Debug, Clone)]
40pub struct EllipticOptions {
41    /// Maximum number of iterations
42    pub max_iterations: usize,
43
44    /// Tolerance for convergence
45    pub tolerance: f64,
46
47    /// Whether to save convergence history
48    pub save_convergence_history: bool,
49
50    /// Relaxation parameter for iterative methods (0 < omega < 2)
51    pub omega: f64,
52
53    /// Print detailed progress information
54    pub verbose: bool,
55
56    /// Finite difference scheme for discretization
57    pub fd_scheme: FiniteDifferenceScheme,
58}
59
60impl Default for EllipticOptions {
61    fn default() -> Self {
62        EllipticOptions {
63            max_iterations: 1000,
64            tolerance: 1e-6,
65            save_convergence_history: false,
66            omega: 1.0,
67            verbose: false,
68            fd_scheme: FiniteDifferenceScheme::CentralDifference,
69        }
70    }
71}
72
73/// Poisson's equation solver for 2D problems
74///
75/// Solves ∇²u = f(x, y), or in expanded form:
76/// ∂²u/∂x² + ∂²u/∂y² = f(x, y)
77pub struct PoissonSolver2D {
78    /// Spatial domain
79    domain: Domain,
80
81    /// Source term function f(x, y)
82    source_term: Box<dyn Fn(f64, f64) -> f64 + Send + Sync>,
83
84    /// Boundary conditions
85    boundary_conditions: Vec<BoundaryCondition<f64>>,
86
87    /// Solver options
88    options: EllipticOptions,
89}
90
91impl PoissonSolver2D {
92    /// Create a new Poisson equation solver for 2D problems
93    pub fn new(
94        domain: Domain,
95        source_term: impl Fn(f64, f64) -> f64 + Send + Sync + 'static,
96        boundary_conditions: Vec<BoundaryCondition<f64>>,
97        options: Option<EllipticOptions>,
98    ) -> PDEResult<Self> {
99        // Validate the domain
100        if domain.dimensions() != 2 {
101            return Err(PDEError::DomainError(
102                "Domain must be 2-dimensional for 2D Poisson solver".to_string(),
103            ));
104        }
105
106        // Validate boundary _conditions
107        if boundary_conditions.len() != 4 {
108            return Err(PDEError::BoundaryConditions(
109                "2D Poisson equation requires exactly 4 boundary _conditions (one for each edge)"
110                    .to_string(),
111            ));
112        }
113
114        // Ensure we have boundary _conditions for all dimensions/edges
115        let has_x_lower = boundary_conditions
116            .iter()
117            .any(|bc| bc.location == BoundaryLocation::Lower && bc.dimension == 0);
118        let has_x_upper = boundary_conditions
119            .iter()
120            .any(|bc| bc.location == BoundaryLocation::Upper && bc.dimension == 0);
121        let has_y_lower = boundary_conditions
122            .iter()
123            .any(|bc| bc.location == BoundaryLocation::Lower && bc.dimension == 1);
124        let has_y_upper = boundary_conditions
125            .iter()
126            .any(|bc| bc.location == BoundaryLocation::Upper && bc.dimension == 1);
127
128        if !has_x_lower || !has_x_upper || !has_y_lower || !has_y_upper {
129            return Err(PDEError::BoundaryConditions(
130                "2D Poisson equation requires boundary _conditions for all edges of the domain"
131                    .to_string(),
132            ));
133        }
134
135        Ok(PoissonSolver2D {
136            domain,
137            source_term: Box::new(source_term),
138            boundary_conditions,
139            options: options.unwrap_or_default(),
140        })
141    }
142
143    /// Solve the Poisson equation using Successive Over-Relaxation (SOR)
144    pub fn solve_sor(&self) -> PDEResult<EllipticResult> {
145        let start_time = Instant::now();
146
147        // Generate spatial grids
148        let x_grid = self.domain.grid(0)?;
149        let y_grid = self.domain.grid(1)?;
150        let nx = x_grid.len();
151        let ny = y_grid.len();
152        let dx = self.domain.grid_spacing(0)?;
153        let dy = self.domain.grid_spacing(1)?;
154
155        // Initialize solution with zeros
156        let mut u = Array2::zeros((ny, nx));
157
158        // Set Dirichlet boundary conditions in the initial guess
159        self.apply_dirichlet_boundary_conditions(&mut u, &x_grid, &y_grid);
160
161        // Prepare for iteration
162        let mut residual_norm = f64::INFINITY;
163        let mut iter = 0;
164        let mut convergence_history = if self.options.save_convergence_history {
165            Some(Vec::new())
166        } else {
167            None
168        };
169
170        // Main iteration loop (Successive Over-Relaxation)
171        while residual_norm > self.options.tolerance && iter < self.options.max_iterations {
172            // Store previous solution for computing residual
173            let u_prev = u.clone();
174
175            // Update interior points
176            for j in 1..ny - 1 {
177                for i in 1..nx - 1 {
178                    let x = x_grid[i];
179                    let y = y_grid[j];
180
181                    // Compute the right-hand side (source term)
182                    let f_xy = (self.source_term)(x, y);
183
184                    // Compute next iterate using 5-point stencil
185                    let u_new = ((u[[j, i + 1]] + u[[j, i - 1]]) / (dx * dx)
186                        + (u[[j + 1, i]] + u[[j - 1, i]]) / (dy * dy)
187                        - f_xy)
188                        / (2.0 / (dx * dx) + 2.0 / (dy * dy));
189
190                    // Apply relaxation
191                    u[[j, i]] = (1.0 - self.options.omega) * u[[j, i]] + self.options.omega * u_new;
192                }
193            }
194
195            // Apply boundary conditions after each iteration
196            self.apply_boundary_conditions(&mut u, &x_grid, &y_grid, dx, dy);
197
198            // Compute residual norm
199            let mut residual_sum = 0.0;
200            for j in 1..ny - 1 {
201                for i in 1..nx - 1 {
202                    let diff = u[[j, i]] - u_prev[[j, i]];
203                    residual_sum += diff * diff;
204                }
205            }
206            residual_norm = residual_sum.sqrt();
207
208            // Save convergence history if requested
209            if let Some(ref mut history) = convergence_history {
210                history.push(residual_norm);
211            }
212
213            // Print progress if verbose
214            if self.options.verbose && (iter % 100 == 0 || iter == self.options.max_iterations - 1)
215            {
216                println!("Iteration {iter}: residual = {residual_norm:.6e}");
217            }
218
219            iter += 1;
220        }
221
222        let computation_time = start_time.elapsed().as_secs_f64();
223
224        if iter == self.options.max_iterations && residual_norm > self.options.tolerance {
225            println!("Warning: Maximum iterations reached without convergence");
226            println!(
227                "Final residual: {:.6e}, tolerance: {:.6e}",
228                residual_norm, self.options.tolerance
229            );
230        }
231
232        Ok(EllipticResult {
233            u,
234            residual_norm,
235            num_iterations: iter,
236            computation_time,
237            convergence_history,
238        })
239    }
240
241    /// Solve the Poisson equation using direct sparse solver
242    ///
243    /// This method sets up the linear system Au = b and solves it directly.
244    /// It creates a sparse matrix for the Laplacian operator and applies
245    /// boundary conditions to the system.
246    pub fn solve_direct(&self) -> PDEResult<EllipticResult> {
247        let start_time = Instant::now();
248
249        // Generate spatial grids
250        let x_grid = self.domain.grid(0)?;
251        let y_grid = self.domain.grid(1)?;
252        let nx = x_grid.len();
253        let ny = y_grid.len();
254        let dx = self.domain.grid_spacing(0)?;
255        let dy = self.domain.grid_spacing(1)?;
256
257        // Total number of grid points
258        let n = nx * ny;
259
260        // Create matrix A (discretized Laplacian) and right-hand side vector b
261        let mut a = Array2::zeros((n, n));
262        let mut b = Array1::zeros(n);
263
264        // Fill the matrix and vector for interior points using 5-point stencil
265        for j in 1..ny - 1 {
266            for i in 1..nx - 1 {
267                let index = j * nx + i;
268                let x = x_grid[i];
269                let y = y_grid[j];
270
271                // Diagonal term
272                a[[index, index]] = -2.0 / (dx * dx) - 2.0 / (dy * dy);
273
274                // Off-diagonal terms
275                a[[index, index - 1]] = 1.0 / (dx * dx); // left
276                a[[index, index + 1]] = 1.0 / (dx * dx); // right
277                a[[index, index - nx]] = 1.0 / (dy * dy); // bottom
278                a[[index, index + nx]] = 1.0 / (dy * dy); // top
279
280                // Right-hand side
281                b[index] = -(self.source_term)(x, y);
282            }
283        }
284
285        // Apply boundary conditions to the system
286        self.apply_boundary_conditions_to_system(&mut a, &mut b, &x_grid, &y_grid, nx, ny, dx, dy);
287
288        // Solve the linear system using Gaussian elimination
289        // For a real implementation, use a sparse solver library
290        let u_flat = PoissonSolver2D::solve_linear_system(&a, &b)?;
291
292        // Reshape the solution to 2D
293        let mut u = Array2::zeros((ny, nx));
294        for j in 0..ny {
295            for i in 0..nx {
296                u[[j, i]] = u_flat[j * nx + i];
297            }
298        }
299
300        // Compute residual
301        let mut residual_norm = 0.0;
302        for j in 1..ny - 1 {
303            for i in 1..nx - 1 {
304                let x = x_grid[i];
305                let y = y_grid[j];
306
307                let laplacian = (u[[j, i + 1]] - 2.0 * u[[j, i]] + u[[j, i - 1]]) / (dx * dx)
308                    + (u[[j + 1, i]] - 2.0 * u[[j, i]] + u[[j - 1, i]]) / (dy * dy);
309
310                let residual = laplacian - (self.source_term)(x, y);
311                residual_norm += residual * residual;
312            }
313        }
314        residual_norm = residual_norm.sqrt();
315
316        let computation_time = start_time.elapsed().as_secs_f64();
317
318        Ok(EllipticResult {
319            u,
320            residual_norm,
321            num_iterations: 1, // Direct method requires only one "iteration"
322            computation_time,
323            convergence_history: None,
324        })
325    }
326
327    // Helper method to solve linear system Ax = b using Gaussian elimination
328    // In a real implementation, this would use a specialized sparse solver
329    fn solve_linear_system(a: &Array2<f64>, b: &Array1<f64>) -> PDEResult<Array1<f64>> {
330        let n = b.len();
331
332        // Simple Gaussian elimination for demonstration purposes
333        // For real applications, use a linear algebra library
334
335        // Create copies of A and b
336        let mut a_copy = a.clone();
337        let mut b_copy = b.clone();
338
339        // Forward elimination
340        for i in 0..n {
341            // Find pivot
342            let mut max_val = a_copy[[i, i]].abs();
343            let mut max_row = i;
344
345            for k in i + 1..n {
346                if a_copy[[k, i]].abs() > max_val {
347                    max_val = a_copy[[k, i]].abs();
348                    max_row = k;
349                }
350            }
351
352            // Check if matrix is singular
353            if max_val < 1e-10 {
354                return Err(PDEError::Other(
355                    "Matrix is singular or nearly singular".to_string(),
356                ));
357            }
358
359            // Swap rows if necessary
360            if max_row != i {
361                for j in i..n {
362                    let temp = a_copy[[i, j]];
363                    a_copy[[i, j]] = a_copy[[max_row, j]];
364                    a_copy[[max_row, j]] = temp;
365                }
366
367                let temp = b_copy[i];
368                b_copy[i] = b_copy[max_row];
369                b_copy[max_row] = temp;
370            }
371
372            // Eliminate below
373            for k in i + 1..n {
374                let factor = a_copy[[k, i]] / a_copy[[i, i]];
375
376                for j in i..n {
377                    a_copy[[k, j]] -= factor * a_copy[[i, j]];
378                }
379
380                b_copy[k] -= factor * b_copy[i];
381            }
382        }
383
384        // Back substitution
385        let mut x = Array1::zeros(n);
386        for i in (0..n).rev() {
387            let mut sum = 0.0;
388            for j in i + 1..n {
389                sum += a_copy[[i, j]] * x[j];
390            }
391
392            x[i] = (b_copy[i] - sum) / a_copy[[i, i]];
393        }
394
395        Ok(x)
396    }
397
398    // Helper method to apply boundary conditions to the solution vector
399    fn apply_boundary_conditions(
400        &self,
401        u: &mut Array2<f64>,
402        x_grid: &Array1<f64>,
403        y_grid: &Array1<f64>,
404        dx: f64,
405        dy: f64,
406    ) {
407        let nx = x_grid.len();
408        let ny = y_grid.len();
409
410        // Apply Dirichlet boundary conditions first
411        self.apply_dirichlet_boundary_conditions(u, x_grid, y_grid);
412
413        // Apply Neumann and Robin boundary conditions
414        for bc in &self.boundary_conditions {
415            match bc.bc_type {
416                BoundaryConditionType::Dirichlet => {
417                    // Already handled above
418                }
419                BoundaryConditionType::Neumann => {
420                    match (bc.dimension, bc.location) {
421                        (0, BoundaryLocation::Lower) => {
422                            // Left boundary (x = x[0]): ∂u/∂x = bc.value
423                            // Use one-sided difference: (u[i+1] - u[i])/dx = bc.value
424                            for j in 0..ny {
425                                u[[j, 0]] = u[[j, 1]] - dx * bc.value;
426                            }
427                        }
428                        (0, BoundaryLocation::Upper) => {
429                            // Right boundary (x = x[nx-1]): ∂u/∂x = bc.value
430                            // Use one-sided difference: (u[i] - u[i-1])/dx = bc.value
431                            for j in 0..ny {
432                                u[[j, nx - 1]] = u[[j, nx - 2]] + dx * bc.value;
433                            }
434                        }
435                        (1, BoundaryLocation::Lower) => {
436                            // Bottom boundary (y = y[0]): ∂u/∂y = bc.value
437                            // Use one-sided difference: (u[j+1] - u[j])/dy = bc.value
438                            for i in 0..nx {
439                                u[[0, i]] = u[[1, i]] - dy * bc.value;
440                            }
441                        }
442                        (1, BoundaryLocation::Upper) => {
443                            // Top boundary (y = y[ny-1]): ∂u/∂y = bc.value
444                            // Use one-sided difference: (u[j] - u[j-1])/dy = bc.value
445                            for i in 0..nx {
446                                u[[ny - 1, i]] = u[[ny - 2, i]] + dy * bc.value;
447                            }
448                        }
449                        _ => {
450                            // Invalid dimension (should be caught during validation)
451                        }
452                    }
453                }
454                BoundaryConditionType::Robin => {
455                    // Robin boundary condition: a*u + b*∂u/∂n = c
456                    if let Some([a, b, c]) = bc.coefficients {
457                        match (bc.dimension, bc.location) {
458                            (0, BoundaryLocation::Lower) => {
459                                // Left boundary (x = x[0])
460                                for j in 0..ny {
461                                    // a*u + b*(u[i+1] - u[i])/dx = c
462                                    // Solve for u[i]
463                                    u[[j, 0]] = (b * u[[j, 1]] / dx + c) / (a + b / dx);
464                                }
465                            }
466                            (0, BoundaryLocation::Upper) => {
467                                // Right boundary (x = x[nx-1])
468                                for j in 0..ny {
469                                    // a*u + b*(u[i] - u[i-1])/dx = c
470                                    // Solve for u[i]
471                                    u[[j, nx - 1]] = (b * u[[j, nx - 2]] / dx + c) / (a - b / dx);
472                                }
473                            }
474                            (1, BoundaryLocation::Lower) => {
475                                // Bottom boundary (y = y[0])
476                                for i in 0..nx {
477                                    // a*u + b*(u[j+1] - u[j])/dy = c
478                                    // Solve for u[j]
479                                    u[[0, i]] = (b * u[[1, i]] / dy + c) / (a + b / dy);
480                                }
481                            }
482                            (1, BoundaryLocation::Upper) => {
483                                // Top boundary (y = y[ny-1])
484                                for i in 0..nx {
485                                    // a*u + b*(u[j] - u[j-1])/dy = c
486                                    // Solve for u[j]
487                                    u[[ny - 1, i]] = (b * u[[ny - 2, i]] / dy + c) / (a - b / dy);
488                                }
489                            }
490                            _ => {
491                                // Invalid dimension (should be caught during validation)
492                            }
493                        }
494                    }
495                }
496                BoundaryConditionType::Periodic => {
497                    // Periodic boundary conditions
498                    match bc.dimension {
499                        0 => {
500                            // Periodic in x-direction: u(0,y) = u(L,y)
501                            for j in 0..ny {
502                                let avg = 0.5 * (u[[j, 0]] + u[[j, nx - 1]]);
503                                u[[j, 0]] = avg;
504                                u[[j, nx - 1]] = avg;
505                            }
506                        }
507                        1 => {
508                            // Periodic in y-direction: u(x,0) = u(x,H)
509                            for i in 0..nx {
510                                let avg = 0.5 * (u[[0, i]] + u[[ny - 1, i]]);
511                                u[[0, i]] = avg;
512                                u[[ny - 1, i]] = avg;
513                            }
514                        }
515                        _ => {
516                            // Invalid dimension (should be caught during validation)
517                        }
518                    }
519                }
520            }
521        }
522    }
523
524    // Helper method to apply only Dirichlet boundary conditions to the solution vector
525    fn apply_dirichlet_boundary_conditions(
526        &self,
527        u: &mut Array2<f64>,
528        x_grid: &Array1<f64>,
529        y_grid: &Array1<f64>,
530    ) {
531        let nx = x_grid.len();
532        let ny = y_grid.len();
533
534        for bc in &self.boundary_conditions {
535            if bc.bc_type == BoundaryConditionType::Dirichlet {
536                match (bc.dimension, bc.location) {
537                    (0, BoundaryLocation::Lower) => {
538                        // Left boundary (x = x[0])
539                        for j in 0..ny {
540                            u[[j, 0]] = bc.value;
541                        }
542                    }
543                    (0, BoundaryLocation::Upper) => {
544                        // Right boundary (x = x[nx-1])
545                        for j in 0..ny {
546                            u[[j, nx - 1]] = bc.value;
547                        }
548                    }
549                    (1, BoundaryLocation::Lower) => {
550                        // Bottom boundary (y = y[0])
551                        for i in 0..nx {
552                            u[[0, i]] = bc.value;
553                        }
554                    }
555                    (1, BoundaryLocation::Upper) => {
556                        // Top boundary (y = y[ny-1])
557                        for i in 0..nx {
558                            u[[ny - 1, i]] = bc.value;
559                        }
560                    }
561                    _ => {
562                        // Invalid dimension (should be caught during validation)
563                    }
564                }
565            }
566        }
567    }
568
569    // Helper method to apply boundary conditions to the linear system
570    #[allow(clippy::too_many_arguments)]
571    fn apply_boundary_conditions_to_system(
572        &self,
573        a: &mut Array2<f64>,
574        b: &mut Array1<f64>,
575        _x_grid: &Array1<f64>,
576        _y_grid: &Array1<f64>,
577        nx: usize,
578        ny: usize,
579        dx: f64,
580        dy: f64,
581    ) {
582        for bc in &self.boundary_conditions {
583            match bc.bc_type {
584                BoundaryConditionType::Dirichlet => {
585                    match (bc.dimension, bc.location) {
586                        (0, BoundaryLocation::Lower) => {
587                            // Left boundary (x = x[0])
588                            for j in 0..ny {
589                                let index = j * nx;
590
591                                // Set row to identity
592                                for k in 0..a.shape()[1] {
593                                    a[[index, k]] = 0.0;
594                                }
595                                a[[index, index]] = 1.0;
596
597                                // Set right-hand side to boundary value
598                                b[index] = bc.value;
599                            }
600                        }
601                        (0, BoundaryLocation::Upper) => {
602                            // Right boundary (x = x[nx-1])
603                            for j in 0..ny {
604                                let index = j * nx + (nx - 1);
605
606                                // Set row to identity
607                                for k in 0..a.shape()[1] {
608                                    a[[index, k]] = 0.0;
609                                }
610                                a[[index, index]] = 1.0;
611
612                                // Set right-hand side to boundary value
613                                b[index] = bc.value;
614                            }
615                        }
616                        (1, BoundaryLocation::Lower) => {
617                            // Bottom boundary (y = y[0])
618                            for i in 0..nx {
619                                let index = i;
620
621                                // Set row to identity
622                                for k in 0..a.shape()[1] {
623                                    a[[index, k]] = 0.0;
624                                }
625                                a[[index, index]] = 1.0;
626
627                                // Set right-hand side to boundary value
628                                b[index] = bc.value;
629                            }
630                        }
631                        (1, BoundaryLocation::Upper) => {
632                            // Top boundary (y = y[ny-1])
633                            for i in 0..nx {
634                                let index = (ny - 1) * nx + i;
635
636                                // Set row to identity
637                                for k in 0..a.shape()[1] {
638                                    a[[index, k]] = 0.0;
639                                }
640                                a[[index, index]] = 1.0;
641
642                                // Set right-hand side to boundary value
643                                b[index] = bc.value;
644                            }
645                        }
646                        _ => {
647                            // Invalid dimension (should be caught during validation)
648                        }
649                    }
650                }
651                BoundaryConditionType::Neumann => {
652                    match (bc.dimension, bc.location) {
653                        (0, BoundaryLocation::Lower) => {
654                            // Left boundary (x = x[0]): ∂u/∂x = bc.value
655                            for j in 0..ny {
656                                let index = j * nx;
657
658                                // Modify matrix row to represent one-sided difference
659                                // (u[i+1] - u[i])/dx = bc.value
660                                for k in 0..a.shape()[1] {
661                                    a[[index, k]] = 0.0;
662                                }
663                                a[[index, index]] = -1.0 / dx;
664                                a[[index, index + 1]] = 1.0 / dx;
665
666                                // Set right-hand side to boundary value
667                                b[index] = bc.value;
668                            }
669                        }
670                        (0, BoundaryLocation::Upper) => {
671                            // Right boundary (x = x[nx-1]): ∂u/∂x = bc.value
672                            for j in 0..ny {
673                                let index = j * nx + (nx - 1);
674
675                                // Modify matrix row to represent one-sided difference
676                                // (u[i] - u[i-1])/dx = bc.value
677                                for k in 0..a.shape()[1] {
678                                    a[[index, k]] = 0.0;
679                                }
680                                a[[index, index - 1]] = -1.0 / dx;
681                                a[[index, index]] = 1.0 / dx;
682
683                                // Set right-hand side to boundary value
684                                b[index] = bc.value;
685                            }
686                        }
687                        (1, BoundaryLocation::Lower) => {
688                            // Bottom boundary (y = y[0]): ∂u/∂y = bc.value
689                            for i in 0..nx {
690                                let index = i;
691
692                                // Modify matrix row to represent one-sided difference
693                                // (u[j+1] - u[j])/dy = bc.value
694                                for k in 0..a.shape()[1] {
695                                    a[[index, k]] = 0.0;
696                                }
697                                a[[index, index]] = -1.0 / dy;
698                                a[[index, index + nx]] = 1.0 / dy;
699
700                                // Set right-hand side to boundary value
701                                b[index] = bc.value;
702                            }
703                        }
704                        (1, BoundaryLocation::Upper) => {
705                            // Top boundary (y = y[ny-1]): ∂u/∂y = bc.value
706                            for i in 0..nx {
707                                let index = (ny - 1) * nx + i;
708
709                                // Modify matrix row to represent one-sided difference
710                                // (u[j] - u[j-1])/dy = bc.value
711                                for k in 0..a.shape()[1] {
712                                    a[[index, k]] = 0.0;
713                                }
714                                a[[index, index - nx]] = -1.0 / dy;
715                                a[[index, index]] = 1.0 / dy;
716
717                                // Set right-hand side to boundary value
718                                b[index] = bc.value;
719                            }
720                        }
721                        _ => {
722                            // Invalid dimension (should be caught during validation)
723                        }
724                    }
725                }
726                BoundaryConditionType::Robin => {
727                    // Robin boundary condition: a*u + b*∂u/∂n = c
728                    if let Some([a_coef, b_coef, c_coef]) = bc.coefficients {
729                        match (bc.dimension, bc.location) {
730                            (0, BoundaryLocation::Lower) => {
731                                // Left boundary (x = x[0])
732                                for j in 0..ny {
733                                    let index = j * nx;
734
735                                    // Modify matrix row to represent robin condition
736                                    // a*u + b*(u[i+1] - u[i])/dx = c
737                                    for k in 0..a.shape()[1] {
738                                        a[[index, k]] = 0.0;
739                                    }
740                                    a[[index, index]] = a_coef - b_coef / dx;
741                                    a[[index, index + 1]] = b_coef / dx;
742
743                                    // Set right-hand side to boundary value
744                                    b[index] = c_coef;
745                                }
746                            }
747                            (0, BoundaryLocation::Upper) => {
748                                // Right boundary (x = x[nx-1])
749                                for j in 0..ny {
750                                    let index = j * nx + (nx - 1);
751
752                                    // Modify matrix row to represent robin condition
753                                    // a*u + b*(u[i] - u[i-1])/dx = c
754                                    for k in 0..a.shape()[1] {
755                                        a[[index, k]] = 0.0;
756                                    }
757                                    a[[index, index - 1]] = -b_coef / dx;
758                                    a[[index, index]] = a_coef + b_coef / dx;
759
760                                    // Set right-hand side to boundary value
761                                    b[index] = c_coef;
762                                }
763                            }
764                            (1, BoundaryLocation::Lower) => {
765                                // Bottom boundary (y = y[0])
766                                for i in 0..nx {
767                                    let index = i;
768
769                                    // Modify matrix row to represent robin condition
770                                    // a*u + b*(u[j+1] - u[j])/dy = c
771                                    for k in 0..a.shape()[1] {
772                                        a[[index, k]] = 0.0;
773                                    }
774                                    a[[index, index]] = a_coef - b_coef / dy;
775                                    a[[index, index + nx]] = b_coef / dy;
776
777                                    // Set right-hand side to boundary value
778                                    b[index] = c_coef;
779                                }
780                            }
781                            (1, BoundaryLocation::Upper) => {
782                                // Top boundary (y = y[ny-1])
783                                for i in 0..nx {
784                                    let index = (ny - 1) * nx + i;
785
786                                    // Modify matrix row to represent robin condition
787                                    // a*u + b*(u[j] - u[j-1])/dy = c
788                                    for k in 0..a.shape()[1] {
789                                        a[[index, k]] = 0.0;
790                                    }
791                                    a[[index, index - nx]] = -b_coef / dy;
792                                    a[[index, index]] = a_coef + b_coef / dy;
793
794                                    // Set right-hand side to boundary value
795                                    b[index] = c_coef;
796                                }
797                            }
798                            _ => {
799                                // Invalid dimension (should be caught during validation)
800                            }
801                        }
802                    }
803                }
804                BoundaryConditionType::Periodic => {
805                    // Periodic boundary conditions are more complex for the matrix system
806                    // For simplicity, we'll just handle them in a basic way
807                    match bc.dimension {
808                        0 => {
809                            // Periodic in x-direction: u(0,y) = u(L,y)
810                            for j in 0..ny {
811                                let left_index = j * nx;
812                                let right_index = j * nx + (nx - 1);
813
814                                // Set these rows to represent equality
815                                for k in 0..a.shape()[1] {
816                                    a[[left_index, k]] = 0.0;
817                                    a[[right_index, k]] = 0.0;
818                                }
819
820                                a[[left_index, left_index]] = 1.0;
821                                a[[left_index, right_index]] = -1.0;
822
823                                a[[right_index, left_index]] = -1.0;
824                                a[[right_index, right_index]] = 1.0;
825
826                                b[left_index] = 0.0;
827                                b[right_index] = 0.0;
828                            }
829                        }
830                        1 => {
831                            // Periodic in y-direction: u(x,0) = u(x,H)
832                            for i in 0..nx {
833                                let bottom_index = i;
834                                let top_index = (ny - 1) * nx + i;
835
836                                // Set these rows to represent equality
837                                for k in 0..a.shape()[1] {
838                                    a[[bottom_index, k]] = 0.0;
839                                    a[[top_index, k]] = 0.0;
840                                }
841
842                                a[[bottom_index, bottom_index]] = 1.0;
843                                a[[bottom_index, top_index]] = -1.0;
844
845                                a[[top_index, bottom_index]] = -1.0;
846                                a[[top_index, top_index]] = 1.0;
847
848                                b[bottom_index] = 0.0;
849                                b[top_index] = 0.0;
850                            }
851                        }
852                        _ => {
853                            // Invalid dimension (should be caught during validation)
854                        }
855                    }
856                }
857            }
858        }
859    }
860}
861
862/// Laplace's equation solver for 2D problems
863///
864/// Solves ∇²u = 0, or in expanded form:
865/// ∂²u/∂x² + ∂²u/∂y² = 0
866///
867/// This is a specialized version of the Poisson solver with the right-hand side set to zero.
868pub struct LaplaceSolver2D {
869    /// Underlying Poisson solver
870    poisson_solver: PoissonSolver2D,
871}
872
873impl LaplaceSolver2D {
874    /// Create a new Laplace equation solver for 2D problems
875    pub fn new(
876        domain: Domain,
877        boundary_conditions: Vec<BoundaryCondition<f64>>,
878        options: Option<EllipticOptions>,
879    ) -> PDEResult<Self> {
880        // Create a Poisson solver with zero source term
881        let poisson_solver =
882            PoissonSolver2D::new(domain, |_x, _y| 0.0, boundary_conditions, options)?;
883
884        Ok(LaplaceSolver2D { poisson_solver })
885    }
886
887    /// Solve Laplace's equation using Successive Over-Relaxation (SOR)
888    pub fn solve_sor(&self) -> PDEResult<EllipticResult> {
889        self.poisson_solver.solve_sor()
890    }
891
892    /// Solve Laplace's equation using a direct sparse solver
893    pub fn solve_direct(&self) -> PDEResult<EllipticResult> {
894        self.poisson_solver.solve_direct()
895    }
896}
897
898/// Convert EllipticResult to PDESolution
899impl From<EllipticResult> for PDESolution<f64> {
900    fn from(result: EllipticResult) -> Self {
901        let mut grids = Vec::new();
902
903        // Extract grid dimensions from solution (assuming they're regular)
904        let ny = result.u.shape()[0];
905        let nx = result.u.shape()[1];
906
907        // Create grids (since we don't have the actual grid values, use linspace)
908        let x_grid = Array1::linspace(0.0, 1.0, nx);
909        let y_grid = Array1::linspace(0.0, 1.0, ny);
910
911        grids.push(x_grid);
912        grids.push(y_grid);
913
914        // Create solution values
915        let values = vec![result.u];
916
917        // Create solver info
918        let info = PDESolverInfo {
919            num_iterations: result.num_iterations,
920            computation_time: result.computation_time,
921            residual_norm: Some(result.residual_norm),
922            convergence_history: result.convergence_history,
923            method: "Elliptic PDE Solver".to_string(),
924        };
925
926        PDESolution {
927            grids,
928            values,
929            error_estimate: None,
930            info,
931        }
932    }
933}