import numpy as np
import pytest
from astrora._core import OrbitalElements, coe_to_rv, constants, rv_to_coe
class TestOrbitalElementsClass:
def test_create_orbital_elements(self):
elements = OrbitalElements(
a=7000e3, e=0.01, i=np.deg2rad(28.5), raan=np.deg2rad(45.0),
argp=np.deg2rad(30.0),
nu=np.deg2rad(60.0),
)
assert elements.a == 7000e3
assert elements.e == 0.01
assert abs(elements.i - np.deg2rad(28.5)) < 1e-10
def test_orbital_period(self):
elements = OrbitalElements(
a=6778e3, e=0.0001,
i=0.0,
raan=0.0,
argp=0.0,
nu=0.0,
)
period = elements.orbital_period(constants.GM_EARTH)
expected_period = 92.0 * 60.0
assert abs(period - expected_period) < 120.0
def test_periapsis_apoapsis(self):
a = 8000e3 e = 0.1
elements = OrbitalElements(a, e, 0.0, 0.0, 0.0, 0.0)
r_p = elements.periapsis_distance
r_a = elements.apoapsis_distance
assert abs(r_p - a * (1.0 - e)) < 1.0
assert abs(r_a - a * (1.0 + e)) < 1.0
assert abs(r_p - 7200e3) < 1.0
assert abs(r_a - 8800e3) < 1.0
def test_semi_latus_rectum(self):
elements = OrbitalElements(7000e3, 0.1, 0.0, 0.0, 0.0, 0.0)
p = elements.p
expected = 7000e3 * (1.0 - 0.1**2)
assert abs(p - expected) < 1.0
def test_string_representation(self):
elements = OrbitalElements(7000e3, 0.01, 0.0, 0.0, 0.0, 0.0)
repr_str = repr(elements)
assert "OrbitalElements" in repr_str
assert "7.000e" in repr_str or "7000000" in repr_str
str_str = str(elements)
assert "km" in str_str or "°" in str_str
def test_get_set_attributes(self):
elements = OrbitalElements(7000e3, 0.01, 0.0, 0.0, 0.0, 0.0)
elements.a = 8000e3
assert elements.a == 8000e3
elements.e = 0.05
assert elements.e == 0.05
class TestRvToCoe:
def test_circular_equatorial_orbit(self):
r = np.array([7000e3, 0.0, 0.0])
v_mag = np.sqrt(constants.GM_EARTH / 7000e3)
v = np.array([0.0, v_mag, 0.0])
elements = rv_to_coe(r, v, constants.GM_EARTH)
assert abs(elements.a - 7000e3) < 10.0
assert elements.e < 1e-6
assert elements.i < 1e-6
def test_elliptical_orbit(self):
r = np.array([7000e3, 0.0, 0.0])
v = np.array([0.0, 8000.0, 0.0])
elements = rv_to_coe(r, v, constants.GM_EARTH)
assert elements.e > 0.0
assert elements.e < 1.0
def test_inclined_circular_orbit(self):
r = np.array([7000e3, 0.0, 0.0])
v_mag = np.sqrt(constants.GM_EARTH / 7000e3)
angle = np.deg2rad(45.0)
v = np.array([0.0, v_mag * np.cos(angle), v_mag * np.sin(angle)])
elements = rv_to_coe(r, v, constants.GM_EARTH)
assert abs(elements.i - angle) < 1e-4
def test_polar_orbit(self):
r = np.array([7000e3, 0.0, 0.0])
v_mag = np.sqrt(constants.GM_EARTH / 7000e3)
v = np.array([0.0, 0.0, v_mag])
elements = rv_to_coe(r, v, constants.GM_EARTH)
assert abs(elements.i - np.pi / 2.0) < 1e-4
def test_invalid_input_wrong_size(self):
r = np.array([7000e3, 0.0]) v = np.array([0.0, 7500.0, 0.0])
with pytest.raises(ValueError, match="exactly 3 components"):
rv_to_coe(r, v, constants.GM_EARTH)
def test_zero_angular_momentum(self):
r = np.array([7000e3, 0.0, 0.0])
v = np.array([1000.0, 0.0, 0.0])
with pytest.raises(ValueError, match="degenerate"):
rv_to_coe(r, v, constants.GM_EARTH)
def test_custom_tolerance(self):
r = np.array([7000e3, 0.0, 0.0])
v_mag = np.sqrt(constants.GM_EARTH / 7000e3)
v = np.array([0.0, v_mag, 0.0])
elements = rv_to_coe(r, v, constants.GM_EARTH, tol=1e-10)
assert elements.e < 1e-6
class TestCoeToRv:
def test_circular_equatorial_at_periapsis(self):
elements = OrbitalElements(
a=7000e3,
e=0.0,
i=0.0,
raan=0.0,
argp=0.0,
nu=0.0,
)
r, v = coe_to_rv(elements, constants.GM_EARTH)
assert abs(np.linalg.norm(r) - 7000e3) < 10.0
assert abs(r[0] - 7000e3) < 10.0
assert abs(r[1]) < 10.0
assert abs(r[2]) < 10.0
v_expected = np.sqrt(constants.GM_EARTH / 7000e3)
assert abs(np.linalg.norm(v) - v_expected) < 1.0
def test_elliptical_orbit_at_periapsis(self):
elements = OrbitalElements(
a=8000e3,
e=0.1,
i=0.0,
raan=0.0,
argp=0.0,
nu=0.0,
)
r, v = coe_to_rv(elements, constants.GM_EARTH)
r_expected = 8000e3 * (1.0 - 0.1)
assert abs(np.linalg.norm(r) - r_expected) < 10.0
def test_inclined_orbit(self):
elements = OrbitalElements(
a=7000e3,
e=0.01,
i=np.deg2rad(60.0),
raan=0.0,
argp=0.0,
nu=0.0,
)
r, v = coe_to_rv(elements, constants.GM_EARTH)
assert r.shape == (3,)
assert v.shape == (3,)
assert r is not None
class TestRoundtripConversions:
def test_roundtrip_circular_equatorial(self):
r_orig = np.array([7000e3, 0.0, 0.0])
v_mag = np.sqrt(constants.GM_EARTH / 7000e3)
v_orig = np.array([0.0, v_mag, 0.0])
elements = rv_to_coe(r_orig, v_orig, constants.GM_EARTH)
r_new, v_new = coe_to_rv(elements, constants.GM_EARTH)
assert np.allclose(r_new, r_orig, atol=1.0)
assert np.allclose(v_new, v_orig, atol=0.1)
def test_roundtrip_elliptical(self):
r_orig = np.array([7000e3, 0.0, 0.0])
v_orig = np.array([0.0, 8000.0, 0.0])
elements = rv_to_coe(r_orig, v_orig, constants.GM_EARTH)
r_new, v_new = coe_to_rv(elements, constants.GM_EARTH)
assert np.allclose(r_new, r_orig, atol=10.0)
assert np.allclose(v_new, v_orig, atol=1.0)
def test_roundtrip_inclined(self):
r_orig = np.array([7000e3, 0.0, 0.0])
v_mag = np.sqrt(constants.GM_EARTH / 7000e3)
angle = np.deg2rad(60.0)
v_orig = np.array([0.0, v_mag * np.cos(angle), v_mag * np.sin(angle)])
elements = rv_to_coe(r_orig, v_orig, constants.GM_EARTH)
r_new, v_new = coe_to_rv(elements, constants.GM_EARTH)
assert np.allclose(r_new, r_orig, atol=10.0)
assert np.allclose(v_new, v_orig, atol=1.0)
h_orig = np.cross(r_orig, v_orig)
h_new = np.cross(r_new, v_new)
i_check = np.arccos(h_new[2] / np.linalg.norm(h_new))
assert abs(i_check - angle) < 1e-5
def test_roundtrip_high_eccentricity(self):
r_orig = np.array([7000e3, 0.0, 0.0])
v_orig = np.array([0.0, 10000.0, 0.0])
elements = rv_to_coe(r_orig, v_orig, constants.GM_EARTH)
assert elements.e > 0.5
assert elements.e < 1.0
r_new, v_new = coe_to_rv(elements, constants.GM_EARTH)
assert np.allclose(r_new, r_orig, atol=100.0)
assert np.allclose(v_new, v_orig, atol=10.0)
class TestKnownOrbits:
def test_iss_orbit_approximate(self):
elements = OrbitalElements(
a=6778e3, e=0.0001, i=np.deg2rad(51.6), raan=0.0,
argp=0.0,
nu=0.0,
)
period = elements.orbital_period(constants.GM_EARTH)
assert 5400 < period < 5700
r, v = coe_to_rv(elements, constants.GM_EARTH)
altitude = np.linalg.norm(r) - constants.R_EARTH
assert 390e3 < altitude < 420e3
def test_geostationary_orbit(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)
expected_period = 86164.0
assert abs(period - expected_period) < 60.0
class TestConservationLaws:
def test_energy_conservation(self):
r = np.array([7000e3, 0.0, 0.0])
v = np.array([0.0, 8000.0, 0.0])
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
assert abs(energy_new - energy_orig) / abs(energy_orig) < 1e-6
def test_angular_momentum_conservation(self):
r = np.array([7000e3, 1000e3, 0.0])
v = np.array([0.0, 7500.0, 500.0])
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)
assert abs(h_mag_new - h_mag_orig) / h_mag_orig < 1e-6
if __name__ == "__main__":
pytest.main([__file__, "-v"])