use crate::math::constants;
pub const EARTH_ROTATION_RATE: f64 = 7.2921e-5; pub const STANDARD_PRESSURE: f64 = 101_325.0; pub const STANDARD_TEMPERATURE: f64 = 288.15; pub const DRY_AIR_MOLAR_MASS: f64 = 0.028_97;
const MAGNUS_A: f64 = 17.27;
const MAGNUS_B: f64 = 237.7;
const WIND_CHILL_MAX_TEMP_C: f64 = 10.0;
const WIND_CHILL_MIN_SPEED_KMH: f64 = 4.8;
const MAX_BEAUFORT: u32 = 12;
const BEAUFORT_COEFFICIENT: f64 = 0.836;
const DENSITY_ALT_COEFF: f64 = 36.576;
const STD_ATMO_EXPONENT: f64 = 0.1903;
#[must_use]
pub fn wind_chill(temperature_c: f64, wind_speed_kmh: f64) -> f64 {
debug_assert!(
temperature_c <= WIND_CHILL_MAX_TEMP_C,
"wind chill formula valid for T <= 10°C"
);
debug_assert!(
wind_speed_kmh >= WIND_CHILL_MIN_SPEED_KMH,
"wind chill formula valid for V >= 4.8 km/h"
);
let v_pow = wind_speed_kmh.powf(0.16);
13.12 + 0.6215 * temperature_c - 11.37 * v_pow + 0.3965 * temperature_c * v_pow
}
#[must_use]
pub fn beaufort_to_speed(beaufort: u32) -> f64 {
BEAUFORT_COEFFICIENT * (beaufort as f64).powf(1.5)
}
#[must_use]
pub fn speed_to_beaufort(speed_ms: f64) -> u32 {
if speed_ms <= 0.0 {
return 0;
}
let b = (speed_ms / BEAUFORT_COEFFICIENT).powf(2.0 / 3.0);
(b.round() as u32).min(MAX_BEAUFORT)
}
#[must_use]
pub fn wind_shear(v_top: f64, v_bottom: f64, height_diff: f64) -> f64 {
assert!(height_diff != 0.0, "height_diff must be non-zero");
(v_top - v_bottom) / height_diff
}
#[must_use]
pub fn wind_power_density(density: f64, velocity: f64) -> f64 {
0.5 * density * velocity * velocity * velocity
}
#[must_use]
pub fn coriolis_parameter(latitude_rad: f64) -> f64 {
2.0 * EARTH_ROTATION_RATE * latitude_rad.sin()
}
#[must_use]
pub fn coriolis_acceleration(velocity: f64, latitude_rad: f64) -> f64 {
coriolis_parameter(latitude_rad) * velocity
}
#[must_use]
pub fn barometric_pressure(
p0: f64,
molar_mass: f64,
g: f64,
height: f64,
temperature: f64,
) -> f64 {
assert!(temperature > 0.0, "temperature must be positive");
p0 * (-molar_mass * g * height / (constants::R * temperature)).exp()
}
#[must_use]
pub fn dry_adiabatic_lapse_rate(g: f64, cp: f64) -> f64 {
assert!(cp > 0.0, "specific heat capacity must be positive");
g / cp
}
#[must_use]
pub fn temperature_at_altitude(t0: f64, lapse_rate: f64, altitude: f64) -> f64 {
t0 - lapse_rate * altitude
}
#[must_use]
pub fn pressure_altitude(p0: f64, pressure: f64, lapse_rate: f64, t0: f64) -> f64 {
assert!(p0 > 0.0, "reference pressure must be positive");
assert!(lapse_rate != 0.0, "lapse_rate must be non-zero");
(t0 / lapse_rate) * (1.0 - (pressure / p0).powf(STD_ATMO_EXPONENT))
}
#[must_use]
pub fn density_altitude(
pressure_alt: f64,
temperature_c: f64,
standard_temp_c: f64,
) -> f64 {
pressure_alt + DENSITY_ALT_COEFF * (temperature_c - standard_temp_c)
}
#[must_use]
pub fn scale_height(temperature: f64, molar_mass: f64, g: f64) -> f64 {
assert!(molar_mass > 0.0, "molar_mass must be positive");
assert!(g > 0.0, "gravitational acceleration must be positive");
constants::R * temperature / (molar_mass * g)
}
#[must_use]
pub fn dew_point(temperature_c: f64, relative_humidity: f64) -> f64 {
assert!(relative_humidity > 0.0, "relative_humidity must be positive for ln()");
assert!((MAGNUS_B + temperature_c) != 0.0, "temperature_c must not equal -MAGNUS_B");
let alpha =
(MAGNUS_A * temperature_c) / (MAGNUS_B + temperature_c) + relative_humidity.ln();
assert!((MAGNUS_A - alpha) != 0.0, "degenerate dew point formula: alpha equals MAGNUS_A");
(MAGNUS_B * alpha) / (MAGNUS_A - alpha)
}
#[must_use]
pub fn relative_humidity(temperature_c: f64, dew_point_c: f64) -> f64 {
((MAGNUS_A * dew_point_c) / (MAGNUS_B + dew_point_c)
- (MAGNUS_A * temperature_c) / (MAGNUS_B + temperature_c))
.exp()
}
#[must_use]
pub fn heat_index(temperature_c: f64, relative_humidity: f64) -> f64 {
let t = temperature_c;
let rh = relative_humidity;
let t2 = t * t;
let rh2 = rh * rh;
-8.785 + 1.611 * t + 2.339 * rh - 0.1461 * t * rh - 0.012_31 * t2 - 0.016_42 * rh2
+ 0.002_212 * t2 * rh
+ 0.000_725 * t * rh2
- 0.000_003_582 * t2 * rh2
}
#[must_use]
pub fn absolute_humidity(relative_humidity: f64, temperature_c: f64) -> f64 {
let t = temperature_c;
assert!((t + 243.5) != 0.0, "temperature_c must not equal -243.5");
assert!((273.15 + t) != 0.0, "temperature_c must not equal -273.15");
let saturation = 6.112 * (17.67 * t / (t + 243.5)).exp();
(saturation * relative_humidity * 2.1674) / (273.15 + t)
}
#[cfg(test)]
mod tests {
use super::*;
const EPSILON: f64 = 1e-3;
fn approx(a: f64, b: f64, eps: f64) -> bool {
(a - b).abs() < eps
}
#[test]
fn test_wind_chill() {
let wc = wind_chill(-10.0, 30.0);
assert!(
approx(wc, -19.52, 0.1),
"wind chill at -10°C, 30km/h was {wc}"
);
}
#[test]
fn test_wind_chill_zero_temp() {
let wc = wind_chill(0.0, 10.0);
assert!(approx(wc, -3.305, 0.1), "wind chill at 0°C, 10km/h was {wc}");
}
#[test]
fn test_beaufort_to_speed() {
assert!(approx(beaufort_to_speed(0), 0.0, EPSILON));
assert!(approx(beaufort_to_speed(1), 0.836, EPSILON));
assert!(approx(beaufort_to_speed(4), 0.836 * 8.0, EPSILON));
}
#[test]
fn test_speed_to_beaufort() {
assert_eq!(speed_to_beaufort(0.0), 0);
assert_eq!(speed_to_beaufort(0.836), 1);
assert_eq!(speed_to_beaufort(100.0), MAX_BEAUFORT);
}
#[test]
fn test_beaufort_roundtrip() {
for b in 0..=MAX_BEAUFORT {
let speed = beaufort_to_speed(b);
assert_eq!(speed_to_beaufort(speed), b, "roundtrip failed for B={b}");
}
}
#[test]
fn test_wind_shear() {
assert!(approx(wind_shear(20.0, 10.0, 500.0), 0.02, EPSILON));
}
#[test]
fn test_wind_power_density() {
assert!(approx(wind_power_density(1.225, 10.0), 612.5, EPSILON));
}
#[test]
fn test_coriolis_parameter_equator() {
assert!(approx(coriolis_parameter(0.0), 0.0, 1e-10));
}
#[test]
fn test_coriolis_parameter_45deg() {
let lat = constants::PI / 4.0;
let f = coriolis_parameter(lat);
assert!(approx(f, 1.031_26e-4, 1e-8));
}
#[test]
fn test_coriolis_acceleration() {
let lat = constants::PI / 4.0;
let a = coriolis_acceleration(10.0, lat);
assert!(approx(a, 1.031_26e-3, 1e-7));
}
#[test]
fn test_barometric_pressure_sea_level() {
let p = barometric_pressure(
STANDARD_PRESSURE,
DRY_AIR_MOLAR_MASS,
constants::G_ACCEL,
0.0,
STANDARD_TEMPERATURE,
);
assert!(approx(p, STANDARD_PRESSURE, EPSILON));
}
#[test]
fn test_barometric_pressure_at_altitude() {
let p = barometric_pressure(
STANDARD_PRESSURE,
DRY_AIR_MOLAR_MASS,
constants::G_ACCEL,
1000.0,
STANDARD_TEMPERATURE,
);
assert!(p < STANDARD_PRESSURE);
assert!(approx(p, 89_994.0, 50.0));
}
#[test]
fn test_dry_adiabatic_lapse_rate() {
let lr = dry_adiabatic_lapse_rate(constants::G_ACCEL, 1005.0);
assert!(approx(lr, 0.009_76, 0.001));
}
#[test]
fn test_temperature_at_altitude() {
let t = temperature_at_altitude(288.15, 0.0065, 1000.0);
assert!(approx(t, 281.65, EPSILON));
}
#[test]
fn test_pressure_altitude() {
let pa = pressure_altitude(STANDARD_PRESSURE, 90_000.0, 0.0065, STANDARD_TEMPERATURE);
assert!(pa > 0.0);
assert!(approx(pa, 988.0, 50.0));
}
#[test]
fn test_density_altitude() {
let da = density_altitude(1000.0, 25.0, 15.0);
assert!(approx(da, 1365.76, EPSILON));
}
#[test]
fn test_density_altitude_standard_conditions() {
let da = density_altitude(1000.0, 15.0, 15.0);
assert!(approx(da, 1000.0, EPSILON));
}
#[test]
fn test_scale_height() {
let h = scale_height(STANDARD_TEMPERATURE, DRY_AIR_MOLAR_MASS, constants::G_ACCEL);
assert!(approx(h, 8435.0, 50.0));
}
#[test]
fn test_dew_point() {
let td = dew_point(25.0, 0.50);
assert!(approx(td, 13.9, 0.5), "dew point was {td}");
}
#[test]
fn test_relative_humidity() {
let rh = relative_humidity(25.0, 25.0);
assert!(approx(rh, 1.0, EPSILON));
}
#[test]
fn test_humidity_roundtrip() {
let t = 30.0;
let rh_in = 0.65;
let td = dew_point(t, rh_in);
let rh_out = relative_humidity(t, td);
assert!(
approx(rh_out, rh_in, 0.001),
"humidity roundtrip failed: {rh_out} vs {rh_in}"
);
}
#[test]
fn test_heat_index() {
let hi = heat_index(33.0, 70.0);
assert!(approx(hi, 43.5, 1.0), "heat index was {hi}");
}
#[test]
fn test_absolute_humidity() {
let ah = absolute_humidity(0.50, 20.0);
assert!(approx(ah, 0.0864, 0.01), "absolute humidity was {ah}");
}
}