Skip to content

Reference Frame Router

ReferenceFrame 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 (native)
EME2000 Inertial 399 IAU 2006 frame bias (native)
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. GCRFITRF, LCILFPA) 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 GCRFMCMF transform with rotation_frame_to_frame, which uses the compiled-in Mars model and needs no kernel.

import brahe as bh
import numpy as np

# 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.ReferenceFrame.GCRF, bh.ReferenceFrame.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.ReferenceFrame.GCRF, bh.ReferenceFrame.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::ReferenceFrame::GCRF, bh::ReferenceFrame::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::ReferenceFrame::GCRF, bh::ReferenceFrame::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 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.

See Also