High-Precision Propagation
This example demonstrates high-precision orbit propagation using satkit’s advanced numerical integration capabilities. It propagates a GPS satellite orbit over a 24-hour period and validates the results against high-fidelity SP3 ephemeris data from the European Space Agency (ESA).
Overview
The example performs the following steps:
Loads Reference Data: Reads GPS satellite positions from an SP3 file containing precise orbit determination results (note: velocity is not included)
Rotate to Inerital Frame: Rotate the GPS satellite positions from the ITRF (Earth-fixed) to the GCRF (Inertial) frame
Fits Initial Conditions: Uses numerical optimization to determine the initial velocity and solar radiation pressure coefficient (Cr·A/m) that best match the reference trajectory by integrating the initial state and minimizing the difference between the predicted and reported position
Validates Results: Compares the propagated positions against the SP3 truth data and visualizes the position errors
This example showcases satkit’s ability to achieve meter-level accuracy over extended propagation periods when properly configured with high-fidelity force models and optimized initial conditions.
The SP3 file contains a full 24 hours of satellite positions, recorded once every 5 minutes
[1]:
# Necessary imports
import satkit as sk
import numpy as np
import math as m
import numpy.typing as npt
from scipy.optimize import minimize
import plotly.graph_objects as go
[2]:
# Function to read in the SP3 file
def read_sp3file(fname, satnum=20):
"""
Read SP3 file
(file containing "true" GPS ephemerides)
and output UTC time and position in ITRF frame
"""
# Read in the test vectors
with open(fname, "r") as fd:
lines = fd.readlines()
def line2date(lines):
for line in lines:
year = int(line[3:7])
month = int(line[8:10])
day = int(line[11:13])
hour = int(line[14:16])
minute = int(line[17:19])
sec = float(line[20:32])
yield sk.time(year, month, day, hour, minute, sec)
def line2pos(lines):
for line in lines:
lvals = line.split()
yield np.array([float(lvals[1]), float(lvals[2]), float(lvals[3])])
datelines = list(filter(lambda x: x[0] == "*", lines))
match = f"PG{satnum:02d}"
satlines = list(filter(lambda x: x[0:4] == match, lines))
dates = np.fromiter(line2date(datelines), sk.time)
pitrf = np.stack(np.fromiter(line2pos(satlines), list), axis=0) * 1.0e3 # type: ignore
return (pitrf, dates)
[3]:
# Download SP3 file if not present
fname = './ESA0OPSFIN_20233640000_01D_05M_ORB.SP3'
url = "http://navigation-office.esa.int/products/gnss-products/2294/ESA0OPSFIN_20233640000_01D_05M_ORB.SP3.gz"
import os
if not os.path.exists(fname):
import urllib.request
import gzip
with urllib.request.urlopen(url) as response:
with open(fname, 'wb') as out_file:
with gzip.GzipFile(fileobj=response) as uncompressed:
out_file.write(uncompressed.read())
# Read in the SP3 file
[pitrf, timearr] = read_sp3file(fname)
# Rotate positions to the GCRF frame
pgcrf = np.stack(
np.fromiter(
(q * p for q, p in zip(sk.frametransform.qitrf2gcrf(timearr), pitrf)), list # type: ignore
),
axis=0,
) # type: ignore
# Crude estimation of initial velocity
vgcrf = (pgcrf[1, :] - pgcrf[0, :]) / (timearr[1] - timearr[0]).seconds
# Initial state for non-linear least squares is initial velocity
# and susceptibility to radiation pressuer : Cr A / m
# v0 = np.array([vgcrf[0], vgcrf[1], vgcrf[2], 0.02])
# Skip ahead to the right answer... this takes a long time to run
v0 = np.array([2.47130555e03, 2.94682777e03, -5.34171918e02, 2.13018578e-02])
# Function to minimize ... takes the sum squared error between
# propagated position and SP3 truth position
def minfunc(v):
tstart = timearr[0]
tend = timearr[-1]
settings = sk.propsettings()
settings.gravity_order = 10 # type: ignore
settings.enable_interp = True # type: ignore
satprops = sk.satproperties_static(craoverm = v[3])
res = sk.propagate(
np.concatenate((pgcrf[0, :], v[0:3])),
begin=tstart,
end=tend,
propsettings=settings,
satproperties=satprops,
)
pest = np.zeros((len(timearr), 3))
for i in range(len(timearr)):
pest[i, :] = res.interp(timearr[i])[0:3]
return np.sum(np.sum((pest - pgcrf) ** 2, axis=1), axis=0)
# Minimize difference between true satellite state and propagated state
# by tuning initial velocity and CrAoverM
r = minimize(minfunc, v0, method="Nelder-Mead")
# Propagate with optimized initial conditions
settings = sk.propsettings()
satprops = sk.satproperties_static(craoverm = r.x[3])
res = sk.propagate(
np.concatenate((pgcrf[0, :], r.x[0:3])),
begin=timearr[0],
end=timearr[-1],
propsettings=settings,
satproperties=satprops,
)
perr = np.zeros((len(timearr), 3))
for i in range(len(timearr)):
perr[i, :] = res.interp(timearr[i])[0:3] - pgcrf[i, :]
# print the mean error
print("Mean position error (m): ", np.mean(np.linalg.norm(perr, axis=1)))
# Plot position error
fig = go.Figure()
fig.add_trace(
go.Scatter(x=[t.datetime() for t in timearr], y=perr[:, 0], mode="lines", name="X")
)
fig.add_trace(
go.Scatter(x=[t.datetime() for t in timearr], y=perr[:, 1], mode="lines", name="Y")
)
fig.add_trace(
go.Scatter(x=[t.datetime() for t in timearr], y=perr[:, 2], mode="lines", name="Z")
)
fig.update_layout(
title="Propagation Error vs SP3 Truth",
xaxis_title="Time",
yaxis_title="Position Error (m)",
)
fig.show()
Mean position error (m): 0.872184186566293