1use crate::{
3 error::{AnalysisError, Result},
4 interpolation::linear_interpolate_sounding,
5 parcel::Parcel,
6 sounding::{DataRow, Sounding},
7};
8use itertools::izip;
9use metfor::{self, Celsius, CelsiusDiff, HectoPascal, JpKg, Kelvin, Meters, MetersPSec, Quantity};
10use optional::Optioned;
11
12#[derive(Debug, Clone)]
14pub struct ParcelProfile {
15 pub pressure: Vec<HectoPascal>,
17 pub height: Vec<Meters>,
19 pub parcel_t: Vec<Celsius>,
21 pub environment_t: Vec<Celsius>,
23}
24
25pub(crate) mod lift;
26
27#[derive(Debug, Clone)]
33pub struct ParcelAscentAnalysis {
34 parcel: Parcel,
36 profile: ParcelProfile,
37
38 cape: Optioned<JpKg>,
40 hail_cape: Optioned<JpKg>,
41 ncape: Optioned<f64>,
42 lcl_height_agl: Optioned<Meters>, lcl_pressure: Optioned<HectoPascal>, lcl_temperature: Optioned<Celsius>, cin: Optioned<JpKg>,
46 el_pressure: Optioned<HectoPascal>, el_height_asl: Optioned<Meters>, el_temperature: Optioned<Celsius>, lfc_pressure: Optioned<HectoPascal>, lfc_virt_temperature: Optioned<Celsius>, }
52
53impl ParcelAscentAnalysis {
54 pub fn cape(&self) -> Optioned<JpKg> {
56 self.cape
57 }
58
59 pub fn hail_cape(&self) -> Optioned<JpKg> {
61 self.hail_cape
62 }
63
64 pub fn ncape(&self) -> Optioned<f64> {
66 self.ncape
67 }
68
69 pub fn lcl_height_agl(&self) -> Optioned<Meters> {
71 self.lcl_height_agl
72 }
73
74 pub fn lcl_pressure(&self) -> Optioned<HectoPascal> {
76 self.lcl_pressure
77 }
78
79 pub fn lcl_temperature(&self) -> Optioned<Celsius> {
81 self.lcl_temperature
82 }
83
84 pub fn cin(&self) -> Optioned<JpKg> {
86 self.cin
87 }
88
89 pub fn el_pressure(&self) -> Optioned<HectoPascal> {
91 self.el_pressure
92 }
93
94 pub fn el_height_asl(&self) -> Optioned<Meters> {
96 self.el_height_asl
97 }
98
99 pub fn el_temperature(&self) -> Optioned<Celsius> {
101 self.el_temperature
102 }
103
104 pub fn lfc_pressure(&self) -> Optioned<HectoPascal> {
106 self.lfc_pressure
107 }
108
109 pub fn lfc_virt_temperature(&self) -> Optioned<Celsius> {
111 self.lfc_virt_temperature
112 }
113
114 #[inline]
116 pub fn profile(&self) -> &ParcelProfile {
117 &self.profile
118 }
119
120 #[inline]
122 pub fn parcel(&self) -> &Parcel {
123 &self.parcel
124 }
125
126 #[inline]
129 pub fn calculate_cape_speed(&self) -> Option<MetersPSec> {
130 self.cape
131 .map(|cape| MetersPSec::pack(f64::sqrt(2.0 * cape.unpack())))
132 }
133}
134
135pub fn lift_parcel(parcel: Parcel, snd: &Sounding) -> Result<ParcelAscentAnalysis> {
140 lift::lift_parcel(parcel, snd)
141}
142
143pub(crate) fn find_parcel_start_data(snd: &Sounding, parcel: &Parcel) -> Result<(DataRow, Parcel)> {
147 let good_row = |row: &DataRow| -> bool {
148 row.temperature.is_some()
149 && row.dew_point.is_some()
150 && row.pressure.is_some()
151 && row.height.is_some()
152 };
153
154 let first_guess = linear_interpolate_sounding(snd, parcel.pressure)?;
155 if good_row(&first_guess) {
156 return Ok((first_guess, *parcel));
157 }
158
159 let second_guess = snd
160 .bottom_up()
161 .find(good_row)
162 .ok_or(AnalysisError::NotEnoughData)?;
163
164 let pressure = second_guess.pressure.ok_or(AnalysisError::InvalidInput)?;
165 let theta = parcel.theta();
166 let temperature = Celsius::from(metfor::temperature_from_pot_temp(theta, pressure));
167 let mw = parcel.mixing_ratio()?;
168 let dew_point =
169 metfor::dew_point_from_p_and_mw(pressure, mw).ok_or(AnalysisError::MetForError)?;
170 let new_parcel = Parcel {
171 pressure,
172 temperature,
173 dew_point,
174 };
175
176 Ok((second_guess, new_parcel))
177}
178
179pub fn robust_convective_parcel_ascent(snd: &Sounding) -> Result<ParcelAscentAnalysis> {
192 let mut start_parcel = crate::parcel::convective_parcel(snd)?;
193 let mut analysis = lift_parcel(start_parcel, snd)?;
194
195 if analysis.lcl_pressure <= analysis.el_pressure {
196 let mut warmer_t = start_parcel.temperature + CelsiusDiff(1.0);
198 let mut warmer_parcel = Parcel {
199 temperature: warmer_t,
200 ..start_parcel
201 };
202 let mut anal = lift_parcel(warmer_parcel, snd)?;
203
204 while anal.lcl_pressure <= anal.el_pressure {
206 start_parcel = warmer_parcel;
207 warmer_t += CelsiusDiff(1.0);
208 warmer_parcel = Parcel {
209 temperature: warmer_t,
210 ..start_parcel
211 };
212 anal = lift_parcel(warmer_parcel, snd)?;
213 }
214 analysis = anal;
215
216 let mut diff = 1.0;
217 while diff > 0.1 {
218 diff = (warmer_parcel.temperature - start_parcel.temperature).unpack() / 2.0;
220 let mid_t = start_parcel.temperature + CelsiusDiff(diff);
221 let mid_parcel = Parcel {
222 temperature: mid_t,
223 ..start_parcel
224 };
225 anal = lift_parcel(mid_parcel, snd)?;
226 if anal.lcl_pressure <= anal.el_pressure {
227 start_parcel = mid_parcel;
228 } else {
229 warmer_parcel = mid_parcel;
230 analysis = anal;
231 }
232 }
233 }
234
235 Ok(analysis)
236}
237
238pub fn mix_down(parcel: Parcel, snd: &Sounding) -> Result<ParcelProfile> {
244 let theta = parcel.theta();
245 let theta_func = |theta_val, press| {
246 Some(Celsius::from(metfor::temperature_from_pot_temp(
247 theta_val, press,
248 )))
249 };
250
251 descend_parcel(parcel, snd, theta, theta_func, false, false)
252}
253
254fn descend_moist(parcel: Parcel, snd: &Sounding) -> Result<ParcelProfile> {
259 let theta = parcel.theta_e()?;
260
261 let theta_func = |theta_e, press| {
262 metfor::temperature_from_equiv_pot_temp_saturated_and_pressure(press, theta_e)
263 };
264
265 descend_parcel(parcel, snd, theta, theta_func, true, true)
266}
267
268#[inline]
269fn descend_parcel<F>(
270 parcel: Parcel,
271 snd: &Sounding,
272 theta: Kelvin,
273 theta_func: F,
274 saturated: bool,
275 virtual_t: bool,
276) -> Result<ParcelProfile>
277where
278 F: Fn(Kelvin, HectoPascal) -> Option<Celsius>,
279{
280 let mut pressure = Vec::new();
281 let mut height = Vec::new();
282 let mut parcel_t = Vec::new();
283 let mut environment_t = Vec::new();
284
285 let press = snd.pressure_profile();
287 let env_t = snd.temperature_profile();
288 let env_dp = snd.dew_point_profile();
289 let hght = snd.height_profile();
290
291 let pcl_mw = parcel.mixing_ratio()?;
292
293 {
295 let mut add_row = |pp, hh, pcl_tt, env_tt| {
297 pressure.push(pp);
298 height.push(hh);
299 parcel_t.push(pcl_tt);
300 environment_t.push(env_tt);
301 };
302
303 izip!(press, hght, env_t, env_dp)
304 .take_while(|(p_opt, _, _, _)| {
305 if p_opt.is_some() {
306 p_opt.unpack() >= parcel.pressure
307 } else {
308 true }
310 })
311 .filter_map(|(p_opt, h_opt, e_t_opt, e_dp_opt)| {
313 if p_opt.is_some() && h_opt.is_some() && e_t_opt.is_some() && e_dp_opt.is_some() {
314 Some((
315 p_opt.unpack(),
316 h_opt.unpack(),
317 e_t_opt.unpack(),
318 e_dp_opt.unpack(),
319 ))
320 } else {
321 None
322 }
323 })
324 .filter_map(|(p, h, e_t, e_dp)| {
326 theta_func(theta, p).map(|pcl_t| (p, h, pcl_t, e_t, e_dp))
327 })
328 .filter_map(|(p, h, pcl_t, e_t, e_dp)| {
330 let p_dp = if saturated {
331 pcl_t
332 } else {
333 metfor::dew_point_from_p_and_mw(p, pcl_mw)?
334 };
335
336 Some((p, h, pcl_t, p_dp, e_t, e_dp))
337 })
338 .filter_map(|(p, h, pcl_t, p_dp, e_t, e_dp)| {
340 let pcl_t = if virtual_t {
341 Celsius::from(metfor::virtual_temperature(pcl_t, p_dp, p)?)
342 } else {
343 pcl_t
344 };
345
346 let e_t = if virtual_t {
347 Celsius::from(metfor::virtual_temperature(e_t, e_dp, p)?)
348 } else {
349 e_t
350 };
351
352 Some((p, h, pcl_t, e_t))
353 })
354 .for_each(|(p, h, pt, et)| {
355 add_row(p, h, pt, et);
356 });
357
358 let parcel_level = linear_interpolate_sounding(snd, parcel.pressure)?;
360 let parcel_height = parcel_level.height.ok_or(AnalysisError::MissingValue)?;
361 let env_t = parcel_level
362 .temperature
363 .ok_or(AnalysisError::MissingValue)?;
364 add_row(parcel.pressure, parcel_height, parcel.temperature, env_t);
365 }
366
367 Ok(ParcelProfile {
368 pressure,
369 height,
370 parcel_t,
371 environment_t,
372 })
373}
374
375pub fn dcape(snd: &Sounding) -> Result<(ParcelProfile, JpKg, Celsius)> {
382 let t = snd.temperature_profile();
383 let dp = snd.dew_point_profile();
384 let p = snd.pressure_profile();
385
386 let top_p = p
388 .iter()
389 .filter_map(|p| p.into_option())
390 .next()
391 .ok_or(AnalysisError::NotEnoughData)?
392 - HectoPascal(400.0);
393
394 debug_assert!(
398 top_p.into_option().is_some() && top_p > HectoPascal(100.0),
399 "surface pressure is below 500 mb"
400 );
401
402 let pcl = izip!(p, t, dp)
404 .filter_map(|(p, t, dp)| {
405 if p.is_some() && t.is_some() && dp.is_some() {
406 Some((p.unpack(), t.unpack(), dp.unpack()))
407 } else {
408 None
409 }
410 })
411 .take_while(|(p, _, _)| *p >= top_p)
412 .fold(
413 Err(AnalysisError::NotEnoughData),
414 |acc: Result<Parcel>, (p, t, dp)| match metfor::equiv_pot_temperature(t, dp, p) {
415 Some(th_e) => match acc {
416 Ok(parcel) => {
417 if let Ok(old_theta) = parcel.theta_e() {
418 if old_theta < th_e {
419 Ok(parcel)
420 } else {
421 Ok(Parcel {
422 temperature: t,
423 dew_point: dp,
424 pressure: p,
425 })
426 }
427 } else {
428 Ok(Parcel {
429 temperature: t,
430 dew_point: dp,
431 pressure: p,
432 })
433 }
434 }
435 Err(_) => Ok(Parcel {
436 temperature: t,
437 dew_point: dp,
438 pressure: p,
439 }),
440 },
441 None => acc,
442 },
443 )?;
444
445 let profile = descend_moist(pcl, snd)?;
446
447 let mut dcape = 0.0;
448 let mut h0 = Meters(std::f64::MAX); let mut pt0 = Kelvin(0.0);
450 let mut et0 = Kelvin(0.0);
451 for (&p, &h, &pt, &et) in izip!(
452 &profile.pressure,
453 &profile.height,
454 &profile.parcel_t,
455 &profile.environment_t
456 ) {
457 let pt = metfor::potential_temperature(p, pt);
458 let et = metfor::potential_temperature(p, et);
459
460 let dz = h - h0;
461 if dz <= Meters(0.0) {
463 h0 = h;
464 pt0 = pt;
465 et0 = et;
466 continue;
467 }
468
469 dcape +=
470 ((pt - et).unpack() / et.unpack() + (pt0 - et0).unpack() / et0.unpack()) * dz.unpack();
471
472 h0 = h;
473 pt0 = pt;
474 et0 = et;
475 }
476
477 dcape *= metfor::g / 2.0;
479
480 let downrush_t = *profile.parcel_t.get(0).ok_or(AnalysisError::MissingValue)?;
481
482 Ok((profile, JpKg(dcape), downrush_t))
483}