Two-Line Element Set¶
Two-Line Element sets (TLEs) are the most widely used format for distributing satellite orbital data. They encode mean orbital elements designed specifically for use with the SGP4 propagator, and are published by organizations like CelesTrak and Space-Track.
This tutorial demonstrates:
- Loading a TLE and propagating it with SGP4 to get a position and velocity in the TEME (True Equator Mean Equinox) frame
- Rotating from TEME to the ITRF (Earth-fixed) frame to obtain geodetic coordinates
- Plotting the satellite ground track over multiple orbits
Generate State Vector¶
We fetch a current TLE for the International Space Station directly from CelesTrak using sk.TLE.from_url — no external HTTP library required. SGP4 outputs position and velocity in the TEME frame, a pseudo-inertial frame that does not account for precession or nutation. To get a location on the Earth's surface, we rotate into the ITRF frame using a quaternion from satkit.frametransform, then convert the Cartesian position to geodetic coordinates.
import satkit as sk
# Fetch the current TLE for the ISS directly from CelesTrak. sk.TLE.from_url
# returns a single TLE when the response contains exactly one element, or a
# list of TLEs otherwise. No external HTTP library is required.
url = "https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=TLE"
iss = sk.TLE.from_url(url)
# Evaluate the orbital state 6 hours after the TLE epoch
thetime = iss.epoch + sk.duration(hours=6)
# The state is output in the "TEME" frame, which is an approximate inertial
# frame that does not include precession or nutation
# pTEME is geocentric position in meters
# vTEME is geocentric velocity in meters / second
# for now we will ignore the velocity
pTEME, _vTEME = sk.sgp4(iss, thetime)
# Suppose we want current latitude, longitude, and altitude of satellite:
# we need to rotate into an Earth-fixed frame, the ITRF
# We use a "quaternion" to represent the rotation. Quaternion rotations
# in the satkit toolbox can be represented as multiplications of a 3-vector
pITRF = sk.frametransform.qteme2itrf(thetime) * pTEME
# Now lets make a "ITRFCoord" object to extract geodetic coordinates
coord = sk.itrfcoord(pITRF)
# Get the latitude, longitude, and
# altitude (height above ellipsoid, or hae) of the satellite
print(coord)
ITRFCoord(lat: -33.9394 deg, lon: 143.0154 deg, hae: 427.44 km)
Plot Satellite Ground Track¶
The ground track is the projection of the satellite's orbit onto the Earth's surface. The sinusoidal pattern results from the satellite's inclined orbit combined with the Earth's rotation. The mean_motion field in the TLE gives the number of orbits per day, which we use to compute the total time span for 5 complete orbits.
import satkit as sk
import numpy as np
import matplotlib.pyplot as plt
import scienceplots # noqa: F401
plt.style.use(["science", "no-latex", "../satkit.mplstyle"])
%config InlineBackend.figure_formats = ['svg']
import warnings
warnings.filterwarnings("ignore", "Downloading")
import cartopy.crs as ccrs
import cartopy.feature as cfeature
# Fetch the current TLE for the ISS directly from CelesTrak
url = "https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=TLE"
iss = sk.TLE.from_url(url)
# Start the ground-track plot at the TLE epoch
thetime = iss.epoch
# plot for 5 orbits. The mean motion in the TLE is number of orbits in a day
timearr = np.array([thetime + sk.duration(days=x) for x in np.linspace(0, 5/iss.mean_motion, 1000)])
# Get position in the TEME frame
pTEME, _vTEME = sk.sgp4(iss, timearr)
qarr = sk.frametransform.qteme2itrf(timearr)
pITRF = np.array([q * p for q, p in zip(qarr, pTEME)])
coord = [sk.itrfcoord(p) for p in pITRF]
lat, lon, alt = zip(*[(c.latitude_deg, c.longitude_deg, c.altitude) for c in coord])
fig, ax = plt.subplots(figsize=(10, 5), subplot_kw={"projection": ccrs.PlateCarree()})
ax.add_feature(cfeature.LAND, facecolor="lightgray")
ax.add_feature(cfeature.BORDERS, linewidth=0.5)
ax.add_feature(cfeature.COASTLINE, linewidth=0.5)
# Break line at date line crossings
lon_arr, lat_arr = np.array(lon), np.array(lat)
breaks = np.where(np.abs(np.diff(lon_arr)) > 180)[0] + 1
lon_segs = np.split(lon_arr, breaks)
lat_segs = np.split(lat_arr, breaks)
for lo, la in zip(lon_segs, lat_segs):
ax.plot(lo, la, linewidth=1, color="C0", transform=ccrs.PlateCarree())
ax.set_title("Ground Track")
ax.set_global()
plt.tight_layout()
plt.show()
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot([t.as_datetime() for t in timearr], np.array(alt)/1e3)
ax.set_xlabel("Time")
ax.set_ylabel("Altitude (km)")
ax.set_title("Altitude vs Time")
fig.autofmt_xdate()
plt.tight_layout()
plt.show()
--------------------------------------------------------------------------- RuntimeError Traceback (most recent call last) Cell In[2], line 14 10 import cartopy.feature as cfeature 11 12 # Fetch the current TLE for the ISS directly from CelesTrak 13 url = "https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=TLE" ---> 14 iss = sk.TLE.from_url(url) 15 16 # Start the ground-track plot at the TLE epoch 17 thetime = iss.epoch RuntimeError: http status: 503