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¶
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
# The two-line element set
# Lets pick a random StarLink satellite
# The lines below were downloaded from https://www.celetrack.org
tle_lines = [
'0 STARLINK-30477',
'1 57912U 23146X 24099.49439401 .00006757 00000+0 51475-3 0 9997',
'2 57912 43.0018 157.5807 0001420 272.5369 87.5310 15.02537576 31746'
]
# Create a TLE object
starlink30477 = sk.TLE.from_lines(tle_lines)
# We want the orbital state at April 9 2024, 12:00pm UTC
thetime = sk.time(2024, 4, 9, 12, 0, 0)
# 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(starlink30477, 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: 29.3890 deg, lon: 170.8051 deg, hae: 560.11 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 plotly.graph_objects as go
# The two-line element set
# Same satellite as above...
# The lines below were downloaded from https://www.celetrack.org
tle_lines = [
'0 STARLINK-30477',
'1 57912U 23146X 24099.49439401 .00006757 00000+0 51475-3 0 9997',
'2 57912 43.0018 157.5807 0001420 272.5369 87.5310 15.02537576 31746'
]
# Create a TLE object
starlink30477 = sk.TLE.from_lines(tle_lines)
# We want the orbital state at April 9 2024, 12:00pm UTC
thetime = sk.time(2024, 4, 9, 12, 0, 0)
# 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/starlink30477.mean_motion, 1000)])
# Get position in the TEME frame
pTEME, _vTEME = sk.sgp4(starlink30477, 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 = go.Figure()
fig.add_trace(go.Scattergeo(lat=lat, lon=lon, mode='lines'))
fig.update_layout(margin={"r":0,"t":40,"l":0,"b":0}, title='Ground Track', geo=dict(showland=True, showcountries=True))
fig.show()
fig = go.Figure()
fig.add_trace(go.Scatter(x=[t.datetime() for t in timearr], y=np.array(alt)/1e3, mode='lines'))
fig.update_layout(yaxis_title='Altitude (km)', xaxis_title='Time', font=dict(size=14), title='Altitude vs Time')
fig.show()