#![allow(missing_docs)]
use oxiphysics_sph::particle::SphParticleSet;
use oxiphysics_sph::simulation::types_sim::WcSphSim;
const COLUMN_W: f64 = 0.5;
const COLUMN_H: f64 = 1.0;
const DX: f64 = 0.05;
const NZ: usize = 7;
const NZ_CENTER: usize = NZ / 2;
const Z_TOL: f64 = 0.25 * DX;
const WALL_LAYERS: usize = 3;
const H_OVER_DX: f64 = 1.3;
const RHO0: f64 = 1000.0;
const GAMMA: f64 = 7.0;
const C0: f64 = 40.0;
const MU: f64 = 0.01;
const GRAVITY: [f64; 3] = [0.0, -9.81, 0.0];
const DT: f64 = 2.5e-4;
const T_END: f64 = 0.3;
const DOMAIN_X_MAX: f64 = 4.0;
const FIT_T_MIN: f64 = 0.05;
const FIT_T_MAX: f64 = 0.2;
const C_REF: f64 = 2.0;
const C_FLOOR: f64 = 0.01;
fn build_initial_state() -> (SphParticleSet, usize) {
let mut ps = SphParticleSet::new();
let mass = RHO0 * DX * DX * DX;
let viscosity = MU;
let nx_fluid = (COLUMN_W / DX).round() as usize;
let ny_fluid = (COLUMN_H / DX).round() as usize;
for i in 0..nx_fluid {
for j in 0..ny_fluid {
for k in 0..NZ {
let pos = [(i as f64 + 0.5) * DX, (j as f64 + 0.5) * DX, k as f64 * DX];
ps.add_raw(pos, [0.0, 0.0, 0.0], mass, viscosity, false);
}
}
}
let n_fluid = ps.len();
let x_floor_start = -(WALL_LAYERS as f64) * DX;
let x_floor_end = DOMAIN_X_MAX + WALL_LAYERS as f64 * DX;
let nx_floor = ((x_floor_end - x_floor_start) / DX).round() as usize;
for yi in 1..=WALL_LAYERS {
let y = -(yi as f64) * DX;
for i in 0..nx_floor {
let x = x_floor_start + (i as f64 + 0.5) * DX;
for k in 0..NZ {
let pos = [x, y, k as f64 * DX];
ps.add_raw(pos, [0.0, 0.0, 0.0], mass, viscosity, true);
}
}
}
let y_wall_top = COLUMN_H + (WALL_LAYERS as f64) * DX;
let ny_wall = (y_wall_top / DX).round() as usize;
for xi in 1..=WALL_LAYERS {
let x = -(xi as f64) * DX;
for j in 0..ny_wall {
let y = (j as f64 + 0.5) * DX;
for k in 0..NZ {
let pos = [x, y, k as f64 * DX];
ps.add_raw(pos, [0.0, 0.0, 0.0], mass, viscosity, true);
}
}
}
(ps, n_fluid)
}
fn linear_fit(xs: &[f64], ys: &[f64]) -> (f64, f64) {
assert_eq!(xs.len(), ys.len(), "xs/ys length mismatch");
let n = xs.len() as f64;
let sum_x: f64 = xs.iter().sum();
let sum_y: f64 = ys.iter().sum();
let sum_xx: f64 = xs.iter().map(|x| x * x).sum();
let sum_xy: f64 = xs.iter().zip(ys.iter()).map(|(x, y)| x * y).sum();
let denom = n * sum_xx - sum_x * sum_x;
assert!(
denom.abs() > 1e-20,
"linear_fit: degenerate xs (zero variance)"
);
let b = (n * sum_xy - sum_x * sum_y) / denom;
let a = (sum_y - b * sum_x) / n;
(a, b)
}
fn fluid_front_x(ps: &SphParticleSet) -> f64 {
let z_center = NZ_CENTER as f64 * DX;
let mut max_x = f64::NEG_INFINITY;
for (pos, &boundary) in ps.positions.iter().zip(ps.is_boundary.iter()) {
if !boundary && (pos[2] - z_center).abs() < Z_TOL && pos[0] > max_x {
max_x = pos[0];
}
}
max_x
}
fn fluid_density_stats(ps: &SphParticleSet) -> (f64, f64, f64) {
let z_center = NZ_CENTER as f64 * DX;
let mut rho_min = f64::INFINITY;
let mut rho_max = f64::NEG_INFINITY;
let mut rho_sum = 0.0;
let mut n = 0usize;
for ((pos, &boundary), &rho) in ps
.positions
.iter()
.zip(ps.is_boundary.iter())
.zip(ps.densities.iter())
{
if !boundary && (pos[2] - z_center).abs() < Z_TOL {
rho_min = rho_min.min(rho);
rho_max = rho_max.max(rho);
rho_sum += rho;
n += 1;
}
}
let rho_mean = if n > 0 { rho_sum / n as f64 } else { 0.0 };
(rho_min, rho_max, rho_mean)
}
#[test]
#[ignore = "O(n^2) WCSPH dam-break takes ~100 s in release; run with --ignored"]
fn test_dambreak_front_velocity() {
let (particles, n_fluid) = build_initial_state();
let n_total = particles.len();
assert!(
n_fluid > 0,
"no fluid particles generated; check DX/COLUMN_W/COLUMN_H"
);
assert!(
n_total > n_fluid,
"no boundary particles generated; check WALL_LAYERS/DOMAIN_X_MAX"
);
let h = H_OVER_DX * DX;
let mut sim = WcSphSim::new(h, RHO0, C0, GAMMA, MU, 0.0, GRAVITY);
sim.particles = particles;
sim.density_step();
let (rho_min, rho_max, rho_mean) = fluid_density_stats(&sim.particles);
eprintln!(
" t=0 fluid-centre rho: min={rho_min:.1} max={rho_max:.1} \
mean={rho_mean:.1} (rho0={RHO0})"
);
let mut times: Vec<f64> = Vec::new();
let mut fronts: Vec<f64> = Vec::new();
let n_steps = (T_END / DT).round() as usize;
times.push(0.0);
fronts.push(fluid_front_x(&sim.particles));
const BAILOUT_X: f64 = 2.5;
for step in 0..n_steps {
sim.density_step();
sim.pressure_step();
for p in sim.particles.pressures.iter_mut() {
if *p < 0.0 {
*p = 0.0;
}
}
sim.force_step();
sim.integrate(DT);
let max_x = fluid_front_x(&sim.particles);
if !max_x.is_finite() || max_x > BAILOUT_X {
let v_max = sim.particles.max_speed();
panic!(
"SPH simulation diverged (front too far): \
t = {:.4} s, x_front = {:.4} m, |v|_max = {:.2} m/s \
(expected x_front ≲ 0.5 + 2·√(gH)·t = {:.3} m)",
sim.time,
max_x,
v_max,
0.5 + 2.0 * (-GRAVITY[1] * COLUMN_H).sqrt() * sim.time
);
}
if step.is_multiple_of(40) {
let v_max = sim.particles.max_speed();
eprintln!(
" t={:.4}s x_front={:.4}m |v|_max={:.3}m/s",
sim.time, max_x, v_max
);
}
times.push(sim.time);
fronts.push(max_x);
}
let mut ts_fit: Vec<f64> = Vec::new();
let mut xs_fit: Vec<f64> = Vec::new();
for (&t, &x) in times.iter().zip(fronts.iter()) {
if (FIT_T_MIN..=FIT_T_MAX).contains(&t) {
ts_fit.push(t);
xs_fit.push(x);
}
}
assert!(
ts_fit.len() >= 10,
"too few samples in fit window [{FIT_T_MIN}, {FIT_T_MAX}] s: got {}",
ts_fit.len()
);
let (_a, b) = linear_fit(&ts_fit, &xs_fit);
let g = -GRAVITY[1];
let u_ref = (g * COLUMN_H).sqrt();
let c = b / u_ref;
let t0 = times[0];
let x0 = fronts[0];
let t_mid_idx = times.len() / 2;
let t_mid = times[t_mid_idx];
let x_mid = fronts[t_mid_idx];
let t_end = *times.last().expect("time series not empty");
let x_end = *fronts.last().expect("front series not empty");
eprintln!(
" Martin-Moyce measurement: c = u_front / √(gH) = {c:.3} \
(reference = {C_REF:.1}, fitted u_front = {b:.3} m/s, \
√(gH) = {u_ref:.3} m/s)"
);
eprintln!(
" front samples: (t={t0:.3}s, x={x0:.3}m), \
(t={t_mid:.3}s, x={x_mid:.3}m), (t={t_end:.3}s, x={x_end:.3}m); \
{n_fluid} fluid + {} boundary particles, dt={DT} s",
n_total - n_fluid
);
assert!(
c.is_finite(),
"front velocity coefficient is non-finite: c = {c}"
);
assert!(
c >= C_FLOOR,
"front did not advance: measured c = {c:.3e} < floor {C_FLOOR:.2e}; \
fitted u_front = {b:.3e} m/s"
);
}