const DISCHARGE_COEFFICIENT: f64 = 0.6;
const DRY_TOLERANCE: f64 = 1e-10;
const POISSON_MAX_ITER: usize = 500;
const POISSON_TOL: f64 = 1e-6;
const CFL_SAFETY: f64 = 0.9;
pub struct ColumnFluid {
pub heights: Vec<f64>,
pub width: usize,
pub dx: f64,
pub density: f64,
pub g: f64,
}
impl ColumnFluid {
#[must_use]
pub fn new(width: usize, dx: f64, density: f64, g: f64) -> Self {
assert!(dx > 0.0, "column spacing dx must be positive");
assert!(density > 0.0, "fluid density must be positive");
assert!(g > 0.0, "gravitational acceleration must be positive");
Self {
heights: vec![0.0; width],
width,
dx,
density,
g,
}
}
pub fn set_height(&mut self, col: usize, h: f64) {
assert!(col < self.width, "column index out of bounds");
assert!(h >= 0.0, "height must be non-negative");
self.heights[col] = h;
}
pub fn step(&mut self, dt: f64) {
if self.width < 2 {
return;
}
let n_interfaces = self.width - 1;
let mut flow = vec![0.0; n_interfaces];
for k in 0..n_interfaces {
let dh = self.heights[k] - self.heights[k + 1];
let abs_dh = dh.abs();
let q = DISCHARGE_COEFFICIENT * self.dx * dh.signum()
* (2.0 * self.g * abs_dh).sqrt();
flow[k] = q;
}
let mut new_heights = self.heights.clone();
for k in 0..n_interfaces {
let dh_transfer = flow[k] * dt / self.dx;
new_heights[k] -= dh_transfer;
new_heights[k + 1] += dh_transfer;
}
for h in &mut new_heights {
if *h < 0.0 {
*h = 0.0;
}
}
self.heights = new_heights;
}
#[must_use]
pub fn total_volume(&self) -> f64 {
self.heights.iter().sum::<f64>() * self.dx
}
}
pub struct ShallowWater1D {
pub h: Vec<f64>,
pub hu: Vec<f64>,
pub nx: usize,
pub dx: f64,
pub g: f64,
}
impl ShallowWater1D {
#[must_use]
pub fn new(nx: usize, dx: f64, g: f64) -> Self {
assert!(dx > 0.0, "grid spacing dx must be positive");
assert!(g > 0.0, "gravitational acceleration must be positive");
Self {
h: vec![0.0; nx],
hu: vec![0.0; nx],
nx,
dx,
g,
}
}
#[must_use]
pub fn velocity(&self, i: usize) -> f64 {
if self.h[i] < DRY_TOLERANCE {
0.0
} else {
self.hu[i] / self.h[i]
}
}
fn momentum_flux(&self, i: usize) -> f64 {
if self.h[i] < DRY_TOLERANCE {
0.0
} else {
self.hu[i] * self.hu[i] / self.h[i] + 0.5 * self.g * self.h[i] * self.h[i]
}
}
pub fn step_lax_friedrichs(&mut self, dt: f64) {
if self.nx < 3 {
return;
}
let ratio = dt / (2.0 * self.dx);
let mut h_new = vec![0.0; self.nx];
let mut hu_new = vec![0.0; self.nx];
for i in 1..self.nx - 1 {
h_new[i] = 0.5 * (self.h[i - 1] + self.h[i + 1])
- ratio * (self.hu[i + 1] - self.hu[i - 1]);
let f_plus = self.momentum_flux(i + 1);
let f_minus = self.momentum_flux(i - 1);
hu_new[i] = 0.5 * (self.hu[i - 1] + self.hu[i + 1])
- ratio * (f_plus - f_minus);
if h_new[i] < 0.0 {
h_new[i] = 0.0;
hu_new[i] = 0.0;
}
}
h_new[0] = h_new[1];
hu_new[0] = -hu_new[1];
h_new[self.nx - 1] = h_new[self.nx - 2];
hu_new[self.nx - 1] = -hu_new[self.nx - 2];
self.h = h_new;
self.hu = hu_new;
}
#[must_use]
pub fn max_wave_speed(&self) -> f64 {
let mut max_speed: f64 = 0.0;
for i in 0..self.nx {
let u = self.velocity(i);
let c = (self.g * self.h[i].max(0.0)).sqrt();
max_speed = max_speed.max(u.abs() + c);
}
max_speed
}
#[must_use]
pub fn stable_dt(&self) -> f64 {
let s = self.max_wave_speed();
if s < DRY_TOLERANCE {
return self.dx; }
CFL_SAFETY * self.dx / s
}
#[must_use]
pub fn total_volume(&self) -> f64 {
self.h.iter().sum::<f64>() * self.dx
}
#[must_use]
pub fn total_energy(&self) -> f64 {
let mut e = 0.0;
for i in 0..self.nx {
let u = self.velocity(i);
let h = self.h[i];
e += (0.5 * h * u * u + 0.5 * self.g * h * h) * self.dx;
}
e
}
}
pub struct EulerFluid2D {
pub vx: Vec<f64>,
pub vy: Vec<f64>,
pub pressure: Vec<f64>,
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub dy: f64,
pub density: f64,
}
#[inline]
fn idx(i: usize, j: usize, ny: usize) -> usize {
i * ny + j
}
impl EulerFluid2D {
#[must_use]
pub fn new(nx: usize, ny: usize, dx: f64, dy: f64, density: f64) -> Self {
assert!(dx > 0.0, "grid spacing dx must be positive");
assert!(dy > 0.0, "grid spacing dy must be positive");
assert!(density > 0.0, "fluid density must be positive");
let n = nx * ny;
Self {
vx: vec![0.0; n],
vy: vec![0.0; n],
pressure: vec![0.0; n],
nx,
ny,
dx,
dy,
density,
}
}
pub fn set_velocity(&mut self, i: usize, j: usize, vx: f64, vy: f64) {
let k = idx(i, j, self.ny);
self.vx[k] = vx;
self.vy[k] = vy;
}
pub fn step(&mut self, dt: f64, gravity_x: f64, gravity_y: f64) {
let n = self.nx * self.ny;
let mut vx_star = vec![0.0; n];
let mut vy_star = vec![0.0; n];
for i in 0..self.nx {
for j in 0..self.ny {
let k = idx(i, j, self.ny);
let u = self.vx[k];
let v = self.vy[k];
let advect_vx = self.upwind_advect(i, j, u, v, &self.vx);
let advect_vy = self.upwind_advect(i, j, u, v, &self.vy);
vx_star[k] = u - dt * advect_vx + dt * gravity_x;
vy_star[k] = v - dt * advect_vy + dt * gravity_y;
}
}
self.apply_boundary_velocity(&mut vx_star, &mut vy_star);
let mut rhs = vec![0.0; n];
for i in 1..self.nx - 1 {
for j in 1..self.ny - 1 {
let dvx_dx = (vx_star[idx(i + 1, j, self.ny)]
- vx_star[idx(i - 1, j, self.ny)])
/ (2.0 * self.dx);
let dvy_dy = (vy_star[idx(i, j + 1, self.ny)]
- vy_star[idx(i, j - 1, self.ny)])
/ (2.0 * self.dy);
rhs[idx(i, j, self.ny)] = (self.density / dt) * (dvx_dx + dvy_dy);
}
}
self.pressure = self.solve_pressure_poisson(&rhs);
let dt_over_rho = dt / self.density;
for i in 1..self.nx - 1 {
for j in 1..self.ny - 1 {
let k = idx(i, j, self.ny);
let dp_dx = (self.pressure[idx(i + 1, j, self.ny)]
- self.pressure[idx(i - 1, j, self.ny)])
/ (2.0 * self.dx);
let dp_dy = (self.pressure[idx(i, j + 1, self.ny)]
- self.pressure[idx(i, j - 1, self.ny)])
/ (2.0 * self.dy);
self.vx[k] = vx_star[k] - dt_over_rho * dp_dx;
self.vy[k] = vy_star[k] - dt_over_rho * dp_dy;
}
}
let mut vx_tmp = std::mem::take(&mut self.vx);
let mut vy_tmp = std::mem::take(&mut self.vy);
self.apply_boundary_velocity(&mut vx_tmp, &mut vy_tmp);
self.vx = vx_tmp;
self.vy = vy_tmp;
}
fn upwind_advect(&self, i: usize, j: usize, u: f64, v: f64, field: &[f64]) -> f64 {
let phi = field[idx(i, j, self.ny)];
let dphi_dx = if u >= 0.0 {
if i > 0 {
(phi - field[idx(i - 1, j, self.ny)]) / self.dx
} else {
0.0
}
} else if i < self.nx - 1 {
(field[idx(i + 1, j, self.ny)] - phi) / self.dx
} else {
0.0
};
let dphi_dy = if v >= 0.0 {
if j > 0 {
(phi - field[idx(i, j - 1, self.ny)]) / self.dy
} else {
0.0
}
} else if j < self.ny - 1 {
(field[idx(i, j + 1, self.ny)] - phi) / self.dy
} else {
0.0
};
u * dphi_dx + v * dphi_dy
}
fn apply_boundary_velocity(&self, vx: &mut [f64], vy: &mut [f64]) {
for j in 0..self.ny {
vx[idx(0, j, self.ny)] = 0.0;
vx[idx(self.nx - 1, j, self.ny)] = 0.0;
}
for i in 0..self.nx {
vy[idx(i, 0, self.ny)] = 0.0;
vy[idx(i, self.ny - 1, self.ny)] = 0.0;
}
}
fn solve_pressure_poisson(&self, rhs: &[f64]) -> Vec<f64> {
let n = self.nx * self.ny;
let dx2 = self.dx * self.dx;
let dy2 = self.dy * self.dy;
let denom = 2.0 * (1.0 / dx2 + 1.0 / dy2);
let mut p = self.pressure.clone();
let mut p_new = vec![0.0; n];
for _iter in 0..POISSON_MAX_ITER {
let mut max_diff: f64 = 0.0;
for i in 1..self.nx - 1 {
for j in 1..self.ny - 1 {
let k = idx(i, j, self.ny);
let val = ((p[idx(i + 1, j, self.ny)] + p[idx(i - 1, j, self.ny)]) / dx2
+ (p[idx(i, j + 1, self.ny)] + p[idx(i, j - 1, self.ny)]) / dy2
- rhs[k])
/ denom;
p_new[k] = val;
max_diff = max_diff.max((val - p[k]).abs());
}
}
for j in 0..self.ny {
p_new[idx(0, j, self.ny)] = p_new[idx(1, j, self.ny)];
p_new[idx(self.nx - 1, j, self.ny)] =
p_new[idx(self.nx - 2, j, self.ny)];
}
for i in 0..self.nx {
p_new[idx(i, 0, self.ny)] = p_new[idx(i, 1, self.ny)];
p_new[idx(i, self.ny - 1, self.ny)] =
p_new[idx(i, self.ny - 2, self.ny)];
}
std::mem::swap(&mut p, &mut p_new);
if max_diff < POISSON_TOL {
break;
}
}
p
}
#[must_use]
pub fn divergence(&self) -> f64 {
let mut max_div: f64 = 0.0;
for i in 1..self.nx - 1 {
for j in 1..self.ny - 1 {
let dvx_dx = (self.vx[idx(i + 1, j, self.ny)]
- self.vx[idx(i - 1, j, self.ny)])
/ (2.0 * self.dx);
let dvy_dy = (self.vy[idx(i, j + 1, self.ny)]
- self.vy[idx(i, j - 1, self.ny)])
/ (2.0 * self.dy);
max_div = max_div.max((dvx_dx + dvy_dy).abs());
}
}
max_div
}
#[must_use]
pub fn kinetic_energy(&self) -> f64 {
let cell_area = self.dx * self.dy;
let mut ke = 0.0;
for k in 0..self.nx * self.ny {
ke += self.vx[k] * self.vx[k] + self.vy[k] * self.vy[k];
}
0.5 * self.density * ke * cell_area
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::constants;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
fn approx_rel_eq(a: f64, b: f64, tol: f64) -> bool {
if b.abs() < 1e-15 {
a.abs() < tol
} else {
((a - b) / b).abs() < tol
}
}
#[test]
fn column_fluid_equilibrates_to_equal_height() {
let mut fluid = ColumnFluid::new(2, 1.0, 1000.0, constants::G_ACCEL);
fluid.set_height(0, 2.0);
fluid.set_height(1, 0.0);
let expected_height = 1.0;
for _ in 0..10_000 {
fluid.step(0.001);
}
let h0 = fluid.heights[0];
assert!(
approx_eq(h0, expected_height, 0.05),
"column 0: {h0} != {expected_height}",
);
let h1 = fluid.heights[1];
assert!(
approx_eq(h1, expected_height, 0.05),
"column 1: {h1} != {expected_height}",
);
}
#[test]
fn column_fluid_conserves_volume() {
let mut fluid = ColumnFluid::new(10, 0.5, 1000.0, constants::G_ACCEL);
fluid.set_height(0, 3.0);
fluid.set_height(3, 1.5);
fluid.set_height(7, 2.0);
let initial_volume = fluid.total_volume();
for _ in 0..5000 {
fluid.step(0.0005);
}
let final_volume = fluid.total_volume();
assert!(
approx_rel_eq(initial_volume, final_volume, 1e-6),
"volume not conserved: {initial_volume} -> {final_volume}",
);
}
#[test]
fn shallow_water_dam_break_wave_speed() {
let nx = 500;
let dx = 0.1;
let g = constants::G_ACCEL;
let h0 = 1.0;
let mut sw = ShallowWater1D::new(nx, dx, g);
let dam_pos = nx / 2;
for i in 0..dam_pos {
sw.h[i] = h0;
}
let expected_wave_speed = (g * h0).sqrt(); let total_time = 1.0;
let mut t = 0.0;
while t < total_time {
let dt = sw.stable_dt().min(total_time - t);
sw.step_lax_friedrichs(dt);
t += dt;
}
let threshold = 0.01 * h0;
let mut front_idx = dam_pos;
for i in (dam_pos..nx).rev() {
if sw.h[i] > threshold {
front_idx = i;
break;
}
}
let front_distance = (front_idx - dam_pos) as f64 * dx;
let measured_speed = front_distance / total_time;
assert!(
measured_speed > 0.5 * expected_wave_speed,
"wave too slow: measured {measured_speed} m/s vs expected {expected_wave_speed} m/s",
);
assert!(
measured_speed < 2.0 * expected_wave_speed,
"wave too fast: measured {measured_speed} m/s vs expected {expected_wave_speed} m/s",
);
}
#[test]
fn shallow_water_flat_surface_stays_flat() {
let nx = 100;
let dx = 0.1;
let g = constants::G_ACCEL;
let h0 = 1.0;
let mut sw = ShallowWater1D::new(nx, dx, g);
for i in 0..nx {
sw.h[i] = h0;
}
for _ in 0..1000 {
let dt = sw.stable_dt();
sw.step_lax_friedrichs(dt);
}
let max_deviation = sw
.h
.iter()
.skip(1) .take(nx - 2)
.map(|&h| (h - h0).abs())
.fold(0.0_f64, f64::max);
assert!(
max_deviation < 1e-6,
"flat surface developed perturbation: max deviation = {max_deviation}",
);
}
#[test]
fn euler_2d_divergence_near_zero_after_step() {
let nx = 32;
let ny = 32;
let dx = 0.1;
let dy = 0.1;
let density = 1.0;
let mut fluid = EulerFluid2D::new(nx, ny, dx, dy, density);
let cx = (nx as f64 * dx) / 2.0;
let cy = (ny as f64 * dy) / 2.0;
for i in 1..nx - 1 {
for j in 1..ny - 1 {
let x = i as f64 * dx - cx;
let y = j as f64 * dy - cy;
let r = (x * x + y * y).sqrt();
let envelope = (1.0 - r / (cx.min(cy))).max(0.0);
fluid.set_velocity(i, j, -y * envelope * 0.5, x * envelope * 0.5);
}
}
fluid.step(0.001, 0.0, 0.0);
let div = fluid.divergence();
assert!(
div < 0.1,
"divergence too large after projection: {div}",
);
}
#[test]
fn shallow_water_max_wave_speed_still_water() {
let nx = 50;
let dx = 0.1;
let g = 9.81;
let h0 = 2.0;
let mut sw = ShallowWater1D::new(nx, dx, g);
for i in 0..nx {
sw.h[i] = h0;
}
let s = sw.max_wave_speed();
let expected = 4.429446918070020;
assert!(
approx_rel_eq(s, expected, 1e-10),
"max_wave_speed={s}, expected {expected}"
);
}
#[test]
fn shallow_water_max_wave_speed_dry() {
let sw = ShallowWater1D::new(10, 0.1, 9.81);
assert!(approx_eq(sw.max_wave_speed(), 0.0, 1e-15));
}
#[test]
fn shallow_water_total_energy_still_water() {
let nx = 50;
let dx = 0.1;
let g = 9.81;
let h0 = 1.0;
let mut sw = ShallowWater1D::new(nx, dx, g);
for i in 0..nx {
sw.h[i] = h0;
}
let e = sw.total_energy();
let expected = 24.525;
assert!(
approx_rel_eq(e, expected, 1e-10),
"total_energy={e}, expected {expected}"
);
}
#[test]
fn shallow_water_velocity_dry_cell() {
let sw = ShallowWater1D::new(5, 0.1, 9.81);
assert!(approx_eq(sw.velocity(0), 0.0, 1e-15));
}
#[test]
fn shallow_water_velocity_wet_cell() {
let mut sw = ShallowWater1D::new(5, 0.1, 9.81);
sw.h[2] = 2.0;
sw.hu[2] = 6.0;
assert!(approx_rel_eq(sw.velocity(2), 3.0, 1e-10));
}
#[test]
fn euler_2d_approximately_conserves_kinetic_energy() {
let nx = 32;
let ny = 32;
let dx = 0.1;
let dy = 0.1;
let density = 1.0;
let mut fluid = EulerFluid2D::new(nx, ny, dx, dy, density);
let u0 = 1.0;
for i in 1..nx - 1 {
for j in 1..ny - 1 {
fluid.set_velocity(i, j, u0, 0.0);
}
}
let ke_initial = fluid.kinetic_energy();
assert!(ke_initial > 0.0, "initial KE should be positive");
let dt = 0.001;
let n_steps = 50;
for _ in 0..n_steps {
fluid.step(dt, 0.0, 0.0);
}
let ke_final = fluid.kinetic_energy();
assert!(
ke_final <= ke_initial * 1.01,
"KE grew (instability): {ke_initial} -> {ke_final}",
);
assert!(
ke_final > ke_initial * 0.001,
"KE lost too much: {ke_initial} -> {ke_final}",
);
}
#[test]
fn column_fluid_single_column_step() {
let mut fluid = ColumnFluid::new(1, 1.0, 1000.0, constants::G_ACCEL);
fluid.set_height(0, 5.0);
fluid.step(0.001);
assert!(approx_eq(fluid.heights[0], 5.0, 1e-12));
}
#[test]
fn column_fluid_negative_height_clamped() {
let mut fluid = ColumnFluid::new(3, 0.1, 1000.0, constants::G_ACCEL);
fluid.set_height(0, 0.001);
fluid.set_height(1, 10.0);
fluid.set_height(2, 0.001);
for _ in 0..50 {
fluid.step(0.01);
}
for h in &fluid.heights {
assert!(*h >= 0.0, "Height should never be negative, got {h}");
}
}
#[test]
fn shallow_water_small_nx_noop() {
let mut sw = ShallowWater1D::new(2, 1.0, constants::G_ACCEL);
sw.h[0] = 1.0;
sw.h[1] = 1.0;
sw.step_lax_friedrichs(0.01);
assert!(sw.h[0].is_finite());
}
#[test]
fn shallow_water_negative_depth_clamped() {
let mut sw = ShallowWater1D::new(5, 0.1, constants::G_ACCEL);
sw.h[2] = 100.0;
sw.hu[2] = 500.0;
let dt = sw.stable_dt() * 0.5;
for _ in 0..3 {
sw.step_lax_friedrichs(dt);
}
for &h in &sw.h {
assert!(h >= 0.0, "Depth should never be negative, got {h}");
}
}
#[test]
fn shallow_water_dry_stable_dt() {
let sw = ShallowWater1D::new(10, 0.5, constants::G_ACCEL);
let dt = sw.stable_dt();
assert!(approx_eq(dt, sw.dx, 1e-12));
}
#[test]
fn shallow_water_total_volume() {
let mut sw = ShallowWater1D::new(5, 0.5, constants::G_ACCEL);
sw.h = vec![1.0, 2.0, 3.0, 2.0, 1.0];
let v = sw.total_volume();
assert!(approx_eq(v, 9.0 * 0.5, 1e-12));
}
#[test]
fn column_fluid_large_dt_forces_negative_clamp() {
let mut fluid = ColumnFluid::new(2, 1.0, 1000.0, constants::G_ACCEL);
fluid.set_height(0, 10.0);
fluid.set_height(1, 0.0);
fluid.step(100.0);
assert!(fluid.heights[0] >= 0.0);
assert!(fluid.heights[1] >= 0.0);
}
#[test]
fn shallow_water_large_dt_forces_negative_clamp() {
let mut sw = ShallowWater1D::new(5, 0.5, constants::G_ACCEL);
sw.h = vec![0.0, 0.0, 10.0, 0.0, 0.0];
sw.hu = vec![0.0, 0.0, 50.0, 0.0, 0.0];
sw.step_lax_friedrichs(10.0);
for &h in &sw.h {
assert!(h >= 0.0);
}
}
#[test]
fn euler2d_upwind_boundary_edges() {
let nx = 5;
let ny = 5;
let dx = 0.1;
let dy = 0.1;
let mut sim = EulerFluid2D::new(nx, ny, dx, dy, 1.0);
for i in 0..nx {
for j in 0..ny {
let k = i * ny + j;
sim.vx[k] = -1.0;
sim.vy[k] = -1.0;
}
}
for k in 0..nx * ny {
sim.pressure[k] = 1.0;
}
sim.step(0.001, 0.0, 0.0);
assert!(sim.divergence().is_finite());
}
#[test]
fn euler2d_poisson_converges() {
let nx = 10;
let ny = 10;
let dx = 0.1;
let dy = 0.1;
let mut sim = EulerFluid2D::new(nx, ny, dx, dy, 1.0);
for i in 0..nx {
for j in 0..ny {
let k = i * ny + j;
sim.vx[k] = ((i as f64) * 0.1).sin();
sim.vy[k] = ((j as f64) * 0.1).cos();
}
}
sim.step(0.0001, 0.0, 0.0);
let div = sim.divergence();
assert!(div.is_finite());
}
#[test]
fn test_approx_rel_eq_near_zero_b() {
assert!(approx_rel_eq(0.0, 0.0, 1e-6));
assert!(!approx_rel_eq(1.0, 0.0, 0.5));
}
}