1#![allow(clippy::needless_range_loop)]
2#[allow(dead_code)]
20#[derive(Debug, Clone)]
21pub struct CflCondition {
22 pub dx: f64,
24 pub dt: f64,
26 pub speed: f64,
28 pub diffusivity: f64,
30}
31
32impl CflCondition {
33 pub fn new(dx: f64, dt: f64, speed: f64, diffusivity: f64) -> Self {
35 Self {
36 dx,
37 dt,
38 speed,
39 diffusivity,
40 }
41 }
42
43 pub fn advective_cfl(&self) -> f64 {
45 self.speed * self.dt / self.dx
46 }
47
48 pub fn diffusive_cfl(&self) -> f64 {
50 self.diffusivity * self.dt / (self.dx * self.dx)
51 }
52
53 pub fn is_advection_stable(&self) -> bool {
55 self.advective_cfl() <= 1.0
56 }
57
58 pub fn is_diffusion_stable(&self) -> bool {
60 self.diffusive_cfl() <= 0.5
61 }
62}
63
64#[allow(dead_code)]
83pub fn heat_equation_1d(u0: &[f64], alpha: f64, dx: f64, dt: f64, n_steps: usize) -> Vec<f64> {
84 let n = u0.len();
85 let mut u = u0.to_vec();
86 let mut u_new = u.clone();
87 let r = alpha * dt / (dx * dx);
88
89 for _ in 0..n_steps {
90 for i in 1..n - 1 {
92 u_new[i] = u[i] + r * (u[i + 1] - 2.0 * u[i] + u[i - 1]);
93 }
94 u_new[0] = u[0];
96 u_new[n - 1] = u[n - 1];
97
98 std::mem::swap(&mut u, &mut u_new);
99 }
100 u
101}
102
103#[allow(dead_code)]
122pub fn wave_equation_1d(
123 u_prev: &[f64],
124 u_curr: &[f64],
125 c: f64,
126 dx: f64,
127 dt: f64,
128 n_steps: usize,
129) -> Vec<f64> {
130 let n = u_curr.len();
131 let mut prev = u_prev.to_vec();
132 let mut curr = u_curr.to_vec();
133 let mut next = vec![0.0_f64; n];
134 let r2 = (c * dt / dx).powi(2);
135
136 for _ in 0..n_steps {
137 for i in 1..n - 1 {
138 next[i] = 2.0 * curr[i] - prev[i] + r2 * (curr[i + 1] - 2.0 * curr[i] + curr[i - 1]);
139 }
140 next[0] = 0.0;
142 next[n - 1] = 0.0;
143
144 prev.copy_from_slice(&curr);
145 curr.copy_from_slice(&next);
146 }
147 curr
148}
149
150#[allow(dead_code)]
164pub fn poisson_1d(f: &[f64], dx: f64, bc_left: f64, bc_right: f64) -> Vec<f64> {
165 let n = f.len();
166 if n < 2 {
167 return vec![bc_left; n];
168 }
169 let dx2 = dx * dx;
170
171 let m = n - 2; if m == 0 {
175 return vec![bc_left, bc_right];
176 }
177
178 let mut rhs = vec![0.0_f64; m];
179 for i in 0..m {
180 rhs[i] = f[i + 1] * dx2;
181 }
182 rhs[0] += bc_left;
184 rhs[m - 1] += bc_right;
185
186 let mut c_star = vec![0.0_f64; m];
188 let mut d_star = vec![0.0_f64; m];
189
190 c_star[0] = -1.0 / 2.0;
192 d_star[0] = rhs[0] / 2.0;
193 for i in 1..m {
194 let denom = 2.0 + c_star[i - 1];
195 c_star[i] = -1.0 / denom;
196 d_star[i] = (rhs[i] + d_star[i - 1]) / denom;
197 }
198
199 let mut x = vec![0.0_f64; m];
201 x[m - 1] = d_star[m - 1];
202 for i in (0..m - 1).rev() {
203 x[i] = d_star[i] - c_star[i] * x[i + 1];
204 }
205
206 let mut u = vec![0.0_f64; n];
208 u[0] = bc_left;
209 u[n - 1] = bc_right;
210 u[1..m + 1].copy_from_slice(&x[..m]);
211 u
212}
213
214#[allow(dead_code)]
232pub fn advection_1d_upwind(u0: &[f64], c: f64, dx: f64, dt: f64, n_steps: usize) -> Vec<f64> {
233 let n = u0.len();
234 let mut u = u0.to_vec();
235 let mut u_new = u.clone();
236 let nu = c * dt / dx; for _ in 0..n_steps {
239 for i in 0..n {
240 if c >= 0.0 {
241 let im1 = if i == 0 { n - 1 } else { i - 1 };
243 u_new[i] = u[i] - nu * (u[i] - u[im1]);
244 } else {
245 let ip1 = if i == n - 1 { 0 } else { i + 1 };
247 u_new[i] = u[i] - nu * (u[ip1] - u[i]);
248 }
249 }
250 std::mem::swap(&mut u, &mut u_new);
251 }
252 u
253}
254
255#[allow(dead_code)]
270pub fn burger_equation_1d(u0: &[f64], dx: f64, dt: f64, n_steps: usize) -> Vec<f64> {
271 let n = u0.len();
272 let mut u = u0.to_vec();
273 let mut u_new = vec![0.0_f64; n];
274
275 for _ in 0..n_steps {
276 for i in 0..n {
277 let ip1 = (i + 1) % n;
278 let im1 = if i == 0 { n - 1 } else { i - 1 };
279
280 let f_right = godunov_flux_burgers(u[i], u[ip1]);
282 let f_left = godunov_flux_burgers(u[im1], u[i]);
284
285 u_new[i] = u[i] - (dt / dx) * (f_right - f_left);
286 }
287 std::mem::swap(&mut u, &mut u_new);
288 }
289 u
290}
291
292fn godunov_flux_burgers(u_l: f64, u_r: f64) -> f64 {
294 if u_l >= u_r {
295 let s = (u_l + u_r) / 2.0; if s >= 0.0 {
298 u_l * u_l / 2.0
299 } else {
300 u_r * u_r / 2.0
301 }
302 } else {
303 if u_l >= 0.0 {
305 u_l * u_l / 2.0
306 } else if u_r <= 0.0 {
307 u_r * u_r / 2.0
308 } else {
309 0.0 }
311 }
312}
313
314#[allow(dead_code)]
333pub fn diffusion_reaction_1d<F>(
334 u0: &[f64],
335 d: f64,
336 dx: f64,
337 dt: f64,
338 n_steps: usize,
339 reaction: F,
340) -> Vec<f64>
341where
342 F: Fn(f64, usize, f64) -> f64,
343{
344 let n = u0.len();
345 let mut u = u0.to_vec();
346 let mut u_new = u.clone();
347 let r = d * dt / (dx * dx);
348 let mut t = 0.0_f64;
349
350 for _ in 0..n_steps {
351 for i in 1..n - 1 {
352 let diffusion = r * (u[i + 1] - 2.0 * u[i] + u[i - 1]);
353 let react = reaction(u[i], i, t) * dt;
354 u_new[i] = u[i] + diffusion + react;
355 }
356 u_new[0] = u[0];
357 u_new[n - 1] = u[n - 1];
358
359 t += dt;
360 std::mem::swap(&mut u, &mut u_new);
361 }
362 u
363}
364
365#[cfg(test)]
370mod tests {
371 use super::*;
372
373 const EPS: f64 = 1e-10;
374
375 #[test]
378 fn test_cfl_advective_stable() {
379 let cfl = CflCondition::new(0.1, 0.05, 1.0, 0.0);
380 assert!(cfl.is_advection_stable());
382 assert!((cfl.advective_cfl() - 0.5).abs() < EPS);
383 }
384
385 #[test]
386 fn test_cfl_advective_unstable() {
387 let cfl = CflCondition::new(0.1, 0.2, 1.0, 0.0);
388 assert!(!cfl.is_advection_stable());
390 }
391
392 #[test]
393 fn test_cfl_diffusive_stable() {
394 let cfl = CflCondition::new(0.1, 0.001, 0.0, 0.1);
396 assert!(cfl.is_diffusion_stable());
397 }
398
399 #[test]
400 fn test_cfl_diffusive_unstable() {
401 let cfl = CflCondition::new(0.1, 1.0, 0.0, 1.0);
403 assert!(!cfl.is_diffusion_stable());
404 }
405
406 #[test]
409 fn test_heat_bc_fixed() {
410 let n = 20;
412 let u0: Vec<f64> = (0..n)
413 .map(|i| {
414 if i == 0 {
415 0.0
416 } else if i == n - 1 {
417 1.0
418 } else {
419 0.5
420 }
421 })
422 .collect();
423 let u = heat_equation_1d(&u0, 0.1, 0.1, 0.001, 100);
424 assert!((u[0] - u0[0]).abs() < EPS);
425 assert!((u[n - 1] - u0[n - 1]).abs() < EPS);
426 }
427
428 #[test]
429 fn test_heat_steady_state_linear() {
430 let n = 11;
432 let dx = 1.0 / (n - 1) as f64;
433 let mut u0 = vec![0.5_f64; n];
435 u0[0] = 0.0;
436 u0[n - 1] = 1.0;
437
438 let alpha = 0.5;
439 let dt = 0.4 * dx * dx / alpha; let u = heat_equation_1d(&u0, alpha, dx, dt, 5000);
441
442 for i in 1..n - 1 {
443 let x = i as f64 * dx;
444 assert!(
445 (u[i] - x).abs() < 1e-3,
446 "expected {x:.3} at node {i}, got {:.6}",
447 u[i]
448 );
449 }
450 }
451
452 #[test]
453 fn test_heat_conservation_insulated() {
454 let n = 50;
456 let u0: Vec<f64> = (0..n)
457 .map(|i| {
458 let x = i as f64 / (n - 1) as f64;
459 (std::f64::consts::PI * x).sin()
460 })
461 .collect();
462 let dx = 1.0 / (n - 1) as f64;
463 let alpha = 0.01;
464 let dt = 0.4 * dx * dx / alpha;
465 let u = heat_equation_1d(&u0, alpha, dx, dt, 100);
466 for val in &u {
468 assert!(val.is_finite());
469 }
470 }
471
472 #[test]
475 fn test_wave_zero_field_stays_zero() {
476 let n = 30;
477 let u_prev = vec![0.0_f64; n];
478 let u_curr = vec![0.0_f64; n];
479 let u = wave_equation_1d(&u_prev, &u_curr, 1.0, 0.1, 0.05, 10);
480 for v in &u {
481 assert!(v.abs() < EPS);
482 }
483 }
484
485 #[test]
486 fn test_wave_bc_fixed_zero() {
487 let n = 20;
488 let u_prev: Vec<f64> = (0..n).map(|i| (i as f64).sin()).collect();
489 let u_curr = u_prev.clone();
490 let u = wave_equation_1d(&u_prev, &u_curr, 1.0, 0.1, 0.05, 5);
491 assert!(u[0].abs() < EPS);
492 assert!(u[n - 1].abs() < EPS);
493 }
494
495 #[test]
496 fn test_wave_finite_values() {
497 let n = 40;
498 let dx = 1.0 / (n - 1) as f64;
499 let c = 1.0;
500 let dt = 0.9 * dx / c;
501 let u0: Vec<f64> = (0..n)
502 .map(|i| {
503 let x = i as f64 * dx;
504 (2.0 * std::f64::consts::PI * x).sin()
505 })
506 .collect();
507 let u = wave_equation_1d(&u0, &u0, c, dx, dt, 50);
508 for v in &u {
509 assert!(v.is_finite());
510 }
511 }
512
513 #[test]
516 fn test_poisson_bc_satisfied() {
517 let n = 10;
518 let f = vec![1.0_f64; n];
519 let u = poisson_1d(&f, 0.1, 0.0, 0.0);
520 assert!((u[0] - 0.0).abs() < EPS);
521 assert!((u[n - 1] - 0.0).abs() < EPS);
522 }
523
524 #[test]
525 fn test_poisson_zero_rhs_linear_solution() {
526 let n = 11;
528 let dx = 1.0 / (n - 1) as f64;
529 let f = vec![0.0_f64; n];
530 let u = poisson_1d(&f, dx, 0.0, 1.0);
531 for i in 0..n {
532 let x = i as f64 * dx;
533 assert!(
534 (u[i] - x).abs() < 1e-10,
535 "node {i}: expected {x}, got {}",
536 u[i]
537 );
538 }
539 }
540
541 #[test]
542 fn test_poisson_nonzero_rhs_symmetry() {
543 let n = 11;
545 let dx = 1.0 / (n - 1) as f64;
546 let f = vec![2.0_f64; n];
547 let u = poisson_1d(&f, dx, 0.0, 0.0);
548 for i in 0..n {
549 let x = i as f64 * dx;
550 let expected = x * (1.0 - x);
551 assert!(
552 (u[i] - expected).abs() < 1e-10,
553 "node {i}: expected {expected:.6}, got {:.6}",
554 u[i]
555 );
556 }
557 }
558
559 #[test]
560 fn test_poisson_two_node_trivial() {
561 let u = poisson_1d(&[0.0, 0.0], 1.0, 1.0, 2.0);
562 assert_eq!(u.len(), 2);
563 assert!((u[0] - 1.0).abs() < EPS);
564 assert!((u[1] - 2.0).abs() < EPS);
565 }
566
567 #[test]
570 fn test_advection_zero_field() {
571 let u0 = vec![0.0_f64; 20];
572 let u = advection_1d_upwind(&u0, 1.0, 0.1, 0.05, 10);
573 for v in &u {
574 assert!(v.abs() < EPS);
575 }
576 }
577
578 #[test]
579 fn test_advection_positive_speed_shifts_right() {
580 let n = 20;
582 let dx = 1.0 / n as f64;
583 let c = 1.0;
584 let dt = 0.5 * dx / c; let mut u0 = vec![0.0_f64; n];
586 u0[5] = 1.0;
587 let u = advection_1d_upwind(&u0, c, dx, dt, 4);
588 let sum_orig: f64 = u0.iter().sum();
590 let sum_adv: f64 = u.iter().sum();
591 assert!((sum_adv - sum_orig).abs() < 1e-10);
592 }
593
594 #[test]
595 fn test_advection_negative_speed_shifts_left() {
596 let n = 20;
597 let dx = 1.0 / n as f64;
598 let c = -1.0_f64;
599 let dt = 0.5 * dx / c.abs();
600 let mut u0 = vec![0.0_f64; n];
601 u0[15] = 1.0;
602 let u = advection_1d_upwind(&u0, c, dx, dt, 4);
603 let sum_orig: f64 = u0.iter().sum();
605 let sum_adv: f64 = u.iter().sum();
606 assert!((sum_adv - sum_orig).abs() < 1e-10);
607 }
608
609 #[test]
610 fn test_advection_finite_values() {
611 let n = 50;
612 let dx = 1.0 / n as f64;
613 let c = 2.0;
614 let dt = 0.4 * dx / c;
615 let u0: Vec<f64> = (0..n)
616 .map(|i| (i as f64 * dx * std::f64::consts::PI * 2.0).sin())
617 .collect();
618 let u = advection_1d_upwind(&u0, c, dx, dt, 20);
619 for v in &u {
620 assert!(v.is_finite());
621 }
622 }
623
624 #[test]
627 fn test_burgers_zero_field() {
628 let u0 = vec![0.0_f64; 20];
629 let u = burger_equation_1d(&u0, 0.1, 0.01, 10);
630 for v in &u {
631 assert!(v.abs() < EPS);
632 }
633 }
634
635 #[test]
636 fn test_burgers_finite_values() {
637 let n = 40;
638 let dx = 1.0 / n as f64;
639 let dt = 0.3 * dx; let u0: Vec<f64> = (0..n)
641 .map(|i| {
642 let x = i as f64 * dx;
643 (std::f64::consts::PI * 2.0 * x).sin()
644 })
645 .collect();
646 let u = burger_equation_1d(&u0, dx, dt, 30);
647 for v in &u {
648 assert!(v.is_finite());
649 }
650 }
651
652 #[test]
653 fn test_godunov_flux_shock() {
654 let f = super::godunov_flux_burgers(2.0, 0.0);
657 assert!((f - 2.0).abs() < EPS);
658 }
659
660 #[test]
661 fn test_godunov_flux_rarefaction_positive() {
662 let f = super::godunov_flux_burgers(1.0, 2.0);
664 assert!((f - 0.5).abs() < EPS);
665 }
666
667 #[test]
668 fn test_godunov_flux_sonic() {
669 let f = super::godunov_flux_burgers(-1.0, 1.0);
671 assert!(f.abs() < EPS);
672 }
673
674 #[test]
677 fn test_diffusion_reaction_no_reaction_matches_heat() {
678 let n = 20;
679 let dx = 0.05;
680 let d = 0.1;
681 let dt = 0.4 * dx * dx / d;
682 let u0: Vec<f64> = (0..n)
683 .map(|i| (i as f64 / (n - 1) as f64 * std::f64::consts::PI).sin())
684 .collect();
685
686 let u_heat = heat_equation_1d(&u0, d, dx, dt, 50);
687 let u_dr = diffusion_reaction_1d(&u0, d, dx, dt, 50, |_u, _i, _t| 0.0);
688
689 for (a, b) in u_heat.iter().zip(u_dr.iter()) {
690 assert!(
691 (a - b).abs() < 1e-12,
692 "heat and DR (no reaction) differ: {a} vs {b}"
693 );
694 }
695 }
696
697 #[test]
698 fn test_diffusion_reaction_linear_decay() {
699 let n = 20;
701 let dx = 0.05;
702 let d = 0.0; let dt = 0.01;
704 let k = 0.5;
705 let steps = 50;
706 let mut u0 = vec![0.5_f64; n];
707 u0[0] = 0.0;
708 u0[n - 1] = 0.0;
709
710 let u = diffusion_reaction_1d(&u0, d, dx, dt, steps, |val, _i, _t| -k * val);
711
712 let expected = 0.5 * (1.0 - k * dt).powi(steps as i32);
714 for i in 1..n - 1 {
715 assert!(
716 (u[i] - expected).abs() < 1e-6,
717 "node {i}: expected {expected:.8}, got {:.8}",
718 u[i]
719 );
720 }
721 }
722
723 #[test]
724 fn test_diffusion_reaction_bc_preserved() {
725 let n = 15;
726 let u0: Vec<f64> = (0..n).map(|i| i as f64).collect();
727 let dx = 0.1;
728 let d = 0.01;
729 let dt = 0.001;
730 let u = diffusion_reaction_1d(&u0, d, dx, dt, 10, |_u, _i, _t| 0.0);
731 assert!((u[0] - u0[0]).abs() < EPS);
732 assert!((u[n - 1] - u0[n - 1]).abs() < EPS);
733 }
734
735 #[test]
736 fn test_diffusion_reaction_finite_values() {
737 let n = 30;
738 let dx = 0.05;
739 let d = 0.05;
740 let dt = 0.4 * dx * dx / d;
741 let u0: Vec<f64> = (0..n)
742 .map(|i| (i as f64 / (n - 1) as f64).powi(2))
743 .collect();
744 let u = diffusion_reaction_1d(&u0, d, dx, dt, 20, |val, _i, _t| val * 0.1);
745 for v in &u {
746 assert!(v.is_finite());
747 }
748 }
749}