Skip to content

Reference Frame Router

CelestialFrame is a single enum spanning every reference frame in Brahe — Earth-centered, lunar, Martian, barycentric, and generic NAIF-ID variants — and three router functions (rotation_frame_to_frame, position_frame_to_frame, state_frame_to_frame) that convert between any two of them. The router is the frame machinery underlying multibody numerical propagation; see Cislunar and Lunar Propagation and Propagation Around Other Central Bodies for the propagation side.

Available Frames

The transformation source column names how each frame's orientation is realized: native frames are computed from models compiled into Brahe (no kernel), SPICE frames are evaluated from a loaded NAIF kernel. Re-centering between frames with different origins is a separate step resolved through the loaded SPK kernels in the global registry (last-loaded-wins for overlapping segments); the default de440s ephemeris is loaded automatically when no SPK is resident.

Frame Kind NAIF ID (If Any) Transformation source
GCRF Inertial 399 ICRF-aligned identity
ITRF Earth-fixed 399 IAU 2006/2000A precession-nutation with EOP (native, model selectable)
EME2000 Inertial 399 Frame bias (native)
MOD Inertial 399 IAU 2006 bias-precession (native, model selectable)
TOD Inertial 399 IAU 2006/2000A bias-precession-nutation with EOP (native, model selectable)
TEME Inertial 399 GMST 1982 anchored true equator, mean equinox of date (native)
Centered(center, TODofEpoch(epoch)) Inertial center TOD rotation frozen at epoch
Centered(center, TEMEofEpoch(epoch)) Inertial center TEME rotation frozen at epoch
LCI Inertial 301 ICRF-aligned identity
LFPA Moon-fixed 301 DE440 binary PCK (SPICE)
LFME Moon-fixed 301 DE440 PA + constant PA→ME rotation (native)
MCI Inertial 499 ICRF-aligned identity
MCMF Mars-fixed 499 IAU/WGCCRE analytic (native)
EMBI Inertial 3 ICRF-aligned identity
SSBI Inertial 0 ICRF-aligned identity
EMR Rotating 3 SPK-derived, analytic (native)
SER Rotating — (synthetic barycenter) SPK-derived, analytic (native)
GSE Rotating 399 SPK-derived, analytic (native)
BodyCenteredICRF(naif_id) Inertial naif_id ICRF-aligned identity
BodyFixedIAU(naif_id) Body-fixed naif_id IAU/WGCCRE analytic (native)
BodyFixedPCK(center, frame_id) Body-fixed center Loaded binary PCK (SPICE)
BodyFixedCustom(center, key) Body-fixed center User rotation callback (native)

The NAIF ID column gives the NAIF integer ID of the body each frame is centered on; it is the ID used to resolve the frame's origin through the SPK kernels when re-centering. For BodyFixedCustom, center is that same center NAIF ID, while key is an integer handle naming a rotation callback function previously registered with register_custom_frame — see Generic NAIF-ID Variants below.

See Lunar Reference Frames and Mars Reference Frames for LCI/LFPA/LFME and MCI/MCMF respectively, and Synodic Reference Frames for EMR/SER/GSE.

Router Functions

rotation_frame_to_frame, position_frame_to_frame, and state_frame_to_frame take a from frame, a to frame, and an epoch (plus a position/state vector for the latter two), and dispatch through a hub-and-spoke design: the source is rotated into ICRF axes centered on its own origin, re-centered to the target's origin if the two frames have different centers, and rotated into the target's axes. Same-frame calls short-circuit to an identity/no-op with no SPK query at all; same-center calls (e.g. GCRF ↔ ITRF, LCI ↔ LFPA) skip the re-centering step and are bit-identical to the underlying pairwise function.

The example below routes a GCRF (Earth-centered) state into LCI (Moon-centered) with state_frame_to_frame — a cross-center conversion that re-centers through the DE440 ephemeris — and takes a rotation-only GCRF → MCMF transform with rotation_frame_to_frame, which uses the compiled-in Mars model and needs no kernel.

import numpy as np

import brahe as bh

# Initialize EOP and the DE440s ephemeris used to re-center GCRF (Earth) to
# LCI (Moon) inside the router.
bh.initialize_eop()
bh.load_common_spice_kernels()

epc = bh.Epoch.from_datetime(2024, 3, 1, 0, 0, 0.0, 0.0, bh.TimeSystem.UTC)

# A GCRF (Earth-centered inertial) state, built from Keplerian elements.
oe = np.array([bh.R_EARTH + 500e3, 0.01, 45.0, 15.0, 30.0, 45.0])
x_gcrf = bh.state_koe_to_eci(oe, bh.AngleFormat.DEGREES)

# state_frame_to_frame routes through ICRF and re-centers Earth -> Moon, so the
# same physical state is now expressed relative to the Moon.
x_lci = bh.state_frame_to_frame(
    bh.CelestialFrame.GCRF, bh.CelestialFrame.LCI, epc, x_gcrf
)

# rotation_frame_to_frame returns only the 3x3 axis rotation (no re-centering);
# GCRF -> MCMF uses the compiled-in WGCCRE Mars model and needs no kernel.
r_gcrf_to_mcmf = bh.rotation_frame_to_frame(
    bh.CelestialFrame.GCRF, bh.CelestialFrame.MCMF, epc
)

print(f"Epoch: {epc}")
print("\nGCRF state (Earth-centered inertial):")
print(
    f"  Position (km): [{x_gcrf[0] / 1e3:.3f}, {x_gcrf[1] / 1e3:.3f}, {x_gcrf[2] / 1e3:.3f}]"
)
print("\nSame state expressed in LCI (Moon-centered inertial):")
print(
    f"  Position (km): [{x_lci[0] / 1e3:.3f}, {x_lci[1] / 1e3:.3f}, {x_lci[2] / 1e3:.3f}]"
)

# The GCRF->LCI position shift equals the Moon's distance from Earth (~384,400 km),
# since both frames share ICRF axes and differ only in origin.
offset = np.linalg.norm(x_lci[:3] - x_gcrf[:3])
print(f"\nGCRF->LCI position offset (km): {offset / 1e3:.1f}")
assert 350_000e3 < offset < 410_000e3

# The returned rotation is a proper orthonormal direction cosine matrix.
assert np.allclose(r_gcrf_to_mcmf @ r_gcrf_to_mcmf.T, np.eye(3), atol=1e-9)
print("\nExample validated successfully!")
use brahe as bh;
use nalgebra as na;

fn main() {
    // Initialize EOP and the DE440s ephemeris used to re-center GCRF (Earth)
    // to LCI (Moon) inside the router.
    bh::initialize_eop().unwrap();
    bh::load_common_spice_kernels().unwrap();

    let epc = bh::Epoch::from_datetime(2024, 3, 1, 0, 0, 0.0, 0.0, bh::TimeSystem::UTC);

    // A GCRF (Earth-centered inertial) state, built from Keplerian elements.
    let oe = na::SVector::<f64, 6>::new(bh::R_EARTH + 500e3, 0.01, 45.0, 15.0, 30.0, 45.0);
    let x_gcrf = bh::state_koe_to_eci(oe, bh::AngleFormat::Degrees);

    // state_frame_to_frame routes through ICRF and re-centers Earth -> Moon, so
    // the same physical state is now expressed relative to the Moon.
    let x_lci =
        bh::state_frame_to_frame(bh::CelestialFrame::GCRF, bh::CelestialFrame::LCI, epc, x_gcrf)
            .unwrap();

    // rotation_frame_to_frame returns only the 3x3 axis rotation (no
    // re-centering); GCRF -> MCMF uses the compiled-in WGCCRE Mars model and
    // needs no kernel.
    let r_gcrf_to_mcmf =
        bh::rotation_frame_to_frame(bh::CelestialFrame::GCRF, bh::CelestialFrame::MCMF, epc)
            .unwrap();

    println!("Epoch: {}", epc);
    println!("\nGCRF state (Earth-centered inertial):");
    println!(
        "  Position (km): [{:.3}, {:.3}, {:.3}]",
        x_gcrf[0] / 1e3,
        x_gcrf[1] / 1e3,
        x_gcrf[2] / 1e3
    );
    println!("\nSame state expressed in LCI (Moon-centered inertial):");
    println!(
        "  Position (km): [{:.3}, {:.3}, {:.3}]",
        x_lci[0] / 1e3,
        x_lci[1] / 1e3,
        x_lci[2] / 1e3
    );

    // The GCRF->LCI position shift equals the Moon's distance from Earth
    // (~384,400 km), since both frames share ICRF axes and differ only in origin.
    let offset = (x_lci.fixed_rows::<3>(0) - x_gcrf.fixed_rows::<3>(0)).norm();
    println!("\nGCRF->LCI position offset (km): {:.1}", offset / 1e3);
    assert!(offset > 350_000e3 && offset < 410_000e3);

    // The returned rotation is a proper orthonormal direction cosine matrix.
    let m = r_gcrf_to_mcmf;
    assert!((m * m.transpose() - na::Matrix3::identity()).norm() < 1e-9);
    println!("\nExample validated successfully!");
}
Output
Epoch: 2024-03-01 00:00:00.000 UTC

GCRF state (Earth-centered inertial):
  Position (km): [404.522, 4955.794, 4682.232]

Same state expressed in LCI (Moon-centered inertial):
  Position (km): [305137.940, 234784.024, 120741.751]

GCRF->LCI position offset (km): 398940.2

Example validated successfully!
Epoch: 2024-03-01 00:00:00.000 UTC

GCRF state (Earth-centered inertial):
  Position (km): [404.522, 4955.794, 4682.232]

Same state expressed in LCI (Moon-centered inertial):
  Position (km): [305137.940, 234784.024, 120741.751]

GCRF->LCI position offset (km): 398940.2

Example validated successfully!

Generic NAIF-ID Variants

Four variants cover bodies without a dedicated named frame:

  • BodyCenteredICRF(naif_id): ICRF-aligned axes centered on naif_id. Requires an SPK segment for naif_id when translating to/from a differently-centered frame.
  • BodyFixedIAU(naif_id): the compiled-in IAU/WGCCRE body-fixed rotation for naif_id, centered on naif_id itself. Requires no kernel. iau_rotation_model_ids() returns the supported NAIF IDs: the Sun, Mercury, Venus, the Moon (a lower-precision IAU model, distinct from LFPA), Mars, the Galilean moons (Io/Europa/Ganymede/Callisto), Phobos, Deimos, Enceladus, Titan, Jupiter, Saturn, Uranus, and Neptune.
  • BodyFixedPCK { center, frame_id }: a body-fixed frame evaluated from a loaded binary PCK's frame_id (e.g. 31008 for MOON_PA_DE440), centered on center. The PCK must already be loaded — the router never auto-loads a generic PCK.
  • BodyFixedCustom { center, key }: a body-fixed frame evaluated from a user-supplied rotation callback. center is the NAIF ID of the body the frame is centered on (used for SPK re-centering, exactly like the other variants). key is an arbitrary integer handle, unrelated to NAIF IDs: it names a callback function previously registered with register_custom_frame(key, rotation, omega=None), and the router looks the callback up by key at evaluation time. rotation is a proper callback function mapping an epoch to the 3×3 ICRF→body-fixed rotation matrix; omega is an optional second callback mapping an epoch to the frame's angular velocity vector (rad/s), which supplies the velocity transport term (derived numerically by central differencing of rotation when omitted). This enables support for user-defined orientation models, enabling the framework to extend to additional bodies without needing hard-coded support for them in the library. For a body with no catalogued NAIF ID, self-assign a unique negative center, mirroring NAIF's convention for non-catalogued objects. See Propagation Around Other Central Bodies for a worked registration example.

Kernel Requirements Per Frame

Frame(s) Kernel needed Auto-loaded?
GCRF, ITRF, EME2000, MOD, TOD, TEME, TODofEpoch(epoch), TEMEofEpoch(epoch) None (SOFA-based) N/A
LCI (rotation only) None (ICRF-aligned) N/A
LCI, MCI, EMBI, SSBI (translation to/from another center) de440s SPK Yes, on first spk_* query
MCI, MCMF (translation to/from another center) de440s SPK + mar099s satellite ephemeris Yes, on first Mars body-center query
LFPA, LFME moon_pa_de440 binary PCK Yes, on first LCI ↔ LFPA/LFME conversion
MCMF (rotation only) None (compiled-in WGCCRE polynomial) N/A
EMR, SER, GSE de440s SPK Yes, on first query
BodyFixedIAU(naif_id) None (compiled-in), if naif_id is in iau_rotation_model_ids() N/A
BodyFixedPCK { .. } The named binary PCK No; must be loaded explicitly with load_spice_kernel
BodyFixedCustom { .. } None (user callback) N/A

The lunar PCK auto-load is a narrow exception to the general SPICE registry rule that binary PCKs are never auto-initialized (see SPICE Kernels); it exists because LFPA/LFME have no meaning without moon_pa_de440 loaded, so every lunar body-fixed conversion loads it transparently on first use. MCI/MCMF are centered on the Mars body center (NAIF 499); a translation to or from another center resolves the body-center leg through the mar099s satellite ephemeris kernel, which is auto-loaded the same way.

Axes and Centers

CelestialFrame pairs one origin with one orientation. FrameCenter names the origin alone: Body(naif_id) for a catalogued body or system barycenter (or any raw ID, including a self-assigned negative one), and Barycenter(primary, secondary) for the GM-weighted barycenter of a two-body pair that the synodic frames compute analytically. FrameAxes names the orientation alone — ICRF, EME2000, MOD, TOD, TEME, ITRF, LunarPA, LunarME, MarsFixed, EMR, SER, GSE, and the parameterized BodyFixedIAU(naif_id), BodyFixedPCK(frame_id), BodyFixedCustom(key), Synodic(primary, secondary), TODofEpoch(epoch), and TEMEofEpoch(epoch). CelestialFrame::centered(center, axes) builds the pair, returning a named shorthand when one exists (centered(399, EME2000) is EME2000; centered(301, ICRF) is LCI) and the generic Centered { center, axes } variant otherwise. center() and axes() split any frame back into its two halves, so centered(f.center(), f.axes()) == f holds for every frame Brahe produces.

center() returns the semantic FrameCenter rather than the raw integer center_naif_id() returns. FrameCenter::naif_id() is the identity the router translates on, so a Barycenter and the raw synthetic ID encoding the same pair reach the same origin; centered canonicalizes on that ID, and stores the FrameCenter as given when the pair has no named form. FrameCenter::name() prints the NAIF body name, the raw ID for an uncatalogued body, or BARYCENTER(primary, secondary).

This split matters because most named frames only cover one origin: EME2000, MOD, TOD, TEME, and ITRF are welded to Earth, MCI/MCMF to Mars, and so on. Centered lifts that restriction, pairing any orientation with any NAIF center.

In Python a trajectory's frame attribute is a ReferenceFrame, and its celestial_frame attribute gives the underlying CelestialFrame whose axes and center split it into its two halves.

Converting between two frames that share a center is a rotation only and never touches an SPK kernel; converting between frames centered on different bodies also translates by the vector between those centers, resolved through the loaded ephemeris exactly as any other cross-center router call. The example below loads an OEM whose CENTER_NAME is Mars, converts a sample to MCI (same center, rotation only) and to GCRF (different center, needs the de440s kernel loaded below):

import brahe as bh
from brahe.ccsds import OEM

bh.initialize_eop()
bh.load_common_spice_kernels()

oem = OEM.from_file("test_assets/ccsds/oem/OEMExample4.txt")
seg = oem.segments[0]
print(f"REF_FRAME = {seg.ref_frame}, CENTER_NAME = {seg.center_name}")

traj = oem.to_trajectories()[0]
print(f"\nTrajectory frame: {traj.frame}")

# The trajectory's frame is a CelestialFrame naming the (center, axes)
# pair; celestial_frame unwraps the ReferenceFrame and axes and center split
# the result back into its two halves.
traj_frame = traj.frame.celestial_frame
print(f"  Axes:   {traj_frame.axes}")
print(f"  Center: {traj_frame.center.name}")

# Same-center conversion: EME2000 to MCI is a rotation only, both centered on
# Mars.
traj_mci = traj.to_frame(bh.CelestialFrame.MCI)
epc, x_mci = traj_mci.get(0)
print(f"\nState at {epc} in MCI (Mars-centered):")
print(
    f"  Position (km): [{x_mci[0] / 1e3:.3f}, {x_mci[1] / 1e3:.3f}, {x_mci[2] / 1e3:.3f}]"
)
print(f"  Velocity (m/s): [{x_mci[3]:.3f}, {x_mci[4]:.3f}, {x_mci[5]:.3f}]")

# Cross-center conversion: EME2000/Mars to GCRF/Earth adds the Earth-Mars
# translation from the loaded SPK kernels.
traj_gcrf = traj.to_frame(bh.CelestialFrame.GCRF)
_, x_gcrf = traj_gcrf.get(0)
print(f"\nState at {epc} in GCRF (Earth-centered):")
print(
    f"  Position (km): [{x_gcrf[0] / 1e3:.3e}, {x_gcrf[1] / 1e3:.3e}, {x_gcrf[2] / 1e3:.3e}]"
)
print(f"  Velocity (m/s): [{x_gcrf[3]:.3f}, {x_gcrf[4]:.3f}, {x_gcrf[5]:.3f}]")

assert traj_frame.center == bh.NAIFId.MARS
assert traj_frame.axes == bh.FrameAxes.EME2000
print("\nExample validated successfully!")
use brahe as bh;
use brahe::ccsds::OEM;
use brahe::frames::{CelestialFrame, FrameCenter, ReferenceFrame};
use brahe::traits::Trajectory;

fn main() {
    bh::initialize_eop().unwrap();
    bh::load_common_spice_kernels().unwrap();

    let oem = OEM::from_file("test_assets/ccsds/oem/OEMExample4.txt").unwrap();
    let seg = &oem.segments[0];
    println!(
        "REF_FRAME = {}, CENTER_NAME = {}",
        seg.metadata.ref_frame, seg.metadata.center_name
    );

    let traj = oem.to_trajectories().unwrap().remove(0);
    let ReferenceFrame::Celestial(traj_frame) = &traj.frame else {
        panic!("OEM trajectory frame is always celestial");
    };
    let traj_frame = *traj_frame;
    println!("\nTrajectory frame: {}", traj_frame);
    println!("  Axes:   {}", traj_frame.axes());
    println!("  Center: {}", traj_frame.center());

    // Same-center conversion: EME2000 to MCI is a rotation only, both
    // centered on Mars.
    let traj_mci = traj.to_frame(CelestialFrame::MCI).unwrap();
    let (epc, x_mci) = traj_mci.first().unwrap();
    println!("\nState at {} in MCI (Mars-centered):", epc);
    println!(
        "  Position (km): [{:.3}, {:.3}, {:.3}]",
        x_mci[0] / 1e3,
        x_mci[1] / 1e3,
        x_mci[2] / 1e3
    );
    println!(
        "  Velocity (m/s): [{:.3}, {:.3}, {:.3}]",
        x_mci[3], x_mci[4], x_mci[5]
    );

    // Cross-center conversion: EME2000/Mars to GCRF/Earth adds the
    // Earth-Mars translation from the loaded SPK kernels.
    let traj_gcrf = traj.to_frame(CelestialFrame::GCRF).unwrap();
    let (_, x_gcrf) = traj_gcrf.first().unwrap();
    println!("\nState at {} in GCRF (Earth-centered):", epc);
    println!(
        "  Position (km): [{:.3e}, {:.3e}, {:.3e}]",
        x_gcrf[0] / 1e3,
        x_gcrf[1] / 1e3,
        x_gcrf[2] / 1e3
    );
    println!(
        "  Velocity (m/s): [{:.3}, {:.3}, {:.3}]",
        x_gcrf[3], x_gcrf[4], x_gcrf[5]
    );

    assert_eq!(traj_frame.center(), FrameCenter::Body(bh::NAIFId::Mars));
    assert_eq!(traj_frame.axes(), bh::FrameAxes::EME2000);
    println!("\nExample validated successfully!");
}
Output
REF_FRAME = EME2000, CENTER_NAME = MARS

Trajectory frame: Centered(MARS, EME2000)
  Axes:   EME2000
  Center: MARS

State at 2002-12-18 12:00:00.331 UTC in MCI (Mars-centered):
  Position (km): [2789.619, -280.045, -1746.755]
  Velocity (m/s): [4733.720, -2495.860, -1041.950]

State at 2002-12-18 12:00:00.331 UTC in GCRF (Earth-centered):
  Position (km): [-2.443e+08, -1.955e+08, -7.994e+07]
  Velocity (m/s): [42236.069, -23421.813, -10795.077]

Example validated successfully!
REF_FRAME = EME2000, CENTER_NAME = MARS

Trajectory frame: Centered(MARS, EME2000)
  Axes:   EME2000
  Center: MARS

State at 2002-12-18 12:00:00.331 UTC in MCI (Mars-centered):
  Position (km): [2789.619, -280.045, -1746.755]
  Velocity (m/s): [4733.720, -2495.860, -1041.950]

State at 2002-12-18 12:00:00.331 UTC in GCRF (Earth-centered):
  Position (km): [-2.443e8, -1.955e8, -7.994e7]
  Velocity (m/s): [42236.069, -23421.813, -10795.077]

Example validated successfully!

See Also