Orbital Mean-Element Messages¶
Orbital Mean-Element Messages (OMMs) are a standardized data product defined by CCSDS 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.
import satkit as sk
# Fetch the current 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"
omm = sk.omm_from_url(url)
# 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 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 ISS ephemeris in XML format. sk.omm_from_url auto-detects
# the format and normalizes the result to the same list-of-dicts shape as the
# JSON example above — no xmltodict or hand-written tree walking needed.
url = "https://celestrak.org/NORAD/elements/gp.php?CATNR=25544&FORMAT=xml"
omm = sk.omm_from_url(url)[0]
# Get a representative time from the OMM epoch
epoch = sk.time(omm["EPOCH"])
# Time array spanning roughly one orbit at 1-minute cadence
time_array = [epoch + sk.duration(minutes=i) for i in range(97)]
# TEME (inertial) output from SGP4
pTEME, _vTEME = sk.sgp4(omm, 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)]
coord = [sk.itrfcoord(x) for x in pITRF]
lat, lon, alt = zip(*[(c.latitude_deg, c.longitude_deg, c.altitude) for c in coord])
# Break ground track at longitude discontinuities (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)
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)
for lo, la in zip(lon_segs, lat_segs):
ax.plot(lo, la, linewidth=1.5, color="C0", transform=ccrs.PlateCarree())
ax.set_title("ISS Ground Track")
ax.set_global()
plt.tight_layout()
plt.show()
fig, ax = plt.subplots(figsize=(10, 4))
ax.plot([t.datetime() for t in time_array], np.array(alt) / 1e3)
ax.set_xlabel("Time")
ax.set_ylabel("Altitude (km)")
ax.set_title("ISS Altitude vs Time")
fig.autofmt_xdate()
plt.tight_layout()
plt.show()
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
import xmltodict
# --- 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
# The entire "loader": stdlib json -> list of dicts -> sgp4
with open(json_path) as fh:
omm_list = json.load(fh)
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}")
os.unlink(json_path)
# --- XML file ------------------------------------------------------------
# The same OMM in CCSDS XML. xmltodict parses it; navigate down to the
# body/segment/data element and hand that dictionary to sgp4 — the nested
# meanElements / tleParameters groups are understood directly.
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>
<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>"""
parsed = xmltodict.parse(omm_xml)
# Multi-satellite files have a list under ["ndm"]["omm"]; this one is a
# single entry, so index straight down to the data element.
data = parsed["ndm"]["omm"]["body"]["segment"]["data"]
p_xml, v_xml = sk.sgp4(data, epoch + sk.duration(minutes=30))
print(f"Position from XML file (m): {p_xml}")
# Same OMM, same epoch -> identical states from both file formats
assert (p_json == p_xml).all() and (v_json == v_xml).all()
print("JSON and XML file routes agree.")
Position from JSON file (m): [-5776274.26143913 -1669209.3089887 -3178794.9297256 ] Position from XML file (m): [-5776274.26143913 -1669209.3089887 -3178794.9297256 ] JSON and XML file routes agree.