1use 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#[derive(Clone)]
44pub struct OrbitalDynamics {
45 pub accel_models: Vec<Arc<dyn AccelModel + Sync>>,
46}
47
48impl OrbitalDynamics {
49 pub fn point_masses(celestial_objects: Vec<i32>) -> Self {
51 Self::new(vec![Arc::new(PointMasses::new(celestial_objects))])
53 }
54
55 pub fn two_body() -> Self {
57 Self::new(vec![])
58 }
59
60 pub fn new(accel_models: Vec<Arc<dyn AccelModel + Sync>>) -> Self {
62 Self { accel_models }
63 }
64
65 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 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 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 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 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 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 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 Ok((dx, grad))
172 }
173}
174
175#[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 pub correction: Option<Aberration>,
185}
186
187impl PointMasses {
188 pub fn new(celestial_objects: Vec<i32>) -> Self {
190 Self {
191 celestial_objects,
192 correction: None,
193 }
194 }
195
196 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 for third_body in self.celestial_objects.iter().copied() {
221 if osc.frame.ephem_origin_id_match(third_body) {
222 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 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; 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 let radius: Vector3<OHyperdual<f64, Const<7>>> = hyperspace_from_vector(&osc.radius_km);
259 let mut fx = Vector3::zeros();
261 let mut grad = Matrix3::zeros();
262
263 for third_body in self.celestial_objects.iter().copied() {
265 if osc.frame.ephem_origin_id_match(third_body) {
266 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 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 let mut r_j = radius - r_ij; 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 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}