1use serde_json::Value;
16
17use crate::metrics::{Reference, ReferenceValue};
18use crate::run::{
19 DescentDevice, DescentSetup, FlightDevice, FlightMotor, SeriesRow, WholeFlightSetup,
20};
21
22#[must_use]
29pub fn descent_case(
30 document: &Value,
31 case: &str,
32 file: &str,
33 sha256: &str,
34) -> Option<(Reference, DescentSetup)> {
35 let (reference, found) = reference_of(document, case, file, sha256)?;
36 let environment = found.get("environment")?;
37 let elevation_m = number(environment.get("elevation_m"))?;
38 let start = found.get("start")?;
39 let position = start.get("position_msl_m")?.as_array()?;
40 let velocity = start.get("velocity_m_s")?.as_array()?;
41 let devices = found
42 .get("devices")?
43 .as_array()?
44 .iter()
45 .map(|device| {
46 Some(DescentDevice {
47 name: device.get("name")?.as_str()?.to_owned(),
48 cd_s_m2: number(device.get("cd_s_m2"))?,
49 lag_s: number(device.get("lag_s"))?,
50 height_above_ground_m: match device.get("trigger")?.get("kind")?.as_str()? {
51 "apogee" => None,
52 "descending_below_height_agl" => {
53 Some(number(device.get("trigger")?.get("height_m"))?)
54 }
55 _ => return None,
56 },
57 })
58 })
59 .collect::<Option<Vec<_>>>()?;
60
61 let setup = DescentSetup {
62 design: text(found.get("design"))?,
63 dry_mass_kg: positive(number(found.get("dry_mass_kg"))?)?,
64 start_height_above_ground_m: number(start.get("height_above_ground_m"))?,
65 latitude_deg: number(environment.get("latitude_deg"))?,
66 longitude_deg: number(environment.get("longitude_deg"))?,
67 elevation_m,
68 wind: wind_levels(environment, elevation_m)?,
69 start_time_s: number(start.get("time_s"))?,
70 start_position_m: (
71 number(position.first())?,
72 number(position.get(1))?,
73 number(position.get(2))?,
74 ),
75 start_velocity_m_s: (
76 number(velocity.first())?,
77 number(velocity.get(1))?,
78 number(velocity.get(2))?,
79 ),
80 devices,
81 };
82 Some((reference, setup))
83}
84
85#[must_use]
97pub fn whole_flight_case(
98 document: &Value,
99 case: &str,
100 file: &str,
101 sha256: &str,
102) -> Option<(Reference, WholeFlightSetup)> {
103 let (reference, found) = reference_of(document, case, file, sha256)?;
104 let environment = found.get("environment")?;
105 let elevation_m = number(environment.get("elevation_m"))?;
106 let rail = found.get("rail")?;
107 let drag = found.get("drag")?;
108 let motor = found.get("motor")?;
109 let devices = found
110 .get("devices")?
111 .as_array()?
112 .iter()
113 .map(|device| {
114 let trigger = device.get("trigger")?;
115 Some(FlightDevice {
116 name: text(device.get("name"))?,
117 cd_s_m2: positive(number(device.get("cd_s_m2"))?)?,
118 lag_s: number(device.get("lag_s"))?,
119 height_above_ground_m: match trigger.get("kind")?.as_str()? {
120 "apogee" => None,
121 "descending_below_height_agl" => Some(number(trigger.get("height_m"))?),
122 _ => return None,
123 },
124 })
125 })
126 .collect::<Option<Vec<_>>>()?;
127 let setup = WholeFlightSetup {
128 design: text(found.get("design"))?,
129 dry_mass_kg: positive(number(found.get("dry_mass_kg"))?)?,
130 latitude_deg: number(environment.get("latitude_deg"))?,
131 longitude_deg: number(environment.get("longitude_deg"))?,
132 elevation_m,
133 wind: wind_levels(environment, elevation_m)?,
134 rail_length_m: positive(number(rail.get("rail_length_m"))?)?,
135 inclination_deg: number(rail.get("inclination_deg"))?,
136 heading_deg: number(rail.get("heading_deg"))?,
137 effective_1rl_m: positive(number(rail.get("effective_1rl_m"))?)?,
138 motor: FlightMotor {
139 total_impulse_ns: positive(number(motor.get("total_impulse_ns"))?)?,
140 burn_out_time_s: positive(number(motor.get("burn_out_time_s"))?)?,
141 propellant_initial_mass_kg: positive(number(motor.get("propellant_initial_mass_kg"))?)?,
142 reference_pressure_pa: match motor.get("reference_pressure_pa")? {
144 Value::Null => None,
145 value => Some(number(Some(value))?),
146 },
147 },
148 cd0_vs_mach: match drag.get("own") {
151 Some(_) => None,
152 None => Some(pairs(drag.get("cd0_vs_mach"))?),
153 },
154 declared_cd0_vs_mach: match drag.get("own") {
155 Some(_) => None,
156 None => Some(pairs(document.get("declared_drag")?.get("cd0_vs_mach"))?),
157 },
158 own_drag_source: match drag.get("own") {
159 Some(_) => Some(text(drag.get("source"))?),
160 None => None,
161 },
162 reference_radius_m: positive(number(drag.get("reference_radius_m"))?)?,
163 reference_area_m2: positive(number(drag.get("reference_area_m2"))?)?,
164 devices,
165 series: series(found.get("series"))?,
166 };
167 let mut reference = reference;
168 for name in ["series_height_rms_m", "series_speed_rms_m_s"] {
169 reference.values.insert(
170 name.to_owned(),
171 ReferenceValue {
172 value: 0.0,
173 source: format!(
174 "0, exact agreement with {}, case {case}, series: the RMS of hpr's value less \
175 the reference's at its times",
176 reference.generator
177 ),
178 },
179 );
180 }
181 Some((reference, setup))
182}
183
184fn reference_of<'a>(
187 document: &'a Value,
188 case: &str,
189 file: &str,
190 sha256: &str,
191) -> Option<(Reference, &'a Value)> {
192 let oracle = text(document.get("oracle"))?;
193 let generator = text(document.get("generator"))?;
194 let command = text(document.get("command"))?;
195 let model = text(document.get("model"))?;
196 let overrides = text(document.get("overrides"))?;
197 let found = document
198 .get("cases")?
199 .as_array()?
200 .iter()
201 .find(|entry| entry.get("name").and_then(Value::as_str) == Some(case))?;
202
203 let metrics = found.get("metrics")?.as_object()?;
204 let mut values = std::collections::BTreeMap::new();
205 for (name, value) in metrics {
206 values.insert(
207 name.clone(),
208 ReferenceValue {
209 value: value.as_f64()?,
210 source: format!("{oracle}, {generator}, case {case}, metrics.{name}"),
211 },
212 );
213 }
214 let reference = Reference {
215 oracle,
216 generator,
217 command,
218 model,
219 overrides,
220 case: case.to_owned(),
221 file: file.to_owned(),
222 sha256: sha256.to_owned(),
223 values,
224 };
225 Some((reference, found))
226}
227
228fn pairs(value: Option<&Value>) -> Option<Vec<(f64, f64)>> {
230 value?
231 .as_array()?
232 .iter()
233 .map(|row| {
234 let row = row.as_array()?;
235 (row.len() == 2).then_some(())?;
236 Some((number(row.first())?, number(row.get(1))?))
237 })
238 .collect()
239}
240
241fn series(value: Option<&Value>) -> Option<Vec<SeriesRow>> {
244 let value = value?;
245 let columns: Vec<&str> = value
246 .get("columns")?
247 .as_array()?
248 .iter()
249 .map(Value::as_str)
250 .collect::<Option<_>>()?;
251 (columns == ["time_s", "height_above_ground_m", "speed_m_s"]).then_some(())?;
252 let rows = value
253 .get("rows")?
254 .as_array()?
255 .iter()
256 .map(|row| {
257 let row = row.as_array()?;
258 (row.len() == 3).then_some(())?;
259 Some((
260 number(row.first())?,
261 number(row.get(1))?,
262 number(row.get(2))?,
263 ))
264 })
265 .collect::<Option<Vec<_>>>()?;
266 let starts_at_ignition = rows.first()?.0 == 0.0;
267 let rising = rows.windows(2).all(|pair| pair[1].0 > pair[0].0);
268 (starts_at_ignition && rising).then_some(rows)
269}
270
271fn wind_levels(environment: &Value, elevation_m: f64) -> Option<Vec<(f64, f64, f64)>> {
274 let east = environment.get("wind_u")?;
275 let north = environment.get("wind_v")?;
276 match (east.as_array(), north.as_array()) {
277 (Some(east), Some(north)) if east.len() == north.len() => east
278 .iter()
279 .zip(north)
280 .map(|(east, north)| {
281 let east = east.as_array()?;
282 let north = north.as_array()?;
283 let height = number(east.first())?;
284 if (height - number(north.first())?).abs() > 0.0 {
285 return None;
286 }
287 Some((height, number(east.get(1))?, number(north.get(1))?))
288 })
289 .collect::<Option<Vec<_>>>(),
290 (None, None) => Some(vec![(
291 elevation_m,
292 number(Some(east))?,
293 number(Some(north))?,
294 )]),
295 _ => None,
296 }
297}
298
299fn text(value: Option<&Value>) -> Option<String> {
301 let text = value?.as_str()?.trim();
302 (!text.is_empty()).then(|| text.to_owned())
303}
304
305fn number(value: Option<&Value>) -> Option<f64> {
307 value?.as_f64()
308}
309
310fn positive(value: f64) -> Option<f64> {
312 (value.is_finite() && value > 0.0).then_some(value)
313}
314
315#[cfg(test)]
316mod tests;