1use crate::{
6 error::{
7 AnalysisError::{InvalidInput, MissingProfile},
8 {AnalysisError, Result},
9 },
10 interpolation::{linear_interp, linear_interpolate_sounding},
11 layers::Layer,
12 sounding::{DataRow, Sounding},
13};
14use itertools::{izip, Itertools};
15use metfor::{Celsius, HectoPascal, Meters, FREEZING};
16use optional::Optioned;
17
18pub type Level = DataRow;
20
21pub type Levels = Vec<Level>;
23
24pub fn freezing_levels(snd: &Sounding) -> Result<Levels> {
26 find_temperature_levels(
27 FREEZING,
28 snd.pressure_profile(),
29 snd.temperature_profile(),
30 snd,
31 )
32}
33
34pub fn wet_bulb_zero_levels(snd: &Sounding) -> Result<Levels> {
36 find_temperature_levels(
37 FREEZING,
38 snd.pressure_profile(),
39 snd.wet_bulb_profile(),
40 snd,
41 )
42}
43
44fn find_temperature_levels(
45 target_t: Celsius,
46 p_profile: &[Optioned<HectoPascal>],
47 t_profile: &[Optioned<Celsius>],
48 snd: &Sounding,
49) -> Result<Levels> {
50 const TOP_PRESSURE: HectoPascal = HectoPascal(500.0); if t_profile.is_empty() || p_profile.is_empty() {
53 return Err(AnalysisError::MissingProfile);
54 }
55
56 #[inline]
57 fn crosses_target(t0: Celsius, t1: Celsius, target_t: Celsius) -> bool {
58 (t0 <= target_t && t1 >= target_t) || (t0 >= target_t && t1 <= target_t)
59 }
60
61 izip!(p_profile, t_profile)
62 .filter(|(p, t)| p.is_some() && t.is_some())
64 .map(|(p, t)| (p.unpack(), t.unpack()))
66 .take_while(|&(p, _)| p >= TOP_PRESSURE)
68 .tuple_windows::<(_, _)>()
70 .filter(|&((_p0, t0), (_p1, t1))| crosses_target(t0, t1, target_t))
72 .map(|((p0, t0), (p1, t1))| linear_interp(target_t, t0, t1, p0, p1))
74 .map(|p_level| linear_interpolate_sounding(snd, p_level))
76 .collect()
77}
78
79pub fn max_wet_bulb_in_profile(snd: &Sounding) -> Result<Level> {
81 max_t_aloft(snd, snd.wet_bulb_profile())
82}
83
84pub fn max_temperature_in_profile(snd: &Sounding) -> Result<Level> {
86 max_t_aloft(snd, snd.temperature_profile())
87}
88
89fn max_t_aloft(snd: &Sounding, t_profile: &[Optioned<Celsius>]) -> Result<Level> {
91 const TOP_PRESSURE: HectoPascal = HectoPascal(500.0); let p_profile = snd.pressure_profile();
94
95 if t_profile.is_empty() || p_profile.is_empty() {
96 return Err(AnalysisError::MissingProfile);
97 }
98
99 let sentinel = Celsius::from(metfor::Kelvin(0.0));
100
101 let (idx, mx_t) = izip!(0usize.., p_profile, t_profile)
102 .filter(|(_, p, t)| p.is_some() && t.is_some())
104 .map(|(i, p, t)| (i, p.unpack(), t.unpack()))
106 .take_while(|&(_, p, _)| p >= TOP_PRESSURE)
108 .fold((0, sentinel), |acc, (i, _, t)| {
110 let (_, mx_t) = acc;
111 if t > mx_t {
112 (i, t)
113 } else {
114 acc
116 }
117 });
118
119 if mx_t == sentinel {
120 Err(AnalysisError::NotEnoughData)
121 } else {
122 snd.data_row(idx).ok_or(InvalidInput)
123 }
124}
125
126pub fn max_temperature_in_layer(snd: &Sounding, lyr: &Layer) -> Result<Level> {
128 max_t_in_layer(snd, snd.temperature_profile(), lyr)
129}
130
131pub fn max_wet_bulb_in_layer(snd: &Sounding, lyr: &Layer) -> Result<Level> {
133 max_t_in_layer(snd, snd.wet_bulb_profile(), lyr)
134}
135
136#[inline]
137fn max_t_in_layer(snd: &Sounding, t_profile: &[Optioned<Celsius>], lyr: &Layer) -> Result<Level> {
138 let (bottom_p, top_p) = if lyr.bottom.pressure.is_some() && lyr.top.pressure.is_some() {
139 (lyr.bottom.pressure.unpack(), lyr.top.pressure.unpack())
140 } else {
141 return Err(AnalysisError::InvalidInput);
142 };
143
144 let p_profile = snd.pressure_profile();
145
146 if t_profile.is_empty() || p_profile.is_empty() {
147 return Err(AnalysisError::MissingProfile);
148 }
149
150 izip!(0usize.., p_profile, t_profile)
151 .filter(|(_, p, t)| p.is_some() && t.is_some())
153 .map(|(i, p, t)| (i, p.unpack(), t.unpack()))
155 .skip_while(|(_, p, _)| *p > bottom_p)
157 .take_while(|(_, p, _)| *p >= top_p)
159 .fold(
161 Err(AnalysisError::NotEnoughData),
162 |acc: Result<_>, (i, _, t)| {
163 if let Ok((_, mx_t)) = acc {
164 if t > mx_t {
165 Ok((i, t))
166 } else {
167 acc
169 }
170 } else {
171 Ok((i, t))
173 }
174 },
175 )
176 .and_then(|(idx, _)| snd.data_row(idx).ok_or(InvalidInput))
178}
179
180pub(crate) fn height_level(tgt_height: Meters, snd: &Sounding) -> Result<Level> {
182 let h_profile = snd.height_profile();
183 let p_profile = snd.pressure_profile();
184
185 if h_profile.is_empty() || p_profile.is_empty() {
186 return Err(MissingProfile);
187 }
188
189 #[inline]
191 fn is_cross_over(h0: Meters, h1: Meters, tgt_height: Meters) -> bool {
192 debug_assert!(h0 <= h1);
193 h0 <= tgt_height && h1 >= tgt_height
194 }
195
196 izip!(p_profile, h_profile)
197 .filter(|(p, h)| p.is_some() && h.is_some())
199 .map(|(p, h)| (p.unpack(), h.unpack()))
201 .tuple_windows::<(_, _)>()
203 .find(|&((_p0, h0), (_p1, h1))| is_cross_over(h0, h1, tgt_height))
205 .map(|((p0, h0), (p1, h1))| linear_interp(tgt_height, h0, h1, p0, p1))
207 .ok_or(AnalysisError::InvalidInput)
209 .and_then(|level_p| linear_interpolate_sounding(snd, level_p))
211}