From cfa96d4877b2b164a26dcf599b8d4d9ce92288a7 Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 01:08:11 +0000 Subject: [PATCH 01/13] orbit_angle returns tuple instead of linspace --- src/orbit_visualiser/core/orbit.py | 8 +++---- .../ui/figure/orbit_figure.py | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/orbit_visualiser/core/orbit.py b/src/orbit_visualiser/core/orbit.py index cc6eb2a..83ec712 100644 --- a/src/orbit_visualiser/core/orbit.py +++ b/src/orbit_visualiser/core/orbit.py @@ -20,7 +20,6 @@ def y(self) -> Callable[[float], float]: return self._y -# TODO: self.orbital_angles should return just a tuple, not the linspace. The linspace is only for plotting so logic for calculating it should be in OrbitFigure. class Orbit(): @@ -86,12 +85,11 @@ def is_closed(self) -> bool: def orbit_eq(self) -> PerifocalOrbitEq: return PerifocalOrbitEq(self._e, self._p) - def orbital_angles(self): + def orbital_angles(self) -> tuple[float]: if self._e < 1: - return np.linspace(0, 2*pi, 100_000) + return 0, 2*pi - delta = 0.0001 - return np.linspace(-self._t_asymp + delta, self._t_asymp - delta, 100_000) + return -self._t_asymp, self._t_asymp def update_orbital_properties(self): e, rp = self._e, self._rp diff --git a/src/orbit_visualiser/ui/figure/orbit_figure.py b/src/orbit_visualiser/ui/figure/orbit_figure.py index 9150f32..4726cfc 100644 --- a/src/orbit_visualiser/ui/figure/orbit_figure.py +++ b/src/orbit_visualiser/ui/figure/orbit_figure.py @@ -3,12 +3,15 @@ from matplotlib.figure import Figure from matplotlib.axes import Axes from matplotlib.patches import Circle +import numpy as np +from numpy.typing import NDArray from orbit_visualiser.core import Orbit, CentralBody, Satellite # TODO: Fix bug where scroll zoom doesn't register as changing the view so the native matplotlib home button has unexpected (and often undesirable) behaviour. class OrbitFigure(): DISPLAY_TEXT_OFFSET = (1.5, 1.5) + NUM_POINTS = 100_000 def __init__( self, @@ -75,7 +78,7 @@ def _configure_axes(self) -> None: def _initialise_plot(self) -> None: # Plot the initial orbit - t = self._orbit.orbital_angles() + t = self._get_anomaly_data(self._orbit.orbital_angles()) orbit_eq = self._orbit.orbit_eq x, y = orbit_eq.x, orbit_eq.y self._line, = self._ax.plot(x(t) , y(t), color = "#2F2F2F", alpha = 0.5, linewidth = 1.5) @@ -105,8 +108,17 @@ def _build_toolbar(self) -> None: toolbar.update() toolbar.pack(side = "bottom", fill = "x") + def _get_anomaly_data(self, angles: tuple[float]) -> NDArray[np.float64]: + lower_lim, upper_lim = angles + + # Check if angles represent closed or open orbit (open orbits will have negative angles). + # If the orbit is open then we need a small offset so the plot doesn't evaluate to infinity + # and cause runtime errors or unusual graphical artifacts. + delta = 0.0001 if lower_lim < 0 else 0 + return np.linspace(lower_lim + delta, upper_lim - delta, OrbitFigure.NUM_POINTS) + def redraw_orbit(self) -> None: - t = self._orbit.orbital_angles() + t = self._get_anomaly_data(self._orbit.orbital_angles()) orbit_eq = self._orbit.orbit_eq self._line.set_data(orbit_eq.x(t), orbit_eq.y(t)) @@ -131,7 +143,13 @@ def plot_periapsis_point(self) -> None: self._rp_point, = self._ax.plot( self._orbit.rp, 0, ms = 3, marker = "o", zorder = 9, color = "#502BF2", label = "$r_p$" ) - self._rp_annotation = self._ax.annotate("$r_p$", xy = (self._orbit.rp, 0), xycoords = "data", xytext = self.DISPLAY_TEXT_OFFSET, textcoords = "offset points") + self._rp_annotation = self._ax.annotate( + "$r_p$", + xy = (self._orbit.rp, 0), + xycoords = "data", + xytext = OrbitFigure.DISPLAY_TEXT_OFFSET, + textcoords = "offset points" + ) @staticmethod def _zoom_factory(ax: Axes, base_scale = 2.): From bba2f867e5fd21f8ca3f8019031f95d5943ea9ae Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 01:08:28 +0000 Subject: [PATCH 02/13] Wrote small explanatory comment --- src/orbit_visualiser/core/satellite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/orbit_visualiser/core/satellite.py b/src/orbit_visualiser/core/satellite.py index 46ff554..9b1a3ec 100644 --- a/src/orbit_visualiser/core/satellite.py +++ b/src/orbit_visualiser/core/satellite.py @@ -95,7 +95,7 @@ def update_satellite_properties(self) -> None: self._h = h # Helper quantities - den = 1 + e*np.cos(nu) + den = 1 + e*np.cos(nu) # Denominator of the orbit equation mu_over_h = mu/h # Geometry From 873b6c8414fd40f7ecdb49f693bd5d67c43b60c1 Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:02:25 +0000 Subject: [PATCH 03/13] Wrote pytest fixtures --- CHANGELOG.md | 3 +++ tests/conftest.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 tests/conftest.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1536941..d96afc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented here. # Unreleased - Certain orbital parameters can be displayed in the plot +- Tests architecture rewritten +- Wrote invariants unit tests +- PerifocalOrbitEq refactored to live in the Satellite class # 0.4.2 - 08/02/2026 - Refactored config builder and controller into variable, property and display builders/controllers diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..091b0de --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,42 @@ +import pytest +import numpy as np +from numpy.typing import NDArray +from math import pi +from typing import Callable +from orbit_visualiser.core import Satellite, Orbit, CentralBody + +# ---------- True anomaly grids -------------------- +@pytest.fixture(scope = "session") +def closed_anomaly_grid() -> NDArray[np.float64]: + return np.linspace(0, 2*pi, 20) + +@pytest.fixture(scope = "session") +def open_anomaly_grid() -> Callable[[Orbit, int], NDArray[np.float64]]: + def _create_grid(orbit: Orbit, num: int = 50) -> NDArray[np.float64]: + orbit_angles = orbit.orbital_angles() + return np.linspace(orbit_angles[0], orbit_angles[1], num) + return _create_grid + +# ---------- Orbital object factories -------------- +@pytest.fixture +def orbit_factory() -> Callable[[float, float], Orbit]: + def _create(e: float = 0.0, rp: float = 50_000.0) -> Orbit: + return Orbit(e, rp) + return _create + +@pytest.fixture +def central_body_factory() -> Callable[[float], CentralBody]: + def _create(mu: float = 398_600.0) -> CentralBody: + return CentralBody(mu) + return _create + +@pytest.fixture +def satellite_factory( + orbit_factory: Callable[[float, float], Orbit], + central_body_factory: Callable[[float], CentralBody] +) -> Callable[[float, float, float], Satellite]: + def _create(e: float = 0.0, rp: float = 50_000.0, mu: float = 398_600.0) -> Satellite: + orbit: Orbit = orbit_factory(e, rp) + central_body: CentralBody = central_body_factory(mu) + return Satellite(orbit, central_body) + return _create \ No newline at end of file From fc5a599bf5c6963e83a2a311da7fc411490d8c09 Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:02:43 +0000 Subject: [PATCH 04/13] Wrote test case script --- tests/test_cases.py | 46 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 tests/test_cases.py diff --git a/tests/test_cases.py b/tests/test_cases.py new file mode 100644 index 0000000..d34304b --- /dev/null +++ b/tests/test_cases.py @@ -0,0 +1,46 @@ +import itertools +from typing import Any + +# Tests to implement +# TODO: Magnitude of angular momentum vector +# TODO: Magnitude of eccentricity vector +# TODO: Periapsis/apoapsis symmetry (check apoapsis has true anomaly pi ahead of periapsis in a closed orbit) +# TODO: Orbital period consistency (need to figure out how to parametrize over semi-major axis values and from that get appropriate values for e and rp to pass in the test objects) + +def _create_test_cases(values_dict: dict[str, list[float]]) -> list[list[Any]]: + test_cases = [list(value) for value in itertools.product(*values_dict.values())] + + if "e" in values_dict: + for values in test_cases: + e = values[0] + values.append("closed" if e < 1 else "open") + + orbit_type: str + if e == 0: + orbit_type = "circular" + elif 0 < e < 1: + orbit_type = "elliptical" + elif e == 1: + orbit_type = "parabolic" + else: + orbit_type = "hyperbolic" + values.append(orbit_type) + + + return test_cases + +e_test_cases: list[float] = [0, 0.0001, 0.1, 0.5, 0.9, 0.99999, 1, 1.0000001, 1.5, 2, 10, 1000] +rp_test_cases: list[float] = [10, 6789, 20_000, 50_000, 100_000, 1_000_000, 938_382_001_928_942_153] +mu_test_cases: list[float] = [1.0, 4902.8, 42_828.0, 398600.0, 37_931_200.0] + +# Test cases for variable eccentricity, radius of periapsis and gravitational parameter +standard_test_cases = _create_test_cases({"e": e_test_cases, "rp": rp_test_cases, "mu": mu_test_cases}) + +# Test cases for constant eccentricity and variable radius of periapsis and gravitational parameter +rp_mu_test_cases = _create_test_cases({"rp": rp_test_cases, "mu": mu_test_cases}) + +# Test cases variable eccentricity with orbit type tags +e_tagged_test_cases = _create_test_cases({"e": e_test_cases}) + + + From e848612c01fa5b2dd5f3f7f57c27471a2540d880 Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:03:05 +0000 Subject: [PATCH 05/13] Moved circular orbit tests into invariants folder --- tests/invariants/circular_orbit_test.py | 77 +++++++++++++++++++++++++ tests/satellite_test.py | 60 ++----------------- 2 files changed, 81 insertions(+), 56 deletions(-) create mode 100644 tests/invariants/circular_orbit_test.py diff --git a/tests/invariants/circular_orbit_test.py b/tests/invariants/circular_orbit_test.py new file mode 100644 index 0000000..fe7fd84 --- /dev/null +++ b/tests/invariants/circular_orbit_test.py @@ -0,0 +1,77 @@ +import pytest +import numpy as np +from numpy.typing import NDArray +from typing import Callable +from orbit_visualiser.core import Orbit, Satellite +from tests.test_cases import rp_test_cases, rp_mu_test_cases + +@pytest.mark.parametrize("rp", rp_test_cases) +def test_circular_orbit_distance_parameters(orbit_factory, rp: float): + """ + For circular orbits the radius of periapsis, radius of apoapsis, semi-major axis, semi-minor axis + and orbital parameter should all be equivalent. + """ + orbit: Orbit = orbit_factory(rp = rp) + + periapsis = orbit.rp + distances = [ + orbit.ra, + orbit.a, + orbit.b, + orbit.p + ] + assert np.allclose(distances, periapsis) + +@pytest.mark.parametrize("rp, mu", rp_mu_test_cases) +def test_circular_orbit_flight_angle( + satellite_factory: Callable[[float, float, float], Satellite], + closed_anomaly_grid: NDArray[np.float64], + rp: float, + mu: float): + """ + For circular orbits the flight angle of a satellite at any true anomaly should be 0. + """ + satellite: Satellite = satellite_factory(rp = rp, mu = mu) + + for nu in closed_anomaly_grid: + satellite.nu = nu + satellite.update_satellite_properties() + + gam = satellite.gam + assert np.isclose(gam, 0) + +@pytest.mark.parametrize("rp, mu", rp_mu_test_cases) +def test_circular_orbit_radial_velocity( + satellite_factory: Callable[[float, float, float], Satellite], + closed_anomaly_grid: NDArray[np.float64], + rp: float, + mu: float): + """ + For circular orbits the radial velocity of a satellite at any true anomaly should be 0. + """ + satellite: Satellite = satellite_factory(rp = rp, mu = mu) + + for nu in closed_anomaly_grid: + satellite.nu = nu + satellite.update_satellite_properties() + + v_rad = satellite.v_radial + assert np.isclose(v_rad, 0) + +@pytest.mark.parametrize("rp, mu", rp_mu_test_cases) +def test_circular_orbit_anomalies( + satellite_factory: Callable[[float, float, float], Satellite], + closed_anomaly_grid: NDArray[np.float64], + rp: float, + mu: float): + """ + For circular orbits the eccentric, mean and true anomalies should always be equivalent. + """ + satellite: Satellite = satellite_factory(rp = rp, mu = mu) + + for nu in closed_anomaly_grid: + satellite.nu = nu + satellite.update_satellite_properties() + + anomalies = [satellite.m_anomaly, satellite.e_anomaly] + assert np.allclose(anomalies, nu) \ No newline at end of file diff --git a/tests/satellite_test.py b/tests/satellite_test.py index e1240ed..faf0000 100644 --- a/tests/satellite_test.py +++ b/tests/satellite_test.py @@ -1,59 +1,7 @@ +import pytest from math import pi -from pytest import Subtests import numpy as np -from orbit_visualiser.core import CentralBody, Orbit, Satellite +from orbit_visualiser.core import Satellite - -def test_circular_orbit_flight_angle(subtests: Subtests): - """ - For circular orbits the flight angle of a satellite at any true anomaly should be 0. - - test_cases is the interval [0, 2pi] radians split into 20, so each value of the true - anomaly increases by roughly 0.314 radians for each test. - """ - satellite = Satellite(Orbit(), CentralBody()) - test_cases = np.linspace(0, 2*pi, num = 20) - - for i, nu in enumerate(test_cases): - with subtests.test("Circular orbit flight angle test cases", i = i): - satellite.nu = nu - satellite.update_satellite_properties() - - gam = satellite.gam - assert np.isclose(gam, 0) - -def test_circular_orbit_radial_velocity(subtests: Subtests): - """ - For circular orbits the radial velocity of a satellite at any true anomaly should be 0. - - test_cases is the interval [0, 2pi] radians split into 20, so each value of the true - anomaly increases by roughly 0.314 radians for each test. - """ - satellite = Satellite(Orbit(), CentralBody()) - test_cases = np.linspace(0, 2*pi, num = 20) - - for i, nu in enumerate(test_cases): - with subtests.test("Circular orbit radial velocity test cases", i = i): - satellite.nu = nu - satellite.update_satellite_properties() - - v_rad = satellite.v_radial - assert np.isclose(v_rad, 0) - -def test_circular_orbit_anomalies(subtests: Subtests): - """ - For circular orbits the eccentric, mean and true anomalies should always be equivalent. - - test_cases is the interval [0, 2pi] radians split into 20, so each value of the true - anomaly increases by roughly 0.314 radians for each test. - """ - satellite = Satellite(Orbit(), CentralBody()) - test_cases = np.linspace(0, 2*pi, num = 20) - - for i, nu in enumerate(test_cases): - with subtests.test("Circular orbit mean/eccentric/true anomaly test cases", i = i): - satellite.nu = nu - satellite.update_satellite_properties() - - anomalies = [satellite.m_anomaly, satellite.e_anomaly] - assert np.allclose(anomalies, nu) \ No newline at end of file +def test_satellite_sanity(): + assert True From 64bc84664d9a1be063ff00b2e4360d6b2913daab Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:03:31 +0000 Subject: [PATCH 06/13] Wrote energy invariance tests --- tests/invariants/energy_test.py | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/invariants/energy_test.py diff --git a/tests/invariants/energy_test.py b/tests/invariants/energy_test.py new file mode 100644 index 0000000..73573b6 --- /dev/null +++ b/tests/invariants/energy_test.py @@ -0,0 +1,64 @@ +import pytest +import numpy as np +from typing import Callable +from numpy.typing import NDArray +from orbit_visualiser.core import Orbit, Satellite +from tests.test_cases import standard_test_cases + + +@pytest.mark.parametrize("e, rp, mu, closure, orbit_type", standard_test_cases) +def test_specific_energy_conservation( + satellite_factory: Callable[[float, float, float], Satellite], + closed_anomaly_grid: NDArray[np.float64], + open_anomaly_grid: Callable[[Orbit, int], NDArray[np.float64]], + e: float, + rp: float, + mu: float, + closure: str, + orbit_type: str +): + satellite: Satellite = satellite_factory(e, rp, mu) + + if closure == "closed": + anomaly_grid = closed_anomaly_grid + else: + anomaly_grid = open_anomaly_grid(satellite._orbit) + + for nu in anomaly_grid: + satellite.nu = nu + satellite.update_satellite_properties() + + specific_energy = satellite.eps + + specific_kin_energy = 0.5*satellite.v**2 + specific_pot_energy = -mu/satellite.r + vis_viva_energy = specific_kin_energy + specific_pot_energy + + assert np.isclose(specific_energy, vis_viva_energy) + +@pytest.mark.parametrize("e, rp, mu, closure, orbit_type", standard_test_cases) +def test_specific_and_characteristic_energy_relation( + satellite_factory: Callable[[float, float, float], Satellite], + closed_anomaly_grid: NDArray[np.float64], + open_anomaly_grid: Callable[[Orbit, int], NDArray[np.float64]], + e: float, + rp: float, + mu: float, + closure: str, + orbit_type: str +): + satellite: Satellite = satellite_factory(e, rp, mu) + + if closure == "closed": + anomaly_grid = closed_anomaly_grid + else: + anomaly_grid = open_anomaly_grid(satellite._orbit) + + for nu in anomaly_grid: + satellite.nu = nu + satellite.update_satellite_properties() + + specific_energy = satellite.eps + characteristic_energy = satellite.c3 + + assert np.isclose(2*specific_energy, characteristic_energy) From 5e1806c7f213e3ca4e77fc72c2a6b1f065b8493e Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 14:03:46 +0000 Subject: [PATCH 07/13] Streamlined orbit type sanity test --- tests/orbit_test.py | 65 +++++++++++---------------------------------- 1 file changed, 15 insertions(+), 50 deletions(-) diff --git a/tests/orbit_test.py b/tests/orbit_test.py index 9050c15..e6fb93a 100644 --- a/tests/orbit_test.py +++ b/tests/orbit_test.py @@ -1,58 +1,23 @@ -from pytest import Subtests +import pytest import numpy as np +from typing import Callable from orbit_visualiser.core import Orbit - - - -def test_circular_orbit_distance_parameters(subtests: Subtests): - """ - For circular orbits the radius of periapsis, radius of apoapsis, semi-major axis, semi-minor axis - and orbital parameter should all be equivalent. - - circular_orbit_test_cases is the list of initial radius of periapsis we set for our orbits. - """ - circular_orbit_test_cases = [ - 0.000000001, - 1, - 6789, - 50000, - 1000000, - 4095384905805829034 - ] - - for i, periapsis in enumerate(circular_orbit_test_cases): - with subtests.test("Circular orbit distance parameters test cases", i = i): - orbit = Orbit(rp = periapsis) - periapsis = orbit.rp - distances = [ - orbit.ra, - orbit.a, - orbit.b, - orbit.p - ] - assert np.allclose(distances, periapsis) - -def test_orbit_type(subtests: Subtests): +from tests.test_cases import e_tagged_test_cases + +@pytest.mark.parametrize("e, closure, orbit_type", e_tagged_test_cases) +def test_orbit_type_sanity( + orbit_factory: Callable[[float, float], Orbit], + e: float, + closure: str, + orbit_type: str): """ If the eccentricity is 0<= e < 1 then the orbit is closed (True) and either circular (e = 0) or elliptical (0 < e < 1). If the eccentricity is e >= 1 then the orbit is not closed (False) and is either parabolic (e = 1) or hyperbolic (e > 1). - - eccentricity_test_cases is a dictionary giving the eccentricity test value and the expected - orbit type and is_closed boolean value. """ - eccentricity_test_cases = { - 0 : ("circular", True), - 0.0000000001: ("elliptical", True), - 0.5: ("elliptical", True), - 0.9999999999: ("elliptical", True), - 1: ("parabolic", False), - 1.00000000001: ("hyperbolic", False), - 1.5: ("hyperbolic", False), - 10000000: ("hyperbolic", False) - } + orbit: Orbit = orbit_factory(e = e) + + test_orbit_closure = "closed" if orbit.is_closed else "open" + test_orbit_type = orbit.orbit_type - for i, (e, output) in enumerate(eccentricity_test_cases.items()): - with subtests.test("Orbit type test cases", i = i): - orbit = Orbit(e = e) - assert orbit.orbit_type == output[0] and orbit.is_closed is output[1] \ No newline at end of file + assert test_orbit_closure == closure and test_orbit_type == orbit_type \ No newline at end of file From b0420f91473b73978d4a4e7e7573ffe7b1ee6da6 Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:12:54 +0000 Subject: [PATCH 08/13] Updated unreleased changes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d96afc4..3c72ed3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented here. # Unreleased - Certain orbital parameters can be displayed in the plot +- Implemented automatic testing - Tests architecture rewritten - Wrote invariants unit tests - PerifocalOrbitEq refactored to live in the Satellite class From 7c3cfdf8b3371b1cab6debf090c5f6c3b79d539e Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:13:21 +0000 Subject: [PATCH 09/13] Wrote TODO --- tests/orbit_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/orbit_test.py b/tests/orbit_test.py index e6fb93a..f2a147a 100644 --- a/tests/orbit_test.py +++ b/tests/orbit_test.py @@ -4,6 +4,8 @@ from orbit_visualiser.core import Orbit from tests.test_cases import e_tagged_test_cases +# TODO: implement sanity tests (use different formulae for the same value and check they are equal) + @pytest.mark.parametrize("e, closure, orbit_type", e_tagged_test_cases) def test_orbit_type_sanity( orbit_factory: Callable[[float, float], Orbit], From 02e311d0ff2ba9fdd154f67bbcf4f7669a360b7a Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:13:36 +0000 Subject: [PATCH 10/13] Included additional test case lists --- tests/test_cases.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/test_cases.py b/tests/test_cases.py index d34304b..796b870 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -29,13 +29,23 @@ def _create_test_cases(values_dict: dict[str, list[float]]) -> list[list[Any]]: return test_cases -e_test_cases: list[float] = [0, 0.0001, 0.1, 0.5, 0.9, 0.99999, 1, 1.0000001, 1.5, 2, 10, 1000] -rp_test_cases: list[float] = [10, 6789, 20_000, 50_000, 100_000, 1_000_000, 938_382_001_928_942_153] -mu_test_cases: list[float] = [1.0, 4902.8, 42_828.0, 398600.0, 37_931_200.0] +e_closed_test_cases: list[float] = [0, 0.0001, 0.5, 0.99999] +e_open_test_cases: list[float] = [1, 1.0000001, 1.5, 1000] +e_elliptical_test_cases: list[float] = [0.0001, 0.5, 0.99999] +e_hyperbolic_test_cases: list[float] = [1.0000001, 1.5, 1000] +e_test_cases: list[float] = e_closed_test_cases + e_open_test_cases +rp_test_cases: list[float] = [10, 6789, 100_000, 1_000_000, 938_382_001_928_942_153] +mu_test_cases: list[float] = [1.0, 4902.8, 398600.0, 37_931_200.0] # Test cases for variable eccentricity, radius of periapsis and gravitational parameter standard_test_cases = _create_test_cases({"e": e_test_cases, "rp": rp_test_cases, "mu": mu_test_cases}) +# Test cases for variable eccentricity < 1, radius of periapsis and gravitational parameter +standard_closed_test_cases = _create_test_cases({"e": e_closed_test_cases, "rp": rp_test_cases, "mu": mu_test_cases}) + +# Test cases for variable eccentricity > 1, radius of periapsis and gravitational parameter +standard_open_test_cases = _create_test_cases({"e": e_open_test_cases, "rp": rp_test_cases, "mu": mu_test_cases}) + # Test cases for constant eccentricity and variable radius of periapsis and gravitational parameter rp_mu_test_cases = _create_test_cases({"rp": rp_test_cases, "mu": mu_test_cases}) From d96f9632d2ae683e4886ee04632e9470abb606ac Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:13:51 +0000 Subject: [PATCH 11/13] Wrote sanity checks --- tests/satellite_test.py | 96 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/tests/satellite_test.py b/tests/satellite_test.py index faf0000..06be3bd 100644 --- a/tests/satellite_test.py +++ b/tests/satellite_test.py @@ -1,7 +1,97 @@ import pytest from math import pi import numpy as np -from orbit_visualiser.core import Satellite +from typing import Callable +from numpy.typing import NDArray +from orbit_visualiser.core import Satellite, Orbit +from tests.test_cases import standard_test_cases, standard_open_test_cases + +# TODO: implement sanity tests (use different formulae for the same value and check they are equal) + +@pytest.mark.parametrize("e, rp, mu, closure, orbit_type", standard_open_test_cases) +def test_satellite_velocity_sanity( + satellite_factory: Callable[[float, float, float], Satellite], + open_anomaly_grid: Callable[[Orbit, int], NDArray[np.float64]], + e: float, + rp: float, + mu: float, + closure: str, + orbit_type: str +): + """ + Sanity test to check that different formulae for the velocity give the same result. + """ + satellite: Satellite = satellite_factory(e, rp, mu) + + anomaly_grid = open_anomaly_grid(satellite._orbit) + + for nu in anomaly_grid: + satellite.nu = nu + satellite.update_satellite_properties() + + v = np.sqrt(satellite.v_inf**2 + satellite.v_esc**2) + + assert np.isclose(v, satellite.v) + +@pytest.mark.parametrize("e, rp, mu, closure, orbit_type", standard_test_cases) +def test_satellite_azimuthal_velocity_sanity( + satellite_factory: Callable[[float, float, float], Satellite], + closed_anomaly_grid: NDArray[np.float64], + open_anomaly_grid: Callable[[Orbit, int], NDArray[np.float64]], + e: float, + rp: float, + mu: float, + closure: str, + orbit_type: str +): + """ + Sanity test to check that different formulae for the azimuthal velocity give the same result. + """ + satellite: Satellite = satellite_factory(e, rp, mu) + + if closure == "closed": + anomaly_grid = closed_anomaly_grid + else: + anomaly_grid = open_anomaly_grid(satellite._orbit) + + for nu in anomaly_grid: + satellite.nu = nu + satellite.update_satellite_properties() + + v_azim_vals = [ + satellite.h/satellite.r, + satellite.v*np.cos(satellite.gam) + ] + + + assert np.allclose(v_azim_vals, satellite.v_azim) + +@pytest.mark.parametrize("e, rp, mu, closure, orbit_type", standard_test_cases) +def test_satellite_radial_velocity_sanity( + satellite_factory: Callable[[float, float, float], Satellite], + closed_anomaly_grid: NDArray[np.float64], + open_anomaly_grid: Callable[[Orbit, int], NDArray[np.float64]], + e: float, + rp: float, + mu: float, + closure: str, + orbit_type: str +): + """ + Sanity test to check that different formulae for the radial velocity give the same result. + """ + satellite: Satellite = satellite_factory(e, rp, mu) + + if closure == "closed": + anomaly_grid = closed_anomaly_grid + else: + anomaly_grid = open_anomaly_grid(satellite._orbit) + + for nu in anomaly_grid: + satellite.nu = nu + satellite.update_satellite_properties() + + v_radial = satellite.v*np.sin(satellite.gam) + + assert np.isclose(v_radial, satellite.v_radial) -def test_satellite_sanity(): - assert True From f7e4d21c6d1b540d0e82e481d3dc95c6f6e5cdc5 Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:17:35 +0000 Subject: [PATCH 12/13] Minor readability edit --- tests/test_cases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cases.py b/tests/test_cases.py index 796b870..84d29e7 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -35,7 +35,7 @@ def _create_test_cases(values_dict: dict[str, list[float]]) -> list[list[Any]]: e_hyperbolic_test_cases: list[float] = [1.0000001, 1.5, 1000] e_test_cases: list[float] = e_closed_test_cases + e_open_test_cases rp_test_cases: list[float] = [10, 6789, 100_000, 1_000_000, 938_382_001_928_942_153] -mu_test_cases: list[float] = [1.0, 4902.8, 398600.0, 37_931_200.0] +mu_test_cases: list[float] = [1.0, 4902.8, 398_600.0, 37_931_200.0] # Test cases for variable eccentricity, radius of periapsis and gravitational parameter standard_test_cases = _create_test_cases({"e": e_test_cases, "rp": rp_test_cases, "mu": mu_test_cases}) From b9e45edb87433c52df4b8aca0cb3fdbcfe879c0e Mon Sep 17 00:00:00 2001 From: Andrew Phimister <157378252+Phim226@users.noreply.github.com> Date: Mon, 9 Feb 2026 15:17:53 +0000 Subject: [PATCH 13/13] Created parabolic trajectory test file and TODO --- tests/invariants/parabolic_orbit_test.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 tests/invariants/parabolic_orbit_test.py diff --git a/tests/invariants/parabolic_orbit_test.py b/tests/invariants/parabolic_orbit_test.py new file mode 100644 index 0000000..e112982 --- /dev/null +++ b/tests/invariants/parabolic_orbit_test.py @@ -0,0 +1 @@ +# TODO: Write test that gam = nu/2 for parabolic trajectory \ No newline at end of file