1use 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#[derive(Clone)]
45pub struct OrbitalDynamics {
46 pub accel_models: Vec<Arc<dyn AccelModel + Sync>>,
47}
48
49impl OrbitalDynamics {
50 pub fn point_masses(celestial_objects: Vec<i32>) -> Self {
52 Self::new(vec![Arc::new(PointMasses::new(celestial_objects))])
54 }
55
56 pub fn two_body() -> Self {
58 Self::new(vec![])
59 }
60
61 pub fn new(accel_models: Vec<Arc<dyn AccelModel + Sync>>) -> Self {
63 Self { accel_models }
64 }
65
66 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 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 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 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 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 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 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 Ok((dx, grad))
173 }
174}
175
176#[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 pub correction: Option<Aberration>,
186}
187
188impl PointMasses {
189 pub fn new(celestial_objects: Vec<i32>) -> Self {
191 Self {
192 celestial_objects,
193 correction: None,
194 }
195 }
196
197 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 for third_body in self.celestial_objects.iter().copied() {
226 if osc.frame.ephem_origin_id_match(third_body) {
227 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 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; 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 let radius: Vector3<OHyperdual<f64, Const<7>>> = hyperspace_from_vector(&osc.radius_km);
264 let mut fx = Vector3::zeros();
266 let mut grad = Matrix3::zeros();
267
268 for third_body in self.celestial_objects.iter().copied() {
270 if osc.frame.ephem_origin_id_match(third_body) {
271 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 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 let mut r_j = radius - r_ij; 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 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}