import math
import numpy as np
import pytest
from astrora import _core
hohmann_transfer = _core.hohmann_transfer
hohmann_phase_angle = _core.hohmann_phase_angle
hohmann_synodic_period = _core.hohmann_synodic_period
hohmann_time_to_window = _core.hohmann_time_to_window
GM_EARTH = _core.constants.GM_EARTH
R_MEAN_EARTH = _core.constants.R_MEAN_EARTH
GM_SUN = _core.constants.GM_SUN
AU = _core.constants.AU
class TestHohmannTransferBasic:
def test_leo_to_geo_transfer(self):
r_leo = R_MEAN_EARTH + 400e3 r_geo = R_MEAN_EARTH + 35_786e3
result = hohmann_transfer(r_leo, r_geo, GM_EARTH)
assert "r_initial" in result
assert "r_final" in result
assert "mu" in result
assert "delta_v1" in result
assert "delta_v2" in result
assert "delta_v_total" in result
assert "transfer_time" in result
assert "transfer_sma" in result
assert "transfer_eccentricity" in result
assert result["delta_v1"] == pytest.approx(2427.0, abs=50.0)
assert result["delta_v2"] == pytest.approx(1469.0, abs=50.0)
assert result["delta_v_total"] == pytest.approx(3896.0, abs=100.0)
transfer_time_hours = result["transfer_time"] / 3600.0
assert transfer_time_hours == pytest.approx(5.25, abs=0.1)
assert result["delta_v_total"] == pytest.approx(
result["delta_v1"] + result["delta_v2"], abs=1e-10
)
def test_geo_to_leo_transfer(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = R_MEAN_EARTH + 35_786e3
result_ascending = hohmann_transfer(r_leo, r_geo, GM_EARTH)
result_descending = hohmann_transfer(r_geo, r_leo, GM_EARTH)
assert result_ascending["delta_v_total"] == pytest.approx(
result_descending["delta_v_total"], abs=1e-10
)
assert result_ascending["transfer_time"] == pytest.approx(
result_descending["transfer_time"], abs=1e-10
)
def test_small_altitude_change(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 500e3
result = hohmann_transfer(r1, r2, GM_EARTH)
assert result["delta_v_total"] < 100.0
assert result["transfer_time"] < 3600.0
def test_earth_mars_transfer(self):
r_earth = 1.0 * AU
r_mars = 1.524 * AU
result = hohmann_transfer(r_earth, r_mars, GM_SUN)
transfer_days = result["transfer_time"] / 86400.0
assert transfer_days == pytest.approx(259.0, abs=10.0)
delta_v_km_s = result["delta_v_total"] / 1000.0
assert delta_v_km_s == pytest.approx(5.7, rel=0.5)
class TestHohmannTransferProperties:
def test_transfer_orbit_semi_major_axis(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 10_000e3
result = hohmann_transfer(r1, r2, GM_EARTH)
expected_sma = (r1 + r2) / 2.0
assert result["transfer_sma"] == pytest.approx(expected_sma, rel=1e-10)
def test_transfer_orbit_eccentricity(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 10_000e3
result = hohmann_transfer(r1, r2, GM_EARTH)
assert result["transfer_eccentricity"] >= 0.0
assert result["transfer_eccentricity"] < 1.0
def test_energy_conservation(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 10_000e3
result = hohmann_transfer(r1, r2, GM_EARTH)
e_peri = 0.5 * result["v_transfer_periapsis"] ** 2 - GM_EARTH / r1
e_apo = 0.5 * result["v_transfer_apoapsis"] ** 2 - GM_EARTH / r2
assert e_peri == pytest.approx(e_apo, rel=1e-3)
e_expected = -GM_EARTH / (2.0 * result["transfer_sma"])
assert e_peri == pytest.approx(e_expected, rel=1e-3)
def test_circular_orbit_velocities(self):
r_leo = R_MEAN_EARTH + 400e3
result = hohmann_transfer(r_leo, r_leo * 2, GM_EARTH)
expected_v_initial = math.sqrt(GM_EARTH / r_leo)
assert result["v_initial"] == pytest.approx(expected_v_initial, rel=1e-10)
class TestHohmannErrorHandling:
def test_negative_initial_radius(self):
with pytest.raises(Exception): hohmann_transfer(-1.0, 1e7, GM_EARTH)
def test_negative_final_radius(self):
with pytest.raises(Exception):
hohmann_transfer(1e7, -1.0, GM_EARTH)
def test_zero_radius(self):
with pytest.raises(Exception):
hohmann_transfer(0.0, 1e7, GM_EARTH)
def test_equal_radii(self):
with pytest.raises(Exception):
hohmann_transfer(1e7, 1e7, GM_EARTH)
def test_negative_mu(self):
with pytest.raises(Exception):
hohmann_transfer(7e6, 1e7, -1.0)
class TestPhaseAngle:
def test_phase_angle_leo_to_geo(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = R_MEAN_EARTH + 35_786e3
phase = hohmann_phase_angle(r_leo, r_geo, GM_EARTH)
assert phase >= 0.0
assert phase < 2.0 * math.pi
assert phase == pytest.approx(1.75, abs=0.01)
def test_phase_angle_consistency(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 800e3
phase = hohmann_phase_angle(r1, r2, GM_EARTH)
transfer_result = hohmann_transfer(r1, r2, GM_EARTH)
n_final = math.sqrt(GM_EARTH / r2**3) theta_target = n_final * transfer_result["transfer_time"]
expected_phase = (math.pi - theta_target) % (2 * math.pi)
assert phase == pytest.approx(expected_phase, abs=1e-6)
class TestSynodicPeriod:
def test_synodic_period_basic(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 800e3
synodic = hohmann_synodic_period(r1, r2, GM_EARTH)
assert synodic > 0.0
t1 = 2 * math.pi * math.sqrt(r1**3 / GM_EARTH)
t2 = 2 * math.pi * math.sqrt(r2**3 / GM_EARTH)
assert synodic > t1
assert synodic > t2
def test_synodic_period_formula(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 1000e3
synodic = hohmann_synodic_period(r1, r2, GM_EARTH)
t1 = 2 * math.pi * math.sqrt(r1**3 / GM_EARTH)
t2 = 2 * math.pi * math.sqrt(r2**3 / GM_EARTH)
expected_synodic = 1.0 / abs(1.0 / t1 - 1.0 / t2)
assert synodic == pytest.approx(expected_synodic, rel=1e-10)
def test_synodic_period_error_handling(self):
with pytest.raises(Exception):
hohmann_synodic_period(-1.0, 1e7, GM_EARTH)
with pytest.raises(Exception):
hohmann_synodic_period(1e7, -1.0, GM_EARTH)
class TestTransferWindow:
def test_time_to_window_aligned(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = R_MEAN_EARTH + 35_786e3
optimal_phase = hohmann_phase_angle(r_leo, r_geo, GM_EARTH)
wait_time = hohmann_time_to_window(optimal_phase, r_leo, r_geo, GM_EARTH)
assert wait_time == pytest.approx(0.0, abs=1.0) or wait_time > 1000.0
def test_time_to_window_opposite(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = R_MEAN_EARTH + 35_786e3
wait_time = hohmann_time_to_window(0.0, r_leo, r_geo, GM_EARTH)
assert wait_time >= 0.0
synodic = hohmann_synodic_period(r_leo, r_geo, GM_EARTH)
assert wait_time <= synodic
def test_time_to_window_range(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 800e3
synodic = hohmann_synodic_period(r1, r2, GM_EARTH)
for current_phase in np.linspace(0, 2 * math.pi, 10):
wait_time = hohmann_time_to_window(current_phase, r1, r2, GM_EARTH)
assert wait_time >= 0.0
assert wait_time <= synodic
class TestComprehensiveScenarios:
def test_geostationary_transfer_orbit(self):
r_gto_perigee = R_MEAN_EARTH + 185e3
r_geo = R_MEAN_EARTH + 35_786e3
result = hohmann_transfer(r_gto_perigee, r_geo, GM_EARTH)
assert result["delta_v2"] == pytest.approx(1500.0, abs=100.0)
def test_multiple_transfers_same_body(self):
altitudes = [400e3, 800e3, 1200e3, 35_786e3]
for i in range(len(altitudes) - 1):
r1 = R_MEAN_EARTH + altitudes[i]
r2 = R_MEAN_EARTH + altitudes[i + 1]
result = hohmann_transfer(r1, r2, GM_EARTH)
assert result["delta_v_total"] > 0.0
assert result["transfer_time"] > 0.0
def test_numerical_stability(self):
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 401e3
result_small = hohmann_transfer(r1, r2, GM_EARTH)
assert result_small["delta_v_total"] > 0.0
r1 = R_MEAN_EARTH + 400e3
r2 = R_MEAN_EARTH + 100_000e3
result_large = hohmann_transfer(r1, r2, GM_EARTH)
assert result_large["delta_v_total"] > 0.0
result_sun = hohmann_transfer(1.0 * AU, 1.5 * AU, GM_SUN)
assert result_sun["delta_v_total"] > 0.0
if __name__ == "__main__":
pytest.main([__file__, "-v"])