use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ThreatLevel {
None,
Marginal,
Slight,
Enhanced,
Moderate,
High,
}
impl fmt::Display for ThreatLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::None => write!(f, "None"),
Self::Marginal => write!(f, "Marginal"),
Self::Slight => write!(f, "Slight"),
Self::Enhanced => write!(f, "Enhanced"),
Self::Moderate => write!(f, "Moderate"),
Self::High => write!(f, "High"),
}
}
}
#[must_use]
#[inline]
pub fn supercell_composite(cape: f64, shear_0_6km: f64, storm_relative_helicity: f64) -> f64 {
if cape <= 0.0 || shear_0_6km <= 0.0 || storm_relative_helicity <= 0.0 {
return 0.0;
}
(cape / 1000.0) * (shear_0_6km / 40.0) * (storm_relative_helicity / 50.0)
}
#[must_use]
pub fn significant_tornado(
cape: f64,
bulk_wind_diff: f64,
storm_relative_helicity: f64,
lcl_height_m: f64,
) -> f64 {
if cape <= 0.0 || bulk_wind_diff <= 0.0 || storm_relative_helicity <= 0.0 {
return 0.0;
}
let lcl_term = ((2000.0 - lcl_height_m) / 1000.0).clamp(0.0, 2.0);
(cape / 1500.0) * (bulk_wind_diff / 12.0) * (storm_relative_helicity / 150.0) * lcl_term
}
#[must_use]
#[inline]
pub fn derecho_composite(cape: f64, shear_0_6km: f64, mean_wind_0_6km: f64) -> f64 {
if cape <= 0.0 || shear_0_6km <= 0.0 || mean_wind_0_6km <= 0.0 {
return 0.0;
}
(cape / 980.0) * (shear_0_6km / 18.0) * (mean_wind_0_6km / 16.0)
}
#[must_use]
#[inline]
pub fn bulk_richardson_number(cape: f64, bulk_shear: f64) -> f64 {
let denom = 0.5 * bulk_shear * bulk_shear;
if denom < f64::EPSILON || cape <= 0.0 {
return 0.0;
}
cape / denom
}
#[must_use]
#[inline]
pub fn energy_helicity_index(cape: f64, storm_relative_helicity: f64) -> f64 {
if cape <= 0.0 || storm_relative_helicity <= 0.0 {
return 0.0;
}
cape * storm_relative_helicity / 160_000.0
}
#[must_use]
pub fn classify_threat(scp: f64, stp: f64) -> ThreatLevel {
if stp > 4.0 || scp > 8.0 {
ThreatLevel::High
} else if stp > 2.0 || scp > 4.0 {
ThreatLevel::Moderate
} else if stp > 1.0 || scp > 2.0 {
ThreatLevel::Enhanced
} else if scp > 1.0 {
ThreatLevel::Slight
} else if scp > 0.5 {
ThreatLevel::Marginal
} else {
ThreatLevel::None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scp_typical_supercell() {
let scp = supercell_composite(2000.0, 40.0, 200.0);
assert!(
scp > 2.0,
"typical supercell environment should give SCP > 2, got {scp}"
);
}
#[test]
fn scp_weak_environment() {
let scp = supercell_composite(500.0, 10.0, 30.0);
assert!(scp < 1.0, "weak environment should give SCP < 1, got {scp}");
}
#[test]
fn scp_zero_cape() {
assert_eq!(supercell_composite(0.0, 25.0, 200.0), 0.0);
}
#[test]
fn scp_zero_shear() {
assert_eq!(supercell_composite(2000.0, 0.0, 200.0), 0.0);
}
#[test]
fn scp_zero_srh() {
assert_eq!(supercell_composite(2000.0, 25.0, 0.0), 0.0);
}
#[test]
fn stp_tornado_favorable() {
let stp = significant_tornado(3000.0, 20.0, 300.0, 800.0);
assert!(
stp > 2.0,
"strong tornado environment should give STP > 2, got {stp}"
);
}
#[test]
fn stp_high_lcl_unfavorable() {
let stp = significant_tornado(3000.0, 20.0, 300.0, 2000.0);
assert_eq!(stp, 0.0, "LCL at 2000m should zero out STP");
}
#[test]
fn stp_low_lcl_favorable() {
let high_lcl = significant_tornado(2000.0, 15.0, 200.0, 1500.0);
let low_lcl = significant_tornado(2000.0, 15.0, 200.0, 500.0);
assert!(low_lcl > high_lcl, "lower LCL should increase STP");
}
#[test]
fn stp_zero_inputs() {
assert_eq!(significant_tornado(0.0, 15.0, 200.0, 500.0), 0.0);
assert_eq!(significant_tornado(2000.0, 0.0, 200.0, 500.0), 0.0);
assert_eq!(significant_tornado(2000.0, 15.0, 0.0, 500.0), 0.0);
}
#[test]
fn dcp_derecho_favorable() {
let dcp = derecho_composite(2000.0, 25.0, 20.0);
assert!(
dcp > 2.0,
"strong derecho environment should give DCP > 2, got {dcp}"
);
}
#[test]
fn dcp_weak_wind() {
let dcp = derecho_composite(2000.0, 25.0, 5.0);
assert!(dcp < 2.0, "weak mean wind should reduce DCP");
}
#[test]
fn dcp_zero_inputs() {
assert_eq!(derecho_composite(0.0, 25.0, 20.0), 0.0);
assert_eq!(derecho_composite(2000.0, 0.0, 20.0), 0.0);
assert_eq!(derecho_composite(2000.0, 25.0, 0.0), 0.0);
}
#[test]
fn brn_supercell_range() {
let brn = bulk_richardson_number(2000.0, 20.0);
assert!(
(10.0..=45.0).contains(&brn),
"BRN should be in supercell range, got {brn}"
);
}
#[test]
fn brn_multicell() {
let brn = bulk_richardson_number(3000.0, 5.0);
assert!(
brn > 45.0,
"low shear should give BRN > 45 (multicell), got {brn}"
);
}
#[test]
fn brn_zero_shear() {
assert_eq!(bulk_richardson_number(2000.0, 0.0), 0.0);
}
#[test]
fn brn_zero_cape() {
assert_eq!(bulk_richardson_number(0.0, 20.0), 0.0);
}
#[test]
fn ehi_tornado_favorable() {
let ehi = energy_helicity_index(2000.0, 200.0);
assert!((ehi - 2.5).abs() < 0.01, "EHI should be 2.5, got {ehi}");
}
#[test]
fn ehi_zero_inputs() {
assert_eq!(energy_helicity_index(0.0, 200.0), 0.0);
assert_eq!(energy_helicity_index(2000.0, 0.0), 0.0);
}
#[test]
fn threat_none() {
assert_eq!(classify_threat(0.3, 0.0), ThreatLevel::None);
}
#[test]
fn threat_marginal() {
assert_eq!(classify_threat(0.7, 0.0), ThreatLevel::Marginal);
}
#[test]
fn threat_slight() {
assert_eq!(classify_threat(1.5, 0.5), ThreatLevel::Slight);
}
#[test]
fn threat_enhanced() {
assert_eq!(classify_threat(2.5, 1.5), ThreatLevel::Enhanced);
}
#[test]
fn threat_moderate() {
assert_eq!(classify_threat(5.0, 0.5), ThreatLevel::Moderate);
}
#[test]
fn threat_high() {
assert_eq!(classify_threat(9.0, 5.0), ThreatLevel::High);
}
#[test]
fn threat_ordering() {
assert!(ThreatLevel::High > ThreatLevel::Moderate);
assert!(ThreatLevel::Moderate > ThreatLevel::None);
}
#[test]
fn threat_display() {
assert_eq!(ThreatLevel::Enhanced.to_string(), "Enhanced");
assert_eq!(ThreatLevel::High.to_string(), "High");
}
#[test]
fn threat_serde_roundtrip() {
let t = ThreatLevel::Moderate;
let json = serde_json::to_string(&t).unwrap();
let t2: ThreatLevel = serde_json::from_str(&json).unwrap();
assert_eq!(t, t2);
}
}