pub const EARTH_ANGULAR_VELOCITY: f64 = 7.292e-5;
#[must_use]
#[inline]
pub fn coriolis_parameter(latitude_rad: f64) -> f64 {
2.0 * EARTH_ANGULAR_VELOCITY * latitude_rad.sin()
}
#[must_use]
pub fn wind_chill(temp_celsius: f64, wind_speed_kmh: f64) -> f64 {
if temp_celsius > 10.0 || wind_speed_kmh < 4.8 {
return temp_celsius;
}
let v016 = wind_speed_kmh.powf(0.16);
13.12 + 0.6215 * temp_celsius - 11.37 * v016 + 0.3965 * temp_celsius * v016
}
#[must_use]
pub fn beaufort_scale(wind_speed_ms: f64) -> u8 {
match wind_speed_ms {
v if v < 0.5 => 0, v if v < 1.6 => 1, v if v < 3.4 => 2, v if v < 5.5 => 3, v if v < 8.0 => 4, v if v < 10.8 => 5, v if v < 13.9 => 6, v if v < 17.2 => 7, v if v < 20.8 => 8, v if v < 24.5 => 9, v if v < 28.5 => 10, v if v < 32.7 => 11, _ => 12, }
}
#[must_use]
#[inline]
pub fn thermal_wind_shear(temp_gradient_k_per_m: f64, coriolis: f64) -> f64 {
if coriolis.abs() < f64::EPSILON {
return 0.0;
}
(9.81 / (coriolis.abs() * 280.0)) * temp_gradient_k_per_m.abs() * 1000.0
}
#[must_use]
#[inline]
pub fn wind_direction(u: f64, v: f64) -> f64 {
if u.abs() < f64::EPSILON && v.abs() < f64::EPSILON {
return 0.0; }
let dir = (-u).atan2(-v).to_degrees();
if dir < 0.0 { dir + 360.0 } else { dir }
}
#[must_use]
#[inline]
pub fn wind_speed(u: f64, v: f64) -> f64 {
u.hypot(v)
}
#[must_use]
pub fn log_wind_profile(speed_ref: f64, z_ref: f64, z_target: f64, z0: f64) -> f64 {
if z0 <= 0.0 || z_ref <= z0 || z_target <= z0 || speed_ref <= 0.0 {
return 0.0;
}
speed_ref * (z_target / z0).ln() / (z_ref / z0).ln()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn coriolis_at_45_degrees() {
let f = coriolis_parameter(45.0_f64.to_radians());
assert!(
(f - 1.031e-4).abs() < 0.005e-4,
"coriolis at 45° should be ~1.03e-4, got {f}"
);
}
#[test]
fn coriolis_at_equator() {
let f = coriolis_parameter(0.0);
assert!(f.abs() < 1e-10, "coriolis at equator should be ~0");
}
#[test]
fn coriolis_at_pole() {
let f = coriolis_parameter(90.0_f64.to_radians());
assert!(
(f - 1.4584e-4).abs() < 0.001e-4,
"coriolis at pole should be ~1.46e-4, got {f}"
);
}
#[test]
fn wind_chill_cold() {
let wc = wind_chill(-10.0, 30.0);
assert!(wc < -10.0, "wind chill should be colder, got {wc}");
}
#[test]
fn wind_chill_warm_no_effect() {
assert!((wind_chill(15.0, 30.0) - 15.0).abs() < 0.01);
}
#[test]
fn wind_chill_low_wind_no_effect() {
assert!((wind_chill(-10.0, 3.0) - (-10.0)).abs() < 0.01);
}
#[test]
fn beaufort_calm() {
assert_eq!(beaufort_scale(0.2), 0);
}
#[test]
fn beaufort_hurricane() {
assert_eq!(beaufort_scale(35.0), 12);
}
#[test]
fn beaufort_moderate() {
assert_eq!(beaufort_scale(6.0), 4); }
#[test]
fn beaufort_monotonic() {
for i in 0..12 {
let low = beaufort_scale(i as f64 * 2.5);
let high = beaufort_scale((i + 1) as f64 * 2.5);
assert!(high >= low, "Beaufort should be monotonically increasing");
}
}
#[test]
fn thermal_wind_shear_positive() {
let f = coriolis_parameter(45.0_f64.to_radians());
let shear = thermal_wind_shear(1e-5, f);
assert!(
shear > 0.0,
"thermal wind shear should be positive, got {shear}"
);
}
#[test]
fn thermal_wind_shear_zero_coriolis() {
assert_eq!(thermal_wind_shear(1e-5, 0.0), 0.0);
}
#[test]
fn thermal_wind_shear_zero_gradient() {
let f = coriolis_parameter(45.0_f64.to_radians());
assert_eq!(thermal_wind_shear(0.0, f), 0.0);
}
#[test]
fn wind_direction_from_south() {
let dir = wind_direction(0.0, 1.0);
assert!(
(dir - 180.0).abs() < 1.0,
"southerly wind (v>0) should be ~180°, got {dir}"
);
}
#[test]
fn wind_direction_from_west() {
let dir = wind_direction(1.0, 0.0);
assert!(
(dir - 270.0).abs() < 1.0,
"westerly wind (u>0) should be ~270°, got {dir}"
);
}
#[test]
fn wind_direction_from_north() {
let dir = wind_direction(0.0, -1.0);
assert!(
!(1.0..=359.0).contains(&dir),
"northerly wind should be ~0°/360°, got {dir}"
);
}
#[test]
fn wind_direction_calm() {
assert_eq!(wind_direction(0.0, 0.0), 0.0);
}
#[test]
fn wind_speed_basic() {
assert!((wind_speed(3.0, 4.0) - 5.0).abs() < f64::EPSILON);
}
#[test]
fn log_profile_same_height() {
let v = log_wind_profile(10.0, 10.0, 10.0, 0.03);
assert!((v - 10.0).abs() < 0.01);
}
#[test]
fn log_profile_higher() {
let v = log_wind_profile(10.0, 10.0, 50.0, 0.03);
assert!(v > 10.0, "wind should increase with height, got {v}");
}
#[test]
fn log_profile_lower() {
let v = log_wind_profile(10.0, 10.0, 2.0, 0.03);
assert!(v < 10.0, "wind should decrease below ref height, got {v}");
}
}