astrodyn 0.2.0

Gateway to the astrodyn orbital-dynamics framework — a pure-Rust, engine-agnostic port of NASA JEOD (spherical-harmonics gravity, RNP Earth rotation, atmosphere, drag/SRP, multi-body dynamics) composing the astrodyn_* physics crates into one pipeline API any host can drive
Documentation
//! Planetary ephemeris recipes backed by JPL kernel assets.
//!
//! Each recipe pulls a kernel via [`astrodyn_ephemeris::data::load`] and
//! returns an [`Ephemeris`] ready to plug into a
//! [`SimulationBuilder`](crate::SimulationBuilder) via `.ephemeris(...)`.
//! Kernels resolve from the in-workspace `assets/` dir during dev/test
//! and from the project's `kernels-v1` GitHub Release (cached in
//! `~/.cache/astrodyn-ephemeris/`) for downstream consumers. See the
//! [`astrodyn_ephemeris::data`] module docs for the full lookup order
//! and offline-build instructions.
//!
//! ```ignore
//! use astrodyn::recipes::ephemeris;
//! let eph = ephemeris::de421()?;
//! # Ok::<(), astrodyn::EphemerisError>(())
//! ```

use crate::{Ephemeris, EphemerisError};

/// JPL DE421 planetary ephemeris (Sun, Moon, planets, 1900–2050).
///
/// Equivalent to `Ephemeris::from_bsp("de421.bsp")` against the JEOD-
/// vendored kernel.
pub fn de421() -> Result<Ephemeris, EphemerisError> {
    let bytes = astrodyn_ephemeris::data::load(&astrodyn_ephemeris::data::DE421)?;
    Ephemeris::from_bsp_bytes(&bytes)
}

/// DE421 ephemeris plus the Moon principal-axes orientation kernel.
///
/// Use this when the simulation needs the Moon's body-fixed attitude
/// (libration) — e.g., lunar-fixed frames, lunar-surface targeting, or
/// torque computations against the Moon. The plain [`de421`] recipe
/// suffices when only Moon position/velocity are needed.
pub fn de421_with_moon_pa() -> Result<Ephemeris, EphemerisError> {
    let mut eph = de421()?;
    let bpc = astrodyn_ephemeris::data::load(&astrodyn_ephemeris::data::MOON_PA)?;
    eph.load_bpc_bytes(&bpc)?;
    Ok(eph)
}

/// DE421 ephemeris plus the Moon principal-axes BPC **and** the PA→ME frame
/// kernel, so the Moon mean-Earth/mean-rotation (ME) frame resolves.
///
/// Use this for lunar **cartography** — DEMs (LOLA, SLDEM2015) and all lunar
/// map lat/lon are referenced to ME, which differs from the principal-axes
/// (PA) frame by ~tens of arcseconds (≈0.5 km on the surface). Query it via
/// [`Ephemeris::get_body_rotation_to`] with
/// [`BodyFixedFrame::MoonMeDe421`](astrodyn_ephemeris::BodyFixedFrame::MoonMeDe421).
/// Use [`de421_with_moon_pa`] instead when you want the gravity/libration PA
/// frame rather than the cartographic ME frame.
pub fn de421_with_moon_me() -> Result<Ephemeris, EphemerisError> {
    let mut eph = de421_with_moon_pa()?;
    let fk = astrodyn_ephemeris::data::load(&astrodyn_ephemeris::data::MOON_FK)?;
    eph.load_euler_bytes(&fk)?;
    Ok(eph)
}

/// DE421 ephemeris plus the IAU planetary-constants kernel, so body-fixed
/// IAU rotations resolve for the planets.
///
/// Use this whenever a consumer needs an IAU body-fixed orientation (a ground
/// track or rotating texture) for any body — Jupiter, Venus, the Moon, Mars,
/// …: every [`BodyFixedFrame::Iau`](astrodyn_ephemeris::BodyFixedFrame::Iau)
/// query resolves from this planetary-constants kernel. The IAU frames use the
/// ANISE IAU 2015 model (NAIF `pck00011`).
pub fn de421_with_iau() -> Result<Ephemeris, EphemerisError> {
    let mut eph = de421()?;
    let pca = astrodyn_ephemeris::data::load(&astrodyn_ephemeris::data::PCK11)?;
    eph.load_pca_bytes(&pca)?;
    Ok(eph)
}

/// JPL DE440 planetary ephemeris (Sun, Moon, planets, 1849–2150).
///
/// Required by the NASA NESC GN&C Lunar Check Cases (NESC-RP-23-01853);
/// CC8 in particular pins its reference trajectory to this generation.
/// We ship the `de440s` short-subset (~32 MB) — sufficient for the
/// 2026-class epochs we currently target and two orders of magnitude
/// smaller than the full DE440 archive.
pub fn de440() -> Result<Ephemeris, EphemerisError> {
    let bytes = astrodyn_ephemeris::data::load(&astrodyn_ephemeris::data::DE440)?;
    Ephemeris::from_bsp_bytes(&bytes)
}

/// DE440 ephemeris plus the Moon principal-axes orientation kernel.
///
/// Use this when the simulation needs the Moon's body-fixed attitude
/// (libration) — e.g., lunar-fixed frames, lunar-surface targeting, or
/// torque computations against the Moon. The plain [`de440`] recipe
/// suffices when only Moon position/velocity are needed.
///
/// The bundled BPC kernel is `moon_pa_de421_1900-2050.bpc` (the same
/// kernel [`de421_with_moon_pa`] uses). Mixing a DE440 BSP with a DE421
/// BPC introduces a small inconsistency in the Moon's libration model
/// (sub-arcsecond level over a few-day propagation), which is acceptable
/// for the NESC CC8 NRHO use case but may be tightened later by
/// switching to the DE440-aligned `moon_pa_de440_*.bpc` kernel.
pub fn de440_with_moon_pa() -> Result<Ephemeris, EphemerisError> {
    let mut eph = de440()?;
    let bpc = astrodyn_ephemeris::data::load(&astrodyn_ephemeris::data::MOON_PA)?;
    eph.load_bpc_bytes(&bpc)?;
    Ok(eph)
}

#[cfg(test)]
mod tests {
    use crate::{BodyFixedFrame, EphemerisBody, Moon};

    // The `de421_with_moon_me` recipe resolves the J2000→ME rotation through
    // the facade, and ME is genuinely distinct from PA (the lunar libration
    // offset), proving the bundled FK loaded and chained onto the PA BPC.
    #[test]
    fn de421_with_moon_me_resolves_me_distinct_from_pa() {
        let eph = super::de421_with_moon_me().expect("load DE421 + Moon PA + ME FK");
        let pa = eph
            .get_body_rotation_to_jd::<Moon>(
                EphemerisBody::Moon,
                BodyFixedFrame::MoonPaDe421,
                2_451_545.0,
            )
            .expect("PA")
            .matrix();
        let me = eph
            .get_body_rotation_to_jd::<Moon>(
                EphemerisBody::Moon,
                BodyFixedFrame::MoonMeDe421,
                2_451_545.0,
            )
            .expect("ME")
            .matrix();
        // The two frames must differ (PA→ME offset is ~104 arcsec ≈ 5e-4 rad).
        let max_diff = (pa - me)
            .to_cols_array()
            .iter()
            .fold(0.0_f64, |m, &x| m.max(x.abs()));
        assert!(
            max_diff > 1e-5,
            "ME must differ from PA by the libration offset; max element diff = {max_diff:e}",
        );
    }

    // The `de421_with_iau` recipe resolves a planet IAU rotation through the
    // facade (a valid, non-identity orientation), proving the PCA loaded.
    #[test]
    fn de421_with_iau_resolves_jupiter_rotation() {
        use crate::Jupiter;

        let eph = super::de421_with_iau().expect("load DE421 + IAU PCA");
        let m = eph
            .get_body_rotation_to_jd::<Jupiter>(
                EphemerisBody::Jupiter,
                BodyFixedFrame::Iau,
                2_451_545.0,
            )
            .expect("Jupiter IAU rotation")
            .matrix();
        // Jupiter's prime meridian has rotated far from J2000 ICRF: the matrix
        // must be a real rotation, not identity.
        let off_identity = (m - glam::DMat3::IDENTITY)
            .to_cols_array()
            .iter()
            .fold(0.0_f64, |acc, &x| acc.max(x.abs()));
        assert!(
            off_identity > 0.1,
            "Jupiter IAU orientation must be a real rotation, not identity (got {off_identity:e})",
        );
    }
}