Skip to main content

nyx_space/dynamics/
drag.rs

1/*
2    Nyx, blazing fast astrodynamics
3    Copyright (C) 2018-onwards Christopher Rabotin <christopher.rabotin@gmail.com>
4
5    This program is free software: you can redistribute it and/or modify
6    it under the terms of the GNU Affero General Public License as published
7    by the Free Software Foundation, either version 3 of the License, or
8    (at your option) any later version.
9
10    This program is distributed in the hope that it will be useful,
11    but WITHOUT ANY WARRANTY; without even the implied warranty of
12    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13    GNU Affero General Public License for more details.
14
15    You should have received a copy of the GNU Affero General Public License
16    along with this program.  If not, see <https://www.gnu.org/licenses/>.
17*/
18
19use anise::almanac::Almanac;
20use anise::constants::frames::IAU_EARTH_FRAME;
21use snafu::ResultExt;
22
23use super::{
24    DynamicsAlmanacSnafu, DynamicsAstroSnafu, DynamicsError, DynamicsPlanetarySnafu, ForceModel,
25};
26use crate::cosmic::{AstroError, AstroPhysicsSnafu, Frame, Spacecraft};
27use crate::linalg::{Matrix4x3, Vector3};
28use serde::{Deserialize, Serialize};
29use serde_dhall::StaticType;
30use std::fmt;
31use std::sync::Arc;
32
33#[cfg(feature = "python")]
34use pyo3::prelude::*;
35#[cfg(feature = "python")]
36use pyo3::types::PyType;
37
38/// Density in kg/m^3 and altitudes in meters, not kilometers!
39#[derive(Clone, Copy, Debug, Serialize, Deserialize, StaticType)]
40#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
41pub enum AtmDensity {
42    Constant(f64),
43    Exponential { rho0: f64, r0: f64, ref_alt_m: f64 },
44    StdAtm { max_alt_m: f64 },
45}
46
47#[cfg(feature = "python")]
48#[cfg_attr(feature = "python", pymethods)]
49impl AtmDensity {
50    #[classmethod]
51    fn earth_exponential(_cls: &Bound<'_, PyType>) -> Self {
52        AtmDensity::Exponential {
53            rho0: 3.614e-13,
54            r0: 700_000.0,
55            ref_alt_m: 88_667.0,
56        }
57    }
58}
59
60/// `ConstantDrag` implements a constant drag model as defined in Vallado, 4th ed., page 551, with an important caveat.
61///
62/// **WARNING:** This basic model assumes that the velocity of the spacecraft is identical to the velocity of the upper atmosphere.
63/// This is a **bad** assumption and **should not** be used for high fidelity simulations.
64/// This will be resolved after https://github.com/nyx-space/nyx/issues/317 is implemented.
65#[derive(Clone)]
66pub struct ConstantDrag {
67    /// atmospheric density in kg/m^3
68    pub rho: f64,
69    /// Frame causing the drag
70    pub frame: Frame,
71    /// Set to true to estimate the coefficient of drag
72    pub estimate: bool,
73}
74
75impl fmt::Display for ConstantDrag {
76    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
77        write!(
78            f,
79            "\tConstant Drag rho = {} kg/m^3 in frame {}",
80            self.rho, self.frame
81        )
82    }
83}
84
85impl ForceModel for ConstantDrag {
86    fn estimation_index(&self) -> Option<usize> {
87        if self.estimate { Some(7) } else { None }
88    }
89
90    fn eom(&self, ctx: &Spacecraft, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
91        let osc =
92            almanac
93                .transform_to(ctx.orbit, self.frame, None)
94                .context(DynamicsAlmanacSnafu {
95                    action: "transforming into drag frame",
96                })?;
97
98        let velocity = osc.velocity_km_s;
99        // Note the 1e3 factor to convert drag units from ((kg * km^2 * s^-2) / m^1) to (kg * km * s^-2)
100        Ok(-0.5
101            * 1e3
102            * self.rho
103            * ctx.drag.coeff_drag
104            * ctx.drag.area_m2
105            * velocity.norm()
106            * velocity)
107    }
108
109    fn gradient(
110        &self,
111        _osc_ctx: &Spacecraft,
112        _almanac: &Almanac,
113    ) -> Result<(Vector3<f64>, Matrix4x3<f64>), DynamicsError> {
114        Err(DynamicsError::DynamicsAstro {
115            source: AstroError::PartialsUndefined,
116        })
117    }
118}
119
120/// `Drag` implements all three drag models.
121#[derive(Copy, Clone, Debug, Serialize, Deserialize, StaticType)]
122#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
123pub struct Drag {
124    /// Density computation method
125    pub density: AtmDensity,
126    /// Frame to compute the drag in
127    pub frame: Frame,
128    /// Set to true to estimate the coefficient of drag
129    pub estimate: bool,
130}
131
132impl Drag {
133    /// Common exponential drag model for the Earth
134    pub fn earth_exp(almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
135        Ok(Arc::new(Self {
136            density: AtmDensity::Exponential {
137                rho0: 3.614e-13,
138                r0: 700_000.0,
139                ref_alt_m: 88_667.0,
140            },
141            frame: almanac.frame_info(IAU_EARTH_FRAME).context({
142                DynamicsPlanetarySnafu {
143                    action: "planetary data from third body not loaded",
144                }
145            })?,
146            estimate: false,
147        }))
148    }
149
150    /// Drag model which uses the standard atmosphere 1976 model for atmospheric density
151    pub fn std_atm1976(almanac: &Almanac) -> Result<Arc<Self>, DynamicsError> {
152        Ok(Arc::new(Self {
153            density: AtmDensity::StdAtm {
154                max_alt_m: 1_000_000.0,
155            },
156            frame: almanac.frame_info(IAU_EARTH_FRAME).context({
157                DynamicsPlanetarySnafu {
158                    action: "planetary data from third body not loaded",
159                }
160            })?,
161            estimate: false,
162        }))
163    }
164}
165
166impl fmt::Display for Drag {
167    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
168        write!(
169            f,
170            "\tDrag density {:?} in frame {}",
171            self.density, self.frame
172        )
173    }
174}
175
176impl ForceModel for Drag {
177    fn estimation_index(&self) -> Option<usize> {
178        if self.estimate { Some(7) } else { None }
179    }
180
181    fn eom(&self, ctx: &Spacecraft, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
182        let integration_frame = ctx.orbit.frame;
183
184        let osc_drag_frame =
185            almanac
186                .transform_to(ctx.orbit, self.frame, None)
187                .context(DynamicsAlmanacSnafu {
188                    action: "transforming into drag frame",
189                })?;
190
191        match self.density {
192            AtmDensity::Constant(rho) => {
193                let velocity = osc_drag_frame.velocity_km_s;
194                // Note the 1e3 factor to convert drag units from ((kg * km^2 * s^-2) / m^1) to (kg * km * s^-2)
195                Ok(-0.5
196                    * 1e3
197                    * rho
198                    * ctx.drag.coeff_drag
199                    * ctx.drag.area_m2
200                    * velocity.norm()
201                    * velocity)
202            }
203
204            AtmDensity::Exponential {
205                rho0,
206                r0,
207                ref_alt_m,
208            } => {
209                // Compute rho in the drag frame.
210                let rho = rho0
211                    * (-(osc_drag_frame.rmag_km()
212                        - (r0
213                            + self
214                                .frame
215                                .mean_equatorial_radius_km()
216                                .context(AstroPhysicsSnafu)
217                                .context(DynamicsAstroSnafu)?))
218                        / ref_alt_m)
219                        .exp();
220
221                // TODO: Drag modeling will be improved in https://github.com/nyx-space/nyx/issues/317
222                // The frame will be double checked in this PR as well.
223                let velocity_integr_frame = almanac
224                    .transform_to(osc_drag_frame, integration_frame, None)
225                    .context(DynamicsAlmanacSnafu {
226                        action: "rotating into the integration frame",
227                    })?
228                    .velocity_km_s;
229
230                let velocity = velocity_integr_frame - osc_drag_frame.velocity_km_s;
231                // Note the 1e3 factor to convert drag units from ((kg * km^2 * s^-2) / m^1) to (kg * km * s^-2)
232                Ok(-0.5
233                    * 1e3
234                    * rho
235                    * ctx.drag.coeff_drag
236                    * ctx.drag.area_m2
237                    * velocity.norm()
238                    * velocity)
239            }
240
241            AtmDensity::StdAtm { max_alt_m } => {
242                let altitude_km = osc_drag_frame.rmag_km()
243                    - self
244                        .frame
245                        .mean_equatorial_radius_km()
246                        .context(AstroPhysicsSnafu)
247                        .context(DynamicsAstroSnafu)?;
248                let rho = if altitude_km > max_alt_m / 1_000.0 {
249                    // Use a constant density
250                    10.0_f64.powf((-7e-5) * altitude_km - 14.464)
251                } else {
252                    // Code from AVS/Schaub's Basilisk
253                    // Calculating the density based on a scaled 6th order polynomial fit to the log of density
254                    let scale = (altitude_km - 526.8000) / 292.8563;
255                    let logdensity =
256                        0.34047 * scale.powi(6) - 0.5889 * scale.powi(5) - 0.5269 * scale.powi(4)
257                            + 1.0036 * scale.powi(3)
258                            + 0.60713 * scale.powi(2)
259                            - 2.3024 * scale
260                            - 12.575;
261
262                    /* Calculating density by raising 10 to the log of density */
263                    10.0_f64.powf(logdensity)
264                };
265
266                let velocity_integr_frame = almanac
267                    .transform_to(osc_drag_frame, integration_frame, None)
268                    .context(DynamicsAlmanacSnafu {
269                        action: "rotating into the integration frame",
270                    })?
271                    .velocity_km_s;
272
273                let velocity = velocity_integr_frame - osc_drag_frame.velocity_km_s;
274                // Note the 1e3 factor to convert drag units from ((kg * km^2 * s^-2) / m^1) to (kg * km * s^-2)
275                Ok(-0.5
276                    * 1e3
277                    * rho
278                    * ctx.drag.coeff_drag
279                    * ctx.drag.area_m2
280                    * velocity.norm()
281                    * velocity)
282            }
283        }
284    }
285
286    fn gradient(
287        &self,
288        _osc_ctx: &Spacecraft,
289        _almanac: &Almanac,
290    ) -> Result<(Vector3<f64>, Matrix4x3<f64>), DynamicsError> {
291        Err(DynamicsError::DynamicsAstro {
292            source: AstroError::PartialsUndefined,
293        })
294    }
295}
296
297#[cfg(feature = "python")]
298#[cfg_attr(feature = "python", pymethods)]
299impl Drag {
300    #[pyo3(signature = (density, frame, estimate=true))]
301    #[new]
302    fn py_new(density: AtmDensity, frame: Frame, estimate: bool) -> Self {
303        Self {
304            density,
305            frame,
306            estimate,
307        }
308    }
309
310    fn __str__(&self) -> String {
311        format!("{self}")
312    }
313
314    fn __repr__(&self) -> String {
315        format!("{self} @ {self:p}")
316    }
317}