924 lines
32 KiB
Python
924 lines
32 KiB
Python
"""Test suite for SGP4."""
|
|
|
|
try:
|
|
from unittest2 import TestCase, SkipTest, main
|
|
except ImportError:
|
|
from unittest import TestCase, SkipTest, main
|
|
|
|
import datetime as dt
|
|
import re
|
|
import os
|
|
import sys
|
|
from doctest import DocTestSuite, ELLIPSIS
|
|
from math import pi, isnan
|
|
from pkgutil import get_data
|
|
|
|
try:
|
|
from io import StringIO
|
|
except ImportError:
|
|
from StringIO import StringIO
|
|
|
|
from sgp4.api import WGS72OLD, WGS72, WGS84, Satrec, jday
|
|
from sgp4.earth_gravity import wgs72
|
|
from sgp4.ext import invjday, newtonnu, rv2coe
|
|
from sgp4.functions import days2mdhms, _day_of_year_to_month_day
|
|
from sgp4.propagation import sgp4, sgp4init
|
|
from sgp4 import api, conveniences, io, omm
|
|
from sgp4.exporter import export_omm, export_tle
|
|
import sgp4.model as model
|
|
|
|
_testcase = TestCase('setUp')
|
|
_testcase.maxDiff = 9999
|
|
assertEqual = _testcase.assertEqual
|
|
assertAlmostEqual = _testcase.assertAlmostEqual
|
|
assertRaises = _testcase.assertRaises
|
|
try:
|
|
assertRaisesRegex = _testcase.assertRaisesRegex
|
|
except AttributeError:
|
|
assertRaisesRegex = _testcase.assertRaisesRegexp
|
|
|
|
error = 2e-7
|
|
rad = 180.0 / pi
|
|
LINE1 = '1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753'
|
|
LINE2 = '2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667'
|
|
BAD2 = '2 00007 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413669'
|
|
VANGUARD_ATTRS = {
|
|
# Identity
|
|
'satnum': 5,
|
|
'satnum_str': '00005',
|
|
'classification': 'U',
|
|
'operationmode': 'i',
|
|
# Time
|
|
'epochyr': 0,
|
|
'epochdays': 179.78495062,
|
|
'jdsatepoch': 2451722.5,
|
|
'jdsatepochF': 0.78495062,
|
|
# Orbit
|
|
'bstar': 2.8098e-05,
|
|
'ndot': 6.96919666594958e-13,
|
|
'nddot': 0.0,
|
|
'ecco': 0.1859667,
|
|
'argpo': 5.790416027488515,
|
|
'inclo': 0.5980929187319208,
|
|
'mo': 0.3373093125574321,
|
|
'no_kozai': 0.04722944544077857,
|
|
'nodeo': 6.08638547138321,
|
|
}
|
|
VANGUARD_EPOCH = 18441.78495062
|
|
|
|
# Handle deprecated assertRaisesRegexp, but allow its use Python 2.6 and 2.7
|
|
if sys.version_info[:2] == (2, 7) or sys.version_info[:2] == (2, 6):
|
|
TestCase.assertRaisesRegex = TestCase.assertRaisesRegexp
|
|
|
|
def skip_under_python2():
|
|
if sys.version_info[0] < 3:
|
|
raise SkipTest('feature not supported under Python 2')
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Core Attributes
|
|
#
|
|
|
|
def test_satrec_built_with_twoline2rv():
|
|
sat = Satrec.twoline2rv(LINE1, LINE2)
|
|
verify_vanguard_1(sat)
|
|
|
|
def test_legacy_built_with_twoline2rv():
|
|
sat = io.twoline2rv(LINE1, LINE2, wgs72)
|
|
verify_vanguard_1(sat, legacy=True)
|
|
|
|
def test_satrec_initialized_with_sgp4init():
|
|
sat = Satrec()
|
|
sat.sgp4init(
|
|
WGS72,
|
|
'i',
|
|
VANGUARD_ATTRS['satnum'],
|
|
VANGUARD_EPOCH,
|
|
*sgp4init_args(VANGUARD_ATTRS)
|
|
)
|
|
verify_vanguard_1(sat)
|
|
|
|
def test_satrec_initialized_with_sgp4init_in_afspc_mode():
|
|
sat = Satrec()
|
|
sat.sgp4init(
|
|
WGS72,
|
|
'a',
|
|
VANGUARD_ATTRS['satnum'],
|
|
VANGUARD_EPOCH,
|
|
*sgp4init_args(VANGUARD_ATTRS)
|
|
)
|
|
assertEqual(sat.operationmode, 'a')
|
|
|
|
def test_legacy_initialized_with_sgp4init():
|
|
sat = model.Satellite()
|
|
sgp4init(
|
|
wgs72, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH,
|
|
*sgp4init_args(VANGUARD_ATTRS) + (sat,)
|
|
)
|
|
verify_vanguard_1(sat, legacy=True)
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Test array API
|
|
|
|
def test_whether_array_logic_writes_nan_values_to_correct_row():
|
|
skip_under_python2()
|
|
|
|
import numpy as np
|
|
|
|
# https://github.com/brandon-rhodes/python-sgp4/issues/87
|
|
l1 = "1 44160U 19006AX 20162.79712247 +.00816806 +19088-3 +34711-2 0 9997"
|
|
l2 = "2 44160 095.2472 272.0808 0216413 032.6694 328.7739 15.58006382062511"
|
|
sat = Satrec.twoline2rv(l1, l2)
|
|
jd0 = np.array([2459054.5, 2459055.5])
|
|
jd1 = np.array([0.79712247, 0.79712247])
|
|
e, r, v = sat.sgp4_array(jd0, jd1)
|
|
assert list(e) == [6, 1]
|
|
assert np.isnan(r).tolist() == [[False, False, False], [True, True, True]]
|
|
assert np.isnan(v).tolist() == [[False, False, False], [True, True, True]]
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Other Officially Supported Routines
|
|
#
|
|
|
|
def test_days2mdhms():
|
|
# See https://github.com/brandon-rhodes/python-sgp4/issues/64
|
|
tup = days2mdhms(2020, 133.35625)
|
|
assertEqual(tup, (5, 12, 8, 33, 0.0))
|
|
|
|
def test_jday2():
|
|
jd, fr = jday(2019, 10, 9, 16, 57, 15)
|
|
assertEqual(jd, 2458765.5)
|
|
assertAlmostEqual(fr, 0.7064236111111111)
|
|
|
|
def test_jday_datetime():
|
|
# define local time
|
|
# UTC equivalent: 2011-11-03 20:05:23+00:00
|
|
|
|
class UTC_plus_4(dt.tzinfo):
|
|
'UTC'
|
|
offset = dt.timedelta(hours=4)
|
|
def utcoffset(self, datetime):
|
|
return self.offset
|
|
def tzname(self, datetime):
|
|
return 'UTC plus 4'
|
|
def dst(self, datetime):
|
|
return self.offset
|
|
|
|
datetime_local = dt.datetime(2011, 11, 4, 0, 5, 23, 0, UTC_plus_4())
|
|
jd, fr = conveniences.jday_datetime(datetime_local)
|
|
|
|
# jd of this date is 2455868.5 + 0.8370717592592593
|
|
assertEqual(jd, 2455868.5)
|
|
assertAlmostEqual(fr, 0.8370717592592593)
|
|
|
|
def test_sat_epoch_datetime():
|
|
sat = Satrec.twoline2rv(LINE1, LINE2)
|
|
datetime = conveniences.sat_epoch_datetime(sat)
|
|
zone = conveniences.UTC
|
|
assertEqual(datetime, dt.datetime(2000, 6, 27, 18, 50, 19, 733568, zone))
|
|
|
|
def test_check_satrec():
|
|
sat = Satrec.twoline2rv(LINE1, LINE2)
|
|
conveniences.check_satrec(sat) # should succeed with no exception
|
|
|
|
sat = Satrec.twoline2rv(LINE1, LINE2.replace('34.2682', '-4.2682'))
|
|
msg = ('satellite parameters out of range:\n'
|
|
' inclo = -0.074494 is outside the range 0 <= inclo < pi')
|
|
with assertRaisesRegex(ValueError, msg):
|
|
conveniences.check_satrec(sat)
|
|
|
|
def test_good_tle_checksum():
|
|
for line in LINE1, LINE2:
|
|
checksum = int(line[-1])
|
|
assertEqual(io.compute_checksum(line), checksum)
|
|
assertEqual(io.fix_checksum(line[:68]), line)
|
|
io.verify_checksum(line)
|
|
|
|
def test_bad_tle_checksum():
|
|
checksum = LINE1[-1]
|
|
assertEqual(checksum, '3')
|
|
bad = LINE1[:68] + '7'
|
|
assertRaises(ValueError, io.verify_checksum, bad)
|
|
assertEqual(io.fix_checksum(bad), LINE1)
|
|
|
|
def test_tle_export():
|
|
"""Check `export_tle()` round-trip using all the TLEs in the test file.
|
|
|
|
This iterates through the satellites in "SGP4-VER.TLE",
|
|
generates `Satrec` objects and exports the TLEs. These exported
|
|
TLEs are then compared to the original TLE, closing the loop (or
|
|
the round-trip).
|
|
|
|
"""
|
|
data = get_data(__name__, 'SGP4-VER.TLE')
|
|
tle_lines = iter(data.decode('ascii').splitlines())
|
|
|
|
# Skip these lines, known errors
|
|
# Resulting TLEs are equivalent (same values in the Satrec object), but they are not the same
|
|
# 25954: BSTAR = 0 results in a negative exp, not positive
|
|
# 29141: BSTAR = 0.13519 results in a negative exp, not positive
|
|
# 33333: Checksum error as expected on both lines
|
|
# 33334: Checksum error as expected on line 1
|
|
# 33335: Checksum error as expected on line 1
|
|
expected_errs_line1 = set([25954, 29141, 33333, 33334, 33335])
|
|
expected_errs_line2 = set([33333, 33335])
|
|
|
|
# Non-standard: omits the ephemeris type integer.
|
|
expected_errs_line1.add(11801)
|
|
|
|
for line1 in tle_lines:
|
|
|
|
if not line1.startswith('1'):
|
|
continue
|
|
|
|
line2 = next(tle_lines)
|
|
|
|
# trim lines to normal TLE string size
|
|
line1 = line1[:69]
|
|
line2 = line2[:69]
|
|
satrec = Satrec.twoline2rv(line1, line2)
|
|
satrec_old = io.twoline2rv(line1, line2, wgs72)
|
|
|
|
# Generate TLE from satrec
|
|
actual_line1, actual_line2 = export_tle(satrec)
|
|
actual_line1_old, actual_line2_old = export_tle(satrec_old)
|
|
|
|
if satrec.satnum not in expected_errs_line1:
|
|
assertEqual(actual_line1, line1)
|
|
assertEqual(actual_line1_old, line1)
|
|
if satrec.satnum not in expected_errs_line2:
|
|
assertEqual(actual_line2, line2)
|
|
assertEqual(actual_line2_old, line2)
|
|
|
|
def test_export_tle_works_with_sgp4init():
|
|
# Does sgp4init() build a sat that can successfully export?
|
|
sat = Satrec()
|
|
args = sgp4init_args(VANGUARD_ATTRS)
|
|
sat.sgp4init(WGS72, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args)
|
|
export_tle(sat)
|
|
|
|
def test_export_tle_raises_error_for_out_of_range_angles():
|
|
# See https://github.com/brandon-rhodes/python-sgp4/issues/70
|
|
for angle in 'inclo', 'nodeo', 'argpo', 'mo':
|
|
sat = Satrec()
|
|
wrong_vanguard_attrs = VANGUARD_ATTRS.copy()
|
|
wrong_vanguard_attrs[angle] = -1.0
|
|
sat.sgp4init(
|
|
WGS84, 'i', wrong_vanguard_attrs['satnum'], VANGUARD_EPOCH,
|
|
*sgp4init_args(wrong_vanguard_attrs)
|
|
)
|
|
assertRaises(ValueError, export_tle, sat)
|
|
|
|
def test_tle_import_export_round_trips():
|
|
for line1, line2 in [(
|
|
'1 44542U 19061A 21180.78220369 -.00000015 00000-0 -66561+1 0 9997',
|
|
'2 44542 54.7025 244.1098 0007981 318.8601 283.5781 1.86231125 12011',
|
|
)]:
|
|
sat = Satrec.twoline2rv(line1, line2)
|
|
outline1, outline2 = export_tle(sat)
|
|
assertEqual(line1, outline1)
|
|
assertEqual(line2, outline2)
|
|
|
|
def test_all_three_gravity_models_with_twoline2rv():
|
|
# The numbers below are those produced by Vallado's C++ code.
|
|
# (Why does the Python version not produce the same values to
|
|
# high accuracy, instead of agreeing to only 4 places?)
|
|
|
|
assert_wgs72old(Satrec.twoline2rv(LINE1, LINE2, WGS72OLD))
|
|
assert_wgs72(Satrec.twoline2rv(LINE1, LINE2, WGS72))
|
|
assert_wgs84(Satrec.twoline2rv(LINE1, LINE2, WGS84))
|
|
|
|
# Not specifying a gravity model should select WGS72.
|
|
|
|
assert_wgs72(Satrec.twoline2rv(LINE1, LINE2))
|
|
|
|
def test_all_three_gravity_models_with_sgp4init():
|
|
# Gravity models specified with sgp4init() should also change the
|
|
# positions generated.
|
|
|
|
sat = Satrec()
|
|
args = sgp4init_args(VANGUARD_ATTRS)
|
|
|
|
sat.sgp4init(WGS72OLD, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args)
|
|
assert_wgs72old(sat)
|
|
|
|
sat.sgp4init(WGS72, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args)
|
|
assert_wgs72(sat)
|
|
|
|
sat.sgp4init(WGS84, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args)
|
|
assert_wgs84(sat)
|
|
|
|
GRAVITY_DIGITS = (
|
|
# Why don't Python and C agree more closely?
|
|
4 if not api.accelerated
|
|
|
|
# Otherwise, try 10 digits. Note that at least 6 digits past the
|
|
# decimal point are necessary to let the test distinguish between
|
|
# WSG72OLD and WGS72. See:
|
|
# https://github.com/conda-forge/sgp4-feedstock/pull/19
|
|
# https://github.com/brandon-rhodes/python-sgp4/issues/69
|
|
else 10
|
|
)
|
|
|
|
def assert_wgs72old(sat):
|
|
e, r, v = sat.sgp4_tsince(309.67110720001529)
|
|
assertAlmostEqual(r[0], -3754.251473242793, GRAVITY_DIGITS)
|
|
assertAlmostEqual(r[1], 7876.346815095482, GRAVITY_DIGITS)
|
|
assertAlmostEqual(r[2], 4719.220855042922, GRAVITY_DIGITS)
|
|
|
|
def assert_wgs72(sat):
|
|
e, r, v = sat.sgp4_tsince(309.67110720001529)
|
|
assertAlmostEqual(r[0], -3754.2514743216166, GRAVITY_DIGITS)
|
|
assertAlmostEqual(r[1], 7876.346817439062, GRAVITY_DIGITS)
|
|
assertAlmostEqual(r[2], 4719.220856478582, GRAVITY_DIGITS)
|
|
|
|
def assert_wgs84(sat):
|
|
e, r, v = sat.sgp4_tsince(309.67110720001529)
|
|
assertAlmostEqual(r[0], -3754.2437675772426, GRAVITY_DIGITS)
|
|
assertAlmostEqual(r[1], 7876.3549956188945, GRAVITY_DIGITS)
|
|
assertAlmostEqual(r[2], 4719.227897029576, GRAVITY_DIGITS)
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Special Cases
|
|
#
|
|
|
|
def test_satnum_leading_spaces():
|
|
# https://github.com/brandon-rhodes/python-sgp4/issues/81
|
|
# https://github.com/brandon-rhodes/python-sgp4/issues/90
|
|
l1 = '1 4859U 21001A 21007.63955392 .00000000 00000+0 00000+0 0 9990'
|
|
l2 = '2 4859 000.0000 000.0000 0000000 000.0000 000.0000 01.00000000 09'
|
|
sat = Satrec.twoline2rv(l1, l2)
|
|
assertEqual(sat.satnum, 4859)
|
|
assertEqual(sat.classification, 'U')
|
|
assertEqual(sat.intldesg, '21001A')
|
|
|
|
def test_satnum_alpha5_encoding():
|
|
def make_sat(satnum_string):
|
|
return Satrec.twoline2rv(LINE1.replace('00005', satnum_string),
|
|
LINE2.replace('00005', satnum_string))
|
|
|
|
# Test cases from https://www.space-track.org/documentation#tle-alpha5
|
|
cases = [(100000, 'A0000'),
|
|
(148493, 'E8493'),
|
|
(182931, 'J2931'),
|
|
(234018, 'P4018'),
|
|
(301928, 'W1928'),
|
|
(339999, 'Z9999')]
|
|
|
|
for satnum, satnum_string in cases:
|
|
sat = make_sat(satnum_string)
|
|
assertEqual(sat.satnum, satnum)
|
|
assertEqual(sat.satnum_str, satnum_string)
|
|
|
|
args = sgp4init_args(VANGUARD_ATTRS)
|
|
for satnum, satnum_string in cases:
|
|
sat.sgp4init(WGS72, 'i', satnum, VANGUARD_EPOCH, *args)
|
|
assertEqual(sat.satnum, satnum)
|
|
|
|
def test_satnum_that_is_too_large():
|
|
sat = Satrec()
|
|
with assertRaisesRegex(ValueError, 'cannot exceed 339999'):
|
|
sat.sgp4init(
|
|
WGS72,
|
|
'i',
|
|
340000,
|
|
VANGUARD_EPOCH,
|
|
*sgp4init_args(VANGUARD_ATTRS)
|
|
)
|
|
|
|
def test_intldesg_with_6_characters():
|
|
sat = Satrec.twoline2rv(LINE1, LINE2)
|
|
assertEqual(sat.intldesg, '58002B')
|
|
|
|
def test_intldesg_with_7_characters():
|
|
sat = Satrec.twoline2rv(
|
|
'1 39444U 13066AE 20110.89708219 .00000236 00000-0'
|
|
' 35029-4 0 9992',
|
|
'2 39444 97.5597 114.3769 0059573 102.0933 258.6965 '
|
|
'14.82098949344697',
|
|
)
|
|
assertEqual(sat.intldesg, '13066AE')
|
|
|
|
def test_1990s_satrec_initialized_with_sgp4init():
|
|
sat = Satrec()
|
|
sat.sgp4init(
|
|
WGS72,
|
|
'i',
|
|
VANGUARD_ATTRS['satnum'],
|
|
VANGUARD_EPOCH - 365.0, # change year 2000 to 1999
|
|
*sgp4init_args(VANGUARD_ATTRS)
|
|
)
|
|
assertEqual(sat.epochyr, 99)
|
|
|
|
def test_tle_that_lacks_checksums():
|
|
Satrec.twoline2rv(
|
|
'1 00058U 60013A 97142.85906518 .00000093 00000-0 +10762-4 0 274',
|
|
'2 00058 028.3286 356.4726 0164991 158.6392 202.1128 13.4602145880282',
|
|
)
|
|
|
|
def test_setters():
|
|
sat = Satrec()
|
|
|
|
sat.classification = 'S'
|
|
assert sat.classification == 'S'
|
|
|
|
sat.intldesg = 'abcdefg'
|
|
assert sat.intldesg == 'abcdefg'
|
|
|
|
sat.ephtype = 23
|
|
assert sat.ephtype == 23
|
|
|
|
sat.elnum = 123
|
|
assert sat.elnum == 123
|
|
|
|
sat.revnum = 1234
|
|
assert sat.revnum == 1234
|
|
|
|
sat.satnum_str = 'abcde'
|
|
assert sat.satnum_str == 'abcde'
|
|
|
|
def test_hyperbolic_orbit():
|
|
# Exercise the newtonnu() code path with asinh() to see whether
|
|
# we can replace it with the one from Python's math module.
|
|
|
|
e0, m = newtonnu(1.0, 2.9) # parabolic
|
|
assertAlmostEqual(e0, 8.238092752965605, places=12)
|
|
assertAlmostEqual(m, 194.60069989482898, places=12)
|
|
|
|
e0, m = newtonnu(1.1, 2.7) # hyperbolic
|
|
assertAlmostEqual(e0, 4.262200676156417, places=12)
|
|
assertAlmostEqual(m, 34.76134082028372, places=12)
|
|
|
|
def test_correct_epochyr():
|
|
# Make sure that the non-standard four-digit epochyr I switched
|
|
# to in the Python version of SGP4 is reverted back to the
|
|
# official behavior when that code is used behind Satrec.
|
|
sat = Satrec.twoline2rv(LINE1, LINE2)
|
|
assertEqual(sat.epochyr, 0)
|
|
|
|
def test_legacy_epochyr():
|
|
# Apparently I saw fit to change the meaning of this attribute
|
|
# in the Python version of SGP4.
|
|
sat = io.twoline2rv(LINE1, LINE2, wgs72)
|
|
assertEqual(sat.epochyr, 2000)
|
|
|
|
def test_support_for_old_no_attribute():
|
|
s = io.twoline2rv(LINE1, LINE2, wgs72)
|
|
assert s.no == s.no_kozai
|
|
|
|
def test_months_and_days():
|
|
# Make sure our hand-written months-and-days routine is perfect.
|
|
|
|
month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
|
|
|
|
day_of_year = 1
|
|
for month, length in enumerate(month_lengths, 1):
|
|
for day in range(1, length + 1):
|
|
tup = _day_of_year_to_month_day(day_of_year, False)
|
|
assertEqual((month, day), tup)
|
|
day_of_year += 1
|
|
|
|
month_lengths[1] = 29 # February, during a leap year
|
|
day_of_year = 1
|
|
for month, length in enumerate(month_lengths, 1):
|
|
for day in range(1, length + 1):
|
|
tup = _day_of_year_to_month_day(day_of_year, True)
|
|
assertEqual((month, day), tup)
|
|
day_of_year += 1
|
|
|
|
def test_december_32():
|
|
# ISS [Orbit 606], whose date is 2019 plus 366.82137887 days.
|
|
# The core SGP4 routines handled this fine, but my hamfisted
|
|
# attempt to provide a Python datetime for "convenience" ran
|
|
# into an overflow.
|
|
a = '1 25544U 98067A 19366.82137887 .00016717 00000-0 10270-3 0 9129'
|
|
b = '2 25544 51.6392 96.6358 0005156 88.7140 271.4601 15.49497216 6061'
|
|
correct_epoch = dt.datetime(2020, 1, 1, 19, 42, 47, 134368)
|
|
|
|
# Legacy API.
|
|
sat = io.twoline2rv(a, b, wgs72)
|
|
assertEqual(sat.epoch, correct_epoch)
|
|
|
|
correct_epoch = correct_epoch.replace(tzinfo=conveniences.UTC)
|
|
|
|
# Modern API.
|
|
sat = Satrec.twoline2rv(a, b)
|
|
assertEqual(conveniences.sat_epoch_datetime(sat), correct_epoch)
|
|
|
|
def test_non_ascii_first_line():
|
|
if sys.version_info < (3,):
|
|
return
|
|
with assertRaisesRegex(ValueError, re.escape("""your TLE lines are broken because they contain non-ASCII characters:
|
|
|
|
1 00005U 58002B 00179.78495062 .00000023\\xa0 00000-0 28098-4 0 4753
|
|
2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667""")):
|
|
io.twoline2rv(LINE1.replace('23 ', '23\xa0'), LINE2, wgs72)
|
|
|
|
def test_non_ascii_second_line():
|
|
if sys.version_info < (3,):
|
|
return
|
|
with assertRaisesRegex(ValueError, re.escape("""your TLE lines are broken because they contain non-ASCII characters:
|
|
|
|
1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753
|
|
2 00005 \\xa034.2682\\xa0348.7242 1859667 331.7664 19.3264 10.82419157413667\
|
|
""")):
|
|
io.twoline2rv(LINE1, LINE2.replace(' 34', '\xa034'), wgs72)
|
|
|
|
def test_bad_first_line():
|
|
with assertRaisesRegex(ValueError, re.escape("""TLE format error
|
|
|
|
The Two-Line Element (TLE) format was designed for punch cards, and so
|
|
is very strict about the position of every period, space, and digit.
|
|
Your line does not quite match. Here is the official format for line 1
|
|
with an N where each digit should go, followed by the line you provided:
|
|
|
|
1 NNNNNC NNNNNAAA NNNNN.NNNNNNNN +.NNNNNNNN +NNNNN-N +NNNNN-N N NNNNN
|
|
1 00005U 58002B 00179.78495062 .000000234 00000-0 28098-4 0 4753""")):
|
|
io.twoline2rv(LINE1.replace('23 ', '234'), LINE2, wgs72)
|
|
|
|
def test_bad_second_line():
|
|
with assertRaisesRegex(ValueError, re.escape("""TLE format error
|
|
|
|
The Two-Line Element (TLE) format was designed for punch cards, and so
|
|
is very strict about the position of every period, space, and digit.
|
|
Your line does not quite match. Here is the official format for line 2
|
|
with an N where each digit should go, followed by the line you provided:
|
|
|
|
2 NNNNN NNN.NNNN NNN.NNNN NNNNNNN NNN.NNNN NNN.NNNN NN.NNNNNNNNNNNNNN
|
|
2 00005 34 .268234 8.7242 1859667 331.7664 19.3264 10.82419157413667""")):
|
|
io.twoline2rv(LINE1, LINE2.replace(' 34', '34 '), wgs72)
|
|
|
|
def test_mismatched_lines():
|
|
msg = "Object numbers in lines 1 and 2 do not match"
|
|
with assertRaisesRegex(ValueError, re.escape(msg)):
|
|
io.twoline2rv(LINE1, BAD2, wgs72)
|
|
|
|
# ------------------------------------------------------------------------
|
|
# Helper routines
|
|
#
|
|
|
|
def verify_vanguard_1(sat, legacy=False):
|
|
attrs = VANGUARD_ATTRS
|
|
|
|
if legacy:
|
|
attrs = attrs.copy()
|
|
del attrs['epochyr']
|
|
del attrs['epochdays']
|
|
del attrs['jdsatepoch']
|
|
del attrs['jdsatepochF']
|
|
|
|
for name, value in attrs.items():
|
|
try:
|
|
assertEqual(getattr(sat, name), value)
|
|
except AssertionError as e:
|
|
message, = e.args
|
|
e.args = ('for attribute %s, %s' % (name, message),)
|
|
raise e
|
|
|
|
def sgp4init_args(d):
|
|
"""Given a dict of orbital parameters, return them in sgp4init order."""
|
|
return (d['bstar'], d['ndot'], d['nddot'], d['ecco'], d['argpo'],
|
|
d['inclo'], d['mo'], d['no_kozai'], d['nodeo'])
|
|
|
|
# ----------------------------------------------------------------------
|
|
# INTEGRATION TEST
|
|
#
|
|
# This runs both new and old satellite objects against every example
|
|
# computation in the official `tcppver.out` test case file. Instead of
|
|
# trying to parse the file, it instead re-generates it using Python
|
|
# satellite objects, then compares the resulting text with the file.
|
|
|
|
def test_satrec_against_tcppver_using_julian_dates():
|
|
|
|
def invoke(satrec, tsince):
|
|
whole, fraction = divmod(tsince / 1440.0, 1.0)
|
|
jd = satrec.jdsatepoch + whole
|
|
fr = satrec.jdsatepochF + fraction
|
|
e, r, v = satrec.sgp4(jd, fr)
|
|
assert e == satrec.error
|
|
return e, r, v
|
|
|
|
run_satellite_against_tcppver(Satrec.twoline2rv, invoke, [1,1,6,6,4,3,6])
|
|
|
|
def test_satrec_against_tcppver_using_tsince():
|
|
|
|
def invoke(satrec, tsince):
|
|
e, r, v = satrec.sgp4_tsince(tsince)
|
|
assert e == satrec.error
|
|
return e, r, v
|
|
|
|
run_satellite_against_tcppver(Satrec.twoline2rv, invoke, [1,1,6,6,4,3,6])
|
|
|
|
def test_legacy_against_tcppver():
|
|
|
|
def make_legacy_satellite(line1, line2):
|
|
sat = io.twoline2rv(line1, line2, wgs72)
|
|
return sat
|
|
|
|
def run_legacy_sgp4(satrec, tsince):
|
|
r, v = sgp4(satrec, tsince)
|
|
return (satrec.error, satrec.error_message), r, v
|
|
|
|
errs = [
|
|
(1, 'mean eccentricity -0.001329 not within range 0.0 <= e < 1.0'),
|
|
(1, 'mean eccentricity -0.001208 not within range 0.0 <= e < 1.0'),
|
|
(6, 'mrt 0.996159 is less than 1.0'
|
|
' indicating the satellite has decayed'),
|
|
(6, 'mrt 0.996252 is less than 1.0'
|
|
' indicating the satellite has decayed'),
|
|
(4, 'semilatus rectum -0.103223 is less than zero'),
|
|
(3, 'perturbed eccentricity -122.217193'
|
|
' not within range 0.0 <= e <= 1.0'),
|
|
(6, 'mrt 0.830534 is less than 1.0'
|
|
' indicating the satellite has decayed'),
|
|
]
|
|
|
|
run_satellite_against_tcppver(make_legacy_satellite, run_legacy_sgp4, errs)
|
|
|
|
def run_satellite_against_tcppver(twoline2rv, invoke, expected_errors):
|
|
# Check whether this library can produce (at least roughly) the
|
|
# output in tcppver.out.
|
|
|
|
data = get_data(__name__, 'tcppver.out')
|
|
data = data.replace(b'\r', b'')
|
|
tcppver_lines = data.decode('ascii').splitlines(True)
|
|
|
|
error_list = []
|
|
actual_lines = list(generate_test_output(twoline2rv, invoke, error_list))
|
|
|
|
assert len(tcppver_lines) == len(actual_lines) == 700
|
|
|
|
previous_data_line = None
|
|
linepairs = zip(tcppver_lines, actual_lines)
|
|
|
|
for lineno, (expected_line, actual_line) in enumerate(linepairs, start=1):
|
|
|
|
if actual_line == '(Use previous data line)':
|
|
actual_line = (' 0.00000000' +
|
|
previous_data_line[17:107])
|
|
|
|
# Compare the lines. The first seven fields are printed
|
|
# to very high precision, so we allow a small error due
|
|
# to rounding differences; the rest are printed to lower
|
|
# precision, and so can be compared textually.
|
|
|
|
if 'xx' in actual_line:
|
|
similar = (actual_line == expected_line)
|
|
else:
|
|
afields = actual_line.split()
|
|
efields = expected_line.split()
|
|
actual7 = [ float(a) for a in afields[:7] ]
|
|
expected7 = [ float(e) for e in efields[:7] ]
|
|
similar = (
|
|
len(actual7) == len(expected7)
|
|
and
|
|
all(
|
|
-error < (a - e) < error
|
|
for a, e in zip(actual7, expected7)
|
|
)
|
|
and
|
|
afields[7:] == efields[7:] # just compare text
|
|
)
|
|
|
|
if not similar:
|
|
raise ValueError(
|
|
'Line %d of output does not match:\n'
|
|
'\n'
|
|
'Expected: %r\n'
|
|
'Got back: %r'
|
|
% (lineno, expected_line, actual_line))
|
|
|
|
if 'xx' not in actual_line:
|
|
previous_data_line = actual_line
|
|
|
|
# Make sure we produced the correct list of errors.
|
|
assertEqual(error_list, expected_errors)
|
|
|
|
def generate_test_output(twoline2rv, invoke, error_list):
|
|
"""Generate lines like those in the test file tcppver.out.
|
|
|
|
This iterates through the satellites in "SGP4-VER.TLE", which are
|
|
each supplemented with a time start/stop/step over which we are
|
|
supposed to print results.
|
|
|
|
"""
|
|
data = get_data(__name__, 'SGP4-VER.TLE')
|
|
tle_lines = iter(data.decode('ascii').splitlines())
|
|
|
|
for line1 in tle_lines:
|
|
|
|
if not line1.startswith('1'):
|
|
continue
|
|
|
|
line2 = next(tle_lines)
|
|
satrec = twoline2rv(line1, line2)
|
|
|
|
yield '%ld xx\n' % (satrec.satnum,)
|
|
|
|
for line in generate_satellite_output(
|
|
satrec, invoke, line2, error_list):
|
|
yield line
|
|
|
|
def generate_satellite_output(satrec, invoke, line2, error_list):
|
|
"""Print a data line for each time in line2's start/stop/step field."""
|
|
|
|
mu = wgs72.mu
|
|
|
|
e, r, v = invoke(satrec, 0.0)
|
|
if isnan(r[0]) and isnan(r[1]) and isnan(r[2]):
|
|
error_list.append(e)
|
|
yield '(Use previous data line)'
|
|
return
|
|
yield format_short_line(0.0, r, v)
|
|
|
|
tstart, tend, tstep = (float(field) for field in line2[69:].split())
|
|
|
|
tsince = tstart
|
|
while tsince <= tend:
|
|
if tsince == tstart == 0.0:
|
|
tsince += tstep
|
|
continue # avoid duplicating the first line
|
|
|
|
e, r, v = invoke(satrec, tsince)
|
|
|
|
if e != 0 and e != (0, None):
|
|
error_list.append(e)
|
|
return
|
|
yield format_long_line(satrec, tsince, mu, r, v)
|
|
|
|
tsince += tstep
|
|
|
|
if tsince - tend < tstep - 1e-6: # do not miss last line!
|
|
e, r, v = invoke(satrec, tend)
|
|
if e != 0 and e != (0, None):
|
|
error_list.append(e)
|
|
return
|
|
yield format_long_line(satrec, tend, mu, r, v)
|
|
|
|
def format_short_line(tsince, r, v):
|
|
"""Short line, using the same format string that testcpp.cpp uses."""
|
|
|
|
return ' %16.8f %16.8f %16.8f %16.8f %12.9f %12.9f %12.9f\n' % (
|
|
tsince, r[0], r[1], r[2], v[0], v[1], v[2])
|
|
|
|
def format_long_line(satrec, tsince, mu, r, v):
|
|
"""Long line, using the same format string that testcpp.cpp uses."""
|
|
|
|
short = format_short_line(tsince, r, v).strip('\n')
|
|
|
|
jd = satrec.jdsatepoch + satrec.jdsatepochF + tsince / 1440.0
|
|
year, mon, day, hr, minute, sec = invjday(jd)
|
|
|
|
(p, a, ecc, incl, node, argp, nu, m, arglat, truelon, lonper
|
|
) = rv2coe(r, v, mu)
|
|
|
|
return short + (
|
|
' %14.6f %8.6f %10.5f %10.5f %10.5f %10.5f %10.5f'
|
|
' %5i%3i%3i %2i:%2i:%9.6f\n'
|
|
) % (
|
|
a, ecc, incl*rad, node*rad, argp*rad, nu*rad,
|
|
m*rad, year, mon, day, hr, minute, sec,
|
|
)
|
|
|
|
# ----------------------------------------------------------------------
|
|
# NEW "OMM" FORMAT TESTS
|
|
|
|
|
|
# https://celestrak.com/satcat/tle.php?CATNR=5
|
|
VANGUARD_TLE = """\
|
|
VANGUARD 1 \n\
|
|
1 00005U 58002B 20287.20333880 -.00000016 00000-0 -22483-4 0 9998
|
|
2 00005 34.2443 225.5254 1845686 162.2516 205.2356 10.84869164218149
|
|
"""
|
|
|
|
# The MARIO satellite was chosen by reading through stations.txt looking
|
|
# for an element set with every field nonzero, since a zero in any field
|
|
# would hide errors in our unit conversion: zero times any conversion
|
|
# factor is zero.
|
|
MARIO_TLE = """\
|
|
MARIO \n\
|
|
1 55123U 98067UQ 23115.44827133 .00787702 29408-3 15680-2 0 9999
|
|
2 55123 51.6242 216.2930 0014649 331.8976 28.1241 15.99081912 18396
|
|
"""
|
|
|
|
# https://celestrak.org/NORAD/elements/gp.php?CATNR=55123&FORMAT=XML
|
|
MARIO_XML = """\
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<ndm xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://sanaregistry.org/r/ndmxml_unqualified/ndmxml-2.0.0-master-2.0.xsd">
|
|
<omm id="CCSDS_OMM_VERS" version="2.0">
|
|
<header><CREATION_DATE/><ORIGINATOR/></header><body><segment><metadata><OBJECT_NAME>MARIO</OBJECT_NAME><OBJECT_ID>1998-067UQ</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>2023-04-25T10:45:30.642912</EPOCH><MEAN_MOTION>15.99081912</MEAN_MOTION><ECCENTRICITY>.0014649</ECCENTRICITY><INCLINATION>51.6242</INCLINATION><RA_OF_ASC_NODE>216.2930</RA_OF_ASC_NODE><ARG_OF_PERICENTER>331.8976</ARG_OF_PERICENTER><MEAN_ANOMALY>28.1241</MEAN_ANOMALY></meanElements><tleParameters><EPHEMERIS_TYPE>0</EPHEMERIS_TYPE><CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE><NORAD_CAT_ID>55123</NORAD_CAT_ID><ELEMENT_SET_NO>999</ELEMENT_SET_NO><REV_AT_EPOCH>1839</REV_AT_EPOCH><BSTAR>.1568E-2</BSTAR><MEAN_MOTION_DOT>.787702E-2</MEAN_MOTION_DOT><MEAN_MOTION_DDOT>.29408E-3</MEAN_MOTION_DDOT></tleParameters></data></segment></body></omm>
|
|
</ndm>
|
|
"""
|
|
|
|
# https://celestrak.com/NORAD/elements/gp.php?CATNR=55123&FORMAT=CSV
|
|
MARIO_CSV = """\
|
|
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
|
|
MARIO,1998-067UQ,2023-04-25T10:45:30.642912,15.99081912,.0014649,51.6242,216.2930,331.8976,28.1241,0,U,55123,999,1839,.1568E-2,.787702E-2,.29408E-3
|
|
"""
|
|
|
|
def test_omm_xml_matches_old_tle():
|
|
line0, line1, line2 = MARIO_TLE.splitlines()
|
|
sat1 = Satrec.twoline2rv(line1, line2)
|
|
|
|
fields = next(omm.parse_xml(StringIO(MARIO_XML)))
|
|
sat2 = Satrec()
|
|
omm.initialize(sat2, fields)
|
|
|
|
assert_satellites_match(sat1, sat2)
|
|
|
|
def test_omm_csv_matches_old_tle():
|
|
line0, line1, line2 = MARIO_TLE.splitlines()
|
|
sat1 = Satrec.twoline2rv(line1, line2)
|
|
|
|
fields = next(omm.parse_csv(StringIO(MARIO_CSV)))
|
|
sat2 = Satrec()
|
|
omm.initialize(sat2, fields)
|
|
|
|
assert_satellites_match(sat1, sat2)
|
|
|
|
def assert_satellites_match(sat1, sat2):
|
|
for attr in dir(sat1):
|
|
if attr.startswith('_'):
|
|
continue
|
|
value1 = getattr(sat1, attr, None)
|
|
if value1 is None:
|
|
continue
|
|
if callable(value1):
|
|
continue
|
|
value2 = getattr(sat2, attr)
|
|
assertEqual(value1, value2, '%s %r != %r' % (attr, value1, value2))
|
|
|
|
def test_omm_uses_gravconst_parameter():
|
|
fields = next(omm.parse_csv(StringIO(MARIO_CSV)))
|
|
|
|
sat1 = Satrec()
|
|
omm.initialize(sat1, fields, WGS72)
|
|
|
|
sat2 = Satrec()
|
|
omm.initialize(sat2, fields, WGS84)
|
|
|
|
assertEqual(sat1.mu, 398600.8)
|
|
assertEqual(sat2.mu, 398600.5)
|
|
|
|
# Live example of OMM:
|
|
# https://celestrak.com/NORAD/elements/gp.php?INTDES=2020-025&FORMAT=JSON-PRETTY
|
|
|
|
def test_omm_export():
|
|
line0, line1, line2 = VANGUARD_TLE.splitlines()
|
|
sat = Satrec.twoline2rv(line1, line2)
|
|
|
|
fields = export_omm(sat, 'VANGUARD 1')
|
|
assertEqual(fields, {
|
|
'ARG_OF_PERICENTER': 162.2516,
|
|
'BSTAR': -2.2483e-05,
|
|
'CENTER_NAME': 'EARTH',
|
|
'CLASSIFICATION_TYPE': 'U',
|
|
'ECCENTRICITY': 0.1845686,
|
|
'ELEMENT_SET_NO': 999,
|
|
'EPHEMERIS_TYPE': 0,
|
|
'EPOCH': '2020-10-13T04:52:48.472320',
|
|
'INCLINATION': 34.2443,
|
|
'MEAN_ANOMALY': 205.2356,
|
|
'MEAN_ELEMENT_THEORY': 'SGP4',
|
|
'MEAN_MOTION': 10.84869164,
|
|
'MEAN_MOTION_DDOT': 0.0,
|
|
'MEAN_MOTION_DOT': -1.6e-07,
|
|
'NORAD_CAT_ID': 5,
|
|
'OBJECT_ID': '1958-002B',
|
|
'OBJECT_NAME': 'VANGUARD 1',
|
|
'RA_OF_ASC_NODE': 225.5254,
|
|
'REF_FRAME': 'TEME',
|
|
'REV_AT_EPOCH': 21814,
|
|
'TIME_SYSTEM': 'UTC',
|
|
})
|
|
|
|
# ----------------------------------------------------------------------
|
|
|
|
def load_tests(loader, tests, ignore):
|
|
"""Run our main documentation as a test, plus all test functions."""
|
|
|
|
from sgp4.wulfgar import add_test_functions
|
|
add_test_functions(loader, tests, __name__)
|
|
|
|
if sys.version_info[0] > 2:
|
|
load_doctests(tests)
|
|
|
|
return tests
|
|
|
|
def load_doctests(tests):
|
|
def setUp(suite):
|
|
suite.olddir = os.getcwd()
|
|
os.chdir(os.path.dirname(__file__))
|
|
suite.oldaccel = api.accelerated
|
|
api.accelerated = True # so doctests pass even if compilation failed
|
|
def tearDown(suite):
|
|
os.chdir(suite.olddir)
|
|
api.accelerated = suite.oldaccel
|
|
|
|
options = dict(optionflags=ELLIPSIS, setUp=setUp, tearDown=tearDown)
|
|
tests.addTests(DocTestSuite('sgp4', **options))
|
|
tests.addTests(DocTestSuite('sgp4.conveniences', **options))
|
|
tests.addTests(DocTestSuite('sgp4.functions', **options))
|
|
|
|
if __name__ == '__main__':
|
|
main()
|