Skip to content

Coordinate Frame Transforms

The satkit.frametransform module provides functions for transforming between various coordinate frames used in satellite tracking and orbit determination. These include multiple variations of "inertial" coordinate frames, and multiple versions of "Earth-fixed" coordinate frames.

Some notes:

  • Most of the algorithms in this module are from the book "Fundamentals of Astrodynamics and Applications" by David Vallado.
  • The frame transforms are defined as arbitrary rotations in a 3-dimensional space. The rotations are a function of time, and are represented as quaternions.
  • The rotation from the Geocentric Celestial Reference Frame (GCRF) to the Earth-Centered Inertial (ECI) frame is defined by the International Astronomical Union (IAU), available at https://www.iers.org/. See IERS Technical Note 36 for the latest values.

Dispatch API

The recommended entry points are the frame-enum dispatch functions, which take a source and destination frame and pick the appropriate rotation internally:

import satkit as sk

t = sk.time(2024, 1, 1, 12, 0, 0)

# Full IERS 2010 reduction. Keyword arguments are recommended at the call
# site so the source / destination direction is unambiguous; positional
# args work too once you know the order (from, to, tm).
q = sk.frametransform.rotation(
    from_frame=sk.frame.ITRF, to_frame=sk.frame.GCRF, tm=t,
)

# IAU-76/FK5 approximation (~1 arcsec), inertial cluster + ITRF only
q_approx = sk.frametransform.rotation_approx(
    from_frame=sk.frame.ITRF, to_frame=sk.frame.GCRF, tm=t,
)

# Position + velocity (handles the Earth-rotation sweep term)
pos_gcrf, vel_gcrf = sk.frametransform.transform_state(
    from_frame=sk.frame.ITRF, to_frame=sk.frame.GCRF,
    tm=t, pos=pos_itrf, vel=vel_itrf,
)

rotation accepts any pair of ITRF, GCRF, TEME, EME2000, ICRF, TIRS, CIRS and picks the shortest path through the frame graph (it does not always pivot through GCRF). Pairs involving the orbit-dependent frames LVLH, RTN, NTW need a state and so go through to_gcrf / from_gcrf instead.

Which function do I call?

There are three related quaternion entry points; pick by what your frames need:

Function Frames it handles Extra arguments Returns
rotation Earth chain only (ITRF, TIRS, CIRS, GCRF, TEME, EME2000, ICRF) quaternion
to_gcrf / from_gcrf Orbit frames only (LVLH, RTN, NTW) pos, vel (GCRF) 3×3 matrix
rotation_with_state All frames (Earth and orbit) pos, vel (GCRF) quaternion

Use rotation_with_state when a pair mixes an Earth frame and an orbit frame — e.g. going straight from TEME to RTN — without manually composing two transforms through GCRF. Note that the solution does not always go through GCRF: a purely Earth-frame pair delegates to rotation, which picks the shortest path through the frame graph (e.g. ITRFTIRS is a single polar-motion rotation, with no IERS reduction paid at all); only pairs that involve an orbit-dependent frame compose through GCRF. The orbit state is only consulted when an orbit frame is involved:

# TEME (Earth-fixed SGP4 frame) directly to RTN (orbit-local) in one call.
q = sk.frametransform.rotation_with_state(
    from_frame=sk.frame.TEME, to_frame=sk.frame.RTN,
    tm=t, pos=pos_gcrf, vel=vel_gcrf,
)

Any of these functions accept either a satkit.time or a datetime.datetime for the tm argument.

The per-pair functions below (qitrf2gcrf, qteme2itrf, qcirs2gcrf, …) remain available for direct use when the source / destination pair is hard-coded in the surrounding code.

frametransform

Transformations between coordinate frames, and associated utility functions

Coordinate frame transforms are mostly pulled from Vallado: https://www.google.com/books/edition/Fundamentals_of_Astrodynamics_and_Applic/PJLlWzMBKjkC?hl=en&gbpv=0

or the IERS: https://www.iers.org/

earth_orientation_params(time)

Get Earth Orientation Parameters at given instant

Parameters:

Name Type Description Default
time time

Instant at which to query parameters

required

Returns:

Type Description
tuple[float, float, float, float, float, float]

(float, float, float, float, float, float) | None: Tuple with following elements: 0 : (UT1 - UTC) in seconds 1 : X polar motion in arcsecs 2 : Y polar motion in arcsecs 3 : LOD: instantaneous rate of change in (UT1-UTC), msec/day 4 : dX wrt IAU-2000A nutation, milli-arcsecs 5 : dY wrt IAU-2000A nutation, milli-arcsecs

Notes
Example
t = satkit.time(2024, 1, 1)
eop = satkit.frametransform.earth_orientation_params(t)
if eop is not None:
    ut1_utc, xp, yp, lod, dx, dy = eop
    print(f"UT1-UTC: {ut1_utc:.6f} s")

to_gcrf(frame, pos, vel)

Return the 3x3 DCM that transforms a vector from a satellite-local orbital frame into GCRF at the current state.

This is the unified dispatch for satellite-local orbital frames. Supported values:

  • frame.GCRF — returns the 3x3 identity matrix (trivial case)
  • frame.LVLH — Local Vertical / Local Horizontal
  • frame.RTN — Radial / In-track / Cross-track (= RSW = RTN)
  • frame.NTW — Normal-to-velocity / Tangent / Cross-track

For an arbitrary frame-to-frame rotation, compose with :func:from_gcrf::

# NTW -> RIC
dcm = sk.frametransform.from_gcrf(sk.frame.RTN, pos, vel) @ \
      sk.frametransform.to_gcrf(sk.frame.NTW, pos, vel)

Parameters:

Name Type Description Default
frame frame

Source satellite-local frame

required
pos ArrayLike

3-element position vector in GCRF [m]

required
vel ArrayLike

3-element velocity vector in GCRF [m/s]

required

Returns:

Type Description
NDArray[float64]

numpy.ndarray: 3x3 rotation matrix (frame → GCRF)

Raises:

Type Description
RuntimeError

if frame is not a satellite-local orbital frame. Earth-fixed / celestial frames (ITRF, TEME, EME2000, etc.) need a time argument for their rotation to GCRF and must use the dedicated quaternion helpers (:func:qitrf2gcrf, :func:qteme2gcrf, etc.) instead.

Example
import satkit as sk
dcm = sk.frametransform.to_gcrf(sk.frame.NTW, pos_gcrf, vel_gcrf)
v_gcrf = dcm @ v_ntw

itrf_to_gcrf_state(pos_itrf, vel_itrf, time)

Transform a satellite state (position + velocity) from ITRF to GCRF.

Accepts either a single state (pos/vel are length-3 vectors and time is a single satkit.time) or a batch of N states (pos/vel are shape (N, 3) arrays and time is a length-N array/list of times). The output shape matches the input.

Unlike the raw :func:qitrf2gcrf quaternion, this function correctly handles the Earth-rotation contribution to velocity. A point at rest on Earth's surface has zero velocity in ITRF but ~465 m/s in GCRF at the equator, and this function accounts for that term.

The IERS 2010 ITRF → GCRF reduction decomposes into three stages: polar motion (ITRF → TIRS), Earth rotation about the CIO polar axis (TIRS → CIRS), and precession-nutation (CIRS → GCRF). The Earth-rotation sweep term omega_earth x r is computed in TIRS — not ITRF or GCRF — because TIRS is defined such that Earth's rotation axis is exactly along its +z axis. Computing the sweep anywhere else would introduce either a polar-motion-sized error (~0.3 arcsec in ITRF) or a precession-sized error (tens of degrees in GCRF).

Implementation:

  1. Rotate pos_itrf and vel_itrf into TIRS via polar motion.
  2. Add omega_earth x r_tirs to the velocity in TIRS, where omega_earth = (0, 0, OMEGA_EARTH) exactly.
  3. Rotate TIRS → CIRS → GCRF via the full IERS 2010 chain.

Uses the full IERS 2010 reduction (polar motion + Earth rotation + precession-nutation with dX/dY corrections from Earth orientation parameters).

Parameters:

Name Type Description Default
pos_itrf ArrayLike

3-element position vector in ITRF [m]

required
vel_itrf ArrayLike

3-element velocity vector as observed in ITRF [m/s] (zero for a point at rest on Earth's surface)

required
time TimeInput

Epoch of the state

required

Returns:

Type Description
NDArray[float64]

A 2-tuple (pos_gcrf, vel_gcrf) of numpy arrays with the

NDArray[float64]

state expressed in GCRF.

Example
import satkit as sk
import numpy as np

# Geostationary satellite, stationary in ITRF
t = sk.time(2024, 1, 1)
pos_itrf = np.array([42164.17e3, 0.0, 0.0])
vel_itrf = np.array([0.0, 0.0, 0.0])
pos_gcrf, vel_gcrf = sk.frametransform.itrf_to_gcrf_state(
    pos_itrf, vel_itrf, t)
# |vel_gcrf| ≈ 3075 m/s (the GEO orbital speed)

gcrf_to_itrf_state(pos_gcrf, vel_gcrf, time)

Transform a satellite state (position + velocity) from GCRF to ITRF.

Inverse of :func:itrf_to_gcrf_state. Rotates the state through GCRF → CIRS → TIRS, subtracts the Earth-rotation omega_earth x r term in TIRS (where Earth's rotation axis is exactly along +z), then applies inverse polar motion to reach ITRF. A geostationary satellite (whose GCRF velocity is pure orbital motion) produces zero velocity in ITRF. Uses the full IERS 2010 reduction.

Accepts either a single state or a batch of N states: when pos/vel are shape (N, 3) arrays, time must be a length-N array/list of times, and the returned arrays have shape (N, 3).

Parameters:

Name Type Description Default
pos_gcrf ArrayLike

3-element position vector in GCRF [m]

required
vel_gcrf ArrayLike

3-element velocity vector in GCRF [m/s]

required
time TimeInput

Epoch of the state

required

Returns:

Type Description
NDArray[float64]

A 2-tuple (pos_itrf, vel_itrf) where vel_itrf is the

NDArray[float64]

velocity as observed in ITRF.

itrf_to_gcrf_state_approx(pos_itrf, vel_itrf, time)

Approximate ITRF → GCRF state transform using the IAU-76/FK5 reduction (accurate to ~1 arcsec on position).

Faster alternative to :func:itrf_to_gcrf_state when the full IERS 2010 precision is not required. Neglects polar motion, so the Earth-rotation sweep omega_earth x r is evaluated in ITRF directly. Accepts scalar or batched inputs like :func:itrf_to_gcrf_state.

gcrf_to_itrf_state_approx(pos_gcrf, vel_gcrf, time)

Approximate GCRF → ITRF state transform using the IAU-76/FK5 reduction. Inverse of :func:itrf_to_gcrf_state_approx; accurate to ~1 arcsec on position. Accepts scalar or batched inputs.

from_gcrf(frame, pos, vel)

Return the 3x3 DCM that transforms a vector from GCRF into a satellite-local orbital frame at the current state.

Transpose of :func:to_gcrf. See that function for the list of supported frames, composition examples, and error conditions.

Parameters:

Name Type Description Default
frame frame

Destination satellite-local frame

required
pos ArrayLike

3-element position vector in GCRF [m]

required
vel ArrayLike

3-element velocity vector in GCRF [m/s]

required

Returns:

Type Description
NDArray[float64]

numpy.ndarray: 3x3 rotation matrix (GCRF → frame)

Raises:

Type Description
RuntimeError

if frame is not a satellite-local orbital frame.

Example
import satkit as sk
dcm = sk.frametransform.from_gcrf(sk.frame.RTN, pos_gcrf, vel_gcrf)
v_ric = dcm @ v_gcrf

disable_eop_time_warning()

Disable the warning printed to stderr when Earth Orientation Parameters (EOP) are not available for a given time.

Notes
  • This function is used to disable the warning printed when EOP are not available for a given time.
  • If not disabled, warning will be shown only once per library load,

rotation(from_frame, to_frame, tm)

Quaternion rotating a vector from from_frame to to_frame at tm. Full IERS 2010 reduction.

Uses the shortest path through the frame graph for each pair (does not always pivot through GCRF). Pairs involving orbit-dependent frames (LVLH, RTN, NTW) require state and are not supported here — use :func:to_gcrf / :func:from_gcrf for those.

Parameters:

Name Type Description Default
from_frame frame

Source frame

required
to_frame frame

Destination frame

required
tm TimeScalar

Epoch

required

Returns:

Type Description
quaternion

Rotation from from_frame to to_frame at tm.

Raises:

Type Description
RuntimeError

if the pair involves LVLH / RTN / NTW.

rotation_with_state(from_frame, to_frame, tm, pos, vel)

Quaternion rotating a vector from from_frame to to_frame — the unified front door supporting all frames, both the time-parameterised Earth chain (ITRF, TIRS, CIRS, GCRF, TEME, EME2000, ICRF) and the orbit-dependent frames (LVLH, RTN, NTW), in a single call.

Unlike :func:rotation (which rejects the orbit frames) and :func:to_gcrf (which rejects the Earth frames), this accepts any pair. It does not always pivot through GCRF: a purely Earth-frame pair delegates to :func:rotation, which takes the shortest path through the frame graph; only pairs involving an orbit-dependent frame compose through GCRF. The orbit state (pos, vel, both in GCRF) is only consulted when an orbit-dependent frame is involved.

Parameters:

Name Type Description Default
from_frame frame

Source frame

required
to_frame frame

Destination frame

required
tm TimeScalar

Epoch

required
pos ArrayLike

3-element GCRF position vector [m]

required
vel ArrayLike

3-element GCRF velocity vector [m/s]

required

Returns:

Type Description
quaternion

Rotation from from_frame to to_frame at tm.

rotation_approx(from_frame, to_frame, tm)

Quaternion rotating a vector from from_frame to to_frame using the IAU-76/FK5 approximate reduction (~1 arcsec).

Only valid between ITRF and the inertial cluster (GCRF, EME2000, ICRF, TEME). TIRS and CIRS are defined by the IERS 2010 reduction and have no FK5 analogue.

Raises:

Type Description
RuntimeError

if either frame is TIRS / CIRS, or if the pair involves orbit-dependent frames.

transform_state(from_frame, to_frame, tm, pos, vel)

State (position + velocity) transform from from_frame to to_frame at tm. Properly handles the Earth-rotation sweep term when transitioning between rotating (ITRF) and inertial frames.

Currently supported pairs: identity, ITRF↔{GCRF, EME2000, ICRF, TEME}, and within-inertial pairs. Other pairs raise RuntimeError in this version.

Parameters:

Name Type Description Default
from_frame frame

Source frame

required
to_frame frame

Destination frame

required
tm TimeScalar

Epoch

required
pos ArrayLike

3-element position vector [m]

required
vel ArrayLike

3-element velocity vector [m/s]

required

Returns:

Type Description
tuple[NDArray[float64], NDArray[float64]]

(pos, vel) in to_frame.

transform_state_approx(from_frame, to_frame, tm, pos, vel)

State transform using the IAU-76/FK5 approximate reduction. Same supported-pair set as :func:transform_state.