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