1#![allow(clippy::needless_range_loop, clippy::ptr_arg)]
2#![allow(dead_code)]
14
15use std::f64::consts::PI;
16
17#[derive(Debug, Clone, Copy, PartialEq)]
23pub enum BoundaryCondition {
24 Dirichlet(f64),
26 Neumann(f64),
28 Periodic,
30}
31
32#[allow(clippy::many_single_char_names)]
42pub fn thomas_algorithm(a: &[f64], b: &[f64], c: &[f64], d: &[f64]) -> Vec<f64> {
43 let n = b.len();
44 assert!(n >= 2, "system must have at least 2 unknowns");
45 assert_eq!(a.len(), n);
46 assert_eq!(c.len(), n);
47 assert_eq!(d.len(), n);
48
49 let mut cp = vec![0.0_f64; n];
50 let mut dp = vec![0.0_f64; n];
51 let mut x = vec![0.0_f64; n];
52
53 cp[0] = c[0] / b[0];
54 dp[0] = d[0] / b[0];
55 for i in 1..n {
56 let denom = b[i] - a[i] * cp[i - 1];
57 cp[i] = if i < n - 1 { c[i] / denom } else { 0.0 };
58 dp[i] = (d[i] - a[i] * dp[i - 1]) / denom;
59 }
60
61 x[n - 1] = dp[n - 1];
62 for i in (0..n - 1).rev() {
63 x[i] = dp[i] - cp[i] * x[i + 1];
64 }
65 x
66}
67
68pub fn first_derivative_central(u: &[f64], dx: f64) -> Vec<f64> {
76 let n = u.len();
77 let mut du = vec![0.0_f64; n];
78 if n < 2 {
79 return du;
80 }
81 du[0] = (u[1] - u[0]) / dx;
82 for i in 1..n - 1 {
83 du[i] = (u[i + 1] - u[i - 1]) / (2.0 * dx);
84 }
85 du[n - 1] = (u[n - 1] - u[n - 2]) / dx;
86 du
87}
88
89pub fn second_derivative(u: &[f64], dx: f64) -> Vec<f64> {
93 let n = u.len();
94 let mut d2u = vec![0.0_f64; n];
95 for i in 1..n.saturating_sub(1) {
96 d2u[i] = (u[i - 1] - 2.0 * u[i] + u[i + 1]) / (dx * dx);
97 }
98 d2u
99}
100
101pub fn first_derivative_fourth_order(u: &[f64], dx: f64) -> Vec<f64> {
106 let n = u.len();
107 let mut du = vec![0.0_f64; n];
108 if n < 2 {
109 return du;
110 }
111 du[0] = (u[1] - u[0]) / dx;
113 if n > 1 {
114 du[n - 1] = (u[n - 1] - u[n - 2]) / dx;
115 }
116 if n > 2 {
117 du[1] = (u[2] - u[0]) / (2.0 * dx);
118 du[n - 2] = (u[n - 1] - u[n - 3]) / (2.0 * dx);
119 }
120 for i in 2..n.saturating_sub(2) {
122 du[i] = (-u[i + 2] + 8.0 * u[i + 1] - 8.0 * u[i - 1] + u[i - 2]) / (12.0 * dx);
123 }
124 du
125}
126
127#[allow(clippy::too_many_arguments)]
135pub fn advection_upwind_step(
136 u: &[f64],
137 dx: f64,
138 dt: f64,
139 c: f64,
140 bc_left: BoundaryCondition,
141 bc_right: BoundaryCondition,
142) -> Vec<f64> {
143 let n = u.len();
144 let mut un = u.to_vec();
145 let r = c * dt / dx;
146 for i in 1..n - 1 {
147 if c > 0.0 {
148 un[i] = u[i] - r * (u[i] - u[i - 1]);
149 } else {
150 un[i] = u[i] - r * (u[i + 1] - u[i]);
151 }
152 }
153 apply_bc_1d(&mut un, bc_left, bc_right);
154 un
155}
156
157pub fn apply_bc_1d(u: &mut Vec<f64>, left: BoundaryCondition, right: BoundaryCondition) {
159 let n = u.len();
160 if n == 0 {
161 return;
162 }
163 match left {
164 BoundaryCondition::Dirichlet(v) => u[0] = v,
165 BoundaryCondition::Neumann(dv) => {
166 if n > 1 {
167 u[0] = u[1] - dv * 1.0; }
169 }
170 BoundaryCondition::Periodic => {
171 if n > 1 {
172 u[0] = u[n - 2];
173 }
174 }
175 }
176 match right {
177 BoundaryCondition::Dirichlet(v) => u[n - 1] = v,
178 BoundaryCondition::Neumann(dv) => {
179 if n > 1 {
180 u[n - 1] = u[n - 2] + dv * 1.0;
181 }
182 }
183 BoundaryCondition::Periodic => {
184 if n > 1 {
185 u[n - 1] = u[1];
186 }
187 }
188 }
189}
190
191pub fn diffusion_explicit_step(
199 u: &[f64],
200 dx: f64,
201 dt: f64,
202 alpha: f64,
203 bc_left: BoundaryCondition,
204 bc_right: BoundaryCondition,
205) -> Vec<f64> {
206 let n = u.len();
207 let r = alpha * dt / (dx * dx);
208 let mut un = u.to_vec();
209 for i in 1..n - 1 {
210 un[i] = u[i] + r * (u[i - 1] - 2.0 * u[i] + u[i + 1]);
211 }
212 apply_bc_1d(&mut un, bc_left, bc_right);
213 un
214}
215
216pub fn diffusion_crank_nicolson_step(
225 u: &[f64],
226 dx: f64,
227 dt: f64,
228 alpha: f64,
229 bc_left: BoundaryCondition,
230 bc_right: BoundaryCondition,
231) -> Vec<f64> {
232 let n = u.len();
233 let r = alpha * dt / (2.0 * dx * dx);
234
235 let mut rhs = vec![0.0_f64; n];
237 rhs[0] = match bc_left {
238 BoundaryCondition::Dirichlet(v) => v,
239 _ => u[0],
240 };
241 rhs[n - 1] = match bc_right {
242 BoundaryCondition::Dirichlet(v) => v,
243 _ => u[n - 1],
244 };
245 for i in 1..n - 1 {
246 rhs[i] = r * u[i - 1] + (1.0 - 2.0 * r) * u[i] + r * u[i + 1];
247 }
248
249 let mut a_diag = vec![0.0_f64; n]; let mut b_diag = vec![0.0_f64; n]; let mut c_diag = vec![0.0_f64; n]; b_diag[0] = 1.0;
255 b_diag[n - 1] = 1.0;
256 for i in 1..n - 1 {
257 a_diag[i] = -r;
258 b_diag[i] = 1.0 + 2.0 * r;
259 c_diag[i] = -r;
260 }
261
262 let mut sol = thomas_algorithm(&a_diag, &b_diag, &c_diag, &rhs);
263 apply_bc_1d(&mut sol, bc_left, bc_right);
264 sol
265}
266
267pub fn diffusion_implicit_step(
275 u: &[f64],
276 dx: f64,
277 dt: f64,
278 alpha: f64,
279 bc_left: BoundaryCondition,
280 bc_right: BoundaryCondition,
281) -> Vec<f64> {
282 let n = u.len();
283 let r = alpha * dt / (dx * dx);
284 let mut a_diag = vec![0.0_f64; n];
285 let mut b_diag = vec![0.0_f64; n];
286 let mut c_diag = vec![0.0_f64; n];
287 let mut rhs = u.to_vec();
288
289 b_diag[0] = 1.0;
290 b_diag[n - 1] = 1.0;
291 for i in 1..n - 1 {
292 a_diag[i] = -r;
293 b_diag[i] = 1.0 + 2.0 * r;
294 c_diag[i] = -r;
295 }
296
297 if let BoundaryCondition::Dirichlet(v) = bc_left {
299 rhs[0] = v;
300 }
301 if let BoundaryCondition::Dirichlet(v) = bc_right {
302 rhs[n - 1] = v;
303 }
304
305 let mut sol = thomas_algorithm(&a_diag, &b_diag, &c_diag, &rhs);
306 apply_bc_1d(&mut sol, bc_left, bc_right);
307 sol
308}
309
310fn weno5_beta(v0: f64, v1: f64, v2: f64) -> f64 {
318 let d1 = v1 - v0;
319 let d2 = v2 - 2.0 * v1 + v0;
320 13.0 / 12.0 * d2 * d2 + 0.25 * (3.0 * v1 - 4.0 * v0 + v2).powi(2) - 0.25 * (v1 - v0).powi(2)
321 + 0.25 * d1 * d1
322}
323
324pub fn weno5_flux(v: [f64; 5]) -> f64 {
328 let eps = 1e-36_f64;
329
330 let q0 = (2.0 * v[0] - 7.0 * v[1] + 11.0 * v[2]) / 6.0;
332 let q1 = (-v[1] + 5.0 * v[2] + 2.0 * v[3]) / 6.0;
333 let q2 = (2.0 * v[2] + 5.0 * v[3] - v[4]) / 6.0;
334
335 let b0 = 13.0 / 12.0 * (v[0] - 2.0 * v[1] + v[2]).powi(2)
337 + 0.25 * (v[0] - 4.0 * v[1] + 3.0 * v[2]).powi(2);
338 let b1 = 13.0 / 12.0 * (v[1] - 2.0 * v[2] + v[3]).powi(2) + 0.25 * (v[1] - v[3]).powi(2);
339 let b2 = 13.0 / 12.0 * (v[2] - 2.0 * v[3] + v[4]).powi(2)
340 + 0.25 * (3.0 * v[2] - 4.0 * v[3] + v[4]).powi(2);
341
342 let d0 = 1.0 / 10.0;
343 let d1 = 6.0 / 10.0;
344 let d2 = 3.0 / 10.0;
345
346 let w0 = d0 / (eps + b0).powi(2);
347 let w1 = d1 / (eps + b1).powi(2);
348 let w2 = d2 / (eps + b2).powi(2);
349 let wsum = w0 + w1 + w2;
350
351 (w0 * q0 + w1 * q1 + w2 * q2) / wsum
352}
353
354pub fn advection_weno5_step(u: &[f64], dx: f64, dt: f64, c: f64) -> Vec<f64> {
359 let n = u.len();
360 let mut un = u.to_vec();
361
362 let pad = 3_usize;
364 let mut up = vec![0.0_f64; n + 2 * pad];
365 for i in 0..pad {
366 up[i] = u[0];
367 up[n + pad + i] = u[n - 1];
368 }
369 up[pad..pad + n].copy_from_slice(u);
370
371 for i in 0..n {
372 let ip = i + pad; let flux_right = if c > 0.0 {
374 weno5_flux([up[ip - 2], up[ip - 1], up[ip], up[ip + 1], up[ip + 2]])
375 } else {
376 weno5_flux([up[ip + 3], up[ip + 2], up[ip + 1], up[ip], up[ip - 1]])
377 };
378 let flux_left = if c > 0.0 {
379 weno5_flux([up[ip - 3], up[ip - 2], up[ip - 1], up[ip], up[ip + 1]])
380 } else {
381 weno5_flux([up[ip + 2], up[ip + 1], up[ip], up[ip - 1], up[ip - 2]])
382 };
383 un[i] = u[i] - c * dt / dx * (flux_right - flux_left);
384 }
385 un
386}
387
388pub fn compact_first_derivative(u: &[f64], dx: f64) -> Vec<f64> {
397 let n = u.len();
398 if n < 3 {
399 return first_derivative_central(u, dx);
400 }
401
402 let mut rhs = vec![0.0_f64; n];
403 rhs[0] = (u[1] - u[0]) / dx;
404 rhs[n - 1] = (u[n - 1] - u[n - 2]) / dx;
405 for i in 1..n - 1 {
406 rhs[i] = (3.0 / 2.0) * (u[i + 1] - u[i - 1]) / (2.0 * dx);
407 }
408
409 let mut a_d = vec![0.0_f64; n];
410 let b_d = vec![1.0_f64; n];
411 let mut c_d = vec![0.0_f64; n];
412 for i in 1..n - 1 {
413 a_d[i] = 1.0 / 4.0;
414 c_d[i] = 1.0 / 4.0;
415 }
416
417 thomas_algorithm(&a_d, &b_d, &c_d, &rhs)
418}
419
420pub fn richardson_extrapolation(f_h: f64, f_h2: f64, order: u32) -> f64 {
429 let r = (2_u64.pow(order)) as f64;
430 (r * f_h2 - f_h) / (r - 1.0)
431}
432
433pub fn richardson_derivative(u_coarse: &[f64], u_fine: &[f64], dx: f64, order: u32) -> Vec<f64> {
438 let nc = u_coarse.len();
439 let _nf = u_fine.len();
440 let dc = first_derivative_central(u_coarse, dx);
441 let df_on_coarse: Vec<f64> = (0..nc)
442 .map(|i| {
443 let if_ = i * 2;
444 let nf = u_fine.len();
445 if if_ > 0 && if_ < nf - 1 {
446 (u_fine[if_ + 1] - u_fine[if_ - 1]) / (2.0 * dx / 2.0)
447 } else {
448 dc[i]
449 }
450 })
451 .collect();
452
453 dc.iter()
454 .zip(df_on_coarse.iter())
455 .map(|(&fh, &fh2)| richardson_extrapolation(fh, fh2, order))
456 .collect()
457}
458
459pub fn cfl_number(c: f64, dt: f64, dx: f64) -> f64 {
467 c.abs() * dt / dx
468}
469
470pub fn von_neumann_diffusion_number(alpha: f64, dt: f64, dx: f64) -> f64 {
474 alpha * dt / (dx * dx)
475}
476
477pub fn is_advection_stable(c: f64, dt: f64, dx: f64) -> bool {
479 cfl_number(c, dt, dx) <= 1.0
480}
481
482pub fn is_diffusion_stable(alpha: f64, dt: f64, dx: f64) -> bool {
484 von_neumann_diffusion_number(alpha, dt, dx) <= 0.5
485}
486
487pub fn max_dt_advection(c: f64, dx: f64) -> f64 {
489 dx / c.abs().max(f64::EPSILON)
490}
491
492pub fn max_dt_diffusion(alpha: f64, dx: f64) -> f64 {
494 dx * dx / (2.0 * alpha.abs().max(f64::EPSILON))
495}
496
497pub fn mol_diffusion_rhs(u: &[f64], dx: f64, alpha: f64) -> Vec<f64> {
506 second_derivative(u, dx)
507 .iter()
508 .map(|&v| alpha * v)
509 .collect()
510}
511
512pub fn mol_advection_rhs(u: &[f64], dx: f64, c: f64) -> Vec<f64> {
516 let n = u.len();
517 let mut rhs = vec![0.0_f64; n];
518 for i in 1..n - 1 {
519 let dudx = if c > 0.0 {
520 (u[i] - u[i - 1]) / dx
521 } else {
522 (u[i + 1] - u[i]) / dx
523 };
524 rhs[i] = -c * dudx;
525 }
526 rhs
527}
528
529pub fn rk4_step<F>(u: &[f64], t: f64, dt: f64, f: &F) -> Vec<f64>
533where
534 F: Fn(f64, &[f64]) -> Vec<f64>,
535{
536 let n = u.len();
537 let k1 = f(t, u);
538 let u2: Vec<f64> = (0..n).map(|i| u[i] + 0.5 * dt * k1[i]).collect();
539 let k2 = f(t + 0.5 * dt, &u2);
540 let u3: Vec<f64> = (0..n).map(|i| u[i] + 0.5 * dt * k2[i]).collect();
541 let k3 = f(t + 0.5 * dt, &u3);
542 let u4: Vec<f64> = (0..n).map(|i| u[i] + dt * k3[i]).collect();
543 let k4 = f(t + dt, &u4);
544 (0..n)
545 .map(|i| u[i] + dt / 6.0 * (k1[i] + 2.0 * k2[i] + 2.0 * k3[i] + k4[i]))
546 .collect()
547}
548
549#[derive(Debug, Clone)]
557pub struct StaggeredGrid1D {
558 pub u: Vec<f64>,
560 pub flux: Vec<f64>,
562 pub dx: f64,
564}
565
566impl StaggeredGrid1D {
567 pub fn new(n: usize, dx: f64) -> Self {
569 Self {
570 u: vec![0.0_f64; n],
571 flux: vec![0.0_f64; n + 1],
572 dx,
573 }
574 }
575
576 pub fn interpolate_to_faces(&mut self) {
578 let n = self.u.len();
579 self.flux[0] = self.u[0];
580 self.flux[n] = self.u[n - 1];
581 for i in 1..n {
582 self.flux[i] = 0.5 * (self.u[i - 1] + self.u[i]);
583 }
584 }
585
586 pub fn divergence(&self) -> Vec<f64> {
588 let n = self.u.len();
589 (0..n)
590 .map(|i| (self.flux[i + 1] - self.flux[i]) / self.dx)
591 .collect()
592 }
593}
594
595#[derive(Debug, Clone)]
601pub struct FractionalStepResult {
602 pub u_star: Vec<f64>,
604 pub pressure: Vec<f64>,
606 pub u_new: Vec<f64>,
608}
609
610#[allow(clippy::too_many_arguments)]
616pub fn fractional_step_1d(
617 u: &[f64],
618 dx: f64,
619 dt: f64,
620 nu: f64,
621 body_force: f64,
622) -> FractionalStepResult {
623 let n = u.len();
624
625 let mut u_star = u.to_vec();
627 for i in 1..n - 1 {
628 let adv = u[i] * (u[i] - u[i - 1]) / dx;
629 let diff = nu * (u[i - 1] - 2.0 * u[i] + u[i + 1]) / (dx * dx);
630 u_star[i] = u[i] + dt * (-adv + diff + body_force);
631 }
632
633 let mut rhs_p = vec![0.0_f64; n];
635 for i in 1..n - 1 {
636 rhs_p[i] = (u_star[i + 1] - u_star[i - 1]) / (2.0 * dx) / dt;
637 }
638 let a_d: Vec<f64> = (0..n)
639 .map(|i| {
640 if i == 0 || i == n - 1 {
641 0.0
642 } else {
643 -1.0 / (dx * dx)
644 }
645 })
646 .collect();
647 let b_d: Vec<f64> = (0..n)
648 .map(|i| {
649 if i == 0 || i == n - 1 {
650 1.0
651 } else {
652 2.0 / (dx * dx)
653 }
654 })
655 .collect();
656 let c_d: Vec<f64> = a_d.clone();
657 let pressure = thomas_algorithm(&a_d, &b_d, &c_d, &rhs_p);
658
659 let mut u_new = u_star.clone();
661 for i in 1..n - 1 {
662 u_new[i] -= dt * (pressure[i + 1] - pressure[i - 1]) / (2.0 * dx);
663 }
664
665 FractionalStepResult {
666 u_star,
667 pressure,
668 u_new,
669 }
670}
671
672pub fn laplacian_2d(u: &[f64], nx: usize, ny: usize, dx: f64, dy: f64) -> Vec<f64> {
681 let mut lap = vec![0.0_f64; nx * ny];
682 for j in 1..ny - 1 {
683 for i in 1..nx - 1 {
684 let idx = j * nx + i;
685 let d2x = (u[idx - 1] - 2.0 * u[idx] + u[idx + 1]) / (dx * dx);
686 let d2y = (u[idx - nx] - 2.0 * u[idx] + u[idx + nx]) / (dy * dy);
687 lap[idx] = d2x + d2y;
688 }
689 }
690 lap
691}
692
693pub fn apply_dirichlet_2d(u: &mut [f64], nx: usize, ny: usize, value: f64) {
698 for i in 0..nx {
699 u[i] = value;
700 u[(ny - 1) * nx + i] = value;
701 }
702 for j in 0..ny {
703 u[j * nx] = value;
704 u[j * nx + nx - 1] = value;
705 }
706}
707
708pub fn diffusion_2d_explicit_step(
712 u: &[f64],
713 nx: usize,
714 ny: usize,
715 dx: f64,
716 dy: f64,
717 dt: f64,
718 alpha: f64,
719) -> Vec<f64> {
720 let mut un = u.to_vec();
721 let rx = alpha * dt / (dx * dx);
722 let ry = alpha * dt / (dy * dy);
723 for j in 1..ny - 1 {
724 for i in 1..nx - 1 {
725 let idx = j * nx + i;
726 un[idx] = u[idx]
727 + rx * (u[idx - 1] - 2.0 * u[idx] + u[idx + 1])
728 + ry * (u[idx - nx] - 2.0 * u[idx] + u[idx + nx]);
729 }
730 }
731 un
732}
733
734pub fn adi_diffusion_step_2d(
743 u: &[f64],
744 nx: usize,
745 ny: usize,
746 dx: f64,
747 dy: f64,
748 dt: f64,
749 alpha: f64,
750) -> Vec<f64> {
751 let rx = alpha * dt / (2.0 * dx * dx);
752 let ry = alpha * dt / (2.0 * dy * dy);
753 let mut u_half = u.to_vec();
754
755 for j in 1..ny - 1 {
757 let mut a_d = vec![0.0_f64; nx];
758 let mut b_d = vec![0.0_f64; nx];
759 let mut c_d = vec![0.0_f64; nx];
760 let mut rhs = vec![0.0_f64; nx];
761
762 b_d[0] = 1.0;
763 b_d[nx - 1] = 1.0;
764 rhs[0] = 0.0;
766 rhs[nx - 1] = 0.0;
767
768 for i in 1..nx - 1 {
769 let idx = j * nx + i;
770 a_d[i] = -rx;
771 b_d[i] = 1.0 + 2.0 * rx;
772 c_d[i] = -rx;
773 rhs[i] = ry * u[idx - nx] + (1.0 - 2.0 * ry) * u[idx] + ry * u[idx + nx];
774 }
775
776 let row_sol = thomas_algorithm(&a_d, &b_d, &c_d, &rhs);
777 for i in 0..nx {
778 u_half[j * nx + i] = row_sol[i];
779 }
780 }
781
782 let mut u_new = u_half.clone();
784 for i in 1..nx - 1 {
785 let mut a_d = vec![0.0_f64; ny];
786 let mut b_d = vec![0.0_f64; ny];
787 let mut c_d = vec![0.0_f64; ny];
788 let mut rhs = vec![0.0_f64; ny];
789
790 b_d[0] = 1.0;
791 b_d[ny - 1] = 1.0;
792 rhs[0] = 0.0;
793 rhs[ny - 1] = 0.0;
794
795 for j in 1..ny - 1 {
796 let idx = j * nx + i;
797 a_d[j] = -ry;
798 b_d[j] = 1.0 + 2.0 * ry;
799 c_d[j] = -ry;
800 rhs[j] = rx * u_half[idx - 1] + (1.0 - 2.0 * rx) * u_half[idx] + rx * u_half[idx + 1];
801 }
802
803 let col_sol = thomas_algorithm(&a_d, &b_d, &c_d, &rhs);
804 for j in 0..ny {
805 u_new[j * nx + i] = col_sol[j];
806 }
807 }
808
809 u_new
810}
811
812pub fn laplacian_3d(
821 u: &[f64],
822 nx: usize,
823 ny: usize,
824 nz: usize,
825 dx: f64,
826 dy: f64,
827 dz: f64,
828) -> Vec<f64> {
829 let mut lap = vec![0.0_f64; nx * ny * nz];
830 for k in 1..nz - 1 {
831 for j in 1..ny - 1 {
832 for i in 1..nx - 1 {
833 let idx = k * ny * nx + j * nx + i;
834 let d2x = (u[idx - 1] - 2.0 * u[idx] + u[idx + 1]) / (dx * dx);
835 let d2y = (u[idx - nx] - 2.0 * u[idx] + u[idx + nx]) / (dy * dy);
836 let d2z = (u[idx - ny * nx] - 2.0 * u[idx] + u[idx + ny * nx]) / (dz * dz);
837 lap[idx] = d2x + d2y + d2z;
838 }
839 }
840 }
841 lap
842}
843
844pub fn advection_periodic_step(u: &[f64], dx: f64, dt: f64, c: f64) -> Vec<f64> {
852 let n = u.len();
853 let r = c * dt / dx;
854 let mut un = vec![0.0_f64; n];
855 for i in 0..n {
856 let im1 = if i == 0 { n - 1 } else { i - 1 };
857 let ip1 = (i + 1) % n;
858 un[i] = if c > 0.0 {
859 u[i] - r * (u[i] - u[im1])
860 } else {
861 u[i] - r * (u[ip1] - u[i])
862 };
863 }
864 un
865}
866
867pub fn l2_norm(u: &[f64], dx: f64) -> f64 {
873 (dx * u.iter().map(|v| v * v).sum::<f64>()).sqrt()
874}
875
876pub fn linf_norm(u: &[f64]) -> f64 {
878 u.iter().cloned().fold(0.0_f64, f64::max)
879}
880
881pub fn l2_error(computed: &[f64], exact: &[f64], dx: f64) -> f64 {
883 let err: Vec<f64> = computed
884 .iter()
885 .zip(exact.iter())
886 .map(|(c, e)| c - e)
887 .collect();
888 l2_norm(&err, dx)
889}
890
891pub fn ftcs_amplification(r: f64, k: f64, dx: f64) -> f64 {
900 let s = (k * dx / 2.0).sin();
901 (1.0 - 4.0 * r * s * s).abs()
902}
903
904pub fn ftcs_max_amplification(r: f64, dx: f64, n_modes: usize) -> f64 {
908 (0..=n_modes)
909 .map(|m| {
910 let k = m as f64 * PI / (n_modes as f64 * dx);
911 ftcs_amplification(r, k, dx)
912 })
913 .fold(0.0_f64, f64::max)
914}
915
916pub fn apply_neumann_reflection(u: &mut Vec<f64>) {
925 let n = u.len();
926 if n < 3 {
927 return;
928 }
929 u[0] = u[1];
930 u[n - 1] = u[n - 2];
931}
932
933pub fn uniform_grid_1d(x_start: f64, x_end: f64, n: usize) -> Vec<f64> {
939 let dx = (x_end - x_start) / (n - 1) as f64;
940 (0..n).map(|i| x_start + i as f64 * dx).collect()
941}
942
943pub fn uniform_grid_2d(
947 x0: f64,
948 x1: f64,
949 nx: usize,
950 y0: f64,
951 y1: f64,
952 ny: usize,
953) -> (Vec<f64>, Vec<f64>) {
954 let dx = (x1 - x0) / (nx - 1) as f64;
955 let dy = (y1 - y0) / (ny - 1) as f64;
956 let mut xs = vec![0.0_f64; nx * ny];
957 let mut ys = vec![0.0_f64; nx * ny];
958 for j in 0..ny {
959 for i in 0..nx {
960 xs[j * nx + i] = x0 + i as f64 * dx;
961 ys[j * nx + i] = y0 + j as f64 * dy;
962 }
963 }
964 (xs, ys)
965}
966
967pub fn advection_lax_wendroff_step(u: &[f64], dx: f64, dt: f64, c: f64) -> Vec<f64> {
975 let n = u.len();
976 let r = c * dt / dx;
977 let mut un = u.to_vec();
978 for i in 1..n - 1 {
979 un[i] = u[i] - 0.5 * r * (u[i + 1] - u[i - 1])
980 + 0.5 * r * r * (u[i + 1] - 2.0 * u[i] + u[i - 1]);
981 }
982 un
983}
984
985pub fn advection_beam_warming_step(u: &[f64], dx: f64, dt: f64, c: f64) -> Vec<f64> {
993 let n = u.len();
994 let r = c * dt / dx;
995 let mut un = u.to_vec();
996 for i in 2..n {
997 un[i] = u[i] - 0.5 * r * (3.0 * u[i] - 4.0 * u[i - 1] + u[i - 2])
998 + 0.5 * r * r * (u[i] - 2.0 * u[i - 1] + u[i - 2]);
999 }
1000 un
1001}
1002
1003pub fn compact_second_derivative(u: &[f64], dx: f64) -> Vec<f64> {
1011 let n = u.len();
1012 if n < 3 {
1013 return second_derivative(u, dx);
1014 }
1015
1016 let mut rhs = vec![0.0_f64; n];
1017 rhs[0] = (u[1] - 2.0 * u[0] + u[0]) / (dx * dx); rhs[n - 1] = (u[n - 1] - 2.0 * u[n - 1] + u[n - 2]) / (dx * dx);
1019 for i in 1..n - 1 {
1020 rhs[i] = (12.0 / 10.0) * (u[i + 1] - 2.0 * u[i] + u[i - 1]) / (dx * dx);
1021 }
1022
1023 let mut a_d = vec![0.0_f64; n];
1024 let b_d = vec![1.0_f64; n];
1025 let mut c_d = vec![0.0_f64; n];
1026 for i in 1..n - 1 {
1027 a_d[i] = 1.0 / 10.0;
1028 c_d[i] = 1.0 / 10.0;
1029 }
1030
1031 thomas_algorithm(&a_d, &b_d, &c_d, &rhs)
1032}
1033
1034pub fn wave_equation_step(u: &[f64], u_prev: &[f64], dx: f64, dt: f64, c: f64) -> Vec<f64> {
1042 let n = u.len();
1043 let r2 = (c * dt / dx).powi(2);
1044 let mut u_next = u.to_vec();
1045 for i in 1..n - 1 {
1046 u_next[i] = 2.0 * u[i] - u_prev[i] + r2 * (u[i - 1] - 2.0 * u[i] + u[i + 1]);
1047 }
1048 u_next
1049}
1050
1051pub fn total_variation(u: &[f64]) -> f64 {
1057 u.windows(2).map(|w| (w[1] - w[0]).abs()).sum()
1058}
1059
1060pub fn l1_norm(u: &[f64], dx: f64) -> f64 {
1062 dx * u.iter().map(|v| v.abs()).sum::<f64>()
1063}
1064
1065pub fn weno5_min_smoothness(u: &[f64]) -> f64 {
1071 let n = u.len();
1072 if n < 5 {
1073 return 0.0;
1074 }
1075 let mut min_b = f64::MAX;
1076 for i in 2..n - 2 {
1077 let b = weno5_beta(u[i - 1], u[i], u[i + 1]);
1078 if b < min_b {
1079 min_b = b;
1080 }
1081 }
1082 min_b
1083}
1084
1085#[cfg(test)]
1090mod tests {
1091 use super::*;
1092
1093 #[test]
1096 fn test_thomas_identity_system() {
1097 let a = vec![0.0, 0.0, 0.0];
1099 let b = vec![1.0, 1.0, 1.0];
1100 let c = vec![0.0, 0.0, 0.0];
1101 let d = vec![2.0, 3.0, 4.0];
1102 let x = thomas_algorithm(&a, &b, &c, &d);
1103 assert!((x[0] - 2.0).abs() < 1e-10);
1104 assert!((x[1] - 3.0).abs() < 1e-10);
1105 assert!((x[2] - 4.0).abs() < 1e-10);
1106 }
1107
1108 #[test]
1109 fn test_thomas_simple_tridiagonal() {
1110 let a = vec![0.0, -1.0];
1112 let b = vec![2.0, 2.0];
1113 let c = vec![-1.0, 0.0];
1114 let d = vec![1.0, 1.0];
1115 let x = thomas_algorithm(&a, &b, &c, &d);
1116 assert!((x[0] - 1.0).abs() < 1e-10);
1117 assert!((x[1] - 1.0).abs() < 1e-10);
1118 }
1119
1120 #[test]
1121 fn test_thomas_larger_system() {
1122 let n = 5_usize;
1123 let a: Vec<f64> = (0..n).map(|i| if i == 0 { 0.0 } else { -1.0 }).collect();
1125 let b = vec![4.0_f64; n];
1126 let c: Vec<f64> = (0..n)
1127 .map(|i| if i == n - 1 { 0.0 } else { -1.0 })
1128 .collect();
1129 let d = vec![1.0_f64; n];
1130 let x = thomas_algorithm(&a, &b, &c, &d);
1131 for i in 0..n {
1133 let lhs = b[i] * x[i]
1134 + if i > 0 { a[i] * x[i - 1] } else { 0.0 }
1135 + if i < n - 1 { c[i] * x[i + 1] } else { 0.0 };
1136 assert!((lhs - d[i]).abs() < 1e-9, "row {i}: lhs={lhs}");
1137 }
1138 }
1139
1140 #[test]
1143 fn test_first_derivative_linear() {
1144 let n = 10;
1146 let dx = 0.1;
1147 let u: Vec<f64> = (0..n).map(|i| i as f64 * dx).collect();
1148 let du = first_derivative_central(&u, dx);
1149 for v in &du {
1150 assert!((v - 1.0).abs() < 1e-9, "got {v}");
1151 }
1152 }
1153
1154 #[test]
1155 fn test_second_derivative_quadratic() {
1156 let n = 20;
1158 let dx = 0.1;
1159 let u: Vec<f64> = (0..n).map(|i| (i as f64 * dx).powi(2)).collect();
1160 let d2u = second_derivative(&u, dx);
1161 for i in 1..n - 1 {
1162 assert!((d2u[i] - 2.0).abs() < 1e-6, "i={i}: {}", d2u[i]);
1163 }
1164 }
1165
1166 #[test]
1167 fn test_fourth_order_derivative_sine() {
1168 let n = 100;
1170 let dx = 2.0 * PI / n as f64;
1171 let u: Vec<f64> = (0..n).map(|i| (i as f64 * dx).sin()).collect();
1172 let du = first_derivative_fourth_order(&u, dx);
1173 for i in 4..n - 4 {
1175 let exact = (i as f64 * dx).cos();
1176 assert!(
1177 (du[i] - exact).abs() < 1e-6,
1178 "i={i}: got {} exp {}",
1179 du[i],
1180 exact
1181 );
1182 }
1183 }
1184
1185 #[test]
1188 fn test_upwind_advects_rightward() {
1189 let n = 20;
1190 let dx = 1.0 / n as f64;
1191 let dt = 0.4 * dx; let c = 1.0;
1193 let mut u: Vec<f64> = (0..n).map(|i| if i == n / 2 { 1.0 } else { 0.0 }).collect();
1194 let bc = BoundaryCondition::Dirichlet(0.0);
1195 u = advection_upwind_step(&u, dx, dt, c, bc, bc);
1196 let peak_idx = u
1198 .iter()
1199 .enumerate()
1200 .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
1201 .map(|(i, _)| i)
1202 .unwrap();
1203 assert!(peak_idx >= n / 2);
1204 }
1205
1206 #[test]
1207 fn test_cfl_number_correct() {
1208 assert!((cfl_number(2.0, 0.5, 1.0) - 1.0).abs() < 1e-12);
1209 assert!((cfl_number(1.0, 0.1, 1.0) - 0.1).abs() < 1e-12);
1210 }
1211
1212 #[test]
1213 fn test_stability_flags() {
1214 assert!(is_advection_stable(1.0, 0.9, 1.0));
1215 assert!(!is_advection_stable(1.0, 1.1, 1.0));
1216 assert!(is_diffusion_stable(0.1, 0.4, 1.0));
1217 assert!(!is_diffusion_stable(0.1, 6.0, 1.0));
1218 }
1219
1220 #[test]
1223 fn test_crank_nicolson_preserves_constant() {
1224 let n = 10;
1226 let dx = 0.1;
1227 let dt = 1.0;
1228 let alpha = 0.01;
1229 let u = vec![3.0_f64; n];
1230 let bc = BoundaryCondition::Dirichlet(3.0);
1231 let un = diffusion_crank_nicolson_step(&u, dx, dt, alpha, bc, bc);
1232 for v in &un {
1233 assert!((v - 3.0).abs() < 1e-9, "got {v}");
1234 }
1235 }
1236
1237 #[test]
1238 fn test_implicit_diffusion_stable_large_dt() {
1239 let n = 10;
1241 let dx = 0.1;
1242 let dt = 100.0; let alpha = 0.01;
1244 let mut u = vec![0.0_f64; n];
1245 u[5] = 1.0;
1246 let bc = BoundaryCondition::Dirichlet(0.0);
1247 let un = diffusion_implicit_step(&u, dx, dt, alpha, bc, bc);
1248 for v in &un {
1250 assert!(v.is_finite(), "got NaN/inf");
1251 }
1252 }
1253
1254 #[test]
1255 fn test_cn_dirichlet_boundary_held() {
1256 let n = 8;
1257 let dx = 0.1;
1258 let dt = 0.1;
1259 let alpha = 0.1;
1260 let u: Vec<f64> = (0..n).map(|i| i as f64).collect();
1261 let bc_l = BoundaryCondition::Dirichlet(0.0);
1262 let bc_r = BoundaryCondition::Dirichlet(7.0);
1263 let un = diffusion_crank_nicolson_step(&u, dx, dt, alpha, bc_l, bc_r);
1264 assert!((un[0] - 0.0).abs() < 1e-10);
1265 assert!((un[n - 1] - 7.0).abs() < 1e-10);
1266 }
1267
1268 #[test]
1271 fn test_explicit_diffusion_gaussian_decay() {
1272 let n = 50;
1274 let dx = 1.0 / n as f64;
1275 let dt = 0.4 * dx * dx / 0.01; let alpha = 0.01;
1277 let sigma = 5.0 * dx;
1278 let mid = n as f64 / 2.0 * dx;
1279 let u: Vec<f64> = (0..n)
1280 .map(|i| {
1281 let x = i as f64 * dx - mid;
1282 (-0.5 * x * x / (sigma * sigma)).exp()
1283 })
1284 .collect();
1285 let bc = BoundaryCondition::Dirichlet(0.0);
1286 let un = diffusion_explicit_step(&u, dx, dt, alpha, bc, bc);
1287 let peak_old = u.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1288 let peak_new = un.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
1289 assert!(peak_new < peak_old, "Gaussian peak should decrease");
1290 }
1291
1292 #[test]
1295 fn test_weno5_flux_smooth_region() {
1296 let v = [1.0_f64; 5];
1298 let f = weno5_flux(v);
1299 assert!((f - 1.0).abs() < 1e-10, "got {f}");
1300 }
1301
1302 #[test]
1303 fn test_weno5_advection_conserves_mass() {
1304 let n = 40;
1305 let dx = 1.0 / n as f64;
1306 let dt = 0.3 * dx;
1307 let c = 1.0;
1308 let u: Vec<f64> = (0..n)
1309 .map(|i| ((i as f64 * dx - 0.3) * 20.0 * PI).sin().powi(2) * 0.5)
1310 .collect();
1311 let mass_before: f64 = u.iter().sum::<f64>() * dx;
1312 let un = advection_weno5_step(&u, dx, dt, c);
1313 let mass_after: f64 = un.iter().sum::<f64>() * dx;
1314 assert!(
1316 (mass_before - mass_after).abs() < 0.20 * mass_before.abs().max(1e-6),
1317 "mass_before={mass_before}, mass_after={mass_after}"
1318 );
1319 }
1320
1321 #[test]
1324 fn test_compact_derivative_linear() {
1325 let n = 10;
1326 let dx = 0.1;
1327 let u: Vec<f64> = (0..n).map(|i| 3.0 * i as f64 * dx).collect();
1328 let du = compact_first_derivative(&u, dx);
1329 for i in 2..n - 2 {
1331 assert!((du[i] - 3.0).abs() < 1e-8, "i={i}: {}", du[i]);
1332 }
1333 }
1334
1335 #[test]
1338 fn test_richardson_second_order() {
1339 let h = 0.1;
1341 let f_h = 1.0 + h * h;
1342 let f_h2 = 1.0 + h * h / 4.0;
1343 let ext = richardson_extrapolation(f_h, f_h2, 2);
1344 assert!((ext - 1.0).abs() < 1e-10, "got {ext}");
1345 }
1346
1347 #[test]
1350 fn test_staggered_grid_divergence_constant_flux() {
1351 let n = 10;
1352 let dx = 0.1;
1353 let mut sg = StaggeredGrid1D::new(n, dx);
1354 sg.u = vec![1.0_f64; n];
1356 sg.interpolate_to_faces();
1357 let div = sg.divergence();
1358 for v in &div {
1359 assert!(v.abs() < 1e-12, "got {v}");
1360 }
1361 }
1362
1363 #[test]
1364 fn test_staggered_grid_face_count() {
1365 let sg = StaggeredGrid1D::new(5, 0.1);
1366 assert_eq!(sg.flux.len(), 6);
1367 assert_eq!(sg.u.len(), 5);
1368 }
1369
1370 #[test]
1373 fn test_fractional_step_returns_finite() {
1374 let n = 20;
1375 let u: Vec<f64> = (0..n).map(|i| (i as f64 / n as f64).sin()).collect();
1376 let res = fractional_step_1d(&u, 1.0 / n as f64, 0.001, 0.01, 0.0);
1377 for v in &res.u_new {
1378 assert!(v.is_finite(), "NaN/inf in u_new");
1379 }
1380 }
1381
1382 #[test]
1385 fn test_laplacian_2d_constant_field() {
1386 let nx = 5;
1387 let ny = 5;
1388 let u = vec![2.0_f64; nx * ny];
1389 let lap = laplacian_2d(&u, nx, ny, 0.1, 0.1);
1390 for j in 1..ny - 1 {
1391 for i in 1..nx - 1 {
1392 assert!(lap[j * nx + i].abs() < 1e-12);
1393 }
1394 }
1395 }
1396
1397 #[test]
1398 fn test_laplacian_2d_parabolic() {
1399 let nx = 10;
1401 let ny = 10;
1402 let dx = 0.1;
1403 let dy = 0.1;
1404 let u: Vec<f64> = (0..ny)
1405 .flat_map(|j| {
1406 (0..nx).map(move |i| {
1407 let x = i as f64 * dx;
1408 let y = j as f64 * dy;
1409 x * x + y * y
1410 })
1411 })
1412 .collect();
1413 let lap = laplacian_2d(&u, nx, ny, dx, dy);
1414 for j in 1..ny - 1 {
1415 for i in 1..nx - 1 {
1416 assert!(
1417 (lap[j * nx + i] - 4.0).abs() < 1e-6,
1418 "({i},{j}): {}",
1419 lap[j * nx + i]
1420 );
1421 }
1422 }
1423 }
1424
1425 #[test]
1428 fn test_adi_constant_field_unchanged() {
1429 let nx = 6;
1430 let ny = 6;
1431 let u = vec![0.0_f64; nx * ny]; let un = adi_diffusion_step_2d(&u, nx, ny, 0.1, 0.1, 0.01, 0.01);
1433 for v in &un {
1434 assert!(v.abs() < 1e-12, "got {v}");
1435 }
1436 }
1437
1438 #[test]
1439 fn test_adi_energy_decreases() {
1440 let nx = 8;
1441 let ny = 8;
1442 let mut u = vec![0.0_f64; nx * ny];
1443 u[ny / 2 * nx + nx / 2] = 1.0; let energy_before: f64 = u.iter().map(|v| v * v).sum();
1445 let un = adi_diffusion_step_2d(&u, nx, ny, 0.125, 0.125, 0.01, 0.01);
1446 let energy_after: f64 = un.iter().map(|v| v * v).sum();
1447 assert!(energy_after < energy_before, "Energy should decrease");
1448 }
1449
1450 #[test]
1453 fn test_wave_equation_step_finite() {
1454 let n = 30;
1455 let dx = 1.0 / n as f64;
1456 let dt = 0.4 * dx;
1457 let c = 1.0;
1458 let u: Vec<f64> = (0..n).map(|i| (PI * i as f64 * dx).sin()).collect();
1459 let u_prev = u.clone();
1460 let u_next = wave_equation_step(&u, &u_prev, dx, dt, c);
1461 for v in &u_next {
1462 assert!(v.is_finite(), "NaN/inf in wave step");
1463 }
1464 }
1465
1466 #[test]
1469 fn test_periodic_advection_conserves_mass() {
1470 let n = 32;
1471 let dx = 1.0 / n as f64;
1472 let dt = 0.4 * dx;
1473 let c = 1.0;
1474 let u: Vec<f64> = (0..n)
1475 .map(|i| (2.0 * PI * i as f64 / n as f64).sin())
1476 .collect();
1477 let mass0: f64 = u.iter().sum();
1478 let un = advection_periodic_step(&u, dx, dt, c);
1479 let mass1: f64 = un.iter().sum();
1480 assert!(
1481 (mass0 - mass1).abs() < 1e-10,
1482 "mass change = {}",
1483 (mass0 - mass1).abs()
1484 );
1485 }
1486
1487 #[test]
1490 fn test_l2_norm_constant() {
1491 let n = 10;
1492 let dx = 0.1;
1493 let u = vec![2.0_f64; n];
1494 let norm = l2_norm(&u, dx);
1495 let expected = (dx * n as f64 * 4.0).sqrt();
1496 assert!((norm - expected).abs() < 1e-10);
1497 }
1498
1499 #[test]
1500 fn test_linf_norm() {
1501 let u = vec![-3.0_f64, 1.0, 2.0, -1.0, 2.5];
1502 assert!((linf_norm(&u) - 2.5).abs() < 1e-12);
1503 }
1504
1505 #[test]
1508 fn test_total_variation_monotone() {
1509 let u: Vec<f64> = (0..10).map(|i| i as f64).collect();
1510 let tv = total_variation(&u);
1511 assert!((tv - 9.0).abs() < 1e-12);
1512 }
1513
1514 #[test]
1517 fn test_ftcs_amplification_r_half_stable() {
1518 let r = 0.5;
1519 let dx = 0.1;
1520 let max_g = ftcs_max_amplification(r, dx, 200);
1521 assert!(max_g <= 1.0 + 1e-10, "unstable: max|G|={max_g}");
1522 }
1523
1524 #[test]
1525 fn test_ftcs_amplification_r_too_large() {
1526 let r = 0.6; let dx = 0.1;
1528 let max_g = ftcs_max_amplification(r, dx, 200);
1529 assert!(max_g > 1.0, "|G| should exceed 1.0 for r=0.6, got {max_g}");
1530 }
1531
1532 #[test]
1535 fn test_neumann_reflection_zero_gradient() {
1536 let mut u = vec![1.0, 2.0, 3.0, 4.0, 5.0];
1537 apply_neumann_reflection(&mut u);
1538 assert_eq!(u[0], u[1]);
1539 assert_eq!(u[4], u[3]);
1540 }
1541
1542 #[test]
1545 fn test_uniform_grid_endpoints() {
1546 let grid = uniform_grid_1d(0.0, 1.0, 11);
1547 assert!((grid[0] - 0.0).abs() < 1e-12);
1548 assert!((grid[10] - 1.0).abs() < 1e-12);
1549 assert_eq!(grid.len(), 11);
1550 }
1551
1552 #[test]
1553 fn test_uniform_grid_2d_dimensions() {
1554 let (xs, ys) = uniform_grid_2d(0.0, 1.0, 5, 0.0, 2.0, 4);
1555 assert_eq!(xs.len(), 20);
1556 assert_eq!(ys.len(), 20);
1557 }
1558
1559 #[test]
1562 fn test_lax_wendroff_cfl_one_exact() {
1563 let n = 20;
1565 let dx = 1.0 / n as f64;
1566 let c = 1.0;
1567 let dt = dx; let u: Vec<f64> = (0..n).map(|i| (2.0 * PI * i as f64 * dx).sin()).collect();
1569 let expected: Vec<f64> = (0..n)
1570 .map(|i| (2.0 * PI * ((i as f64 * dx) - c * dt)).sin())
1571 .collect();
1572 let un = advection_lax_wendroff_step(&u, dx, dt, c);
1573 for i in 1..n - 1 {
1574 assert!(
1575 (un[i] - expected[i]).abs() < 1e-8,
1576 "i={i}: {} vs {}",
1577 un[i],
1578 expected[i]
1579 );
1580 }
1581 }
1582
1583 #[test]
1586 fn test_mol_rk4_diffusion_finite() {
1587 let n = 20;
1588 let dx = 1.0 / n as f64;
1589 let dt = 0.1 * dx * dx;
1590 let alpha = 0.01;
1591 let u: Vec<f64> = (0..n).map(|i| (PI * i as f64 * dx).sin()).collect();
1592 let u_next = rk4_step(&u, 0.0, dt, &|_t, u| mol_diffusion_rhs(u, dx, alpha));
1593 for v in &u_next {
1594 assert!(v.is_finite(), "NaN/inf in RK4 diffusion");
1595 }
1596 }
1597
1598 #[test]
1601 fn test_laplacian_3d_constant_zero() {
1602 let nx = 5;
1603 let ny = 5;
1604 let nz = 5;
1605 let u = vec![0.0_f64; nx * ny * nz];
1606 let lap = laplacian_3d(&u, nx, ny, nz, 0.1, 0.1, 0.1);
1607 for v in &lap {
1608 assert!(v.abs() < 1e-12);
1609 }
1610 }
1611
1612 #[test]
1613 fn test_laplacian_3d_linear_field_zero() {
1614 let nx = 6;
1616 let ny = 6;
1617 let nz = 6;
1618 let dx = 0.1;
1619 let dy = 0.1;
1620 let dz = 0.1;
1621 let u: Vec<f64> = (0..nz)
1622 .flat_map(|k| {
1623 (0..ny).flat_map(move |j| {
1624 (0..nx).map(move |i| i as f64 * dx + j as f64 * dy + k as f64 * dz)
1625 })
1626 })
1627 .collect();
1628 let lap = laplacian_3d(&u, nx, ny, nz, dx, dy, dz);
1629 for k in 1..nz - 1 {
1630 for j in 1..ny - 1 {
1631 for i in 1..nx - 1 {
1632 let v = lap[k * ny * nx + j * nx + i];
1633 assert!(v.abs() < 1e-9, "({i},{j},{k}): {v}");
1634 }
1635 }
1636 }
1637 }
1638
1639 #[test]
1642 fn test_compact_second_derivative_constant() {
1643 let n = 10;
1644 let dx = 0.1;
1645 let u = vec![5.0_f64; n];
1646 let d2u = compact_second_derivative(&u, dx);
1647 for i in 1..n - 1 {
1648 assert!(d2u[i].abs() < 1e-9, "i={i}: {}", d2u[i]);
1649 }
1650 }
1651
1652 #[test]
1655 fn test_max_dt_advection() {
1656 let dt_max = max_dt_advection(2.0, 0.1);
1657 assert!((dt_max - 0.05).abs() < 1e-12);
1658 }
1659
1660 #[test]
1661 fn test_max_dt_diffusion() {
1662 let dt_max = max_dt_diffusion(0.1, 0.1);
1663 assert!((dt_max - 0.05).abs() < 1e-12);
1664 }
1665
1666 #[test]
1669 fn test_l1_norm_ones() {
1670 let u = vec![1.0_f64; 10];
1671 let norm = l1_norm(&u, 0.1);
1672 assert!((norm - 1.0).abs() < 1e-12);
1673 }
1674
1675 #[test]
1678 fn test_weno5_min_smoothness_zero_constant() {
1679 let u = vec![1.0_f64; 10];
1681 let b = weno5_min_smoothness(&u);
1682 assert!(b.abs() < 1e-12, "got {b}");
1683 }
1684
1685 #[test]
1688 fn test_apply_dirichlet_2d_border() {
1689 let nx = 5;
1690 let ny = 5;
1691 let mut u = vec![9.0_f64; nx * ny];
1692 apply_dirichlet_2d(&mut u, nx, ny, 0.0);
1693 for i in 0..nx {
1695 assert_eq!(u[i], 0.0);
1696 assert_eq!(u[(ny - 1) * nx + i], 0.0);
1697 }
1698 for j in 0..ny {
1699 assert_eq!(u[j * nx], 0.0);
1700 assert_eq!(u[j * nx + nx - 1], 0.0);
1701 }
1702 }
1703
1704 #[test]
1707 fn test_beam_warming_finite() {
1708 let n = 20;
1709 let dx = 1.0 / n as f64;
1710 let dt = 0.4 * dx;
1711 let u: Vec<f64> = (0..n).map(|i| (2.0 * PI * i as f64 * dx).sin()).collect();
1712 let un = advection_beam_warming_step(&u, dx, dt, 1.0);
1713 for v in &un {
1714 assert!(v.is_finite(), "NaN/inf");
1715 }
1716 }
1717}