"""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 = """\
MARIO1998-067UQEARTHTEMEUTCSGP42023-04-25T10:45:30.64291215.99081912.001464951.6242216.2930331.897628.12410U551239991839.1568E-2.787702E-2.29408E-3
""" # 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()