import numpy as np
import pytest
from astrora._core import OrbitalElements, coe_to_rv, constants, rv_to_coe
class TestCurtisExamples:
def test_curtis_example_4_3(self):
r = np.array([-6045.0e3, -3490.0e3, 2500.0e3]) v = np.array([-3.457e3, 6.618e3, 2.533e3])
elements = rv_to_coe(r, v, constants.GM_EARTH)
expected_e = 0.1712 expected_i = 153.25 expected_raan = 255.28 expected_argp = 20.07 expected_nu = 28.45
assert (
abs(elements.e - expected_e) < 0.001
), f"Eccentricity mismatch: got {elements.e}, expected {expected_e}"
assert (
abs(np.rad2deg(elements.i) - expected_i) < 0.1
), f"Inclination mismatch: got {np.rad2deg(elements.i)}°, expected {expected_i}°"
assert (
abs(np.rad2deg(elements.raan) - expected_raan) < 0.1
), f"RAAN mismatch: got {np.rad2deg(elements.raan)}°, expected {expected_raan}°"
assert (
abs(np.rad2deg(elements.argp) - expected_argp) < 0.1
), f"Argument of periapsis mismatch: got {np.rad2deg(elements.argp)}°, expected {expected_argp}°"
assert (
abs(np.rad2deg(elements.nu) - expected_nu) < 0.1
), f"True anomaly mismatch: got {np.rad2deg(elements.nu)}°, expected {expected_nu}°"
expected_p = 8530.47e3 actual_p = elements.p assert (
abs(actual_p - expected_p) < 100.0
), f"Semi-latus rectum mismatch: got {actual_p/1e3} km, expected {expected_p/1e3} km"
class TestMolniyaOrbit:
def test_molniya_orbit_example(self):
r = np.array([9031.5e3, -5316.9e3, -1647.2e3])
v = np.array([-2.8640e3, 5.1112e3, -5.0805e3])
elements = rv_to_coe(r, v, constants.GM_EARTH)
expected_a_min = 26000e3 expected_a_max = 27000e3
expected_e_min = 0.70
expected_e_max = 0.78
expected_i_min = 60.0 expected_i_max = 66.0
assert (
expected_a_min < elements.a < expected_a_max
), f"Semi-major axis out of Molniya range: got {elements.a/1e3} km"
assert (
expected_e_min < elements.e < expected_e_max
), f"Eccentricity out of Molniya range: got {elements.e}"
assert (
expected_i_min < np.rad2deg(elements.i) < expected_i_max
), f"Inclination out of Molniya range: got {np.rad2deg(elements.i)}°"
period = elements.orbital_period(constants.GM_EARTH)
expected_period = 12.0 * 3600.0 assert (
abs(period - expected_period) < 600.0
), f"Orbital period mismatch: got {period/3600} hours, expected ~12 hours"
def test_molniya_apogee_perigee(self):
a = 26564e3 e = 0.74 i = np.deg2rad(63.4)
elements = OrbitalElements(
a=a,
e=e,
i=i,
raan=0.0,
argp=np.deg2rad(-90.0), nu=0.0,
)
r_a = elements.apoapsis_distance
r_p = elements.periapsis_distance
altitude_a = (r_a - constants.R_EARTH) / 1e3 altitude_p = (r_p - constants.R_EARTH) / 1e3
assert 35000 < altitude_a < 45000, f"Apogee altitude out of range: {altitude_a} km"
assert 400 < altitude_p < 1500, f"Perigee altitude out of range: {altitude_p} km"
class TestGeostationary:
def test_geo_orbit_parameters(self):
a_geo = 42164e3
elements = OrbitalElements(
a=a_geo, e=0.0, i=0.0, raan=0.0, argp=0.0, nu=0.0 )
period = elements.orbital_period(constants.GM_EARTH)
sidereal_day = 86164.0916
assert (
abs(period - sidereal_day) < 1.0
), f"GEO period mismatch: got {period} s, expected {sidereal_day} s"
r, v = coe_to_rv(elements, constants.GM_EARTH)
assert (
abs(np.linalg.norm(r) - a_geo) < 1.0
), "Position magnitude doesn't match semi-major axis"
altitude = (np.linalg.norm(r) - constants.R_EARTH) / 1e3
expected_altitude = 35786.0
assert (
abs(altitude - expected_altitude) < 10.0
), f"GEO altitude mismatch: got {altitude} km, expected {expected_altitude} km"
v_mag = np.linalg.norm(v)
expected_v = 3.0747e3
assert (
abs(v_mag - expected_v) < 1.0
), f"GEO velocity mismatch: got {v_mag} m/s, expected {expected_v} m/s"
class TestLEOOrbits:
def test_iss_current_orbit(self):
altitude = 408e3 a = constants.R_EARTH + altitude
e = 0.0002 i = np.deg2rad(51.64)
elements = OrbitalElements(a=a, e=e, i=i, raan=0.0, argp=0.0, nu=0.0)
period = elements.orbital_period(constants.GM_EARTH)
period_minutes = period / 60.0
assert 90.0 < period_minutes < 95.0, f"ISS period out of range: {period_minutes} minutes"
r, v = coe_to_rv(elements, constants.GM_EARTH)
v_mag = np.linalg.norm(v) / 1e3
assert 7.5 < v_mag < 7.8, f"ISS velocity out of range: {v_mag} km/s"
def test_polar_sun_synchronous_orbit(self):
altitude = 700e3 a = constants.R_EARTH + altitude
e = 0.001 i = np.deg2rad(98.0)
elements = OrbitalElements(a=a, e=e, i=i, raan=0.0, argp=0.0, nu=0.0)
period = elements.orbital_period(constants.GM_EARTH)
period_minutes = period / 60.0
assert (
97.0 < period_minutes < 101.0
), f"Sun-synchronous orbit period out of range: {period_minutes} minutes"
assert 97.0 < np.rad2deg(elements.i) < 99.0, "Inclination not in sun-synchronous range"
class TestRoundtripConsistency:
def test_roundtrip_curtis_example(self):
r_orig = np.array([-6045.0e3, -3490.0e3, 2500.0e3])
v_orig = np.array([-3.457e3, 6.618e3, 2.533e3])
elements = rv_to_coe(r_orig, v_orig, constants.GM_EARTH)
r_new, v_new = coe_to_rv(elements, constants.GM_EARTH)
r_error = np.linalg.norm(r_new - r_orig)
v_error = np.linalg.norm(v_new - v_orig)
assert r_error < 100.0, f"Position error in roundtrip: {r_error} m"
assert v_error < 0.1, f"Velocity error in roundtrip: {v_error} m/s"
energy_orig = np.linalg.norm(v_orig) ** 2 / 2.0 - constants.GM_EARTH / np.linalg.norm(
r_orig
)
energy_new = np.linalg.norm(v_new) ** 2 / 2.0 - constants.GM_EARTH / np.linalg.norm(r_new)
assert (
abs(energy_new - energy_orig) / abs(energy_orig) < 1e-10
), "Energy not conserved in roundtrip"
def test_roundtrip_molniya_orbit(self):
r_orig = np.array([9031.5e3, -5316.9e3, -1647.2e3])
v_orig = np.array([-2.8640e3, 5.1112e3, -5.0805e3])
elements = rv_to_coe(r_orig, v_orig, constants.GM_EARTH)
r_new, v_new = coe_to_rv(elements, constants.GM_EARTH)
r_error = np.linalg.norm(r_new - r_orig)
v_error = np.linalg.norm(v_new - v_orig)
assert r_error < 200.0, f"Position error in Molniya roundtrip: {r_error} m"
assert v_error < 1.0, f"Velocity error in Molniya roundtrip: {v_error} m/s"
class TestConservationLaws:
def test_energy_conservation_curtis_example(self):
r = np.array([-6045.0e3, -3490.0e3, 2500.0e3])
v = np.array([-3.457e3, 6.618e3, 2.533e3])
r_mag = np.linalg.norm(r)
v_mag = np.linalg.norm(v)
energy_orig = v_mag**2 / 2.0 - constants.GM_EARTH / r_mag
elements = rv_to_coe(r, v, constants.GM_EARTH)
r_new, v_new = coe_to_rv(elements, constants.GM_EARTH)
r_mag_new = np.linalg.norm(r_new)
v_mag_new = np.linalg.norm(v_new)
energy_new = v_mag_new**2 / 2.0 - constants.GM_EARTH / r_mag_new
relative_error = abs(energy_new - energy_orig) / abs(energy_orig)
assert relative_error < 1e-10, f"Energy not conserved: relative error = {relative_error}"
def test_angular_momentum_conservation_curtis_example(self):
r = np.array([-6045.0e3, -3490.0e3, 2500.0e3])
v = np.array([-3.457e3, 6.618e3, 2.533e3])
h_orig = np.cross(r, v)
h_mag_orig = np.linalg.norm(h_orig)
elements = rv_to_coe(r, v, constants.GM_EARTH)
r_new, v_new = coe_to_rv(elements, constants.GM_EARTH)
h_new = np.cross(r_new, v_new)
h_mag_new = np.linalg.norm(h_new)
relative_error = abs(h_mag_new - h_mag_orig) / h_mag_orig
assert (
relative_error < 1e-10
), f"Angular momentum not conserved: relative error = {relative_error}"
h_orig_unit = h_orig / h_mag_orig
h_new_unit = h_new / h_mag_new
direction_error = np.linalg.norm(h_orig_unit - h_new_unit)
assert (
direction_error < 1e-10
), f"Angular momentum direction not conserved: error = {direction_error}"
class TestEdgeCasesWithKnownOrbits:
def test_highly_eccentric_orbit(self):
a = 20000e3 e = 0.85 i = np.deg2rad(45.0)
elements = OrbitalElements(a=a, e=e, i=i, raan=0.0, argp=0.0, nu=0.0)
r, v = coe_to_rv(elements, constants.GM_EARTH)
elements_new = rv_to_coe(r, v, constants.GM_EARTH)
assert abs(elements_new.a - a) < 100.0, "Semi-major axis not preserved"
assert abs(elements_new.e - e) < 0.001, "Eccentricity not preserved"
assert abs(elements_new.i - i) < 1e-6, "Inclination not preserved"
def test_near_polar_orbit(self):
altitude = 800e3
a = constants.R_EARTH + altitude
e = 0.001
i = np.deg2rad(89.9)
elements = OrbitalElements(a=a, e=e, i=i, raan=0.0, argp=0.0, nu=0.0)
r, v = coe_to_rv(elements, constants.GM_EARTH)
elements_new = rv_to_coe(r, v, constants.GM_EARTH)
assert (
abs(elements_new.i - i) < 1e-6
), f"Polar orbit inclination not preserved: {np.rad2deg(elements_new.i)}°"
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])