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 p_top_ml: HectoPascal,
114 pub p_fc: HectoPascal,
116 pub sp_curve: Vec<(HectoPascal, Celsius)>,
118 pub theta_e_fc: Kelvin,
121}
122
123pub fn pft_analysis(snd: &Sounding, moisture_ratio: f64) -> Result<PFTAnalysis> {
143 let (theta_ml, q_ml, _z_ml, p_sfc, p_top_ml) = entrained_mixed_layer(snd)?;
144
145 let (z_fc, p_fc, theta_fc, d_theta_fc, theta_e_fc, sp_curve) =
146 free_convection_level(snd, moisture_ratio, theta_ml, q_ml)?;
147
148 let u_ml: MetersPSec = entrained_layer_mean_wind_speed(z_fc, snd)?;
149
150 let pft = metfor::pft(z_fc, p_fc, u_ml, d_theta_fc, theta_fc, p_sfc);
151
152 Ok(PFTAnalysis {
153 pft,
154 theta_ml,
155 q_ml,
156 p_top_ml,
157 p_fc,
158 sp_curve,
159 theta_e_fc,
160 })
161}
162
163pub fn pft(snd: &Sounding, moisture_ratio: f64) -> Result<GigaWatts> {
183 let (theta_ml, q_ml, _z_ml, p_sfc, _p_top_ml) = entrained_mixed_layer(snd)?;
184
185 let (z_fc, p_fc, theta_fc, d_theta_fc, _theta_e, _sp_curve) =
186 free_convection_level(snd, moisture_ratio, theta_ml, q_ml)?;
187
188 let u_ml: MetersPSec = entrained_layer_mean_wind_speed(z_fc, snd)?;
189
190 Ok(metfor::pft(z_fc, p_fc, u_ml, d_theta_fc, theta_fc, p_sfc))
191}
192
193fn entrained_mixed_layer(
203 snd: &Sounding,
204) -> Result<(Kelvin, f64, Meters, HectoPascal, HectoPascal)> {
205 let elevation: Meters = snd
206 .station_info()
207 .elevation()
208 .ok_or(AnalysisError::NotEnoughData)?;
209
210 let mut ps = snd.pressure_profile();
211 let mut zs = snd.height_profile();
212 let mut ts = snd.temperature_profile();
213 let mut dps = snd.dew_point_profile();
214
215 if zs.len() >= 2 && zs[0] == zs[1] {
217 ps = &ps[1..];
218 zs = &zs[1..];
219 ts = &ts[1..];
220 dps = &dps[1..];
221 }
222
223 let bottom_p = ps
224 .iter()
225 .filter_map(|&p| p.into_option())
226 .next()
227 .ok_or(AnalysisError::NotEnoughData)?;
228
229 let max_p = bottom_p - HectoPascal(50.0);
230
231 match itertools::izip!(ps, zs, ts, dps)
232 .filter(|(p, z, t, dp)| p.is_some() && z.is_some() && t.is_some() && dp.is_some())
234 .map(|(p, z, t, dp)| (p.unpack(), z.unpack(), t.unpack(), dp.unpack()))
236 .map(|(p, z, t, dp)| (p, z - elevation, t, dp))
238 .map(|(p, h, t, dp)| (p, h, metfor::potential_temperature(p, t), dp))
240 .filter_map(|(p, h, theta, dp)| metfor::specific_humidity(dp, p).map(|q| (p, h, theta, q)))
242 .tuple_windows::<(_, _)>()
244 .scan(
246 (0.0, 0.0),
247 |state, ((_p0, h0, theta0, q0), (p1, h1, theta1, q1))| {
248 let (sum_theta, sum_q): (&mut f64, &mut f64) = (&mut state.0, &mut state.1);
249
250 let dh = (h1 - h0).unpack(); debug_assert!(dh > 0.0);
252
253 *sum_theta += (theta0.unpack() * h0.unpack() + theta1.unpack() * h1.unpack()) * dh;
254 *sum_q += (q0 * h0.unpack() + q1 * h1.unpack()) * dh;
255
256 let h_sq = h1.unpack() * h1.unpack();
258 let avg_theta = Kelvin(*sum_theta / h_sq);
259 let avg_q = *sum_q / h_sq;
260
261 Some((p1, h1, avg_theta, avg_q))
262 },
263 )
264 .filter(|(p1, _h1, _avg_theta, _avg_q)| *p1 <= max_p)
266 .filter_map(|(p1, h1, avg_theta, avg_q)| {
268 let temperature = Celsius::from(metfor::temperature_from_pot_temp(avg_theta, bottom_p));
269 let dew_point = metfor::dew_point_from_p_and_specific_humidity(bottom_p, avg_q)?;
270
271 let pcl = Parcel {
272 temperature,
273 dew_point,
274 pressure: bottom_p,
275 };
276
277 Some((p1, h1, avg_theta, avg_q, pcl))
278 })
279 .filter_map(|(p1, h1, avg_theta, avg_q, pcl)| {
281 metfor::pressure_at_lcl(pcl.temperature, pcl.dew_point, pcl.pressure)
282 .map(|lcl_pres| (p1, h1, avg_theta, avg_q, lcl_pres))
283 })
284 .tuple_windows::<(_, _)>()
286 .find(
288 |(
289 (p0, _h0, _avg_theta0, _avg_q0, lcl_pres0),
290 (p1, _h1, _avg_theta1, _avg_q1, lcl_pres1),
291 )| p0 >= lcl_pres0 && p1 < lcl_pres1,
292 ) {
293 Some(((p0, h0, avg_theta0, avg_q0, _), _)) => Ok((avg_theta0, avg_q0, h0, bottom_p, p0)),
294 None => Err(AnalysisError::FailedPrerequisite),
295 }
296}
297
298fn free_convection_level(
307 snd: &Sounding,
308 moisture_ratio: f64,
309 theta_ml: Kelvin,
310 q_ml: f64,
311) -> Result<(
312 Meters,
313 HectoPascal,
314 Kelvin,
315 KelvinDiff,
316 Kelvin,
317 Vec<(HectoPascal, Celsius)>,
318)> {
319 const SP_BETA_MAX: f64 = 0.25;
320 const DELTA_BETA: f64 = 0.001;
321
322 let apply_beta = move |beta| {
323 let theta_sp = Kelvin((1.0 + beta) * theta_ml.unpack());
324 let q_sp = q_ml + beta / moisture_ratio / 1000.0 * theta_ml.unpack();
325
326 (theta_sp, q_sp)
327 };
328
329 let mut sp_curve: Vec<(HectoPascal, Celsius)> = Vec::with_capacity(100);
330
331 let mut low_beta: f64 = f64::NAN;
332 let mut high_beta: f64 = f64::NAN;
333 let mut beta = 0.0;
334 let mut i = 0;
335 while beta <= SP_BETA_MAX {
336 let (theta_sp, q_sp) = apply_beta(beta);
337
338 let skew_t_coords =
339 potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)?;
340
341 sp_curve.push(skew_t_coords);
342
343 let (p, t) = skew_t_coords;
344
345 let sp_theta_e = match metfor::equiv_pot_temperature(t, t, p) {
346 Some(val) => val,
347 None => continue,
348 };
349
350 let meets_theta_e_requirements = is_free_convecting(snd, p, sp_theta_e)?;
351
352 if !meets_theta_e_requirements && high_beta.is_nan() {
353 low_beta = beta;
354 } else if meets_theta_e_requirements && high_beta.is_nan() {
355 high_beta = beta;
356 }
357
358 if !high_beta.is_nan() && i >= 100 {
359 break;
360 }
361
362 beta += DELTA_BETA;
363 i += 1;
364 }
365
366 if high_beta.is_nan() || low_beta.is_nan() {
367 return Err(AnalysisError::FailedPrerequisite);
368 }
369
370 let beta = metfor::find_root(
371 &|b| {
372 let (theta_sp, q_sp) = apply_beta(b);
373 let (p, t) =
374 potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)
375 .ok()?;
376 let sp_theta_e = metfor::equiv_pot_temperature(t, t, p)?;
377
378 Some(
379 (min_temperature_diff_to_max_cloud_top_temperature(snd, p, sp_theta_e).ok()?
380 - TEMPERATURE_BUFFER)
381 .unpack(),
382 )
383 },
384 low_beta,
385 high_beta,
386 )
387 .ok_or(AnalysisError::FailedPrerequisite)?;
388
389 let (theta_sp, q_sp) = apply_beta(beta);
390 let (p_lfc, t_lfc) =
391 potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)?;
392 let theta_e_lfc = metfor::equiv_pot_temperature(t_lfc, t_lfc, p_lfc)
393 .ok_or(AnalysisError::FailedPrerequisite)?;
394
395 let dtheta = theta_sp - theta_ml;
396
397 let heights = snd.height_profile();
398 let pressures = snd.pressure_profile();
399 let height_asl_lfc = crate::interpolation::linear_interpolate(pressures, heights, p_lfc)
400 .into_option()
401 .ok_or(AnalysisError::InterpolationError)?;
402 let sfc_height = heights
403 .iter()
404 .filter_map(|h| h.into_option())
405 .next()
406 .ok_or(AnalysisError::NotEnoughData)?;
407
408 Ok((
409 height_asl_lfc - sfc_height,
410 p_lfc,
411 theta_sp,
412 dtheta,
413 theta_e_lfc,
414 sp_curve,
415 ))
416}
417
418fn potential_t_and_specific_humidity_to_pressure_and_temperature(
419 theta: Kelvin,
420 specific_humidity: f64,
421) -> Result<(HectoPascal, Celsius)> {
422 let diff = |p_hpa: f64| -> Option<f64> {
423 let t = metfor::temperature_from_pot_temp(theta, HectoPascal(p_hpa));
424 let dp =
425 metfor::dew_point_from_p_and_specific_humidity(HectoPascal(p_hpa), specific_humidity)?;
426
427 Some((t - dp).unpack())
428 };
429
430 let p = metfor::find_root(
431 &diff,
432 HectoPascal(1080.0).unpack(),
433 HectoPascal(100.0).unpack(),
434 )
435 .map(HectoPascal)
436 .ok_or(AnalysisError::FailedPrerequisite)?;
437
438 let t = Celsius::from(metfor::temperature_from_pot_temp(theta, p));
439
440 Ok((p, t))
441}
442
443fn min_temperature_diff_to_max_cloud_top_temperature(
445 snd: &Sounding,
446 starting_pressure: HectoPascal,
447 starting_theta_e: Kelvin,
448) -> Result<CelsiusDiff> {
449 const MAX_PLUME_TOP_T: Celsius = Celsius(-20.0);
450
451 let pp = snd.pressure_profile();
452 let tp = snd.temperature_profile();
453 let dpp = snd.dew_point_profile();
454
455 let min_diff: CelsiusDiff = izip!(pp, tp, dpp)
456 .filter(|(p, t, dp)| p.is_some() && t.is_some() && dp.is_some())
458 .map(|(p, t, dp)| (p.unpack(), t.unpack(), dp.unpack()))
460 .skip_while(|(p, _t, _dp)| *p > starting_pressure)
462 .filter_map(|(p, t, dp)| {
464 metfor::temperature_from_equiv_pot_temp_saturated_and_pressure(p, starting_theta_e)
465 .map(|pcl_t| (p, t, dp, pcl_t))
466 })
467 .enumerate()
469 .take_while(|(i, (_p, _t, _dp, pcl_t))| *i < 2 || *pcl_t >= MAX_PLUME_TOP_T)
470 .map(|(_i, row)| row)
472 .filter_map(|(p, t, dp, pcl_t)| metfor::virtual_temperature(t, dp, p).map(|vt| (p, vt, pcl_t)))
474 .filter_map(|(p, vt, pcl_t)| metfor::virtual_temperature(pcl_t, pcl_t, p).map(|pcl_vt| (vt, pcl_vt)))
476 .fold(CelsiusDiff(500.0), |min_diff, (vt, pcl_vt)| {
478 CelsiusDiff::from(pcl_vt - vt).min(min_diff)
479 });
480
481 if min_diff == CelsiusDiff(500.0) {
482 Err(AnalysisError::FailedPrerequisite)
483 } else {
484 Ok(min_diff)
485 }
486}
487
488const TEMPERATURE_BUFFER: CelsiusDiff = CelsiusDiff(0.5);
491fn is_free_convecting(
492 snd: &Sounding,
493 starting_pressure: HectoPascal,
494 starting_theta_e: Kelvin,
495) -> Result<bool> {
496 Ok(
497 min_temperature_diff_to_max_cloud_top_temperature(
498 snd,
499 starting_pressure,
500 starting_theta_e,
501 )? >= TEMPERATURE_BUFFER,
502 )
503}
504
505fn entrained_layer_mean_wind_speed(zfc: Meters, snd: &Sounding) -> Result<MetersPSec> {
509 let layer = crate::layer_agl(snd, zfc)?;
510
511 let mean_wind = crate::wind::mean_wind(&layer, snd)?;
512 let mean_wind = WindSpdDir::from(mean_wind).speed;
513
514 Ok(mean_wind)
515}