use std::f64::consts::PI;
#[must_use]
pub fn sea_land_breeze(land_temp_k: f64, sea_temp_k: f64, boundary_layer_depth_m: f64) -> f64 {
let t_mean = (land_temp_k + sea_temp_k) / 2.0;
if t_mean <= 0.0 || boundary_layer_depth_m <= 0.0 {
return 0.0;
}
let dt = land_temp_k - sea_temp_k;
let magnitude = (9.81 * boundary_layer_depth_m * dt.abs() / t_mean).sqrt();
if dt >= 0.0 { magnitude } else { -magnitude }
}
#[must_use]
#[inline]
pub fn sea_breeze_penetration_km(breeze_speed_ms: f64, elapsed_hours: f64) -> f64 {
if breeze_speed_ms <= 0.0 || elapsed_hours <= 0.0 {
return 0.0;
}
breeze_speed_ms * elapsed_hours * 3600.0 * 0.5 / 1000.0
}
#[must_use]
pub fn katabatic_wind(
slope_angle_rad: f64,
slope_length_m: f64,
temp_deficit_k: f64,
env_temp_k: f64,
) -> f64 {
if env_temp_k <= 0.0 || temp_deficit_k <= 0.0 || slope_length_m <= 0.0 {
return 0.0;
}
let dz = slope_length_m * slope_angle_rad.sin().abs();
(2.0 * 9.81 * dz * temp_deficit_k / env_temp_k).sqrt()
}
#[must_use]
pub fn anabatic_wind(
slope_angle_rad: f64,
slope_length_m: f64,
temp_excess_k: f64,
env_temp_k: f64,
) -> f64 {
katabatic_wind(slope_angle_rad, slope_length_m, temp_excess_k, env_temp_k)
}
#[must_use]
#[inline]
pub fn valley_wind_phase(solar_hour: f64) -> f64 {
let angle = 2.0 * PI * (solar_hour - 3.0) / 24.0;
-angle.cos()
}
#[must_use]
pub fn urban_heat_island(population: f64, wind_speed_ms: f64, cloud_fraction: f64) -> f64 {
if population <= 0.0 {
return 0.0;
}
let base_dt = (population / 10000.0).powf(0.27);
let wind_factor = if wind_speed_ms > 0.5 {
1.0 / (1.0 + 0.3 * wind_speed_ms)
} else {
1.0
};
let cf = cloud_fraction.clamp(0.0, 1.0);
let cloud_factor = 1.0 - 0.6 * cf;
base_dt * wind_factor * cloud_factor
}
#[must_use]
#[inline]
pub fn canyon_temperature_excess(uhi_intensity: f64, sky_view_factor: f64) -> f64 {
let svf = sky_view_factor.clamp(0.0, 1.0);
uhi_intensity * (1.0 + (1.0 - svf))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sea_breeze_land_warmer() {
let v = sea_land_breeze(298.0, 293.0, 1000.0);
assert!(v > 0.0, "land warmer → positive (sea breeze), got {v}");
assert!(
v > 2.0 && v < 15.0,
"sea breeze should be reasonable, got {v}"
);
}
#[test]
fn land_breeze_sea_warmer() {
let v = sea_land_breeze(283.0, 293.0, 1000.0);
assert!(v < 0.0, "sea warmer → negative (land breeze), got {v}");
}
#[test]
fn no_breeze_equal_temps() {
let v = sea_land_breeze(293.0, 293.0, 1000.0);
assert_eq!(v, 0.0);
}
#[test]
fn breeze_increases_with_contrast() {
let weak = sea_land_breeze(295.0, 293.0, 1000.0).abs();
let strong = sea_land_breeze(303.0, 293.0, 1000.0).abs();
assert!(strong > weak);
}
#[test]
fn breeze_zero_depth() {
assert_eq!(sea_land_breeze(303.0, 293.0, 0.0), 0.0);
}
#[test]
fn penetration_distance_typical() {
let d = sea_breeze_penetration_km(5.0, 6.0);
assert!(d > 40.0 && d < 60.0, "expected ~54 km, got {d}");
}
#[test]
fn penetration_zero_inputs() {
assert_eq!(sea_breeze_penetration_km(0.0, 6.0), 0.0);
assert_eq!(sea_breeze_penetration_km(5.0, 0.0), 0.0);
}
#[test]
fn katabatic_wind_typical() {
let v = katabatic_wind(10.0_f64.to_radians(), 2000.0, 5.0, 280.0);
assert!(
v > 3.0 && v < 15.0,
"katabatic wind should be 3-15 m/s, got {v}"
);
}
#[test]
fn katabatic_zero_deficit() {
assert_eq!(
katabatic_wind(10.0_f64.to_radians(), 2000.0, 0.0, 280.0),
0.0
);
}
#[test]
fn katabatic_steeper_is_faster() {
let gentle = katabatic_wind(5.0_f64.to_radians(), 2000.0, 5.0, 280.0);
let steep = katabatic_wind(20.0_f64.to_radians(), 2000.0, 5.0, 280.0);
assert!(steep > gentle);
}
#[test]
fn anabatic_equals_katabatic_same_params() {
let k = katabatic_wind(10.0_f64.to_radians(), 2000.0, 3.0, 290.0);
let a = anabatic_wind(10.0_f64.to_radians(), 2000.0, 3.0, 290.0);
assert!((k - a).abs() < f64::EPSILON);
}
#[test]
fn valley_wind_phase_afternoon_upvalley() {
let p = valley_wind_phase(14.0);
assert!(p > 0.5, "afternoon should be strongly upvalley, got {p}");
}
#[test]
fn valley_wind_phase_night_downvalley() {
let p = valley_wind_phase(3.0);
assert!(p < -0.5, "predawn should be strongly downvalley, got {p}");
}
#[test]
fn valley_wind_phase_transition() {
let dawn = valley_wind_phase(9.0).abs();
let dusk = valley_wind_phase(21.0).abs();
assert!(dawn < 0.3, "dawn should be near transition, got {dawn}");
assert!(dusk < 0.3, "dusk should be near transition, got {dusk}");
}
#[test]
fn uhi_large_city() {
let dt = urban_heat_island(1_000_000.0, 1.0, 0.0);
assert!(
dt > 2.0 && dt < 8.0,
"large city UHI should be 2-8°C, got {dt}"
);
}
#[test]
fn uhi_small_town() {
let dt = urban_heat_island(10_000.0, 1.0, 0.0);
assert!(dt < 2.0, "small town UHI should be < 2°C, got {dt}");
}
#[test]
fn uhi_zero_population() {
assert_eq!(urban_heat_island(0.0, 5.0, 0.0), 0.0);
}
#[test]
fn uhi_wind_reduces() {
let calm = urban_heat_island(500_000.0, 0.5, 0.0);
let windy = urban_heat_island(500_000.0, 10.0, 0.0);
assert!(
windy < calm,
"wind should reduce UHI: calm={calm}, windy={windy}"
);
}
#[test]
fn uhi_clouds_reduce() {
let clear = urban_heat_island(500_000.0, 2.0, 0.0);
let cloudy = urban_heat_island(500_000.0, 2.0, 0.8);
assert!(
cloudy < clear,
"clouds should reduce UHI: clear={clear}, cloudy={cloudy}"
);
}
#[test]
fn canyon_open_field() {
let excess = canyon_temperature_excess(5.0, 1.0);
assert!(
(excess - 5.0).abs() < f64::EPSILON,
"SVF=1 → no canyon amplification"
);
}
#[test]
fn canyon_narrow_amplifies() {
let open = canyon_temperature_excess(5.0, 1.0);
let narrow = canyon_temperature_excess(5.0, 0.3);
assert!(narrow > open, "narrow canyon should amplify UHI");
}
#[test]
fn canyon_minimum_svf() {
let excess = canyon_temperature_excess(5.0, 0.0);
assert!(
(excess - 10.0).abs() < f64::EPSILON,
"SVF=0 → 2× amplification"
);
}
}