use crate::ephem::{moon_position, sun_position};
use crate::forces::{srp_accel, third_body_accel, MU_EARTH, MU_SUN};
use crate::gravity_sh::SphericalHarmonicField;
use crate::lunar_frame::icrf_to_moon_pa;
use crate::precession::{julian_centuries_tt, mat_vec, mod_to_gcrs, transpose};
use crate::precise_od::{empirical_accel, EmpiricalAccel, ForceModel};
use crate::timescales::SECONDS_PER_DAY;
type Vec3 = [f64; 3];
type Mat3 = [[f64; 3]; 3];
pub trait LunarEnvironment: Clone + std::fmt::Debug {
fn icrf_to_moon_pa(&self, jd_tdb: f64) -> Mat3;
fn geocentric_sun_moon(&self, jd_tdb: f64) -> (Vec3, Vec3);
}
#[derive(Clone, Copy, Debug, Default)]
pub struct AnalyticLunarEnvironment;
impl LunarEnvironment for AnalyticLunarEnvironment {
fn icrf_to_moon_pa(&self, jd_tdb: f64) -> Mat3 {
icrf_to_moon_pa(jd_tdb)
}
fn geocentric_sun_moon(&self, jd: f64) -> (Vec3, Vec3) {
let tjc = julian_centuries_tt(jd);
(
mod_to_gcrs(sun_position(tjc), jd),
mod_to_gcrs(moon_position(tjc), jd),
)
}
}
#[derive(Clone, Debug)]
pub struct LunarForceModel<E: LunarEnvironment = AnalyticLunarEnvironment> {
pub field: SphericalHarmonicField,
pub epoch_jd_tdb: f64,
pub earth: bool,
pub sun: bool,
pub srp: bool,
pub cr: f64,
pub area_over_mass: f64,
pub empirical: Option<EmpiricalAccel>,
pub env: E,
}
impl LunarForceModel<AnalyticLunarEnvironment> {
pub fn new(field: SphericalHarmonicField, epoch_jd_tdb: f64) -> Self {
Self::with_env(field, epoch_jd_tdb, AnalyticLunarEnvironment)
}
}
impl<E: LunarEnvironment> LunarForceModel<E> {
pub fn with_env(field: SphericalHarmonicField, epoch_jd_tdb: f64, env: E) -> Self {
Self {
field,
epoch_jd_tdb,
earth: true,
sun: true,
srp: false,
cr: 1.0,
area_over_mass: 0.0,
empirical: None,
env,
}
}
pub fn solar_radiation(mut self, cr: f64, area_over_mass: f64) -> Self {
self.srp = true;
self.cr = cr;
self.area_over_mass = area_over_mass;
self
}
}
impl<E: LunarEnvironment> ForceModel for LunarForceModel<E> {
fn accel_rv(&self, t: f64, r: Vec3, v: Vec3) -> Vec3 {
let jd = self.epoch_jd_tdb + t / SECONDS_PER_DAY;
let m = self.env.icrf_to_moon_pa(jd);
let r_bf = mat_vec(&m, r);
let a_bf = self.field.acceleration(r_bf);
let mut a = mat_vec(&transpose(&m), a_bf);
let mut add = |p: Vec3| {
a = [a[0] + p[0], a[1] + p[1], a[2] + p[2]];
};
let need_sun = self.sun || self.srp;
if self.earth || need_sun {
let (sun_geo, moon_geo) = self.env.geocentric_sun_moon(jd);
if self.earth {
let earth_wrt_moon = [-moon_geo[0], -moon_geo[1], -moon_geo[2]];
add(third_body_accel(r, earth_wrt_moon, MU_EARTH));
}
if need_sun {
let sun_wrt_moon = [
sun_geo[0] - moon_geo[0],
sun_geo[1] - moon_geo[1],
sun_geo[2] - moon_geo[2],
];
if self.sun {
add(third_body_accel(r, sun_wrt_moon, MU_SUN));
}
if self.srp {
add(srp_accel(r, sun_wrt_moon, self.cr, self.area_over_mass));
}
}
}
if let Some(emp) = self.empirical {
add(empirical_accel(&emp, r, v));
}
a
}
fn cr(&self) -> f64 {
self.cr
}
fn set_cr(&mut self, cr: f64) {
self.cr = cr;
}
fn set_empirical(&mut self, empirical: Option<EmpiricalAccel>) {
self.empirical = empirical;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lunar::{MOON_GM_M3_S2, R_MOON_M};
fn norm(v: Vec3) -> f64 {
(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt()
}
fn synthetic_moon_field() -> SphericalHarmonicField {
let mut f = SphericalHarmonicField::zeros(MOON_GM_M3_S2, R_MOON_M, 2);
f.set(0, 0, 1.0, 0.0);
f.set(2, 0, -9.088_292_365e-5, 0.0); f.set(2, 2, 3.467_094_427e-5, -2.406_424_452e-10); f
}
fn lro_state() -> (Vec3, Vec3) {
let r = [1.50e6, 0.70e6, 0.55e6]; let v = [-0.55e3, 0.40e3, 1.50e3];
(r, v)
}
#[test]
fn lunar_force_is_gravity_dominated() {
let fm = LunarForceModel::new(synthetic_moon_field(), 2_459_580.5);
let (r, v) = lro_state();
let a = norm(fm.accel_rv(0.0, r, v));
let central = MOON_GM_M3_S2 / norm(r).powi(2);
assert!(
(a - central).abs() / central < 0.02,
"|a| {a} vs central {central} (>2% off — gravity should dominate)"
);
}
#[test]
fn earth_third_body_is_the_dominant_perturbation() {
let (r, v) = lro_state();
let base = LunarForceModel {
earth: false,
sun: false,
..LunarForceModel::new(synthetic_moon_field(), 2_459_580.5)
};
let with_earth = LunarForceModel {
earth: true,
..base.clone()
};
let d = norm([
with_earth.accel_rv(0.0, r, v)[0] - base.accel_rv(0.0, r, v)[0],
with_earth.accel_rv(0.0, r, v)[1] - base.accel_rv(0.0, r, v)[1],
with_earth.accel_rv(0.0, r, v)[2] - base.accel_rv(0.0, r, v)[2],
]);
assert!(
(5e-6..1e-4).contains(&d),
"Earth third-body magnitude {d} m/s² off the ~3e-5 band"
);
}
#[test]
fn body_fixed_field_rotates_with_the_moon() {
let grav_only = LunarForceModel {
earth: false,
sun: false,
..LunarForceModel::new(synthetic_moon_field(), 2_459_580.5)
};
let (r, v) = lro_state();
let a0 = grav_only.accel_rv(0.0, r, v);
let a5 = grav_only.accel_rv(5.0 * SECONDS_PER_DAY, r, v);
let d = norm([a5[0] - a0[0], a5[1] - a0[1], a5[2] - a0[2]]);
assert!(
(1e-7..1e-3).contains(&d),
"body-fixed C22 reorientation over 5 d changed accel by {d} m/s² (expected a real, \
bounded change — 0 would mean the lunar rotation was not applied)"
);
}
#[test]
fn empirical_tier_adds_its_rtn_acceleration() {
let (r, v) = lro_state();
let base = LunarForceModel {
earth: false,
sun: false,
..LunarForceModel::new(synthetic_moon_field(), 2_459_580.5)
};
let amp = 1.0e-6;
let withe = LunarForceModel {
empirical: Some(EmpiricalAccel {
radial: [amp, 0.0, 0.0],
..Default::default()
}),
..base.clone()
};
let d = norm([
withe.accel_rv(0.0, r, v)[0] - base.accel_rv(0.0, r, v)[0],
withe.accel_rv(0.0, r, v)[1] - base.accel_rv(0.0, r, v)[1],
withe.accel_rv(0.0, r, v)[2] - base.accel_rv(0.0, r, v)[2],
]);
assert!(
(d - amp).abs() / amp < 1e-6,
"empirical radial Δ {d} vs {amp}"
);
}
#[test]
fn custom_environment_provider_drives_the_force_model() {
#[derive(Clone, Debug)]
struct ShiftedEnv;
impl LunarEnvironment for ShiftedEnv {
fn icrf_to_moon_pa(&self, jd: f64) -> Mat3 {
let base = crate::lunar_frame::icrf_to_moon_pa(jd);
let (c, s) = (10.0_f64.to_radians().cos(), 10.0_f64.to_radians().sin());
let rz = [[c, s, 0.0], [-s, c, 0.0], [0.0, 0.0, 1.0]];
let mut out = [[0.0; 3]; 3];
for (i, row) in out.iter_mut().enumerate() {
for (j, e) in row.iter_mut().enumerate() {
*e = (0..3).map(|k| rz[i][k] * base[k][j]).sum();
}
}
out
}
fn geocentric_sun_moon(&self, jd: f64) -> (Vec3, Vec3) {
AnalyticLunarEnvironment.geocentric_sun_moon(jd)
}
}
let (r, v) = lro_state();
let analytic = LunarForceModel {
earth: false,
sun: false,
..LunarForceModel::new(synthetic_moon_field(), 2_459_580.5)
};
let shifted = LunarForceModel {
earth: false,
sun: false,
..LunarForceModel::with_env(synthetic_moon_field(), 2_459_580.5, ShiftedEnv)
};
let a0 = analytic.accel_rv(0.0, r, v);
let a1 = shifted.accel_rv(0.0, r, v);
let d = norm([a1[0] - a0[0], a1[1] - a0[1], a1[2] - a0[2]]);
assert!(
d > 1e-9,
"custom orientation provider changed the body-fixed gravity accel by {d} m/s² \
(0 would mean the LunarEnvironment seam is not consumed by accel_rv)"
);
}
}