1use crate::{
4 error::{AnalysisError, Result},
5 interpolation::{linear_interp, linear_interpolate_sounding},
6 layers::{effective_inflow_layer, Layer},
7 profile::equivalent_potential_temperature,
8 sounding::{DataRow, Sounding},
9};
10use itertools::{izip, Itertools};
11use metfor::{self, Celsius, HectoPascal, Kelvin, Quantity};
12use optional::Optioned;
13
14#[derive(Debug, Clone, Copy)]
16pub struct Parcel {
17 pub temperature: Celsius,
19 pub pressure: HectoPascal,
21 pub dew_point: Celsius,
23}
24
25impl Parcel {
26 pub fn theta(&self) -> Kelvin {
28 metfor::potential_temperature(self.pressure, self.temperature)
29 }
30
31 pub fn theta_e(&self) -> Result<Kelvin> {
33 metfor::equiv_pot_temperature(self.temperature, self.dew_point, self.pressure)
34 .ok_or(AnalysisError::MetForError)
35 }
36
37 pub fn mixing_ratio(&self) -> Result<f64> {
39 metfor::mixing_ratio(self.dew_point, self.pressure).ok_or(AnalysisError::MetForError)
40 }
41
42 pub fn virtual_temperature(&self) -> Result<Kelvin> {
44 metfor::virtual_temperature(self.temperature, self.dew_point, self.pressure)
45 .ok_or(AnalysisError::MetForError)
46 }
47
48 pub fn from_datarow(mut dr: DataRow) -> Option<Self> {
50 let temperature = dr.temperature.take()?;
51 let pressure = dr.pressure.take()?;
52 let dew_point = dr.dew_point.take()?;
53
54 Some(Parcel {
55 temperature,
56 pressure,
57 dew_point,
58 })
59 }
60}
61
62pub fn mixed_layer_parcel(snd: &Sounding) -> Result<Parcel> {
68 let press = snd.pressure_profile();
69 let t = snd.temperature_profile();
70 let dp = snd.dew_point_profile();
71
72 if press.is_empty() || t.is_empty() || dp.is_empty() {
73 return Err(AnalysisError::MissingProfile);
74 }
75
76 let bottom_p = press
77 .iter()
78 .filter_map(|&p| p.into_option())
80 .next()
82 .ok_or(AnalysisError::NoDataProfile)?;
84
85 let top_p = bottom_p - HectoPascal(100.0);
86
87 let (sum_p, sum_t, sum_mw) = izip!(press, t, dp)
88 .filter(|(p, t, dp)| p.is_some() && t.is_some() && dp.is_some())
90 .map(|(p, t, dp)| (p.unpack(), t.unpack(), dp.unpack()))
92 .take_while(|&(p, _, _)| p >= top_p)
94 .map(|(p, t, dp)| (p, dp, metfor::potential_temperature(p, t)))
96 .filter_map(|(p, dp, th)| metfor::mixing_ratio(dp, p).map(|mw| (p, th, mw)))
98 .fold((HectoPascal(0.0), 0.0f64, 0.0f64), |acc, (p, theta, mw)| {
100 let (sum_p, sum_t, sum_mw) = acc;
101 (
102 sum_p + p,
103 sum_t + theta.unpack() * p.unpack(),
104 sum_mw + mw * p.unpack(),
105 )
106 });
107
108 if sum_p == HectoPascal(0.0) {
109 return Err(AnalysisError::NotEnoughData);
110 }
111
112 let pressure = bottom_p;
114 let temperature = Celsius::from(metfor::temperature_from_pot_temp(
115 Kelvin(sum_t / sum_p.unpack()),
116 bottom_p,
117 ));
118 let dew_point = metfor::dew_point_from_p_and_mw(bottom_p, sum_mw / sum_p.unpack())
119 .ok_or(AnalysisError::InvalidInput)?;
120
121 Ok(Parcel {
122 temperature,
123 pressure,
124 dew_point,
125 })
126}
127
128pub fn surface_parcel(snd: &Sounding) -> Result<Parcel> {
130 let pressure = snd.station_pressure().ok_or(AnalysisError::MissingValue)?;
131 let temperature = snd.sfc_temperature().ok_or(AnalysisError::MissingValue)?;
132 let dew_point = snd.sfc_dew_point().ok_or(AnalysisError::MissingValue)?;
133
134 Ok(Parcel {
135 temperature,
136 pressure,
137 dew_point,
138 })
139}
140
141pub fn lowest_level_parcel(snd: &Sounding) -> Result<Parcel> {
144 snd.bottom_up()
145 .filter_map(Parcel::from_datarow)
146 .next()
147 .ok_or(AnalysisError::NotEnoughData)
148}
149
150pub fn pressure_parcel(snd: &Sounding, pressure: HectoPascal) -> Result<Parcel> {
152 let row = linear_interpolate_sounding(snd, pressure)?;
153
154 Parcel::from_datarow(row).ok_or(AnalysisError::MissingValue)
155}
156
157pub fn most_unstable_parcel(snd: &Sounding) -> Result<Parcel> {
162 let temp_vec: Vec<Optioned<Kelvin>>;
163
164 let theta_e = if !snd.theta_e_profile().is_empty() {
165 snd.theta_e_profile()
166 } else {
167 temp_vec = equivalent_potential_temperature(snd);
168 &temp_vec
169 };
170 let press = snd.pressure_profile();
171
172 let top_pressure = press
173 .iter()
174 .filter_map(|p| p.into_option())
175 .next()
176 .ok_or(AnalysisError::NotEnoughData)?
177 - HectoPascal(300.0);
178
179 let (idx, _) = izip!(0.., press, theta_e)
180 .filter(|(_, p, theta_e)| p.is_some() && theta_e.is_some())
181 .map(|(i, p, theta_e)| (i, p.unpack(), theta_e.unpack()))
182 .take_while(|&(_, p, _)| p >= top_pressure)
183 .fold(
184 (::std::usize::MAX, metfor::ABSOLUTE_ZERO),
185 |(max_idx, max_val), (i, _, theta_e)| {
186 if theta_e > max_val {
187 (i, theta_e)
188 } else {
189 (max_idx, max_val)
190 }
191 },
192 );
193
194 snd.data_row(idx)
195 .and_then(Parcel::from_datarow)
196 .ok_or(AnalysisError::MissingValue)
197}
198
199pub fn convective_parcel(snd: &Sounding) -> Result<Parcel> {
203 let Parcel {
204 dew_point: dp,
205 pressure: initial_p,
206 temperature: initial_t,
207 } = mixed_layer_parcel(snd)?;
208
209 let tgt_mw = metfor::mixing_ratio(dp, initial_p).ok_or(AnalysisError::MetForError)?;
210
211 let pressure = snd.pressure_profile();
212 let temperature = snd.temperature_profile();
213
214 let (tgt_p, tgt_t) = izip!(pressure, temperature)
215 .filter(|(p, t)| p.is_some() && t.is_some())
217 .map(|(p, t)| (p.unpack(), t.unpack()))
219 .filter_map(|(p, t)| {
221 let mw = metfor::mixing_ratio(t, p)?;
222
223 Some((p, t, mw))
224 })
225 .skip_while(|(_, _, mw)| *mw <= tgt_mw)
228 .tuple_windows::<(_, _)>()
230 .filter_map(|((p0, t0, mw0), (p1, t1, mw1))| {
232 if mw0 >= tgt_mw && mw1 <= tgt_mw {
233 let tgt_p = linear_interp(tgt_mw, mw0, mw1, p0, p1);
234 let tgt_t = linear_interp(tgt_mw, mw0, mw1, t0, t1);
235 Some((tgt_p, tgt_t))
236 } else {
237 None
238 }
239 })
240 .next()
242 .ok_or(AnalysisError::NotEnoughData)?;
245
246 let tgt_theta = metfor::potential_temperature(tgt_p, tgt_t);
248 let tgt_t =
249 Celsius::from(metfor::temperature_from_pot_temp(tgt_theta, initial_p)).max(initial_t);
250
251 Ok(Parcel {
252 pressure: initial_p,
253 temperature: tgt_t,
254 dew_point: dp,
255 })
256}
257
258pub fn effective_layer_parcel(snd: &Sounding) -> Result<Parcel> {
261 let effective_layer = &effective_inflow_layer(snd).ok_or(AnalysisError::FailedPrerequisite)?;
262 average_parcel(snd, effective_layer)
263}
264
265pub fn average_parcel(snd: &Sounding, layer: &Layer) -> Result<Parcel> {
267 let press = snd.pressure_profile();
268 let t = snd.temperature_profile();
269 let dp = snd.dew_point_profile();
270
271 if press.is_empty() || t.is_empty() || dp.is_empty() {
272 return Err(AnalysisError::MissingProfile);
273 }
274
275 let bottom_p = layer.bottom.pressure.ok_or(AnalysisError::NoDataProfile)?;
276 let top_p = layer.top.pressure.ok_or(AnalysisError::NoDataProfile)?;
277
278 let (sum_p, sum_t, sum_mw, count) = izip!(press, t, dp)
279 .filter(|(p, t, dp)| p.is_some() && t.is_some() && dp.is_some())
280 .map(|(p, t, dp)| (p.unpack(), t.unpack(), dp.unpack()))
282 .skip_while(|&(p, _, _)| p > bottom_p)
283 .take_while(|&(p, _, _)| p >= top_p)
284 .map(|(p, t, dp)| (p, dp, metfor::potential_temperature(p, t)))
285 .filter_map(|(p, dp, th)| metfor::mixing_ratio(dp, p).map(|mw| (p, th, mw)))
286 .fold(
287 (HectoPascal(0.0), 0.0f64, 0.0f64, 0),
288 |acc, (p, theta, mw)| {
289 let (sum_p, sum_t, sum_mw, count) = acc;
290 (
291 sum_p + p,
292 sum_t + theta.unpack() * p.unpack(),
293 sum_mw + mw * p.unpack(),
294 count + 1,
295 )
296 },
297 );
298
299 if sum_p == HectoPascal(0.0) {
300 return Err(AnalysisError::NotEnoughData);
301 }
302
303 let pressure = HectoPascal(sum_p.unpack() / f64::from(count));
305 let temperature = Celsius::from(metfor::temperature_from_pot_temp(
306 Kelvin(sum_t / sum_p.unpack()),
307 pressure,
308 ));
309 let dew_point = metfor::dew_point_from_p_and_mw(pressure, sum_mw / sum_p.unpack())
310 .ok_or(AnalysisError::InvalidInput)?;
311
312 Ok(Parcel {
313 temperature,
314 pressure,
315 dew_point,
316 })
317}