1use super::{Layer, Layers};
2use crate::{
3 error::{AnalysisError, Result},
4 interpolation::{linear_interp, linear_interpolate_sounding},
5 sounding::Sounding,
6};
7use itertools::{izip, Itertools};
8use metfor::{
9 Celsius, CelsiusDiff, HectoPascal, JpKg, Kelvin, KelvinDiff, Meters, Quantity, FREEZING,
10};
11use optional::Optioned;
12
13use std::iter::once;
14
15pub fn dendritic_snow_zone(snd: &Sounding) -> Result<Layers> {
20 temperature_layer(snd, Celsius(-12.0), Celsius(-18.0), HectoPascal(300.0))
21}
22
23pub fn hail_growth_zone(snd: &Sounding) -> Result<Layers> {
28 temperature_layer(snd, Celsius(-10.0), Celsius(-30.0), HectoPascal(1.0))
29}
30
31#[inline]
32fn temperature_layer(
33 snd: &Sounding,
34 warm_side: Celsius,
35 cold_side: Celsius,
36 top_pressure: HectoPascal,
37) -> Result<Layers> {
38 let t_profile = snd.temperature_profile();
39 let p_profile = snd.pressure_profile();
40
41 if t_profile.is_empty() || p_profile.is_empty() {
42 return Err(AnalysisError::MissingProfile);
43 }
44
45 enum Crossing {
48 Enter(HectoPascal),
50 Exit(HectoPascal),
52 Over(HectoPascal, HectoPascal),
55 In(HectoPascal),
57 }
58
59 fn to_crossing_type(
62 pnt0: (HectoPascal, Celsius),
63 pnt1: (HectoPascal, Celsius),
64 warm_side: Celsius,
65 cold_side: Celsius,
66 ) -> Option<Crossing> {
67 let (p0, t0) = pnt0;
68 let (p1, t1) = pnt1;
69
70 debug_assert!(p0 >= p1, "Pressure must decrease with height.");
71
72 if t0 < cold_side && t1 >= cold_side && t1 <= warm_side {
73 let cold_p = linear_interp(cold_side, t0, t1, p0, p1);
75 Some(Crossing::Enter(cold_p))
76 } else if t0 > warm_side && t1 <= warm_side && t1 >= cold_side {
77 let warm_p = linear_interp(warm_side, t0, t1, p0, p1);
79 Some(Crossing::Enter(warm_p))
80 } else if (t0 < cold_side && t1 > warm_side) || (t0 > warm_side && t1 < cold_side) {
81 let warm_p = linear_interp(warm_side, t0, t1, p0, p1);
83 let cold_p = linear_interp(cold_side, t0, t1, p0, p1);
84 Some(Crossing::Over(warm_p.max(cold_p), warm_p.min(cold_p)))
85 } else if t0 >= cold_side && t0 <= warm_side && t1 > warm_side {
86 let warm_p = linear_interp(warm_side, t0, t1, p0, p1);
88 Some(Crossing::Exit(warm_p))
89 } else if t0 >= cold_side && t0 <= warm_side && t1 < cold_side {
90 let cold_p = linear_interp(cold_side, t0, t1, p0, p1);
92 Some(Crossing::Exit(cold_p))
93 } else if t0 >= cold_side && t0 <= warm_side && t1 >= cold_side && t1 <= warm_side {
94 Some(Crossing::In(p0))
96 } else {
97 None
98 }
99 }
100
101 izip!(p_profile, t_profile)
102 .filter(|(p, t)| p.is_some() && t.is_some())
104 .map(|(p, t)| (p.unpack(), t.unpack()))
106 .take_while(move |&(p, _)| p > top_pressure)
108 .tuple_windows::<(_, _)>()
110 .filter_map(|(pnt0, pnt1)| to_crossing_type(pnt0, pnt1, warm_side, cold_side))
112 .scan(None, |bottom_p: &mut Option<_>, crossing_type: Crossing| {
114 match crossing_type {
115 Crossing::In(p) => {
116 if bottom_p.is_none() {
117 *bottom_p = Some(p);
119 }
120 Some(None)
123 }
124 Crossing::Enter(p) => {
125 *bottom_p = Some(p);
126 Some(None)
127 }
128 Crossing::Exit(top_p) => {
129 if let Some(bp) = bottom_p.take() {
130 Some(Some((bp, top_p)))
131 } else {
132 Some(None)
136 }
137 }
138 Crossing::Over(p0, p1) => {
139 debug_assert!(bottom_p.is_none());
140 Some(Some((p0, p1)))
141 }
142 }
143 })
144 .flatten()
146 .map(|(bottom_p, top_p)| {
148 linear_interpolate_sounding(snd, bottom_p).and_then(|bottom| {
149 linear_interpolate_sounding(snd, top_p).map(|top| Layer { bottom, top })
150 })
151 }) .collect()
153}
154
155pub fn warm_temperature_layer_aloft(snd: &Sounding) -> Result<Layers> {
162 warm_layer_aloft(snd, snd.temperature_profile())
163}
164
165pub fn warm_wet_bulb_layer_aloft(snd: &Sounding) -> Result<Layers> {
169 warm_layer_aloft(snd, snd.wet_bulb_profile())
170}
171
172#[inline]
173fn warm_layer_aloft(snd: &Sounding, t_profile: &[Optioned<Celsius>]) -> Result<Layers> {
174 let p_profile = snd.pressure_profile();
175
176 if t_profile.is_empty() || p_profile.is_empty() {
177 return Err(AnalysisError::MissingProfile);
178 }
179
180 enum Crossing {
181 IntoWarmLayer(HectoPascal),
182 OutOfWarmLayer(HectoPascal),
183 }
184
185 fn crossing_type(
186 pnt0: (HectoPascal, Celsius),
187 pnt1: (HectoPascal, Celsius),
188 ) -> Option<Crossing> {
189 let (p0, t0) = pnt0;
190 let (p1, t1) = pnt1;
191
192 if t0 < FREEZING && t1 >= FREEZING {
193 let crossing_p = linear_interp(FREEZING, t0, t1, p0, p1);
194 Some(Crossing::IntoWarmLayer(crossing_p))
195 } else if t0 >= FREEZING && t1 < FREEZING {
196 let crossing_p = linear_interp(FREEZING, t0, t1, p0, p1);
197 Some(Crossing::OutOfWarmLayer(crossing_p))
198 } else {
199 None
200 }
201 }
202
203 izip!(p_profile, t_profile)
204 .filter(|(p, t)| p.is_some() && t.is_some())
206 .map(|(p, t)| (p.unpack(), t.unpack()))
208 .skip_while(|&(_, t)| t > FREEZING)
210 .take_while(|&(p, _)| p > HectoPascal(500.0))
212 .tuple_windows::<(_, _)>()
214 .filter_map(|(pnt0, pnt1)| crossing_type(pnt0, pnt1))
216 .scan(
218 None,
219 |bottom: &mut Option<_>, crossing: Crossing| match crossing {
220 Crossing::IntoWarmLayer(bottom_p) => {
221 debug_assert!(bottom.is_none());
222 *bottom = Some(bottom_p);
223 Some(None)
224 }
225 Crossing::OutOfWarmLayer(top_p) => {
226 Some(bottom.take().map(|bottom_p| (bottom_p, top_p)))
229 }
230 },
231 )
232 .flatten()
234 .map(|(bottom_p, top_p)| {
236 linear_interpolate_sounding(snd, bottom_p).and_then(|bottom| {
237 linear_interpolate_sounding(snd, top_p).map(|top| Layer { bottom, top })
238 })
239 }) .collect()
241}
242
243pub fn cold_surface_temperature_layer(
247 snd: &Sounding,
248 warm_layers: &[Layer],
249) -> Result<Option<Layer>> {
250 Ok(cold_surface_layer(
251 snd,
252 snd.temperature_profile(),
253 warm_layers,
254 ))
255}
256
257fn cold_surface_layer(
258 snd: &Sounding,
259 t_profile: &[Optioned<Celsius>],
260 warm_layers: &[Layer],
261) -> Option<Layer> {
262 if warm_layers.is_empty() {
263 return None;
264 }
265
266 let p_profile = snd.pressure_profile();
267
268 let layer: Option<Layer> = izip!(0usize.., p_profile, t_profile)
269 .filter(|(_, p, t)| p.is_some() && t.is_some())
271 .map(|(i, _, t)| (i, t.unpack()))
273 .take_while(|(_, t)| *t <= FREEZING)
275 .next() .and_then(|(i, _)| snd.data_row(i)) .and_then(|bottom| warm_layers.get(0).map(|lyr| (bottom, lyr.bottom)))
281 .map(|(bottom, top)| Layer { bottom, top });
283
284 layer
285}
286
287pub fn warm_surface_temperature_layer(snd: &Sounding) -> Result<Option<Layer>> {
289 warm_surface_layer(snd, snd.temperature_profile())
290}
291
292fn warm_surface_layer(snd: &Sounding, t_profile: &[Optioned<Celsius>]) -> Result<Option<Layer>> {
293 let p_profile = snd.pressure_profile();
294
295 if t_profile.is_empty() || p_profile.is_empty() {
296 return Err(AnalysisError::NotEnoughData);
297 }
298
299 let iter = izip!(0usize.., p_profile, t_profile)
300 .filter(|(_, p, t)| p.is_some() && t.is_some())
302 .map(|(i, p, t)| (i, p.unpack(), t.unpack()));
304
305 let bottom = iter
306 .clone()
307 .take_while(|(_, _, t)| *t >= FREEZING)
308 .next()
309 .and_then(|(i, _, _)| snd.data_row(i));
310
311 let top = iter
312 .tuple_windows::<(_, _)>()
313 .take_while(|((_, _, t), _)| *t >= FREEZING)
315 .last()
317 .map(|((_, p0, t0), (_, p1, t1))| linear_interp(FREEZING, t0, t1, p0, p1))
319 .map(|target_p| linear_interpolate_sounding(snd, target_p))
320 .transpose()?;
321
322 Ok(bottom.and_then(|bottom| top.map(|top| Layer { bottom, top })))
323}
324
325pub fn melting_freezing_energy_area(snd: &Sounding, lyr: &Layer) -> Result<JpKg> {
333 let top = &lyr.top;
334 let bottom = &lyr.bottom;
335
336 let theta_top: Kelvin = top
342 .pressure
343 .map(|p| metfor::potential_temperature(p, FREEZING))
344 .ok_or(AnalysisError::MissingValue)?;
345
346 let theta_bottom: Kelvin = bottom
347 .pressure
348 .map(|p| metfor::potential_temperature(p, FREEZING))
349 .ok_or(AnalysisError::MissingValue)?;
350
351 let bottom_h = bottom.height.ok_or(AnalysisError::MissingValue)?;
352 let top_h = top.height.ok_or(AnalysisError::MissingValue)?;
353
354 let bottom_iter = once((bottom.height, bottom.temperature))
355 .filter(|(h, t)| h.is_some() && t.is_some())
356 .map(|(h, t)| (h.unpack(), t.unpack()));
357 let top_iter = once((top.height, top.temperature))
358 .filter(|(h, t)| h.is_some() && t.is_some())
359 .map(|(h, t)| (h.unpack(), t.unpack()));
360
361 let middle_iter = izip!(snd.height_profile(), snd.temperature_profile())
362 .filter(|(h, t)| h.is_some() && t.is_some())
363 .map(|(h, t)| (h.unpack(), t.unpack()))
364 .skip_while(|(h, _)| *h <= bottom_h)
365 .take_while(|(h, _)| *h < top_h);
366
367 let iter = bottom_iter
368 .chain(middle_iter)
369 .chain(top_iter)
370 .map(|(h, t)| (h, Kelvin::from(t)))
371 .tuple_windows::<(_, _)>();
372
373 let (sum_dz, sum_kelvin_dz) = iter.fold((Meters(0.0), 0.0), |acc, ((h0, t0), (h1, t1))| {
374 let (mut sum_dz, mut sum_kelvin_dz) = acc;
375
376 let dz: Meters = h1 - h0;
377 let dt0: KelvinDiff = t0 - Kelvin::from(FREEZING);
378 let dt1: KelvinDiff = t1 - Kelvin::from(FREEZING);
379 let kelvin_dz = (dt0 + dt1).unpack() * dz.unpack();
380
381 sum_dz += dz;
382 sum_kelvin_dz += kelvin_dz;
383
384 (sum_dz, sum_kelvin_dz)
385 });
386
387 let mean_temp = CelsiusDiff(sum_kelvin_dz / sum_dz.unpack() / 2.0);
388
389 let energy = metfor::cpd * mean_temp * f64::ln(theta_top / theta_bottom);
390
391 Ok(energy)
392}