import numpy as np
import pytest
from astrora import _core as core
class TestSTMTwoBody:
def test_stm_import(self):
assert hasattr(core, "propagate_stm_rk4")
assert hasattr(core, "propagate_stm_dopri5")
assert hasattr(core, "propagate_stm_j2_rk4")
def test_stm_basic_propagation(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0 mu = core.constants.GM_EARTH
r, v, stm = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=100)
assert np.linalg.norm(r) > 6000e3
assert np.linalg.norm(r) < 8000e3
assert np.linalg.norm(v) > 1000.0
assert np.linalg.norm(v) < 10000.0
assert stm.shape == (6, 6)
assert not np.allclose(stm, np.eye(6), rtol=0.01)
def test_stm_det_conservation(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
mu = core.constants.GM_EARTH
r, v, stm = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=100)
det = np.linalg.det(stm)
assert np.abs(det) > 0.99 and np.abs(det) < 1.01
def test_stm_linearity(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
mu = core.constants.GM_EARTH
r_nom, v_nom, stm = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=100)
dr0 = 1000.0
r0_pert = r0 + np.array([dr0, 0.0, 0.0])
r_pert, v_pert, _ = core.propagate_stm_rk4(r0_pert, v0, dt, mu, n_steps=100)
dr_actual = r_pert - r_nom
dv_actual = v_pert - v_nom
delta_x0 = np.array([dr0, 0.0, 0.0, 0.0, 0.0, 0.0])
delta_x = stm @ delta_x0
pos_error = np.abs(dr_actual[0] - delta_x[0])
vel_error = np.abs(dv_actual[0] - delta_x[3])
assert pos_error < dr0 * 0.01 assert vel_error < 1.0
def test_stm_dopri5_vs_rk4(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
mu = core.constants.GM_EARTH
r_rk4, v_rk4, stm_rk4 = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=100)
r_dopri5, v_dopri5, stm_dopri5 = core.propagate_stm_dopri5(r0, v0, dt, mu, tol=1e-10)
assert np.linalg.norm(r_rk4 - r_dopri5) < 100.0 assert np.linalg.norm(v_rk4 - v_dopri5) < 0.1
assert np.linalg.norm(stm_rk4 - stm_dopri5) < 0.01
def test_stm_zero_time(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 1e-10 mu = core.constants.GM_EARTH
r, v, stm = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=1)
assert np.allclose(stm, np.eye(6), atol=1e-6)
def test_stm_backward_propagation(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
mu = core.constants.GM_EARTH
r_fwd, v_fwd, stm_fwd = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=100)
r_back, v_back, stm_back = core.propagate_stm_rk4(r_fwd, v_fwd, -dt, mu, n_steps=100)
assert np.linalg.norm(r_back - r0) < 1000.0 assert np.linalg.norm(v_back - v0) < 1.0
stm_product = stm_back @ stm_fwd
assert np.allclose(stm_product, np.eye(6), atol=0.01)
class TestSTMJ2:
def test_stm_j2_basic(self):
r0 = np.array([7000e3, 0.0, 1000e3]) v0 = np.array([0.0, 7546.0, 100.0])
dt = 600.0
mu = core.constants.GM_EARTH
j2 = core.constants.J2_EARTH
R = core.constants.R_EARTH
r, v, stm = core.propagate_stm_j2_rk4(r0, v0, dt, mu, j2, R, n_steps=100)
assert np.linalg.norm(r) > 6000e3
assert np.linalg.norm(v) > 1000.0
assert stm.shape == (6, 6)
det = np.linalg.det(stm)
assert np.abs(det) > 0.9 and np.abs(det) < 1.1
def test_stm_j2_vs_two_body(self):
r0 = np.array([7000e3, 0.0, 1000e3])
v0 = np.array([0.0, 7546.0, 100.0])
dt = 3600.0 mu = core.constants.GM_EARTH
j2 = core.constants.J2_EARTH
R = core.constants.R_EARTH
r_tb, v_tb, stm_tb = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=100)
r_j2, v_j2, stm_j2 = core.propagate_stm_j2_rk4(r0, v0, dt, mu, j2, R, n_steps=100)
assert np.linalg.norm(r_tb - r_j2) > 10.0
stm_diff = np.linalg.norm(stm_tb - stm_j2)
assert stm_diff > 0.001
class TestSTMFiniteDifferences:
def test_stm_vs_finite_differences(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 300.0 mu = core.constants.GM_EARTH
r_nom, v_nom, stm = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=100)
eps = 1.0 fd_matrix = np.zeros((6, 6))
for i in range(6):
delta = np.zeros(6)
delta[i] = eps
r0_pert = r0 + delta[0:3]
v0_pert = v0 + delta[3:6]
r_pert, v_pert, _ = core.propagate_stm_rk4(r0_pert, v0_pert, dt, mu, n_steps=100)
dr = r_pert - r_nom
dv = v_pert - v_nom
fd_matrix[:, i] = np.concatenate([dr, dv]) / eps
rel_error = np.linalg.norm(stm - fd_matrix) / np.linalg.norm(stm)
assert rel_error < 0.05
class TestSTMEdgeCases:
def test_stm_invalid_input_shape(self):
r0 = np.array([7000e3, 0.0]) v0 = np.array([0.0, 7546.0, 0.0])
with pytest.raises(ValueError):
core.propagate_stm_rk4(r0, v0, 600.0, core.constants.GM_EARTH)
def test_stm_large_time_step(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 5400.0 mu = core.constants.GM_EARTH
r, v, stm = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=200)
assert np.linalg.norm(r) > 6000e3
assert stm.shape == (6, 6)
det = np.linalg.det(stm)
assert np.abs(det) > 0.5 and np.abs(det) < 2.0
def test_stm_high_eccentricity(self):
r0 = np.array([6678e3, 0.0, 0.0]) v0 = np.array([0.0, 10300.0, 0.0]) dt = 600.0
mu = core.constants.GM_EARTH
r, v, stm = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=200)
assert np.linalg.norm(r) > 6000e3
assert stm.shape == (6, 6)
def test_stm_consistency_across_methods(self):
r0 = np.array([7000e3, 0.0, 0.0])
v0 = np.array([0.0, 7546.0, 0.0])
dt = 600.0
mu = core.constants.GM_EARTH
_, _, stm_rk4_50 = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=50)
_, _, stm_rk4_100 = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=100)
_, _, stm_rk4_200 = core.propagate_stm_rk4(r0, v0, dt, mu, n_steps=200)
diff_100_200 = np.linalg.norm(stm_rk4_100 - stm_rk4_200)
diff_50_100 = np.linalg.norm(stm_rk4_50 - stm_rk4_100)
assert diff_100_200 < diff_50_100
if __name__ == "__main__":
pytest.main([__file__, "-v"])