import numpy as np
import pytest
from astrora._core import (
constants,
drag_acceleration,
exponential_density,
propagate_drag_dopri5,
propagate_drag_rk4,
propagate_j2_drag_dopri5,
propagate_j2_drag_rk4,
propagate_state_keplerian,
)
class TestExponentialDensity:
def test_density_at_sea_level(self):
rho = exponential_density(0.0, constants.RHO0_EARTH, constants.H0_EARTH)
assert abs(rho - constants.RHO0_EARTH) < 1e-10
def test_density_decreases_with_altitude(self):
rho_100km = exponential_density(100e3, constants.RHO0_EARTH, constants.H0_EARTH)
rho_200km = exponential_density(200e3, constants.RHO0_EARTH, constants.H0_EARTH)
rho_400km = exponential_density(400e3, constants.RHO0_EARTH, constants.H0_EARTH)
assert rho_100km < constants.RHO0_EARTH
assert rho_200km < rho_100km
assert rho_400km < rho_200km
assert rho_400km < 1e-10
assert rho_400km > 0.0
def test_density_at_scale_height(self):
rho_H = exponential_density(constants.H0_EARTH, constants.RHO0_EARTH, constants.H0_EARTH)
expected = constants.RHO0_EARTH / np.e
assert abs(rho_H - expected) < 1e-10
class TestDragAcceleration:
def test_drag_opposes_velocity(self):
r = np.array([6778e3, 0.0, 0.0])
v = np.array([0.0, 7670.0, 0.0])
B = 50.0
a_drag = drag_acceleration(
r, v, constants.R_EARTH, constants.RHO0_EARTH, constants.H0_EARTH, B
)
dot_product = np.dot(a_drag, v)
assert dot_product < 0.0, "Drag should oppose velocity"
a_drag_normalized = a_drag / np.linalg.norm(a_drag)
v_normalized = v / np.linalg.norm(v)
cos_angle = np.dot(a_drag_normalized, v_normalized)
assert abs(cos_angle - (-1.0)) < 1e-10
def test_drag_acceleration_magnitude(self):
r = np.array([6778e3, 0.0, 0.0])
v = np.array([0.0, 7670.0, 0.0])
B = 82.0
a_drag = drag_acceleration(
r, v, constants.R_EARTH, constants.RHO0_EARTH, constants.H0_EARTH, B
)
mag = np.linalg.norm(a_drag)
assert mag > 0.0 and mag < 1e-5
def test_drag_increases_at_lower_altitude(self):
B = 50.0
r_300km = np.array([6678e3, 0.0, 0.0])
v = np.array([0.0, 7730.0, 0.0])
a_drag_300 = drag_acceleration(
r_300km, v, constants.R_EARTH, constants.RHO0_EARTH, constants.H0_EARTH, B
)
r_400km = np.array([6778e3, 0.0, 0.0])
v2 = np.array([0.0, 7670.0, 0.0])
a_drag_400 = drag_acceleration(
r_400km, v2, constants.R_EARTH, constants.RHO0_EARTH, constants.H0_EARTH, B
)
assert np.linalg.norm(a_drag_300) > np.linalg.norm(a_drag_400)
def test_drag_with_zero_velocity(self):
r = np.array([6778e3, 0.0, 0.0])
v = np.zeros(3)
B = 50.0
a_drag = drag_acceleration(
r, v, constants.R_EARTH, constants.RHO0_EARTH, constants.H0_EARTH, B
)
assert np.linalg.norm(a_drag) < 1e-20
def test_ballistic_coefficient_effect(self):
r = np.array([6778e3, 0.0, 0.0])
v = np.array([0.0, 7670.0, 0.0])
B_small = 20.0 B_large = 200.0
a_drag_small = drag_acceleration(
r, v, constants.R_EARTH, constants.RHO0_EARTH, constants.H0_EARTH, B_small
)
a_drag_large = drag_acceleration(
r, v, constants.R_EARTH, constants.RHO0_EARTH, constants.H0_EARTH, B_large
)
assert np.linalg.norm(a_drag_small) > np.linalg.norm(a_drag_large)
ratio = np.linalg.norm(a_drag_small) / np.linalg.norm(a_drag_large)
expected_ratio = B_large / B_small
assert abs(ratio - expected_ratio) < 1e-10
def test_invalid_vector_sizes(self):
r = np.array([6778e3, 0.0]) v = np.array([0.0, 7670.0, 0.0])
B = 50.0
with pytest.raises(ValueError):
drag_acceleration(r, v, constants.R_EARTH, constants.RHO0_EARTH, constants.H0_EARTH, B)
class TestDragPropagation:
def test_propagate_drag_rk4_basic(self):
r0 = np.array([6778e3, 0.0, 0.0])
v0 = np.array([0.0, 7670.0, 0.0])
B = 50.0
r1, v1 = propagate_drag_rk4(
r0,
v0,
600.0,
constants.GM_EARTH,
constants.R_EARTH,
constants.RHO0_EARTH,
constants.H0_EARTH,
B,
n_steps=100,
)
assert np.linalg.norm(r1) > 6000e3 assert np.linalg.norm(v1) > 1000.0
def test_propagate_drag_dopri5_basic(self):
r0 = np.array([6778e3, 0.0, 0.0])
v0 = np.array([0.0, 7670.0, 0.0])
B = 50.0
r1, v1 = propagate_drag_dopri5(
r0,
v0,
600.0,
constants.GM_EARTH,
constants.R_EARTH,
constants.RHO0_EARTH,
constants.H0_EARTH,
B,
tol=1e-8,
)
assert np.linalg.norm(r1) > 6000e3
assert np.linalg.norm(v1) > 1000.0
def test_drag_causes_orbit_decay(self):
r0 = np.array([6778e3, 0.0, 0.0])
v0 = np.array([0.0, 7670.0, 0.0])
B = 50.0
dt = 3600.0
r_twobody, v_twobody = propagate_state_keplerian(r0, v0, dt, constants.GM_EARTH)
r_drag, v_drag = propagate_drag_rk4(
r0,
v0,
dt,
constants.GM_EARTH,
constants.R_EARTH,
constants.RHO0_EARTH,
constants.H0_EARTH,
B,
n_steps=100,
)
e_twobody = np.linalg.norm(v_twobody) ** 2 / 2.0 - constants.GM_EARTH / np.linalg.norm(
r_twobody
)
e_drag = np.linalg.norm(v_drag) ** 2 / 2.0 - constants.GM_EARTH / np.linalg.norm(r_drag)
assert e_drag < e_twobody, "Drag should decrease orbital energy"
assert np.linalg.norm(r_drag) < np.linalg.norm(
r_twobody
), "Drag should cause orbit to decay"
class TestCombinedJ2Drag:
def test_propagate_j2_drag_rk4(self):
r0 = np.array([6778e3, 0.0, 1000e3]) v0 = np.array([100.0, 7670.0, 0.0])
B = 50.0
r1, v1 = propagate_j2_drag_rk4(
r0,
v0,
600.0,
constants.GM_EARTH,
constants.J2_EARTH,
constants.R_EARTH,
constants.RHO0_EARTH,
constants.H0_EARTH,
B,
n_steps=100,
)
assert np.linalg.norm(r1) > 6000e3
assert np.linalg.norm(v1) > 1000.0
def test_propagate_j2_drag_dopri5(self):
r0 = np.array([6778e3, 0.0, 0.0])
v0 = np.array([0.0, 7670.0, 0.0])
B = 50.0
r1, v1 = propagate_j2_drag_dopri5(
r0,
v0,
600.0,
constants.GM_EARTH,
constants.J2_EARTH,
constants.R_EARTH,
constants.RHO0_EARTH,
constants.H0_EARTH,
B,
tol=1e-8,
)
assert np.linalg.norm(r1) > 6000e3
assert np.linalg.norm(v1) > 1000.0
def test_invalid_vector_sizes_propagation(self):
r0 = np.array([6778e3, 0.0]) v0 = np.array([0.0, 7670.0, 0.0])
B = 50.0
with pytest.raises(ValueError):
propagate_drag_rk4(
r0,
v0,
600.0,
constants.GM_EARTH,
constants.R_EARTH,
constants.RHO0_EARTH,
constants.H0_EARTH,
B,
)
if __name__ == "__main__":
pytest.main([__file__, "-v"])