#![allow(clippy::needless_range_loop)]
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct CflCondition {
pub dx: f64,
pub dt: f64,
pub speed: f64,
pub diffusivity: f64,
}
impl CflCondition {
pub fn new(dx: f64, dt: f64, speed: f64, diffusivity: f64) -> Self {
Self {
dx,
dt,
speed,
diffusivity,
}
}
pub fn advective_cfl(&self) -> f64 {
self.speed * self.dt / self.dx
}
pub fn diffusive_cfl(&self) -> f64 {
self.diffusivity * self.dt / (self.dx * self.dx)
}
pub fn is_advection_stable(&self) -> bool {
self.advective_cfl() <= 1.0
}
pub fn is_diffusion_stable(&self) -> bool {
self.diffusive_cfl() <= 0.5
}
}
#[allow(dead_code)]
pub fn heat_equation_1d(u0: &[f64], alpha: f64, dx: f64, dt: f64, n_steps: usize) -> Vec<f64> {
let n = u0.len();
let mut u = u0.to_vec();
let mut u_new = u.clone();
let r = alpha * dt / (dx * dx);
for _ in 0..n_steps {
for i in 1..n - 1 {
u_new[i] = u[i] + r * (u[i + 1] - 2.0 * u[i] + u[i - 1]);
}
u_new[0] = u[0];
u_new[n - 1] = u[n - 1];
std::mem::swap(&mut u, &mut u_new);
}
u
}
#[allow(dead_code)]
pub fn wave_equation_1d(
u_prev: &[f64],
u_curr: &[f64],
c: f64,
dx: f64,
dt: f64,
n_steps: usize,
) -> Vec<f64> {
let n = u_curr.len();
let mut prev = u_prev.to_vec();
let mut curr = u_curr.to_vec();
let mut next = vec![0.0_f64; n];
let r2 = (c * dt / dx).powi(2);
for _ in 0..n_steps {
for i in 1..n - 1 {
next[i] = 2.0 * curr[i] - prev[i] + r2 * (curr[i + 1] - 2.0 * curr[i] + curr[i - 1]);
}
next[0] = 0.0;
next[n - 1] = 0.0;
prev.copy_from_slice(&curr);
curr.copy_from_slice(&next);
}
curr
}
#[allow(dead_code)]
pub fn poisson_1d(f: &[f64], dx: f64, bc_left: f64, bc_right: f64) -> Vec<f64> {
let n = f.len();
if n < 2 {
return vec![bc_left; n];
}
let dx2 = dx * dx;
let m = n - 2; if m == 0 {
return vec![bc_left, bc_right];
}
let mut rhs = vec![0.0_f64; m];
for i in 0..m {
rhs[i] = f[i + 1] * dx2;
}
rhs[0] += bc_left;
rhs[m - 1] += bc_right;
let mut c_star = vec![0.0_f64; m];
let mut d_star = vec![0.0_f64; m];
c_star[0] = -1.0 / 2.0;
d_star[0] = rhs[0] / 2.0;
for i in 1..m {
let denom = 2.0 + c_star[i - 1];
c_star[i] = -1.0 / denom;
d_star[i] = (rhs[i] + d_star[i - 1]) / denom;
}
let mut x = vec![0.0_f64; m];
x[m - 1] = d_star[m - 1];
for i in (0..m - 1).rev() {
x[i] = d_star[i] - c_star[i] * x[i + 1];
}
let mut u = vec![0.0_f64; n];
u[0] = bc_left;
u[n - 1] = bc_right;
u[1..m + 1].copy_from_slice(&x[..m]);
u
}
#[allow(dead_code)]
pub fn advection_1d_upwind(u0: &[f64], c: f64, dx: f64, dt: f64, n_steps: usize) -> Vec<f64> {
let n = u0.len();
let mut u = u0.to_vec();
let mut u_new = u.clone();
let nu = c * dt / dx;
for _ in 0..n_steps {
for i in 0..n {
if c >= 0.0 {
let im1 = if i == 0 { n - 1 } else { i - 1 };
u_new[i] = u[i] - nu * (u[i] - u[im1]);
} else {
let ip1 = if i == n - 1 { 0 } else { i + 1 };
u_new[i] = u[i] - nu * (u[ip1] - u[i]);
}
}
std::mem::swap(&mut u, &mut u_new);
}
u
}
#[allow(dead_code)]
pub fn burger_equation_1d(u0: &[f64], dx: f64, dt: f64, n_steps: usize) -> Vec<f64> {
let n = u0.len();
let mut u = u0.to_vec();
let mut u_new = vec![0.0_f64; n];
for _ in 0..n_steps {
for i in 0..n {
let ip1 = (i + 1) % n;
let im1 = if i == 0 { n - 1 } else { i - 1 };
let f_right = godunov_flux_burgers(u[i], u[ip1]);
let f_left = godunov_flux_burgers(u[im1], u[i]);
u_new[i] = u[i] - (dt / dx) * (f_right - f_left);
}
std::mem::swap(&mut u, &mut u_new);
}
u
}
fn godunov_flux_burgers(u_l: f64, u_r: f64) -> f64 {
if u_l >= u_r {
let s = (u_l + u_r) / 2.0; if s >= 0.0 {
u_l * u_l / 2.0
} else {
u_r * u_r / 2.0
}
} else {
if u_l >= 0.0 {
u_l * u_l / 2.0
} else if u_r <= 0.0 {
u_r * u_r / 2.0
} else {
0.0 }
}
}
#[allow(dead_code)]
pub fn diffusion_reaction_1d<F>(
u0: &[f64],
d: f64,
dx: f64,
dt: f64,
n_steps: usize,
reaction: F,
) -> Vec<f64>
where
F: Fn(f64, usize, f64) -> f64,
{
let n = u0.len();
let mut u = u0.to_vec();
let mut u_new = u.clone();
let r = d * dt / (dx * dx);
let mut t = 0.0_f64;
for _ in 0..n_steps {
for i in 1..n - 1 {
let diffusion = r * (u[i + 1] - 2.0 * u[i] + u[i - 1]);
let react = reaction(u[i], i, t) * dt;
u_new[i] = u[i] + diffusion + react;
}
u_new[0] = u[0];
u_new[n - 1] = u[n - 1];
t += dt;
std::mem::swap(&mut u, &mut u_new);
}
u
}
#[cfg(test)]
mod tests {
use super::*;
const EPS: f64 = 1e-10;
#[test]
fn test_cfl_advective_stable() {
let cfl = CflCondition::new(0.1, 0.05, 1.0, 0.0);
assert!(cfl.is_advection_stable());
assert!((cfl.advective_cfl() - 0.5).abs() < EPS);
}
#[test]
fn test_cfl_advective_unstable() {
let cfl = CflCondition::new(0.1, 0.2, 1.0, 0.0);
assert!(!cfl.is_advection_stable());
}
#[test]
fn test_cfl_diffusive_stable() {
let cfl = CflCondition::new(0.1, 0.001, 0.0, 0.1);
assert!(cfl.is_diffusion_stable());
}
#[test]
fn test_cfl_diffusive_unstable() {
let cfl = CflCondition::new(0.1, 1.0, 0.0, 1.0);
assert!(!cfl.is_diffusion_stable());
}
#[test]
fn test_heat_bc_fixed() {
let n = 20;
let u0: Vec<f64> = (0..n)
.map(|i| {
if i == 0 {
0.0
} else if i == n - 1 {
1.0
} else {
0.5
}
})
.collect();
let u = heat_equation_1d(&u0, 0.1, 0.1, 0.001, 100);
assert!((u[0] - u0[0]).abs() < EPS);
assert!((u[n - 1] - u0[n - 1]).abs() < EPS);
}
#[test]
fn test_heat_steady_state_linear() {
let n = 11;
let dx = 1.0 / (n - 1) as f64;
let mut u0 = vec![0.5_f64; n];
u0[0] = 0.0;
u0[n - 1] = 1.0;
let alpha = 0.5;
let dt = 0.4 * dx * dx / alpha; let u = heat_equation_1d(&u0, alpha, dx, dt, 5000);
for i in 1..n - 1 {
let x = i as f64 * dx;
assert!(
(u[i] - x).abs() < 1e-3,
"expected {x:.3} at node {i}, got {:.6}",
u[i]
);
}
}
#[test]
fn test_heat_conservation_insulated() {
let n = 50;
let u0: Vec<f64> = (0..n)
.map(|i| {
let x = i as f64 / (n - 1) as f64;
(std::f64::consts::PI * x).sin()
})
.collect();
let dx = 1.0 / (n - 1) as f64;
let alpha = 0.01;
let dt = 0.4 * dx * dx / alpha;
let u = heat_equation_1d(&u0, alpha, dx, dt, 100);
for val in &u {
assert!(val.is_finite());
}
}
#[test]
fn test_wave_zero_field_stays_zero() {
let n = 30;
let u_prev = vec![0.0_f64; n];
let u_curr = vec![0.0_f64; n];
let u = wave_equation_1d(&u_prev, &u_curr, 1.0, 0.1, 0.05, 10);
for v in &u {
assert!(v.abs() < EPS);
}
}
#[test]
fn test_wave_bc_fixed_zero() {
let n = 20;
let u_prev: Vec<f64> = (0..n).map(|i| (i as f64).sin()).collect();
let u_curr = u_prev.clone();
let u = wave_equation_1d(&u_prev, &u_curr, 1.0, 0.1, 0.05, 5);
assert!(u[0].abs() < EPS);
assert!(u[n - 1].abs() < EPS);
}
#[test]
fn test_wave_finite_values() {
let n = 40;
let dx = 1.0 / (n - 1) as f64;
let c = 1.0;
let dt = 0.9 * dx / c;
let u0: Vec<f64> = (0..n)
.map(|i| {
let x = i as f64 * dx;
(2.0 * std::f64::consts::PI * x).sin()
})
.collect();
let u = wave_equation_1d(&u0, &u0, c, dx, dt, 50);
for v in &u {
assert!(v.is_finite());
}
}
#[test]
fn test_poisson_bc_satisfied() {
let n = 10;
let f = vec![1.0_f64; n];
let u = poisson_1d(&f, 0.1, 0.0, 0.0);
assert!((u[0] - 0.0).abs() < EPS);
assert!((u[n - 1] - 0.0).abs() < EPS);
}
#[test]
fn test_poisson_zero_rhs_linear_solution() {
let n = 11;
let dx = 1.0 / (n - 1) as f64;
let f = vec![0.0_f64; n];
let u = poisson_1d(&f, dx, 0.0, 1.0);
for i in 0..n {
let x = i as f64 * dx;
assert!(
(u[i] - x).abs() < 1e-10,
"node {i}: expected {x}, got {}",
u[i]
);
}
}
#[test]
fn test_poisson_nonzero_rhs_symmetry() {
let n = 11;
let dx = 1.0 / (n - 1) as f64;
let f = vec![2.0_f64; n];
let u = poisson_1d(&f, dx, 0.0, 0.0);
for i in 0..n {
let x = i as f64 * dx;
let expected = x * (1.0 - x);
assert!(
(u[i] - expected).abs() < 1e-10,
"node {i}: expected {expected:.6}, got {:.6}",
u[i]
);
}
}
#[test]
fn test_poisson_two_node_trivial() {
let u = poisson_1d(&[0.0, 0.0], 1.0, 1.0, 2.0);
assert_eq!(u.len(), 2);
assert!((u[0] - 1.0).abs() < EPS);
assert!((u[1] - 2.0).abs() < EPS);
}
#[test]
fn test_advection_zero_field() {
let u0 = vec![0.0_f64; 20];
let u = advection_1d_upwind(&u0, 1.0, 0.1, 0.05, 10);
for v in &u {
assert!(v.abs() < EPS);
}
}
#[test]
fn test_advection_positive_speed_shifts_right() {
let n = 20;
let dx = 1.0 / n as f64;
let c = 1.0;
let dt = 0.5 * dx / c; let mut u0 = vec![0.0_f64; n];
u0[5] = 1.0;
let u = advection_1d_upwind(&u0, c, dx, dt, 4);
let sum_orig: f64 = u0.iter().sum();
let sum_adv: f64 = u.iter().sum();
assert!((sum_adv - sum_orig).abs() < 1e-10);
}
#[test]
fn test_advection_negative_speed_shifts_left() {
let n = 20;
let dx = 1.0 / n as f64;
let c = -1.0_f64;
let dt = 0.5 * dx / c.abs();
let mut u0 = vec![0.0_f64; n];
u0[15] = 1.0;
let u = advection_1d_upwind(&u0, c, dx, dt, 4);
let sum_orig: f64 = u0.iter().sum();
let sum_adv: f64 = u.iter().sum();
assert!((sum_adv - sum_orig).abs() < 1e-10);
}
#[test]
fn test_advection_finite_values() {
let n = 50;
let dx = 1.0 / n as f64;
let c = 2.0;
let dt = 0.4 * dx / c;
let u0: Vec<f64> = (0..n)
.map(|i| (i as f64 * dx * std::f64::consts::PI * 2.0).sin())
.collect();
let u = advection_1d_upwind(&u0, c, dx, dt, 20);
for v in &u {
assert!(v.is_finite());
}
}
#[test]
fn test_burgers_zero_field() {
let u0 = vec![0.0_f64; 20];
let u = burger_equation_1d(&u0, 0.1, 0.01, 10);
for v in &u {
assert!(v.abs() < EPS);
}
}
#[test]
fn test_burgers_finite_values() {
let n = 40;
let dx = 1.0 / n as f64;
let dt = 0.3 * dx; let u0: Vec<f64> = (0..n)
.map(|i| {
let x = i as f64 * dx;
(std::f64::consts::PI * 2.0 * x).sin()
})
.collect();
let u = burger_equation_1d(&u0, dx, dt, 30);
for v in &u {
assert!(v.is_finite());
}
}
#[test]
fn test_godunov_flux_shock() {
let f = super::godunov_flux_burgers(2.0, 0.0);
assert!((f - 2.0).abs() < EPS);
}
#[test]
fn test_godunov_flux_rarefaction_positive() {
let f = super::godunov_flux_burgers(1.0, 2.0);
assert!((f - 0.5).abs() < EPS);
}
#[test]
fn test_godunov_flux_sonic() {
let f = super::godunov_flux_burgers(-1.0, 1.0);
assert!(f.abs() < EPS);
}
#[test]
fn test_diffusion_reaction_no_reaction_matches_heat() {
let n = 20;
let dx = 0.05;
let d = 0.1;
let dt = 0.4 * dx * dx / d;
let u0: Vec<f64> = (0..n)
.map(|i| (i as f64 / (n - 1) as f64 * std::f64::consts::PI).sin())
.collect();
let u_heat = heat_equation_1d(&u0, d, dx, dt, 50);
let u_dr = diffusion_reaction_1d(&u0, d, dx, dt, 50, |_u, _i, _t| 0.0);
for (a, b) in u_heat.iter().zip(u_dr.iter()) {
assert!(
(a - b).abs() < 1e-12,
"heat and DR (no reaction) differ: {a} vs {b}"
);
}
}
#[test]
fn test_diffusion_reaction_linear_decay() {
let n = 20;
let dx = 0.05;
let d = 0.0; let dt = 0.01;
let k = 0.5;
let steps = 50;
let mut u0 = vec![0.5_f64; n];
u0[0] = 0.0;
u0[n - 1] = 0.0;
let u = diffusion_reaction_1d(&u0, d, dx, dt, steps, |val, _i, _t| -k * val);
let expected = 0.5 * (1.0 - k * dt).powi(steps as i32);
for i in 1..n - 1 {
assert!(
(u[i] - expected).abs() < 1e-6,
"node {i}: expected {expected:.8}, got {:.8}",
u[i]
);
}
}
#[test]
fn test_diffusion_reaction_bc_preserved() {
let n = 15;
let u0: Vec<f64> = (0..n).map(|i| i as f64).collect();
let dx = 0.1;
let d = 0.01;
let dt = 0.001;
let u = diffusion_reaction_1d(&u0, d, dx, dt, 10, |_u, _i, _t| 0.0);
assert!((u[0] - u0[0]).abs() < EPS);
assert!((u[n - 1] - u0[n - 1]).abs() < EPS);
}
#[test]
fn test_diffusion_reaction_finite_values() {
let n = 30;
let dx = 0.05;
let d = 0.05;
let dt = 0.4 * dx * dx / d;
let u0: Vec<f64> = (0..n)
.map(|i| (i as f64 / (n - 1) as f64).powi(2))
.collect();
let u = diffusion_reaction_1d(&u0, d, dx, dt, 20, |val, _i, _t| val * 0.1);
for v in &u {
assert!(v.is_finite());
}
}
}