Skip to content

Two-Line Element Sets, OMMs, and SGP4

API reference for the SGP4 analytic propagator and its inputs. For background, see:

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
tle = satkit.TLE.from_lines([
    "0 ISS (ZARYA)",
    "1 25544U 98067A   21264.51782528  .00002893  00000-0  58680-4 0  9991",
    "2 25544  51.6442 208.5856 0001458  47.2277  50.1624 15.48919419302878",
])[0]
print(tle.name)
# ISS (ZARYA)

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:TLE.from_lines

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:TLE.from_lines

Example
tles = satkit.TLE.from_file("gps-ops.txt")
for tle in tles:
    print(tle.name, tle.satnum)

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 check_checksum, has a wrong checksum); the message gives the line the record starts on and its satellite

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 (SATKIT_OFFLINE=1 or satkit.utils.set_offline(True); no connection is opened), the request fails, or a record fails to parse

Example
tles = sk.TLE.from_url("https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=tle")

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

Example
omm = sk.omm_from_url("https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=json")[0]
tle = sk.TLE.from_omm(omm)
print("\n".join(tle.to_2line()))

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 sat_num is 340000 or above: Alpha-5 (used for the 5-character satellite number field) ends at Z9999 (339999), so the catalog number cannot be written to a TLE. Use to_omm() instead.

Example
lines = tle.to_2line()
print(lines[0])
# 1 25544U 98067A  ...
print(lines[1])
# 2 25544  51.6442 ...

to_3line()

Output as 2 canonical TLE lines preceded by a name line (3-line element set)

Returns:

Type Description
list[str]

3-line element set, name line then 2 canonical TLE lines

Example
lines = tle.to_3line()
for line in lines:
    print(line)

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 TLE object, an OMM dictionary (see :class:OMMDict), or a list of either (or mixing both). Must be a list; tuples and other sequences are not accepted

required
time time | list[time] | list[datetime] | ArrayLike[time] | ArrayLike[datetime]

time(s) at which to compute position and velocity. A naive datetime is local time, not UTC (see :meth:time.from_datetime)

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"): sgp4([tle], t) is (1, 3) and sgp4([tle], [t]) is (1, 1, 3). Empty lists give empty arrays of the matching shape, e.g. sgp4(tle, []) is (0, 3) and sgp4([tle], []) is (1, 0, 3). If errflag is True, a third element is returned: an int32 numpy array of error codes, one per TLE and time — shape (1,) for a single TLE and time, (Ntime,), (Ntle,) or (Ntle, Ntime) otherwise. 0 is success. The codes are the integer values of :class:sgp4_error, so comparing against the enum works elementwise: err == satkit.sgp4_error.success is a boolean array.

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_PERICENTER and MEAN_ANOMALY (plus optional BSTAR, MEAN_MOTION_DOT, MEAN_MOTION_DDOT) is accepted, whether it came from :func:omm_from_url / :func:omm_from_file / :func:omm_from_text, from json.load on a CelesTrak or Space-Track response (numbers may be strings), or from xmltodict on the XML form (the nested meanElements / tleParameters groups are understood). EPOCH may be an RFC 3339 string, a CCSDS day-of-year string (YYYY-DDDThh:mm:ss), a satkit.time or a datetime (naive = local time). Other keys are ignored, except that MEAN_ELEMENT_THEORY must be SGP4, TIME_SYSTEM must be UTC, REF_FRAME must be TEME, CENTER_NAME must be EARTH and EPHEMERIS_TYPE must 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 errflag is 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) raises RuntimeError when errflag is False; for a list, the message gives its index. When errflag is True it does not raise: its rows are NaN at every time and every time carries its init code (sgp4_error.unsupported for 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:OMMDict)

Raises:

Type Description
RuntimeError

if offline mode is on (SATKIT_OFFLINE=1 or satkit.utils.set_offline(True)); no connection is opened

Example
import satkit as sk

omms = sk.omm_from_url("https://celestrak.org/NORAD/elements/gp.php?GROUP=stations&FORMAT=json")
pos, vel = sk.sgp4(omms[0], sk.time(2024, 1, 1))

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:OMMDict)

Example
import satkit as sk

omms = sk.omm_from_file("gp.xml")   # saved from Space-Track or CelesTrak
pos, vel = sk.sgp4(omms, sk.time(2024, 1, 1))

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)

Example
import satkit as sk
import requests

r = requests.get("https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=xml")
omm = sk.omm_from_text(r.text)[0]

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.