sounding_analysis/
precip_type.rs

1//! This module contains types and functions related to assessing the precipitation type from a
2//! sounding. The types are based on the WMO Manual On Codes Vol I.1, Part A Alphanumeric Codes.
3//!
4//! The codes are drawn from table 4680 for automated weather stations. Note that not all observable
5//! weather types are diagnosable from a sounding (eg blowing sand, dust, etc), and only
6//! precipitation types are considered for this crate, so no fog or mist will be included. Most
7//! algorithms for diagnosing precipitation type do not classify all possible precipitation types.
8//!
9//! Thunderstorms are also not diagnosed. Though it is possible that someday I may try to implement
10//! algorithms to diagnose fog, mist, and thunder from soundings.
11
12use crate::sounding::Sounding;
13use itertools::izip;
14use metfor::{Celsius, Mm, StatuteMiles};
15use std::{convert::From, fmt::Display};
16use strum_macros::EnumIter;
17
18mod bourgouin;
19pub use bourgouin::bourgouin_precip_type;
20mod nssl;
21pub use nssl::nssl_precip_type;
22
23/// Precipitation type enum. Values are meant to correspond to the code values from table 4680 in
24/// the WMO Manual On Codes Vol I.1 Part A, Alphanumeric Codes.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter, Hash, PartialOrd, Ord)]
26#[repr(u8)]
27#[allow(missing_docs)]
28pub enum PrecipType {
29    // No weather
30    None = 0,
31
32    // Drizzle category
33    Drizzle = 50,
34    LightDrizzle = 51,
35    ModerateDrizzle = 52,
36    HeavyDrizzle = 53,
37    LightFreezingDrizzle = 54,
38    ModerateFreezingDrizzle = 55,
39    HeavyFreezingDrizzle = 56,
40    LightDrizzleAndRain = 57,
41    ModerateDrizzleAndRain = 58,
42    // DrizzleReserved = 59,
43
44    // Rain category
45    Rain = 60,
46    LightRain = 61,
47    ModerateRain = 62,
48    HeavyRain = 63,
49    LightFreezingRain = 64,
50    ModerateFreezingRain = 65,
51    HeavyFreezingRain = 66,
52    LightRainAndSnow = 67,
53    ModerateRainAndSnow = 68,
54    // RainReserved = 69,
55
56    // Snow/Ice category
57    Snow = 70,
58    LightSnow = 71,
59    ModerateSnow = 72,
60    HeavySnow = 73,
61    LightIcePellets = 74,
62    ModerateIcePellets = 75,
63    HeavyIcePellets = 76,
64    SnowGrains = 77,
65    IceCrystals = 78,
66    // SnowIceReserved = 79
67
68    // Showers
69    Showers = 80,
70    LightRainShowers = 81,
71    ModerateRainShowers = 82,
72    HeavyRainShowers = 83,
73    ViolentRainShowers = 84,
74    LightSnowShowers = 85,
75    ModerateSnowShowers = 86,
76    HeavySnowShowers = 87,
77    // ShowersReserved = 88,
78    Hail = 89,
79
80    // 90 series is for thunderstorms
81
82    // Catch all
83    Unknown = 100,
84}
85
86impl From<u8> for PrecipType {
87    fn from(val: u8) -> Self {
88        use PrecipType::*;
89
90        match val {
91            0 => None,
92
93            // Drizzle category
94            50 => Drizzle,
95            51 => LightDrizzle,
96            52 => ModerateDrizzle,
97            53 => HeavyDrizzle,
98            54 => LightFreezingDrizzle,
99            55 => ModerateFreezingDrizzle,
100            56 => HeavyFreezingDrizzle,
101            57 => LightDrizzleAndRain,
102            58 => ModerateDrizzleAndRain,
103
104            // Rain category
105            60 => Rain,
106            61 => LightRain,
107            62 => ModerateRain,
108            63 => HeavyRain,
109            64 => LightFreezingRain,
110            65 => ModerateFreezingRain,
111            66 => HeavyFreezingRain,
112            67 => LightRainAndSnow,
113            68 => ModerateRainAndSnow,
114
115            // Snow/Ice category
116            70 => Snow,
117            71 => LightSnow,
118            72 => ModerateSnow,
119            73 => HeavySnow,
120            74 => LightIcePellets,
121            75 => ModerateIcePellets,
122            76 => HeavyIcePellets,
123            77 => SnowGrains,
124            78 => IceCrystals,
125
126            // Showers
127            80 => Showers,
128            81 => LightRainShowers,
129            82 => ModerateRainShowers,
130            83 => HeavyRainShowers,
131            84 => ViolentRainShowers,
132            85 => LightSnowShowers,
133            86 => ModerateSnowShowers,
134            87 => HeavySnowShowers,
135
136            89 => Hail,
137
138            _ => Unknown,
139        }
140    }
141}
142
143impl Display for PrecipType {
144    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
145        write!(formatter, "{} => {:?}", *self as u8, self)
146    }
147}
148
149fn is_drizzler(snd: &Sounding) -> bool {
150    let ts = snd.temperature_profile();
151    let tds = snd.dew_point_profile();
152
153    !izip!(ts, tds)
154        // Remove levels with missing data
155        .filter(|(t, td)| t.is_some() && td.is_some())
156        // Unpack `Optioned` values
157        .map(|(t, td)| (t.unpack(), td.unpack()))
158        // Filter out non-dendritic zone temperatures
159        .filter(|&(t, _)| t <= Celsius(-12.0) && t >= Celsius(-18.0))
160        // Map to RH values
161        .filter_map(|(t, td)| metfor::rh(t, td))
162        // Find if any are over 90% rh
163        .any(|rh_val| rh_val >= 0.80)
164}
165
166/// Given a `PrecipType` and potentially hourly precipitation, hourly convective precipitation, and
167/// visibility, make a best effor to correct the weather type code, or PrecipType.
168///
169/// The adjustments are based on the definition of light, moderate, and heavy under the rain entry
170/// in the American Meteorological Society Glossary of Meteorology, and if the original PrecipType
171/// is a snow type and visibility information is available it is based off the visibility.
172///
173/// This function is not exhuastive, but it is a best effort attempt to handle the most common
174/// precipitation types: rain, snow, freezing rain, ice pellets, and mixtures of these. It only
175/// works with the light intensity level for these types since that is the default return type from
176/// the algorithms provided here which have no way of knowing intensity. If a weather code provided
177/// by some other provider has some more information, it should be used rather than adjusted by
178/// this routine.
179///
180pub fn check_precip_type_intensity<S1, S2, V>(
181    precip_type: PrecipType,
182    hourly_precipitation: Option<S1>,
183    convective_precipitation: Option<S2>,
184    visibility: Option<V>,
185) -> PrecipType
186where
187    S1: Into<Mm>,
188    S2: Into<Mm>,
189    V: Into<StatuteMiles>,
190{
191    use PrecipType::*;
192
193    // Force into units we like.
194    let hourly_precipitation = hourly_precipitation.map(Into::<Mm>::into);
195    let convective_precipitation = convective_precipitation.map(Into::<Mm>::into);
196    let visibility = visibility.map(Into::<StatuteMiles>::into);
197
198    // Check whether we have enough information to make an adjustment.
199    match check_preconditions_for_wx_type_adjustment(precip_type, hourly_precipitation, visibility)
200    {
201        PreconditionsCheckResult::Pass => {}
202        PreconditionsCheckResult::Fail => return precip_type,
203        PreconditionsCheckResult::NoQPF => return PrecipType::None,
204    }
205
206    let mode = get_precip_type_mode(hourly_precipitation, convective_precipitation);
207
208    let intensity = get_precip_type_intensity(precip_type, hourly_precipitation, visibility);
209
210    match precip_type {
211        LightDrizzle => match intensity {
212            Intensity::Light => LightDrizzle,
213            Intensity::Moderate => ModerateDrizzle,
214            Intensity::Heavy => HeavyDrizzle,
215        },
216        LightFreezingDrizzle => match intensity {
217            Intensity::Light => LightFreezingDrizzle,
218            Intensity::Moderate => ModerateFreezingDrizzle,
219            Intensity::Heavy => HeavyFreezingDrizzle,
220        },
221        LightDrizzleAndRain => match intensity {
222            Intensity::Light => LightDrizzleAndRain,
223            Intensity::Moderate | Intensity::Heavy => ModerateDrizzleAndRain,
224        },
225        LightRain => match intensity {
226            Intensity::Light => match mode {
227                Mode::Convective => LightRainShowers,
228                Mode::Stratiform => LightRain,
229            },
230            Intensity::Moderate => match mode {
231                Mode::Convective => ModerateRainShowers,
232                Mode::Stratiform => ModerateRain,
233            },
234            Intensity::Heavy => match mode {
235                Mode::Convective => HeavyRainShowers,
236                Mode::Stratiform => HeavyRain,
237            },
238        },
239        LightFreezingRain => match intensity {
240            Intensity::Light => LightFreezingRain,
241            Intensity::Moderate => ModerateFreezingRain,
242            Intensity::Heavy => HeavyFreezingRain,
243        },
244        LightRainAndSnow => match intensity {
245            Intensity::Light => LightRainAndSnow,
246            Intensity::Moderate | Intensity::Heavy => ModerateRainAndSnow,
247        },
248        LightSnow => match intensity {
249            Intensity::Light => match mode {
250                Mode::Convective => LightSnowShowers,
251                Mode::Stratiform => LightSnow,
252            },
253            Intensity::Moderate => match mode {
254                Mode::Convective => ModerateSnowShowers,
255                Mode::Stratiform => ModerateSnow,
256            },
257            Intensity::Heavy => match mode {
258                Mode::Convective => HeavySnowShowers,
259                Mode::Stratiform => HeavySnow,
260            },
261        },
262        LightIcePellets => match intensity {
263            Intensity::Light => LightIcePellets,
264            Intensity::Moderate => ModerateIcePellets,
265            Intensity::Heavy => HeavyIcePellets,
266        },
267        // Anything else should be unreachable due to the preconditions checks.
268        _ => unreachable!(),
269    }
270}
271
272enum Mode {
273    Stratiform,
274    Convective,
275}
276
277enum Intensity {
278    Light,
279    Moderate,
280    Heavy,
281}
282
283enum PreconditionsCheckResult {
284    Pass,
285    Fail,
286    NoQPF,
287}
288
289/// Check all the preconditions for adjusting the wx type.
290fn check_preconditions_for_wx_type_adjustment(
291    precip_type: PrecipType,
292    hourly_qpf: Option<Mm>,
293    visibility: Option<StatuteMiles>,
294) -> PreconditionsCheckResult {
295    use PrecipType::*;
296    use PreconditionsCheckResult::*;
297
298    // Bail out if it isn't one of the cases which are light.
299    match precip_type {
300        LightDrizzle | LightFreezingDrizzle | LightDrizzleAndRain | LightRain
301        | LightFreezingRain | LightRainAndSnow | LightSnow | LightIcePellets => {}
302        _ => return Fail,
303    }
304
305    // Bail if there is not QPF
306    match hourly_qpf {
307        Some(pcp) => {
308            if pcp <= Mm(0.0) {
309                return NoQPF;
310            }
311        }
312        std::option::Option::None => return Fail,
313    }
314
315    // Bail if we don't have enough information to make an adjustment
316    match precip_type {
317        LightDrizzle | LightFreezingDrizzle | LightDrizzleAndRain | LightRain
318        | LightFreezingRain | LightRainAndSnow | LightIcePellets => {
319            if hourly_qpf.is_none() {
320                return Fail;
321            }
322        }
323        LightSnow => {
324            if hourly_qpf.is_none() && visibility.is_none() {
325                return Fail;
326            }
327        }
328        _ => unreachable!(),
329    }
330    Pass
331}
332
333fn get_precip_type_mode(hourly_qpf: Option<Mm>, convective_qpf: Option<Mm>) -> Mode {
334    hourly_qpf
335        .and_then(|total_pcp| {
336            convective_qpf.map(|conv_pcp| {
337                if conv_pcp > total_pcp * 0.5 {
338                    Mode::Convective
339                } else {
340                    Mode::Stratiform
341                }
342            })
343        })
344        .unwrap_or(Mode::Stratiform)
345}
346
347fn get_precip_type_intensity(
348    precip_type: PrecipType,
349    hourly_qpf: Option<Mm>,
350    visibility: Option<StatuteMiles>,
351) -> Intensity {
352    use PrecipType::*;
353
354    match precip_type {
355        LightDrizzleAndRain | LightRain | LightFreezingRain | LightRainAndSnow
356        | LightIcePellets => {
357            if let Some(pcp) = hourly_qpf {
358                qpf_to_intensity_for_rain(pcp)
359            } else {
360                Intensity::Light
361            }
362        }
363        LightDrizzle | LightFreezingDrizzle => {
364            if let Some(pcp) = hourly_qpf {
365                qpf_to_intensity_for_drizzle(pcp)
366            } else {
367                Intensity::Light
368            }
369        }
370        LightSnow => {
371            if let Some(vsby) = visibility {
372                visibility_to_intensity(vsby)
373            } else if let Some(pcp) = hourly_qpf {
374                qpf_to_intensity_for_rain(pcp)
375            } else {
376                Intensity::Light
377            }
378        }
379        // Should be unreachable because of the function precondition checks done earlier.
380        _ => unreachable!(),
381    }
382}
383
384// Definition based on the AMS Glossary of Meteorology definition for rain.
385fn qpf_to_intensity_for_rain(qpf: Mm) -> Intensity {
386    if qpf <= Mm(2.5) {
387        Intensity::Light
388    } else if qpf <= Mm(7.6) {
389        Intensity::Moderate
390    } else {
391        Intensity::Heavy
392    }
393}
394
395// Definition based on the AMS Glossary of Meteorology definition for drizzle.
396fn qpf_to_intensity_for_drizzle(qpf: Mm) -> Intensity {
397    if qpf <= Mm(0.3) {
398        Intensity::Light
399    } else if qpf <= Mm(0.5) {
400        Intensity::Moderate
401    } else {
402        Intensity::Heavy
403    }
404}
405
406// Definition base on the AMS Glossary of Meteorology definition for snow.
407fn visibility_to_intensity(vsby: StatuteMiles) -> Intensity {
408    if vsby < StatuteMiles(5.0 / 16.0) {
409        Intensity::Heavy
410    } else if vsby < StatuteMiles(5.0 / 8.0) {
411        Intensity::Moderate
412    } else {
413        Intensity::Light
414    }
415}
416
417#[cfg(test)]
418mod test {
419    use super::PrecipType::*;
420    use super::*;
421    use strum::IntoEnumIterator;
422
423    #[test]
424    #[rustfmt::skip]
425    fn test_adjust_check_precip_type_intensity() {
426
427        assert_eq!(check_precip_type_intensity(LightDrizzle, Some(Mm(0.01)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), LightDrizzle);
428        assert_eq!(check_precip_type_intensity(LightDrizzle, Some(Mm(0.4)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), ModerateDrizzle);
429        assert_eq!(check_precip_type_intensity(LightDrizzle, Some(Mm(0.6)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), HeavyDrizzle);
430
431        assert_eq!(check_precip_type_intensity(LightFreezingDrizzle, Some(Mm(0.1)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), LightFreezingDrizzle);
432        assert_eq!(check_precip_type_intensity(LightFreezingDrizzle, Some(Mm(0.4)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), ModerateFreezingDrizzle);
433        assert_eq!(check_precip_type_intensity(LightFreezingDrizzle, Some(Mm(0.6)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), HeavyFreezingDrizzle);
434
435        assert_eq!(check_precip_type_intensity(LightRain, Some(Mm(1.0)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), LightRain);
436        assert_eq!(check_precip_type_intensity(LightRain, Some(Mm(5.0)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), ModerateRain);
437        assert_eq!(check_precip_type_intensity(LightRain, Some(Mm(8.0)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), HeavyRain);
438
439        assert_eq!(check_precip_type_intensity(LightFreezingRain, Some(Mm(1.0)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), LightFreezingRain);
440        assert_eq!(check_precip_type_intensity(LightFreezingRain, Some(Mm(5.0)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), ModerateFreezingRain);
441        assert_eq!(check_precip_type_intensity(LightFreezingRain, Some(Mm(8.0)), Some(Mm(0.0)), Some(StatuteMiles(10.0))), HeavyFreezingRain);
442
443        assert_eq!(check_precip_type_intensity(LightSnow, Some(Mm(1.0)), Some(Mm(0.0)), std::option::Option::<StatuteMiles>::None), LightSnow);
444        assert_eq!(check_precip_type_intensity(LightSnow, Some(Mm(5.0)), Some(Mm(0.0)), std::option::Option::<StatuteMiles>::None), ModerateSnow);
445        assert_eq!(check_precip_type_intensity(LightSnow, Some(Mm(8.0)), Some(Mm(0.0)), std::option::Option::<StatuteMiles>::None), HeavySnow);
446
447        for variant in PrecipType::iter() {
448            // Skip special cases handled above
449            match variant {
450                LightIcePellets | LightRainAndSnow | LightDrizzleAndRain | LightDrizzle | 
451                    LightFreezingDrizzle | LightRain | LightSnow | LightFreezingRain => {
452                    continue
453                }
454                _ => {}
455            }
456
457            for &qpf in &[Mm(1.0), Mm(5.0), Mm(8.0)] {
458                assert_eq!(check_precip_type_intensity(variant, Some(qpf), Some(Mm(0.0)), Some(StatuteMiles(10.0))), variant);
459            }
460        }
461    }
462
463    #[test]
464    fn test_from_u8_and_back() {
465        for variant in PrecipType::iter() {
466            assert_eq!(variant, PrecipType::from(variant as u8));
467        }
468    }
469}