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}