Orbital Mean-Element Messages¶
Orbital Mean-Element Messages (OMMs) are a standardized data product defined by the CCSDS Orbit Data Messages standard (CCSDS 502.0-B-3) for exchanging satellite orbital elements in a machine-readable way. They contain the same orbital parameters as TLEs but in a more structured format, and are growing in popularity as the modern replacement for TLE distribution.
OMMs are available from CelesTrak and Space-Track in multiple encodings:
- JSON — the most common and straightforward format
- XML — more verbose with deeper hierarchy, but widely supported
- KVN — key-value notation; imposes very little structure
satkit supports SGP4 propagation of OMMs represented as Python dictionaries. KVN is not supported. The helper sk.omm_from_url(url) fetches an OMM endpoint, auto-detects JSON vs XML, and returns a list of dictionaries that can be passed directly to sk.sgp4 — no external HTTP or XML libraries needed.
Example 1: JSON Format¶
Load a JSON OMM for the International Space Station from CelesTrak, propagate with SGP4, and convert to geodetic coordinates. The JSON format maps directly to a Python dictionary, making it simple to work with.
Note: The live fetch is wrapped in a
try/except: CelesTrak throttles repeated identical GP queries (HTTP 503 — its usage policy asks for at most one request per object every ~2 hours), so when the fetch is unavailable the notebook falls back to a pinned element set embedded below. Cache what you download rather than re-fetching in a loop.
import satkit as sk
# Fetch the latest ephemeris for the International Space Station (ISS).
# sk.omm_from_url auto-detects JSON vs XML response format and returns a list
# of OMM dictionaries that can be passed directly to sk.sgp4.
url = "https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=json"
# A pinned OMM for the ISS (epoch 2026-08-29T15:50:02.012064), used when the live
# fetch is unavailable — CelesTrak throttles repeated identical queries.
# sk.sgp4 accepts this dictionary directly.
ISS_OMM = {
"OBJECT_NAME": "ISS (ZARYA)",
"OBJECT_ID": "1998-067A",
"EPOCH": "2026-08-29T15:50:02.012064",
"MEAN_MOTION": 15.48929126,
"ECCENTRICITY": 0.0004994,
"INCLINATION": 51.6318,
"RA_OF_ASC_NODE": 296.4399,
"ARG_OF_PERICENTER": 88.0334,
"MEAN_ANOMALY": 272.1226,
"EPHEMERIS_TYPE": 0,
"CLASSIFICATION_TYPE": "U",
"NORAD_CAT_ID": 25544,
"ELEMENT_SET_NO": 999,
"REV_AT_EPOCH": 58314,
"BSTAR": 0.00011198,
"MEAN_MOTION_DOT": 5.709e-05,
"MEAN_MOTION_DDOT": 0
}
try:
omm = sk.omm_from_url(url)
except RuntimeError as e:
print(f"Live fetch unavailable ({e}); using the pinned OMM")
omm = [ISS_OMM]
# Get a representative time from the OMM epoch
epoch = sk.time(omm[0]["EPOCH"])
# Create a list of times — once every 10 minutes
time_array = [epoch + sk.duration(minutes=i * 10) for i in range(6)]
# TEME (inertial) output from SGP4
pTEME, _vTEME = sk.sgp4(omm[0], time_array)
# Rotate to Earth-fixed
pITRF = [sk.frametransform.rotation(sk.frame.TEME, sk.frame.ITRF, t) * p for t, p in zip(time_array, pTEME)]
# Geodetic coordinates of space station at given times
coord = [sk.itrfcoord(x) for x in pITRF]
Example 2: XML Format¶
sk.omm_from_url automatically detects the response format. Passing an XML endpoint returns the same list-of-dicts shape as the JSON version, so the downstream SGP4 call is identical — no xmltodict plumbing or manual tree traversal required. Here we fetch the same ISS OMM in XML, propagate it over roughly one orbit, and plot the ground track.
import json
import tempfile
import os
# --- JSON file -----------------------------------------------------------
# Write a small single-satellite OMM JSON file, standing in for a
# Space-Track / CelesTrak bulk download saved to disk.
omm_json = """[{
"OBJECT_NAME": "ISS (ZARYA)", "OBJECT_ID": "1998-067A",
"EPOCH": "2021-10-02T14:10:59.999", "TIME_SYSTEM": "UTC",
"MEAN_ELEMENT_THEORY": "SGP4",
"MEAN_MOTION": "15.48915330", "ECCENTRICITY": "0.0007417",
"INCLINATION": "51.6432", "RA_OF_ASC_NODE": "351.4697",
"ARG_OF_PERICENTER": "130.5364", "MEAN_ANOMALY": "329.6482",
"BSTAR": "0.0001027", "MEAN_MOTION_DOT": "0.00016717",
"MEAN_MOTION_DDOT": "0"
}]"""
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh:
fh.write(omm_json)
json_path = fh.name
# sk.omm_from_file detects JSON vs XML from the content and returns the
# same list of dicts as sk.omm_from_url. (Plain json.load works too: sgp4
# accepts the raw dict, quoted numbers and all.)
omm_list = sk.omm_from_file(json_path)
os.unlink(json_path)
epoch = sk.time(omm_list[0]["EPOCH"])
p_json, v_json = sk.sgp4(omm_list[0], epoch + sk.duration(minutes=30))
print(f"Position from JSON file (m): {p_json}")
# --- XML text ------------------------------------------------------------
# The same OMM in CCSDS XML. sk.omm_from_text parses it directly; no XML
# library or tree-walking needed.
omm_xml = """<?xml version="1.0" encoding="UTF-8"?>
<ndm><omm id="CCSDS_OMM_VERS" version="2.0">
<body><segment>
<metadata>
<OBJECT_NAME>ISS (ZARYA)</OBJECT_NAME><OBJECT_ID>1998-067A</OBJECT_ID>
<CENTER_NAME>EARTH</CENTER_NAME><REF_FRAME>TEME</REF_FRAME>
<TIME_SYSTEM>UTC</TIME_SYSTEM>
<MEAN_ELEMENT_THEORY>SGP4</MEAN_ELEMENT_THEORY>
</metadata>
<data>
<meanElements>
<EPOCH>2021-10-02T14:10:59.999</EPOCH>
<MEAN_MOTION>15.48915330</MEAN_MOTION>
<ECCENTRICITY>0.0007417</ECCENTRICITY>
<INCLINATION>51.6432</INCLINATION>
<RA_OF_ASC_NODE>351.4697</RA_OF_ASC_NODE>
<ARG_OF_PERICENTER>130.5364</ARG_OF_PERICENTER>
<MEAN_ANOMALY>329.6482</MEAN_ANOMALY>
</meanElements>
<tleParameters>
<EPHEMERIS_TYPE>0</EPHEMERIS_TYPE>
<CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE>
<NORAD_CAT_ID>25544</NORAD_CAT_ID>
<ELEMENT_SET_NO>900</ELEMENT_SET_NO>
<REV_AT_EPOCH>29935</REV_AT_EPOCH>
<BSTAR>0.0001027</BSTAR>
<MEAN_MOTION_DOT>0.00016717</MEAN_MOTION_DOT>
<MEAN_MOTION_DDOT>0</MEAN_MOTION_DDOT>
</tleParameters>
</data>
</segment></body>
</omm></ndm>"""
omm_from_xml = sk.omm_from_text(omm_xml)[0]
p_xml, v_xml = sk.sgp4(omm_from_xml, epoch + sk.duration(minutes=30))
print(f"Position from XML text (m): {p_xml}")
# Same OMM, same epoch -> identical states from both formats
assert (p_json == p_xml).all() and (v_json == v_xml).all()
print("JSON and XML routes agree.")
# --- TLE <-> OMM -----------------------------------------------------------
# An OMM dict converts to a TLE object and back; the TLE lines are the
# classic 69-character form of the same element set.
tle = sk.TLE.from_omm(omm_from_xml)
print("\n".join(tle.to_2line()))
print(json.dumps(tle.to_omm(), indent=1)[:200], "...")
Position from JSON file (m): [-5776274.26143913 -1669209.3089887 -3178794.9297256 ]
Position from XML text (m): [-5776274.26143913 -1669209.3089887 -3178794.9297256 ]
JSON and XML routes agree.
1 25544U 98067A 21275.59097221 .00016717 00000-0 10270-3 0 9009
2 25544 51.6432 351.4697 0007417 130.5364 329.6482 15.48915330299350
{
"ARG_OF_PERICENTER": 130.5364,
"BSTAR": 0.0001027,
"CENTER_NAME": "EARTH",
"CLASSIFICATION_TYPE": "U",
"ECCENTRICITY": 0.0007417,
"ELEMENT_SET_NO": 900,
"EPHEMERIS_TYPE": 0,
"EPOCH": "2021-1 ...
Example 3: Loading local OMM files¶
Bulk catalog downloads from Space-Track or CelesTrak usually land on disk as JSON or XML files. No special satkit loader is needed for these: parse them with the Python standard-library json module (or xmltodict for XML) and pass the resulting dictionary — with its standard CCSDS field names — straight to sk.sgp4.
Two notes on the dictionary route:
- satkit consumes the SGP4-relevant fields (
EPOCH, mean elements,BSTAR, mean-motion derivatives) and validatesMEAN_ELEMENT_THEORY/TIME_SYSTEMif present; other CCSDS fields (mass, cross-sections, covariance, …) are simply ignored — they remain available in your dictionary. - For CCSDS XML,
xmltodictproduces the nestedbody → segment → datastructure; satkit understands the nestedmeanElements/tleParametersgroups directly, so you only need to navigate down to thedataelement.
import json
import tempfile
import os
# --- JSON file -----------------------------------------------------------
# Write a small single-satellite OMM JSON file, standing in for a
# Space-Track / CelesTrak bulk download saved to disk.
omm_json = """[{
"OBJECT_NAME": "ISS (ZARYA)", "OBJECT_ID": "1998-067A",
"EPOCH": "2021-10-02T14:10:59.999", "TIME_SYSTEM": "UTC",
"MEAN_ELEMENT_THEORY": "SGP4",
"MEAN_MOTION": "15.48915330", "ECCENTRICITY": "0.0007417",
"INCLINATION": "51.6432", "RA_OF_ASC_NODE": "351.4697",
"ARG_OF_PERICENTER": "130.5364", "MEAN_ANOMALY": "329.6482",
"BSTAR": "0.0001027", "MEAN_MOTION_DOT": "0.00016717",
"MEAN_MOTION_DDOT": "0"
}]"""
with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False) as fh:
fh.write(omm_json)
json_path = fh.name
# sk.omm_from_file detects JSON vs XML from the content and returns the
# same list of dicts as sk.omm_from_url. (Plain json.load works too: sgp4
# accepts the raw dict, quoted numbers and all.)
omm_list = sk.omm_from_file(json_path)
os.unlink(json_path)
epoch = sk.time(omm_list[0]["EPOCH"])
p_json, v_json = sk.sgp4(omm_list[0], epoch + sk.duration(minutes=30))
print(f"Position from JSON file (m): {p_json}")
# --- XML text ------------------------------------------------------------
# The same OMM in CCSDS XML. sk.omm_from_text parses it directly; no XML
# library or tree-walking needed.
omm_xml = """<?xml version="1.0" encoding="UTF-8"?>
<ndm><omm id="CCSDS_OMM_VERS" version="2.0">
<body><segment>
<metadata>
<OBJECT_NAME>ISS (ZARYA)</OBJECT_NAME><OBJECT_ID>1998-067A</OBJECT_ID>
<CENTER_NAME>EARTH</CENTER_NAME><REF_FRAME>TEME</REF_FRAME>
<TIME_SYSTEM>UTC</TIME_SYSTEM>
<MEAN_ELEMENT_THEORY>SGP4</MEAN_ELEMENT_THEORY>
</metadata>
<data>
<meanElements>
<EPOCH>2021-10-02T14:10:59.999</EPOCH>
<MEAN_MOTION>15.48915330</MEAN_MOTION>
<ECCENTRICITY>0.0007417</ECCENTRICITY>
<INCLINATION>51.6432</INCLINATION>
<RA_OF_ASC_NODE>351.4697</RA_OF_ASC_NODE>
<ARG_OF_PERICENTER>130.5364</ARG_OF_PERICENTER>
<MEAN_ANOMALY>329.6482</MEAN_ANOMALY>
</meanElements>
<tleParameters>
<EPHEMERIS_TYPE>0</EPHEMERIS_TYPE>
<CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE>
<NORAD_CAT_ID>25544</NORAD_CAT_ID>
<ELEMENT_SET_NO>900</ELEMENT_SET_NO>
<REV_AT_EPOCH>29935</REV_AT_EPOCH>
<BSTAR>0.0001027</BSTAR>
<MEAN_MOTION_DOT>0.00016717</MEAN_MOTION_DOT>
<MEAN_MOTION_DDOT>0</MEAN_MOTION_DDOT>
</tleParameters>
</data>
</segment></body>
</omm></ndm>"""
omm_from_xml = sk.omm_from_text(omm_xml)[0]
p_xml, v_xml = sk.sgp4(omm_from_xml, epoch + sk.duration(minutes=30))
print(f"Position from XML text (m): {p_xml}")
# Same OMM, same epoch -> identical states from both formats
assert (p_json == p_xml).all() and (v_json == v_xml).all()
print("JSON and XML routes agree.")
# --- TLE <-> OMM -----------------------------------------------------------
# An OMM dict converts to a TLE object and back; the TLE lines are the
# classic 69-character form of the same element set.
tle = sk.TLE.from_omm(omm_from_xml)
print("\n".join(tle.to_2line()))
print(json.dumps(tle.to_omm(), indent=1)[:200], "...")
Position from JSON file (m): [-5776274.26143913 -1669209.3089887 -3178794.9297256 ]
Position from XML text (m): [-5776274.26143913 -1669209.3089887 -3178794.9297256 ]
JSON and XML routes agree.
1 25544U 98067A 21275.59097221 .00016717 00000-0 10270-3 0 9009
2 25544 51.6432 351.4697 0007417 130.5364 329.6482 15.48915330299350
{
"ARG_OF_PERICENTER": 130.5364,
"BSTAR": 0.0001027,
"CENTER_NAME": "EARTH",
"CLASSIFICATION_TYPE": "U",
"ECCENTRICITY": 0.0007417,
"ELEMENT_SET_NO": 900,
"EPHEMERIS_TYPE": 0,
"EPOCH": "2021-1 ...