Two-Line Element Sets, OMMs, and SGP4¶
API reference for the SGP4 analytic propagator and its inputs. For background, see:
- TLEs, SGP4 & OMMs — theory and format descriptions
- Primary sources: Hoots & Roehrich (1980) and Vallado et al. (2006) for SGP4; Vallado & Crawford (2008) for TLE fitting; CCSDS 502.0-B-3 for OMMs
- Tutorials: Two-Line Element Set, Orbital Mean-Element Message, TLE Fitting, SGP4 vs Numerical Propagation
TLE
¶
Two-Line Element Set (TLE) representing a satellite ephemeris
A Two-Line Element Set is a satellite ephemeris format from the 1970s
that is still in wide use. Its mean elements are propagated with the
"Simplified General Perturbations-4" (SGP4) model (satkit.sgp4),
which gives position and velocity in the "TEME" frame (not-quite GCRF).
For details, see: https://en.wikipedia.org/wiki/Two-line_element_set
Catalogs in this format are publicly available at https://www.space-track.org (registration required) and https://celestrak.org (no registration needed).
TLEs sometimes have a "line 0" that includes the name of the satellite.
Load TLEs with TLE.from_lines, TLE.from_file or TLE.from_url;
each returns a list[TLE], even for a single element set.
Example
satnum
property
writable
¶
Satellite number, or equivalently the NORAD ID
intl_desig
property
writable
¶
International designator (e.g. "98067A": launch year, launch number, piece)
desig_year
property
writable
¶
Launch year from the international designator (2-digit, as in the TLE)
desig_launch
property
writable
¶
Launch number of the year from the international designator
desig_piece
property
writable
¶
Piece of the launch from the international designator (e.g. "A")
classification
property
writable
¶
Classification (line 1, column 8): "U", "C" or "S" in practice, but any single ASCII letter round-trips
ephem_type
property
writable
¶
Ephemeris type (usually 0). A value of 4 marks an SGP4-XP element set, which sgp4 rejects: satkit implements classic SGP4 only.
element_num
property
writable
¶
Element set number
rev_num
property
writable
¶
Revolution number at epoch
raan
property
writable
¶
Right Ascension of Ascending Node, in degrees
eccen
property
writable
¶
Satellite eccentricity, unitless, in range [0,1]
mean_anomaly
property
writable
¶
Mean anomaly in degrees
mean_motion
property
writable
¶
Mean motion in revs / day
inclination
property
writable
¶
Inclination, in degrees
epoch
property
writable
¶
TLE epoch
arg_of_perigee
property
writable
¶
Argument of Perigee, in degrees
mean_motion_dot
property
writable
¶
1/2 of first derivative of mean motion, in revs/day^2
Notes
The "1/2" is because that is how number is stored in the TLE.
mean_motion_dot_dot
property
writable
¶
1/6 of 2nd derivative of mean motion, in revs/day^3
Notes
The "1/6" is because that is how number is stored in the TLE.
name
property
writable
¶
The name of the satellite
bstar
property
writable
¶
Drag term (B*) of the satellite, in units of 1 / Earth radii
should be rho0 * Cd * A / 2 / m
Units (which are strange) is multiples of 1 / Earth radius
from_file(filename, *, check_checksum=False)
staticmethod
¶
Load TLEs from a text file, parsed as :meth:TLE.from_lines does
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filename
|
str
|
name of the text file holding the TLE lines |
required |
check_checksum
|
bool
|
as in :meth: |
False
|
Returns:
| Type | Description |
|---|---|
list[TLE]
|
list[TLE]: one TLE per element set in the file |
Raises:
| Type | Description |
|---|---|
ValueError
|
if the file holds no TLEs |
RuntimeError
|
if a record fails to parse; see :meth: |
from_lines(lines, *, check_checksum=False)
staticmethod
¶
Load TLEs from a list of lines
:meth:TLE.from_file and :meth:TLE.from_url parse their text the
same way. See the class docstring for an example.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
lines
|
Sequence[str]
|
the TLE lines (2-line or 3-line format, any number of element sets; any sequence type is accepted, e.g. list or tuple) |
required |
check_checksum
|
bool
|
also verify the checksum digit (column 69) of every data line. Default False. |
False
|
Returns:
| Type | Description |
|---|---|
list[TLE]
|
list[TLE]: one TLE per element set, even if there is only one |
Raises:
| Type | Description |
|---|---|
ValueError
|
if the lines hold no TLEs |
RuntimeError
|
if a record fails to parse (or, with
|
from_url(url)
staticmethod
¶
Load TLEs from a URL, parsing the response as :meth:TLE.from_lines does
Works with any URL that returns plain-text TLE data.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
URL to fetch TLE data from |
required |
Returns:
| Type | Description |
|---|---|
list[TLE]
|
list[TLE]: one TLE per element set in the response |
Raises:
| Type | Description |
|---|---|
ValueError
|
if the response holds no TLEs |
RuntimeError
|
if offline mode is on ( |
from_omm(omm)
staticmethod
¶
Build a TLE from an OMM (Orbital Mean-Element Message) dictionary
The dictionary is the same shape :func:sgp4 accepts: the flat CCSDS
keys of a Space-Track / CelesTrak JSON record (see :class:OMMDict),
or the nested meanElements / tleParameters groups of an
XML-derived dict. Numbers may be strings.
The six mean elements, epoch, BSTAR, MEAN_MOTION_DOT,
MEAN_MOTION_DDOT, NORAD_CAT_ID, ELEMENT_SET_NO,
REV_AT_EPOCH and EPHEMERIS_TYPE carry over; absent optional
values become zero. OBJECT_ID in YYYY-NNNP form becomes the
international designator. Other metadata is dropped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
omm
|
OMMDict
|
OMM dictionary |
required |
Returns:
| Name | Type | Description |
|---|---|---|
TLE |
TLE
|
the equivalent two-line element set |
to_omm()
¶
Render this TLE as an OMM (Orbital Mean-Element Message) dictionary
The result uses the flat CCSDS keys (see :class:OMMDict) with
EPOCH as an RFC 3339 string, angles in degrees and mean motion in
revolutions per day, and can be passed back to :func:sgp4 or
serialized with json.dumps. OBJECT_ID is derived from the
international designator (98067A becomes 1998-067A), and
CLASSIFICATION_TYPE is the TLE's classification letter.
Returns:
| Name | Type | Description |
|---|---|---|
OMMDict |
OMMDict
|
OMM dictionary |
to_2line()
¶
Output as 2 canonical TLE Lines
The element set number (4 columns) and revolution number (5 columns) are written modulo 10,000 and 100,000, so a larger value wraps around the way catalog TLEs roll the revolution counter over.
Returns:
| Type | Description |
|---|---|
list[str]
|
2 canonical TLE Lines |
Raises:
| Type | Description |
|---|---|
ValueError
|
if |
to_3line()
¶
fit_from_states(states, times, epoch, *, gravconst=..., opsmode=...)
staticmethod
¶
Perform non-linear least squares fit of TLE parameters to a list of GCRF states
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
states
|
ArrayLike
|
GCRF states to fit to: an Nx6 array, or a list of 6-element vectors. The first 3 values are positions in meters. The last 3 values are velocities in meters / second. |
required |
times
|
TimeArrayLike
|
List of times corresponding to the states |
required |
epoch
|
TimeScalar
|
Epoch time for the TLE. Must be within range of times. |
required |
Other Parameters:
| Name | Type | Description |
|---|---|---|
gravconst |
sgp4_gravconst
|
gravity model SGP4 uses in the fit. Default is gravconst.wgs72 |
opsmode |
sgp4_opsmode
|
SGP4 ops mode used in the fit. Default is opsmode.afspc |
Returns:
| Type | Description |
|---|---|
tuple[TLE, dict]
|
Fitted TLE and fitting results in a dictionary |
Notes
SGP4 propagator is used to match TLE to the states. The fitted TLE
reproduces the states when propagated with :func:sgp4 under the same
gravconst and opsmode; the defaults match :func:sgp4's.
satkit before 0.24 fitted with WGS84.
Input GCRF states are rotated into TEME frame used by SGP4, with the
full IERS 2010 rotation.
First and second derivatives of mean motion are ignored, as they are not used by SGP4.
Non-linear Levenberg-Marquardt optimization is performed to fit
inclination, eccentricity, RAAN, argument of perigee, mean anomaly,
mean motion, and drag (bstar) to the provided states. The solver
is built on top of the numeris linear algebra crate.
The results dictionary includes the following keys:
status (a :class:tlefitstatus), converged (bool),
orig_norm, best_norm, grad_norm, n_iter,
n_res_evals.
Example
import numpy as np
# Given a list of GCRF states and times
states = [np.array([pos0[0], pos0[1], pos0[2], vel0[0], vel0[1], vel0[2]])]
times = [satkit.time(2024, 1, 1)]
epoch = satkit.time(2024, 1, 1)
tle, results = satkit.TLE.fit_from_states(states, times, epoch)
if results["converged"]:
print("Fit successful")
sgp4(tle, time, *, gravconst=..., opsmode=..., errflag)
¶
sgp4(tle: TLE | OMMDict | list[TLE] | list[OMMDict] | list[TLE | OMMDict], time: TimeInput, *, gravconst: sgp4_gravconst = ..., opsmode: sgp4_opsmode = ..., errflag: typing.Literal[True]) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.int32]]
sgp4(tle: TLE | OMMDict | list[TLE] | list[OMMDict] | list[TLE | OMMDict], time: TimeInput, *, gravconst: sgp4_gravconst = ..., opsmode: sgp4_opsmode = ..., errflag: typing.Literal[False] = False) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]
sgp4(tle: TLE | OMMDict | list[TLE] | list[OMMDict] | list[TLE | OMMDict], time: TimeInput, *, gravconst: sgp4_gravconst = ..., opsmode: sgp4_opsmode = ..., errflag: bool) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]] | tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.int32]]
SGP-4 propagator for TLE
Run Simplified General Perturbations (SGP)-4 propagator on Two-Line Element Set to output satellite position and velocity at given time in the "TEME" coordinate system.
A detailed description is in Vallado, Crawford, Hujsak & Kelso, "Revisiting Spacetrack Report #3", AIAA 2006-6753: https://doi.org/10.2514/6.2006-6753 https://celestrak.org/publications/AIAA/2006-6753/AIAA-2006-6753-Rev3.pdf
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tle
|
TLE | OMMDict | list[TLE | OMMDict]
|
element set(s) to propagate: a
|
required |
time
|
time | list[time] | list[datetime] | ArrayLike[time] | ArrayLike[datetime]
|
time(s) at which to compute position and velocity.
A naive |
required |
Other Parameters:
| Name | Type | Description |
|---|---|---|
gravconst |
sgp4_gravconst
|
gravity constant to use. Default is gravconst.wgs72 |
opsmode |
sgp4_opsmode
|
opsmode.afspc (Air Force Space Command) or opsmode.improved. Default is opsmode.afspc |
errflag |
bool
|
whether or not to output error conditions for each TLE and time output. Default is False (this is likely rarely needed, but can be useful for debugging) |
Returns:
| Type | Description |
|---|---|
tuple[NDArray[float64], NDArray[float64]] | tuple[NDArray[float64], NDArray[float64], NDArray[int32]]
|
tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: position and velocity
in meters and meters/second, respectively,
in the TEME frame at each of the "Ntime" input times and each of the "Ntle" tles.
Shape is (3,) for a single TLE and single time, (Ntime, 3) for a single TLE
and multiple times, (Ntle, 3) for a list of TLEs and a single time, and
(Ntle, Ntime, 3) for a list of TLEs and multiple times.
A list keeps its axis even with one element ("list in, list out"):
|
Notes
- Units: the canonical Vallado SGP4 implementation (and most other SGP4 libraries) return position in kilometers and velocity in kilometers/second. satkit converts these to meters and meters/second so that SGP4 output is consistent with every other position and velocity in the library.
- OMM dictionaries: any dict with the CCSDS keys
EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTERandMEAN_ANOMALY(plus optionalBSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT) is accepted, whether it came from :func:omm_from_url/ :func:omm_from_file/ :func:omm_from_text, fromjson.loadon a CelesTrak or Space-Track response (numbers may be strings), or fromxmltodicton the XML form (the nestedmeanElements/tleParametersgroups are understood).EPOCHmay be an RFC 3339 string, a CCSDS day-of-year string (YYYY-DDDThh:mm:ss), asatkit.timeor adatetime(naive = local time). Other keys are ignored, except thatMEAN_ELEMENT_THEORYmust beSGP4,TIME_SYSTEMmust beUTC,REF_FRAMEmust beTEME,CENTER_NAMEmust beEARTHandEPHEMERIS_TYPEmust not be 4 (SGP4-XP) when present. - The "TEME" frame of the SGP4 state vectors is not a truly inertial frame. It is a "True Equator Mean Equinox"
frame: its axes are the true equator and the mean equinox of date, i.e. of each output time (not of the
TLE epoch), so the frame itself moves with precession and nutation. Rotate to GCRF with
frametransform.rotation(frame.TEME, frame.GCRF, time). - Errors: a time at which propagation fails (e.g. the orbit has decayed) gives a NaN row,
with its code in the error array when
errflagis True. An element set that cannot be initialized at all (e.g. decayed or eccentricity out of range at epoch, or an SGP4-XP set) raisesRuntimeErrorwhenerrflagis False; for a list, the message gives its index. Whenerrflagis True it does not raise: its rows are NaN at every time and every time carries its init code (sgp4_error.unsupportedfor an SGP4-XP set or OMM metadata SGP4 cannot use), so one bad element set does not fail a list. - Leap seconds: the time since epoch is the physical (SI) time elapsed. Across a leap second this is one second more than the difference of the UTC labels that Vallado's reference code and python-sgp4 use, so satkit differs from them by 1 s of along-track motion (~7.6 km at LEO) per leap second between epoch and time. This is deliberate: the satellite really flies 86,401 s over such a day, and the SGP4 mean motion is per SI day.
Example
import numpy as np
import satkit
lines = [
"0 INTELSAT 902",
"1 26900U 01039A 06106.74503247 .00000045 00000-0 10000-3 0 8290",
"2 26900 0.0164 266.5378 0003319 86.1794 182.2590 1.00273847 16981",
]
tle = satkit.TLE.from_lines(lines)[0] # from_lines always returns a list
tm = tle.epoch
# Compute TEME position & velocity at epoch
pteme, vteme = satkit.sgp4(tle, tm)
# Rotate to ITRF frame; the velocity also loses the Earth-rotation term
q = satkit.frametransform.qteme2itrf(tm)
pitrf = q * pteme
vitrf = q * vteme - np.cross(np.array([0, 0, satkit.consts.omega_earth]), pitrf)
# convert to ITRF coordinate object
coord = satkit.itrfcoord(pitrf)
# Print ITRF coordinate object location
print(coord)
# ITRFCoord(lat: -0.0362 deg, lon: 62.0172 deg, hae: 35799.52 km)
# Error codes per output
pteme, vteme, err = satkit.sgp4(tle, tm, errflag=True)
assert (err == satkit.sgp4_error.success).all()
# Query the OMM for the International Space Station (ISS)
url = "https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=json"
omm = satkit.omm_from_url(url)[0]
# Get a representative time from the output
epoch = satkit.time(omm["EPOCH"])
# Compute TEME position & velocity at epoch
pteme, vteme = satkit.sgp4(omm, epoch)
omm_from_url(url)
¶
Load OMM(s) from a URL as a list of dictionaries
Fetches the content at the given URL and auto-detects JSON vs XML format.
Returns a list of dictionaries that can be passed directly to :func:sgp4.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
url
|
str
|
URL to fetch OMM data from (e.g. CelesTrak or Space-Track endpoint) |
required |
Returns:
| Type | Description |
|---|---|
list[OMMDict]
|
list[OMMDict]: one dictionary per message, with every CCSDS field the
source provided plus its extra keys (see :class: |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
if offline mode is on ( |
omm_from_file(filename)
¶
Load OMM(s) from a JSON or XML file as a list of dictionaries
The format is detected from the content, not the file extension: a file
starting with [ or { is JSON (one message or an array of them), one
starting with < is a CCSDS NDM/XML document. KVN is not supported.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
filename
|
str
|
path to the file |
required |
Returns:
| Type | Description |
|---|---|
list[OMMDict]
|
list[OMMDict]: one dictionary per message (see :class: |
omm_from_text(text)
¶
Parse OMM(s) from JSON or XML text as a list of dictionaries
Text starting with [ or { is parsed as JSON (one message or an
array of them), text starting with < as a CCSDS NDM/XML document.
KVN is not supported.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
text
|
str
|
the document |
required |
Returns:
| Type | Description |
|---|---|
list[OMMDict]
|
list[OMMDict]: one dictionary per message (see :class: |
OMMDict
¶
An Orbital Mean-Element Message (OMM) as a plain dictionary
This is the shape returned by :func:omm_from_url, :func:omm_from_file,
:func:omm_from_text and :meth:TLE.to_omm, and accepted by :func:sgp4
and :meth:TLE.from_omm. It is an ordinary dict (this class exists for
type checking only): the keys are the CCSDS 502.0-B-3 field names, so
json.load on a Space-Track or CelesTrak JSON response produces one
directly. Every key is optional in the type; :func:sgp4 needs EPOCH
and the six mean elements.
Units follow the TLE convention: angles in degrees, MEAN_MOTION in
revolutions per day, MEAN_MOTION_DOT in rev/day² and
MEAN_MOTION_DDOT in rev/day³ (the TLE fields, i.e. ṅ/2 and n̈/6),
BSTAR in inverse Earth radii, GM in km³/s², MASS in kg, areas in
m², BTERM and AGOM in m²/kg.
The loaders return numbers as float / int (Space-Track quotes them
as strings in its JSON; :func:sgp4 accepts either), EPOCH as an
RFC 3339 string, absent optional fields omitted, and every key that is not
a CCSDS field (Space-Track's OBJECT_TYPE, RCS_SIZE, LAUNCH_DATE,
TLE_LINE1, ...; XML USER_DEFINED parameters) kept verbatim.