1use crate::{
4 error::{AnalysisError, Result},
5 parcel::Parcel,
6 sounding::Sounding,
7};
8use itertools::{izip, Itertools};
9use metfor::{
10 Celsius, CelsiusDiff, GigaWatts, HectoPascal, Kelvin, KelvinDiff, Meters, MetersPSec, Quantity,
11 WindSpdDir,
12};
13
14#[inline]
24pub fn hot_dry_windy(snd: &Sounding) -> Result<f64> {
25 let elevation = if let Some(sfc_h) = snd.station_info().elevation().into_option() {
26 sfc_h
27 } else if let Some(lowest_h) = snd
28 .height_profile()
29 .iter()
30 .filter_map(|optd| optd.into_option())
31 .next()
32 {
33 lowest_h
34 } else {
35 return Err(AnalysisError::NotEnoughData);
36 };
37
38 let sfc_pressure: HectoPascal = snd
39 .pressure_profile()
40 .iter()
41 .filter_map(|p| p.into_option())
42 .next()
43 .ok_or(AnalysisError::NotEnoughData)?;
44
45 let p_profile = snd.pressure_profile();
46 let h_profile = snd.height_profile();
47 let t_profile = snd.temperature_profile();
48 let dp_profile = snd.dew_point_profile();
49 let ws_profile = snd.wind_profile();
50
51 let (vpd, ws) = izip!(p_profile, h_profile, t_profile, dp_profile, ws_profile)
52 .filter(|(p, h, t, dp, ws)| {
54 p.is_some() && h.is_some() && t.is_some() && dp.is_some() && ws.is_some()
55 })
56 .map(|(p, h, t, dp, ws)| {
58 (
59 p.unpack(),
60 h.unpack(),
61 t.unpack(),
62 dp.unpack(),
63 ws.unpack().speed,
64 )
65 })
66 .take_while(|(_, h, _, _, _)| *h <= elevation + Meters(500.0))
68 .map(|(p, _, t, dp, ws)| {
70 (
71 p,
72 metfor::temperature_from_pot_temp(
73 metfor::potential_temperature(p, t),
74 sfc_pressure,
75 ),
76 dp,
77 ws,
78 )
79 })
80 .filter_map(|(p, t, dp, ws)| {
82 metfor::specific_humidity(dp, p)
83 .and_then(|q| metfor::dew_point_from_p_and_specific_humidity(sfc_pressure, q))
84 .map(|dp| (t, dp, ws))
85 })
86 .filter_map(|(t, dp, ws)| {
88 metfor::vapor_pressure_water(t)
89 .and_then(|sat_vap| metfor::vapor_pressure_water(dp).map(|vap| (sat_vap - vap, ws)))
90 })
91 .map(|(vpd, ws)| (vpd.unpack(), MetersPSec::from(ws).unpack()))
93 .fold((0.0, 0.0), |(vpd_max, ws_max), (vpd, ws)| {
95 (vpd.max(vpd_max), ws.max(ws_max))
96 });
97
98 Ok(vpd * ws)
99}
100
101#[derive(Clone, Debug)]
104pub struct PFTAnalysis {
105 pub pft: GigaWatts,
108 pub theta_ml: Kelvin,
110 pub q_ml: f64,
112 pub u_ml: MetersPSec,
114 pub p_top_ml: HectoPascal,
116 pub p_fc: HectoPascal,
118 pub z_fc: Meters,
120 pub d_theta: KelvinDiff,
122 pub sp_curve: Vec<(HectoPascal, Celsius)>,
124 pub theta_e_fc: Kelvin,
127 pub theta_curve: Vec<(HectoPascal, Celsius)>,
130}
131
132pub fn pft_analysis(snd: &Sounding, moisture_ratio: f64) -> Result<PFTAnalysis> {
152 let (theta_ml, q_ml, _z_ml, p_sfc, p_top_ml) = entrained_mixed_layer(snd)?;
153
154 let (z_fc, p_fc, theta_fc, d_theta_fc, theta_e_fc, sp_curve, theta_curve) =
155 free_convection_level(snd, moisture_ratio, theta_ml, q_ml)?;
156
157 let u_ml: MetersPSec = entrained_layer_mean_wind_speed(z_fc, snd)?;
158
159 let pft = metfor::pft(z_fc, p_fc, u_ml, d_theta_fc, theta_fc, p_sfc);
160
161 Ok(PFTAnalysis {
162 pft,
163 theta_ml,
164 q_ml,
165 u_ml,
166 p_top_ml,
167 p_fc,
168 z_fc,
169 d_theta: d_theta_fc,
170 sp_curve,
171 theta_e_fc,
172 theta_curve,
173 })
174}
175
176pub fn pft(snd: &Sounding, moisture_ratio: f64) -> Result<GigaWatts> {
196 let (theta_ml, q_ml, _z_ml, p_sfc, _p_top_ml) = entrained_mixed_layer(snd)?;
197
198 let (z_fc, p_fc, theta_fc, d_theta_fc, _theta_e, _sp_curve, _theta_curve) =
199 free_convection_level(snd, moisture_ratio, theta_ml, q_ml)?;
200
201 let u_ml: MetersPSec = entrained_layer_mean_wind_speed(z_fc, snd)?;
202
203 Ok(metfor::pft(z_fc, p_fc, u_ml, d_theta_fc, theta_fc, p_sfc))
204}
205
206pub(crate) fn entrained_mixed_layer(
217 snd: &Sounding,
218) -> Result<(Kelvin, f64, Meters, HectoPascal, HectoPascal)> {
219 let elevation: Meters = snd
220 .station_info()
221 .elevation()
222 .ok_or(AnalysisError::NotEnoughData)?;
223
224 let mut ps = snd.pressure_profile();
225 let mut zs = snd.height_profile();
226 let mut ts = snd.temperature_profile();
227 let mut dps = snd.dew_point_profile();
228
229 if zs.len() >= 2 && zs[0] == zs[1] {
231 ps = &ps[1..];
232 zs = &zs[1..];
233 ts = &ts[1..];
234 dps = &dps[1..];
235 }
236
237 let bottom_p = ps
238 .iter()
239 .filter_map(|&p| p.into_option())
240 .next()
241 .ok_or(AnalysisError::NotEnoughData)?;
242
243 let max_p = bottom_p - HectoPascal(50.0);
244
245 match itertools::izip!(ps, zs, ts, dps)
246 .filter(|(p, z, t, dp)| p.is_some() && z.is_some() && t.is_some() && dp.is_some())
248 .map(|(p, z, t, dp)| (p.unpack(), z.unpack(), t.unpack(), dp.unpack()))
250 .map(|(p, z, t, dp)| (p, z - elevation, t, dp))
252 .map(|(p, h, t, dp)| (p, h, metfor::potential_temperature(p, t), dp))
254 .filter_map(|(p, h, theta, dp)| metfor::specific_humidity(dp, p).map(|q| (p, h, theta, q)))
256 .tuple_windows::<(_, _)>()
258 .scan(
260 (0.0, 0.0),
261 |state, ((_p0, h0, theta0, q0), (p1, h1, theta1, q1))| {
262 let (sum_theta, sum_q): (&mut f64, &mut f64) = (&mut state.0, &mut state.1);
263
264 let dh = (h1 - h0).unpack(); debug_assert!(dh > 0.0);
266
267 *sum_theta += (theta0.unpack() * h0.unpack() + theta1.unpack() * h1.unpack()) * dh;
268 *sum_q += (q0 * h0.unpack() + q1 * h1.unpack()) * dh;
269
270 let h_sq = h1.unpack() * h1.unpack();
272 let avg_theta = Kelvin(*sum_theta / h_sq);
273 let avg_q = *sum_q / h_sq;
274
275 Some((p1, h1, avg_theta, avg_q))
276 },
277 )
278 .filter(|(p1, _h1, _avg_theta, _avg_q)| *p1 <= max_p)
280 .filter_map(|(p1, h1, avg_theta, avg_q)| {
282 let temperature = Celsius::from(metfor::temperature_from_pot_temp(avg_theta, bottom_p));
283 let dew_point = metfor::dew_point_from_p_and_specific_humidity(bottom_p, avg_q)?;
284
285 let pcl = Parcel {
286 temperature,
287 dew_point,
288 pressure: bottom_p,
289 };
290
291 Some((p1, h1, avg_theta, avg_q, pcl))
292 })
293 .filter_map(|(p1, h1, avg_theta, avg_q, pcl)| {
295 metfor::pressure_at_lcl(pcl.temperature, pcl.dew_point, pcl.pressure)
296 .map(|lcl_pres| (p1, h1, avg_theta, avg_q, lcl_pres))
297 })
298 .tuple_windows::<(_, _)>()
300 .find(
302 |(
303 (p0, _h0, _avg_theta0, _avg_q0, lcl_pres0),
304 (p1, _h1, _avg_theta1, _avg_q1, lcl_pres1),
305 )| p0 >= lcl_pres0 && p1 < lcl_pres1,
306 ) {
307 Some(((p0, h0, avg_theta0, avg_q0, _), _)) => Ok((avg_theta0, avg_q0, h0, bottom_p, p0)),
308 None => Err(AnalysisError::FailedPrerequisite),
309 }
310}
311
312fn free_convection_level(
321 snd: &Sounding,
322 moisture_ratio: f64,
323 theta_ml: Kelvin,
324 q_ml: f64,
325) -> Result<(
326 Meters,
327 HectoPascal,
328 Kelvin,
329 KelvinDiff,
330 Kelvin,
331 Vec<(HectoPascal, Celsius)>,
332 Vec<(HectoPascal, Celsius)>,
333)> {
334 const SP_BETA_MAX: f64 = 0.20;
335 const DELTA_BETA: f64 = 0.001;
336
337 let apply_beta = move |beta| {
338 let theta_sp = Kelvin((1.0 + beta) * theta_ml.unpack());
339 let q_sp = q_ml + beta / moisture_ratio / 1000.0 * theta_ml.unpack();
340
341 (theta_sp, q_sp)
342 };
343
344 let mut sp_curve: Vec<(HectoPascal, Celsius)> = Vec::with_capacity(100);
345
346 let mut low_beta: f64 = f64::NAN;
347 let mut high_beta: f64 = f64::NAN;
348 let mut beta = 0.0;
349 let mut i = 0;
350 while beta <= SP_BETA_MAX {
351 let (theta_sp, q_sp) = apply_beta(beta);
352
353 let skew_t_coords =
354 potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)?;
355
356 sp_curve.push(skew_t_coords);
357
358 let (p, t) = skew_t_coords;
359
360 let sp_theta_e = match metfor::equiv_pot_temperature(t, t, p) {
361 Some(val) => val,
362 None => continue,
363 };
364
365 let meets_theta_e_requirements = is_free_convecting(snd, p, sp_theta_e)?;
366
367 if !meets_theta_e_requirements && high_beta.is_nan() {
368 low_beta = beta;
369 } else if meets_theta_e_requirements && high_beta.is_nan() {
370 high_beta = beta;
371 }
372
373 if !high_beta.is_nan() && i >= 100 {
374 break;
375 }
376
377 beta += DELTA_BETA;
378 i += 1;
379 }
380
381 if high_beta.is_nan() || low_beta.is_nan() {
382 return Err(AnalysisError::FailedPrerequisite);
383 }
384
385 let beta = metfor::find_root(
386 &|b| {
387 let (theta_sp, q_sp) = apply_beta(b);
388 let (p, t) =
389 potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)
390 .ok()?;
391 let sp_theta_e = metfor::equiv_pot_temperature(t, t, p)?;
392
393 Some(
394 (min_temperature_diff_to_max_cloud_top_temperature(snd, p, sp_theta_e).ok()?
395 - TEMPERATURE_BUFFER)
396 .unpack(),
397 )
398 },
399 low_beta,
400 high_beta,
401 )
402 .ok_or(AnalysisError::FailedPrerequisite)?;
403
404 let (theta_sp, q_sp) = apply_beta(beta);
405 let (p_lfc, t_lfc) =
406 potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)?;
407 let theta_e_lfc = metfor::equiv_pot_temperature(t_lfc, t_lfc, p_lfc)
408 .ok_or(AnalysisError::FailedPrerequisite)?;
409
410 let dtheta = theta_sp - theta_ml;
411
412 let heights = snd.height_profile();
413 let pressures = snd.pressure_profile();
414 let height_asl_lfc = crate::interpolation::linear_interpolate(pressures, heights, p_lfc)
415 .into_option()
416 .ok_or(AnalysisError::InterpolationError)?;
417 let sfc_height = heights
418 .iter()
419 .filter_map(|h| h.into_option())
420 .next()
421 .ok_or(AnalysisError::NotEnoughData)?;
422
423 let mut theta_curve: Vec<(HectoPascal, Celsius)> = pressures
424 .iter()
425 .filter_map(|p| {
426 p.into_option()
427 .map(|p| (p, metfor::temperature_from_pot_temp(theta_sp, p)))
428 })
429 .take_while(|&(p, _)| p > p_lfc)
430 .map(|(p, t)| (p, Celsius::from(t)))
431 .collect();
432 theta_curve.push((p_lfc, t_lfc));
433
434 Ok((
435 height_asl_lfc - sfc_height,
436 p_lfc,
437 theta_sp,
438 dtheta,
439 theta_e_lfc,
440 sp_curve,
441 theta_curve,
442 ))
443}
444
445pub(crate) fn potential_t_and_specific_humidity_to_pressure_and_temperature(
446 theta: Kelvin,
447 specific_humidity: f64,
448) -> Result<(HectoPascal, Celsius)> {
449 let diff = |p_hpa: f64| -> Option<f64> {
450 let t = metfor::temperature_from_pot_temp(theta, HectoPascal(p_hpa));
451 let dp =
452 metfor::dew_point_from_p_and_specific_humidity(HectoPascal(p_hpa), specific_humidity)?;
453
454 Some((t - dp).unpack())
455 };
456
457 let p = metfor::find_root(
458 &diff,
459 HectoPascal(1080.0).unpack(),
460 HectoPascal(100.0).unpack(),
461 )
462 .map(HectoPascal)
463 .ok_or(AnalysisError::FailedPrerequisite)?;
464
465 let t = Celsius::from(metfor::temperature_from_pot_temp(theta, p));
466
467 Ok((p, t))
468}
469
470fn min_temperature_diff_to_max_cloud_top_temperature(
472 snd: &Sounding,
473 starting_pressure: HectoPascal,
474 starting_theta_e: Kelvin,
475) -> Result<CelsiusDiff> {
476 const MAX_PLUME_TOP_T: Celsius = Celsius(-20.0);
477
478 let pp = snd.pressure_profile();
479 let tp = snd.temperature_profile();
480 let dpp = snd.dew_point_profile();
481
482 let min_diff: CelsiusDiff = izip!(pp, tp, dpp)
483 .filter(|(p, t, dp)| p.is_some() && t.is_some() && dp.is_some())
485 .map(|(p, t, dp)| (p.unpack(), t.unpack(), dp.unpack()))
487 .skip_while(|(p, _t, _dp)| *p > starting_pressure)
489 .filter_map(|(p, t, dp)| {
491 metfor::temperature_from_equiv_pot_temp_saturated_and_pressure(p, starting_theta_e)
492 .map(|pcl_t| (p, t, dp, pcl_t))
493 })
494 .enumerate()
496 .take_while(|(i, (_p, _t, _dp, pcl_t))| *i < 2 || *pcl_t >= MAX_PLUME_TOP_T)
497 .map(|(_i, row)| row)
499 .filter_map(|(p, t, dp, pcl_t)| {
501 metfor::virtual_temperature(t, dp, p).map(|vt| (p, vt, pcl_t))
502 })
503 .filter_map(|(p, vt, pcl_t)| {
505 metfor::virtual_temperature(pcl_t, pcl_t, p).map(|pcl_vt| (vt, pcl_vt))
506 })
507 .fold(CelsiusDiff(500.0), |min_diff, (vt, pcl_vt)| {
509 CelsiusDiff::from(pcl_vt - vt).min(min_diff)
510 });
511
512 if min_diff == CelsiusDiff(500.0) {
513 Err(AnalysisError::FailedPrerequisite)
514 } else {
515 Ok(min_diff)
516 }
517}
518
519const TEMPERATURE_BUFFER: CelsiusDiff = CelsiusDiff(0.5);
522fn is_free_convecting(
523 snd: &Sounding,
524 starting_pressure: HectoPascal,
525 starting_theta_e: Kelvin,
526) -> Result<bool> {
527 Ok(
528 min_temperature_diff_to_max_cloud_top_temperature(
529 snd,
530 starting_pressure,
531 starting_theta_e,
532 )? >= TEMPERATURE_BUFFER,
533 )
534}
535
536pub(crate) fn entrained_layer_mean_wind_speed(zfc: Meters, snd: &Sounding) -> Result<MetersPSec> {
540 let layer = crate::layer_agl(snd, zfc)?;
541
542 let mean_wind = crate::wind::mean_wind(&layer, snd)?;
543 let mean_wind = WindSpdDir::from(mean_wind).speed;
544
545 Ok(mean_wind)
546}