use super::{
AccelModel, DynamicsAlmanacSnafu, DynamicsAstroSnafu, DynamicsError, DynamicsPlanetarySnafu,
};
use crate::cosmic::{AstroPhysicsSnafu, Frame, Orbit};
use crate::linalg::{Const, Matrix3, Matrix6, OVector, Vector3, Vector6};
use anise::almanac::Almanac;
use anise::astro::Aberration;
use hyperdual::linalg::norm;
use hyperdual::{Float, OHyperdual, extract_jacobian_and_result, hyperspace_from_vector};
use serde::{Deserialize, Serialize};
use serde_dhall::{SimpleType, StaticType};
use snafu::ResultExt;
use std::collections::HashMap;
use std::f64;
use std::fmt;
use std::sync::Arc;
#[cfg(feature = "python")]
use pyo3::prelude::*;
pub use super::gravity_field::GravityField;
#[derive(Clone)]
pub struct OrbitalDynamics {
pub accel_models: Vec<Arc<dyn AccelModel + Sync>>,
}
impl OrbitalDynamics {
pub fn point_masses(celestial_objects: Vec<i32>) -> Self {
Self::new(vec![Arc::new(PointMasses::new(celestial_objects))])
}
pub fn two_body() -> Self {
Self::new(vec![])
}
pub fn new(accel_models: Vec<Arc<dyn AccelModel + Sync>>) -> Self {
Self { accel_models }
}
pub fn from_model(accel_model: Arc<dyn AccelModel + Sync>) -> Self {
Self::new(vec![accel_model])
}
}
impl fmt::Display for OrbitalDynamics {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let models: Vec<String> = self.accel_models.iter().map(|x| format!("{x}")).collect();
write!(f, "Orbital dynamics: {}", models.join("; "))
}
}
impl OrbitalDynamics {
pub(crate) fn eom(
&self,
osc: &Orbit,
almanac: &Almanac,
) -> Result<OVector<f64, Const<42>>, DynamicsError> {
let body_acceleration = (-osc
.frame
.mu_km3_s2()
.context(AstroPhysicsSnafu)
.context(DynamicsAstroSnafu)?
/ osc.rmag_km().powi(3))
* osc.radius_km;
let mut d_x = Vector6::from_iterator(
osc.velocity_km_s
.iter()
.chain(body_acceleration.iter())
.cloned(),
);
for model in &self.accel_models {
let model_acc = model.eom(osc, almanac)?;
for i in 0..3 {
d_x[i + 3] += model_acc[i];
}
}
Ok(OVector::<f64, Const<42>>::from_iterator(
d_x.iter()
.chain(OVector::<f64, Const<36>>::zeros().iter())
.cloned(),
))
}
pub fn dual_eom(
&self,
_delta_t_s: f64,
osc: &Orbit,
almanac: &Almanac,
) -> Result<(Vector6<f64>, Matrix6<f64>), DynamicsError> {
let state: Vector6<OHyperdual<f64, Const<7>>> =
hyperspace_from_vector(&osc.to_cartesian_pos_vel());
let radius = state.fixed_rows::<3>(0).into_owned();
let velocity = state.fixed_rows::<3>(3).into_owned();
let rmag = norm(&radius);
let body_acceleration = radius
* (OHyperdual::<f64, Const<7>>::from_real(
-osc.frame
.mu_km3_s2()
.context(AstroPhysicsSnafu)
.context(DynamicsAstroSnafu)?,
) / rmag.powi(3));
let mut dx = Vector6::zeros();
let mut grad = Matrix6::zeros();
for i in 0..6 {
dx[i] = if i < 3 {
velocity[i].real()
} else {
body_acceleration[i - 3].real()
};
for j in 1..7 {
grad[(i, j - 1)] = if i < 3 {
velocity[i][j]
} else {
body_acceleration[i - 3][j]
};
}
}
for model in &self.accel_models {
let (model_acc, model_grad) = model.gradient(osc, almanac)?;
for i in 0..3 {
dx[i + 3] += model_acc[i];
for j in 1..4 {
grad[(i + 3, j - 1)] += model_grad[(i, j - 1)];
}
}
}
Ok((dx, grad))
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "python", pyclass(from_py_object, get_all, set_all))]
pub struct PointMasses {
pub celestial_objects: Vec<i32>,
pub correction: Option<Aberration>,
}
impl PointMasses {
pub fn new(celestial_objects: Vec<i32>) -> Self {
Self {
celestial_objects,
correction: None,
}
}
pub fn with_correction(celestial_objects: Vec<i32>, correction: Option<Aberration>) -> Self {
Self {
celestial_objects,
correction,
}
}
}
impl fmt::Display for PointMasses {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let masses: Vec<String> = self
.celestial_objects
.iter()
.map(|third_body| format!("{}", Frame::from_ephem_j2000(*third_body)))
.collect();
write!(f, "Point masses of {}", masses.join(", "))
}
}
impl AccelModel for PointMasses {
fn eom(&self, osc: &Orbit, almanac: &Almanac) -> Result<Vector3<f64>, DynamicsError> {
let mut d_x = Vector3::zeros();
for third_body in self.celestial_objects.iter().copied() {
if osc.frame.ephem_origin_id_match(third_body) {
continue;
}
let third_body_frame = almanac
.frame_info(osc.frame.with_ephem(third_body))
.context(DynamicsPlanetarySnafu {
action: "planetary data from third body not loaded",
})?;
let st_ij = almanac
.transform(third_body_frame, osc.frame, osc.epoch, self.correction)
.context(DynamicsAlmanacSnafu {
action: "computing third body gravitational pull",
})?;
let r_ij = st_ij.radius_km;
let r_ij3 = st_ij.rmag_km().powi(3);
let r_j = osc.radius_km - r_ij; let r_j3 = r_j.norm().powi(3);
d_x += -third_body_frame
.mu_km3_s2()
.context(AstroPhysicsSnafu)
.context(DynamicsAstroSnafu)?
* (r_j / r_j3 + r_ij / r_ij3);
}
Ok(d_x)
}
fn gradient(
&self,
osc: &Orbit,
almanac: &Almanac,
) -> Result<(Vector3<f64>, Matrix3<f64>), DynamicsError> {
let radius: Vector3<OHyperdual<f64, Const<7>>> = hyperspace_from_vector(&osc.radius_km);
let mut fx = Vector3::zeros();
let mut grad = Matrix3::zeros();
for third_body in self.celestial_objects.iter().copied() {
if osc.frame.ephem_origin_id_match(third_body) {
continue;
}
let third_body_frame = almanac
.frame_info(osc.frame.with_ephem(third_body))
.context(DynamicsPlanetarySnafu {
action: "planetary data from third body not loaded",
})?;
let gm_d = OHyperdual::<f64, Const<7>>::from_real(
-third_body_frame
.mu_km3_s2()
.context(AstroPhysicsSnafu)
.context(DynamicsAstroSnafu)?,
);
let st_ij = almanac
.transform(third_body_frame, osc.frame, osc.epoch, self.correction)
.context(DynamicsAlmanacSnafu {
action: "computing third body gravitational pull",
})?;
let r_ij: Vector3<OHyperdual<f64, Const<7>>> = hyperspace_from_vector(&st_ij.radius_km);
let r_ij3 = norm(&r_ij).powi(3);
let mut r_j = radius - r_ij; r_j[0][1] = 1.0;
r_j[1][2] = 1.0;
r_j[2][3] = 1.0;
let r_j3 = norm(&r_j).powi(3);
let mut third_body_acc_d = r_j / r_j3 + r_ij / r_ij3;
third_body_acc_d[0] *= gm_d;
third_body_acc_d[1] *= gm_d;
third_body_acc_d[2] *= gm_d;
let (fxp, gradp) = extract_jacobian_and_result::<_, 3, 3, 7>(&third_body_acc_d);
fx += fxp;
grad += gradp;
}
Ok((fx, grad))
}
}
impl StaticType for PointMasses {
fn static_type() -> SimpleType {
let mut fields = HashMap::new();
fields.insert("celestial_objects".to_string(), Vec::<i32>::static_type());
let aberration_fields = {
let mut f = HashMap::new();
f.insert("converged".to_string(), bool::static_type());
f.insert("stellar".to_string(), bool::static_type());
f.insert("transmit_mode".to_string(), bool::static_type());
SimpleType::Record(f)
};
fields.insert(
"correction".to_string(),
SimpleType::Optional(Box::new(aberration_fields)),
);
SimpleType::Record(fields)
}
}
#[cfg(feature = "python")]
#[cfg_attr(feature = "python", pymethods)]
impl PointMasses {
#[pyo3(signature=(celestial_objects, correction=None))]
#[new]
fn py_new(celestial_objects: Vec<i32>, correction: Option<Aberration>) -> Self {
Self {
celestial_objects,
correction,
}
}
fn __str__(&self) -> String {
format!("{self:?}")
}
fn __repr__(&self) -> String {
format!("{self:?} @ {self:p}")
}
}