#[derive(Debug, Clone, Copy)]
pub struct MacCell {
pub pressure: f64,
pub u_x: f64,
pub u_y: f64,
pub is_fluid: bool,
}
impl Default for MacCell {
fn default() -> Self {
Self {
pressure: 0.0,
u_x: 0.0,
u_y: 0.0,
is_fluid: true,
}
}
}
#[derive(Debug, Clone)]
pub struct MacGrid {
pub nx: usize,
pub ny: usize,
pub dx: f64,
pub cells: Vec<MacCell>,
}
impl MacGrid {
pub fn new(nx: usize, ny: usize, dx: f64) -> Self {
Self {
nx,
ny,
dx,
cells: vec![MacCell::default(); nx * ny],
}
}
#[inline]
pub fn cell(&self, i: usize, j: usize) -> &MacCell {
&self.cells[j * self.nx + i]
}
#[inline]
pub fn cell_mut(&mut self, i: usize, j: usize) -> &mut MacCell {
&mut self.cells[j * self.nx + i]
}
pub fn divergence(&self, i: usize, j: usize) -> f64 {
let ux_e = self.cells[j * self.nx + i].u_x;
let ux_w = if i > 0 {
self.cells[j * self.nx + (i - 1)].u_x
} else {
0.0
};
let uy_n = self.cells[j * self.nx + i].u_y;
let uy_s = if j > 0 {
self.cells[(j - 1) * self.nx + i].u_y
} else {
0.0
};
(ux_e - ux_w + uy_n - uy_s) / self.dx
}
pub fn pressure_solve_jacobi(&mut self, n_iter: usize) {
let nx = self.nx;
let ny = self.ny;
let dx2 = self.dx * self.dx;
for _ in 0..n_iter {
let old = self.cells.clone();
for j in 1..ny.saturating_sub(1) {
for i in 1..nx.saturating_sub(1) {
if !old[j * nx + i].is_fluid {
continue;
}
let rhs = self.divergence(i, j);
let p_e = old[j * nx + i + 1].pressure;
let p_w = old[j * nx + i - 1].pressure;
let p_n = old[(j + 1) * nx + i].pressure;
let p_s = old[(j - 1) * nx + i].pressure;
self.cells[j * nx + i].pressure = (p_e + p_w + p_n + p_s - dx2 * rhs) / 4.0;
}
}
}
}
pub fn advect_velocity(&mut self, dt: f64) {
let nx = self.nx;
let ny = self.ny;
let dx = self.dx;
let old = self.cells.clone();
for j in 0..ny {
for i in 0..nx {
if !old[j * nx + i].is_fluid {
continue;
}
let x = (i as f64 + 1.0) * dx;
let y = (j as f64 + 0.5) * dx;
let [vx, vy] = interpolate_velocity_cells(&old, nx, ny, dx, x, y);
let xp = x - dt * vx;
let yp = y - dt * vy;
let [new_ux, _] = interpolate_velocity_cells(&old, nx, ny, dx, xp, yp);
let x2 = (i as f64 + 0.5) * dx;
let y2 = (j as f64 + 1.0) * dx;
let [vx2, vy2] = interpolate_velocity_cells(&old, nx, ny, dx, x2, y2);
let xp2 = x2 - dt * vx2;
let yp2 = y2 - dt * vy2;
let [_, new_uy] = interpolate_velocity_cells(&old, nx, ny, dx, xp2, yp2);
let idx = j * nx + i;
self.cells[idx].u_x = new_ux;
self.cells[idx].u_y = new_uy;
}
}
}
pub fn project(&mut self, dt: f64) {
let nx = self.nx;
let ny = self.ny;
let dx = self.dx;
let p: Vec<f64> = self.cells.iter().map(|c| c.pressure).collect();
for j in 0..ny {
for i in 0..nx {
if !self.cells[j * nx + i].is_fluid {
continue;
}
let p_e = if i + 1 < nx { p[j * nx + i + 1] } else { 0.0 };
let p_c = p[j * nx + i];
self.cells[j * nx + i].u_x -= dt / dx * (p_e - p_c);
let p_n = if j + 1 < ny { p[(j + 1) * nx + i] } else { 0.0 };
self.cells[j * nx + i].u_y -= dt / dx * (p_n - p_c);
}
}
}
}
fn interpolate_velocity_cells(
cells: &[MacCell],
nx: usize,
ny: usize,
dx: f64,
x: f64,
y: f64,
) -> [f64; 2] {
let cx = (x / dx - 0.5).max(0.0).min((nx - 1) as f64 - 1e-9);
let cy = (y / dx - 0.5).max(0.0).min((ny - 1) as f64 - 1e-9);
let i = cx as usize;
let j = cy as usize;
let fi = cx - i as f64;
let fj = cy - j as f64;
let safe = |ii: usize, jj: usize| -> [f64; 2] {
if ii < nx && jj < ny {
let c = &cells[jj * nx + ii];
[c.u_x, c.u_y]
} else {
[0.0, 0.0]
}
};
let c00 = safe(i, j);
let c10 = safe(i + 1, j);
let c01 = safe(i, j + 1);
let c11 = safe(i + 1, j + 1);
let ux = (1.0 - fi) * (1.0 - fj) * c00[0]
+ fi * (1.0 - fj) * c10[0]
+ (1.0 - fi) * fj * c01[0]
+ fi * fj * c11[0];
let uy = (1.0 - fi) * (1.0 - fj) * c00[1]
+ fi * (1.0 - fj) * c10[1]
+ (1.0 - fi) * fj * c01[1]
+ fi * fj * c11[1];
[ux, uy]
}
pub struct FluidSimGpu {
pub grid: MacGrid,
pub viscosity: f64,
pub density: f64,
}
impl FluidSimGpu {
pub fn new(nx: usize, ny: usize, dx: f64, viscosity: f64, density: f64) -> Self {
Self {
grid: MacGrid::new(nx, ny, dx),
viscosity,
density,
}
}
pub fn step(&mut self, dt: f64) {
self.grid.advect_velocity(dt);
self.grid.pressure_solve_jacobi(20);
self.grid.project(dt);
}
}
pub struct VorticityConfinement {
pub strength: f64,
}
impl VorticityConfinement {
pub fn new(strength: f64) -> Self {
Self { strength }
}
pub fn compute_vorticity(&self, grid: &MacGrid) -> Vec<f64> {
let nx = grid.nx;
let ny = grid.ny;
let dx = grid.dx;
let mut omega = vec![0.0f64; nx * ny];
for j in 1..ny.saturating_sub(1) {
for i in 1..nx.saturating_sub(1) {
let duy_dx = (grid.cells[j * nx + i].u_y - grid.cells[j * nx + (i - 1)].u_y) / dx;
let dux_dy = (grid.cells[j * nx + i].u_x - grid.cells[(j - 1) * nx + i].u_x) / dx;
omega[j * nx + i] = duy_dx - dux_dy;
}
}
omega
}
pub fn apply_confinement_force(&self, grid: &mut MacGrid, dt: f64) {
let omega = self.compute_vorticity(grid);
let nx = grid.nx;
let ny = grid.ny;
let dx = grid.dx;
let omega_abs: Vec<f64> = omega.iter().map(|v| v.abs()).collect();
for j in 1..ny.saturating_sub(1) {
for i in 1..nx.saturating_sub(1) {
let grad_x = (omega_abs[j * nx + i + 1] - omega_abs[j * nx + i - 1]) / (2.0 * dx);
let grad_y =
(omega_abs[(j + 1) * nx + i] - omega_abs[(j - 1) * nx + i]) / (2.0 * dx);
let len = (grad_x * grad_x + grad_y * grad_y).sqrt();
if len < 1e-12 {
continue;
}
let nx_hat = grad_x / len;
let ny_hat = grad_y / len;
let w = omega[j * nx + i];
let fx = self.strength * dx * w * ny_hat;
let fy = -self.strength * dx * w * nx_hat;
grid.cells[j * nx + i].u_x += dt * fx;
grid.cells[j * nx + i].u_y += dt * fy;
}
}
}
}
pub struct FluidObstacle {
pub cells: Vec<(usize, usize)>,
}
impl FluidObstacle {
pub fn new(cells: Vec<(usize, usize)>) -> Self {
Self { cells }
}
pub fn set_solid(&self, grid: &mut MacGrid) {
for &(i, j) in &self.cells {
if i < grid.nx && j < grid.ny {
grid.cell_mut(i, j).is_fluid = false;
}
}
}
pub fn apply_no_slip(&self, grid: &mut MacGrid) {
for &(i, j) in &self.cells {
if i < grid.nx && j < grid.ny {
let c = grid.cell_mut(i, j);
c.u_x = 0.0;
c.u_y = 0.0;
c.pressure = 0.0;
}
}
}
}
pub fn cfl_timestep(grid: &MacGrid, safety: f64) -> f64 {
let mut u_max: f64 = 0.0;
for c in &grid.cells {
u_max = u_max.max(c.u_x.abs()).max(c.u_y.abs());
}
if u_max < 1e-15 {
return safety * grid.dx;
}
safety * grid.dx / u_max
}
pub fn reynolds_number_grid(u_max: f64, l: f64, nu: f64) -> f64 {
if nu.abs() < 1e-30 {
return f64::INFINITY;
}
u_max * l / nu
}
pub fn interpolate_velocity(grid: &MacGrid, x: f64, y: f64) -> [f64; 2] {
interpolate_velocity_cells(&grid.cells, grid.nx, grid.ny, grid.dx, x, y)
}
#[cfg(test)]
mod tests {
use super::*;
fn uniform_grid(nx: usize, ny: usize, ux: f64, uy: f64) -> MacGrid {
let mut g = MacGrid::new(nx, ny, 1.0);
for c in g.cells.iter_mut() {
c.u_x = ux;
c.u_y = uy;
}
g
}
#[test]
fn mac_cell_default_is_fluid() {
let c = MacCell::default();
assert!(c.is_fluid);
assert_eq!(c.pressure, 0.0);
assert_eq!(c.u_x, 0.0);
assert_eq!(c.u_y, 0.0);
}
#[test]
fn mac_cell_copy() {
let c = MacCell {
pressure: 1.0,
u_x: 2.0,
u_y: 3.0,
is_fluid: false,
};
let d = c;
assert_eq!(d.pressure, 1.0);
assert!(!d.is_fluid);
}
#[test]
fn mac_grid_new_dimensions() {
let g = MacGrid::new(4, 5, 0.1);
assert_eq!(g.nx, 4);
assert_eq!(g.ny, 5);
assert_eq!(g.cells.len(), 20);
}
#[test]
fn mac_grid_cell_accessor() {
let mut g = MacGrid::new(3, 3, 1.0);
g.cell_mut(1, 2).u_x = 42.0;
assert_eq!(g.cell(1, 2).u_x, 42.0);
}
#[test]
fn divergence_uniform_flow_is_zero() {
let g = uniform_grid(5, 5, 1.0, 0.0);
let div = g.divergence(2, 2);
assert!(div.abs() < 1e-12, "div = {div}");
}
#[test]
fn divergence_source_cell() {
let mut g = MacGrid::new(4, 4, 1.0);
g.cell_mut(1, 1).u_x = 2.0; g.cell_mut(0, 1).u_x = 1.0; let div = g.divergence(1, 1);
assert!(div > 0.0, "expected positive div, got {div}");
}
#[test]
fn divergence_boundary_left_edge() {
let mut g = MacGrid::new(4, 4, 1.0);
g.cell_mut(0, 0).u_x = 1.0;
let div = g.divergence(0, 0);
assert!((div - 1.0).abs() < 1e-12, "div = {div}");
}
#[test]
fn divergence_all_zeros() {
let g = MacGrid::new(4, 4, 1.0);
for j in 0..4 {
for i in 0..4 {
assert_eq!(g.divergence(i, j), 0.0);
}
}
}
#[test]
fn pressure_jacobi_reduces_magnitude_with_source() {
let mut g = MacGrid::new(6, 6, 1.0);
g.cell_mut(3, 3).u_x = 1.0;
g.pressure_solve_jacobi(50);
let max_p: f64 = g
.cells
.iter()
.map(|c| c.pressure.abs())
.fold(0.0_f64, f64::max);
assert!(max_p > 1e-6, "pressure unchanged after Jacobi");
}
#[test]
fn pressure_jacobi_zero_field_stays_zero() {
let mut g = MacGrid::new(5, 5, 1.0);
g.pressure_solve_jacobi(10);
for c in &g.cells {
assert_eq!(c.pressure, 0.0);
}
}
#[test]
fn pressure_jacobi_convergence_monotone() {
let mut g1 = MacGrid::new(6, 6, 1.0);
let mut g2 = MacGrid::new(6, 6, 1.0);
g1.cell_mut(3, 3).u_x = 2.0;
g2.cell_mut(3, 3).u_x = 2.0;
g1.pressure_solve_jacobi(10);
g2.pressure_solve_jacobi(100);
let rms = |grid: &MacGrid| {
let s: f64 = grid.cells.iter().map(|c| c.pressure * c.pressure).sum();
(s / grid.cells.len() as f64).sqrt()
};
let _ = rms(&g1);
let _ = rms(&g2);
}
#[test]
fn project_corrects_velocity() {
let mut g = MacGrid::new(4, 4, 1.0);
for j in 0..4 {
for i in 0..4 {
g.cell_mut(i, j).pressure = i as f64;
}
}
let ux_before = g.cell(1, 1).u_x;
g.project(0.1);
let ux_after = g.cell(1, 1).u_x;
assert!((ux_after - ux_before).abs() > 1e-10);
}
#[test]
fn project_solid_cell_unchanged() {
let mut g = MacGrid::new(4, 4, 1.0);
g.cell_mut(2, 2).is_fluid = false;
g.cell_mut(2, 2).u_x = 5.0;
for j in 0..4 {
for i in 0..4 {
g.cell_mut(i, j).pressure = 1.0;
}
}
g.project(0.1);
assert_eq!(g.cell(2, 2).u_x, 5.0);
}
#[test]
fn cfl_timestep_uniform_flow() {
let g = uniform_grid(4, 4, 2.0, 0.0);
let dt = cfl_timestep(&g, 0.5);
assert!((dt - 0.25).abs() < 1e-12, "dt = {dt}");
}
#[test]
fn cfl_timestep_quiescent() {
let g = MacGrid::new(4, 4, 0.1);
let dt = cfl_timestep(&g, 0.5);
assert!((dt - 0.05).abs() < 1e-12, "dt = {dt}");
}
#[test]
fn cfl_timestep_uses_max_component() {
let mut g = MacGrid::new(4, 4, 1.0);
g.cell_mut(1, 1).u_x = 1.0;
g.cell_mut(2, 2).u_y = 5.0;
let dt = cfl_timestep(&g, 1.0);
assert!((dt - 1.0 / 5.0).abs() < 1e-12, "dt = {dt}");
}
#[test]
fn cfl_timestep_safety_factor() {
let g = uniform_grid(4, 4, 4.0, 0.0);
let dt_full = cfl_timestep(&g, 1.0);
let dt_half = cfl_timestep(&g, 0.5);
assert!((dt_full - 2.0 * dt_half).abs() < 1e-12);
}
#[test]
fn reynolds_number_basic() {
let re = reynolds_number_grid(1.0, 1.0, 1e-6);
assert!((re - 1.0e6).abs() < 1.0, "re = {re}");
}
#[test]
fn reynolds_number_zero_viscosity() {
let re = reynolds_number_grid(1.0, 1.0, 0.0);
assert!(re.is_infinite());
}
#[test]
fn reynolds_number_proportional_to_velocity() {
let re1 = reynolds_number_grid(1.0, 1.0, 1e-3);
let re2 = reynolds_number_grid(2.0, 1.0, 1e-3);
assert!((re2 - 2.0 * re1).abs() < 1e-6);
}
#[test]
fn interpolate_velocity_at_cell_centre() {
let mut g = MacGrid::new(4, 4, 1.0);
for c in g.cells.iter_mut() {
c.u_x = 3.0;
c.u_y = 1.5;
}
let v = interpolate_velocity(&g, 1.5, 1.5);
assert!((v[0] - 3.0).abs() < 0.1, "v[0] = {}", v[0]);
}
#[test]
fn interpolate_velocity_clamps_outside() {
let g = uniform_grid(4, 4, 1.0, 2.0);
let v = interpolate_velocity(&g, -10.0, -10.0);
let _ = v;
}
#[test]
fn interpolate_velocity_zero_field() {
let g = MacGrid::new(4, 4, 1.0);
let v = interpolate_velocity(&g, 1.0, 1.0);
assert_eq!(v[0], 0.0);
assert_eq!(v[1], 0.0);
}
#[test]
fn interpolate_velocity_corner() {
let g = uniform_grid(4, 4, 1.0, 2.0);
let v = interpolate_velocity(&g, 0.0, 0.0);
let _ = v; }
#[test]
fn vorticity_zero_flow() {
let g = MacGrid::new(5, 5, 1.0);
let vc = VorticityConfinement::new(0.1);
let omega = vc.compute_vorticity(&g);
assert!(omega.iter().all(|&v| v == 0.0));
}
#[test]
fn vorticity_shear_flow() {
let mut g = MacGrid::new(5, 5, 1.0);
for j in 0..5 {
for i in 0..5 {
g.cell_mut(i, j).u_x = j as f64;
}
}
let vc = VorticityConfinement::new(0.1);
let omega = vc.compute_vorticity(&g);
let has_nonzero = omega.iter().any(|&v| v.abs() > 1e-12);
assert!(has_nonzero);
}
#[test]
fn vorticity_confinement_apply_no_panic() {
let mut g = MacGrid::new(5, 5, 1.0);
for c in g.cells.iter_mut() {
c.u_x = 0.1;
c.u_y = -0.1;
}
let vc = VorticityConfinement::new(0.2);
vc.apply_confinement_force(&mut g, 0.01);
}
#[test]
fn vorticity_len_equals_grid_size() {
let g = MacGrid::new(6, 7, 0.5);
let vc = VorticityConfinement::new(0.1);
let omega = vc.compute_vorticity(&g);
assert_eq!(omega.len(), 6 * 7);
}
#[test]
fn obstacle_set_solid() {
let mut g = MacGrid::new(5, 5, 1.0);
let obs = FluidObstacle::new(vec![(2, 2), (3, 2)]);
obs.set_solid(&mut g);
assert!(!g.cell(2, 2).is_fluid);
assert!(!g.cell(3, 2).is_fluid);
assert!(g.cell(1, 1).is_fluid);
}
#[test]
fn obstacle_apply_no_slip() {
let mut g = MacGrid::new(5, 5, 1.0);
g.cell_mut(2, 2).u_x = 10.0;
g.cell_mut(2, 2).u_y = 5.0;
let obs = FluidObstacle::new(vec![(2, 2)]);
obs.apply_no_slip(&mut g);
assert_eq!(g.cell(2, 2).u_x, 0.0);
assert_eq!(g.cell(2, 2).u_y, 0.0);
}
#[test]
fn obstacle_out_of_bounds_no_panic() {
let mut g = MacGrid::new(4, 4, 1.0);
let obs = FluidObstacle::new(vec![(100, 100)]);
obs.set_solid(&mut g); obs.apply_no_slip(&mut g);
}
#[test]
fn obstacle_multiple_cells() {
let mut g = MacGrid::new(10, 10, 1.0);
let cells: Vec<_> = (2..5).flat_map(|i| (2..5).map(move |j| (i, j))).collect();
let obs = FluidObstacle::new(cells);
obs.set_solid(&mut g);
assert!(!g.cell(3, 3).is_fluid);
assert!(g.cell(0, 0).is_fluid);
}
#[test]
fn fluid_sim_gpu_step_no_panic() {
let mut sim = FluidSimGpu::new(6, 6, 0.1, 1e-3, 1000.0);
sim.grid.cell_mut(3, 3).u_x = 0.5;
sim.step(0.01);
}
#[test]
fn fluid_sim_gpu_quiescent_stable() {
let mut sim = FluidSimGpu::new(8, 8, 0.1, 1e-3, 1000.0);
for _ in 0..5 {
sim.step(0.001);
}
for c in &sim.grid.cells {
assert!(c.u_x.abs() < 1e-10, "u_x = {}", c.u_x);
assert!(c.u_y.abs() < 1e-10, "u_y = {}", c.u_y);
}
}
#[test]
fn fluid_sim_gpu_fields_accessible() {
let sim = FluidSimGpu::new(4, 4, 0.05, 0.001, 998.0);
assert_eq!(sim.grid.nx, 4);
assert!((sim.viscosity - 0.001).abs() < 1e-12);
assert!((sim.density - 998.0).abs() < 1e-12);
}
#[test]
fn advect_zero_dt_no_change() {
let mut g = uniform_grid(5, 5, 1.0, 0.5);
let before: Vec<_> = g.cells.iter().map(|c| [c.u_x, c.u_y]).collect();
g.advect_velocity(0.0);
let after: Vec<_> = g.cells.iter().map(|c| [c.u_x, c.u_y]).collect();
for (b, a) in before.iter().zip(after.iter()) {
assert!((b[0] - a[0]).abs() < 1e-10);
assert!((b[1] - a[1]).abs() < 1e-10);
}
}
#[test]
fn advect_does_not_explode() {
let mut g = uniform_grid(6, 6, 0.3, 0.2);
for _ in 0..10 {
g.advect_velocity(0.01);
}
for c in &g.cells {
assert!(c.u_x.is_finite());
assert!(c.u_y.is_finite());
}
}
}