sounding_analysis/
layers.rs

1//! This module finds significant layers.
2//!
3//! Examples are the dendritic snow growth zone, the hail growth zone, and inversions.
4//!
5//! The `Layer` type also provides some methods for doing basic analysis on a given layer.
6//!
7use crate::sounding::DataRow;
8use metfor::{CelsiusDiff, CelsiusPKm, HectoPascal, Km, Meters, MetersPSec, WindUV};
9
10/// A layer in the atmosphere described by the values at the top and bottom.
11#[derive(Debug, Clone, Copy)]
12pub struct Layer {
13    /// Sounding values at the bottom of the layer.
14    pub bottom: DataRow,
15    /// Sounding values at the top of the layer.
16    pub top: DataRow,
17}
18
19/// A list of layers.
20pub type Layers = Vec<Layer>;
21
22impl Layer {
23    /// Get the average lapse rate in C/km
24    pub fn lapse_rate(&self) -> Option<CelsiusPKm> {
25        let top_t = self.top.temperature.into_option()?;
26        let bottom_t = self.bottom.temperature.into_option()?;
27
28        #[allow(clippy::useless_conversion)]
29        let CelsiusDiff(dt) = CelsiusDiff::from(top_t - bottom_t);
30        let Km(dz) = Km::from(self.height_thickness()?);
31
32        Some(CelsiusPKm(dt / dz))
33    }
34
35    /// Get the height thickness in meters
36    pub fn height_thickness(&self) -> Option<Meters> {
37        let top = self.top.height.into_option()?;
38        let bottom = self.bottom.height.into_option()?;
39        if top == bottom {
40            None
41        } else {
42            Some(top - bottom)
43        }
44    }
45
46    /// Get the pressure thickness.
47    pub fn pressure_thickness(&self) -> Option<HectoPascal> {
48        let bottom_p = self.bottom.pressure.into_option()?;
49        let top_p = self.top.pressure.into_option()?;
50        if bottom_p == top_p {
51            None
52        } else {
53            Some(bottom_p - top_p)
54        }
55    }
56
57    /// Get the bulk wind shear (spd kts, direction degrees)
58    pub fn wind_shear(&self) -> Option<WindUV<MetersPSec>> {
59        let top = WindUV::from(self.top.wind.into_option()?);
60        let bottom = WindUV::from(self.bottom.wind.into_option()?);
61
62        Some(top - bottom)
63    }
64}
65
66#[cfg(test)]
67mod layer_tests {
68    use super::*;
69    use crate::sounding::DataRow;
70    use metfor::*;
71    use optional::some;
72
73    fn make_test_layer() -> Layer {
74        let mut bottom = DataRow::default();
75        bottom.pressure = some(HectoPascal(1000.0));
76        bottom.temperature = some(Celsius(20.0));
77        bottom.height = some(Meters(5.0));
78        bottom.wind = some(WindSpdDir::<Knots> {
79            speed: Knots(1.0),
80            direction: 180.0,
81        });
82
83        let mut top = DataRow::default();
84        top.pressure = some(HectoPascal(700.0));
85        top.temperature = some(Celsius(-2.0));
86        top.height = some(Meters(3012.0));
87        top.wind = some(WindSpdDir::<Knots> {
88            speed: Knots(1.0),
89            direction: 90.0,
90        });
91
92        Layer { bottom, top }
93    }
94
95    fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
96        (a - b).abs() <= tol
97    }
98
99    #[test]
100    fn test_height_thickness() {
101        let lyr = make_test_layer();
102        println!("{:#?}", lyr);
103        assert!(lyr
104            .height_thickness()
105            .unwrap()
106            .approx_eq(Meters(3007.0), Meters(std::f64::EPSILON)));
107    }
108
109    #[test]
110    fn test_pressure_thickness() {
111        let lyr = make_test_layer();
112        println!("{:#?}", lyr);
113        assert!(lyr
114            .pressure_thickness()
115            .unwrap()
116            .approx_eq(HectoPascal(300.0), HectoPascal(std::f64::EPSILON)));
117    }
118
119    #[test]
120    fn test_lapse_rate() {
121        let lyr = make_test_layer();
122        println!(
123            "{:#?}\n\n -- \n\n Lapse Rate = {:#?} \n\n --",
124            lyr,
125            lyr.lapse_rate().unwrap()
126        );
127        assert!(lyr
128            .lapse_rate()
129            .unwrap()
130            .approx_eq(CelsiusPKm(-7.31626), CelsiusPKm(1.0e-5)));
131    }
132
133    #[test]
134    fn test_wind_shear() {
135        let lyr = make_test_layer();
136        println!(
137            "{:#?}\n\n -- \n\n {:#?} \n\n --",
138            lyr,
139            lyr.wind_shear().unwrap()
140        );
141        let shear = WindSpdDir::<Knots>::from(lyr.wind_shear().unwrap());
142        let speed_shear = shear.abs();
143        let WindSpdDir {
144            direction: direction_shear,
145            ..
146        } = shear;
147
148        assert!(speed_shear.approx_eq(Knots(::std::f64::consts::SQRT_2), Knots(1.0e-5)));
149        assert!(approx_eq(direction_shear, 45.0, 1.0e-5));
150    }
151}
152
153mod temperature_layers;
154pub use temperature_layers::{
155    cold_surface_temperature_layer, dendritic_snow_zone, hail_growth_zone,
156    melting_freezing_energy_area, warm_surface_temperature_layer, warm_temperature_layer_aloft,
157    warm_wet_bulb_layer_aloft,
158};
159
160mod height_pressure;
161pub use height_pressure::{layer_agl, pressure_layer};
162
163mod inversions;
164pub use inversions::{inversions, sfc_based_inversion};
165
166mod convective;
167pub use convective::effective_inflow_layer;