1"""Test suite for SGP4.""" 2 3try: 4 from unittest2 import TestCase, main 5except: 6 from unittest import TestCase, main 7 8import datetime as dt 9import platform 10import re 11import os 12import sys 13from doctest import DocTestSuite, ELLIPSIS 14from math import pi, isnan 15from pkgutil import get_data 16 17try: 18 from io import StringIO 19except ImportError: 20 from StringIO import StringIO 21 22import numpy as np 23 24from sgp4.api import WGS72OLD, WGS72, WGS84, Satrec, jday, accelerated 25from sgp4.earth_gravity import wgs72 26from sgp4.ext import invjday, newtonnu, rv2coe 27from sgp4.functions import days2mdhms, _day_of_year_to_month_day 28from sgp4.propagation import sgp4, sgp4init 29from sgp4 import conveniences, io, omm 30from sgp4.exporter import export_omm, export_tle 31import sgp4.model as model 32 33_testcase = TestCase('setUp') 34_testcase.maxDiff = 9999 35assertEqual = _testcase.assertEqual 36assertAlmostEqual = _testcase.assertAlmostEqual 37assertRaises = _testcase.assertRaises 38assertRaisesRegex = getattr(_testcase, 'assertRaisesRegex', 39 _testcase.assertRaisesRegexp) 40 41error = 2e-7 42rad = 180.0 / pi 43LINE1 = '1 00005U 58002B 00179.78495062 .00000023 00000-0 28098-4 0 4753' 44LINE2 = '2 00005 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413667' 45BAD2 = '2 00007 34.2682 348.7242 1859667 331.7664 19.3264 10.82419157413669' 46VANGUARD_ATTRS = { 47 # Identity 48 'satnum': 5, 49 'operationmode': 'i', 50 # Time 51 'epochyr': 0, 52 'epochdays': 179.78495062, 53 'jdsatepoch': 2451722.5, 54 'jdsatepochF': 0.78495062, 55 # Orbit 56 'bstar': 2.8098e-05, 57 'ndot': 6.96919666594958e-13, 58 'nddot': 0.0, 59 'ecco': 0.1859667, 60 'argpo': 5.790416027488515, 61 'inclo': 0.5980929187319208, 62 'mo': 0.3373093125574321, 63 'no_kozai': 0.04722944544077857, 64 'nodeo': 6.08638547138321, 65} 66VANGUARD_EPOCH = 18441.78495062 67 68# Handle deprecated assertRaisesRegexp, but allow its use Python 2.6 and 2.7 69if sys.version_info[:2] == (2, 7) or sys.version_info[:2] == (2, 6): 70 TestCase.assertRaisesRegex = TestCase.assertRaisesRegexp 71 72# ------------------------------------------------------------------------ 73# Core Attributes 74# 75 76def test_satrec_built_with_twoline2rv(): 77 sat = Satrec.twoline2rv(LINE1, LINE2) 78 verify_vanguard_1(sat) 79 80def test_legacy_built_with_twoline2rv(): 81 sat = io.twoline2rv(LINE1, LINE2, wgs72) 82 verify_vanguard_1(sat, legacy=True) 83 84def test_satrec_initialized_with_sgp4init(): 85 sat = Satrec() 86 sat.sgp4init( 87 WGS72, 88 'i', 89 VANGUARD_ATTRS['satnum'], 90 VANGUARD_EPOCH, 91 *sgp4init_args(VANGUARD_ATTRS) 92 ) 93 verify_vanguard_1(sat) 94 95def test_satrec_initialized_with_sgp4init_in_afspc_mode(): 96 sat = Satrec() 97 sat.sgp4init( 98 WGS72, 99 'a', 100 VANGUARD_ATTRS['satnum'], 101 VANGUARD_EPOCH, 102 *sgp4init_args(VANGUARD_ATTRS) 103 ) 104 assertEqual(sat.operationmode, 'a') 105 106def test_legacy_initialized_with_sgp4init(): 107 sat = model.Satellite() 108 sgp4init( 109 wgs72, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, 110 *sgp4init_args(VANGUARD_ATTRS) + (sat,) 111 ) 112 verify_vanguard_1(sat, legacy=True) 113 114# ------------------------------------------------------------------------ 115# Test array API 116 117def test_whether_array_logic_writes_nan_values_to_correct_row(): 118 # https://github.com/brandon-rhodes/python-sgp4/issues/87 119 l1 = "1 44160U 19006AX 20162.79712247 +.00816806 +19088-3 +34711-2 0 9997" 120 l2 = "2 44160 095.2472 272.0808 0216413 032.6694 328.7739 15.58006382062511" 121 sat = Satrec.twoline2rv(l1, l2) 122 jd0 = np.array([2459054.5, 2459055.5]) 123 jd1 = np.array([0.79712247, 0.79712247]) 124 e, r, v = sat.sgp4_array(jd0, jd1) 125 assert list(e) == [6, 1] 126 assert np.isnan(r).tolist() == [[False, False, False], [True, True, True]] 127 assert np.isnan(v).tolist() == [[False, False, False], [True, True, True]] 128 129# ------------------------------------------------------------------------ 130# Other Officially Supported Routines 131# 132 133def test_days2mdhms(): 134 # See https://github.com/brandon-rhodes/python-sgp4/issues/64 135 tup = days2mdhms(2020, 133.35625) 136 assertEqual(tup, (5, 12, 8, 33, 0.0)) 137 138def test_jday2(): 139 jd, fr = jday(2019, 10, 9, 16, 57, 15) 140 assertEqual(jd, 2458765.5) 141 assertAlmostEqual(fr, 0.7064236111111111) 142 143def test_jday_datetime(): 144 # define local time 145 # UTC equivalent: 2011-11-03 20:05:23+00:00 146 147 class UTC_plus_4(dt.tzinfo): 148 'UTC' 149 offset = dt.timedelta(hours=4) 150 def utcoffset(self, datetime): 151 return self.offset 152 def tzname(self, datetime): 153 return 'UTC plus 4' 154 def dst(self, datetime): 155 return self.offset 156 157 datetime_local = dt.datetime(2011, 11, 4, 0, 5, 23, 0, UTC_plus_4()) 158 jd, fr = conveniences.jday_datetime(datetime_local) 159 160 # jd of this date is 2455868.5 + 0.8370717592592593 161 assertEqual(jd, 2455868.5) 162 assertAlmostEqual(fr, 0.8370717592592593) 163 164def test_sat_epoch_datetime(): 165 sat = Satrec.twoline2rv(LINE1, LINE2) 166 datetime = conveniences.sat_epoch_datetime(sat) 167 zone = conveniences.UTC 168 assertEqual(datetime, dt.datetime(2000, 6, 27, 18, 50, 19, 733568, zone)) 169 170def test_good_tle_checksum(): 171 for line in LINE1, LINE2: 172 checksum = int(line[-1]) 173 assertEqual(io.compute_checksum(line), checksum) 174 assertEqual(io.fix_checksum(line[:68]), line) 175 io.verify_checksum(line) 176 177def test_bad_tle_checksum(): 178 checksum = LINE1[-1] 179 assertEqual(checksum, '3') 180 bad = LINE1[:68] + '7' 181 assertRaises(ValueError, io.verify_checksum, bad) 182 assertEqual(io.fix_checksum(bad), LINE1) 183 184def test_tle_export(): 185 """Check `export_tle()` round-trip using all the TLEs in the test file. 186 187 This iterates through the satellites in "SGP4-VER.TLE", 188 generates `Satrec` objects and exports the TLEs. These exported 189 TLEs are then compared to the original TLE, closing the loop (or 190 the round-trip). 191 192 """ 193 data = get_data(__name__, 'SGP4-VER.TLE') 194 tle_lines = iter(data.decode('ascii').splitlines()) 195 196 # Skip these lines, known errors 197 # Resulting TLEs are equivalent (same values in the Satrec object), but they are not the same 198 # 25954: BSTAR = 0 results in a negative exp, not positive 199 # 29141: BSTAR = 0.13519 results in a negative exp, not positive 200 # 33333: Checksum error as expected on both lines 201 # 33334: Checksum error as expected on line 1 202 # 33335: Checksum error as expected on line 1 203 expected_errs_line1 = set([25954, 29141, 33333, 33334, 33335]) 204 expected_errs_line2 = set([33333, 33335]) 205 206 # Non-standard: omits the ephemeris type integer. 207 expected_errs_line1.add(11801) 208 209 for line1 in tle_lines: 210 211 if not line1.startswith('1'): 212 continue 213 214 line2 = next(tle_lines) 215 216 # trim lines to normal TLE string size 217 line1 = line1[:69] 218 line2 = line2[:69] 219 satrec = Satrec.twoline2rv(line1, line2) 220 satrec_old = io.twoline2rv(line1, line2, wgs72) 221 222 # Generate TLE from satrec 223 out_line1, out_line2 = export_tle(satrec) 224 out_line1_old, out_line2_old = export_tle(satrec_old) 225 226 if satrec.satnum not in expected_errs_line1: 227 assertEqual(out_line1, line1) 228 assertEqual(out_line1_old, line1) 229 if satrec.satnum not in expected_errs_line2: 230 assertEqual(out_line2, line2) 231 assertEqual(out_line2_old, line2) 232 233def test_export_tle_raises_error_for_out_of_range_angles(): 234 # See https://github.com/brandon-rhodes/python-sgp4/issues/70 235 for angle in 'inclo', 'nodeo', 'argpo', 'mo': 236 sat = Satrec() 237 wrong_vanguard_attrs = VANGUARD_ATTRS.copy() 238 wrong_vanguard_attrs[angle] = -1.0 239 sat.sgp4init( 240 WGS84, 'i', wrong_vanguard_attrs['satnum'], VANGUARD_EPOCH, 241 *sgp4init_args(wrong_vanguard_attrs) 242 ) 243 assertRaises(ValueError, export_tle, sat) 244 245def test_tle_import_export_round_trips(): 246 for line1, line2 in [( 247 '1 44542U 19061A 21180.78220369 -.00000015 00000-0 -66561+1 0 9997', 248 '2 44542 54.7025 244.1098 0007981 318.8601 283.5781 1.86231125 12011', 249 )]: 250 sat = Satrec.twoline2rv(line1, line2) 251 outline1, outline2 = export_tle(sat) 252 assertEqual(line1, outline1) 253 assertEqual(line2, outline2) 254 255def test_all_three_gravity_models_with_twoline2rv(): 256 # The numbers below are those produced by Vallado's C++ code. 257 # (Why does the Python version not produce the same values to 258 # high accuracy, instead of agreeing to only 4 places?) 259 260 assert_wgs72old(Satrec.twoline2rv(LINE1, LINE2, WGS72OLD)) 261 assert_wgs72(Satrec.twoline2rv(LINE1, LINE2, WGS72)) 262 assert_wgs84(Satrec.twoline2rv(LINE1, LINE2, WGS84)) 263 264 # Not specifying a gravity model should select WGS72. 265 266 assert_wgs72(Satrec.twoline2rv(LINE1, LINE2)) 267 268def test_all_three_gravity_models_with_sgp4init(): 269 # Gravity models specified with sgp4init() should also change the 270 # positions generated. 271 272 sat = Satrec() 273 args = sgp4init_args(VANGUARD_ATTRS) 274 275 sat.sgp4init(WGS72OLD, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args) 276 assert_wgs72old(sat) 277 278 sat.sgp4init(WGS72, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args) 279 assert_wgs72(sat) 280 281 sat.sgp4init(WGS84, 'i', VANGUARD_ATTRS['satnum'], VANGUARD_EPOCH, *args) 282 assert_wgs84(sat) 283 284GRAVITY_DIGITS = ( 285 # Why don't Python and C agree more closely? 286 4 if not accelerated 287 288 # Insist on very high precision on my Linux laptop, to signal me if 289 # some future adjustment subtlely changes the library's results. 290 else 12 if platform.system() == 'Linux' and platform.machine() == 'x86_64' 291 292 # Otherwise, reduce our expectations. Note that at least 6 digits 293 # past the decimal point are necessary to let the test distinguish 294 # between WSG72OLD and WGS72. See: 295 # https://github.com/conda-forge/sgp4-feedstock/pull/19 296 # https://github.com/brandon-rhodes/python-sgp4/issues/69 297 else 10 298) 299 300def assert_wgs72old(sat): 301 e, r, v = sat.sgp4_tsince(309.67110720001529) 302 assertAlmostEqual(r[0], -3754.251473242793, GRAVITY_DIGITS) 303 assertAlmostEqual(r[1], 7876.346815095482, GRAVITY_DIGITS) 304 assertAlmostEqual(r[2], 4719.220855042922, GRAVITY_DIGITS) 305 306def assert_wgs72(sat): 307 e, r, v = sat.sgp4_tsince(309.67110720001529) 308 assertAlmostEqual(r[0], -3754.2514743216166, GRAVITY_DIGITS) 309 assertAlmostEqual(r[1], 7876.346817439062, GRAVITY_DIGITS) 310 assertAlmostEqual(r[2], 4719.220856478582, GRAVITY_DIGITS) 311 312def assert_wgs84(sat): 313 e, r, v = sat.sgp4_tsince(309.67110720001529) 314 assertAlmostEqual(r[0], -3754.2437675772426, GRAVITY_DIGITS) 315 assertAlmostEqual(r[1], 7876.3549956188945, GRAVITY_DIGITS) 316 assertAlmostEqual(r[2], 4719.227897029576, GRAVITY_DIGITS) 317 318# ------------------------------------------------------------------------ 319# Special Cases 320# 321 322def test_satnum_leading_spaces(): 323 # https://github.com/brandon-rhodes/python-sgp4/issues/81 324 # https://github.com/brandon-rhodes/python-sgp4/issues/90 325 l1 = '1 4859U 21001A 21007.63955392 .00000000 00000+0 00000+0 0 9990' 326 l2 = '2 4859 000.0000 000.0000 0000000 000.0000 000.0000 01.00000000 09' 327 sat = Satrec.twoline2rv(l1, l2) 328 assertEqual(sat.satnum, 4859) 329 assertEqual(sat.classification, 'U') 330 assertEqual(sat.intldesg, '21001A') 331 332def test_satnum_alpha5_encoding(): 333 def make_sat(satnum_string): 334 return Satrec.twoline2rv(LINE1.replace('00005', satnum_string), 335 LINE2.replace('00005', satnum_string)) 336 337 # Test cases from https://www.space-track.org/documentation#tle-alpha5 338 cases = [(100000, 'A0000'), 339 (148493, 'E8493'), 340 (182931, 'J2931'), 341 (234018, 'P4018'), 342 (301928, 'W1928'), 343 (339999, 'Z9999')] 344 345 for satnum, satnum_string in cases: 346 sat = make_sat(satnum_string) 347 assert sat.satnum == satnum 348 349 args = sgp4init_args(VANGUARD_ATTRS) 350 for satnum, satnum_string in cases: 351 sat.sgp4init(WGS72, 'i', satnum, VANGUARD_EPOCH, *args) 352 assert sat.satnum == satnum 353 354def test_intldesg_with_6_characters(): 355 sat = Satrec.twoline2rv(LINE1, LINE2) 356 assertEqual(sat.intldesg, '58002B') 357 358def test_intldesg_with_7_characters(): 359 sat = Satrec.twoline2rv( 360 '1 39444U 13066AE 20110.89708219 .00000236 00000-0' 361 ' 35029-4 0 9992', 362 '2 39444 97.5597 114.3769 0059573 102.0933 258.6965 ' 363 '14.82098949344697', 364 ) 365 assertEqual(sat.intldesg, '13066AE') 366 367def test_1990s_satrec_initialized_with_sgp4init(): 368 sat = Satrec() 369 sat.sgp4init( 370 WGS72, 371 'i', 372 VANGUARD_ATTRS['satnum'], 373 VANGUARD_EPOCH - 365.0, # change year 2000 to 1999 374 *sgp4init_args(VANGUARD_ATTRS) 375 ) 376 assertEqual(sat.epochyr, 99) 377 378def test_setters(): 379 sat = Satrec() 380 381 sat.classification = 'S' 382 assert sat.classification == 'S' 383 384 sat.intldesg = 'Russian' 385 assert sat.intldesg == 'Russian' 386 387 sat.ephtype = 23 388 assert sat.ephtype == 23 389 390 sat.elnum = 123 391 assert sat.elnum == 123 392 393 sat.revnum = 1234 394 assert sat.revnum == 1234 395 396def test_hyperbolic_orbit(): 397 # Exercise the newtonnu() code path with asinh() to see whether 398 # we can replace it with the one from Python's math module. 399 400 e0, m = newtonnu(1.0, 2.9) # parabolic 401 assertAlmostEqual(e0, 8.238092752965605, places=12) 402 assertAlmostEqual(m, 194.60069989482898, places=12) 403 404 e0, m = newtonnu(1.1, 2.7) # hyperbolic 405 assertAlmostEqual(e0, 4.262200676156417, places=12) 406 assertAlmostEqual(m, 34.76134082028372, places=12) 407 408def test_correct_epochyr(): 409 # Make sure that the non-standard four-digit epochyr I switched 410 # to in the Python version of SGP4 is reverted back to the 411 # official behavior when that code is used behind Satrec. 412 sat = Satrec.twoline2rv(LINE1, LINE2) 413 assertEqual(sat.epochyr, 0) 414 415def test_legacy_epochyr(): 416 # Apparently I saw fit to change the meaning of this attribute 417 # in the Python version of SGP4. 418 sat = io.twoline2rv(LINE1, LINE2, wgs72) 419 assertEqual(sat.epochyr, 2000) 420 421def test_support_for_old_no_attribute(): 422 s = io.twoline2rv(LINE1, LINE2, wgs72) 423 assert s.no == s.no_kozai 424 425def test_months_and_days(): 426 # Make sure our hand-written months-and-days routine is perfect. 427 428 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 429 430 day_of_year = 1 431 for month, length in enumerate(month_lengths, 1): 432 for day in range(1, length + 1): 433 tup = _day_of_year_to_month_day(day_of_year, False) 434 assertEqual((month, day), tup) 435 day_of_year += 1 436 437 month_lengths[1] = 29 # February, during a leap year 438 day_of_year = 1 439 for month, length in enumerate(month_lengths, 1): 440 for day in range(1, length + 1): 441 tup = _day_of_year_to_month_day(day_of_year, True) 442 assertEqual((month, day), tup) 443 day_of_year += 1 444 445def test_december_32(): 446 # ISS [Orbit 606], whose date is 2019 plus 366.82137887 days. 447 # The core SGP4 routines handled this fine, but my hamfisted 448 # attempt to provide a Python datetime for "convenience" ran 449 # into an overflow. 450 a = '1 25544U 98067A 19366.82137887 .00016717 00000-0 10270-3 0 9129' 451 b = '2 25544 51.6392 96.6358 0005156 88.7140 271.4601 15.49497216 6061' 452 correct_epoch = dt.datetime(2020, 1, 1, 19, 42, 47, 134368) 453 454 # Legacy API. 455 sat = io.twoline2rv(a, b, wgs72) 456 assertEqual(sat.epoch, correct_epoch) 457 458 correct_epoch = correct_epoch.replace(tzinfo=conveniences.UTC) 459 460 # Modern API. 461 sat = Satrec.twoline2rv(a, b) 462 assertEqual(conveniences.sat_epoch_datetime(sat), correct_epoch) 463 464 465def test_bad_first_line(): 466 with assertRaisesRegex(ValueError, re.escape("""TLE format error 467 468The Two-Line Element (TLE) format was designed for punch cards, and so 469is very strict about the position of every period, space, and digit. 470Your line does not quite match. Here is the official format for line 1 471with an N where each digit should go, followed by the line you provided: 472 4731 NNNNNC NNNNNAAA NNNNN.NNNNNNNN +.NNNNNNNN +NNNNN-N +NNNNN-N N NNNNN 4741 00005U 58002B 00179.78495062 .000000234 00000-0 28098-4 0 4753""")): 475 io.twoline2rv(LINE1.replace('23 ', '234'), LINE2, wgs72) 476 477def test_bad_second_line(): 478 with assertRaisesRegex(ValueError, re.escape("""TLE format error 479 480The Two-Line Element (TLE) format was designed for punch cards, and so 481is very strict about the position of every period, space, and digit. 482Your line does not quite match. Here is the official format for line 2 483with an N where each digit should go, followed by the line you provided: 484 4852 NNNNN NNN.NNNN NNN.NNNN NNNNNNN NNN.NNNN NNN.NNNN NN.NNNNNNNNNNNNNN 4862 00005 34 .268234 8.7242 1859667 331.7664 19.3264 10.82419157413667""")): 487 io.twoline2rv(LINE1, LINE2.replace(' 34', '34 '), wgs72) 488 489def test_mismatched_lines(): 490 msg = "Object numbers in lines 1 and 2 do not match" 491 with assertRaisesRegex(ValueError, re.escape(msg)): 492 io.twoline2rv(LINE1, BAD2, wgs72) 493 494# ------------------------------------------------------------------------ 495# Helper routines 496# 497 498def verify_vanguard_1(sat, legacy=False): 499 attrs = VANGUARD_ATTRS 500 501 if legacy: 502 attrs = attrs.copy() 503 del attrs['epochyr'] 504 del attrs['epochdays'] 505 del attrs['jdsatepoch'] 506 del attrs['jdsatepochF'] 507 508 for name, value in attrs.items(): 509 try: 510 assertEqual(getattr(sat, name), value) 511 except AssertionError as e: 512 message, = e.args 513 e.args = ('for attribute %s, %s' % (name, message),) 514 raise e 515 516def sgp4init_args(d): 517 """Given a dict of orbital parameters, return them in sgp4init order.""" 518 return (d['bstar'], d['ndot'], d['nddot'], d['ecco'], d['argpo'], 519 d['inclo'], d['mo'], d['no_kozai'], d['nodeo']) 520 521# ---------------------------------------------------------------------- 522# INTEGRATION TEST 523# 524# This runs both new and old satellite objects against every example 525# computation in the official `tcppver.out` test case file. Instead of 526# trying to parse the file, it instead re-generates it using Python 527# satellite objects, then compares the resulting text with the file. 528 529def test_satrec_against_tcppver_using_julian_dates(): 530 531 def invoke(satrec, tsince): 532 whole, fraction = divmod(tsince / 1440.0, 1.0) 533 jd = satrec.jdsatepoch + whole 534 fr = satrec.jdsatepochF + fraction 535 e, r, v = satrec.sgp4(jd, fr) 536 return e, r, v 537 538 run_satellite_against_tcppver(Satrec.twoline2rv, invoke, [1,1,6,6,4,3,6]) 539 540def test_satrec_against_tcppver_using_tsince(): 541 542 def invoke(satrec, tsince): 543 e, r, v = satrec.sgp4_tsince(tsince) 544 return e, r, v 545 546 run_satellite_against_tcppver(Satrec.twoline2rv, invoke, [1,1,6,6,4,3,6]) 547 548def test_legacy_against_tcppver(): 549 550 def make_legacy_satellite(line1, line2): 551 sat = io.twoline2rv(line1, line2, wgs72) 552 return sat 553 554 def run_legacy_sgp4(satrec, tsince): 555 r, v = sgp4(satrec, tsince) 556 return (satrec.error, satrec.error_message), r, v 557 558 errs = [ 559 (1, 'mean eccentricity -0.001329 not within range 0.0 <= e < 1.0'), 560 (1, 'mean eccentricity -0.001208 not within range 0.0 <= e < 1.0'), 561 (6, 'mrt 0.996159 is less than 1.0' 562 ' indicating the satellite has decayed'), 563 (6, 'mrt 0.996252 is less than 1.0' 564 ' indicating the satellite has decayed'), 565 (4, 'semilatus rectum -0.103223 is less than zero'), 566 (3, 'perturbed eccentricity -122.217193' 567 ' not within range 0.0 <= e <= 1.0'), 568 (6, 'mrt 0.830534 is less than 1.0' 569 ' indicating the satellite has decayed'), 570 ] 571 572 run_satellite_against_tcppver(make_legacy_satellite, run_legacy_sgp4, errs) 573 574def run_satellite_against_tcppver(twoline2rv, invoke, expected_errors): 575 # Check whether this library can produce (at least roughly) the 576 # output in tcppver.out. 577 578 data = get_data(__name__, 'tcppver.out') 579 data = data.replace(b'\r', b'') 580 tcppver_lines = data.decode('ascii').splitlines(True) 581 582 error_list = [] 583 actual_lines = list(generate_test_output(twoline2rv, invoke, error_list)) 584 585 assert len(tcppver_lines) == len(actual_lines) == 700 586 587 previous_data_line = None 588 linepairs = zip(tcppver_lines, actual_lines) 589 590 for lineno, (expected_line, actual_line) in enumerate(linepairs, start=1): 591 592 if actual_line == '(Use previous data line)': 593 actual_line = (' 0.00000000' + 594 previous_data_line[17:107]) 595 596 # Compare the lines. The first seven fields are printed 597 # to very high precision, so we allow a small error due 598 # to rounding differences; the rest are printed to lower 599 # precision, and so can be compared textually. 600 601 if 'xx' in actual_line: 602 similar = (actual_line == expected_line) 603 else: 604 afields = actual_line.split() 605 efields = expected_line.split() 606 actual7 = [ float(a) for a in afields[:7] ] 607 expected7 = [ float(e) for e in efields[:7] ] 608 similar = ( 609 len(actual7) == len(expected7) 610 and 611 all( 612 -error < (a - e) < error 613 for a, e in zip(actual7, expected7) 614 ) 615 and 616 afields[7:] == efields[7:] # just compare text 617 ) 618 619 if not similar: 620 raise ValueError( 621 'Line %d of output does not match:\n' 622 '\n' 623 'Expected: %r\n' 624 'Got back: %r' 625 % (lineno, expected_line, actual_line)) 626 627 if 'xx' not in actual_line: 628 previous_data_line = actual_line 629 630 # Make sure we produced the correct list of errors. 631 assertEqual(error_list, expected_errors) 632 633def generate_test_output(twoline2rv, invoke, error_list): 634 """Generate lines like those in the test file tcppver.out. 635 636 This iterates through the satellites in "SGP4-VER.TLE", which are 637 each supplemented with a time start/stop/step over which we are 638 supposed to print results. 639 640 """ 641 data = get_data(__name__, 'SGP4-VER.TLE') 642 tle_lines = iter(data.decode('ascii').splitlines()) 643 644 for line1 in tle_lines: 645 646 if not line1.startswith('1'): 647 continue 648 649 line2 = next(tle_lines) 650 satrec = twoline2rv(line1, line2) 651 652 yield '%ld xx\n' % (satrec.satnum,) 653 654 for line in generate_satellite_output( 655 satrec, invoke, line2, error_list): 656 yield line 657 658def generate_satellite_output(satrec, invoke, line2, error_list): 659 """Print a data line for each time in line2's start/stop/step field.""" 660 661 mu = wgs72.mu 662 663 e, r, v = invoke(satrec, 0.0) 664 if isnan(r[0]) and isnan(r[1]) and isnan(r[2]): 665 error_list.append(e) 666 yield '(Use previous data line)' 667 return 668 yield format_short_line(0.0, r, v) 669 670 tstart, tend, tstep = (float(field) for field in line2[69:].split()) 671 672 tsince = tstart 673 while tsince <= tend: 674 if tsince == tstart == 0.0: 675 tsince += tstep 676 continue # avoid duplicating the first line 677 678 e, r, v = invoke(satrec, tsince) 679 680 if e != 0 and e != (0, None): 681 error_list.append(e) 682 return 683 yield format_long_line(satrec, tsince, mu, r, v) 684 685 tsince += tstep 686 687 if tsince - tend < tstep - 1e-6: # do not miss last line! 688 e, r, v = invoke(satrec, tend) 689 if e != 0 and e != (0, None): 690 error_list.append(e) 691 return 692 yield format_long_line(satrec, tend, mu, r, v) 693 694def format_short_line(tsince, r, v): 695 """Short line, using the same format string that testcpp.cpp uses.""" 696 697 return ' %16.8f %16.8f %16.8f %16.8f %12.9f %12.9f %12.9f\n' % ( 698 tsince, r[0], r[1], r[2], v[0], v[1], v[2]) 699 700def format_long_line(satrec, tsince, mu, r, v): 701 """Long line, using the same format string that testcpp.cpp uses.""" 702 703 short = format_short_line(tsince, r, v).strip('\n') 704 705 jd = satrec.jdsatepoch + satrec.jdsatepochF + tsince / 1440.0 706 year, mon, day, hr, minute, sec = invjday(jd) 707 708 (p, a, ecc, incl, node, argp, nu, m, arglat, truelon, lonper 709 ) = rv2coe(r, v, mu) 710 711 return short + ( 712 ' %14.6f %8.6f %10.5f %10.5f %10.5f %10.5f %10.5f' 713 ' %5i%3i%3i %2i:%2i:%9.6f\n' 714 ) % ( 715 a, ecc, incl*rad, node*rad, argp*rad, nu*rad, 716 m*rad, year, mon, day, hr, minute, sec, 717 ) 718 719# ---------------------------------------------------------------------- 720# NEW "OMM" FORMAT TESTS 721 722 723# https://celestrak.com/satcat/tle.php?CATNR=5 724VANGUARD_TLE = """\ 725VANGUARD 1 \n\ 7261 00005U 58002B 20287.20333880 -.00000016 00000-0 -22483-4 0 9998 7272 00005 34.2443 225.5254 1845686 162.2516 205.2356 10.84869164218149 728""" 729 730# https://celestrak.com/NORAD/elements/gp.php?CATNR=00005&FORMAT=XML 731VANGUARD_XML = """\ 732<?xml version="1.0" encoding="UTF-8"?> 733<ndm xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://sanaregistry.org/r/ndmxml/ndmxml-1.0-master.xsd"> 734<omm id="CCSDS_OMM_VERS" version="2.0"> 735<header><CREATION_DATE/><ORIGINATOR/></header><body><segment><metadata><OBJECT_NAME>VANGUARD 1</OBJECT_NAME><OBJECT_ID>1958-002B</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>2020-10-13T04:52:48.472320</EPOCH><MEAN_MOTION>10.84869164</MEAN_MOTION><ECCENTRICITY>.1845686</ECCENTRICITY><INCLINATION>34.2443</INCLINATION><RA_OF_ASC_NODE>225.5254</RA_OF_ASC_NODE><ARG_OF_PERICENTER>162.2516</ARG_OF_PERICENTER><MEAN_ANOMALY>205.2356</MEAN_ANOMALY></meanElements><tleParameters><EPHEMERIS_TYPE>0</EPHEMERIS_TYPE><CLASSIFICATION_TYPE>U</CLASSIFICATION_TYPE><NORAD_CAT_ID>5</NORAD_CAT_ID><ELEMENT_SET_NO>999</ELEMENT_SET_NO><REV_AT_EPOCH>21814</REV_AT_EPOCH><BSTAR>-.22483E-4</BSTAR><MEAN_MOTION_DOT>-1.6E-7</MEAN_MOTION_DOT><MEAN_MOTION_DDOT>0</MEAN_MOTION_DDOT></tleParameters></data></segment></body></omm> 736</ndm> 737""" 738 739# https://celestrak.com/NORAD/elements/gp.php?CATNR=00005&FORMAT=CSV 740VANGUARD_CSV = """\ 741OBJECT_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 742VANGUARD 1,1958-002B,2020-10-13T04:52:48.472320,10.84869164,.1845686,34.2443,225.5254,162.2516,205.2356,0,U,5,999,21814,-.22483E-4,-1.6E-7,0 743""" 744 745def test_omm_xml_matches_old_tle(): 746 line0, line1, line2 = VANGUARD_TLE.splitlines() 747 sat1 = Satrec.twoline2rv(line1, line2) 748 749 fields = next(omm.parse_xml(StringIO(VANGUARD_XML))) 750 sat2 = Satrec() 751 omm.initialize(sat2, fields) 752 753 assert_satellites_match(sat1, sat2) 754 755def test_omm_csv_matches_old_tle(): 756 line0, line1, line2 = VANGUARD_TLE.splitlines() 757 sat1 = Satrec.twoline2rv(line1, line2) 758 759 fields = next(omm.parse_csv(StringIO(VANGUARD_CSV))) 760 sat2 = Satrec() 761 omm.initialize(sat2, fields) 762 763 assert_satellites_match(sat1, sat2) 764 765def assert_satellites_match(sat1, sat2): 766 todo = {'whichconst'} 767 768 for attr in dir(sat1): 769 if attr.startswith('_'): 770 continue 771 if attr in todo: 772 continue 773 value1 = getattr(sat1, attr, None) 774 if value1 is None: 775 continue 776 if callable(value1): 777 continue 778 value2 = getattr(sat2, attr) 779 assertEqual(value1, value2, '%s %r != %r' % (attr, value1, value2)) 780 781# Live example of OMM: 782# https://celestrak.com/NORAD/elements/gp.php?INTDES=2020-025&FORMAT=JSON-PRETTY 783 784def test_omm_export(): 785 line0, line1, line2 = VANGUARD_TLE.splitlines() 786 sat = Satrec.twoline2rv(line1, line2) 787 788 fields = export_omm(sat, 'VANGUARD 1') 789 assertEqual(fields, { 790 'ARG_OF_PERICENTER': 162.2516, 791 'BSTAR': -2.2483e-05, 792 'CENTER_NAME': 'EARTH', 793 'CLASSIFICATION_TYPE': 'U', 794 'ECCENTRICITY': 0.1845686, 795 'ELEMENT_SET_NO': 999, 796 'EPHEMERIS_TYPE': 0, 797 'EPOCH': '2020-10-13T04:52:48.472320', 798 'INCLINATION': 34.2443, 799 'MEAN_ANOMALY': 205.2356, 800 'MEAN_ELEMENT_THEORY': 'SGP4', 801 'MEAN_MOTION': 10.84869164, 802 'MEAN_MOTION_DDOT': 0.0, 803 'MEAN_MOTION_DOT': -1.6e-07, 804 'NORAD_CAT_ID': 5, 805 'OBJECT_ID': '1958-002B', 806 'OBJECT_NAME': 'VANGUARD 1', 807 'RA_OF_ASC_NODE': 225.5254, 808 'REF_FRAME': 'TEME', 809 'REV_AT_EPOCH': 21814, 810 'TIME_SYSTEM': 'UTC', 811 }) 812 813# ---------------------------------------------------------------------- 814 815def load_tests(loader, tests, ignore): 816 """Run our main documentation as a test, plus all test functions.""" 817 818 from sgp4.wulfgar import add_test_functions 819 add_test_functions(loader, tests, __name__) 820 821 # Python 2.6 formats floating-point numbers a bit differently and 822 # breaks the doctest, so we only run the doctest on later versions. 823 if sys.version_info >= (2, 7): 824 825 def setCwd(suite): 826 suite.olddir = os.getcwd() 827 os.chdir(os.path.dirname(__file__)) 828 def restoreCwd(suite): 829 os.chdir(suite.olddir) 830 831 options = dict(optionflags=ELLIPSIS, setUp=setCwd, tearDown=restoreCwd) 832 tests.addTests(DocTestSuite('sgp4', **options)) 833 tests.addTests(DocTestSuite('sgp4.conveniences', **options)) 834 tests.addTests(DocTestSuite('sgp4.functions', **options)) 835 836 return tests 837 838if __name__ == '__main__': 839 main() 840