Skip to main content

oxiphysics_core/pde/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::needless_range_loop)]
6/// Solves a tridiagonal system Ax = d using the Thomas algorithm.
7///
8/// # Arguments
9/// * `a` - Sub-diagonal (length n-1), a\[0\] unused.
10/// * `b` - Main diagonal (length n).
11/// * `c` - Super-diagonal (length n-1), c\[n-1\] unused.
12/// * `d` - Right-hand side vector (length n).
13///
14/// Returns the solution vector x.
15pub fn thomas_algorithm(a: &[f64], b: &[f64], c: &[f64], d: &[f64]) -> Vec<f64> {
16    let n = b.len();
17    assert!(n >= 2, "system must have at least 2 equations");
18    assert_eq!(a.len(), n);
19    assert_eq!(c.len(), n);
20    assert_eq!(d.len(), n);
21    let mut c_prime = vec![0.0_f64; n];
22    let mut d_prime = vec![0.0_f64; n];
23    let mut x = vec![0.0_f64; n];
24    c_prime[0] = c[0] / b[0];
25    d_prime[0] = d[0] / b[0];
26    for i in 1..n {
27        let m = b[i] - a[i] * c_prime[i - 1];
28        c_prime[i] = if i < n - 1 { c[i] / m } else { 0.0 };
29        d_prime[i] = (d[i] - a[i] * d_prime[i - 1]) / m;
30    }
31    x[n - 1] = d_prime[n - 1];
32    for i in (0..n - 1).rev() {
33        x[i] = d_prime[i] - c_prime[i] * x[i + 1];
34    }
35    x
36}
37/// Computes the first-order finite-difference gradient of `u` with spacing `dx`.
38///
39/// Uses central differences in the interior and forward/backward at boundaries.
40pub fn fdm_gradient(u: &[f64], dx: f64) -> Vec<f64> {
41    let n = u.len();
42    let mut grad = vec![0.0_f64; n];
43    if n == 0 {
44        return grad;
45    }
46    if n == 1 {
47        return grad;
48    }
49    grad[0] = (u[1] - u[0]) / dx;
50    for i in 1..n - 1 {
51        grad[i] = (u[i + 1] - u[i - 1]) / (2.0 * dx);
52    }
53    grad[n - 1] = (u[n - 1] - u[n - 2]) / dx;
54    grad
55}
56/// Computes the second-order finite-difference Laplacian of `u` with spacing `dx`.
57///
58/// Uses the standard 3-point stencil: (u\[i-1\] - 2*u\[i\] + u\[i+1\]) / dx^2.
59pub fn fdm_laplacian(u: &[f64], dx: f64) -> Vec<f64> {
60    let n = u.len();
61    let mut lap = vec![0.0_f64; n];
62    if n < 3 {
63        return lap;
64    }
65    let dx2 = dx * dx;
66    for i in 1..n - 1 {
67        lap[i] = (u[i - 1] - 2.0 * u[i] + u[i + 1]) / dx2;
68    }
69    lap
70}
71/// Bilinear interpolation on a regular 2D grid.
72///
73/// # Arguments
74/// * `grid` - Row-major 2D data, shape (ny, nx).
75/// * `nx`, `ny` - Grid dimensions.
76/// * `x`, `y` - Fractional coordinates in \[0, nx-1\] x \[0, ny-1\].
77pub fn bilinear_interp(grid: &[f64], nx: usize, ny: usize, x: f64, y: f64) -> f64 {
78    let x0 = x.floor() as usize;
79    let y0 = y.floor() as usize;
80    let x0 = x0.min(nx - 2);
81    let y0 = y0.min(ny - 2);
82    let x1 = x0 + 1;
83    let y1 = y0 + 1;
84    let tx = x - x0 as f64;
85    let ty = y - y0 as f64;
86    let f00 = grid[y0 * nx + x0];
87    let f10 = grid[y0 * nx + x1];
88    let f01 = grid[y1 * nx + x0];
89    let f11 = grid[y1 * nx + x1];
90    f00 * (1.0 - tx) * (1.0 - ty) + f10 * tx * (1.0 - ty) + f01 * (1.0 - tx) * ty + f11 * tx * ty
91}
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::pde::AdiMethod2D;
96    use crate::pde::BoundaryCondition;
97    use crate::pde::BurgersShock;
98    use crate::pde::Dct1D;
99    use crate::pde::FftDiff1D;
100    use crate::pde::FiniteDiffOps1D;
101    use crate::pde::FiniteDiffOps2D;
102    use crate::pde::FiniteDiffOps3D;
103    use crate::pde::FiniteDifference1D;
104    use crate::pde::FiniteDifference2D;
105    use crate::pde::FiniteVolume1D;
106    use crate::pde::FitzHughNagumo;
107    use crate::pde::GrayScottSystem;
108    use crate::pde::HeatEquation1D;
109    use crate::pde::HeatEquation3D;
110    use crate::pde::LevelSet2D;
111    use crate::pde::MethodOfLines;
112    use crate::pde::NsBurgers1D;
113    use crate::pde::PhaseField2D;
114    use crate::pde::PoissonSolver;
115    use crate::pde::SpectralMethod;
116    use crate::pde::WaveEq2ndOrder;
117    use crate::pde::WaveEquation1D;
118    use std::f64::consts::PI;
119    #[test]
120    fn test_thomas_algorithm_basic() {
121        let a = vec![0.0, -1.0, -1.0];
122        let b = vec![2.0, 2.0, 2.0];
123        let c = vec![-1.0, -1.0, 0.0];
124        let d = vec![0.0, 0.0, 0.0];
125        let x = thomas_algorithm(&a, &b, &c, &d);
126        assert_eq!(x.len(), 3);
127        for xi in &x {
128            assert!(xi.abs() < 1e-12);
129        }
130    }
131    #[test]
132    fn test_thomas_algorithm_nontrivial() {
133        let a = vec![0.0, -1.0];
134        let b = vec![2.0, 2.0];
135        let c = vec![-1.0, 0.0];
136        let d = vec![1.0, 1.0];
137        let x = thomas_algorithm(&a, &b, &c, &d);
138        assert!((x[0] - 1.0).abs() < 1e-10);
139        assert!((x[1] - 1.0).abs() < 1e-10);
140    }
141    #[test]
142    fn test_fdm_gradient_constant() {
143        let u = vec![3.0; 10];
144        let grad = fdm_gradient(&u, 0.1);
145        for g in &grad {
146            assert!(g.abs() < 1e-12);
147        }
148    }
149    #[test]
150    fn test_fdm_gradient_linear() {
151        let dx = 0.1;
152        let u: Vec<f64> = (0..10).map(|i| i as f64 * dx).collect();
153        let grad = fdm_gradient(&u, dx);
154        for i in 1..9 {
155            assert!((grad[i] - 1.0).abs() < 1e-10);
156        }
157    }
158    #[test]
159    fn test_fdm_laplacian_linear() {
160        let dx = 0.1;
161        let u: Vec<f64> = (0..10).map(|i| i as f64 * dx).collect();
162        let lap = fdm_laplacian(&u, dx);
163        for i in 1..9 {
164            assert!(lap[i].abs() < 1e-10);
165        }
166    }
167    #[test]
168    fn test_fdm_laplacian_quadratic() {
169        let dx = 0.1;
170        let u: Vec<f64> = (0..20).map(|i| (i as f64 * dx).powi(2)).collect();
171        let lap = fdm_laplacian(&u, dx);
172        for i in 1..19 {
173            assert!((lap[i] - 2.0).abs() < 1e-6, "lap[{}] = {}", i, lap[i]);
174        }
175    }
176    #[test]
177    fn test_bilinear_interp_at_nodes() {
178        let grid = vec![1.0, 2.0, 3.0, 4.0];
179        assert!((bilinear_interp(&grid, 2, 2, 0.0, 0.0) - 1.0).abs() < 1e-12);
180        assert!((bilinear_interp(&grid, 2, 2, 1.0, 0.0) - 2.0).abs() < 1e-12);
181        assert!((bilinear_interp(&grid, 2, 2, 0.0, 1.0) - 3.0).abs() < 1e-12);
182        assert!((bilinear_interp(&grid, 2, 2, 1.0, 1.0) - 4.0).abs() < 1e-12);
183    }
184    #[test]
185    fn test_bilinear_interp_midpoint() {
186        let grid = vec![1.0, 2.0, 3.0, 4.0];
187        let v = bilinear_interp(&grid, 2, 2, 0.5, 0.5);
188        assert!((v - 2.5).abs() < 1e-12);
189    }
190    #[test]
191    fn test_fd1d_diffusion_number() {
192        let fd = FiniteDifference1D::new(
193            10,
194            0.1,
195            0.01,
196            0.5,
197            0.0,
198            BoundaryCondition::Dirichlet(0.0),
199            BoundaryCondition::Dirichlet(0.0),
200        );
201        let r = fd.diffusion_number();
202        assert!((r - 0.5).abs() < 1e-12);
203    }
204    #[test]
205    fn test_fd1d_stability_check() {
206        let fd_stable = FiniteDifference1D::new(
207            10,
208            0.1,
209            0.001,
210            0.1,
211            0.0,
212            BoundaryCondition::Dirichlet(0.0),
213            BoundaryCondition::Dirichlet(0.0),
214        );
215        assert!(fd_stable.is_stable_explicit());
216        let fd_unstable = FiniteDifference1D::new(
217            10,
218            0.1,
219            0.1,
220            1.0,
221            0.0,
222            BoundaryCondition::Dirichlet(0.0),
223            BoundaryCondition::Dirichlet(0.0),
224        );
225        assert!(!fd_unstable.is_stable_explicit());
226    }
227    #[test]
228    fn test_fd1d_explicit_diffusion_preserves_bc() {
229        let n = 10;
230        let fd = FiniteDifference1D::new(
231            n,
232            0.1,
233            0.001,
234            0.1,
235            0.0,
236            BoundaryCondition::Dirichlet(0.0),
237            BoundaryCondition::Dirichlet(1.0),
238        );
239        let u: Vec<f64> = (0..n).map(|i| i as f64 / (n - 1) as f64).collect();
240        let u_new = fd.step_diffusion_explicit(&u);
241        assert!((u_new[0] - 0.0).abs() < 1e-12);
242        assert!((u_new[n - 1] - 1.0).abs() < 1e-12);
243    }
244    #[test]
245    fn test_fd1d_implicit_diffusion_preserves_bc() {
246        let n = 10;
247        let fd = FiniteDifference1D::new(
248            n,
249            0.1,
250            0.01,
251            0.1,
252            0.0,
253            BoundaryCondition::Dirichlet(0.0),
254            BoundaryCondition::Dirichlet(1.0),
255        );
256        let u: Vec<f64> = (0..n).map(|i| i as f64 / (n - 1) as f64).collect();
257        let u_new = fd.step_diffusion_implicit(&u);
258        assert!((u_new[0] - 0.0).abs() < 1e-12);
259        assert!((u_new[n - 1] - 1.0).abs() < 1e-12);
260    }
261    #[test]
262    fn test_fd1d_cn_diffusion_preserves_bc() {
263        let n = 10;
264        let fd = FiniteDifference1D::new(
265            n,
266            0.1,
267            0.01,
268            0.1,
269            0.0,
270            BoundaryCondition::Dirichlet(0.0),
271            BoundaryCondition::Dirichlet(1.0),
272        );
273        let u: Vec<f64> = (0..n).map(|i| i as f64 / (n - 1) as f64).collect();
274        let u_new = fd.step_diffusion_cn(&u);
275        assert!((u_new[0] - 0.0).abs() < 1e-12);
276        assert!((u_new[n - 1] - 1.0).abs() < 1e-12);
277    }
278    #[test]
279    fn test_fd1d_advection_upwind_conservation() {
280        let n = 20;
281        let fd = FiniteDifference1D::new(
282            n,
283            0.05,
284            0.01,
285            0.0,
286            1.0,
287            BoundaryCondition::Periodic,
288            BoundaryCondition::Periodic,
289        );
290        let u: Vec<f64> = (0..n)
291            .map(|i| (2.0 * PI * i as f64 / n as f64).sin())
292            .collect();
293        let u_new = fd.step_advection_upwind(&u);
294        let sum_old: f64 = u.iter().sum();
295        let sum_new: f64 = u_new.iter().sum();
296        assert!((sum_old - sum_new).abs() < 1.0);
297    }
298    #[test]
299    fn test_fd2d_laplacian_5pt_zero_for_linear() {
300        let nx = 5;
301        let ny = 5;
302        let fd2d = FiniteDifference2D::new(nx, ny, 0.1, 0.1, 0.001, 0.1);
303        let u: Vec<f64> = (0..ny)
304            .flat_map(|j| (0..nx).map(move |i| i as f64 * 0.1 + j as f64 * 0.1))
305            .collect();
306        let lap = fd2d.laplacian_5pt(&u);
307        for j in 1..ny - 1 {
308            for i in 1..nx - 1 {
309                assert!(lap[j * nx + i].abs() < 1e-10);
310            }
311        }
312    }
313    #[test]
314    fn test_wave1d_courant_number() {
315        let w = WaveEquation1D::new(100, 0.01, 0.005, 1.0);
316        assert!((w.courant_number() - 0.5).abs() < 1e-12);
317    }
318    #[test]
319    fn test_wave1d_lax_wendroff_stable() {
320        let n = 50;
321        let w = WaveEquation1D::new(n, 0.02, 0.01, 1.0);
322        let u: Vec<f64> = (0..n)
323            .map(|i| (2.0 * PI * i as f64 / n as f64).sin())
324            .collect();
325        let u_prev = u.clone();
326        let u_new = w.step_lax_wendroff(&u, &u_prev);
327        let max_val = u_new.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
328        assert!(max_val.abs() <= 2.0);
329    }
330    #[test]
331    fn test_poisson_vcycle_converges() {
332        let n = 17;
333        let dx = 1.0 / (n - 1) as f64;
334        let solver = PoissonSolver::new(n, dx, 4, 10);
335        let f: Vec<f64> = (0..n).map(|i| -(PI * i as f64 * dx).sin()).collect();
336        let u0 = vec![0.0_f64; n];
337        let u = solver.vcycle(&u0, &f, 3);
338        let exact: Vec<f64> = (0..n)
339            .map(|i| (PI * i as f64 * dx).sin() / (PI * PI))
340            .collect();
341        let err = (0..n)
342            .map(|i| (u[i] - exact[i]).powi(2))
343            .sum::<f64>()
344            .sqrt()
345            / n as f64;
346        assert!(err < 0.1, "Error too large: {}", err);
347    }
348    #[test]
349    fn test_burgers_step_no_blowup() {
350        let n = 50;
351        let b = NsBurgers1D::new(n, 0.02, 0.001, 0.01);
352        let u: Vec<f64> = (0..n)
353            .map(|i| (2.0 * PI * i as f64 / n as f64).sin())
354            .collect();
355        let u_new = b.step(&u);
356        let max_val = u_new.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
357        assert!(max_val.abs() < 10.0);
358    }
359    #[test]
360    fn test_spectral_chebyshev_nodes_count() {
361        let sp = SpectralMethod::new(8);
362        let nodes = sp.gauss_lobatto_nodes();
363        assert_eq!(nodes.len(), 8);
364        assert!((nodes[0].abs() - 1.0).abs() < 1e-12);
365        assert!((nodes[7].abs() - 1.0).abs() < 1e-12);
366    }
367    #[test]
368    fn test_spectral_differentiation_matrix_size() {
369        let sp = SpectralMethod::new(5);
370        let d = sp.differentiation_matrix();
371        assert_eq!(d.len(), 25);
372    }
373    #[test]
374    fn test_dct_roundtrip() {
375        let dct = Dct1D::new(8);
376        let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
377        let xk = dct.dct2_forward(&x);
378        let x_rec = dct.dct2_inverse(&xk);
379        for (orig, rec) in x.iter().zip(x_rec.iter()) {
380            assert!((orig - rec).abs() < 1e-10, "orig={} rec={}", orig, rec);
381        }
382    }
383    #[test]
384    fn test_fv1d_minmod() {
385        assert_eq!(FiniteVolume1D::minmod(2.0, 3.0), 2.0);
386        assert_eq!(FiniteVolume1D::minmod(-2.0, -3.0), -2.0);
387        assert_eq!(FiniteVolume1D::minmod(2.0, -3.0), 0.0);
388    }
389    #[test]
390    fn test_fv1d_superbee() {
391        let s = FiniteVolume1D::superbee(1.0, 1.0);
392        assert!(s >= 0.0);
393    }
394    #[test]
395    fn test_fv1d_van_leer() {
396        let s = FiniteVolume1D::van_leer(1.0, 2.0);
397        assert!((s - 4.0 / 3.0).abs() < 1e-12);
398    }
399    #[test]
400    fn test_fv1d_step_conservation() {
401        let n = 30;
402        let fv = FiniteVolume1D::new(n, 0.033, 0.005);
403        let u: Vec<f64> = (0..n)
404            .map(|i| (2.0 * PI * i as f64 / n as f64).sin())
405            .collect();
406        let u_new = fv.step(&u);
407        assert_eq!(u_new.len(), n);
408    }
409    #[test]
410    fn test_levelset2d_creation() {
411        let phi: Vec<f64> = (0..25).map(|i| i as f64 - 12.0).collect();
412        let ls = LevelSet2D::new(5, 5, 0.1, phi);
413        assert_eq!(ls.phi.len(), 25);
414    }
415    #[test]
416    fn test_levelset2d_normal_boundary() {
417        let nx = 5;
418        let ny = 5;
419        let phi: Vec<f64> = (0..nx * ny).map(|i| (i % nx) as f64 * 0.1 - 0.2).collect();
420        let ls = LevelSet2D::new(nx, ny, 0.1, phi);
421        let (nx_vec, _ny_vec) = ls.normal();
422        assert_eq!(nx_vec.len(), nx * ny);
423    }
424    #[test]
425    fn test_phasefield2d_allen_cahn_energy_decrease() {
426        let nx = 10;
427        let ny = 10;
428        let phi0: Vec<f64> = (0..nx * ny)
429            .map(|i| if i < nx * ny / 2 { -1.0 } else { 1.0 })
430            .collect();
431        let mut pf = PhaseField2D::new(nx, ny, 0.1, 0.001, 0.1, 1.0, phi0);
432        let e0 = pf.interface_energy();
433        pf.step_allen_cahn();
434        let e1 = pf.interface_energy();
435        assert!(e1 <= e0 * 2.0);
436    }
437    #[test]
438    fn test_method_of_lines_rk4() {
439        let mol = MethodOfLines::new(3, 0.1);
440        let u0 = vec![1.0, 0.0, 0.0];
441        let u_final = mol.integrate_rk4(&u0, 0.01, 100, |_t, u| u.iter().map(|&x| -x).collect());
442        assert!(u_final[0] < 1.0);
443        assert!(u_final[0] > 0.0);
444    }
445    #[test]
446    fn test_adi_2d_step() {
447        let nx = 5;
448        let ny = 5;
449        let fd2d = FiniteDifference2D::new(nx, ny, 0.1, 0.1, 0.001, 0.1);
450        let u: Vec<f64> = vec![1.0; nx * ny];
451        let u_new = fd2d.adi_step(&u);
452        assert_eq!(u_new.len(), nx * ny);
453    }
454    #[test]
455    fn test_heat3d_explicit_step() {
456        let nx = 4;
457        let ny = 4;
458        let nz = 4;
459        let heat = HeatEquation3D::new(
460            nx,
461            ny,
462            nz,
463            0.1,
464            0.0001,
465            0.01,
466            BoundaryCondition::Dirichlet(0.0),
467        );
468        let u: Vec<f64> = vec![1.0; nx * ny * nz];
469        let u_new = heat.step_explicit(&u);
470        assert_eq!(u_new.len(), nx * ny * nz);
471    }
472    #[test]
473    fn test_thomas_three_by_three() {
474        let a = vec![0.0, 1.0, 1.0];
475        let b = vec![4.0, 4.0, 4.0];
476        let c = vec![1.0, 1.0, 0.0];
477        let d = vec![5.0, 6.0, 5.0];
478        let x = thomas_algorithm(&a, &b, &c, &d);
479        let r0 = 4.0 * x[0] + 1.0 * x[1];
480        let r1 = 1.0 * x[0] + 4.0 * x[1] + 1.0 * x[2];
481        let r2 = 1.0 * x[1] + 4.0 * x[2];
482        assert!((r0 - 5.0).abs() < 1e-10);
483        assert!((r1 - 6.0).abs() < 1e-10);
484        assert!((r2 - 5.0).abs() < 1e-10);
485    }
486    #[test]
487    fn test_levelset2d_reinitialize() {
488        let nx = 7;
489        let ny = 7;
490        let phi0: Vec<f64> = (0..nx * ny)
491            .map(|i| {
492                let x = (i % nx) as f64 * 0.1 - 0.3;
493                let y = (i / nx) as f64 * 0.1 - 0.3;
494                x * x + y * y - 0.09
495            })
496            .collect();
497        let mut ls = LevelSet2D::new(nx, ny, 0.1, phi0);
498        ls.reinitialize(5, 0.01);
499        assert_eq!(ls.phi.len(), nx * ny);
500    }
501    #[test]
502    fn test_phasefield2d_cahn_hilliard_conserves_mass() {
503        let nx = 8;
504        let ny = 8;
505        let phi0: Vec<f64> = (0..nx * ny)
506            .map(|i| if i % 2 == 0 { 0.5 } else { -0.5 })
507            .collect();
508        let mut pf = PhaseField2D::new(nx, ny, 0.1, 0.001, 0.1, 0.1, phi0);
509        let mass_before: f64 = pf.phi.iter().sum();
510        pf.step_cahn_hilliard();
511        let mass_after: f64 = pf.phi.iter().sum();
512        assert!((mass_before - mass_after).abs() < 1.0);
513    }
514    #[test]
515    fn test_fd1d_absorbing_bc() {
516        let n = 10;
517        let fd = FiniteDifference1D::new(
518            n,
519            0.1,
520            0.001,
521            0.1,
522            0.0,
523            BoundaryCondition::Absorbing,
524            BoundaryCondition::Absorbing,
525        );
526        let u: Vec<f64> = (0..n).map(|i| i as f64).collect();
527        let u_new = fd.step_diffusion_explicit(&u);
528        assert!((u_new[0] - u_new[1]).abs() < 1e-10);
529        assert!((u_new[n - 1] - u_new[n - 2]).abs() < 1e-10);
530    }
531    #[test]
532    fn test_wave1d_absorbing_bc() {
533        let n = 20;
534        let w = WaveEquation1D::new(n, 0.05, 0.01, 1.0);
535        let mut u = vec![1.0_f64; n];
536        w.apply_absorbing_bc(&mut u);
537        assert!((u[0] - u[1]).abs() < 1e-12);
538        assert!((u[n - 1] - u[n - 2]).abs() < 1e-12);
539    }
540    #[test]
541    fn test_fft_poisson_periodicity() {
542        let n = 8;
543        let dx = 2.0 * PI / n as f64;
544        let solver = PoissonSolver::new(n, dx, 3, 5);
545        let f: Vec<f64> = (0..n).map(|i| -(i as f64 * dx).sin()).collect();
546        let u = solver.solve_fft_periodic(&f);
547        assert_eq!(u.len(), n);
548    }
549    #[test]
550    fn test_laplacian1d_constant_zero() {
551        let ops = FiniteDiffOps1D::new(10, 0.1);
552        let u = vec![5.0_f64; 10];
553        let lap = ops.laplacian(&u);
554        for i in 1..9 {
555            assert!(lap[i].abs() < 1e-10);
556        }
557    }
558    #[test]
559    fn test_laplacian2d_linear_zero() {
560        let ops = FiniteDiffOps2D::new(5, 5, 0.1, 0.1);
561        let u: Vec<f64> = (0..25).map(|k| (k % 5) as f64 + (k / 5) as f64).collect();
562        let lap = ops.laplacian(&u);
563        for j in 1..4 {
564            for i in 1..4 {
565                assert!(lap[j * 5 + i].abs() < 1e-9);
566            }
567        }
568    }
569    #[test]
570    fn test_gradient2d_constant_zero() {
571        let ops = FiniteDiffOps2D::new(5, 5, 0.1, 0.1);
572        let u = vec![3.0_f64; 25];
573        let (gx, gy) = ops.gradient(&u);
574        for i in 1..4 {
575            for j in 1..4 {
576                let idx = j * 5 + i;
577                assert!(gx[idx].abs() < 1e-10);
578                assert!(gy[idx].abs() < 1e-10);
579            }
580        }
581    }
582    #[test]
583    fn test_divergence2d_result_size() {
584        let ops = FiniteDiffOps2D::new(5, 5, 0.1, 0.1);
585        let fx = vec![1.0_f64; 25];
586        let fy = vec![0.0_f64; 25];
587        let div = ops.divergence(&fx, &fy);
588        assert_eq!(div.len(), 25);
589    }
590    #[test]
591    fn test_curl2d_constant_zero() {
592        let ops = FiniteDiffOps2D::new(5, 5, 0.1, 0.1);
593        let ux = vec![2.0_f64; 25];
594        let uy = vec![2.0_f64; 25];
595        let curl = ops.curl(&ux, &uy);
596        for j in 1..4 {
597            for i in 1..4 {
598                assert!(curl[j * 5 + i].abs() < 1e-9);
599            }
600        }
601    }
602    #[test]
603    fn test_laplacian3d_result_size() {
604        let ops = FiniteDiffOps3D::new(4, 4, 4, 0.1);
605        let u = vec![1.0_f64; 64];
606        let lap = ops.laplacian(&u);
607        assert_eq!(lap.len(), 64);
608    }
609    #[test]
610    fn test_mol_diffusion_rk4_decay() {
611        let n = 20;
612        let dx = 1.0 / (n - 1) as f64;
613        let mol = MethodOfLines::new(n, dx);
614        let u0: Vec<f64> = (0..n).map(|i| (PI * i as f64 * dx).sin()).collect();
615        let d = 0.01_f64;
616        let u_final = mol.integrate_rk4(&u0, 0.001, 50, |_t, u| {
617            fdm_laplacian(u, dx).iter().map(|&v| d * v).collect()
618        });
619        let amp0: f64 = u0.iter().cloned().fold(0.0_f64, f64::max);
620        let amp1: f64 = u_final.iter().cloned().fold(0.0_f64, f64::max);
621        assert!(amp1 <= amp0 + 1e-10);
622    }
623    #[test]
624    fn test_spectral_fft_diff_sine() {
625        let n = 16;
626        let dx = 2.0 * PI / n as f64;
627        let fft = FftDiff1D::new(n, dx);
628        let u: Vec<f64> = (0..n).map(|i| (i as f64 * dx).sin()).collect();
629        let du = fft.differentiate(&u);
630        for i in 2..n - 2 {
631            let x = i as f64 * dx;
632            assert!(
633                (du[i] - x.cos()).abs() < 0.05,
634                "i={} du={} cos={}",
635                i,
636                du[i],
637                x.cos()
638            );
639        }
640    }
641    #[test]
642    fn test_gray_scott_step_size() {
643        let nx = 8;
644        let ny = 8;
645        let (u0, v0) = GrayScottSystem::initial_conditions(nx, ny);
646        let mut gs = GrayScottSystem::new(nx, ny, 0.1, 0.001, 0.04, 0.06);
647        gs.u = u0;
648        gs.v = v0;
649        gs.step();
650        assert_eq!(gs.u.len(), nx * ny);
651        assert_eq!(gs.v.len(), nx * ny);
652    }
653    #[test]
654    fn test_fitzhugh_nagumo_step() {
655        let mut fhn = FitzHughNagumo::new(10, 0.1, 0.001);
656        fhn.step();
657        assert_eq!(fhn.v.len(), 10);
658        assert_eq!(fhn.w.len(), 10);
659    }
660    #[test]
661    fn test_wave_eq_2nd_order_size() {
662        let n = 20;
663        let w = WaveEq2ndOrder::new(n, 0.05, 0.01, 1.0);
664        let u = vec![0.0_f64; n];
665        let u_prev = vec![0.0_f64; n];
666        let u_new = w.step(&u, &u_prev);
667        assert_eq!(u_new.len(), n);
668    }
669    #[test]
670    fn test_burgers_shock_formation() {
671        let n = 50;
672        let b = BurgersShock::new(n, 0.02, 0.001, 0.005);
673        let u: Vec<f64> = (0..n).map(|i| (PI * i as f64 / n as f64).sin()).collect();
674        let u_new = b.step(&u);
675        assert_eq!(u_new.len(), n);
676        let max_abs = u_new.iter().cloned().fold(0.0_f64, |a, v| a.max(v.abs()));
677        assert!(max_abs < 5.0);
678    }
679    #[test]
680    fn test_heat_eq_explicit_implicit_cn_size() {
681        let n = 10;
682        let he = HeatEquation1D::new(n, 0.1, 0.001, 0.1);
683        let u = vec![1.0_f64; n];
684        assert_eq!(he.step_explicit(&u).len(), n);
685        assert_eq!(he.step_implicit(&u).len(), n);
686        assert_eq!(he.step_cn(&u).len(), n);
687    }
688    #[test]
689    fn test_poisson_multigrid_restrict_prolongate_roundtrip() {
690        let ps = PoissonSolver::new(8, 0.1, 3, 5);
691        let f = vec![1.0_f64, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
692        let coarse = ps.restrict(&f);
693        assert_eq!(coarse.len(), 4);
694        let fine = ps.prolongate(&coarse, 8);
695        assert_eq!(fine.len(), 8);
696    }
697    #[test]
698    fn test_adi_method_2d_size() {
699        let adi = AdiMethod2D::new(6, 6, 0.1, 0.1, 0.001, 0.1);
700        let u = vec![1.0_f64; 36];
701        let u_new = adi.step(&u);
702        assert_eq!(u_new.len(), 36);
703    }
704    #[test]
705    fn test_levelset_advection_preserves_size() {
706        let nx = 7;
707        let ny = 7;
708        let phi0: Vec<f64> = (0..nx * ny).map(|i| i as f64 - 24.0).collect();
709        let mut ls = LevelSet2D::new(nx, ny, 0.1, phi0);
710        let vel_u = vec![0.1_f64; nx * ny];
711        let vel_v = vec![0.0_f64; nx * ny];
712        ls.advect(&vel_u, &vel_v, 0.01);
713        assert_eq!(ls.phi.len(), nx * ny);
714    }
715}