use std::sync::Arc;
use arika::epoch::Epoch;
use arika::frame::{self, Vec3};
use nalgebra::Vector3;
use arika::frame::Eci;
use crate::model::ExternalLoads;
use crate::model::{HasOrbit, Model};
pub type BodyPositionFn = Arc<dyn Fn(&Epoch) -> Vec3<frame::Gcrs> + Send + Sync>;
#[derive(Clone)]
pub struct ThirdBodyGravity {
pub name: &'static str,
pub mu_body: f64,
body_position_fn: BodyPositionFn,
}
impl ThirdBodyGravity {
pub fn sun() -> Self {
Self {
name: "third_body_sun",
mu_body: arika::sun::MU,
body_position_fn: Arc::new(arika::sun::sun_position_eci),
}
}
pub fn moon() -> Self {
Self {
name: "third_body_moon",
mu_body: arika::moon::MU,
body_position_fn: Arc::new(arika::moon::moon_position_eci),
}
}
pub fn moon_with_ephemeris<E>(ephem: E) -> Self
where
E: arika::moon::MoonEphemeris + 'static,
{
let ephem = Arc::new(ephem);
Self {
name: "third_body_moon",
mu_body: arika::moon::MU,
body_position_fn: Arc::new(move |epoch| ephem.position_eci(epoch)),
}
}
pub fn custom<F>(name: &'static str, mu_body: f64, position_fn: F) -> Self
where
F: Fn(&Epoch) -> Vec3<frame::Gcrs> + Send + Sync + 'static,
{
Self {
name,
mu_body,
body_position_fn: Arc::new(position_fn),
}
}
}
impl ThirdBodyGravity {
pub(crate) fn acceleration(
&self,
sat_position: &Vector3<f64>,
epoch: Option<&Epoch>,
) -> Vector3<f64> {
let epoch = match epoch {
Some(e) => e,
None => return Vector3::zeros(),
};
let r_body = (self.body_position_fn)(epoch).into_inner();
let r_sat_to_body = r_body - sat_position;
let d = r_sat_to_body.magnitude();
let r_body_mag = r_body.magnitude();
self.mu_body
* (r_sat_to_body / (d * d * d) - r_body / (r_body_mag * r_body_mag * r_body_mag))
}
}
impl<F: Eci, S: HasOrbit<Frame = F>> Model<S, F> for ThirdBodyGravity {
fn name(&self) -> &str {
self.name
}
fn eval(&self, _t: f64, state: &S, epoch: Option<&Epoch>) -> ExternalLoads<F> {
ExternalLoads::acceleration(self.acceleration(state.orbit().position(), epoch))
}
}
const _: fn() = || {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ThirdBodyGravity>();
};
#[cfg(test)]
mod tests {
use super::*;
use crate::OrbitalState;
use arika::earth::{MU as MU_EARTH, R as R_EARTH};
use nalgebra::vector;
fn iss_state() -> OrbitalState {
let r = R_EARTH + 400.0;
let v = (MU_EARTH / r).sqrt();
OrbitalState::new(vector![r, 0.0, 0.0], vector![0.0, v, 0.0])
}
fn test_epoch() -> Epoch {
Epoch::from_gregorian(2024, 3, 20, 12, 0, 0.0)
}
#[test]
fn sun_perturbation_order_of_magnitude() {
let tb = ThirdBodyGravity::sun();
let state = iss_state();
let epoch = test_epoch();
let a = tb.acceleration(state.position(), Some(&epoch));
let a_mag = a.magnitude();
assert!(
a_mag > 1e-11 && a_mag < 1e-8,
"Sun perturbation should be ~5e-10 km/s², got {a_mag:.6e}"
);
}
#[test]
fn moon_perturbation_order_of_magnitude() {
let tb = ThirdBodyGravity::moon();
let state = iss_state();
let epoch = test_epoch();
let a = tb.acceleration(state.position(), Some(&epoch));
let a_mag = a.magnitude();
assert!(
a_mag > 1e-11 && a_mag < 1e-7,
"Moon perturbation should be ~1e-9 km/s², got {a_mag:.6e}"
);
}
#[test]
fn no_epoch_returns_zero() {
let tb = ThirdBodyGravity::sun();
let state = iss_state();
let a = tb.acceleration(state.position(), None);
assert_eq!(
a,
Vector3::zeros(),
"No epoch should give zero acceleration"
);
}
#[test]
fn perturbation_much_smaller_than_central_gravity() {
let tb_sun = ThirdBodyGravity::sun();
let tb_moon = ThirdBodyGravity::moon();
let state = iss_state();
let epoch = test_epoch();
let a_sun = tb_sun
.acceleration(state.position(), Some(&epoch))
.magnitude();
let a_moon = tb_moon
.acceleration(state.position(), Some(&epoch))
.magnitude();
let r = state.position().magnitude();
let a_central = MU_EARTH / (r * r);
assert!(
a_sun < a_central * 1e-4,
"Sun perturbation ({a_sun:.6e}) should be << central gravity ({a_central:.6e})"
);
assert!(
a_moon < a_central * 1e-4,
"Moon perturbation ({a_moon:.6e}) should be << central gravity ({a_central:.6e})"
);
}
#[test]
fn sun_perturbation_varies_with_epoch() {
let tb = ThirdBodyGravity::sun();
let r = R_EARTH + 400.0;
let v = (MU_EARTH / r).sqrt();
let state = OrbitalState::new(vector![0.0, r, 0.0], vector![-v, 0.0, 0.0]);
let epoch1 = Epoch::from_gregorian(2024, 3, 20, 12, 0, 0.0);
let epoch2 = Epoch::from_gregorian(2024, 6, 20, 12, 0, 0.0);
let a1 = tb.acceleration(state.position(), Some(&epoch1));
let a2 = tb.acceleration(state.position(), Some(&epoch2));
let cos_angle = a1.normalize().dot(&a2.normalize());
assert!(
cos_angle < 0.5,
"Sun perturbation should differ between March and June, cos={cos_angle:.3}"
);
}
#[test]
fn third_body_is_clone() {
let tb = ThirdBodyGravity::moon();
let tb2 = tb.clone();
assert_eq!(tb.name, tb2.name);
assert_eq!(tb.mu_body, tb2.mu_body);
let state = iss_state();
let epoch = test_epoch();
let a1 = tb.acceleration(state.position(), Some(&epoch));
let a2 = tb2.acceleration(state.position(), Some(&epoch));
assert_eq!(a1, a2);
}
#[test]
fn moon_constructor_uses_mu_moon_constant() {
let tb = ThirdBodyGravity::moon();
assert_eq!(tb.mu_body, arika::moon::MU);
let tb_trait = ThirdBodyGravity::moon_with_ephemeris(arika::moon::MeeusMoonEphemeris);
assert_eq!(tb_trait.mu_body, arika::moon::MU);
}
#[test]
fn moon_with_ephemeris_accepts_arc_dyn_moon_ephemeris() {
use arika::moon::{MeeusMoonEphemeris, MoonEphemeris};
let shared: Arc<dyn MoonEphemeris> = Arc::new(MeeusMoonEphemeris);
let tb = ThirdBodyGravity::moon_with_ephemeris(Arc::clone(&shared));
let state = iss_state();
let epoch = test_epoch();
let a = tb.acceleration(state.position(), Some(&epoch));
let a_ref = ThirdBodyGravity::moon().acceleration(state.position(), Some(&epoch));
assert_eq!(a, a_ref);
}
#[test]
fn custom_third_body_uses_supplied_closure() {
let fake_body_pos = Vec3::<frame::Gcrs>::new(1.0e6, 0.0, 0.0);
let fake_mu = 1.0e5;
let tb = ThirdBodyGravity::custom("fake", fake_mu, move |_epoch| fake_body_pos);
let state = iss_state();
let epoch = test_epoch();
let a = tb.acceleration(state.position(), Some(&epoch));
let fake_body_raw = fake_body_pos.into_inner();
let r_sat_to_body = fake_body_raw - *state.position();
let d = r_sat_to_body.magnitude();
let r_body_mag = fake_body_raw.magnitude();
let expected = fake_mu
* (r_sat_to_body / (d * d * d)
- fake_body_raw / (r_body_mag * r_body_mag * r_body_mag));
let err = (a - expected).magnitude();
assert!(
err < 1e-15,
"custom body acceleration mismatch: err={err:e}"
);
assert_eq!(tb.name, "fake");
}
#[test]
fn moon_with_ephemeris_uses_supplied_ephemeris() {
use arika::moon::{MeeusMoonEphemeris, MoonEphemeris};
let tb_default = ThirdBodyGravity::moon();
let tb_trait = ThirdBodyGravity::moon_with_ephemeris(MeeusMoonEphemeris);
let state = iss_state();
let epoch = test_epoch();
let a_default = tb_default.acceleration(state.position(), Some(&epoch));
let a_trait = tb_trait.acceleration(state.position(), Some(&epoch));
assert_eq!(a_default, a_trait);
assert_eq!(tb_trait.name, "third_body_moon");
assert_eq!(tb_trait.mu_body, 4902.800066);
let _ = MeeusMoonEphemeris.velocity_eci(&epoch);
}
#[test]
fn moon_with_ephemeris_respects_custom_source() {
use arika::moon::MoonEphemeris;
struct FakeMoonEphem;
impl MoonEphemeris for FakeMoonEphem {
fn position_eci(&self, _epoch: &Epoch) -> Vec3<frame::Gcrs> {
Vec3::new(400_000.0, 0.0, 0.0)
}
fn name(&self) -> &str {
"fake"
}
}
let tb = ThirdBodyGravity::moon_with_ephemeris(FakeMoonEphem);
let state = iss_state();
let epoch = test_epoch();
let a = tb.acceleration(state.position(), Some(&epoch));
let r_body = vector![400_000.0_f64, 0.0, 0.0];
let r_sat_to_body = r_body - *state.position();
let d = r_sat_to_body.magnitude();
let r_body_mag = r_body.magnitude();
let expected = 4902.800066
* (r_sat_to_body / (d * d * d) - r_body / (r_body_mag * r_body_mag * r_body_mag));
let err = (a - expected).magnitude();
assert!(
err < 1e-15,
"Expected moon_with_ephemeris to use the fake source, err={err:e}"
);
}
#[test]
fn custom_third_body_closure_can_capture_state() {
let positions = vec![
Vec3::<frame::Gcrs>::new(1.0e6, 0.0, 0.0),
Vec3::<frame::Gcrs>::new(0.0, 1.0e6, 0.0),
Vec3::<frame::Gcrs>::new(0.0, 0.0, 1.0e6),
];
let tb = ThirdBodyGravity::custom("captured", 1.0e5, move |_epoch| positions[0]);
let state = iss_state();
let epoch = test_epoch();
let a = tb.acceleration(state.position(), Some(&epoch));
assert!(a.magnitude() > 0.0);
}
#[test]
fn geo_larger_perturbation_than_leo() {
let tb_moon = ThirdBodyGravity::moon();
let epoch = test_epoch();
let leo_state = iss_state();
let geo_r = 42164.0; let geo_v = (MU_EARTH / geo_r).sqrt();
let geo_state = OrbitalState::new(vector![geo_r, 0.0, 0.0], vector![0.0, geo_v, 0.0]);
let a_leo = tb_moon
.acceleration(leo_state.position(), Some(&epoch))
.magnitude();
let a_geo = tb_moon
.acceleration(geo_state.position(), Some(&epoch))
.magnitude();
let a_central_leo = MU_EARTH / leo_state.position().magnitude_squared();
let a_central_geo = MU_EARTH / geo_state.position().magnitude_squared();
let ratio_leo = a_leo / a_central_leo;
let ratio_geo = a_geo / a_central_geo;
assert!(
ratio_geo > ratio_leo,
"Moon perturbation ratio at GEO ({ratio_geo:.6e}) should be > LEO ({ratio_leo:.6e})"
);
}
}