pub const G: f64 = 9.80665;
pub const P_ATM: f64 = 101_325.0;
#[derive(Debug, Clone, PartialEq)]
pub struct Fluid {
pub temp_c: f64,
pub salinity_ppt: f64,
pub density_kg_m3: f64,
pub dynamic_viscosity_pa_s: f64,
pub surface_tension_n_m: f64,
pub vapor_pressure_pa: f64,
}
impl Fluid {
pub fn water(temp_c: f64) -> Self {
debug_assert!(
(0.0..=100.0).contains(&temp_c),
"temp_c must be in [0, 100]°C, got {temp_c}"
);
Self {
temp_c,
salinity_ppt: 0.0,
density_kg_m3: water_density(temp_c),
dynamic_viscosity_pa_s: water_viscosity(temp_c),
surface_tension_n_m: water_surface_tension(temp_c),
vapor_pressure_pa: water_vapor_pressure(temp_c),
}
}
pub fn seawater(temp_c: f64, salinity_ppt: f64) -> Self {
debug_assert!(
(0.0..=100.0).contains(&temp_c),
"temp_c must be in [0, 100]°C, got {temp_c}"
);
debug_assert!(
salinity_ppt >= 0.0,
"salinity_ppt must be >= 0, got {salinity_ppt}"
);
Self {
temp_c,
salinity_ppt,
density_kg_m3: seawater_density(temp_c, salinity_ppt),
dynamic_viscosity_pa_s: seawater_viscosity(temp_c, salinity_ppt),
surface_tension_n_m: seawater_surface_tension(temp_c, salinity_ppt),
vapor_pressure_pa: seawater_vapor_pressure(temp_c, salinity_ppt),
}
}
pub fn custom(
temp_c: f64,
density_kg_m3: f64,
dynamic_viscosity_pa_s: f64,
surface_tension_n_m: f64,
vapor_pressure_pa: f64,
) -> Self {
Self {
temp_c,
salinity_ppt: 0.0,
density_kg_m3,
dynamic_viscosity_pa_s,
surface_tension_n_m,
vapor_pressure_pa,
}
}
#[inline]
pub fn kinematic_viscosity_m2_s(&self) -> f64 {
self.dynamic_viscosity_pa_s / self.density_kg_m3
}
}
#[inline]
fn water_density(t: f64) -> f64 {
999.842_594
+ 6.793_952e-2 * t
- 9.095_290e-3 * t * t
+ 1.001_685e-4 * t * t * t
- 1.120_083e-6 * t * t * t * t
+ 6.536_332e-9 * t * t * t * t * t
}
#[inline]
fn water_viscosity(t: f64) -> f64 {
let t_k = t + 273.15; let a: f64 = 2.414e-5;
let b: f64 = 570.58;
let c: f64 = 140.0;
a * libm_exp(b / (t_k - c))
}
#[inline]
fn water_surface_tension(t: f64) -> f64 {
0.075_62 - 1.676e-4 * t
}
#[inline]
fn water_vapor_pressure(t: f64) -> f64 {
let (a, b, c) = if t <= 60.0 {
(8.071_31, 1730.63, 233.426)
} else {
(8.140_19, 1810.94, 244.485)
};
let log_p_mmhg = a - b / (c + t);
libm_pow10(log_p_mmhg) * 133.322
}
#[inline]
fn seawater_density(t: f64, s: f64) -> f64 {
let rho_fw = water_density(t);
if s <= 0.0 {
return rho_fw;
}
let a = 8.244_93e-1 - 4.0899e-3 * t + 7.6438e-5 * t * t
- 8.2467e-7 * t * t * t + 5.3875e-9 * t * t * t * t;
let b = -5.724_66e-3 + 1.0227e-4 * t - 1.6546e-6 * t * t;
let c = 4.8314e-4;
rho_fw + s * (a + b * s.sqrt() + c * s)
}
#[inline]
fn seawater_viscosity(t: f64, s: f64) -> f64 {
let mu_fw = water_viscosity(t);
if s <= 0.0 {
return mu_fw;
}
let a = 1.541e-3 + 1.998e-5 * t - 9.52e-8 * t * t;
let b = 7.974e-6 - 7.561e-8 * t + 4.724e-10 * t * t;
let s_gkg = s; mu_fw * (1.0 + a * s_gkg + b * s_gkg * s_gkg)
}
#[inline]
fn seawater_surface_tension(t: f64, s: f64) -> f64 {
let sigma_fw = water_surface_tension(t);
sigma_fw * (1.0 + 2.26e-4 * s)
}
#[inline]
fn seawater_vapor_pressure(t: f64, s: f64) -> f64 {
let p_fw = water_vapor_pressure(t);
if s <= 0.0 {
return p_fw;
}
let moles_salt = s / 58.44;
let moles_water = (1000.0 - s) / 18.015;
let x_w = moles_water / (moles_water + 2.0 * moles_salt); p_fw * x_w
}
#[inline]
fn libm_exp(x: f64) -> f64 {
x.exp()
}
#[inline]
fn libm_pow10(x: f64) -> f64 {
libm_exp(x * core::f64::consts::LN_10)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_water_density_at_4c() {
let d = water_density(4.0);
assert!((d - 999.97).abs() < 0.1, "density at 4°C = {d}");
}
#[test]
fn test_water_density_at_20c() {
let d = water_density(20.0);
assert!((d - 998.2).abs() < 0.5, "density at 20°C = {d}");
}
#[test]
fn test_water_viscosity_at_20c() {
let v = water_viscosity(20.0);
assert!((v - 1.002e-3).abs() < 5e-5, "viscosity at 20°C = {v:.4e}");
}
#[test]
fn test_water_surface_tension_at_20c() {
let s = water_surface_tension(20.0);
assert!((s - 0.07275).abs() < 0.002, "surface tension at 20°C = {s:.5}");
}
#[test]
fn test_vapor_pressure_at_100c() {
let vp = water_vapor_pressure(100.0);
assert!((vp - 101_325.0).abs() < 2000.0, "vapor pressure at 100°C = {vp:.0}");
}
#[test]
fn test_fluid_water_constructor() {
let f = Fluid::water(20.0);
assert!(f.kinematic_viscosity_m2_s() > 0.0);
assert_eq!(f.salinity_ppt, 0.0);
}
#[test]
fn test_seawater_density_35ppt_20c() {
let d = seawater_density(20.0, 35.0);
assert!((d - 1024.8).abs() < 1.5, "seawater density at 35ppt, 20°C = {d:.1}");
}
#[test]
fn test_seawater_density_zero_salinity_equals_fresh() {
let d_sw = seawater_density(20.0, 0.0);
let d_fw = water_density(20.0);
assert!((d_sw - d_fw).abs() < 1e-10, "zero salinity should equal fresh water");
}
#[test]
fn test_seawater_density_32ppt_25c() {
let d = seawater_density(25.0, 32.0);
assert!((d - 1021.0).abs() < 2.0, "density at 32ppt = {d:.1}");
}
#[test]
fn test_seawater_viscosity_35ppt_20c() {
let mu = seawater_viscosity(20.0, 35.0);
assert!((mu - 1.075e-3).abs() < 1e-4, "seawater viscosity at 35ppt, 20°C = {mu:.4e}");
}
#[test]
fn test_seawater_viscosity_zero_salinity_equals_fresh() {
let mu_sw = seawater_viscosity(20.0, 0.0);
let mu_fw = water_viscosity(20.0);
assert!((mu_sw - mu_fw).abs() < 1e-10);
}
#[test]
fn test_seawater_surface_tension_increases_with_salt() {
let sigma_fw = water_surface_tension(20.0);
let sigma_sw = seawater_surface_tension(20.0, 35.0);
assert!(sigma_sw > sigma_fw, "seawater σ should exceed fresh water σ");
let ratio = sigma_sw / sigma_fw;
assert!((ratio - 1.008).abs() < 0.005, "ratio = {ratio:.4}");
}
#[test]
fn test_seawater_vapor_pressure_lower_than_fresh() {
let vp_fw = water_vapor_pressure(20.0);
let vp_sw = seawater_vapor_pressure(20.0, 35.0);
assert!(vp_sw < vp_fw, "seawater VP should be less than fresh");
let ratio = vp_sw / vp_fw;
assert!(ratio > 0.97 && ratio < 0.99, "VP ratio = {ratio:.4}");
}
#[test]
fn test_brine_density_70ppt() {
let d = seawater_density(25.0, 70.0);
assert!((d - 1050.0).abs() < 5.0, "brine density at 70ppt = {d:.1}");
}
#[test]
fn test_seawater_constructor() {
let f = Fluid::seawater(25.0, 32.0);
assert_eq!(f.salinity_ppt, 32.0);
assert!(f.density_kg_m3 > 1000.0, "seawater should be denser than fresh");
assert!(f.kinematic_viscosity_m2_s() > 0.0);
}
}