1use crate::boundary::BoundaryCondition;
11use crate::boundary2d::{BoundaryConditions2D, BoundaryConditions3D};
12use crate::grid::{Grid2D, Grid3D};
13use faer::{ComplexField, Conjugate, SimpleEntity};
14use numra_core::Scalar;
15use numra_linalg::{LinalgError, SparseMatrix};
16
17pub trait SparseScalar: Scalar + SimpleEntity + Conjugate<Canonical = Self> + ComplexField {}
20impl<S: Scalar + SimpleEntity + Conjugate<Canonical = S> + ComplexField> SparseScalar for S {}
21
22#[derive(Clone, Debug)]
25pub struct Operator2DCoefficients<S: Scalar> {
26 pub a: S,
28 pub b: S,
30 pub c: S,
32 pub d: S,
34 pub e: S,
36}
37
38impl<S: Scalar> Operator2DCoefficients<S> {
39 pub fn laplacian() -> Self {
41 Self {
42 a: S::ONE,
43 b: S::ONE,
44 c: S::ZERO,
45 d: S::ZERO,
46 e: S::ZERO,
47 }
48 }
49
50 pub fn scaled_laplacian(alpha: S) -> Self {
52 Self {
53 a: alpha,
54 b: alpha,
55 c: S::ZERO,
56 d: S::ZERO,
57 e: S::ZERO,
58 }
59 }
60
61 pub fn advection_diffusion(diffusion: S, vx: S, vy: S) -> Self {
63 Self {
64 a: diffusion,
65 b: diffusion,
66 c: -vx,
67 d: -vy,
68 e: S::ZERO,
69 }
70 }
71}
72
73#[inline]
78fn interior_index_2d(i: usize, j: usize, nx_int: usize) -> usize {
79 j * nx_int + i
80}
81
82pub fn assemble_laplacian_2d<S: SparseScalar>(
95 grid: &Grid2D<S>,
96 bc: &BoundaryConditions2D<S>,
97) -> Result<(SparseMatrix<S>, Vec<S>), LinalgError> {
98 let coeffs = Operator2DCoefficients::laplacian();
99 assemble_operator_2d(grid, &coeffs, bc)
100}
101
102pub fn assemble_operator_2d<S: SparseScalar>(
110 grid: &Grid2D<S>,
111 coeffs: &Operator2DCoefficients<S>,
112 bc: &BoundaryConditions2D<S>,
113) -> Result<(SparseMatrix<S>, Vec<S>), LinalgError> {
114 let nx = grid.x_grid.len();
115 let ny = grid.y_grid.len();
116 let nx_int = nx - 2; let ny_int = ny - 2; let n_int = nx_int * ny_int;
119
120 let dx = grid.x_grid.dx_uniform();
121 let dy = grid.y_grid.dx_uniform();
122 let inv_dx2 = S::ONE / (dx * dx);
123 let inv_dy2 = S::ONE / (dy * dy);
124 let inv_2dx = S::ONE / (S::from_f64(2.0) * dx);
125 let inv_2dy = S::ONE / (S::from_f64(2.0) * dy);
126 let two = S::from_f64(2.0);
127
128 let center = -two * coeffs.a * inv_dx2 - two * coeffs.b * inv_dy2 + coeffs.e;
130 let x_plus = coeffs.a * inv_dx2 + coeffs.c * inv_2dx; let x_minus = coeffs.a * inv_dx2 - coeffs.c * inv_2dx; let y_plus = coeffs.b * inv_dy2 + coeffs.d * inv_2dy; let y_minus = coeffs.b * inv_dy2 - coeffs.d * inv_2dy; let mut triplets = Vec::with_capacity(5 * n_int);
136 let mut rhs = vec![S::ZERO; n_int];
137
138 for jj in 0..ny_int {
139 for ii in 0..nx_int {
140 let row = interior_index_2d(ii, jj, nx_int);
141 let gi = ii + 1;
143 let gj = jj + 1;
144
145 triplets.push((row, row, center));
147
148 if ii == 0 {
150 if bc.x_min.is_dirichlet() {
152 let bval = bc.x_min.value(S::ZERO).unwrap_or(S::ZERO);
153 rhs[row] = rhs[row] + x_minus * bval;
154 } else {
155 triplets.push((row, row, x_minus));
158 let flux = bc.x_min.flux(S::ZERO).unwrap_or(S::ZERO);
159 rhs[row] = rhs[row] + x_minus * two * dx * flux;
160 }
161 } else {
162 let col = interior_index_2d(ii - 1, jj, nx_int);
163 triplets.push((row, col, x_minus));
164 }
165
166 if ii == nx_int - 1 {
168 if bc.x_max.is_dirichlet() {
170 let bval = bc.x_max.value(S::ZERO).unwrap_or(S::ZERO);
171 rhs[row] = rhs[row] + x_plus * bval;
172 } else {
173 triplets.push((row, row, x_plus));
174 let flux = bc.x_max.flux(S::ZERO).unwrap_or(S::ZERO);
175 rhs[row] = rhs[row] - x_plus * two * dx * flux;
176 }
177 } else {
178 let col = interior_index_2d(ii + 1, jj, nx_int);
179 triplets.push((row, col, x_plus));
180 }
181
182 if jj == 0 {
184 if bc.y_min.is_dirichlet() {
186 let bval = bc.y_min.value(S::ZERO).unwrap_or(S::ZERO);
187 rhs[row] = rhs[row] + y_minus * bval;
188 } else {
189 triplets.push((row, row, y_minus));
190 let flux = bc.y_min.flux(S::ZERO).unwrap_or(S::ZERO);
191 rhs[row] = rhs[row] + y_minus * two * dy * flux;
192 }
193 } else {
194 let col = interior_index_2d(ii, jj - 1, nx_int);
195 triplets.push((row, col, y_minus));
196 }
197
198 if jj == ny_int - 1 {
200 if bc.y_max.is_dirichlet() {
202 let bval = bc.y_max.value(S::ZERO).unwrap_or(S::ZERO);
203 rhs[row] = rhs[row] + y_plus * bval;
204 } else {
205 triplets.push((row, row, y_plus));
206 let flux = bc.y_max.flux(S::ZERO).unwrap_or(S::ZERO);
207 rhs[row] = rhs[row] - y_plus * two * dy * flux;
208 }
209 } else {
210 let col = interior_index_2d(ii, jj + 1, nx_int);
211 triplets.push((row, col, y_plus));
212 }
213
214 let _ = (gi, gj); }
216 }
217
218 let matrix = SparseMatrix::from_triplets(n_int, n_int, &triplets)?;
219 Ok((matrix, rhs))
220}
221
222#[derive(Clone, Debug)]
225pub struct Operator3DCoefficients<S: Scalar> {
226 pub a: S,
228 pub b: S,
230 pub c: S,
232 pub d: S,
234 pub e: S,
236 pub f: S,
238 pub g: S,
240}
241
242impl<S: Scalar> Operator3DCoefficients<S> {
243 pub fn laplacian() -> Self {
245 Self {
246 a: S::ONE,
247 b: S::ONE,
248 c: S::ONE,
249 d: S::ZERO,
250 e: S::ZERO,
251 f: S::ZERO,
252 g: S::ZERO,
253 }
254 }
255
256 pub fn scaled_laplacian(alpha: S) -> Self {
258 Self {
259 a: alpha,
260 b: alpha,
261 c: alpha,
262 d: S::ZERO,
263 e: S::ZERO,
264 f: S::ZERO,
265 g: S::ZERO,
266 }
267 }
268
269 pub fn advection_diffusion(diffusion: S, vx: S, vy: S, vz: S) -> Self {
271 Self {
272 a: diffusion,
273 b: diffusion,
274 c: diffusion,
275 d: -vx,
276 e: -vy,
277 f: -vz,
278 g: S::ZERO,
279 }
280 }
281}
282
283#[inline]
285fn interior_index_3d(i: usize, j: usize, k: usize, nx_int: usize, ny_int: usize) -> usize {
286 k * (nx_int * ny_int) + j * nx_int + i
287}
288
289pub fn assemble_laplacian_3d<S: SparseScalar>(
296 grid: &Grid3D<S>,
297 bc: &BoundaryConditions3D<S>,
298) -> Result<(SparseMatrix<S>, Vec<S>), LinalgError> {
299 let coeffs = Operator3DCoefficients::laplacian();
300 assemble_operator_3d(grid, &coeffs, bc)
301}
302
303pub fn assemble_operator_3d<S: SparseScalar>(
311 grid: &Grid3D<S>,
312 coeffs: &Operator3DCoefficients<S>,
313 bc: &BoundaryConditions3D<S>,
314) -> Result<(SparseMatrix<S>, Vec<S>), LinalgError> {
315 let nx = grid.x_grid.len();
316 let ny = grid.y_grid.len();
317 let nz = grid.z_grid.len();
318 let nx_int = nx - 2;
319 let ny_int = ny - 2;
320 let nz_int = nz - 2;
321 let n_int = nx_int * ny_int * nz_int;
322
323 let dx = grid.x_grid.dx_uniform();
324 let dy = grid.y_grid.dx_uniform();
325 let dz = grid.z_grid.dx_uniform();
326 let inv_dx2 = S::ONE / (dx * dx);
327 let inv_dy2 = S::ONE / (dy * dy);
328 let inv_dz2 = S::ONE / (dz * dz);
329 let inv_2dx = S::ONE / (S::from_f64(2.0) * dx);
330 let inv_2dy = S::ONE / (S::from_f64(2.0) * dy);
331 let inv_2dz = S::ONE / (S::from_f64(2.0) * dz);
332 let two = S::from_f64(2.0);
333
334 let center =
336 -two * coeffs.a * inv_dx2 - two * coeffs.b * inv_dy2 - two * coeffs.c * inv_dz2 + coeffs.g;
337 let x_plus = coeffs.a * inv_dx2 + coeffs.d * inv_2dx; let x_minus = coeffs.a * inv_dx2 - coeffs.d * inv_2dx; let y_plus = coeffs.b * inv_dy2 + coeffs.e * inv_2dy; let y_minus = coeffs.b * inv_dy2 - coeffs.e * inv_2dy; let z_plus = coeffs.c * inv_dz2 + coeffs.f * inv_2dz; let z_minus = coeffs.c * inv_dz2 - coeffs.f * inv_2dz; let mut triplets = Vec::with_capacity(7 * n_int);
345 let mut rhs = vec![S::ZERO; n_int];
346
347 for kk in 0..nz_int {
348 for jj in 0..ny_int {
349 for ii in 0..nx_int {
350 let row = interior_index_3d(ii, jj, kk, nx_int, ny_int);
351
352 triplets.push((row, row, center));
354
355 if ii == 0 {
357 if bc.x_min.is_dirichlet() {
358 let bval = bc.x_min.value(S::ZERO).unwrap_or(S::ZERO);
359 rhs[row] = rhs[row] + x_minus * bval;
360 } else {
361 triplets.push((row, row, x_minus));
362 let flux = bc.x_min.flux(S::ZERO).unwrap_or(S::ZERO);
363 rhs[row] = rhs[row] + x_minus * two * dx * flux;
364 }
365 } else {
366 let col = interior_index_3d(ii - 1, jj, kk, nx_int, ny_int);
367 triplets.push((row, col, x_minus));
368 }
369
370 if ii == nx_int - 1 {
372 if bc.x_max.is_dirichlet() {
373 let bval = bc.x_max.value(S::ZERO).unwrap_or(S::ZERO);
374 rhs[row] = rhs[row] + x_plus * bval;
375 } else {
376 triplets.push((row, row, x_plus));
377 let flux = bc.x_max.flux(S::ZERO).unwrap_or(S::ZERO);
378 rhs[row] = rhs[row] - x_plus * two * dx * flux;
379 }
380 } else {
381 let col = interior_index_3d(ii + 1, jj, kk, nx_int, ny_int);
382 triplets.push((row, col, x_plus));
383 }
384
385 if jj == 0 {
387 if bc.y_min.is_dirichlet() {
388 let bval = bc.y_min.value(S::ZERO).unwrap_or(S::ZERO);
389 rhs[row] = rhs[row] + y_minus * bval;
390 } else {
391 triplets.push((row, row, y_minus));
392 let flux = bc.y_min.flux(S::ZERO).unwrap_or(S::ZERO);
393 rhs[row] = rhs[row] + y_minus * two * dy * flux;
394 }
395 } else {
396 let col = interior_index_3d(ii, jj - 1, kk, nx_int, ny_int);
397 triplets.push((row, col, y_minus));
398 }
399
400 if jj == ny_int - 1 {
402 if bc.y_max.is_dirichlet() {
403 let bval = bc.y_max.value(S::ZERO).unwrap_or(S::ZERO);
404 rhs[row] = rhs[row] + y_plus * bval;
405 } else {
406 triplets.push((row, row, y_plus));
407 let flux = bc.y_max.flux(S::ZERO).unwrap_or(S::ZERO);
408 rhs[row] = rhs[row] - y_plus * two * dy * flux;
409 }
410 } else {
411 let col = interior_index_3d(ii, jj + 1, kk, nx_int, ny_int);
412 triplets.push((row, col, y_plus));
413 }
414
415 if kk == 0 {
417 if bc.z_min.is_dirichlet() {
418 let bval = bc.z_min.value(S::ZERO).unwrap_or(S::ZERO);
419 rhs[row] = rhs[row] + z_minus * bval;
420 } else {
421 triplets.push((row, row, z_minus));
422 let flux = bc.z_min.flux(S::ZERO).unwrap_or(S::ZERO);
423 rhs[row] = rhs[row] + z_minus * two * dz * flux;
424 }
425 } else {
426 let col = interior_index_3d(ii, jj, kk - 1, nx_int, ny_int);
427 triplets.push((row, col, z_minus));
428 }
429
430 if kk == nz_int - 1 {
432 if bc.z_max.is_dirichlet() {
433 let bval = bc.z_max.value(S::ZERO).unwrap_or(S::ZERO);
434 rhs[row] = rhs[row] + z_plus * bval;
435 } else {
436 triplets.push((row, row, z_plus));
437 let flux = bc.z_max.flux(S::ZERO).unwrap_or(S::ZERO);
438 rhs[row] = rhs[row] - z_plus * two * dz * flux;
439 }
440 } else {
441 let col = interior_index_3d(ii, jj, kk + 1, nx_int, ny_int);
442 triplets.push((row, col, z_plus));
443 }
444 }
445 }
446 }
447
448 let matrix = SparseMatrix::from_triplets(n_int, n_int, &triplets)?;
449 Ok((matrix, rhs))
450}
451
452#[cfg(test)]
453mod tests {
454 use super::*;
455 use numra_linalg::Matrix;
456
457 #[test]
458 fn test_laplacian_2d_size() {
459 let grid = Grid2D::uniform(0.0, 1.0, 5, 0.0, 1.0, 5);
461 let bc = BoundaryConditions2D::all_zero_dirichlet();
462 let (mat, rhs) = assemble_laplacian_2d(&grid, &bc).unwrap();
463 assert_eq!(mat.nrows(), 9);
464 assert_eq!(mat.ncols(), 9);
465 assert_eq!(rhs.len(), 9);
466 for &v in &rhs {
468 assert!(v.abs() < 1e-10);
469 }
470 }
471
472 #[test]
473 fn test_laplacian_2d_diagonal() {
474 let grid = Grid2D::uniform(0.0, 1.0, 4, 0.0, 1.0, 4);
476 let bc = BoundaryConditions2D::all_zero_dirichlet();
477 let (mat, _rhs) = assemble_laplacian_2d(&grid, &bc).unwrap();
478
479 let h = 1.0 / 3.0;
481 let expected_diag = -4.0 / (h * h);
482 assert!(
483 (mat.get(0, 0) - expected_diag).abs() < 1e-8,
484 "diag = {}, expected = {}",
485 mat.get(0, 0),
486 expected_diag
487 );
488 }
489
490 #[test]
491 fn test_laplacian_2d_symmetry() {
492 let grid = Grid2D::uniform(0.0, 1.0, 6, 0.0, 1.0, 6);
494 let bc = BoundaryConditions2D::all_zero_dirichlet();
495 let (mat, _rhs) = assemble_laplacian_2d(&grid, &bc).unwrap();
496
497 let n = mat.nrows();
498 let dense = mat.to_dense();
499 for i in 0..n {
500 for j in i + 1..n {
501 let a_ij = dense.get(i, j);
502 let a_ji = dense.get(j, i);
503 assert!(
504 (a_ij - a_ji).abs() < 1e-10,
505 "Not symmetric at ({}, {}): {} vs {}",
506 i,
507 j,
508 a_ij,
509 a_ji
510 );
511 }
512 }
513 }
514
515 #[test]
516 fn test_laplacian_2d_matvec() {
517 let n = 11;
520 let grid = Grid2D::uniform(0.0, 1.0, n, 0.0, 1.0, n);
521 let bc = BoundaryConditions2D::all_zero_dirichlet();
522 let (mat, rhs) = assemble_laplacian_2d(&grid, &bc).unwrap();
523
524 let nx_int = n - 2;
525 let ny_int = n - 2;
526 let n_int = nx_int * ny_int;
527
528 let mut u = vec![0.0_f64; n_int];
530 for jj in 0..ny_int {
531 for ii in 0..nx_int {
532 let x = grid.x_grid.points()[ii + 1];
533 let y = grid.y_grid.points()[jj + 1];
534 u[jj * nx_int + ii] = x * (1.0 - x) * y * (1.0 - y);
535 }
536 }
537
538 let lu = mat.mul_vec(&u).unwrap();
540 for jj in 0..ny_int {
541 for ii in 0..nx_int {
542 let idx = jj * nx_int + ii;
543 let x = grid.x_grid.points()[ii + 1];
544 let y = grid.y_grid.points()[jj + 1];
545 let exact_lap = -2.0 * y * (1.0 - y) - 2.0 * x * (1.0 - x);
546 let computed = lu[idx] + rhs[idx];
547 assert!(
548 (computed - exact_lap).abs() < 0.05,
549 "At ({}, {}): computed={}, exact={}",
550 x,
551 y,
552 computed,
553 exact_lap
554 );
555 }
556 }
557 }
558
559 #[test]
560 fn test_laplacian_2d_dirichlet_rhs() {
561 let grid = Grid2D::uniform(0.0, 1.0, 5, 0.0, 1.0, 5);
563 let bc = BoundaryConditions2D {
564 x_min: crate::boundary::BoxedBC::dirichlet(1.0),
565 x_max: crate::boundary::BoxedBC::dirichlet(0.0),
566 y_min: crate::boundary::BoxedBC::dirichlet(0.0),
567 y_max: crate::boundary::BoxedBC::dirichlet(0.0),
568 };
569 let (_mat, rhs) = assemble_laplacian_2d(&grid, &bc).unwrap();
570
571 let nx_int = 3;
573 for jj in 0..3 {
574 let idx = jj * nx_int + 0;
575 assert!(
576 rhs[idx].abs() > 1e-10,
577 "Expected nonzero rhs at ii=0, jj={}",
578 jj
579 );
580 }
581 for jj in 0..3 {
583 let idx = jj * nx_int + 2;
584 assert!(
585 rhs[idx].abs() < 1e-10,
586 "Expected zero rhs at ii=2, jj={}",
587 jj
588 );
589 }
590 }
591
592 #[test]
593 fn test_operator_2d_advection_diffusion() {
594 let grid = Grid2D::uniform(0.0, 1.0, 5, 0.0, 1.0, 5);
595 let bc = BoundaryConditions2D::all_zero_dirichlet();
596 let coeffs = Operator2DCoefficients::advection_diffusion(0.1, 1.0, 0.0);
597 let (mat, _rhs) = assemble_operator_2d(&grid, &coeffs, &bc).unwrap();
598 assert_eq!(mat.nrows(), 9);
599 assert_eq!(mat.ncols(), 9);
600 }
601
602 #[test]
603 fn test_laplacian_3d_size() {
604 let grid = Grid3D::uniform(0.0, 1.0, 4, 0.0, 1.0, 4, 0.0, 1.0, 4);
606 let bc = BoundaryConditions3D::all_zero_dirichlet();
607 let (mat, rhs) = assemble_laplacian_3d(&grid, &bc).unwrap();
608 assert_eq!(mat.nrows(), 8);
609 assert_eq!(mat.ncols(), 8);
610 assert_eq!(rhs.len(), 8);
611 }
612
613 #[test]
614 fn test_laplacian_3d_diagonal() {
615 let grid = Grid3D::uniform(0.0, 1.0, 4, 0.0, 1.0, 4, 0.0, 1.0, 4);
616 let bc = BoundaryConditions3D::all_zero_dirichlet();
617 let (mat, _rhs) = assemble_laplacian_3d(&grid, &bc).unwrap();
618
619 let h = 1.0 / 3.0;
621 let expected_diag = -6.0 / (h * h);
622 assert!(
623 (mat.get(0, 0) - expected_diag).abs() < 1e-8,
624 "diag = {}, expected = {}",
625 mat.get(0, 0),
626 expected_diag
627 );
628 }
629
630 #[test]
631 fn test_laplacian_3d_symmetry() {
632 let grid = Grid3D::uniform(0.0, 1.0, 5, 0.0, 1.0, 5, 0.0, 1.0, 5);
633 let bc = BoundaryConditions3D::all_zero_dirichlet();
634 let (mat, _rhs) = assemble_laplacian_3d(&grid, &bc).unwrap();
635
636 let n = mat.nrows();
637 let dense = mat.to_dense();
638 for i in 0..n {
639 for j in i + 1..n {
640 let a_ij = dense.get(i, j);
641 let a_ji = dense.get(j, i);
642 assert!(
643 (a_ij - a_ji).abs() < 1e-10,
644 "Not symmetric at ({}, {}): {} vs {}",
645 i,
646 j,
647 a_ij,
648 a_ji
649 );
650 }
651 }
652 }
653}