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.
\[[\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}]\]
\[[\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.
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.
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.
importnumpyasnpimportbraheasbhbh.initialize_eop()# Define orbital elements in degrees# LEO satellite: 500 km altitude, sun-synchronous orbitoe=np.array([bh.R_EARTH+500e3,# Semi-major axis (m)0.01,# Eccentricity97.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 statestate_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 epochstate_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")
usebraheasbh;usenalgebraasna;fnmain(){bh::initialize_eop().unwrap();// Define orbital elements in degrees// LEO satellite: 500 km altitude, sun-synchronous orbitletoe=na::SVector::<f64,6>::new(bh::R_EARTH+500e3,// Semi-major axis (m)0.01,// Eccentricity97.8,// Inclination (deg)15.0,// Right ascension of ascending node (deg)30.0,// Argument of periapsis (deg)45.0,// Mean anomaly (deg));letepc=bh::Epoch::from_datetime(2024,1,1,12,0,0.0,0.0,bh::TimeSystem::UTC);println!("Epoch: {}",epc);// Convert to GCRF Cartesian stateletstate_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 epochletstate_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]);letpos_gcrf=na::Vector3::new(state_gcrf[0],state_gcrf[1],state_gcrf[2]);letpos_teme=na::Vector3::new(state_teme[0],state_teme[1],state_teme[2]);letpos_diff=(pos_gcrf-pos_teme).norm();println!("Position difference norm: {:.3} m",pos_diff);}
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
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
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:
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
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
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.