use super::eval::{jd_tt_to_spice_et_seconds, DynSegmentStack};
use crate::coordinates::{
cartesian::{Position, Velocity},
centers::{Barycentric, Geocentric, Heliocentric},
frames::EclipticMeanJ2000,
transform::VectorAstroExt,
};
use crate::ephemeris::EphemerisError;
use crate::qtty::{AstronomicalUnit, Day, Kilometer, Per};
use crate::time::JulianDate;
use crate::archive::jpl::constants::EARTH_MOON_RATIO;
const FRAC_EARTH: f64 = EARTH_MOON_RATIO / (EARTH_MOON_RATIO + 1.0);
#[allow(dead_code)]
const FRAC_MOON: f64 = 1.0 / (EARTH_MOON_RATIO + 1.0);
const EARTH_OFFSET_FROM_MOON_OFF: f64 = 1.0 / EARTH_MOON_RATIO;
const MOON_GEO_FROM_MOON_OFF: f64 = 1.0 / FRAC_EARTH;
type AuPerDay = Per<AstronomicalUnit, Day>;
#[inline]
pub(crate) fn try_dyn_sun_barycentric(
jd: JulianDate,
sun: &DynSegmentStack,
) -> Result<Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let sun_icrf = sun.try_position_et(et)?;
let sun_ecl_au = sun_icrf
.to_frame::<EclipticMeanJ2000>(&crate::J2000)
.to_unit::<AstronomicalUnit>();
Ok(Position::new(
sun_ecl_au.x(),
sun_ecl_au.y(),
sun_ecl_au.z(),
))
}
#[inline]
pub(crate) fn dyn_sun_barycentric(
jd: JulianDate,
sun: &DynSegmentStack,
) -> Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> {
try_dyn_sun_barycentric(jd, sun).expect("runtime JPL Sun barycentric position unavailable")
}
#[inline]
pub(crate) fn try_dyn_earth_barycentric(
jd: JulianDate,
emb: &DynSegmentStack,
moon: &DynSegmentStack,
) -> Result<Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let emb_pos = emb.try_position_et(et)?;
let moon_off = moon.try_position_et(et)?;
let earth_icrf = emb_pos - moon_off.scale(EARTH_OFFSET_FROM_MOON_OFF);
let earth_ecl_au = earth_icrf
.to_frame::<EclipticMeanJ2000>(&crate::J2000)
.to_unit::<AstronomicalUnit>();
Ok(Position::new(
earth_ecl_au.x(),
earth_ecl_au.y(),
earth_ecl_au.z(),
))
}
#[inline]
pub(crate) fn try_dyn_earth_barycentric_direct(
jd: JulianDate,
emb: &DynSegmentStack,
earth: &DynSegmentStack,
) -> Result<Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let emb_pos = emb.try_position_et(et)?;
let earth_off = earth.try_position_et(et)?;
let earth_ecl_au = (emb_pos + earth_off)
.to_frame::<EclipticMeanJ2000>(&crate::J2000)
.to_unit::<AstronomicalUnit>();
Ok(Position::new(
earth_ecl_au.x(),
earth_ecl_au.y(),
earth_ecl_au.z(),
))
}
#[inline]
pub(crate) fn dyn_earth_barycentric(
jd: JulianDate,
emb: &DynSegmentStack,
moon: &DynSegmentStack,
) -> Position<Barycentric, EclipticMeanJ2000, AstronomicalUnit> {
try_dyn_earth_barycentric(jd, emb, moon)
.expect("runtime JPL Earth barycentric position unavailable")
}
#[inline]
pub(crate) fn try_dyn_earth_heliocentric(
jd: JulianDate,
sun: &DynSegmentStack,
emb: &DynSegmentStack,
moon: &DynSegmentStack,
) -> Result<Position<Heliocentric, EclipticMeanJ2000, AstronomicalUnit>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let emb_pos = emb.try_position_et(et)?;
let moon_off = moon.try_position_et(et)?;
let sun_pos = sun.try_position_et(et)?;
let earth_icrf = emb_pos - moon_off.scale(EARTH_OFFSET_FROM_MOON_OFF) - sun_pos;
let earth_ecl_au = earth_icrf
.to_frame::<EclipticMeanJ2000>(&crate::J2000)
.to_unit::<AstronomicalUnit>();
Ok(Position::new(
earth_ecl_au.x(),
earth_ecl_au.y(),
earth_ecl_au.z(),
))
}
#[inline]
pub(crate) fn try_dyn_earth_heliocentric_direct(
jd: JulianDate,
sun: &DynSegmentStack,
emb: &DynSegmentStack,
earth: &DynSegmentStack,
) -> Result<Position<Heliocentric, EclipticMeanJ2000, AstronomicalUnit>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let emb_pos = emb.try_position_et(et)?;
let earth_off = earth.try_position_et(et)?;
let sun_pos = sun.try_position_et(et)?;
let earth_ecl_au = (emb_pos + earth_off - sun_pos)
.to_frame::<EclipticMeanJ2000>(&crate::J2000)
.to_unit::<AstronomicalUnit>();
Ok(Position::new(
earth_ecl_au.x(),
earth_ecl_au.y(),
earth_ecl_au.z(),
))
}
#[inline]
pub(crate) fn dyn_earth_heliocentric(
jd: JulianDate,
sun: &DynSegmentStack,
emb: &DynSegmentStack,
moon: &DynSegmentStack,
) -> Position<Heliocentric, EclipticMeanJ2000, AstronomicalUnit> {
try_dyn_earth_heliocentric(jd, sun, emb, moon)
.expect("runtime JPL Earth heliocentric position unavailable")
}
#[inline]
pub(crate) fn try_dyn_earth_barycentric_velocity(
jd: JulianDate,
emb: &DynSegmentStack,
moon: &DynSegmentStack,
) -> Result<Velocity<EclipticMeanJ2000, AuPerDay>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let v_emb = emb.try_velocity_et(et)?;
let v_moon_off = moon.try_velocity_et(et)?;
let v_earth_icrf = v_emb - v_moon_off.scale(EARTH_OFFSET_FROM_MOON_OFF);
Ok(v_earth_icrf
.to_frame::<EclipticMeanJ2000>(&crate::J2000)
.to_unit::<AuPerDay>())
}
#[inline]
pub(crate) fn try_dyn_earth_barycentric_velocity_direct(
jd: JulianDate,
emb: &DynSegmentStack,
earth: &DynSegmentStack,
) -> Result<Velocity<EclipticMeanJ2000, AuPerDay>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let v_emb = emb.try_velocity_et(et)?;
let v_earth_off = earth.try_velocity_et(et)?;
Ok((v_emb + v_earth_off)
.to_frame::<EclipticMeanJ2000>(&crate::J2000)
.to_unit::<AuPerDay>())
}
#[inline]
pub(crate) fn dyn_earth_barycentric_velocity(
jd: JulianDate,
emb: &DynSegmentStack,
moon: &DynSegmentStack,
) -> Velocity<EclipticMeanJ2000, AuPerDay> {
try_dyn_earth_barycentric_velocity(jd, emb, moon)
.expect("runtime JPL Earth barycentric velocity unavailable")
}
#[inline]
pub(crate) fn try_dyn_moon_geocentric(
jd: JulianDate,
moon: &DynSegmentStack,
) -> Result<Position<Geocentric, EclipticMeanJ2000, Kilometer>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let moon_off = moon.try_position_et(et)?;
let moon_geo_icrf = moon_off.scale(MOON_GEO_FROM_MOON_OFF);
let moon_geo_ecl = moon_geo_icrf.to_frame::<EclipticMeanJ2000>(&crate::J2000);
Ok(Position::new(
moon_geo_ecl.x(),
moon_geo_ecl.y(),
moon_geo_ecl.z(),
))
}
#[inline]
pub(crate) fn try_dyn_moon_geocentric_direct(
jd: JulianDate,
moon: &DynSegmentStack,
earth: &DynSegmentStack,
) -> Result<Position<Geocentric, EclipticMeanJ2000, Kilometer>, EphemerisError> {
let et = jd_tt_to_spice_et_seconds(jd);
let moon_off = moon.try_position_et(et)?;
let earth_off = earth.try_position_et(et)?;
let moon_geo_ecl = (moon_off - earth_off).to_frame::<EclipticMeanJ2000>(&crate::J2000);
Ok(Position::new(
moon_geo_ecl.x(),
moon_geo_ecl.y(),
moon_geo_ecl.z(),
))
}
#[inline]
pub(crate) fn dyn_moon_geocentric(
jd: JulianDate,
moon: &DynSegmentStack,
) -> Position<Geocentric, EclipticMeanJ2000, Kilometer> {
try_dyn_moon_geocentric(jd, moon).expect("runtime JPL Moon geocentric position unavailable")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ephemeris::jpl::eval::{DynSegmentDescriptor, DynSegmentStack};
const SECONDS_PER_DAY: f64 = crate::qtty::time::SECONDS_PER_DAY;
const JD_J2000: f64 = tempoch::J2000_JD_TT_DAY.value();
fn make_seg(x_km: f64, y_km: f64, z_km: f64) -> DynSegmentStack {
use crate::qtty::Seconds;
let ncoeff = 2usize;
let rsize = 2 + 3 * ncoeff; let intlen_secs = 1000.0 * SECONDS_PER_DAY;
let mid = intlen_secs / 2.0;
let radius = intlen_secs / 2.0;
let data = vec![mid, radius, x_km, 0.0, y_km, 0.0, z_km, 0.0];
let descriptor = DynSegmentDescriptor {
data_type: 2,
init: Seconds::new(0.0),
intlen: Seconds::new(intlen_secs),
ncoeff,
rsize,
n_records: 1,
data,
};
DynSegmentStack::single(descriptor, 0.0, intlen_secs)
}
fn jd_test() -> JulianDate {
crate::time::JulianDate::new(JD_J2000 + 500.0)
}
#[test]
fn frac_earth_plus_frac_moon_equals_one() {
assert!((FRAC_EARTH + FRAC_MOON - 1.0).abs() < 1e-12);
}
#[test]
fn frac_earth_is_dominant() {
const {
assert!(FRAC_EARTH > 0.98);
}
const {
assert!(FRAC_MOON < 0.02);
}
}
#[test]
fn dyn_sun_barycentric_is_finite() {
let sun = make_seg(1.0e8, 2.0e7, 3.0e6); let pos = dyn_sun_barycentric(jd_test(), &sun);
assert!(pos.x().is_finite());
assert!(pos.y().is_finite());
assert!(pos.z().is_finite());
}
#[test]
fn dyn_earth_barycentric_is_finite() {
let emb = make_seg(1.5e8, 0.0, 0.0);
let moon = make_seg(3.84e5, 0.0, 0.0);
let pos = dyn_earth_barycentric(jd_test(), &emb, &moon);
assert!(pos.x().is_finite());
assert!(pos.y().is_finite());
assert!(pos.z().is_finite());
}
#[test]
fn dyn_earth_heliocentric_is_finite() {
let sun = make_seg(1.0e8, 0.0, 0.0);
let emb = make_seg(1.5e8, 0.0, 0.0);
let moon = make_seg(3.84e5, 0.0, 0.0);
let pos = dyn_earth_heliocentric(jd_test(), &sun, &emb, &moon);
assert!(pos.x().is_finite());
assert!(pos.y().is_finite());
assert!(pos.z().is_finite());
}
#[test]
fn dyn_earth_barycentric_velocity_is_finite() {
let emb = make_seg(1.5e8, 0.0, 0.0);
let moon = make_seg(3.84e5, 0.0, 0.0);
let vel = dyn_earth_barycentric_velocity(jd_test(), &emb, &moon);
assert!(vel.x().is_finite());
assert!(vel.y().is_finite());
assert!(vel.z().is_finite());
}
#[test]
fn dyn_moon_geocentric_is_finite() {
let moon = make_seg(3.84e5, 1.0e4, 2.0e3);
let pos = dyn_moon_geocentric(jd_test(), &moon);
assert!(pos.x().is_finite());
assert!(pos.y().is_finite());
assert!(pos.z().is_finite());
}
#[test]
fn dyn_moon_geocentric_scaled_by_inv_frac_earth() {
let r_km = 384400.0;
let moon = make_seg(r_km, 0.0, 0.0);
let pos = dyn_moon_geocentric(jd_test(), &moon);
let magnitude =
(pos.x().value().powi(2) + pos.y().value().powi(2) + pos.z().value().powi(2)).sqrt();
let expected = r_km / FRAC_EARTH;
assert!(
(magnitude - expected).abs() / expected < 1e-2,
"magnitude={magnitude}, expected={expected}"
);
assert!(
magnitude > r_km,
"magnitude={magnitude} must exceed r_km={r_km}"
);
}
#[test]
fn dyn_earth_barycentric_uses_one_over_emrat_offset() {
let emb = make_seg(1.5e8, 0.0, 0.0);
let r_km = 384400.0;
let moon = make_seg(r_km, 0.0, 0.0);
let pos = dyn_earth_barycentric(jd_test(), &emb, &moon);
let mag_au =
(pos.x().value().powi(2) + pos.y().value().powi(2) + pos.z().value().powi(2)).sqrt();
let earth_km = 1.5e8 - r_km / EARTH_MOON_RATIO;
let expected_au = earth_km / 1.495_978_707e8;
assert!(
(mag_au - expected_au).abs() / expected_au < 1e-6,
"mag_au={mag_au}, expected={expected_au}"
);
}
#[test]
fn dyn_sun_barycentric_small_offset_from_origin() {
let sun = make_seg(5.0e5, 3.0e5, 1.0e5); let pos = dyn_sun_barycentric(jd_test(), &sun);
let mag_au =
(pos.x().value().powi(2) + pos.y().value().powi(2) + pos.z().value().powi(2)).sqrt();
assert!(mag_au < 0.1, "unexpected large Sun-SSB offset: {mag_au} AU");
}
}