def test_from_tle_with_no_date_works(self): # https://github.com/satellogic/orbit-predictor/issues/52 start = dt.datetime(2017, 3, 6, 7, 51) db = MemoryTLESource() db.add_tle(self.SATE_ID, self.LINES, start) KeplerianPredictor.from_tle(self.SATE_ID, db)
class AccuratePredictorCalculationErrorTests(TestCase): """Check that we can learn from calculation errors and provide patches for corner cases""" def setUp(self): # Source self.db = MemoryTLESource() self.db.add_tle(BUGSAT_SATE_ID, BUGSAT1_TLE_LINES, datetime.now()) # Predictor self.predictor = HighAccuracyTLEPredictor(BUGSAT_SATE_ID, self.db) self.is_ascending_mock = self._patch( 'orbit_predictor.accuratepredictor.LocationPredictor.is_ascending') self.start = datetime(2017, 3, 6, 7, 51) logassert.setup(self, 'orbit_predictor.accuratepredictor') def _patch(self, *args, **kwargs): patcher = mock.patch(*args, **kwargs) self.addCleanup(patcher.stop) return patcher.start() def test_ascending_failure(self): self.is_ascending_mock.return_value = False with self.assertRaises(PropagationError): self.predictor.get_next_pass(svalbard, self.start) self.assertLoggedError(str(svalbard), str(self.start), *BUGSAT1_TLE_LINES) def test_descending_failure(self): self.is_ascending_mock.return_value = True with self.assertRaises(PropagationError): self.predictor.get_next_pass(svalbard, self.start) self.assertLoggedError(str(svalbard), str(self.start), *BUGSAT1_TLE_LINES)
def get_position(self, utc_datetime=datetime.utcnow()): source = MemoryTLESource() source.add_tle(sate_id=self.__tle_lines[0], tle=(self.__tle_lines[1], self.__tle_lines[2]), epoch=self.get_epoch_date() ) predictor = source.get_predictor(self.__tle_lines[0], precise=True) return predictor.get_position(utc_datetime).position_llh
class SkippedPassesRegressionTests(TestCase): """Check that we do not skip passes""" # See https://github.com/satellogic/orbit-predictor/issues/99 def setUp(self): self.db = MemoryTLESource() self.db.add_tle(TRICKY_SAT_ID, TRICKY_SAT_TLE_LINES, dt.datetime.now()) self.predictor = TLEPredictor(TRICKY_SAT_ID, self.db) @pytest.mark.xfail(reason="Legacy LocationPredictor skips some passes") def test_pass_is_not_skipped_old(self): loc = Location( name="loc", latitude_deg=-15.137152171507697, longitude_deg=-0.4276612055384211, elevation_m=1.665102900005877e-05, ) PASS_DATE = dt.datetime(2020, 9, 25, 9, 2, 6) LIMIT_DATE = dt.datetime(2020, 9, 25, 10, 36, 0) predicted_passes = list( self.predictor.passes_over( loc, when_utc=PASS_DATE, limit_date=LIMIT_DATE, aos_at_dg=0, max_elevation_gt=0, location_predictor_class=LocationPredictor, )) assert predicted_passes @pytest.mark.skipif(sys.version_info < (3, 5), reason="Not installing SciPy in Python 3.4") def test_pass_is_not_skipped_smart(self): loc = Location( name="loc", latitude_deg=-15.137152171507697, longitude_deg=-0.4276612055384211, elevation_m=1.665102900005877e-05, ) PASS_DATE = dt.datetime(2020, 9, 25, 9, 2, 6) LIMIT_DATE = dt.datetime(2020, 9, 25, 10, 36, 0) predicted_passes = list( self.predictor.passes_over( loc, when_utc=PASS_DATE, limit_date=LIMIT_DATE, aos_at_dg=0, max_elevation_gt=0, location_predictor_class=SmartLocationPredictor, )) assert predicted_passes
class AccurateVsGpredictTests(TestCase): def setUp(self): # Source self.db = MemoryTLESource() self.db.add_tle(BUGSAT_SATE_ID, BUGSAT1_TLE_LINES, dt.datetime.utcnow()) # Predictor self.predictor = TLEPredictor(BUGSAT_SATE_ID, self.db) def test_get_next_pass_with_stk_data(self): STK_DATA = """ ------------------------------------------------------------------------------------------------ AOS TCA LOS Duration Max El ------------------------------------------------------------------------------------------------ 2014/10/23 01:27:33.224 2014/10/23 01:32:41.074 2014/10/23 01:37:47.944 00:10:14.720 12.76 2014/10/23 03:01:37.007 2014/10/23 03:07:48.890 2014/10/23 03:14:01.451 00:12:24.000 39.32 2014/10/23 14:49:34.783 2014/10/23 14:55:44.394 2014/10/23 15:01:51.154 00:12:16.000 41.75 2014/10/23 16:25:54.939 2014/10/23 16:30:50.152 2014/10/23 16:35:44.984 00:09:50.000 11.45 2014/10/24 01:35:47.889 2014/10/24 01:41:13.181 2014/10/24 01:46:37.548 00:10:50.000 16.07 2014/10/24 03:10:23.486 2014/10/24 03:16:27.230 2014/10/24 03:22:31.865 00:12:08.000 30.62 2014/10/24 14:58:07.378 2014/10/24 15:04:21.721 2014/10/24 15:10:33.546 00:12:26.000 54.83 2014/10/24 16:34:48.635 2014/10/24 16:39:20.960 2014/10/24 16:43:53.204 00:09:04.000 8.78 2014/10/25 01:44:05.771 2014/10/25 01:49:45.487 2014/10/25 01:55:24.414 00:11:18.000 20.07 2014/10/25 03:19:12.611 2014/10/25 03:25:05.674 2014/10/25 03:30:59.815 00:11:47.000 24.09""" # NOQA for line in STK_DATA.splitlines()[4:]: line_parts = line.split() aos = dt.datetime.strptime(" ".join(line_parts[:2]), '%Y/%m/%d %H:%M:%S.%f') max_elevation_date = dt.datetime.strptime( " ".join(line_parts[2:4]), '%Y/%m/%d %H:%M:%S.%f') los = dt.datetime.strptime(" ".join(line_parts[4:6]), '%Y/%m/%d %H:%M:%S.%f') duration = dt.datetime.strptime(line_parts[6], '%H:%M:%S.%f') duration_s = dt.timedelta(minutes=duration.minute, seconds=duration.second).total_seconds() max_elev_deg = float(line_parts[7]) try: date = pass_.los # NOQA except UnboundLocalError: date = dt.datetime.strptime("2014-10-22 20:18:11.921921", '%Y-%m-%d %H:%M:%S.%f') pass_ = self.predictor.get_next_pass(ARG, date) self.assertAlmostEqual(pass_.aos, aos, delta=ONE_SECOND) self.assertAlmostEqual(pass_.los, los, delta=ONE_SECOND) self.assertAlmostEqual(pass_.max_elevation_date, max_elevation_date, delta=ONE_SECOND) self.assertAlmostEqual(pass_.duration_s, duration_s, delta=2 * 1) self.assertAlmostEqual(pass_.max_elevation_deg, max_elev_deg, delta=0.05)
class AccurateVsGpredictTests(TestCase): def setUp(self): # Source self.db = MemoryTLESource() self.db.add_tle(BUGSAT_SATE_ID, BUGSAT1_TLE_LINES, datetime.now()) # Predictor self.predictor = HighAccuracyTLEPredictor(BUGSAT_SATE_ID, self.db) def test_get_next_pass_with_gpredict_data(self): GPREDICT_DATA = """ ------------------------------------------------------------------------------------------------- AOS TCA LOS Duration Max El AOS Az LOS Az ------------------------------------------------------------------------------------------------- 2014/10/23 01:27:09 2014/10/23 01:33:03 2014/10/23 01:38:57 00:11:47 25.85 40.28 177.59 2014/10/23 03:02:44 2014/10/23 03:08:31 2014/10/23 03:14:17 00:11:32 20.55 341.35 209.65 2014/10/23 14:48:23 2014/10/23 14:54:39 2014/10/23 15:00:55 00:12:31 75.31 166.30 350.27 2014/10/23 16:25:19 2014/10/23 16:29:32 2014/10/23 16:33:46 00:08:27 7.14 200.60 287.00 2014/10/24 01:35:34 2014/10/24 01:41:37 2014/10/24 01:47:39 00:12:05 32.20 34.97 180.38 2014/10/24 03:11:40 2014/10/24 03:17:11 2014/10/24 03:22:42 00:11:02 16.30 335.44 213.21 2014/10/24 14:57:00 2014/10/24 15:03:16 2014/10/24 15:09:32 00:12:32 84.30 169.06 345.11 2014/10/24 16:34:18 2014/10/24 16:38:02 2014/10/24 16:41:45 00:07:27 5.09 205.18 279.57 2014/10/25 01:44:01 2014/10/25 01:50:11 2014/10/25 01:56:20 00:12:19 40.61 29.75 183.12 2014/10/25 03:20:39 2014/10/25 03:25:51 2014/10/25 03:31:04 00:10:25 12.78 329.21 217.10""" # NOQA for line in GPREDICT_DATA.splitlines()[4:]: line_parts = line.split() aos = datetime.strptime(" ".join(line_parts[:2]), '%Y/%m/%d %H:%M:%S') max_elevation_date = datetime.strptime(" ".join(line_parts[2:4]), '%Y/%m/%d %H:%M:%S') los = datetime.strptime(" ".join(line_parts[4:6]), '%Y/%m/%d %H:%M:%S') duration = datetime.strptime(line_parts[6], '%H:%M:%S') duration_s = timedelta(minutes=duration.minute, seconds=duration.second).total_seconds() max_elev_deg = float(line_parts[7]) try: date = pass_.los # NOQA except UnboundLocalError: date = datetime.strptime("2014-10-22 20:18:11.921921", '%Y-%m-%d %H:%M:%S.%f') pass_ = self.predictor.get_next_pass(tortu1, date) self.assertAlmostEqual(pass_.aos, aos, delta=ONE_SECOND) self.assertAlmostEqual(pass_.los, los, delta=ONE_SECOND) self.assertAlmostEqual(pass_.max_elevation_date, max_elevation_date, delta=ONE_SECOND) self.assertAlmostEqual(pass_.duration_s, duration_s, delta=1) self.assertAlmostEqual(pass_.max_elevation_deg, max_elev_deg, delta=0.05)
def test_from_tle_returns_same_initial_conditions_on_epoch(self): start = datetime(2017, 3, 6, 7, 51) db = MemoryTLESource() db.add_tle(self.SATE_ID, self.LINES, start) keplerian_predictor = KeplerianPredictor.from_tle(self.SATE_ID, db, start) tle_predictor = TLEPredictor(self.SATE_ID, db) epoch = keplerian_predictor._epoch pos_keplerian = keplerian_predictor.get_position(epoch) pos_tle = tle_predictor.get_position(epoch) assert_allclose(pos_keplerian.position_ecef, pos_tle.position_ecef, rtol=1e-11) assert_allclose(pos_keplerian.velocity_ecef, pos_tle.velocity_ecef, rtol=1e-13)
class LOSComputationRegressionTests(TestCase): """Check that the LOS is computed correctly""" # See https://github.com/satellogic/orbit-predictor/issues/104 def setUp(self): tle_lines = ( "1 42760U 17034C 19070.46618549 .00000282 00000-0 30543-4 0 9995", "2 42760 43.0166 56.1509 0009676 356.3576 146.0151 15.09909885 95848", ) self.db = MemoryTLESource() self.db.add_tle("42760U", tle_lines, dt.datetime.now()) self.predictor = TLEPredictor("42760U", self.db) @pytest.mark.skipif(sys.version_info < (3, 5), reason="Not installing SciPy in Python 3.4") def test_los_is_correctly_computed(self): loc = Location( name='loc', latitude_deg=-34.61315, longitude_deg=-58.37723, elevation_m=30, ) PASS_DATE = dt.datetime(2019, 1, 1, 0, 0) LIMIT_DATE = dt.datetime(2019, 1, 15, 0, 0) predicted_passes = list( self.predictor.passes_over( loc, when_utc=PASS_DATE, limit_date=LIMIT_DATE, aos_at_dg=0, max_elevation_gt=0, location_predictor_class=SmartLocationPredictor, )) assert predicted_passes
class AccuratePredictorTests(TestCase): def setUp(self): # Source self.db = MemoryTLESource() self.start = datetime(2017, 3, 6, 7, 51) self.db.add_tle(SATE_ID, LINES, self.start) # Predictor self.predictor = HighAccuracyTLEPredictor(SATE_ID, self.db) self.old_predictor = TLEPredictor(SATE_ID, self.db) self.end = self.start + timedelta(days=5) def all_passes_old_predictor(self, location, start, end): while True: try: pass_ = self.old_predictor.get_next_pass(location, when_utc=start, limit_date=end) start = pass_.los yield pass_ except Exception: break def assertEqualOrGreaterPassesAmount(self, location): """Compare propagators and check no passes lost and no performance degradation""" t0 = time.time() old_passes = list( self.all_passes_old_predictor(location, self.start, self.end)) t1 = time.time() predicted_passes = list( self.predictor.passes_over(location, self.start, self.end)) t2 = time.time() self.assertGreaterEqual(len(predicted_passes), len(old_passes), 'We are loosing passes') self.assertLessEqual(t2 - t1, t1 - t0, 'Performance is degraded') def test_accurate_predictor_find_more_or_equal_passes_amount(self): self.assertEqualOrGreaterPassesAmount( Location('bad-case-1', 11.937501570612568, -55.35189435098657, 1780.674044538666)) self.assertEqualOrGreaterPassesAmount(tortu1) self.assertEqualOrGreaterPassesAmount(svalbard) self.assertEqualOrGreaterPassesAmount( Location('bad-case-2', -11.011509137116818, 123.29554733688798, 1451.5695915302097)) self.assertEqualOrGreaterPassesAmount( Location('bad-case-3', 10.20803236163988, 138.01236517021056, 4967.661890730469)) self.assertEqualOrGreaterPassesAmount( Location('less passes', -82.41515032683046, -33.712555446065664, 4417.427841452149)) def test_predicted_passes_are_equal_between_executions(self): location = Location('bad-case-1', 11.937501570612568, -55.35189435098657, 1780.674044538666) first_set = list( self.predictor.passes_over(location, self.start, self.end)) second_set = list( self.predictor.passes_over(location, self.start + timedelta(seconds=3), self.end)) self.assertEqual(first_set, second_set) def test_predicted_passes_have_elevation_positive_and_visible_on_date( self): end = self.start + timedelta(days=60) for pass_ in self.predictor.passes_over(svalbard, self.start, end): self.assertGreater(pass_.max_elevation_deg, 0) position = self.old_predictor.get_position( pass_.max_elevation_date) svalbard.is_visible(position) self.assertGreaterEqual(pass_.off_nadir_deg, -90) self.assertLessEqual(pass_.off_nadir_deg, 90) def test_predicted_passes_off_nadir_angle_works(self): start = datetime(2017, 3, 6, 13, 30) end = start + timedelta(hours=1) location = Location('bad-case-1', 11.937501570612568, -55.35189435098657, 1780.674044538666) pass_ = self.predictor.get_next_pass(location, when_utc=start, limit_date=end) self.assertGreaterEqual(0, pass_.off_nadir_deg) @given(start=datetimes( min_value=datetime(2017, 1, 1), max_value=datetime(2020, 12, 31), ), location=tuples(floats(min_value=-90, max_value=90), floats(min_value=0, max_value=180), floats(min_value=-200, max_value=9000))) @settings(max_examples=10000, deadline=None) @example(start=datetime(2017, 1, 26, 11, 51, 51), location=(-37.69358328273305, 153.96875, 0.0)) def test_pass_is_always_returned(self, start, location): location = Location('bad-case-1', *location) pass_ = self.predictor.get_next_pass(location, start) self.assertGreater(pass_.max_elevation_deg, 0) def test_aos_deg_can_be_used_in_get_next_pass(self): start = datetime(2017, 3, 6, 13, 30) end = start + timedelta(hours=1) location = Location('bad-case-1', 11.937501570612568, -55.35189435098657, 1780.674044538666) complete_pass = self.predictor.get_next_pass(location, when_utc=start, limit_date=end) pass_with_aos = self.predictor.get_next_pass(location, when_utc=start, limit_date=end, aos_at_dg=5) self.assertGreater(pass_with_aos.aos, complete_pass.aos) self.assertLess(pass_with_aos.aos, complete_pass.max_elevation_date) self.assertAlmostEqual(pass_with_aos.max_elevation_date, complete_pass.max_elevation_date, delta=timedelta(seconds=1)) self.assertGreater(pass_with_aos.los, complete_pass.max_elevation_date) self.assertLess(pass_with_aos.los, complete_pass.los) position = self.old_predictor.get_position(pass_with_aos.aos) _, elev = location.get_azimuth_elev_deg(position) self.assertAlmostEqual(elev, 5, delta=0.1) position = self.old_predictor.get_position(pass_with_aos.los) _, elev = location.get_azimuth_elev_deg(position) self.assertAlmostEqual(elev, 5, delta=0.1) def test_predicted_passes_whit_aos(self): end = self.start + timedelta(days=60) for pass_ in self.predictor.passes_over(svalbard, self.start, end, aos_at_dg=5): self.assertGreater(pass_.max_elevation_deg, 5) position = self.old_predictor.get_position(pass_.aos) _, elev = svalbard.get_azimuth_elev_deg(position) self.assertAlmostEqual(elev, 5, delta=0.1)
class LocationTestCase(unittest.TestCase): def setUp(self): # Source self.db = MemoryTLESource() self.db.add_tle(SATE_ID, BUGSAT1_TLE_LINES, datetime.datetime.now()) # Predictor self.predictor = TLEPredictor(SATE_ID, self.db) date = datetime.datetime.strptime("2014-10-22 20:18:11.921921", '%Y-%m-%d %H:%M:%S.%f') self.next_pass = self.predictor.get_next_pass(ARG, when_utc=date) def test_compare_eq(self): l1 = Location(latitude_deg=1, longitude_deg=2, elevation_m=3, name="location1") l2 = Location(latitude_deg=1, longitude_deg=2, elevation_m=3, name="location1") self.assertEqual(l1, l2) self.assertEqual(l2, l1) def test_compare_no_eq(self): l1 = Location(latitude_deg=1, longitude_deg=2, elevation_m=3, name="location_other") l2 = Location(latitude_deg=1, longitude_deg=2, elevation_m=3, name="location1") self.assertNotEqual(l1, l2) self.assertNotEqual(l2, l1) def test_compare_eq_subclass(self): class SubLocation(Location): pass l1 = Location(latitude_deg=1, longitude_deg=2, elevation_m=3, name="location1") l2 = SubLocation(latitude_deg=1, longitude_deg=2, elevation_m=3, name="location1") self.assertEqual(l1, l2) self.assertEqual(l2, l1) def test_get_azimuth_elev(self): date = datetime.datetime.strptime("2014-10-21 22:47:29.147740", '%Y-%m-%d %H:%M:%S.%f') azimuth, elevation = ARG.get_azimuth_elev( self.predictor.get_position(date)) self.assertAlmostEqual(degrees(azimuth), 249.7, delta=0.1) self.assertAlmostEqual(degrees(elevation), -52.1, delta=0.1) def test_get_azimuth_elev_deg(self): date = datetime.datetime.strptime("2014-10-21 22:47:29.147740", '%Y-%m-%d %H:%M:%S.%f') azimuth, elevation = ARG.get_azimuth_elev_deg( self.predictor.get_position(date)) self.assertAlmostEqual(azimuth, 249.7, delta=0.1) self.assertAlmostEqual(elevation, -52.1, delta=0.1) def test_is_visible(self): position = self.predictor.get_position(self.next_pass.aos) self.assertTrue(ARG.is_visible(position)) def test_no_visible(self): position = self.predictor.get_position(self.next_pass.los + datetime.timedelta(minutes=10)) self.assertFalse(ARG.is_visible(position)) def test_is_visible_with_deg(self): position = self.predictor.get_position(self.next_pass.aos + datetime.timedelta(minutes=4)) # 21 deg self.assertTrue(ARG.is_visible(position, elevation=4)) def test_no_visible_with_deg(self): position = self.predictor.get_position(self.next_pass.aos + datetime.timedelta(minutes=4)) # 21 deg self.assertFalse(ARG.is_visible(position, elevation=30)) def test_doppler_factor(self): date = datetime.datetime.strptime("2014-10-21 23:06:11.132438", '%Y-%m-%d %H:%M:%S.%f') position = self.predictor.get_position(date) doppler_factor = ARG.doppler_factor(position) self.assertAlmostEqual((2 - doppler_factor) * 437.445e6, 437.445632e6, delta=100)
class AccuratePredictorTests(TestCase): def setUp(self): # Source self.db = MemoryTLESource() self.start = datetime(2017, 3, 6, 7, 51) self.db.add_tle(SATE_ID, LINES, self.start) # Predictor self.predictor = HighAccuracyTLEPredictor(SATE_ID, self.db) self.end = self.start + timedelta(days=5) def test_predicted_passes_are_equal_between_executions(self): location = Location('bad-case-1', 11.937501570612568, -55.35189435098657, 1780.674044538666) first_set = list( self.predictor.passes_over(location, self.start, self.end)) second_set = list( self.predictor.passes_over(location, self.start + timedelta(seconds=3), self.end)) # We use delta=ONE_SECOND because # that's the hardcoded value for the precision self.assertAlmostEqual(first_set[0].aos, second_set[0].aos, delta=ONE_SECOND) self.assertAlmostEqual(first_set[0].los, second_set[0].los, delta=ONE_SECOND) def test_predicted_passes_have_elevation_positive_and_visible_on_date( self): end = self.start + timedelta(days=60) for pass_ in self.predictor.passes_over(ARG, self.start, end): self.assertGreater(pass_.max_elevation_deg, 0) position = self.predictor.get_position(pass_.max_elevation_date) ARG.is_visible(position) self.assertGreaterEqual(pass_.off_nadir_deg, -90) self.assertLessEqual(pass_.off_nadir_deg, 90) def test_predicted_passes_off_nadir_angle_works(self): start = datetime(2017, 3, 6, 13, 30) end = start + timedelta(hours=1) location = Location('bad-case-1', 11.937501570612568, -55.35189435098657, 1780.674044538666) pass_ = self.predictor.get_next_pass(location, when_utc=start, limit_date=end) self.assertGreaterEqual(0, pass_.off_nadir_deg) @given(start=datetimes( min_value=datetime(2017, 1, 1), max_value=datetime(2020, 12, 31), ), location=tuples(floats(min_value=-90, max_value=90), floats(min_value=0, max_value=180), floats(min_value=-200, max_value=9000))) @settings(max_examples=10000, deadline=None) @example(start=datetime(2017, 1, 26, 11, 51, 51), location=(-37.69358328273305, 153.96875, 0.0)) def test_pass_is_always_returned(self, start, location): location = Location('bad-case-1', *location) pass_ = self.predictor.get_next_pass(location, start) self.assertGreater(pass_.max_elevation_deg, 0) def test_aos_deg_can_be_used_in_get_next_pass(self): start = datetime(2017, 3, 6, 13, 30) end = start + timedelta(hours=1) location = Location('bad-case-1', 11.937501570612568, -55.35189435098657, 1780.674044538666) complete_pass = self.predictor.get_next_pass(location, when_utc=start, limit_date=end) pass_with_aos = self.predictor.get_next_pass(location, when_utc=start, limit_date=end, aos_at_dg=5) self.assertGreater(pass_with_aos.aos, complete_pass.aos) self.assertLess(pass_with_aos.aos, complete_pass.max_elevation_date) self.assertAlmostEqual(pass_with_aos.max_elevation_date, complete_pass.max_elevation_date, delta=timedelta(seconds=1)) self.assertGreater(pass_with_aos.los, complete_pass.max_elevation_date) self.assertLess(pass_with_aos.los, complete_pass.los) position = self.predictor.get_position(pass_with_aos.aos) _, elev = location.get_azimuth_elev_deg(position) self.assertAlmostEqual(elev, 5, delta=0.1) position = self.predictor.get_position(pass_with_aos.los) _, elev = location.get_azimuth_elev_deg(position) self.assertAlmostEqual(elev, 5, delta=0.1) def test_predicted_passes_whit_aos(self): end = self.start + timedelta(days=60) for pass_ in self.predictor.passes_over(ARG, self.start, end, aos_at_dg=5): self.assertGreater(pass_.max_elevation_deg, 5) position = self.predictor.get_position(pass_.aos) _, elev = ARG.get_azimuth_elev_deg(position) self.assertAlmostEqual(elev, 5, delta=0.1)