Skip to main content

hpr_sim/
error.rs

1//! Errors from setting up or running a flight.
2
3use hpr_aero::AeroError;
4use hpr_atmos::AtmosError;
5use hpr_core::CoreError;
6use hpr_design::{DesignError, Finding, Severity};
7use thiserror::Error;
8
9use crate::integrator::{IntegrationError, SettingsError};
10
11/// An error setting up or running a flight.
12#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum SimError {
15    /// An input outside its domain, such as a negative rail length.
16    #[error("{what} is outside its domain: {value}")]
17    Domain {
18        /// What the value is.
19        what: &'static str,
20        /// The offending value.
21        value: f64,
22    },
23    /// A model doesn't cover the input, such as a flutter panel of an elliptical fin.
24    #[error("unsupported: {what}")]
25    Unsupported {
26        /// What isn't covered.
27        what: &'static str,
28    },
29    /// A separation or ejection that the design can't make, with the component (or stage) id it
30    /// names or runs through.
31    #[error("{what}: `{component}`")]
32    Parting {
33        /// Why it can't be made.
34        what: &'static str,
35        /// The component or stage.
36        component: String,
37    },
38    /// A mass shift that the design can't make, with the component id it names.
39    #[error("{what}: `{component}`")]
40    Shift {
41        /// Why it can't be made.
42        what: &'static str,
43        /// The component.
44        component: String,
45    },
46    /// A mass release that the design can't make, with the component id it names.
47    #[error("{what}: `{component}`")]
48    MassRelease {
49        /// Why it can't be made.
50        what: &'static str,
51        /// The component.
52        component: String,
53    },
54    /// The design's checks found errors, and the settings don't accept them: every finding,
55    /// errors and warnings, in the checks' order. The message counts and names only the errors.
56    #[error(
57        "the design has {} error finding(s); the first is {}",
58        error_count(.0),
59        first_error(.0)
60    )]
61    DesignChecks(Vec<Finding>),
62    /// The design can't be assembled.
63    #[error(transparent)]
64    Design(#[from] DesignError),
65    /// The aerodynamic models refused the design or a flow condition (for example Mach 5 or
66    /// faster, past the normal force's and the drag buildup's range).
67    #[error(transparent)]
68    Aero(#[from] AeroError),
69    /// The atmosphere or wind model refused a height.
70    #[error(transparent)]
71    Atmosphere(#[from] AtmosError),
72    /// A geodesy, gravity or numerics error.
73    #[error(transparent)]
74    Core(#[from] CoreError),
75    /// Bad integrator settings or initial state.
76    #[error(transparent)]
77    Settings(#[from] SettingsError),
78    /// The integration failed for a reason other than the step limit.
79    #[error("the integration failed")]
80    Integration(#[source] Box<IntegrationError<SimError>>),
81}
82
83/// How many findings are errors, for [`SimError::DesignChecks`]'s message.
84fn error_count(findings: &[Finding]) -> usize {
85    findings
86        .iter()
87        .filter(|finding| finding.severity() == Severity::Error)
88        .count()
89}
90
91/// The first finding that is an error, for [`SimError::DesignChecks`]'s message.
92fn first_error(findings: &[Finding]) -> String {
93    findings
94        .iter()
95        .find(|finding| finding.severity() == Severity::Error)
96        .map_or_else(|| "missing".to_owned(), |finding| format!("{finding:?}"))
97}
98
99#[cfg(test)]
100mod tests {
101    use super::*;
102
103    /// #46: a warning ahead of the error is neither counted nor named as the error.
104    #[test]
105    fn design_checks_message_counts_and_names_only_errors() {
106        let warning = Finding::MotorPastMountTop {
107            configuration: "c".to_owned(),
108            mount: "mount".to_owned(),
109            excess_m: 0.01,
110        };
111        let error = Finding::MotorWiderThanMount {
112            configuration: "c".to_owned(),
113            mount: "mount".to_owned(),
114            motor_diameter_m: 0.038,
115            mount_inner_diameter_m: 0.029,
116        };
117        let message = SimError::DesignChecks(vec![warning, error.clone()]).to_string();
118        assert_eq!(
119            message,
120            format!("the design has 1 error finding(s); the first is {error:?}")
121        );
122    }
123}