import numpy as np
import pytest
from astrora._core import (
constants,
propagate_srp_dopri5,
propagate_srp_rk4,
propagate_state_keplerian,
shadow_function,
srp_acceleration,
sun_position_simple,
)
class TestShadowFunction:
def test_shadow_full_sunlight(self):
r_sat = np.array([7000e3, 0.0, 0.0])
r_sun = sun_position_simple(0.0)
k = shadow_function(r_sat, r_sun, constants.R_EARTH)
assert abs(k - 1.0) < 1e-10
def test_shadow_umbra_leo(self):
r_sat = np.array([-7000e3, 0.0, 0.0])
r_sun = np.array([constants.AU, 0.0, 0.0])
k = shadow_function(r_sat, r_sun, constants.R_EARTH)
assert k < 0.1, f"Expected k≈0 in umbra, got k={k}"
def test_shadow_penumbra_exists(self):
r_sat = np.array([-8000e3, constants.R_EARTH * 0.7, 0.0])
r_sun = np.array([constants.AU, 0.0, 0.0])
k = shadow_function(r_sat, r_sun, constants.R_EARTH)
assert 0.0 <= k <= 1.0
def test_shadow_geo_sunlight(self):
r_sat = np.array([42164e3, 0.0, 0.0])
r_sun = sun_position_simple(0.0)
k = shadow_function(r_sat, r_sun, constants.R_EARTH)
assert k > 0.9
def test_shadow_function_range(self):
positions = [
np.array([7000e3, 0.0, 0.0]),
np.array([0.0, 7000e3, 0.0]),
np.array([0.0, 0.0, 7000e3]),
np.array([-7000e3, 0.0, 0.0]),
np.array([42164e3, 0.0, 0.0]),
]
for r_sat in positions:
r_sun = sun_position_simple(0.0)
k = shadow_function(r_sat, r_sun, constants.R_EARTH)
assert 0.0 <= k <= 1.0, f"Shadow factor {k} out of range [0,1]"
def test_shadow_invalid_vector_sizes(self):
r_sat = np.array([7000e3, 0.0]) r_sun = sun_position_simple(0.0)
with pytest.raises(ValueError):
shadow_function(r_sat, r_sun, constants.R_EARTH)
class TestSRPAcceleration:
def test_srp_acceleration_direction(self):
r_sat = np.array([7000e3, 0.0, 0.0])
r_sun = sun_position_simple(0.0)
area_mass_ratio = 0.01 C_r = 1.3
a_srp = srp_acceleration(r_sat, r_sun, area_mass_ratio, C_r, constants.R_EARTH)
r_sun_sat = r_sun - r_sat
dot_product = np.dot(a_srp, r_sun_sat)
assert dot_product > 0.0, "SRP should point away from Sun"
def test_srp_magnitude_reasonable(self):
r_sat = np.array([42164e3, 0.0, 0.0])
r_sun = sun_position_simple(0.0)
area_mass_ratio = 0.01 C_r = 1.3
a_srp = srp_acceleration(r_sat, r_sun, area_mass_ratio, C_r, constants.R_EARTH)
mag = np.linalg.norm(a_srp)
assert mag > 1e-10, "SRP magnitude too small"
assert mag < 1e-5, "SRP magnitude too large"
def test_srp_zero_in_shadow(self):
r_sat = np.array([-7000e3, 0.0, 0.0])
r_sun = np.array([constants.AU, 0.0, 0.0])
area_mass_ratio = 0.01
C_r = 1.3
a_srp = srp_acceleration(r_sat, r_sun, area_mass_ratio, C_r, constants.R_EARTH)
mag = np.linalg.norm(a_srp)
assert mag < 1e-10
def test_srp_scales_with_area_mass_ratio(self):
r_sat = np.array([42164e3, 0.0, 0.0])
r_sun = sun_position_simple(0.0)
C_r = 1.3
area_mass_ratio_1 = 0.01
area_mass_ratio_2 = 0.02
a_srp_1 = srp_acceleration(r_sat, r_sun, area_mass_ratio_1, C_r, constants.R_EARTH)
a_srp_2 = srp_acceleration(r_sat, r_sun, area_mass_ratio_2, C_r, constants.R_EARTH)
mag_1 = np.linalg.norm(a_srp_1)
mag_2 = np.linalg.norm(a_srp_2)
ratio = mag_2 / mag_1
assert abs(ratio - 2.0) < 0.01
def test_srp_scales_with_reflectivity(self):
r_sat = np.array([42164e3, 0.0, 0.0])
r_sun = sun_position_simple(0.0)
area_mass_ratio = 0.01
C_r_1 = 1.0
C_r_2 = 2.0
a_srp_1 = srp_acceleration(r_sat, r_sun, area_mass_ratio, C_r_1, constants.R_EARTH)
a_srp_2 = srp_acceleration(r_sat, r_sun, area_mass_ratio, C_r_2, constants.R_EARTH)
mag_1 = np.linalg.norm(a_srp_1)
mag_2 = np.linalg.norm(a_srp_2)
ratio = mag_2 / mag_1
assert abs(ratio - 2.0) < 0.01
def test_srp_inverse_square_law(self):
r_sat_1au = np.array([0.0, 0.0, 0.0]) r_sun = sun_position_simple(0.0)
r_sun_direction = r_sun / np.linalg.norm(r_sun)
r_sat_2au = -constants.AU * r_sun_direction
area_mass_ratio = 0.01
C_r = 1.3
a_srp_1au = srp_acceleration(r_sat_1au, r_sun, area_mass_ratio, C_r, constants.R_EARTH)
a_srp_2au = srp_acceleration(r_sat_2au, r_sun, area_mass_ratio, C_r, constants.R_EARTH)
mag_1au = np.linalg.norm(a_srp_1au)
mag_2au = np.linalg.norm(a_srp_2au)
if mag_2au > 1e-15: ratio = mag_1au / mag_2au
assert 3.0 < ratio < 5.0, f"Inverse square law violated: ratio={ratio}"
def test_srp_invalid_vector_sizes(self):
r_sat = np.array([7000e3, 0.0]) r_sun = sun_position_simple(0.0)
area_mass_ratio = 0.01
C_r = 1.3
with pytest.raises(ValueError):
srp_acceleration(r_sat, r_sun, area_mass_ratio, C_r, constants.R_EARTH)
class TestSRPPropagation:
def test_propagate_srp_rk4_basic(self):
r0 = np.array([42164e3, 0.0, 0.0])
v0 = np.array([0.0, 3075.0, 0.0])
area_mass_ratio = 0.01
C_r = 1.3
t0 = 0.0
r1, v1 = propagate_srp_rk4(
r0,
v0,
3600.0, constants.GM_EARTH,
area_mass_ratio,
C_r,
constants.R_EARTH,
t0,
n_steps=100,
)
assert np.linalg.norm(r1) > 40e6 assert np.linalg.norm(v1) > 1000.0
def test_propagate_srp_dopri5_basic(self):
r0 = np.array([42164e3, 0.0, 0.0])
v0 = np.array([0.0, 3075.0, 0.0])
area_mass_ratio = 0.01
C_r = 1.3
t0 = 0.0
r1, v1 = propagate_srp_dopri5(
r0,
v0,
3600.0,
constants.GM_EARTH,
area_mass_ratio,
C_r,
constants.R_EARTH,
t0,
tol=1e-8,
)
assert np.linalg.norm(r1) > 40e6
assert np.linalg.norm(v1) > 1000.0
def test_srp_causes_orbital_drift(self):
r0 = np.array([42164e3, 0.0, 0.0])
v0 = np.array([0.0, 3075.0, 0.0])
area_mass_ratio = 0.02 C_r = 1.5
t0 = 0.0
dt = 86400.0
r_twobody, v_twobody = propagate_state_keplerian(r0, v0, dt, constants.GM_EARTH)
r_srp, v_srp = propagate_srp_rk4(
r0,
v0,
dt,
constants.GM_EARTH,
area_mass_ratio,
C_r,
constants.R_EARTH,
t0,
n_steps=1000,
)
pos_diff = np.linalg.norm(r_twobody - r_srp)
print(f"Position difference after 1 day: {pos_diff:.2f} m")
assert pos_diff > 1.0, "SRP should cause measurable difference"
assert pos_diff < 1e6, "Difference seems unreasonably large"
def test_srp_effect_increases_with_duration(self):
r0 = np.array([42164e3, 0.0, 0.0])
v0 = np.array([0.0, 3075.0, 0.0])
area_mass_ratio = 0.01
C_r = 1.3
t0 = 0.0
r_1day, v_1day = propagate_srp_rk4(
r0,
v0,
86400.0,
constants.GM_EARTH,
area_mass_ratio,
C_r,
constants.R_EARTH,
t0,
n_steps=1000,
)
r_2day, v_2day = propagate_srp_rk4(
r0,
v0,
2 * 86400.0,
constants.GM_EARTH,
area_mass_ratio,
C_r,
constants.R_EARTH,
t0,
n_steps=2000,
)
r_tb_1day, _ = propagate_state_keplerian(r0, v0, 86400.0, constants.GM_EARTH)
r_tb_2day, _ = propagate_state_keplerian(r0, v0, 2 * 86400.0, constants.GM_EARTH)
diff_1day = np.linalg.norm(r_1day - r_tb_1day)
diff_2day = np.linalg.norm(r_2day - r_tb_2day)
assert diff_2day > diff_1day, "SRP effect should accumulate"
def test_rk4_vs_dopri5_agreement(self):
r0 = np.array([42164e3, 0.0, 0.0])
v0 = np.array([0.0, 3075.0, 0.0])
area_mass_ratio = 0.01
C_r = 1.3
t0 = 0.0
dt = 3600.0
r_rk4, v_rk4 = propagate_srp_rk4(
r0, v0, dt, constants.GM_EARTH, area_mass_ratio, C_r, constants.R_EARTH, t0, n_steps=200
)
r_dopri5, v_dopri5 = propagate_srp_dopri5(
r0, v0, dt, constants.GM_EARTH, area_mass_ratio, C_r, constants.R_EARTH, t0, tol=1e-10
)
pos_diff = np.linalg.norm(r_rk4 - r_dopri5)
vel_diff = np.linalg.norm(v_rk4 - v_dopri5)
assert pos_diff < 1000.0, f"Position difference too large: {pos_diff} m"
assert vel_diff < 1.0, f"Velocity difference too large: {vel_diff} m/s"
def test_srp_leo_vs_geo(self):
r0_leo = np.array([7000e3, 0.0, 0.0])
v0_leo = np.array([0.0, 7546.0, 0.0])
r0_geo = np.array([42164e3, 0.0, 0.0])
v0_geo = np.array([0.0, 3075.0, 0.0])
area_mass_ratio = 0.01
C_r = 1.3
t0 = 0.0
dt = 3600.0
r_sun = sun_position_simple(t0)
a_srp_leo = srp_acceleration(r0_leo, r_sun, area_mass_ratio, C_r, constants.R_EARTH)
a_srp_geo = srp_acceleration(r0_geo, r_sun, area_mass_ratio, C_r, constants.R_EARTH)
mag_leo = np.linalg.norm(a_srp_leo)
mag_geo = np.linalg.norm(a_srp_geo)
assert mag_geo > 0.0
assert mag_leo > 0.0
def test_invalid_vector_sizes_propagation(self):
r0 = np.array([42164e3, 0.0]) v0 = np.array([0.0, 3075.0, 0.0])
area_mass_ratio = 0.01
C_r = 1.3
t0 = 0.0
with pytest.raises(ValueError):
propagate_srp_rk4(
r0, v0, 3600.0, constants.GM_EARTH, area_mass_ratio, C_r, constants.R_EARTH, t0
)
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])