import numpy as np
import pytest
from astrora._core import (
constants,
moon_position_simple,
propagate_state_keplerian,
propagate_thirdbody_dopri5,
propagate_thirdbody_rk4,
sun_moon_perturbation,
sun_position_simple,
third_body_perturbation,
)
class TestSunEphemeris:
def test_sun_position_at_j2000(self):
r_sun = sun_position_simple(0.0)
assert r_sun.shape == (3,)
assert abs(r_sun[0] - constants.AU) < 1e6 assert abs(r_sun[1]) < 1e9 assert abs(r_sun[2]) < 1.0
r_mag = np.linalg.norm(r_sun)
assert abs(r_mag - constants.AU) < 1e6
def test_sun_position_circular_orbit(self):
year_sec = 365.25636 * 86400.0
r_sun_0 = sun_position_simple(0.0)
r_sun_1yr = sun_position_simple(year_sec)
diff = r_sun_1yr - r_sun_0
assert np.linalg.norm(diff) < 1e6
def test_sun_position_quarter_year(self):
year_sec = 365.25636 * 86400.0
quarter_year = year_sec / 4.0
r_sun_q = sun_position_simple(quarter_year)
assert abs(r_sun_q[0]) < 1e9 assert abs(r_sun_q[1] - constants.AU) < 1e6
class TestMoonEphemeris:
def test_moon_position_at_j2000(self):
r_moon = moon_position_simple(0.0)
a_moon = 384_400_000.0
assert r_moon.shape == (3,)
assert abs(r_moon[0] - a_moon) < 1e3 assert abs(r_moon[1]) < 1e3
assert abs(r_moon[2]) < 1.0
r_mag = np.linalg.norm(r_moon)
assert abs(r_mag - a_moon) < 1e3
def test_moon_position_circular_orbit(self):
month_sec = 27.321661 * 86400.0
r_moon_0 = moon_position_simple(0.0)
r_moon_1m = moon_position_simple(month_sec)
diff = r_moon_1m - r_moon_0
assert np.linalg.norm(diff) < 1e4
class TestThirdBodyPerturbation:
def test_sun_perturbation_magnitude(self):
r_sat = np.array([7000e3, 0.0, 0.0]) r_sun = sun_position_simple(0.0)
a_sun = third_body_perturbation(r_sat, r_sun, constants.GM_SUN)
assert a_sun.shape == (3,)
a_mag = np.linalg.norm(a_sun)
assert a_mag > 1e-7
assert a_mag < 3e-6
def test_moon_perturbation_magnitude(self):
r_sat = np.array([7000e3, 0.0, 0.0])
r_moon = moon_position_simple(0.0)
a_moon = third_body_perturbation(r_sat, r_moon, constants.GM_MOON)
assert a_moon.shape == (3,)
a_mag = np.linalg.norm(a_moon)
assert a_mag > 1e-7
assert a_mag < 5e-6
def test_third_body_perturbation_geo(self):
r_geo = np.array([42164e3, 0.0, 0.0]) r_sun = sun_position_simple(0.0)
a_sun = third_body_perturbation(r_geo, r_sun, constants.GM_SUN)
a_mag = np.linalg.norm(a_sun)
assert a_mag > 1e-7
assert a_mag < 1e-4
def test_sun_moon_combined(self):
r_sat = np.array([7000e3, 0.0, 0.0])
t = 0.0
a_combined = sun_moon_perturbation(r_sat, t)
r_sun = sun_position_simple(t)
r_moon = moon_position_simple(t)
a_sun = third_body_perturbation(r_sat, r_sun, constants.GM_SUN)
a_moon = third_body_perturbation(r_sat, r_moon, constants.GM_MOON)
a_manual = a_sun + a_moon
assert np.allclose(a_combined, a_manual, rtol=1e-12)
def test_third_body_time_dependence(self):
r_sat = np.array([7000e3, 0.0, 0.0])
a0 = sun_moon_perturbation(r_sat, 0.0)
a1 = sun_moon_perturbation(r_sat, 86400.0)
diff = np.linalg.norm(a1 - a0)
assert diff > 1e-8
class TestThirdBodyPropagation:
def test_propagate_thirdbody_rk4_sun_only(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0 t0 = 0.0
r1, v1 = propagate_thirdbody_rk4(
r0, v0, dt, constants.GM_EARTH, t0, include_sun=True, include_moon=False, n_steps=10
)
assert np.linalg.norm(r1) > 6e6 assert np.linalg.norm(v1) > 1000.0
def test_propagate_thirdbody_rk4_moon_only(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
t0 = 0.0
r1, v1 = propagate_thirdbody_rk4(
r0, v0, dt, constants.GM_EARTH, t0, include_sun=False, include_moon=True, n_steps=10
)
assert np.linalg.norm(r1) > 6e6
assert np.linalg.norm(v1) > 1000.0
def test_propagate_thirdbody_rk4_both(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
t0 = 0.0
r1, v1 = propagate_thirdbody_rk4(
r0, v0, dt, constants.GM_EARTH, t0, include_sun=True, include_moon=True, n_steps=10
)
assert np.linalg.norm(r1) > 6e6
assert np.linalg.norm(v1) > 1000.0
def test_propagate_thirdbody_dopri5(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
t0 = 0.0
r1, v1 = propagate_thirdbody_dopri5(
r0, v0, dt, constants.GM_EARTH, t0, include_sun=True, include_moon=True, tol=1e-8
)
assert np.linalg.norm(r1) > 6e6
assert np.linalg.norm(v1) > 1000.0
def test_thirdbody_vs_twobody_difference(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 3600.0 t0 = 0.0
r_twobody, v_twobody = propagate_state_keplerian(r0, v0, dt, constants.GM_EARTH)
r_thirdbody, v_thirdbody = propagate_thirdbody_rk4(
r0, v0, dt, constants.GM_EARTH, t0, include_sun=True, include_moon=True, n_steps=100
)
pos_diff = np.linalg.norm(r_twobody - r_thirdbody)
print(f"Position difference after 1 hour: {pos_diff:.2f} m")
assert pos_diff > 0.1 assert pos_diff < 1000.0
def test_thirdbody_geo_significant(self):
r0 = np.array([42164e3, 0.0, 0.0]) v0 = np.array([0.0, 3075.0, 0.0]) dt = 86400.0 t0 = 0.0
r_twobody, v_twobody = propagate_state_keplerian(r0, v0, dt, constants.GM_EARTH)
r_thirdbody, v_thirdbody = propagate_thirdbody_rk4(
r0, v0, dt, constants.GM_EARTH, t0, include_sun=True, include_moon=True, n_steps=1000
)
pos_diff = np.linalg.norm(r_twobody - r_thirdbody)
print(f"GEO position difference after 1 day: {pos_diff:.2f} m")
assert pos_diff > 100.0 assert pos_diff < 1e6
def test_rk4_vs_dopri5_agreement(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
t0 = 0.0
r_rk4, v_rk4 = propagate_thirdbody_rk4(
r0, v0, dt, constants.GM_EARTH, t0, include_sun=True, include_moon=True, n_steps=100
)
r_dopri5, v_dopri5 = propagate_thirdbody_dopri5(
r0, v0, dt, constants.GM_EARTH, t0, include_sun=True, include_moon=True, tol=1e-10
)
pos_diff = np.linalg.norm(r_rk4 - r_dopri5)
vel_diff = np.linalg.norm(v_rk4 - v_dopri5)
assert pos_diff < 100.0 assert vel_diff < 0.1
class TestEdgeCases:
def test_third_body_perturbation_wrong_shape(self):
r_sat = np.array([7000e3, 0.0]) r_sun = sun_position_simple(0.0)
with pytest.raises(ValueError):
third_body_perturbation(r_sat, r_sun, constants.GM_SUN)
def test_propagate_thirdbody_wrong_shape(self):
r0 = np.array([7000e3, 0.0]) v0 = np.array([0.0, 7546.0, 0.0])
with pytest.raises(ValueError):
propagate_thirdbody_rk4(
r0, v0, 600.0, constants.GM_EARTH, 0.0, include_sun=True, include_moon=True
)
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])