Skip to main content

nyx_space/dynamics/
orbital.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 super::{
20    AccelModel, DynamicsAlmanacSnafu, DynamicsAstroSnafu, DynamicsError, DynamicsPlanetarySnafu,
21};
22use crate::cosmic::{AstroPhysicsSnafu, Orbit};
23use crate::linalg::{Const, Matrix3, Matrix6, OVector, Vector3, Vector6};
24
25use anise::almanac::Almanac;
26use anise::astro::Aberration;
27use anise::constants::celestial_objects::celestial_name_from_id;
28use hyperdual::linalg::norm;
29use hyperdual::{Float, OHyperdual, extract_jacobian_and_result, hyperspace_from_vector};
30use serde::{Deserialize, Serialize};
31use serde_dhall::{SimpleType, StaticType};
32use snafu::ResultExt;
33use std::collections::HashMap;
34use std::f64;
35use std::fmt;
36use std::sync::Arc;
37
38#[cfg(feature = "python")]
39use pyo3::prelude::*;
40
41pub use super::gravity_field::GravityField;
42
43/// `OrbitalDynamics` provides the equations of motion for any celestial dynamic, without state transition matrix computation.
44#[derive(Clone)]
45pub struct OrbitalDynamics {
46    pub accel_models: Vec<Arc<dyn AccelModel + Sync>>,
47}
48
49impl OrbitalDynamics {
50    /// Initializes the point masses gravities with the provided list of bodies
51    pub fn point_masses(celestial_objects: Vec<i32>) -> Self {
52        // Create the point masses
53        Self::new(vec![Arc::new(PointMasses::new(celestial_objects))])
54    }
55
56    /// Initializes a OrbitalDynamics which does not simulate the gravity pull of other celestial objects but the primary one.
57    pub fn two_body() -> Self {
58        Self::new(vec![])
59    }
60
61    /// Initialize orbital dynamics with a list of acceleration models
62    pub fn new(accel_models: Vec<Arc<dyn AccelModel + Sync>>) -> Self {
63        Self { accel_models }
64    }
65
66    /// Initialize new orbital mechanics with the provided model.
67    /// **Note:** Orbital dynamics _always_ include two body dynamics, these cannot be turned off.
68    pub fn from_model(accel_model: Arc<dyn AccelModel + Sync>) -> Self {
69        Self::new(vec![accel_model])
70    }
71}
72
73impl fmt::Display for OrbitalDynamics {
74    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
75        let models: Vec<String> = self.accel_models.iter().map(|x| format!("{x}")).collect();
76        write!(f, "Orbital dynamics: {}", models.join("; "))
77    }
78}
79
80impl OrbitalDynamics {
81    pub(crate) fn eom(
82        &self,
83        osc: &Orbit,
84        almanac: &Almanac,
85    ) -> Result<OVector<f64, Const<42>>, DynamicsError> {
86        // Still return something of size 42, but the STM will be zeros.
87        let body_acceleration = (-osc
88            .frame
89            .mu_km3_s2()
90            .context(AstroPhysicsSnafu)
91            .context(DynamicsAstroSnafu)?
92            / osc.rmag_km().powi(3))
93            * osc.radius_km;
94
95        let mut d_x = Vector6::from_iterator(
96            osc.velocity_km_s
97                .iter()
98                .chain(body_acceleration.iter())
99                .cloned(),
100        );
101
102        // Apply the acceleration models
103        for model in &self.accel_models {
104            let model_acc = model.eom(osc, almanac)?;
105            for i in 0..3 {
106                d_x[i + 3] += model_acc[i];
107            }
108        }
109
110        Ok(OVector::<f64, Const<42>>::from_iterator(
111            d_x.iter()
112                .chain(OVector::<f64, Const<36>>::zeros().iter())
113                .cloned(),
114        ))
115    }
116
117    pub fn dual_eom(
118        &self,
119        _delta_t_s: f64,
120        osc: &Orbit,
121        almanac: &Almanac,
122    ) -> Result<(Vector6<f64>, Matrix6<f64>), DynamicsError> {
123        // Extract data from hyperspace
124        // Build full state vector with partials in the right position (hence building with all six components)
125        let state: Vector6<OHyperdual<f64, Const<7>>> =
126            hyperspace_from_vector(&osc.to_cartesian_pos_vel());
127
128        let radius = state.fixed_rows::<3>(0).into_owned();
129        let velocity = state.fixed_rows::<3>(3).into_owned();
130
131        // Code up math as usual
132        let rmag = norm(&radius);
133        let body_acceleration = radius
134            * (OHyperdual::<f64, Const<7>>::from_real(
135                -osc.frame
136                    .mu_km3_s2()
137                    .context(AstroPhysicsSnafu)
138                    .context(DynamicsAstroSnafu)?,
139            ) / rmag.powi(3));
140
141        // Extract result into Vector6 and Matrix6
142        let mut dx = Vector6::zeros();
143        let mut grad = Matrix6::zeros();
144        for i in 0..6 {
145            dx[i] = if i < 3 {
146                velocity[i].real()
147            } else {
148                body_acceleration[i - 3].real()
149            };
150            for j in 1..7 {
151                grad[(i, j - 1)] = if i < 3 {
152                    velocity[i][j]
153                } else {
154                    body_acceleration[i - 3][j]
155                };
156            }
157        }
158
159        // Apply the acceleration models
160        for model in &self.accel_models {
161            let (model_acc, model_grad) = model.gradient(osc, almanac)?;
162            for i in 0..3 {
163                dx[i + 3] += model_acc[i];
164                for j in 1..4 {
165                    grad[(i + 3, j - 1)] += model_grad[(i, j - 1)];
166                }
167            }
168        }
169
170        // This function returns the time derivative of each function. The propagator will add this to the state vector (which has the previous STM).
171        // This is why we don't multiply the gradient (A matrix) with the previous STM
172        Ok((dx, grad))
173    }
174}
175
176/// PointMasses model
177///
178/// :type celestial_objects: list[int]
179/// :type correction: Aberration | None
180#[derive(Clone, Debug, Serialize, Deserialize)]
181#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
182pub struct PointMasses {
183    pub celestial_objects: Vec<i32>,
184    /// Light-time correction computation if extra point masses are needed
185    pub correction: Option<Aberration>,
186}
187
188impl PointMasses {
189    /// Initializes the point masses gravities with the provided list of bodies
190    pub fn new(celestial_objects: Vec<i32>) -> Self {
191        Self {
192            celestial_objects,
193            correction: None,
194        }
195    }
196
197    /// Initializes the point masses gravities with the provided list of bodies, and accounting for some light time correction
198    pub fn with_correction(celestial_objects: Vec<i32>, correction: Option<Aberration>) -> Self {
199        Self {
200            celestial_objects,
201            correction,
202        }
203    }
204}
205
206impl fmt::Display for PointMasses {
207    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
208        let masses: Vec<String> = self
209            .celestial_objects
210            .iter()
211            .map(|third_body| {
212                celestial_name_from_id(*third_body)
213                    .unwrap_or(&format!("{third_body}"))
214                    .to_string()
215            })
216            .collect();
217        write!(f, "Point masses of {}", masses.join(", "))
218    }
219}
220
221impl AccelModel for PointMasses {
222    fn eom(&self, osc: &Orbit, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
223        let mut d_x = Vector3::zeros();
224        // Get all of the position vectors between the center body and the third bodies
225        for third_body in self.celestial_objects.iter().copied() {
226            if osc.frame.ephem_origin_id_match(third_body) {
227                // Ignore the contribution of the integration frame, that's handled by OrbitalDynamics
228                continue;
229            }
230
231            let third_body_frame = almanac
232                .frame_info(osc.frame.with_ephem(third_body))
233                .context(DynamicsPlanetarySnafu {
234                    action: "planetary data from third body not loaded",
235                })?;
236
237            // Orbit of j-th body as seen from primary body
238            let st_ij = almanac
239                .transform(third_body_frame, osc.frame, osc.epoch, self.correction)
240                .context(DynamicsAlmanacSnafu {
241                    action: "computing third body gravitational pull",
242                })?;
243
244            let r_ij = st_ij.radius_km;
245            let r_ij3 = st_ij.rmag_km().powi(3);
246            let r_j = osc.radius_km - r_ij; // sc as seen from 3rd body
247            let r_j3 = r_j.norm().powi(3);
248            d_x += -third_body_frame
249                .mu_km3_s2()
250                .context(AstroPhysicsSnafu)
251                .context(DynamicsAstroSnafu)?
252                * (r_j / r_j3 + r_ij / r_ij3);
253        }
254        Ok(d_x)
255    }
256
257    fn gradient(
258        &self,
259        osc: &Orbit,
260        almanac: &Almanac,
261    ) -> Result<(Vector3<f64>, Matrix3<f64>), DynamicsError> {
262        // Build the hyperdual space of the radius vector
263        let radius: Vector3<OHyperdual<f64, Const<7>>> = hyperspace_from_vector(&osc.radius_km);
264        // Extract result into Vector6 and Matrix6
265        let mut fx = Vector3::zeros();
266        let mut grad = Matrix3::zeros();
267
268        // Get all of the position vectors between the center body and the third bodies
269        for third_body in self.celestial_objects.iter().copied() {
270            if osc.frame.ephem_origin_id_match(third_body) {
271                // Ignore the contribution of the integration frame, that's handled by OrbitalDynamics
272                continue;
273            }
274
275            let third_body_frame = almanac
276                .frame_info(osc.frame.with_ephem(third_body))
277                .context(DynamicsPlanetarySnafu {
278                    action: "planetary data from third body not loaded",
279                })?;
280
281            let gm_d = OHyperdual::<f64, Const<7>>::from_real(
282                -third_body_frame
283                    .mu_km3_s2()
284                    .context(AstroPhysicsSnafu)
285                    .context(DynamicsAstroSnafu)?,
286            );
287
288            // Orbit of j-th body as seen from primary body
289            let st_ij = almanac
290                .transform(third_body_frame, osc.frame, osc.epoch, self.correction)
291                .context(DynamicsAlmanacSnafu {
292                    action: "computing third body gravitational pull",
293                })?;
294
295            let r_ij: Vector3<OHyperdual<f64, Const<7>>> = hyperspace_from_vector(&st_ij.radius_km);
296            let r_ij3 = norm(&r_ij).powi(3);
297
298            // The difference leads to the dual parts nulling themselves out, so let's fix that.
299            let mut r_j = radius - r_ij; // sc as seen from 3rd body
300            r_j[0][1] = 1.0;
301            r_j[1][2] = 1.0;
302            r_j[2][3] = 1.0;
303
304            let r_j3 = norm(&r_j).powi(3);
305            let mut third_body_acc_d = r_j / r_j3 + r_ij / r_ij3;
306            third_body_acc_d[0] *= gm_d;
307            third_body_acc_d[1] *= gm_d;
308            third_body_acc_d[2] *= gm_d;
309
310            let (fxp, gradp) = extract_jacobian_and_result::<_, 3, 3, 7>(&third_body_acc_d);
311            fx += fxp;
312            grad += gradp;
313        }
314
315        Ok((fx, grad))
316    }
317}
318
319impl StaticType for PointMasses {
320    fn static_type() -> SimpleType {
321        let mut fields = HashMap::new();
322
323        fields.insert("celestial_objects".to_string(), Vec::<i32>::static_type());
324
325        // Manually define the record for Aberration right here
326        // instead of calling Aberration::static_type()
327        let aberration_fields = {
328            let mut f = HashMap::new();
329            f.insert("converged".to_string(), bool::static_type());
330            f.insert("stellar".to_string(), bool::static_type());
331            f.insert("transmit_mode".to_string(), bool::static_type());
332            SimpleType::Record(f)
333        };
334
335        fields.insert(
336            "correction".to_string(),
337            SimpleType::Optional(Box::new(aberration_fields)),
338        );
339
340        SimpleType::Record(fields)
341    }
342}
343
344#[cfg(feature = "python")]
345#[cfg_attr(feature = "python", pymethods)]
346impl PointMasses {
347    #[pyo3(signature=(celestial_objects, correction=None))]
348    #[new]
349    fn py_new(celestial_objects: Vec<i32>, correction: Option<Aberration>) -> Self {
350        Self {
351            celestial_objects,
352            correction,
353        }
354    }
355
356    fn __str__(&self) -> String {
357        format!("{self:?}")
358    }
359
360    fn __repr__(&self) -> String {
361        format!("{self:?} @ {self:p}")
362    }
363}