Skip to content

GCRF ↔ TEME ↔ ITRF Transformations

TEME is the true equator, mean equinox of date frame in which SGP4 expresses its output. Brahe defines it from the ITRF through Greenwich mean sidereal time on the IAU 1982 model, and it is available both as pairwise functions and through the frame router as CelestialFrame.TEME.

Reference Frame

TEME is defined by the relation

\[[\mathrm{ITRF}] = W \, R_3(\mathrm{GMST}_{82}) \, [\mathrm{TEME}]\]

where \(W\) is polar motion and \(\mathrm{GMST}_{82}\) is Greenwich mean sidereal time on the IAU 1982 model evaluated on UT1. This is the convention of Vallado et al., Revisiting Spacetrack Report #3, Appendix C, which fixes the meaning of the frame SGP4 produces. Combined with the CIO-based chain

\[[\mathrm{ITRF}] = W \, R_3(\mathrm{ERA}) \, C \, [\mathrm{GCRF}]\]

from the SOFA C transformation cookbook, the rotation from the GCRF is

\[[\mathrm{TEME}] = R_3(\mathrm{ERA} - \mathrm{GMST}_{82}) \, C \, [\mathrm{GCRF}]\]

where \(C\) is the bias-precession-nutation matrix used by GCRF ↔ ITRF Transformations. The TEME equator is therefore the true equator of date. Its origin of right ascension is the mean equinox implied by the IAU 1982 sidereal time model, which differs from the IAU 2006 mean equinox of MOD by the precession-model offset and from the true equinox of TOD by the equation of the equinoxes. The rotation from TEME to TOD is \(R_3(\mathrm{GMST}_{82} - \mathrm{GAST})\), where \(\mathrm{GAST}\) is Greenwich apparent sidereal time (GAST), evaluated by the frame router.

Velocities

TEME is treated as non-rotating relative to the GCRF: its precession and nutation rates are below \(10^{-11}\) rad/s, so state transforms between GCRF and TEME rotate position and velocity by the same matrix. The TEME to ITRF transformation includes the Earth rotation transport term, exactly as the GCRF to ITRF transformation does.

SGP4 Propagator

SGPPropagator.state returns the raw SGP4 output in TEME. The configured output frame, GCRF by default, applies to propagate_to, the stored trajectory, and current_state/initial_state. The frame-specific accessors state_gcrf and state_itrf always return the GCRF and ITRF states respectively, using the pairwise TEME transforms, regardless of the configured output frame. state_in_frame likewise always returns the requested frame, converting the TEME output through the frame router.

GCRF to TEME

State Vector

Transform a complete state vector (position and velocity) from GCRF to TEME:

import numpy as np

import brahe as bh

bh.initialize_eop()

# Define orbital elements in degrees
# LEO satellite: 500 km altitude, sun-synchronous orbit
oe = np.array(
    [
        bh.R_EARTH + 500e3,  # Semi-major axis (m)
        0.01,  # Eccentricity
        97.8,  # Inclination (deg)
        15.0,  # Right ascension of ascending node (deg)
        30.0,  # Argument of periapsis (deg)
        45.0,  # Mean anomaly (deg)
    ]
)

epc = bh.Epoch(2024, 1, 1, 12, 0, 0.0, time_system=bh.UTC)
print(f"Epoch: {epc}")

# Convert to GCRF Cartesian state
state_gcrf = bh.state_koe_to_eci(oe, bh.AngleFormat.DEGREES)

print("\nGCRF state vector:")
print(f"  Position: [{state_gcrf[0]:.3f}, {state_gcrf[1]:.3f}, {state_gcrf[2]:.3f}] m")
print(
    f"  Velocity: [{state_gcrf[3]:.6f}, {state_gcrf[4]:.6f}, {state_gcrf[5]:.6f}] m/s\n"
)

# Transform to TEME at the given epoch
state_teme = bh.state_gcrf_to_teme(epc, state_gcrf)

print("TEME state vector:")
print(f"  Position: [{state_teme[0]:.3f}, {state_teme[1]:.3f}, {state_teme[2]:.3f}] m")
print(
    f"  Velocity: [{state_teme[3]:.6f}, {state_teme[4]:.6f}, {state_teme[5]:.6f}] m/s\n"
)

pos_diff = np.linalg.norm(state_gcrf[0:3] - state_teme[0:3])
print(f"Position difference norm: {pos_diff:.3f} m")
use brahe as bh;
use nalgebra as na;

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

    // Define orbital elements in degrees
    // LEO satellite: 500 km altitude, sun-synchronous orbit
    let oe = na::SVector::<f64, 6>::new(
        bh::R_EARTH + 500e3,  // Semi-major axis (m)
        0.01,                  // Eccentricity
        97.8,                  // Inclination (deg)
        15.0,                  // Right ascension of ascending node (deg)
        30.0,                  // Argument of periapsis (deg)
        45.0,                  // Mean anomaly (deg)
    );

    let epc = bh::Epoch::from_datetime(2024, 1, 1, 12, 0, 0.0, 0.0, bh::TimeSystem::UTC);
    println!("Epoch: {}", epc);

    // Convert to GCRF Cartesian state
    let state_gcrf = bh::state_koe_to_eci(oe, bh::AngleFormat::Degrees);

    println!("\nGCRF state vector:");
    println!("  Position: [{:.3}, {:.3}, {:.3}] m", state_gcrf[0], state_gcrf[1], state_gcrf[2]);
    println!("  Velocity: [{:.6}, {:.6}, {:.6}] m/s\n", state_gcrf[3], state_gcrf[4], state_gcrf[5]);

    // Transform to TEME at the given epoch
    let state_teme = bh::state_gcrf_to_teme(epc, state_gcrf);

    println!("TEME state vector:");
    println!("  Position: [{:.3}, {:.3}, {:.3}] m", state_teme[0], state_teme[1], state_teme[2]);
    println!("  Velocity: [{:.6}, {:.6}, {:.6}] m/s\n", state_teme[3], state_teme[4], state_teme[5]);

    let pos_gcrf = na::Vector3::new(state_gcrf[0], state_gcrf[1], state_gcrf[2]);
    let pos_teme = na::Vector3::new(state_teme[0], state_teme[1], state_teme[2]);
    let pos_diff = (pos_gcrf - pos_teme).norm();
    println!("Position difference norm: {:.3} m", pos_diff);
}
Output
Epoch: 2024-01-01 12:00:00.000 UTC

GCRF state vector:
  Position: [1848964.106, -434937.468, 6560410.530] m
  Velocity: [-7098.379734, -2173.344867, 1913.333385] m/s

TEME state vector:
  Position: [1836037.476, -425305.576, 6564671.107] m
  Velocity: [-7091.035289, -2211.497999, 1896.776877] m/s

Position difference norm: 16674.041 m
Epoch: 2024-01-01 12:00:00.000 UTC

GCRF state vector:
  Position: [1848964.106, -434937.468, 6560410.530] m
  Velocity: [-7098.379734, -2173.344867, 1913.333385] m/s

TEME state vector:
  Position: [1836037.476, -425305.576, 6564671.107] m
  Velocity: [-7091.035289, -2211.497999, 1896.776877] m/s

Position difference norm: 16674.041 m

TEME to ITRF

State Vector

Transform a TEME state vector from an SGP4 propagator into the ITRF:

import numpy as np

import brahe as bh

bh.initialize_eop()

line1 = "1 25544U 98067A   08264.51782528 -.00002182  00000-0 -11606-4 0  2927"
line2 = "2 25544  51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537"
prop = bh.SGPPropagator.from_tle(line1, line2, 60.0)

epc = prop.epoch + 600.0
state_teme = prop.state(epc)
print(f"Epoch: {epc}")
print("TEME state vector:")
print(f"  Position: [{state_teme[0]:.3f}, {state_teme[1]:.3f}, {state_teme[2]:.3f}] m")
print(
    f"  Velocity: [{state_teme[3]:.6f}, {state_teme[4]:.6f}, {state_teme[5]:.6f}] m/s\n"
)

state_itrf = bh.state_teme_to_itrf(epc, state_teme)
print("ITRF state vector:")
print(f"  Position: [{state_itrf[0]:.3f}, {state_itrf[1]:.3f}, {state_itrf[2]:.3f}] m")
print(
    f"  Velocity: [{state_itrf[3]:.6f}, {state_itrf[4]:.6f}, {state_itrf[5]:.6f}] m/s\n"
)

speed_teme = np.linalg.norm(state_teme[3:6])
speed_itrf = np.linalg.norm(state_itrf[3:6])
print(f"Inertial speed: {speed_teme:.3f} m/s, Earth-fixed speed: {speed_itrf:.3f} m/s")
use brahe as bh;
use brahe::traits::SStateProvider;

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

    let line1 = "1 25544U 98067A   08264.51782528 -.00002182  00000-0 -11606-4 0  2927";
    let line2 = "2 25544  51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
    let prop = bh::SGPPropagator::from_tle(line1, line2, 60.0).unwrap();

    let epc = prop.epoch + 600.0;
    let state_teme = prop.state(epc).unwrap();
    println!("Epoch: {}", epc);
    println!("TEME state vector:");
    println!("  Position: [{:.3}, {:.3}, {:.3}] m", state_teme[0], state_teme[1], state_teme[2]);
    println!("  Velocity: [{:.6}, {:.6}, {:.6}] m/s\n", state_teme[3], state_teme[4], state_teme[5]);

    let state_itrf = bh::state_teme_to_itrf(epc, state_teme);
    println!("ITRF state vector:");
    println!("  Position: [{:.3}, {:.3}, {:.3}] m", state_itrf[0], state_itrf[1], state_itrf[2]);
    println!("  Velocity: [{:.6}, {:.6}, {:.6}] m/s\n", state_itrf[3], state_itrf[4], state_itrf[5]);

    let speed_teme = state_teme.fixed_rows::<3>(3).norm();
    let speed_itrf = state_itrf.fixed_rows::<3>(3).norm();
    println!("Inertial speed: {:.3} m/s, Earth-fixed speed: {:.3} m/s", speed_teme, speed_itrf);
}
Output
Epoch: 2008-09-20 12:35:40.104 UTC
TEME state vector:
  Position: [4550084.410, 3253042.848, 3728355.240] m
  Velocity: [-1021.319331, 6334.629918, -4267.256174] m/s

ITRF state vector:
  Position: [-4989209.285, -2528501.268, 3728358.989] m
  Velocity: [-131.542881, -6052.392221, -4267.263413] m/s

Inertial speed: 7705.849 m/s, Earth-fixed speed: 7406.638 m/s
Epoch: 2008-09-20 12:35:40.104 UTC
TEME state vector:
  Position: [4550084.410, 3253042.848, 3728355.240] m
  Velocity: [-1021.319331, 6334.629918, -4267.256174] m/s

ITRF state vector:
  Position: [-4989209.285, -2528501.268, 3728358.989] m
  Velocity: [-131.542881, -6052.392221, -4267.263413] m/s

Inertial speed: 7705.849 m/s, Earth-fixed speed: 7406.638 m/s

SGP4 Output in TOD

state_in_frame converts the raw TEME output of SGPPropagator.state into any other router frame, here TOD, and can be compared against the GCRF accessor:

import numpy as np

import brahe as bh

bh.initialize_eop()

line1 = "1 25544U 98067A   08264.51782528 -.00002182  00000-0 -11606-4 0  2927"
line2 = "2 25544  51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537"
prop = bh.SGPPropagator.from_tle(line1, line2, 60.0)

epc = prop.epoch + 600.0
state_tod = prop.state_in_frame(bh.CelestialFrame.TOD, epc)
state_gcrf = prop.state_gcrf(epc)

print(f"Epoch: {epc}")
print("TOD state vector:")
print(f"  Position: [{state_tod[0]:.3f}, {state_tod[1]:.3f}, {state_tod[2]:.3f}] m")
print(f"  Velocity: [{state_tod[3]:.6f}, {state_tod[4]:.6f}, {state_tod[5]:.6f}] m/s\n")

print("GCRF state vector:")
print(f"  Position: [{state_gcrf[0]:.3f}, {state_gcrf[1]:.3f}, {state_gcrf[2]:.3f}] m")
print(
    f"  Velocity: [{state_gcrf[3]:.6f}, {state_gcrf[4]:.6f}, {state_gcrf[5]:.6f}] m/s\n"
)

pos_diff = np.linalg.norm(state_tod[0:3] - state_gcrf[0:3])
print(f"Position difference norm: {pos_diff:.3f} m")
use brahe as bh;
use brahe::traits::SOrbitStateProvider;
use nalgebra as na;

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

    let line1 = "1 25544U 98067A   08264.51782528 -.00002182  00000-0 -11606-4 0  2927";
    let line2 = "2 25544  51.6416 247.4627 0006703 130.5360 325.0288 15.72125391563537";
    let prop = bh::SGPPropagator::from_tle(line1, line2, 60.0).unwrap();

    let epc = prop.epoch + 600.0;
    let state_tod = prop
        .state_in_frame(bh::CelestialFrame::TOD, epc)
        .unwrap();
    let state_gcrf = prop.state_gcrf(epc).unwrap();

    println!("Epoch: {}", epc);
    println!("TOD state vector:");
    println!("  Position: [{:.3}, {:.3}, {:.3}] m", state_tod[0], state_tod[1], state_tod[2]);
    println!("  Velocity: [{:.6}, {:.6}, {:.6}] m/s\n", state_tod[3], state_tod[4], state_tod[5]);

    println!("GCRF state vector:");
    println!("  Position: [{:.3}, {:.3}, {:.3}] m", state_gcrf[0], state_gcrf[1], state_gcrf[2]);
    println!("  Velocity: [{:.6}, {:.6}, {:.6}] m/s\n", state_gcrf[3], state_gcrf[4], state_gcrf[5]);

    let pos_tod = na::Vector3::new(state_tod[0], state_tod[1], state_tod[2]);
    let pos_gcrf = na::Vector3::new(state_gcrf[0], state_gcrf[1], state_gcrf[2]);
    let pos_diff = (pos_tod - pos_gcrf).norm();
    println!("Position difference norm: {:.3} m", pos_diff);
}
Output
Epoch: 2008-09-20 12:35:40.104 UTC
TOD state vector:
  Position: [4549919.503, 3253273.494, 3728355.240] m
  Velocity: [-1021.640442, 6334.578137, -4267.256174] m/s

GCRF state vector:
  Position: [4559658.424, 3244289.422, 3724283.812] m
  Velocity: [-1012.673886, 6336.465766, -4266.590890] m/s

Position difference norm: 13861.337 m
Epoch: 2008-09-20 12:35:40.104 UTC
TOD state vector:
  Position: [4549919.503, 3253273.494, 3728355.240] m
  Velocity: [-1021.640442, 6334.578137, -4267.256174] m/s

GCRF state vector:
  Position: [4559658.424, 3244289.422, 3724283.812] m
  Velocity: [-1012.673886, 6336.465766, -4266.590890] m/s

Position difference norm: 13861.337 m

Of-Epoch Frames

FrameAxes.TODofEpoch(epoch) and FrameAxes.TEMEofEpoch(epoch) are the TOD and TEME axes evaluated once at epoch and held fixed. Because the axes do not move, a frame built on them is inertial: its rotation to the GCRF is

\[R_{\mathrm{GCRF} \to \mathrm{TODofEpoch}(t_0)} = R_{\mathrm{GCRF} \to \mathrm{TOD}}(t_0)\]

for every transform epoch, and velocities transform by the same rotation with no transport term. CelestialFrame.tod_of_epoch(epoch) and CelestialFrame.teme_of_epoch(epoch) build the Earth-centered frames, and CelestialFrame.Centered(center, axes) pairs the axes with any other center. Two of-epoch axes are equal only when their epochs are equal, and frame_epoch returns the frozen epoch on both FrameAxes and CelestialFrame.

These frames are how Brahe represents a CCSDS ODM message whose REF_FRAME is TOD or TEME and which also carries a REF_FRAME_EPOCH: the message loads in the of-epoch frame and converts only when asked. The CCSDS ADM TEMEOFEPOCH token cannot be mapped, because ADM metadata has no keyword for the frozen epoch.

References

See Also