From fd7accb8816ca9c5789fadbf65d4ac0983fec810 Mon Sep 17 00:00:00 2001 From: "Acciarini, Giacomo (PG/R - Maths & Physics)" Date: Fri, 11 Oct 2024 10:54:05 +0200 Subject: [PATCH 1/2] v1.0.2 --- dsgp4/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsgp4/__init__.py b/dsgp4/__init__.py index af11457..c5fae93 100644 --- a/dsgp4/__init__.py +++ b/dsgp4/__init__.py @@ -1,4 +1,4 @@ -__version__ = '1.0.1' +__version__ = '1.0.2' import torch torch.set_default_dtype(torch.float64) From 581d186178ab52cc6fad27be3c9f808583756d08 Mon Sep 17 00:00:00 2001 From: Sceki Date: Fri, 27 Mar 2026 13:57:15 +0100 Subject: [PATCH 2/2] add tests for edge cases to reach max coverage --- tests/test_coverage_edges.py | 434 +++++++++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 tests/test_coverage_edges.py diff --git a/tests/test_coverage_edges.py b/tests/test_coverage_edges.py new file mode 100644 index 0000000..8ee852c --- /dev/null +++ b/tests/test_coverage_edges.py @@ -0,0 +1,434 @@ +import contextlib +import importlib +import io +import os +import tempfile +import types +import unittest +from unittest import mock + +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +import torch + +import dsgp4 +from dsgp4 import initl as initl_fn +from dsgp4 import util +from dsgp4.mldsgp4 import mldsgp4 +from dsgp4.plot import plot_orbit, plot_tles +from dsgp4.sgp4 import sgp4 +from dsgp4.sgp4_batched import sgp4_batched +from dsgp4.sgp4init import sgp4init +from dsgp4.sgp4init_batch import initl_batch, sgp4init_batch +from dsgp4.tle import TLE, load, load_from_lines, read_satellite_catalog_number + + +SAMPLE_LINE1 = "1 43437U 18100A 20143.90384230 .00041418 00000-0 10000-3 0 99968" +SAMPLE_LINE2 = "2 43437 97.8268 249.9127 0221000 123.9136 259.1144 15.12608579563539" + +INITL_MODULE = importlib.import_module("dsgp4.initl") +NEWTON_MODULE = importlib.import_module("dsgp4.newton_method") +SGP4INIT_BATCH_MODULE = importlib.import_module("dsgp4.sgp4init_batch") + + +def _sample_tle(): + return TLE([SAMPLE_LINE1, SAMPLE_LINE2]) + + +class CoverageEdgesTestCase(unittest.TestCase): + def test_initl_opsmode_a_negative_gsto_branch(self): + tle = _sample_tle() + whichconst = util.get_gravity_constants("wgs-84") + _, _, _, xke, j2, _, _, _ = whichconst + with mock.patch.object(torch.Tensor, "__lt__", return_value=torch.tensor(True)): + out = initl_fn( + xke, + j2, + tle._ecco, + (tle._jdsatepoch + tle._jdsatepochF) - 2433281.5, + tle._inclo, + tle._no_kozai, + "a", + "n", + ) + self.assertTrue(torch.isfinite(out[-1])) + + def test_mldsgp4_load_model_sets_eval(self): + model = mldsgp4(hidden_size=8) + model.train() + self.assertTrue(model.training) + with tempfile.NamedTemporaryFile(suffix=".pth", delete=False) as tmp: + tmp_path = tmp.name + try: + torch.save(model.state_dict(), tmp_path) + model.load_model(tmp_path) + self.assertFalse(model.training) + finally: + os.unlink(tmp_path) + + def test_newton_verbose_converged(self): + dummy = types.SimpleNamespace( + _ecco=0.2, + _argpo=0.1, + _inclo=0.3, + _mo=0.4, + _no_kozai=0.05, + _nodeo=0.6, + _epoch=util.from_string_to_datetime("2020-01-01 00:00:00"), + ) + target_state = torch.tensor([[0.2, 0.1, 0.3], [0.4, 0.05, 0.6]]) + + def fake_propagate(x, *_args, **_kwargs): + return torch.stack((x[:3], x[3:6])) + + with mock.patch.object(NEWTON_MODULE, "initial_guess_tle", return_value=dummy), mock.patch.object( + NEWTON_MODULE, "_propagate", side_effect=fake_propagate + ), mock.patch.object(NEWTON_MODULE, "update_TLE", return_value=dummy): + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + dsgp4.newton_method( + tle0=dummy, + time_mjd=59000.0, + max_iter=2, + verbose=True, + target_state=target_state, + ) + self.assertIn("converged", buf.getvalue()) + + def test_newton_eccentricity_lower_bound(self): + dummy = types.SimpleNamespace( + _ecco=0.1, + _argpo=0.1, + _inclo=0.3, + _mo=0.4, + _no_kozai=0.05, + _nodeo=0.6, + _epoch=util.from_string_to_datetime("2020-01-01 00:00:00"), + ) + + def fake_propagate(x, *_args, **_kwargs): + return torch.stack((x[:3], x[3:6])) + + with mock.patch.object(NEWTON_MODULE, "initial_guess_tle", return_value=dummy), mock.patch.object( + NEWTON_MODULE, "_propagate", side_effect=fake_propagate + ), mock.patch.object(NEWTON_MODULE, "update_TLE", return_value=dummy), mock.patch.object( + NEWTON_MODULE.np.linalg, "solve", return_value=np.array([-10.0, 0, 0, 0, 0, 0]) + ): + _, y = dsgp4.newton_method( + tle0=dummy, + time_mjd=59000.0, + max_iter=1, + target_state=torch.zeros((2, 3)), + ) + self.assertGreater(float(y[0]), 0.0) + + def test_newton_eccentricity_upper_bound_and_max_iter_verbose(self): + dummy = types.SimpleNamespace( + _ecco=0.9, + _argpo=0.1, + _inclo=0.3, + _mo=0.4, + _no_kozai=0.05, + _nodeo=0.6, + _epoch=util.from_string_to_datetime("2020-01-01 00:00:00"), + ) + + def fake_propagate(x, *_args, **_kwargs): + return torch.stack((x[:3], x[3:6])) + + with mock.patch.object(NEWTON_MODULE, "initial_guess_tle", return_value=dummy), mock.patch.object( + NEWTON_MODULE, "_propagate", side_effect=fake_propagate + ), mock.patch.object(NEWTON_MODULE, "update_TLE", return_value=dummy), mock.patch.object( + NEWTON_MODULE.np.linalg, "solve", return_value=np.array([10.0, 0, 0, 0, 0, 0]) + ): + _, y = dsgp4.newton_method( + tle0=dummy, + time_mjd=59000.0, + max_iter=1, + target_state=torch.zeros((2, 3)), + ) + self.assertLess(float(y[0]), 2.0) + + with mock.patch.object(NEWTON_MODULE, "initial_guess_tle", return_value=dummy): + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + dsgp4.newton_method( + tle0=dummy, + time_mjd=59000.0, + max_iter=0, + verbose=True, + target_state=torch.zeros((2, 3)), + ) + self.assertIn("Solution not found", buf.getvalue()) + + def test_plot_functions(self): + states = torch.zeros((5, 2, 3)) + states[:, 0, 0] = torch.linspace(0.0, 1000.0, 5) + states[:, 0, 1] = torch.linspace(0.0, 500.0, 5) + states[:, 0, 2] = torch.linspace(0.0, 200.0, 5) + ax = plot_orbit(states, elevation_azimuth=(20, 40), color="red", label="orb") + self.assertIsNotNone(ax) + fig = plt.figure() + ax2 = fig.add_subplot(111, projection="3d") + ax2 = plot_orbit(states, ax=ax2, color="blue", label="orb2") + self.assertIsNotNone(ax2) + + tle1 = _sample_tle() + tle2 = tle1.copy() + tle2.update({"mean_motion": tle1.mean_motion * 1.01, "eccentricity": tle1.eccentricity * 1.1}) + with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: + plot_path = tmp.name + try: + axs = plot_tles( + [tle1, tle2], + file_name=plot_path, + show=False, + return_axs=True, + log_yscale=True, + color="green", + ) + self.assertEqual(axs.shape, (3, 3)) + self.assertTrue(os.path.exists(plot_path)) + with mock.patch("matplotlib.pyplot.show") as mocked_show: + plot_tles([tle1, tle2], show=True, return_axs=False) + mocked_show.assert_called_once() + finally: + if os.path.exists(plot_path): + os.unlink(plot_path) + plt.close("all") + + def test_sgp4_type_and_attribute_errors(self): + with self.assertRaises(TypeError): + sgp4(object(), torch.tensor([0.0])) + with self.assertRaises(AttributeError): + sgp4(_sample_tle(), torch.tensor([0.0])) + + def test_sgp4_batched_value_errors(self): + tle = _sample_tle() + with self.assertRaises(ValueError): + sgp4_batched(object(), torch.tensor([0.0])) + with self.assertRaises(ValueError): + sgp4_batched(tle, [0.0]) + with self.assertRaises(ValueError): + sgp4_batched(tle, torch.tensor([[0.0]])) + tle._argpo = torch.tensor([float(tle._argpo)]) + with self.assertRaises(ValueError): + sgp4_batched(tle, torch.tensor([0.0, 1.0])) + with self.assertRaises(AttributeError): + sgp4_batched(tle, torch.tensor([0.0])) + + def test_sgp4init_perigee_and_cosio_edge(self): + tle = _sample_tle() + whichconst = util.get_gravity_constants("wgs-84") + sgp4init( + whichconst=whichconst, + opsmode="i", + satn=tle.satellite_catalog_number, + epoch=(tle._jdsatepoch + tle._jdsatepochF) - 2433281.5, + xbstar=tle._bstar, + xndot=tle._ndot, + xnddot=tle._nddot, + xecco=torch.tensor(0.03), + xargpo=tle._argpo, + xinclo=tle._inclo, + xmo=tle._mo, + xno_kozai=torch.tensor(0.08), + xnodeo=tle._nodeo, + satellite=tle, + ) + self.assertTrue(hasattr(tle, "_cc1")) + + tle2 = _sample_tle() + sgp4init( + whichconst=whichconst, + opsmode="i", + satn=tle2.satellite_catalog_number, + epoch=(tle2._jdsatepoch + tle2._jdsatepochF) - 2433281.5, + xbstar=tle2._bstar, + xndot=tle2._ndot, + xnddot=tle2._nddot, + xecco=tle2._ecco, + xargpo=tle2._argpo, + xinclo=torch.tensor(np.pi), + xmo=tle2._mo, + xno_kozai=tle2._no_kozai, + xnodeo=tle2._nodeo, + satellite=tle2, + ) + self.assertTrue(torch.isfinite(torch.tensor(float(tle2._xlcof)))) + + def test_sgp4init_batch_opsmode_and_deep_space(self): + tle = _sample_tle() + whichconst = util.get_gravity_constants("wgs-84") + + with mock.patch.object(SGP4INIT_BATCH_MODULE, "numpy", types.SimpleNamespace(pi=np.pi), create=True), mock.patch.object( + torch.Tensor, "__lt__", return_value=torch.tensor(True) + ): + out = initl_batch( + whichconst[3], + whichconst[4], + torch.tensor([tle._ecco]), + (tle._jdsatepoch + tle._jdsatepochF) - 2433281.5, + torch.tensor([tle._inclo]), + torch.tensor([tle._no_kozai]), + "a", + 1, + "n", + ) + self.assertEqual(len(out[1]), 1) + + batch = tle.copy() + with self.assertRaises(RuntimeError): + sgp4init_batch( + whichconst=whichconst, + opsmode="i", + satn=tle.satellite_catalog_number, + epoch=(tle._jdsatepoch + tle._jdsatepochF) - 2433281.5, + xbstar=torch.tensor([tle._bstar]), + xndot=torch.tensor([tle._ndot]), + xnddot=torch.tensor([tle._nddot]), + xecco=torch.tensor([tle._ecco]), + xargpo=torch.tensor([tle._argpo]), + xinclo=torch.tensor([tle._inclo]), + xmo=torch.tensor([tle._mo]), + xno_kozai=torch.tensor([0.01]), + xnodeo=torch.tensor([tle._nodeo]), + satellite_batch=batch, + ) + + def test_tle_missing_paths(self): + self.assertEqual(read_satellite_catalog_number("A1234"), 101234) + + with self.assertRaises(ValueError): + load_from_lines([123, "x"]) # list non-string + with self.assertRaises(ValueError): + load_from_lines(12) # invalid type + with self.assertRaises(ValueError): + load_from_lines([SAMPLE_LINE1]) # wrong length + with self.assertRaises(ValueError): + load_from_lines(["bad", SAMPLE_LINE2]) + + bad_l2 = SAMPLE_LINE2.replace("43437", "43438", 1) + with self.assertRaises(ValueError): + load_from_lines([SAMPLE_LINE1, bad_l2]) + + with self.assertRaises(ValueError): + load_from_lines([SAMPLE_LINE1, "2 bad"]) + + line1_1999 = SAMPLE_LINE1[:18] + "99" + SAMPLE_LINE1[20:] + lines, data = load_from_lines([line1_1999, SAMPLE_LINE2]) + self.assertEqual(len(lines), 2) + self.assertGreaterEqual(data["epoch_year"], 1999) + + lines_from_string, _ = load_from_lines(SAMPLE_LINE1 + "\n" + SAMPLE_LINE2) + self.assertEqual(len(lines_from_string), 2) + + with mock.patch("dsgp4.tle.util.days2mdhms", return_value=(2, 31, 0, 0, 0.0)), mock.patch( + "dsgp4.tle.util.invjday", return_value=(2020, 5, 22, 0, 0, 0.0) + ): + _, data_fallback = load_from_lines([SAMPLE_LINE1, SAMPLE_LINE2]) + self.assertIn("_epoch", data_fallback) + + tle = _sample_tle() + with mock.patch("dsgp4.tle.util.days2mdhms", return_value=(2, 31, 0, 0, 0.0)), mock.patch( + "dsgp4.tle.util.invjday", return_value=(2020, 5, 22, 0, 0, 0.0) + ): + tle2 = TLE(dict(tle._data)) + self.assertIn("_epoch", tle2._data) + + with tempfile.NamedTemporaryFile("w", delete=False) as tmp: + tmp.write("0 NAME\n" + SAMPLE_LINE1 + "\n" + SAMPLE_LINE2 + "\n") + tmp.write(SAMPLE_LINE1 + "\n" + SAMPLE_LINE2 + "\n") + tmp_path = tmp.name + try: + tles = load(tmp_path) + self.assertEqual(len(tles), 2) + finally: + os.unlink(tmp_path) + + with self.assertRaises(RuntimeError): + TLE(3) + + d = dict(tle._data) + d["epoch_year"] = 1999 + lines_99, _ = importlib.import_module("dsgp4.tle").load_from_data(d) + self.assertEqual(len(lines_99), 2) + + with mock.patch("dsgp4.tle.compute_checksum", return_value=10): + with self.assertRaises(RuntimeError): + importlib.import_module("dsgp4.tle").load_from_data(dict(tle._data)) + + with mock.patch("dsgp4.tle.compute_checksum", side_effect=[1, 10]): + with self.assertRaises(RuntimeError): + importlib.import_module("dsgp4.tle").load_from_data(dict(tle._data)) + + old_epoch_days = tle.epoch_days + tle.set_time(tle.date_mjd + 1.0) + self.assertNotEqual(old_epoch_days, tle.epoch_days) + + old_mo = float(tle._mo) + tle.update({"mean_anomaly": float(tle.mean_anomaly) + 0.01}) + self.assertNotEqual(old_mo, float(tle._mo)) + + self.assertTrue(np.isfinite(tle.perigee_alt())) + self.assertTrue(np.isfinite(tle.apogee_alt())) + self.assertIn("TLE(", repr(tle)) + self.assertEqual(tle["line1"], tle.line1) + self.assertEqual(tle.mean_motion, tle.__getattr__("mean_motion")) + with self.assertRaises(AttributeError): + tle.__getattr__("_data") + with self.assertRaises(AttributeError): + tle.__getattr__("does_not_exist") + + def test_util_missing_paths(self): + with self.assertRaises(RuntimeError): + util.get_gravity_constants("bad") + + tle = _sample_tle() + err = "Error: deep space propagation not supported (yet). The provided satellite has an orbital period above 225 minutes. If you want to let us know you need it or you want to contribute to implement it, open a PR or raise an issue at: https://github.com/esa/dSGP4." + with mock.patch.object(SGP4INIT_BATCH_MODULE, "sgp4init_batch", side_effect=Exception(err)): + buf = io.StringIO() + with contextlib.redirect_stdout(buf): + util.initialize_tle([tle]) + self.assertIn("were not initialized", buf.getvalue()) + + with mock.patch.object(SGP4INIT_BATCH_MODULE, "sgp4init_batch", side_effect=Exception("other")): + with self.assertRaises(Exception): + util.initialize_tle([tle]) + + d = util.from_year_day_to_date(2020, 32) + self.assertEqual(d.month, 2) + + y, *_ = util.invjday(2415384.8) + self.assertLessEqual(y, 1900) + + dt = util.from_string_to_datetime("2020-01-01 00:00:00") + self.assertEqual(dt.year, 2020) + + days = util.from_mjd_to_epoch_days_after_1_jan(59000.0) + self.assertTrue(days > 0) + + with self.assertRaises(ValueError): + util.get_non_empty_lines(["a"]) # wrong input type + self.assertEqual(util.get_non_empty_lines("a\n\n b\n"), ["a", " b"]) + + mu = 1.0 + r_par = np.array([1.0, 0.0, 0.0]) + v_par = np.array([0.0, np.sqrt(2.0), 0.0]) + kep_par = util.from_cartesian_to_keplerian(r_par, v_par, mu) + self.assertTrue(np.isinf(kep_par[0])) + + r_hyp = np.array([1.0, 0.0, 0.0]) + v_hyp = np.array([0.0, 2.0, 0.0]) + kep_hyp = util.from_cartesian_to_keplerian(r_hyp, v_hyp, mu) + self.assertTrue(kep_hyp[1] > 1.0) + + with mock.patch("dsgp4.util.np.linalg.norm", side_effect=[1.0, np.sqrt(2.0), 1.0, 1.0, 1.0]): + kep_parabolic = util.from_cartesian_to_keplerian(r_par, v_par, mu) + self.assertTrue(np.isnan(kep_parabolic[5])) + + +if __name__ == "__main__": + unittest.main()