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