Skip to main content

hpr/
environment.rs

1//! Where and in what weather a rocket flies.
2
3use hpr_atmos::{Atmosphere, ConstantWind, Wind};
4use hpr_core::earth::{Earth, GravityModel};
5use hpr_core::geodesy::Geodetic;
6
7use crate::error::{Error, finite};
8
9/// Where and in what weather a rocket flies: a launch site, an atmosphere and a wind.
10///
11/// [`Environment::new`] puts the site on the WGS 84 ellipsoid, with the 1976 US Standard
12/// Atmosphere and no wind. The `with_` methods change the weather. For what this type doesn't
13/// offer, such as a geoid undulation, build an [`hpr_sim::Environment`] and wrap it with
14/// [`Environment::from_sim`].
15///
16/// ```
17/// use hpr::Environment;
18///
19/// // Spaceport America, 1,400 m up, with 5 m/s of wind from the west.
20/// let environment = Environment::new(32.99, -106.97, 1400.0)?.with_constant_wind(5.0, 270.0)?;
21/// assert_eq!(environment.sim().site().height_m, 1400.0);
22/// # Ok::<(), hpr::Error>(())
23/// ```
24#[derive(Debug, Clone)]
25pub struct Environment {
26    sim: hpr_sim::Environment,
27}
28
29impl Environment {
30    /// A launch site at latitude `latitude_deg` (degrees north), longitude `longitude_deg`
31    /// (degrees east) and elevation `elevation_m`, with the 1976 US Standard Atmosphere and no
32    /// wind.
33    ///
34    /// The elevation is taken as both the site's height above mean sea level, which the
35    /// atmosphere and wind are read at, and its height above the WGS 84 ellipsoid, which the
36    /// flight's position is measured from, as if the geoid undulation, the gap between the two,
37    /// were zero; hpr has no geoid model. The air is read where you said; the site's gravity is
38    /// off by the free-air gradient, about 3.1 µm/s² per meter of undulation. To give an
39    /// undulation `N`, build an [`hpr_sim::Environment`] on the site's ellipsoidal height
40    /// `h = H + N`, set `N` with [`hpr_sim::Environment::with_geoid_undulation_m`], and wrap it
41    /// with [`Environment::from_sim`]; the air is then read at `H`. Heights in a flight's results
42    /// are above the site. Longitude is positive east, so a site in the Americas has a negative
43    /// one.
44    ///
45    /// # Errors
46    ///
47    /// [`Error::Core`] for a latitude outside `[−90°, 90°]` or a value that isn't finite.
48    pub fn new(latitude_deg: f64, longitude_deg: f64, elevation_m: f64) -> Result<Self, Error> {
49        let site = Geodetic::from_degrees(latitude_deg, longitude_deg, elevation_m)?;
50        Ok(Self {
51            sim: hpr_sim::Environment::standard(site)?,
52        })
53    }
54
55    /// The same place and atmosphere, with `wind` in place of the wind: any [`Wind`], such as
56    /// [`hpr_atmos`]'s power law, log law or layers by height, or a model of your own.
57    #[must_use]
58    pub fn with_wind(self, wind: impl Wind + 'static) -> Self {
59        Self {
60            sim: self.sim.with_wind(wind),
61        }
62    }
63
64    /// The same place and atmosphere, with a wind of `speed_m_s` at every height, blowing from
65    /// `from_deg`: the direction it comes from, clockwise from true north (270° is a west wind).
66    ///
67    /// # Errors
68    ///
69    /// [`Error::Domain`] for a direction that isn't finite; [`Error::Atmos`] for a negative or
70    /// non-finite speed.
71    pub fn with_constant_wind(self, speed_m_s: f64, from_deg: f64) -> Result<Self, Error> {
72        let from_deg = finite("wind direction, degrees", from_deg)?;
73        Ok(self.with_wind(ConstantWind::new(speed_m_s, from_deg.to_radians())?))
74    }
75
76    /// The same place and wind, with `atmosphere` in place of the standard one: any
77    /// [`Atmosphere`], such as a balloon sounding's levels ([`hpr_atmos::SoundingProfile`]), or a
78    /// model of your own.
79    #[must_use]
80    pub fn with_atmosphere(mut self, atmosphere: impl Atmosphere + 'static) -> Self {
81        self.sim.atmosphere = std::sync::Arc::new(atmosphere);
82        self
83    }
84
85    /// The same place and weather, with gravity evaluated by `gravity` in place of the default,
86    /// [`GravityModel::Ellipsoidal`]: the full normal gravity vector at the rocket's position.
87    /// [`GravityModel::VerticalTaylor`] is RocketPy's formula, for like-for-like comparisons with
88    /// it; [`GravityModel`] has the rest. The Earth's rotation is kept.
89    ///
90    /// ```
91    /// use hpr::Environment;
92    /// use hpr::hpr_core::earth::GravityModel;
93    ///
94    /// let environment =
95    ///     Environment::new(32.99, -106.97, 1400.0)?.with_gravity(GravityModel::VerticalTaylor)?;
96    /// assert_eq!(environment.sim().earth.gravity_model(), GravityModel::VerticalTaylor);
97    /// # Ok::<(), hpr::Error>(())
98    /// ```
99    ///
100    /// # Errors
101    ///
102    /// [`Error::Core`] for a constant gravity that is negative or not finite.
103    pub fn with_gravity(mut self, gravity: GravityModel) -> Result<Self, Error> {
104        let earth = &self.sim.earth;
105        self.sim.earth = Earth::new(*earth.field(), self.sim.site(), gravity, earth.rotation())?;
106        Ok(self)
107    }
108
109    /// An environment built with [`hpr_sim`] directly.
110    #[must_use]
111    pub fn from_sim(sim: hpr_sim::Environment) -> Self {
112        Self { sim }
113    }
114
115    /// The [`hpr_sim::Environment`] this wraps.
116    #[must_use]
117    pub fn sim(&self) -> &hpr_sim::Environment {
118        &self.sim
119    }
120}