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}