use crate::{
error::{AnalysisError, Result},
parcel::{mixed_layer_parcel, Parcel},
parcel_profile::{
find_parcel_start_data,
lift::{
create_level_type_mapping, create_parcel_calc_t, parcel_lcl, AnalLevel, AnalLevelType,
},
ParcelAscentAnalysis, ParcelProfile,
},
sounding::Sounding,
};
use itertools::{izip, Itertools};
use metfor::{Celsius, CelsiusDiff, JpKg, Kelvin, Meters, Quantity};
#[derive(Debug, Clone, Copy)]
pub struct PlumeAscentAnalysis {
pub parcel: Parcel,
pub net_cape: Option<JpKg>,
pub lcl_height: Option<Meters>,
pub el_height: Option<Meters>,
pub max_height: Option<Meters>,
}
#[derive(Debug)]
pub struct BlowUpAnalysis {
pub starting_parcel: Parcel,
pub delta_t: CelsiusDiff,
pub height: Meters,
}
pub fn calc_plumes(
snd: &Sounding,
increment: CelsiusDiff,
max_range: CelsiusDiff,
) -> Result<Vec<PlumeAscentAnalysis>> {
let (_starting_parcel, parcels_iter) = plume_parcels(snd, max_range, increment)?;
Ok(parcels_iter
.filter_map(|(_, pcl)| analyze_plume_parcel(pcl, snd).ok())
.skip_while(|anal| anal.el_height.is_none())
.take_while(|anal| anal.el_height.is_some())
.collect())
}
pub fn blow_up(snd: &Sounding) -> Result<BlowUpAnalysis> {
const INCREMENT: CelsiusDiff = CelsiusDiff(0.1);
const MAX_RANGE: CelsiusDiff = CelsiusDiff(20.0);
let (starting_parcel, parcel_iter) = plume_parcels(snd, MAX_RANGE, INCREMENT)?;
let (delta_t, _max_deriv, height): (CelsiusDiff, f64, Meters) = parcel_iter
.filter_map(|(dt, pcl)| analyze_plume_parcel(pcl, snd).ok().map(|anal| (dt, anal)))
.skip_while(|(_dt, anal)| anal.el_height.is_none())
.take_while(|(_dt, anal)| anal.el_height.is_some())
.filter_map(|(dt, anal)| anal.el_height.map(|h| (dt, h)))
.scan(Meters(0.0), |prev, (dt, el)| {
if el >= *prev {
*prev = el;
Some(Some((dt, el)))
} else {
Some(None)
}
})
.filter_map(|opt| opt)
.tuple_windows::<(_, _)>()
.fold((CelsiusDiff(0.0), 0.0, Meters(0.0)), |acc, (lvl0, lvl1)| {
let (blow_up_dt, deriv, jump) = acc;
let (dt0, el0) = lvl0;
let (dt1, el1) = lvl1;
debug_assert_ne!(dt0, dt1);
let derivative = (el1 - el0).unpack() / (dt1 - dt0).unpack();
let diff = el1 - el0;
if derivative > deriv {
let actual_dt = CelsiusDiff((dt1 + dt0).unpack() / 2.0);
(actual_dt, derivative, diff)
} else {
(blow_up_dt, deriv, jump)
}
});
Ok(BlowUpAnalysis {
starting_parcel,
delta_t,
height,
})
}
pub fn analyze_plume_parcel(parcel: Parcel, snd: &Sounding) -> Result<PlumeAscentAnalysis> {
let (parcel, lift_iter) = lift_parcel(parcel, snd)?;
let (lcl_height, el_height, max_height, net_bouyancy) = lift_iter
.fold(
(None, None, Meters(0.0), 0.0f64),
|acc, (int_bouyancy, anal_level_type)| {
use crate::parcel_profile::lift::AnalLevelType::*;
let (lcl, el, _, max_bouyancy) = acc;
match anal_level_type {
Normal(level) | LFC(level) => {
let max_hgt = level.height;
let max_bouyancy = max_bouyancy.max(int_bouyancy);
(lcl, el, max_hgt, max_bouyancy)
}
LCL(level) => {
let lcl = Some(level.height);
let max_hgt = level.height;
let max_bouyancy = max_bouyancy.max(int_bouyancy);
(lcl, el, max_hgt, max_bouyancy)
}
EL(level) => {
let el = Some(level.height);
let max_hgt = level.height;
let max_bouyancy = max_bouyancy.max(int_bouyancy);
(lcl, el, max_hgt, max_bouyancy)
}
}
},
);
let (net_cape, max_height) = if el_height.is_some() {
(
Some(JpKg(net_bouyancy / 2.0 * -metfor::g)),
Some(max_height),
)
} else {
(None, None)
};
Ok(PlumeAscentAnalysis {
lcl_height,
el_height,
max_height,
net_cape,
parcel,
})
}
pub fn lift_plume_parcel(
parcel: Parcel,
snd: &Sounding,
) -> Result<(ParcelProfile, PlumeAscentAnalysis)> {
let (parcel, lift_iter) = lift_parcel(parcel, snd)?;
let len = snd.pressure_profile().len();
let mut pressure = Vec::with_capacity(len);
let mut height = Vec::with_capacity(len);
let mut parcel_t = Vec::with_capacity(len);
let mut environment_t = Vec::with_capacity(len);
let (lcl_height, el_height, max_height, net_bouyancy) = lift_iter
.scan((), |_dummy, (int_bouyancy, anal_level_type)| {
use crate::parcel_profile::lift::AnalLevelType::*;
match anal_level_type {
Normal(lvl) | LFC(lvl) | LCL(lvl) | EL(lvl) => {
let AnalLevel {
pressure: p,
height: h,
pcl_virt_t,
env_virt_t,
} = lvl;
pressure.push(p);
height.push(h);
parcel_t.push(pcl_virt_t);
environment_t.push(env_virt_t);
}
}
Some((int_bouyancy, anal_level_type))
})
.fold(
(None, None, Meters(0.0), 0.0f64),
|acc, (int_bouyancy, anal_level_type)| {
use crate::parcel_profile::lift::AnalLevelType::*;
let (lcl, el, _, max_bouyancy) = acc;
match anal_level_type {
Normal(level) | LFC(level) => {
let max_hgt = level.height;
let max_bouyancy = max_bouyancy.max(int_bouyancy);
(lcl, el, max_hgt, max_bouyancy)
}
LCL(level) => {
let lcl = Some(level.height);
let max_hgt = level.height;
let max_bouyancy = max_bouyancy.max(int_bouyancy);
(lcl, el, max_hgt, max_bouyancy)
}
EL(level) => {
let el = Some(level.height);
let max_hgt = level.height;
let max_bouyancy = max_bouyancy.max(int_bouyancy);
(lcl, el, max_hgt, max_bouyancy)
}
}
},
);
let (net_cape, max_height) = if el_height.is_some() {
(
Some(JpKg(net_bouyancy / 2.0 * -metfor::g)),
Some(max_height),
)
} else {
(None, None)
};
Ok((
ParcelProfile {
pressure,
height,
parcel_t,
environment_t,
},
PlumeAscentAnalysis {
lcl_height,
el_height,
max_height,
net_cape,
parcel,
},
))
}
fn lift_parcel<'a>(
parcel: Parcel,
snd: &'a Sounding,
) -> Result<(Parcel, impl Iterator<Item = (f64, AnalLevelType)> + 'a)> {
let (pcl_lcl, _lcl_temperature) = parcel_lcl(&parcel, snd)?;
let (_parcel_start_data, parcel) = find_parcel_start_data(snd, &parcel)?;
let parcel_calc_t = create_parcel_calc_t(parcel, pcl_lcl)?;
let level_type_mapping = create_level_type_mapping(pcl_lcl);
let snd_pressure = snd.pressure_profile();
let hgt = snd.height_profile();
let env_t = snd.temperature_profile();
let env_dp = snd.dew_point_profile();
let p0 = parcel.pressure;
let iter = izip!(snd_pressure, hgt, env_t, env_dp)
.filter(|(p, h, t, dp)| p.is_some() && h.is_some() && t.is_some() && dp.is_some())
.map(|(p, h, t, dp)| (p.unpack(), h.unpack(), t.unpack(), dp.unpack()))
.filter(move |(p, _, _, _)| *p <= p0)
.filter_map(move |(p, h, env_t, env_dp)| {
parcel_calc_t(p).map(|pcl_virt_t| (p, h, env_t, env_dp, pcl_virt_t))
})
.filter_map(|(p, h, env_t, env_dp, pcl_virt_t)| {
metfor::virtual_temperature(env_t, env_dp, p)
.map(|env_vt| (p, h, Celsius::from(env_vt), pcl_virt_t))
})
.map(|(pressure, height, env_virt_t, pcl_virt_t)| AnalLevel {
pressure,
height,
pcl_virt_t,
env_virt_t,
})
.tuple_windows::<(_, _)>()
.flat_map(move |(lvl0, lvl1)| level_type_mapping(lvl0, lvl1))
.tuple_windows::<(_, _)>()
.scan(
(0.0, 0usize),
|(int_bouyancy, count), (anal_level_type0, anal_level_type1)| {
use crate::parcel_profile::lift::AnalLevelType::*;
let level_data0: &AnalLevel = match &anal_level_type0 {
Normal(data) | LFC(data) | LCL(data) | EL(data) => data,
};
let level_data1: &AnalLevel = match &anal_level_type1 {
Normal(data) | LFC(data) | LCL(data) | EL(data) => data,
};
let &AnalLevel {
height: h0,
pcl_virt_t: pcl0,
env_virt_t: env0,
..
} = level_data0;
let &AnalLevel {
height: h1,
pcl_virt_t: pcl1,
env_virt_t: env1,
..
} = level_data1;
let Meters(dz) = h1 - h0;
debug_assert!(dz >= 0.0);
let b0 = (pcl0 - env0).unpack() / Kelvin::from(env0).unpack();
let b1 = (pcl1 - env1).unpack() / Kelvin::from(env1).unpack();
let bouyancy = (b0 + b1) * dz;
*int_bouyancy += bouyancy;
*count += 1;
Some(((*int_bouyancy, *count), anal_level_type1))
},
)
.take_while(|((int_bouyancy, count), _)| *int_bouyancy >= 0.0 || *count < 5)
.map(|((int_bouyancy, _), anal_level)| (int_bouyancy, anal_level));
Ok((parcel, iter))
}
fn plume_parcels(
snd: &Sounding,
plus_range: CelsiusDiff,
increment: CelsiusDiff,
) -> Result<(Parcel, impl Iterator<Item = (CelsiusDiff, Parcel)>)> {
let parcel = mixed_layer_parcel(snd)?;
let (row, mut parcel) = find_parcel_start_data(snd, &parcel)?;
if row.temperature.unwrap() > parcel.temperature {
parcel.temperature = row.temperature.unwrap();
}
let CelsiusDiff(inc_val) = increment;
let next_dt = CelsiusDiff(-inc_val);
let next_p = Parcel {
temperature: parcel.temperature + next_dt,
..parcel
};
let max_t = parcel.temperature + plus_range;
Ok((
parcel,
PlumeParcelIterator {
next_p,
next_dt,
max_t,
increment,
},
))
}
struct PlumeParcelIterator {
next_p: Parcel,
next_dt: CelsiusDiff,
max_t: Celsius,
increment: CelsiusDiff,
}
impl Iterator for PlumeParcelIterator {
type Item = (CelsiusDiff, Parcel);
fn next(&mut self) -> Option<Self::Item> {
let next_t = self.next_p.temperature + self.increment;
self.next_dt += self.increment;
if next_t > self.max_t {
None
} else {
self.next_p = Parcel {
temperature: next_t,
..self.next_p
};
Some((self.next_dt, self.next_p))
}
}
}
pub fn partition_cape(pa: &ParcelAscentAnalysis) -> Result<(JpKg, JpKg)> {
let lcl = pa.lcl_pressure().ok_or(AnalysisError::MissingValue)?;
let el = pa.el_pressure().ok_or(AnalysisError::MissingValue)?;
let parcel_theta = pa.parcel().theta();
let profile = pa.profile();
let lower_dry_profile = izip!(
&profile.pressure,
&profile.height,
&profile.parcel_t,
&profile.environment_t
)
.take_while(|(p, _, _, _)| **p >= lcl)
.map(|(_, h, pt, et)| (*h, Kelvin::from(*pt), Kelvin::from(*et)));
let upper_dry_profile = izip!(&profile.pressure, &profile.height, &profile.environment_t)
.skip_while(|(p, _, _)| **p >= lcl)
.filter_map(|(p, h, et)| {
let t_k = metfor::temperature_from_theta(parcel_theta, *p);
metfor::virtual_temperature(t_k, t_k, *p).map(|pt_k| (*p, *h, pt_k, *et))
})
.take_while(|(_, _, pt, et)| pt >= et)
.map(|(_, h, pt, et)| (h, pt, Kelvin::from(et)));
let dry_profile = lower_dry_profile.chain(upper_dry_profile);
let full_profile = izip!(
&profile.pressure,
&profile.height,
&profile.parcel_t,
&profile.environment_t
)
.take_while(|(p, _, _, _)| **p >= el)
.map(|(_, h, pt, et)| (*h, Kelvin::from(*pt), Kelvin::from(*et)));
fn calc_cape<T: Iterator<Item = (Meters, Kelvin, Kelvin)>>(iter: T) -> f64 {
let cape = iter
.fold(
(0.0, Meters(std::f64::MAX), Kelvin(0.0), Kelvin(0.0)),
|acc, (h, pt, et)| {
let (mut cape, prev_h, prev_pt, prev_et) = acc;
let dz = h - prev_h;
if dz <= Meters(0.0) {
(cape, h, pt, et)
} else {
let bouyancy = ((pt - et).unpack() / et.unpack()
+ (prev_pt - prev_et).unpack() / prev_et.unpack())
* dz.unpack();
cape += bouyancy;
(cape, h, pt, et)
}
},
)
.0;
cape / 2.0 * -metfor::g
}
let total_cape = calc_cape(full_profile).max(0.0);
let dry_cape = calc_cape(dry_profile).max(0.0).min(total_cape);
let wet_cape = total_cape - dry_cape;
Ok((JpKg(dry_cape), JpKg(wet_cape)))
}