use pravash::buoyancy;
use pravash::{FluidConfig, FluidParticle};
pub mod fluids {
use pravash::FluidMaterial;
pub const GROUNDWATER: FluidMaterial = FluidMaterial::WATER;
pub const LAVA: FluidMaterial = FluidMaterial::LAVA;
#[must_use]
pub fn brine(salinity_fraction: f64) -> Option<FluidMaterial> {
if !(0.0..=0.35).contains(&salinity_fraction) {
return None;
}
let density = 1000.0 + 700.0 * salinity_fraction;
let viscosity = 0.001 * (1.0 + 1.5 * salinity_fraction);
FluidMaterial::custom(density, viscosity, 0.073, 1500.0).ok()
}
#[must_use]
pub fn sediment_laden(sediment_fraction: f64) -> Option<FluidMaterial> {
if !(0.0..=0.6).contains(&sediment_fraction) {
return None;
}
let density = 1000.0 * (1.0 - sediment_fraction) + 2650.0 * sediment_fraction;
let viscosity = 0.001 * (1.0 + 2.5 * sediment_fraction);
FluidMaterial::custom(density, viscosity, 0.073, 1500.0).ok()
}
}
#[must_use]
pub fn stokes_settling_velocity(
grain_density: f64,
fluid_density: f64,
grain_diameter_m: f64,
fluid_viscosity: f64,
gravity: f64,
) -> f64 {
let delta_rho = grain_density - fluid_density;
(delta_rho * gravity * grain_diameter_m.powi(2)) / (18.0 * fluid_viscosity)
}
#[must_use]
pub fn grain_reynolds_number(
fluid_density: f64,
velocity: f64,
grain_diameter_m: f64,
fluid_viscosity: f64,
) -> Option<f64> {
buoyancy::reynolds_number(fluid_density, velocity, grain_diameter_m, fluid_viscosity).ok()
}
#[must_use]
pub fn flow_regime(
fluid_density: f64,
velocity: f64,
channel_depth_m: f64,
fluid_viscosity: f64,
) -> Option<buoyancy::FlowRegime> {
let re = buoyancy::reynolds_number(fluid_density, velocity, channel_depth_m, fluid_viscosity)
.ok()?;
if re < 500.0 {
Some(buoyancy::FlowRegime::Laminar)
} else if re < 2000.0 {
Some(buoyancy::FlowRegime::Transitional)
} else {
Some(buoyancy::FlowRegime::Turbulent)
}
}
#[must_use]
pub fn buoyancy_force(fluid_density: f64, gravity: f64, displaced_volume: f64) -> f64 {
buoyancy::buoyancy_force(fluid_density, gravity, displaced_volume)
}
#[must_use]
pub fn sediment_drag_force(
fluid_density: f64,
velocity: f64,
drag_coefficient: f64,
cross_section_area: f64,
) -> f64 {
buoyancy::drag_force(
fluid_density,
velocity,
drag_coefficient,
cross_section_area,
)
}
#[must_use]
pub fn terminal_velocity(
mass: f64,
gravity: f64,
fluid_density: f64,
drag_coefficient: f64,
area: f64,
) -> Option<f64> {
buoyancy::terminal_velocity(mass, gravity, fluid_density, drag_coefficient, area).ok()
}
#[must_use]
pub fn darcy_flow(hydraulic_conductivity: f64, hydraulic_gradient: f64, area: f64) -> f64 {
hydraulic_conductivity * hydraulic_gradient * area
}
pub mod storativity {
pub const CONFINED_TYPICAL: f64 = 1e-4;
pub const UNCONFINED_SAND: f64 = 0.25;
pub const UNCONFINED_GRAVEL: f64 = 0.22;
pub const UNCONFINED_SILT: f64 = 0.08;
pub const UNCONFINED_CLAY: f64 = 0.03;
}
#[must_use]
pub fn well_function(u: f64) -> f64 {
use hisab::calc;
if u <= 0.0 {
return f64::INFINITY;
}
let s_lo = u.ln();
let s_hi = (u + 40.0).ln(); calc::integral_gauss_legendre(|s| (-s.exp()).exp(), s_lo, s_hi, 200)
.unwrap_or(0.0)
.max(0.0)
}
#[must_use]
pub fn theis_drawdown(
pumping_rate: f64,
transmissivity: f64,
storativity: f64,
distance_m: f64,
time_seconds: f64,
) -> f64 {
if transmissivity <= 0.0 || storativity <= 0.0 || time_seconds <= 0.0 || distance_m <= 0.0 {
return 0.0;
}
let u = distance_m.powi(2) * storativity / (4.0 * transmissivity * time_seconds);
let w = well_function(u);
pumping_rate / (4.0 * std::f64::consts::PI * transmissivity) * w
}
#[must_use]
pub fn cooper_jacob_drawdown(
pumping_rate: f64,
transmissivity: f64,
storativity: f64,
distance_m: f64,
time_seconds: f64,
) -> f64 {
if transmissivity <= 0.0 || storativity <= 0.0 || time_seconds <= 0.0 || distance_m <= 0.0 {
return 0.0;
}
let arg = 2.25 * transmissivity * time_seconds / (distance_m.powi(2) * storativity);
if arg <= 1.0 {
return 0.0; }
pumping_rate / (4.0 * std::f64::consts::PI * transmissivity) * arg.ln()
}
#[must_use]
pub fn radius_of_influence(transmissivity: f64, storativity: f64, time_seconds: f64) -> f64 {
(2.25 * transmissivity * time_seconds / storativity).sqrt()
}
pub mod conductivity {
pub const GRAVEL: f64 = 1e-2;
pub const COARSE_SAND: f64 = 1e-3;
pub const FINE_SAND: f64 = 1e-5;
pub const SILT: f64 = 1e-7;
pub const CLAY: f64 = 1e-9;
pub const SANDSTONE: f64 = 1e-6;
pub const FRACTURED_GRANITE: f64 = 1e-6;
pub const UNFRACTURED_GRANITE: f64 = 1e-12;
pub const LIMESTONE_KARST: f64 = 1e-2;
pub const LIMESTONE_INTACT: f64 = 1e-8;
}
#[must_use]
pub fn surface_water_config(width: f64, height: f64, smoothing_radius: f64) -> FluidConfig {
use hisab::DVec3;
let mut config = FluidConfig::water_2d();
config.smoothing_radius = smoothing_radius;
config.bounds_max = DVec3::new(width, height, 0.0);
config
}
#[must_use]
pub fn water_particle(x: f64, y: f64) -> FluidParticle {
FluidParticle::new_2d(x, y, 1.0)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TransportRegime {
Deposition,
Transport,
Erosion,
}
#[must_use]
pub fn hjulstrom_erosion_velocity(grain_diameter_m: f64) -> f64 {
let d = grain_diameter_m;
if d < 1e-6 {
5.0
} else if d < 6.25e-5 {
0.1 * (6.25e-5 / d).powf(0.4)
} else {
4.0 * d.sqrt()
}
}
#[must_use]
pub fn hjulstrom_deposition_velocity(grain_diameter_m: f64) -> f64 {
let d = grain_diameter_m;
let v = 1.6 * d.powf(0.8);
v.max(0.001)
}
#[must_use]
pub fn transport_regime(grain_diameter_m: f64, flow_velocity: f64) -> TransportRegime {
let v_erosion = hjulstrom_erosion_velocity(grain_diameter_m);
let v_deposition = hjulstrom_deposition_velocity(grain_diameter_m);
if flow_velocity >= v_erosion {
TransportRegime::Erosion
} else if flow_velocity >= v_deposition {
TransportRegime::Transport
} else {
TransportRegime::Deposition
}
}
#[must_use]
pub fn shields_parameter(
shear_stress: f64,
grain_density: f64,
fluid_density: f64,
grain_diameter_m: f64,
gravity: f64,
) -> f64 {
shear_stress / ((grain_density - fluid_density) * gravity * grain_diameter_m)
}
pub const SHIELDS_CRITICAL: f64 = 0.047;
#[must_use]
pub fn is_grain_mobile(
shear_stress: f64,
grain_density: f64,
fluid_density: f64,
grain_diameter_m: f64,
gravity: f64,
) -> bool {
shields_parameter(
shear_stress,
grain_density,
fluid_density,
grain_diameter_m,
gravity,
) > SHIELDS_CRITICAL
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stokes_settling_sand_grain() {
let v = stokes_settling_velocity(2650.0, 1000.0, 0.0005, 0.001, 9.81);
assert!(v > 0.0, "sand grain should settle");
assert!(v > 0.1 && v < 1.0);
}
#[test]
fn clay_settles_slower_than_sand() {
let sand = stokes_settling_velocity(2650.0, 1000.0, 0.001, 0.001, 9.81);
let clay = stokes_settling_velocity(2650.0, 1000.0, 0.00001, 0.001, 9.81);
assert!(sand > clay);
}
#[test]
fn darcy_flow_through_sandstone() {
let q = darcy_flow(conductivity::SANDSTONE, 0.01, 100.0);
assert!(q > 0.0);
assert!((q - 1e-6).abs() < 1e-10);
}
#[test]
fn unfractured_granite_nearly_impermeable() {
let q_granite = darcy_flow(conductivity::UNFRACTURED_GRANITE, 0.01, 100.0);
let q_gravel = darcy_flow(conductivity::GRAVEL, 0.01, 100.0);
assert!(q_gravel > q_granite * 1e6);
}
#[test]
fn brine_denser_than_freshwater() {
let brine = fluids::brine(0.035).unwrap(); assert!(brine.density > 1000.0);
}
#[test]
fn sediment_laden_flow_denser() {
let clear = fluids::GROUNDWATER;
let laden = fluids::sediment_laden(0.3).unwrap();
assert!(laden.density > clear.density);
}
#[test]
fn buoyancy_on_submerged_rock() {
let fb = buoyancy_force(1000.0, 9.81, 1.0);
assert!((fb - 9810.0).abs() < 1.0);
}
#[test]
fn surface_water_config_valid() {
let config = surface_water_config(100.0, 50.0, 0.5);
assert!((config.rest_density - 1000.0).abs() < f64::EPSILON);
}
#[test]
fn flow_regime_classification() {
let regime = flow_regime(1000.0, 0.0001, 0.01, 0.001);
assert_eq!(regime, Some(buoyancy::FlowRegime::Laminar));
}
#[test]
fn grain_reynolds_small_for_clay() {
let v = stokes_settling_velocity(2650.0, 1000.0, 0.00001, 0.001, 9.81);
let re = grain_reynolds_number(1000.0, v, 0.00001, 0.001).unwrap();
assert!(
re < 1.0,
"Clay grain Re should be <1 for Stokes validity, got {re}"
);
}
#[test]
fn hjulstrom_sand_easiest_to_erode() {
let sand = hjulstrom_erosion_velocity(0.0005);
let clay = hjulstrom_erosion_velocity(0.00001);
let gravel = hjulstrom_erosion_velocity(0.01);
assert!(sand < clay, "Sand should erode easier than clay");
assert!(sand < gravel, "Sand should erode easier than gravel");
}
#[test]
fn hjulstrom_deposition_increases_with_size() {
let fine = hjulstrom_deposition_velocity(0.0001);
let coarse = hjulstrom_deposition_velocity(0.001);
assert!(coarse > fine);
}
#[test]
fn transport_regime_classification() {
let d = 0.001; let v_erosion = hjulstrom_erosion_velocity(d);
let v_deposition = hjulstrom_deposition_velocity(d);
assert_eq!(
transport_regime(d, v_erosion + 0.1),
TransportRegime::Erosion
);
let v_transport = (v_deposition + v_erosion) / 2.0;
assert_eq!(transport_regime(d, v_transport), TransportRegime::Transport);
assert_eq!(
transport_regime(d, v_deposition * 0.5),
TransportRegime::Deposition
);
}
#[test]
fn shields_mobile_at_high_stress() {
assert!(is_grain_mobile(5.0, 2650.0, 1000.0, 0.001, 9.81));
}
#[test]
fn shields_immobile_at_low_stress() {
assert!(!is_grain_mobile(0.01, 2650.0, 1000.0, 0.01, 9.81));
}
#[test]
fn theis_drawdown_near_well() {
let s = theis_drawdown(0.01, 0.001, 1e-4, 10.0, 86400.0);
assert!(s > 0.0, "Should have measurable drawdown near well");
}
#[test]
fn theis_drawdown_decreases_with_distance() {
let near = theis_drawdown(0.01, 0.001, 1e-4, 10.0, 86400.0);
let far = theis_drawdown(0.01, 0.001, 1e-4, 100.0, 86400.0);
assert!(near > far);
}
#[test]
fn theis_drawdown_increases_with_time() {
let early = theis_drawdown(0.01, 0.001, 1e-4, 50.0, 3600.0);
let late = theis_drawdown(0.01, 0.001, 1e-4, 50.0, 86400.0);
assert!(late > early);
}
#[test]
fn cooper_jacob_agrees_with_theis_at_late_time() {
let theis = theis_drawdown(0.01, 0.001, 1e-4, 10.0, 864_000.0);
let cj = cooper_jacob_drawdown(0.01, 0.001, 1e-4, 10.0, 864_000.0);
let rel_err = (theis - cj).abs() / theis;
assert!(
rel_err < 0.05,
"Cooper-Jacob should agree within 5% at late time, got {:.1}%",
rel_err * 100.0
);
}
#[test]
fn radius_of_influence_grows_with_time() {
let r1 = radius_of_influence(0.001, 1e-4, 3600.0);
let r2 = radius_of_influence(0.001, 1e-4, 86400.0);
assert!(r2 > r1);
}
#[test]
fn well_function_decreasing() {
assert!(well_function(0.01) > well_function(0.1));
assert!(well_function(0.1) > well_function(1.0));
}
}