use crate::{
error::{AnalysisError, Result},
parcel::Parcel,
sounding::Sounding,
};
use itertools::{izip, Itertools};
use metfor::{
Celsius, CelsiusDiff, GigaWatts, HectoPascal, Kelvin, KelvinDiff, Meters, MetersPSec, Quantity,
WindSpdDir,
};
#[inline]
pub fn hot_dry_windy(snd: &Sounding) -> Result<f64> {
let elevation = if let Some(sfc_h) = snd.station_info().elevation().into_option() {
sfc_h
} else if let Some(lowest_h) = snd
.height_profile()
.iter()
.filter_map(|optd| optd.into_option())
.next()
{
lowest_h
} else {
return Err(AnalysisError::NotEnoughData);
};
let sfc_pressure: HectoPascal = snd
.pressure_profile()
.iter()
.filter_map(|p| p.into_option())
.next()
.ok_or(AnalysisError::NotEnoughData)?;
let p_profile = snd.pressure_profile();
let h_profile = snd.height_profile();
let t_profile = snd.temperature_profile();
let dp_profile = snd.dew_point_profile();
let ws_profile = snd.wind_profile();
let (vpd, ws) = izip!(p_profile, h_profile, t_profile, dp_profile, ws_profile)
.filter(|(p, h, t, dp, ws)| {
p.is_some() && h.is_some() && t.is_some() && dp.is_some() && ws.is_some()
})
.map(|(p, h, t, dp, ws)| {
(
p.unpack(),
h.unpack(),
t.unpack(),
dp.unpack(),
ws.unpack().speed,
)
})
.take_while(|(_, h, _, _, _)| *h <= elevation + Meters(500.0))
.map(|(p, _, t, dp, ws)| {
(
p,
metfor::temperature_from_pot_temp(
metfor::potential_temperature(p, t),
sfc_pressure,
),
dp,
ws,
)
})
.filter_map(|(p, t, dp, ws)| {
metfor::specific_humidity(dp, p)
.and_then(|q| metfor::dew_point_from_p_and_specific_humidity(sfc_pressure, q))
.map(|dp| (t, dp, ws))
})
.filter_map(|(t, dp, ws)| {
metfor::vapor_pressure_water(t)
.and_then(|sat_vap| metfor::vapor_pressure_water(dp).map(|vap| (sat_vap - vap, ws)))
})
.map(|(vpd, ws)| (vpd.unpack(), MetersPSec::from(ws).unpack()))
.fold((0.0, 0.0), |(vpd_max, ws_max), (vpd, ws)| {
(vpd.max(vpd_max), ws.max(ws_max))
});
Ok(vpd * ws)
}
#[derive(Clone, Debug)]
pub struct PFTAnalysis {
pub pft: GigaWatts,
pub theta_ml: Kelvin,
pub q_ml: f64,
pub u_ml: MetersPSec,
pub p_top_ml: HectoPascal,
pub p_fc: HectoPascal,
pub z_fc: Meters,
pub d_theta: KelvinDiff,
pub sp_curve: Vec<(HectoPascal, Celsius)>,
pub theta_e_fc: Kelvin,
pub theta_curve: Vec<(HectoPascal, Celsius)>,
}
pub fn pft_analysis(snd: &Sounding, moisture_ratio: f64) -> Result<PFTAnalysis> {
let (theta_ml, q_ml, _z_ml, p_sfc, p_top_ml) = entrained_mixed_layer(snd)?;
let (z_fc, p_fc, theta_fc, d_theta_fc, theta_e_fc, sp_curve, theta_curve) =
free_convection_level(snd, moisture_ratio, theta_ml, q_ml)?;
let u_ml: MetersPSec = entrained_layer_mean_wind_speed(z_fc, snd)?;
let pft = metfor::pft(z_fc, p_fc, u_ml, d_theta_fc, theta_fc, p_sfc);
Ok(PFTAnalysis {
pft,
theta_ml,
q_ml,
u_ml,
p_top_ml,
p_fc,
z_fc,
d_theta: d_theta_fc,
sp_curve,
theta_e_fc,
theta_curve,
})
}
pub fn pft(snd: &Sounding, moisture_ratio: f64) -> Result<GigaWatts> {
let (theta_ml, q_ml, _z_ml, p_sfc, _p_top_ml) = entrained_mixed_layer(snd)?;
let (z_fc, p_fc, theta_fc, d_theta_fc, _theta_e, _sp_curve, _theta_curve) =
free_convection_level(snd, moisture_ratio, theta_ml, q_ml)?;
let u_ml: MetersPSec = entrained_layer_mean_wind_speed(z_fc, snd)?;
Ok(metfor::pft(z_fc, p_fc, u_ml, d_theta_fc, theta_fc, p_sfc))
}
pub(crate) fn entrained_mixed_layer(
snd: &Sounding,
) -> Result<(Kelvin, f64, Meters, HectoPascal, HectoPascal)> {
let elevation: Meters = snd
.station_info()
.elevation()
.ok_or(AnalysisError::NotEnoughData)?;
let mut ps = snd.pressure_profile();
let mut zs = snd.height_profile();
let mut ts = snd.temperature_profile();
let mut dps = snd.dew_point_profile();
if zs.len() >= 2 && zs[0] == zs[1] {
ps = &ps[1..];
zs = &zs[1..];
ts = &ts[1..];
dps = &dps[1..];
}
let bottom_p = ps
.iter()
.filter_map(|&p| p.into_option())
.next()
.ok_or(AnalysisError::NotEnoughData)?;
let max_p = bottom_p - HectoPascal(50.0);
match itertools::izip!(ps, zs, ts, dps)
.filter(|(p, z, t, dp)| p.is_some() && z.is_some() && t.is_some() && dp.is_some())
.map(|(p, z, t, dp)| (p.unpack(), z.unpack(), t.unpack(), dp.unpack()))
.map(|(p, z, t, dp)| (p, z - elevation, t, dp))
.map(|(p, h, t, dp)| (p, h, metfor::potential_temperature(p, t), dp))
.filter_map(|(p, h, theta, dp)| metfor::specific_humidity(dp, p).map(|q| (p, h, theta, q)))
.tuple_windows::<(_, _)>()
.scan(
(0.0, 0.0),
|state, ((_p0, h0, theta0, q0), (p1, h1, theta1, q1))| {
let (sum_theta, sum_q): (&mut f64, &mut f64) = (&mut state.0, &mut state.1);
let dh = (h1 - h0).unpack(); debug_assert!(dh > 0.0);
*sum_theta += (theta0.unpack() * h0.unpack() + theta1.unpack() * h1.unpack()) * dh;
*sum_q += (q0 * h0.unpack() + q1 * h1.unpack()) * dh;
let h_sq = h1.unpack() * h1.unpack();
let avg_theta = Kelvin(*sum_theta / h_sq);
let avg_q = *sum_q / h_sq;
Some((p1, h1, avg_theta, avg_q))
},
)
.filter(|(p1, _h1, _avg_theta, _avg_q)| *p1 <= max_p)
.filter_map(|(p1, h1, avg_theta, avg_q)| {
let temperature = Celsius::from(metfor::temperature_from_pot_temp(avg_theta, bottom_p));
let dew_point = metfor::dew_point_from_p_and_specific_humidity(bottom_p, avg_q)?;
let pcl = Parcel {
temperature,
dew_point,
pressure: bottom_p,
};
Some((p1, h1, avg_theta, avg_q, pcl))
})
.filter_map(|(p1, h1, avg_theta, avg_q, pcl)| {
metfor::pressure_at_lcl(pcl.temperature, pcl.dew_point, pcl.pressure)
.map(|lcl_pres| (p1, h1, avg_theta, avg_q, lcl_pres))
})
.tuple_windows::<(_, _)>()
.find(
|(
(p0, _h0, _avg_theta0, _avg_q0, lcl_pres0),
(p1, _h1, _avg_theta1, _avg_q1, lcl_pres1),
)| p0 >= lcl_pres0 && p1 < lcl_pres1,
) {
Some(((p0, h0, avg_theta0, avg_q0, _), _)) => Ok((avg_theta0, avg_q0, h0, bottom_p, p0)),
None => Err(AnalysisError::FailedPrerequisite),
}
}
fn free_convection_level(
snd: &Sounding,
moisture_ratio: f64,
theta_ml: Kelvin,
q_ml: f64,
) -> Result<(
Meters,
HectoPascal,
Kelvin,
KelvinDiff,
Kelvin,
Vec<(HectoPascal, Celsius)>,
Vec<(HectoPascal, Celsius)>,
)> {
const SP_BETA_MAX: f64 = 0.20;
const DELTA_BETA: f64 = 0.001;
let apply_beta = move |beta| {
let theta_sp = Kelvin((1.0 + beta) * theta_ml.unpack());
let q_sp = q_ml + beta / moisture_ratio / 1000.0 * theta_ml.unpack();
(theta_sp, q_sp)
};
let mut sp_curve: Vec<(HectoPascal, Celsius)> = Vec::with_capacity(100);
let mut low_beta: f64 = f64::NAN;
let mut high_beta: f64 = f64::NAN;
let mut beta = 0.0;
let mut i = 0;
while beta <= SP_BETA_MAX {
let (theta_sp, q_sp) = apply_beta(beta);
let skew_t_coords =
potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)?;
sp_curve.push(skew_t_coords);
let (p, t) = skew_t_coords;
let sp_theta_e = match metfor::equiv_pot_temperature(t, t, p) {
Some(val) => val,
None => continue,
};
let meets_theta_e_requirements = is_free_convecting(snd, p, sp_theta_e)?;
if !meets_theta_e_requirements && high_beta.is_nan() {
low_beta = beta;
} else if meets_theta_e_requirements && high_beta.is_nan() {
high_beta = beta;
}
if !high_beta.is_nan() && i >= 100 {
break;
}
beta += DELTA_BETA;
i += 1;
}
if high_beta.is_nan() || low_beta.is_nan() {
return Err(AnalysisError::FailedPrerequisite);
}
let beta = metfor::find_root(
&|b| {
let (theta_sp, q_sp) = apply_beta(b);
let (p, t) =
potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)
.ok()?;
let sp_theta_e = metfor::equiv_pot_temperature(t, t, p)?;
Some(
(min_temperature_diff_to_max_cloud_top_temperature(snd, p, sp_theta_e).ok()?
- TEMPERATURE_BUFFER)
.unpack(),
)
},
low_beta,
high_beta,
)
.ok_or(AnalysisError::FailedPrerequisite)?;
let (theta_sp, q_sp) = apply_beta(beta);
let (p_lfc, t_lfc) =
potential_t_and_specific_humidity_to_pressure_and_temperature(theta_sp, q_sp)?;
let theta_e_lfc = metfor::equiv_pot_temperature(t_lfc, t_lfc, p_lfc)
.ok_or(AnalysisError::FailedPrerequisite)?;
let dtheta = theta_sp - theta_ml;
let heights = snd.height_profile();
let pressures = snd.pressure_profile();
let height_asl_lfc = crate::interpolation::linear_interpolate(pressures, heights, p_lfc)
.into_option()
.ok_or(AnalysisError::InterpolationError)?;
let sfc_height = heights
.iter()
.filter_map(|h| h.into_option())
.next()
.ok_or(AnalysisError::NotEnoughData)?;
let mut theta_curve: Vec<(HectoPascal, Celsius)> = pressures
.iter()
.filter_map(|p| {
p.into_option()
.map(|p| (p, metfor::temperature_from_pot_temp(theta_sp, p)))
})
.take_while(|&(p, _)| p > p_lfc)
.map(|(p, t)| (p, Celsius::from(t)))
.collect();
theta_curve.push((p_lfc, t_lfc));
Ok((
height_asl_lfc - sfc_height,
p_lfc,
theta_sp,
dtheta,
theta_e_lfc,
sp_curve,
theta_curve,
))
}
pub(crate) fn potential_t_and_specific_humidity_to_pressure_and_temperature(
theta: Kelvin,
specific_humidity: f64,
) -> Result<(HectoPascal, Celsius)> {
let diff = |p_hpa: f64| -> Option<f64> {
let t = metfor::temperature_from_pot_temp(theta, HectoPascal(p_hpa));
let dp =
metfor::dew_point_from_p_and_specific_humidity(HectoPascal(p_hpa), specific_humidity)?;
Some((t - dp).unpack())
};
let p = metfor::find_root(
&diff,
HectoPascal(1080.0).unpack(),
HectoPascal(100.0).unpack(),
)
.map(HectoPascal)
.ok_or(AnalysisError::FailedPrerequisite)?;
let t = Celsius::from(metfor::temperature_from_pot_temp(theta, p));
Ok((p, t))
}
fn min_temperature_diff_to_max_cloud_top_temperature(
snd: &Sounding,
starting_pressure: HectoPascal,
starting_theta_e: Kelvin,
) -> Result<CelsiusDiff> {
const MAX_PLUME_TOP_T: Celsius = Celsius(-20.0);
let pp = snd.pressure_profile();
let tp = snd.temperature_profile();
let dpp = snd.dew_point_profile();
let min_diff: CelsiusDiff = izip!(pp, tp, dpp)
.filter(|(p, t, dp)| p.is_some() && t.is_some() && dp.is_some())
.map(|(p, t, dp)| (p.unpack(), t.unpack(), dp.unpack()))
.skip_while(|(p, _t, _dp)| *p > starting_pressure)
.filter_map(|(p, t, dp)| {
metfor::temperature_from_equiv_pot_temp_saturated_and_pressure(p, starting_theta_e)
.map(|pcl_t| (p, t, dp, pcl_t))
})
.enumerate()
.take_while(|(i, (_p, _t, _dp, pcl_t))| *i < 2 || *pcl_t >= MAX_PLUME_TOP_T)
.map(|(_i, row)| row)
.filter_map(|(p, t, dp, pcl_t)| {
metfor::virtual_temperature(t, dp, p).map(|vt| (p, vt, pcl_t))
})
.filter_map(|(p, vt, pcl_t)| {
metfor::virtual_temperature(pcl_t, pcl_t, p).map(|pcl_vt| (vt, pcl_vt))
})
.fold(CelsiusDiff(500.0), |min_diff, (vt, pcl_vt)| {
CelsiusDiff::from(pcl_vt - vt).min(min_diff)
});
if min_diff == CelsiusDiff(500.0) {
Err(AnalysisError::FailedPrerequisite)
} else {
Ok(min_diff)
}
}
const TEMPERATURE_BUFFER: CelsiusDiff = CelsiusDiff(0.5);
fn is_free_convecting(
snd: &Sounding,
starting_pressure: HectoPascal,
starting_theta_e: Kelvin,
) -> Result<bool> {
Ok(
min_temperature_diff_to_max_cloud_top_temperature(
snd,
starting_pressure,
starting_theta_e,
)? >= TEMPERATURE_BUFFER,
)
}
pub(crate) fn entrained_layer_mean_wind_speed(zfc: Meters, snd: &Sounding) -> Result<MetersPSec> {
let layer = crate::layer_agl(snd, zfc)?;
let mean_wind = crate::wind::mean_wind(&layer, snd)?;
let mean_wind = WindSpdDir::from(mean_wind).speed;
Ok(mean_wind)
}