use hisab::DVec3;
use pravash::buoyancy::{self, DragCoefficient, FlowRegime};
use pravash::common::{FluidConfig, FluidMaterial, FluidParticle};
use pravash::coupling::{self, BodyShape, FlipSolver, RigidBody};
use pravash::grid::FluidGrid;
use pravash::shallow::ShallowWater;
use pravash::sph::{self, SphSolver};
use pravash::vortex;
#[test]
fn sph_dam_break_energy_bounded() {
let mut particles = sph::create_particle_block([0.1, 0.3], [0.2, 0.3], 0.02, 0.001);
let config = FluidConfig::water_2d();
let viscosity = FluidMaterial::WATER.viscosity;
let mut max_ke = 0.0f64;
for _ in 0..200 {
sph::step(&mut particles, &config, viscosity).unwrap();
let ke = sph::total_kinetic_energy(&particles);
max_ke = max_ke.max(ke);
assert!(ke.is_finite(), "kinetic energy diverged to non-finite");
}
assert!(max_ke < 1e6, "kinetic energy exploded: {max_ke}");
}
#[test]
fn sph_particles_stay_in_bounds() {
let mut particles = sph::create_particle_block([0.1, 0.5], [0.3, 0.3], 0.02, 0.001);
let config = FluidConfig::water_2d();
let viscosity = FluidMaterial::WATER.viscosity;
for _ in 0..500 {
sph::step(&mut particles, &config, viscosity).unwrap();
}
let lo = config.bounds_min;
let hi = config.bounds_max;
for (i, p) in particles.iter().enumerate() {
assert!(
p.position.x >= lo.x && p.position.x <= hi.x,
"particle {i} x={} out of bounds [{}, {}]",
p.position.x,
lo.x,
hi.x
);
assert!(
p.position.y >= lo.y && p.position.y <= hi.y,
"particle {i} y={} out of bounds [{}, {}]",
p.position.y,
lo.y,
hi.y
);
}
}
#[test]
fn sph_config_validation_catches_bad_params() {
let mut config = FluidConfig::water_2d();
config.dt = 0.0;
let mut particles = sph::create_particle_block([0.1, 0.1], [0.1, 0.1], 0.05, 0.001);
assert!(sph::step(&mut particles, &config, 0.001).is_err());
}
#[test]
fn sph_single_particle_gravity() {
let mut particles = vec![FluidParticle::new_2d(0.5, 0.8, 0.01)];
particles[0].density = 1000.0;
let config = FluidConfig::water_2d();
let y_initial = particles[0].position[1];
for _ in 0..10 {
sph::step(&mut particles, &config, 0.001).unwrap();
}
assert!(
particles[0].position[1] < y_initial,
"particle should fall under gravity"
);
}
#[test]
fn grid_diffusion_conserves_total() {
let nx = 20;
let ny = 20;
let mut field = vec![0.0; nx * ny];
field[nx * 10 + 10] = 100.0;
let total_before: f64 = field.iter().sum();
FluidGrid::diffuse(&mut field, nx, ny, 0.1, 0.01, 0.1, 50);
let total_after: f64 = field.iter().sum();
assert!(
(total_after - total_before).abs() / total_before.abs() < 0.5,
"diffusion lost too much mass: before={total_before}, after={total_after}"
);
}
#[test]
fn grid_large_grid_creation() {
let grid = FluidGrid::new(256, 256, 0.01).unwrap();
assert_eq!(grid.cell_count(), 256 * 256);
assert!(grid.max_speed().abs() < f64::EPSILON);
}
#[test]
fn grid_navier_stokes_smoke_rises() {
use pravash::grid::GridConfig;
let mut g = FluidGrid::new(30, 30, 0.1).unwrap();
let config = GridConfig::smoke();
for x in 12..18 {
let i = 2 * 30 + x;
g.density[i] = 1.0;
g.vy[i] = 2.0;
}
for _ in 0..50 {
for x in 12..18 {
g.density[2 * 30 + x] = 1.0;
}
g.step(&config).unwrap();
}
assert!(g.max_speed() > 0.01);
assert!(g.max_speed().is_finite());
for v in &g.vx {
assert!(v.is_finite());
}
for v in &g.vy {
assert!(v.is_finite());
}
}
#[test]
fn grid_navier_stokes_stable_empty() {
use pravash::grid::GridConfig;
let mut g = FluidGrid::new(20, 20, 0.1).unwrap();
let config = GridConfig::smoke();
for _ in 0..100 {
g.step(&config).unwrap();
}
assert!(g.max_speed() < f64::EPSILON);
}
#[test]
fn shallow_wave_propagation() {
let mut sw = ShallowWater::new(20, 20, 0.1, 1.0).unwrap();
sw.add_disturbance(1.0, 1.0, 0.3, 0.5);
let near_before = sw.surface_at(5, 5);
for _ in 0..500 {
sw.step(0.001).unwrap();
}
let near_after = sw.surface_at(5, 5);
assert!(
(near_after - near_before).abs() > 1e-8,
"wave didn't propagate: before={near_before}, after={near_after}"
);
}
#[test]
fn shallow_volume_approximately_conserved() {
let mut sw = ShallowWater::new(30, 30, 0.1, 1.0).unwrap();
sw.add_disturbance(1.5, 1.5, 0.3, 0.2);
let vol_before = sw.total_volume();
for _ in 0..100 {
sw.step(0.001).unwrap();
}
let vol_after = sw.total_volume();
let relative_change = (vol_after - vol_before).abs() / vol_before;
assert!(
relative_change < 0.1,
"volume changed by {:.1}%",
relative_change * 100.0
);
}
#[test]
fn shallow_flat_surface_stable() {
let mut sw = ShallowWater::new(20, 20, 0.1, 1.0).unwrap();
for _ in 0..1000 {
sw.step(0.001).unwrap();
}
let max_dev = sw.max_wave_height(1.0);
assert!(max_dev < 1e-10, "flat surface drifted: {max_dev}");
}
#[test]
fn materials_have_consistent_physics() {
let materials = [
("water", FluidMaterial::WATER),
("oil", FluidMaterial::OIL),
("honey", FluidMaterial::HONEY),
("air", FluidMaterial::AIR),
("lava", FluidMaterial::LAVA),
];
for (name, m) in &materials {
assert!(m.density > 0.0, "{name} has non-positive density");
assert!(m.viscosity >= 0.0, "{name} has negative viscosity");
assert!(
m.speed_of_sound > 0.0,
"{name} has non-positive speed of sound"
);
}
const { assert!(FluidMaterial::AIR.density < FluidMaterial::WATER.density) };
const { assert!(FluidMaterial::WATER.viscosity < FluidMaterial::HONEY.viscosity) };
const { assert!(FluidMaterial::AIR.speed_of_sound < FluidMaterial::WATER.speed_of_sound) };
}
#[test]
fn serde_roundtrip_all_types() {
let config = FluidConfig::water_2d();
let json = serde_json::to_string(&config).unwrap();
let config2: FluidConfig = serde_json::from_str(&json).unwrap();
assert!((config2.dt - config.dt).abs() < f64::EPSILON);
let mat = FluidMaterial::WATER;
let json = serde_json::to_string(&mat).unwrap();
let mat2: FluidMaterial = serde_json::from_str(&json).unwrap();
assert!((mat2.density - mat.density).abs() < f64::EPSILON);
let grid = FluidGrid::new(5, 5, 0.1).unwrap();
let json = serde_json::to_string(&grid).unwrap();
let grid2: FluidGrid = serde_json::from_str(&json).unwrap();
assert_eq!(grid2.nx, grid.nx);
let sw = ShallowWater::new(5, 5, 0.1, 1.0).unwrap();
let json = serde_json::to_string(&sw).unwrap();
let sw2: ShallowWater = serde_json::from_str(&json).unwrap();
assert_eq!(sw2.nx, sw.nx);
}
#[test]
fn buoyancy_sphere_in_water() {
let density_water = FluidMaterial::WATER.density;
let radius: f64 = 0.062;
let volume = (4.0 / 3.0) * std::f64::consts::PI * radius.powi(3);
let buoyancy = buoyancy::buoyancy_force(density_water, 9.81, volume);
let weight = 1.0 * 9.81;
assert!(buoyancy > weight * 0.9);
}
#[test]
fn buoyancy_terminal_velocity_validation() {
let vt = buoyancy::terminal_velocity(1.0, 9.81, 1.225, DragCoefficient::SPHERE, 0.01);
assert!(vt.is_ok());
assert!(vt.unwrap() > 0.0);
assert!(buoyancy::terminal_velocity(1.0, 9.81, 0.0, DragCoefficient::SPHERE, 0.01).is_err());
assert!(buoyancy::terminal_velocity(1.0, -9.81, 1.225, DragCoefficient::SPHERE, 0.01).is_err());
}
#[test]
fn buoyancy_reynolds_flow_regime() {
let viscosity_water = FluidMaterial::WATER.viscosity;
let density_water = FluidMaterial::WATER.density;
let re_slow = buoyancy::reynolds_number(density_water, 0.001, 0.01, viscosity_water).unwrap();
assert_eq!(buoyancy::classify_flow(re_slow), FlowRegime::Laminar);
let re_fast = buoyancy::reynolds_number(density_water, 10.0, 0.1, viscosity_water).unwrap();
assert_eq!(buoyancy::classify_flow(re_fast), FlowRegime::Turbulent);
assert!(buoyancy::reynolds_number(density_water, 1.0, 0.1, 0.0).is_err());
}
#[test]
fn vortex_lamb_oseen_decays() {
let circulation = 1.0;
let r = 0.1;
let viscosity = 0.01;
let v_early = vortex::lamb_oseen_velocity(circulation, r, viscosity, 0.1);
let v_late = vortex::lamb_oseen_velocity(circulation, r, viscosity, 10.0);
assert!(v_early.is_finite());
assert!(v_late.is_finite());
}
#[test]
fn vortex_rankine_vs_lamb_oseen_convergence() {
let circulation = 5.0;
let r_far = 100.0;
let v_rankine = vortex::rankine_velocity(circulation, r_far, 1.0);
let v_lo = vortex::lamb_oseen_velocity(circulation, r_far, 0.001, 1.0);
let v_irrot = circulation / (2.0 * std::f64::consts::PI * r_far);
assert!((v_rankine - v_irrot).abs() < 1e-6);
assert!((v_lo - v_irrot).abs() < 1e-3);
}
#[test]
fn vortex_enstrophy_scales_with_dx() {
let field = vec![1.0; 100];
let e1 = vortex::enstrophy(&field, 0.1);
let e2 = vortex::enstrophy(&field, 0.2);
assert!((e2 / e1 - 4.0).abs() < 1e-10);
}
#[test]
fn config_validation_comprehensive() {
let mut c = FluidConfig::water_2d();
assert!(c.validate().is_ok());
c.gas_constant = 0.0;
assert!(c.validate().is_err());
c = FluidConfig::water_2d();
c.boundary_damping = 1.5;
assert!(c.validate().is_err());
c = FluidConfig::water_2d();
c.boundary_damping = -0.1;
assert!(c.validate().is_err());
}
#[test]
fn grid_invalid_dx_rejected() {
assert!(FluidGrid::new(10, 10, 0.0).is_err());
assert!(FluidGrid::new(10, 10, -1.0).is_err());
}
#[test]
fn custom_material_speed_of_sound_validated() {
assert!(FluidMaterial::custom(1000.0, 0.001, 0.072, 0.0).is_err());
assert!(FluidMaterial::custom(1000.0, 0.001, 0.072, -1.0).is_err());
assert!(FluidMaterial::custom(1000.0, 0.001, 0.072, 1480.0).is_ok());
}
#[test]
fn particle_is_copy() {
let p = FluidParticle::new_2d(1.0, 2.0, 0.01);
let p2 = p;
let _ = p; let _ = p2;
}
#[test]
fn config_is_copy() {
let c = FluidConfig::water_2d();
let c2 = c;
let _ = c; let _ = c2;
}
#[test]
fn solver_dam_break_energy_bounded() {
let mut particles = sph::create_particle_block([0.1, 0.3], [0.2, 0.3], 0.02, 0.001);
let config = FluidConfig::water_2d();
let viscosity = FluidMaterial::WATER.viscosity;
let mut solver = SphSolver::new();
let mut max_ke = 0.0f64;
for _ in 0..200 {
solver.step(&mut particles, &config, viscosity).unwrap();
let ke = sph::total_kinetic_energy(&particles);
max_ke = max_ke.max(ke);
assert!(ke.is_finite(), "kinetic energy diverged to non-finite");
}
assert!(max_ke < 1e6, "kinetic energy exploded: {max_ke}");
}
#[test]
fn solver_particles_stay_in_bounds() {
let mut particles = sph::create_particle_block([0.1, 0.5], [0.3, 0.3], 0.02, 0.001);
let config = FluidConfig::water_2d();
let viscosity = FluidMaterial::WATER.viscosity;
let mut solver = SphSolver::new();
for _ in 0..500 {
solver.step(&mut particles, &config, viscosity).unwrap();
}
let lo = config.bounds_min;
let hi = config.bounds_max;
for (i, p) in particles.iter().enumerate() {
assert!(
p.position.x >= lo.x && p.position.x <= hi.x,
"particle {i} x={} out of bounds [{}, {}]",
p.position.x,
lo.x,
hi.x
);
assert!(
p.position.y >= lo.y && p.position.y <= hi.y,
"particle {i} y={} out of bounds [{}, {}]",
p.position.y,
lo.y,
hi.y
);
}
}
#[test]
fn solver_surface_tension_keeps_blob_compact() {
let make_block = || sph::create_particle_block([0.3, 0.3], [0.2, 0.2], 0.02, 0.001);
let config = FluidConfig::water_2d();
let viscosity = FluidMaterial::WATER.viscosity;
let mut particles_no_st = make_block();
let mut solver_no_st = SphSolver::new();
for _ in 0..50 {
solver_no_st
.step(&mut particles_no_st, &config, viscosity)
.unwrap();
}
let spread_no_st: f64 = particles_no_st.iter().map(|p| p.speed_squared()).sum();
let mut particles_st = make_block();
let mut solver_st = SphSolver::with_surface_tension(0.072);
for _ in 0..50 {
solver_st
.step(&mut particles_st, &config, viscosity)
.unwrap();
}
assert!(spread_no_st.is_finite());
for p in &particles_st {
assert!(p.position[0].is_finite());
assert!(p.position[1].is_finite());
}
}
#[test]
fn solver_multi_step_consistency() {
let mut particles = sph::create_particle_block([0.2, 0.5], [0.3, 0.3], 0.03, 0.001);
let config = FluidConfig::water_2d();
let mut solver = SphSolver::new();
for step_num in 0..1000 {
solver.step(&mut particles, &config, 0.001).unwrap();
if step_num % 100 == 0 {
for p in particles.iter() {
assert!(
p.position[0].is_finite() && p.position[1].is_finite(),
"diverged at step {step_num}"
);
}
}
}
}
#[test]
fn solver_symmetric_pressure_conserves_momentum() {
let mut particles = sph::create_particle_block([0.4, 0.4], [0.2, 0.2], 0.02, 0.001);
let mut config = FluidConfig::water_2d();
config.gravity = DVec3::ZERO;
let mut solver = SphSolver::new();
solver.step(&mut particles, &config, 0.001).unwrap();
let total_px: f64 = particles.iter().map(|p| p.velocity.x * p.mass).sum();
let total_py: f64 = particles.iter().map(|p| p.velocity.y * p.mass).sum();
assert!(
total_px.abs() < 1e-8,
"x-momentum not conserved: {total_px}"
);
assert!(
total_py.abs() < 1e-8,
"y-momentum not conserved: {total_py}"
);
}
#[test]
fn coupling_sphere_falls_through_fluid() {
let mut particles = sph::create_particle_block([0.2, 0.1], [0.6, 0.4], 0.03, 0.001);
let config = FluidConfig::water_2d();
let mut solver = SphSolver::new();
let mut body = RigidBody::new(
DVec3::new(0.5, 0.7, 0.0),
0.1,
BodyShape::Sphere { radius: 0.05 },
);
let y_start = body.position.y;
for _ in 0..50 {
solver.step(&mut particles, &config, 0.001).unwrap();
coupling::couple_sph_bodies(&mut particles, &mut [body.clone()], 0.05, 500.0, 5.0);
coupling::integrate_bodies(std::slice::from_mut(&mut body), config.gravity, config.dt);
}
assert!(
body.position.y < y_start,
"body should fall: y_start={y_start}, y_now={}",
body.position.y
);
assert!(body.position.y.is_finite());
}
#[test]
fn coupling_body_receives_force() {
let mut particles = vec![FluidParticle::new(DVec3::new(0.55, 0.0, 0.0), 1.0)];
particles[0].density = 1000.0;
particles[0].velocity = DVec3::new(1.0, 0.0, 0.0);
let mut bodies = vec![RigidBody::new(
DVec3::new(0.5, 0.0, 0.0),
10.0,
BodyShape::Sphere { radius: 0.1 },
)];
coupling::couple_sph_bodies(&mut particles, &mut bodies, 0.15, 1000.0, 10.0);
let force_mag = bodies[0].force.length();
assert!(force_mag > 0.0, "body should receive force from particle");
}
#[test]
fn flip_particles_fall_under_gravity() {
let mut solver = FlipSolver::new(16, 16, 0.1, 0.95).unwrap();
let mut particles = vec![
FluidParticle::new_2d(0.5, 0.8, 0.01),
FluidParticle::new_2d(0.6, 0.8, 0.01),
FluidParticle::new_2d(0.7, 0.8, 0.01),
];
for _ in 0..10 {
solver
.step(&mut particles, DVec3::new(0.0, -9.81, 0.0), 0.01)
.unwrap();
}
for p in &particles {
assert!(p.position.y < 0.8, "particle should fall");
assert!(p.position.y.is_finite());
}
}
#[test]
fn flip_multi_step_stable() {
let mut solver = FlipSolver::new(16, 16, 0.1, 0.9).unwrap();
let mut particles: Vec<FluidParticle> = (0..20)
.map(|i| {
let x = 0.3 + (i % 5) as f64 * 0.05;
let y = 0.3 + (i / 5) as f64 * 0.05;
FluidParticle::new_2d(x, y, 0.01)
})
.collect();
for _ in 0..100 {
solver
.step(&mut particles, DVec3::new(0.0, -1.0, 0.0), 0.01)
.unwrap();
}
for p in &particles {
assert!(p.position.x.is_finite());
assert!(p.position.y.is_finite());
}
}