1use serde::{Deserialize, Serialize};
35
36use super::document::{Document, Element};
37use super::value::Values;
38use super::warning::{Warning, WarningKind};
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
42#[non_exhaustive]
43#[serde(deny_unknown_fields)]
44pub struct StoredSimulation {
45 pub name: String,
47 pub status: Option<String>,
50 pub simulator: Option<String>,
52 pub calculator: Option<String>,
54 pub conditions: Option<LaunchConditions>,
56 pub results: Option<StoredResults>,
58 #[serde(default)]
60 pub parser_warnings: bool,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71#[non_exhaustive]
72pub enum StoredReferenceExclusion {
73 MissingStatus,
75 Outdated,
77 NotSimulated,
79 External,
81 Loaded,
83 CannotRun,
85 Aborted,
87 UnknownStatus,
89 MissingSimulator,
91 UnsupportedSimulator,
93 MissingCalculator,
95 UnsupportedCalculator,
97 MissingResults,
99 MissingSummary,
101 ImpossibleSummary,
103 InconsistentResults,
105 ParserWarning,
107 FatalEvent,
109 UninspectableSeries,
111 ReducedDesign,
113 MissingConfiguration,
115 UnknownConfiguration,
117 UnflyableConfiguration,
119}
120
121impl StoredReferenceExclusion {
122 #[must_use]
124 pub const fn reason(self) -> &'static str {
125 match self {
126 Self::MissingStatus => "status-missing",
127 Self::Outdated => "status-outdated",
128 Self::NotSimulated => "status-not-simulated",
129 Self::External => "status-external",
130 Self::Loaded => "status-loaded",
131 Self::CannotRun => "status-cannot-run",
132 Self::Aborted => "status-aborted",
133 Self::UnknownStatus => "status-unknown",
134 Self::MissingSimulator => "simulator-missing",
135 Self::UnsupportedSimulator => "simulator-unsupported",
136 Self::MissingCalculator => "calculator-missing",
137 Self::UnsupportedCalculator => "calculator-unsupported",
138 Self::MissingResults => "missing-results",
139 Self::MissingSummary => "missing-summary",
140 Self::ImpossibleSummary => "impossible-summary",
141 Self::InconsistentResults => "inconsistent-results",
142 Self::ParserWarning => "parser-warning",
143 Self::FatalEvent => "fatal-event",
144 Self::UninspectableSeries => "uninspectable-series",
145 Self::ReducedDesign => "reduced-design",
146 Self::MissingConfiguration => "configuration-missing",
147 Self::UnknownConfiguration => "configuration-unknown",
148 Self::UnflyableConfiguration => "configuration-unflyable",
149 }
150 }
151}
152
153pub const OPENROCKET_SIMULATOR: &str = "RK4Simulator";
158
159pub const OPENROCKET_CALCULATOR: &str = "BarrowmanCalculator";
164
165impl StoredSimulation {
166 #[must_use]
186 pub fn reference_exclusion(&self) -> Option<StoredReferenceExclusion> {
187 let exclusion = match self.status.as_deref() {
188 Some("uptodate") => None,
189 None => Some(StoredReferenceExclusion::MissingStatus),
190 Some("outdated") => Some(StoredReferenceExclusion::Outdated),
191 Some("notsimulated") => Some(StoredReferenceExclusion::NotSimulated),
192 Some("external") => Some(StoredReferenceExclusion::External),
193 Some("loaded") => Some(StoredReferenceExclusion::Loaded),
194 Some("cantrun") => Some(StoredReferenceExclusion::CannotRun),
195 Some("aborted") => Some(StoredReferenceExclusion::Aborted),
196 Some(_) => Some(StoredReferenceExclusion::UnknownStatus),
197 };
198 if exclusion.is_some() {
199 return exclusion;
200 }
201 let Some(simulator) = self.simulator.as_deref() else {
202 return Some(StoredReferenceExclusion::MissingSimulator);
203 };
204 if simulator != OPENROCKET_SIMULATOR {
205 return Some(StoredReferenceExclusion::UnsupportedSimulator);
206 }
207 let Some(calculator) = self.calculator.as_deref() else {
208 return Some(StoredReferenceExclusion::MissingCalculator);
209 };
210 if calculator != OPENROCKET_CALCULATOR {
211 return Some(StoredReferenceExclusion::UnsupportedCalculator);
212 }
213 let Some(results) = self.results.as_ref() else {
214 return Some(StoredReferenceExclusion::MissingResults);
215 };
216 if let Some(exclusion) = results.reference_exclusion() {
217 return Some(exclusion);
218 }
219 self.parser_warnings
220 .then_some(StoredReferenceExclusion::ParserWarning)
221 }
222}
223
224#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
227#[non_exhaustive]
228#[serde(deny_unknown_fields)]
229pub struct LaunchConditions {
230 pub configuration: Option<String>,
232 pub rod_length_m: Option<f64>,
234 pub rod_angle_rad: Option<f64>,
236 pub rod_direction_rad: Option<f64>,
239 pub into_wind: Option<bool>,
242 pub wind_speed_m_s: Option<f64>,
244 pub wind_turbulence: Option<f64>,
247 pub wind_from_rad: Option<f64>,
250 pub wind_model: Option<String>,
253 pub wind_levels: Vec<WindLevel>,
255 pub wind_levels_above: Option<String>,
258 pub launch_altitude_m: Option<f64>,
260 pub latitude_deg: Option<f64>,
262 pub longitude_deg: Option<f64>,
264 pub geodetic_method: Option<String>,
266 pub atmosphere: Option<Atmosphere>,
268 pub time_step_s: Option<f64>,
270 pub max_time_s: Option<f64>,
272}
273
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
276#[non_exhaustive]
277#[serde(deny_unknown_fields)]
278pub struct WindLevel {
279 pub altitude_m: Option<f64>,
282 pub speed_m_s: Option<f64>,
284 pub from_rad: Option<f64>,
286 pub standard_deviation_m_s: Option<f64>,
288}
289
290#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
292#[serde(rename_all = "snake_case", tag = "model")]
293#[non_exhaustive]
294#[serde(deny_unknown_fields)]
295pub enum Atmosphere {
296 Isa {},
300 Extended {
302 temperature_k: Option<f64>,
304 pressure_pa: Option<f64>,
306 },
307 Other {
309 name: String,
311 },
312}
313
314#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
316#[non_exhaustive]
317#[serde(deny_unknown_fields)]
318pub struct StoredResults {
319 pub max_altitude_m: Option<f64>,
321 pub max_speed_m_s: Option<f64>,
323 pub max_acceleration_m_s2: Option<f64>,
325 pub max_mach: Option<f64>,
327 pub time_to_apogee_s: Option<f64>,
329 pub flight_time_s: Option<f64>,
331 pub ground_hit_speed_m_s: Option<f64>,
333 pub rod_exit_speed_m_s: Option<f64>,
335 pub deployment_speed_m_s: Option<f64>,
337 pub optimum_delay_s: Option<f64>,
339 pub branches: Vec<StoredBranch>,
341 pub warnings: Vec<String>,
343}
344
345const STORED_GROUND_ALTITUDE_TOLERANCE_M: f64 = 1e-3;
349const STORED_TIME_TOLERANCE_S: f64 = 1e-3;
351
352impl StoredResults {
353 fn reference_exclusion(&self) -> Option<StoredReferenceExclusion> {
354 let required = [
355 self.max_altitude_m,
356 self.max_speed_m_s,
357 self.time_to_apogee_s,
358 ];
359 if required.iter().any(Option::is_none) {
360 return Some(StoredReferenceExclusion::MissingSummary);
361 }
362 let all = [
363 self.max_altitude_m,
364 self.max_speed_m_s,
365 self.max_acceleration_m_s2,
366 self.max_mach,
367 self.time_to_apogee_s,
368 self.flight_time_s,
369 self.ground_hit_speed_m_s,
370 self.rod_exit_speed_m_s,
371 self.deployment_speed_m_s,
372 self.optimum_delay_s,
373 ];
374 if all
375 .iter()
376 .flatten()
377 .any(|value| !value.is_finite() || *value < 0.0)
378 || self.max_altitude_m == Some(0.0)
379 || self.max_speed_m_s == Some(0.0)
380 || self.time_to_apogee_s == Some(0.0)
381 {
382 return Some(StoredReferenceExclusion::ImpossibleSummary);
383 }
384 if let (Some(apogee_time), Some(flight_time)) = (self.time_to_apogee_s, self.flight_time_s)
385 && apogee_time > flight_time
386 {
387 return Some(StoredReferenceExclusion::InconsistentResults);
388 }
389 let Some(apogee) = self.max_altitude_m else {
390 return Some(StoredReferenceExclusion::MissingSummary);
391 };
392 let mut altitude_branches = Vec::new();
399 let mut series_maxes = vec![None; self.branches.len()];
400 for (index, branch) in self.branches.iter().enumerate() {
401 let times = branch.column("Time");
402 let altitudes = branch.column("Altitude");
403 if branch
404 .rows
405 .iter()
406 .any(|row| row.iter().all(Option::is_none))
407 || (!branch.rows.is_empty() && times.is_none() && altitudes.is_none())
408 {
409 return Some(StoredReferenceExclusion::UninspectableSeries);
410 }
411 if let Some(times) = times {
412 let mut previous = None;
413 let mut observed = false;
414 for time in times.into_iter().flatten() {
415 observed = true;
416 if !time.is_finite() || time < 0.0 {
417 return Some(StoredReferenceExclusion::ImpossibleSummary);
418 }
419 if previous.is_some_and(|old| time < old) {
420 return Some(StoredReferenceExclusion::InconsistentResults);
421 }
422 if self.flight_time_s.is_some_and(|flight_time| {
423 times_differ_more_than_allowed(time, flight_time) && time > flight_time
424 }) {
425 return Some(StoredReferenceExclusion::InconsistentResults);
426 }
427 previous = Some(time);
428 }
429 if !observed && !branch.rows.is_empty() {
430 return Some(StoredReferenceExclusion::UninspectableSeries);
431 }
432 }
433 let Some(altitudes) = altitudes else {
434 continue;
435 };
436 if !altitudes.is_empty() {
437 altitude_branches.push(index);
438 }
439 let mut series_max = None;
440 for altitude in altitudes.into_iter().flatten() {
441 if !altitude.is_finite() || altitude < -STORED_GROUND_ALTITUDE_TOLERANCE_M {
442 return Some(StoredReferenceExclusion::ImpossibleSummary);
443 }
444 if altitude >= 0.0 {
445 series_max = Some(series_max.map_or(altitude, |old: f64| old.max(altitude)));
446 }
447 }
448 if series_max.is_none() && !branch.rows.is_empty() {
449 return Some(StoredReferenceExclusion::UninspectableSeries);
450 }
451 series_maxes[index] = series_max;
452 }
453 let apogee_branches: Vec<usize> = self
454 .branches
455 .iter()
456 .enumerate()
457 .filter(|(_, branch)| branch.events.iter().any(|event| event.kind == "apogee"))
458 .map(|(index, _)| index)
459 .collect();
460 let summary_branch = match apogee_branches.as_slice() {
461 [] => altitude_branches.first().copied(),
462 [index, ..] => Some(*index),
463 };
464 if let Some(index) = apogee_branches.first().copied()
465 && let Some(event) = self.branches[index]
466 .events
467 .iter()
468 .find(|event| event.kind == "apogee")
469 && times_differ_more_than_allowed(
470 event.time_s,
471 self.time_to_apogee_s.unwrap_or_default(),
472 )
473 {
474 return Some(StoredReferenceExclusion::InconsistentResults);
475 }
476 if apogee_branches.is_empty() && altitude_branches.len() > 1 {
477 return Some(StoredReferenceExclusion::UninspectableSeries);
478 }
479 for (index, series_max) in series_maxes.into_iter().enumerate() {
480 if summary_branch == Some(index)
485 && let Some(series_max) = series_max
486 && (series_max - apogee).abs() > 1e-3_f64.max(apogee * 1e-3)
487 {
488 return Some(StoredReferenceExclusion::InconsistentResults);
489 }
490 }
491 for event in self.branches.iter().flat_map(|branch| &branch.events) {
492 if !event.time_s.is_finite() || event.time_s < 0.0 {
493 return Some(StoredReferenceExclusion::ImpossibleSummary);
494 }
495 if self
496 .flight_time_s
497 .is_some_and(|flight_time| event.time_s > flight_time)
498 {
499 return Some(StoredReferenceExclusion::InconsistentResults);
500 }
501 if is_fatal_event(&event.kind) {
502 return Some(StoredReferenceExclusion::FatalEvent);
503 }
504 }
505 None
506 }
507}
508
509fn times_differ_more_than_allowed(first: f64, second: f64) -> bool {
510 let rounding = f64::EPSILON * first.abs().max(second.abs()).max(1.0) * 4.0;
511 (first - second).abs() > STORED_TIME_TOLERANCE_S + rounding
512}
513
514fn is_fatal_event(kind: &str) -> bool {
515 let normalized: String = kind
516 .chars()
517 .filter(|character| !matches!(character, '_' | '-'))
518 .flat_map(char::to_lowercase)
519 .collect();
520 matches!(
521 normalized.as_str(),
522 "exception" | "simabort" | "simulationabort"
523 )
524}
525
526#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
528#[non_exhaustive]
529#[serde(deny_unknown_fields)]
530pub struct StoredBranch {
531 pub name: String,
533 pub types: Vec<String>,
535 pub rows: Vec<Vec<Option<f64>>>,
539 pub events: Vec<StoredEvent>,
541}
542
543impl StoredBranch {
544 pub fn column(&self, name: &str) -> Option<Vec<Option<f64>>> {
546 let index = self.types.iter().position(|column| column == name)?;
547 Some(
548 self.rows
549 .iter()
550 .map(|row| row.get(index).copied().flatten())
551 .collect(),
552 )
553 }
554}
555
556#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
558#[non_exhaustive]
559#[serde(deny_unknown_fields)]
560pub struct StoredEvent {
561 pub time_s: f64,
563 pub kind: String,
565 pub source: Option<String>,
567}
568
569pub(super) fn read(document: &Document, warnings: &mut Vec<Warning>) -> Vec<StoredSimulation> {
571 let Some(simulations) = document.root.child("simulations") else {
572 return Vec::new();
573 };
574 simulations
575 .children_named("simulation")
576 .enumerate()
577 .map(|(index, element)| {
578 let at = format!("openrocket/simulations/simulation[{index}]");
579 simulation(element, &at, warnings)
580 })
581 .collect()
582}
583
584fn simulation(element: &Element, at: &str, warnings: &mut Vec<Warning>) -> StoredSimulation {
585 let warning_count = warnings.len();
586 super::reads::note(element, "conditions");
588 super::reads::note(element, "flightdata");
589 let mut values = Values::new(element, at, warnings);
590 let name = values.word(&["name"]).unwrap_or_default();
591 let simulator = values.word(&["simulator"]);
592 let calculator = values.word(&["calculator"]);
593 let conditions = element
594 .child("conditions")
595 .map(|c| conditions(c, &format!("{at}/conditions"), warnings));
596 let results = element
597 .child("flightdata")
598 .map(|f| results(f, &format!("{at}/flightdata"), warnings));
599 StoredSimulation {
600 name,
601 status: element.attribute("status").map(str::to_owned),
602 simulator,
603 calculator,
604 conditions,
605 results,
606 parser_warnings: warnings.len() > warning_count,
607 }
608}
609
610fn conditions(element: &Element, at: &str, warnings: &mut Vec<Warning>) -> LaunchConditions {
611 super::reads::note(element, "wind");
613 super::reads::note(element, "atmosphere");
614 let average = element
615 .children_named("wind")
616 .find(|wind| wind.attribute("model") == Some("average"));
617 let multilevel = element
618 .children_named("wind")
619 .find(|wind| wind.attribute("model") == Some("multilevel"));
620 let (average_speed, average_from) = match average {
621 Some(wind) => {
622 let mut values = Values::new(wind, at, warnings);
623 (values.number(&["speed"]), values.number(&["direction"]))
624 }
625 None => (None, None),
626 };
627 let wind_levels = multilevel.map_or_else(Vec::new, |wind| {
628 super::reads::note(wind, "windlevel");
629 wind.children_named("windlevel")
630 .map(|level| {
631 let mut number = |name: &str| kept_number(level, name, at, warnings);
632 WindLevel {
633 altitude_m: number("altitude"),
634 speed_m_s: number("speed"),
635 from_rad: number("direction"),
636 standard_deviation_m_s: number("standarddeviation"),
637 }
638 })
639 .collect()
640 });
641 let atmosphere = element.child("atmosphere").map(|atmosphere| {
642 let model = atmosphere.attribute("model").unwrap_or_default();
643 let unread = match model {
644 "isa" | "extendedisa" => None,
645 "" => Some(
646 "an atmosphere with no `model`, as older files write their own table; it was not \
647 read"
648 .to_owned(),
649 ),
650 other => Some(format!(
651 "an atmosphere whose model is `{other}`, which OpenRocket 24.12 does not write; it \
652 was kept by name only"
653 )),
654 };
655 if let Some(message) = unread {
656 warnings.push(Warning::new(
657 format!("{at}/atmosphere"),
658 WarningKind::Unusual,
659 message,
660 ));
661 }
662 match model {
663 "isa" => Atmosphere::Isa {},
664 "extendedisa" => {
665 let mut values = Values::new(atmosphere, at, warnings);
666 Atmosphere::Extended {
667 temperature_k: values.number(&["basetemperature"]),
668 pressure_pa: values.number(&["basepressure"]),
669 }
670 }
671 other => Atmosphere::Other {
672 name: other.to_owned(),
673 },
674 }
675 });
676 let mut values = Values::new(element, at, warnings);
677 let legacy_speed = values.number(&["windaverage"]);
678 let legacy_from = values.number(&["winddirection"]);
679 let average_wind = average;
680 for (what, legacy, average) in [
681 ("speed", legacy_speed, average_speed),
682 ("direction", legacy_from, average_from),
683 ] {
684 if let (Some(legacy), Some(average)) = (legacy, average)
685 && legacy != average
686 {
687 if let Some(wind) = average_wind {
690 super::reads::forget(wind, what);
691 }
692 values.warn_at(
693 WarningKind::Dropped,
694 format!(
695 "the wind's {what} is {legacy} in the legacy tag and {average} in the average \
696 `wind`; the legacy tag's was taken"
697 ),
698 );
699 }
700 }
701 LaunchConditions {
702 configuration: values.word(&["configid"]).filter(|id| !id.is_empty()),
703 rod_length_m: values.number(&["launchrodlength"]),
704 rod_angle_rad: values.number(&["launchrodangle"]).map(f64::to_radians),
705 rod_direction_rad: values.number(&["launchroddirection"]).map(f64::to_radians),
706 into_wind: values.flag(&["launchintowind"]),
707 wind_speed_m_s: legacy_speed.or(average_speed),
708 wind_turbulence: values.number(&["windturbulence"]),
709 wind_from_rad: legacy_from.or(average_from),
710 wind_model: values.word(&["windmodeltype"]),
711 wind_levels,
712 wind_levels_above: multilevel
713 .and_then(|wind| wind.attribute("altituderef"))
714 .map(str::to_owned),
715 launch_altitude_m: values.number(&["launchaltitude"]),
716 latitude_deg: values.number(&["launchlatitude"]),
717 longitude_deg: values.number(&["launchlongitude"]),
718 geodetic_method: values.word(&["geodeticmethod"]),
719 atmosphere,
720 time_step_s: values.number(&["timestep"]),
721 max_time_s: values.number(&["maxtime"]),
722 }
723}
724
725fn attribute_number(
727 element: &Element,
728 name: &str,
729 at: &str,
730 warnings: &mut Vec<Warning>,
731) -> Option<f64> {
732 let text = element.attribute(name)?;
733 match text.trim().parse::<f64>() {
734 Ok(value) if value.is_finite() => Some(value),
735 _ => {
736 warnings.push(Warning::new(
737 at,
738 WarningKind::Dropped,
739 format!("`{name}` says `{text}`, which is not a number; it was ignored"),
740 ));
741 None
742 }
743 }
744}
745
746fn kept_number(
750 element: &Element,
751 name: &str,
752 at: &str,
753 warnings: &mut Vec<Warning>,
754) -> Option<f64> {
755 let number = attribute_number(element, name, at, warnings);
756 if number.is_none() && element.attribute(name).is_some() {
757 super::reads::forget_attribute(element, name);
758 }
759 number
760}
761
762fn results(element: &Element, at: &str, warnings: &mut Vec<Warning>) -> StoredResults {
763 super::reads::note(element, "warning");
765 super::reads::note(element, "databranch");
766 let mut number = |name: &str| kept_number(element, name, at, warnings);
767 let mut results = StoredResults {
768 max_altitude_m: number("maxaltitude"),
769 max_speed_m_s: number("maxvelocity"),
770 max_acceleration_m_s2: number("maxacceleration"),
771 max_mach: number("maxmach"),
772 time_to_apogee_s: number("timetoapogee"),
773 flight_time_s: number("flighttime"),
774 ground_hit_speed_m_s: number("groundhitvelocity"),
775 rod_exit_speed_m_s: number("launchrodvelocity"),
776 deployment_speed_m_s: number("deploymentvelocity"),
777 optimum_delay_s: number("optimumdelay"),
778 branches: Vec::new(),
779 warnings: element
780 .children_named("warning")
781 .map(|warning| warning.text().trim().to_owned())
782 .collect(),
783 };
784 for (index, element) in element.children_named("databranch").enumerate() {
785 let at = format!("{at}/databranch[{index}]");
786 results.branches.push(branch(element, &at, warnings));
787 }
788 results
789}
790
791fn branch(element: &Element, at: &str, warnings: &mut Vec<Warning>) -> StoredBranch {
792 let types: Vec<String> = element
793 .attribute("types")
794 .map(|types| types.split(',').map(|t| t.trim().to_owned()).collect())
795 .unwrap_or_default();
796 let mut rows = Vec::new();
797 let mut dropped = 0usize;
798 let mut infinite = 0usize;
799 super::reads::note(element, "datapoint");
800 super::reads::note(element, "event");
801 for point in element.children_named("datapoint") {
802 let text = point.text();
803 let row: Option<Vec<Option<f64>>> = text
806 .split(',')
807 .map(|value| match value.trim().parse::<f64>() {
808 Ok(v) if v.is_nan() => Some(None),
809 Ok(v) if v.is_finite() => Some(Some(v)),
810 Ok(_) => {
811 infinite += 1;
812 Some(None)
813 }
814 Err(_) => None,
815 })
816 .collect();
817 match row {
818 Some(row) if row.len() == types.len() => rows.push(row),
819 _ => dropped += 1,
820 }
821 }
822 if infinite > 0 {
823 warnings.push(Warning::new(
824 at,
825 WarningKind::Dropped,
826 format!("{infinite} infinite value(s) in the rows were kept as not computed"),
827 ));
828 }
829 if dropped > 0 {
830 warnings.push(Warning::new(
831 at,
832 WarningKind::Dropped,
833 format!(
834 "{dropped} `datapoint` row(s) are not {} numbers, one per column; they were left \
835 out",
836 types.len()
837 ),
838 ));
839 }
840 let mut events = Vec::new();
841 for event in element.children_named("event") {
842 let (Some(time_s), Some(kind)) = (
843 attribute_number(event, "time", at, warnings),
844 event
845 .attribute("type")
846 .filter(|kind| !kind.trim().is_empty()),
847 ) else {
848 warnings.push(Warning::new(
849 at,
850 WarningKind::Dropped,
851 "an `event` with no time or no type; it was left out",
852 ));
853 continue;
854 };
855 events.push(StoredEvent {
856 time_s,
857 kind: kind.trim().to_owned(),
858 source: event.attribute("source").map(str::to_owned),
859 });
860 }
861 StoredBranch {
862 name: element.attribute("name").unwrap_or_default().to_owned(),
863 types,
864 rows,
865 events,
866 }
867}