import math
import pytest
from astrora._core import (
bielliptic_transfer,
compare_bielliptic_hohmann,
constants,
find_optimal_bielliptic,
)
GM_EARTH = constants.GM_EARTH
R_MEAN_EARTH = constants.R_MEAN_EARTH
@pytest.fixture
def leo_radius():
return R_MEAN_EARTH + 400e3
@pytest.fixture
def geo_radius():
return R_MEAN_EARTH + 35_786e3
@pytest.fixture
def high_radius():
return (R_MEAN_EARTH + 400e3) * 20.0
class TestBiellipticTransferBasic:
def test_basic_calculation(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 3.0
result = bielliptic_transfer(leo_radius, geo_radius, r_intermediate, GM_EARTH)
assert "delta_v1" in result
assert "delta_v2" in result
assert "delta_v3" in result
assert "delta_v_total" in result
assert "transfer_time" in result
assert "transfer1_sma" in result
assert "transfer2_sma" in result
assert result["delta_v1"] > 0
assert result["delta_v2"] > 0
assert result["delta_v3"] > 0
assert result["delta_v_total"] > 0
assert (
abs(
result["delta_v_total"]
- (result["delta_v1"] + result["delta_v2"] + result["delta_v3"])
)
< 1e-6
)
def test_transfer_time_positive(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 5.0
result = bielliptic_transfer(leo_radius, geo_radius, r_intermediate, GM_EARTH)
assert result["transfer_time"] > 0
hohmann_time = 5.3 * 3600 assert result["transfer_time"] > hohmann_time
def test_transfer_orbit_properties(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 4.0
result = bielliptic_transfer(leo_radius, geo_radius, r_intermediate, GM_EARTH)
expected_sma1 = (leo_radius + r_intermediate) / 2.0
expected_sma2 = (geo_radius + r_intermediate) / 2.0
assert abs(result["transfer1_sma"] - expected_sma1) < 1.0
assert abs(result["transfer2_sma"] - expected_sma2) < 1.0
assert 0 < result["transfer1_eccentricity"] < 1
assert 0 < result["transfer2_eccentricity"] < 1
def test_circular_orbit_velocities(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 3.0
result = bielliptic_transfer(leo_radius, geo_radius, r_intermediate, GM_EARTH)
expected_v_initial = math.sqrt(GM_EARTH / leo_radius)
expected_v_final = math.sqrt(GM_EARTH / geo_radius)
assert abs(result["v_initial"] - expected_v_initial) < 1.0
assert abs(result["v_final"] - expected_v_final) < 1.0
class TestBiellipticVsHohmann:
def test_comparison_structure(self, leo_radius, high_radius):
r_intermediate = high_radius * 5.0
result = compare_bielliptic_hohmann(leo_radius, high_radius, r_intermediate, GM_EARTH)
assert "bielliptic" in result
assert "hohmann" in result
assert "dv_savings" in result
assert "time_penalty" in result
assert "delta_v_total" in result["bielliptic"]
assert "delta_v_total" in result["hohmann"]
def test_large_ratio_efficiency(self, leo_radius):
r_final = leo_radius * 25.0
r_intermediate = r_final * 5.0
result = compare_bielliptic_hohmann(leo_radius, r_final, r_intermediate, GM_EARTH)
assert result["dv_savings"] > 0
assert result["time_penalty"] > 1.0
def test_small_ratio_inefficiency(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 2.0
result = compare_bielliptic_hohmann(leo_radius, geo_radius, r_intermediate, GM_EARTH)
assert result["dv_savings"] < 0
def test_time_penalty_grows_with_intermediate(self, leo_radius, high_radius):
r_int_small = high_radius * 2.0
r_int_large = high_radius * 10.0
result_small = compare_bielliptic_hohmann(leo_radius, high_radius, r_int_small, GM_EARTH)
result_large = compare_bielliptic_hohmann(leo_radius, high_radius, r_int_large, GM_EARTH)
assert result_large["time_penalty"] > result_small["time_penalty"]
class TestOptimalIntermediate:
def test_finds_optimal(self, leo_radius, high_radius):
r_opt, result = find_optimal_bielliptic(leo_radius, high_radius, GM_EARTH, 50.0)
assert r_opt > max(leo_radius, high_radius)
assert "delta_v_total" in result
assert result["delta_v_total"] > 0
assert abs(result["r_intermediate"] - r_opt) < 1.0
def test_optimal_saves_deltaV(self, leo_radius):
r_final = leo_radius * 30.0
r_opt, bielliptic_result = find_optimal_bielliptic(leo_radius, r_final, GM_EARTH, 100.0)
comparison = compare_bielliptic_hohmann(leo_radius, r_final, r_opt, GM_EARTH)
assert comparison["dv_savings"] > 0
class TestParameterValidation:
def test_negative_radii_rejected(self):
with pytest.raises(Exception):
bielliptic_transfer(-1e6, 2e6, 3e6, GM_EARTH)
with pytest.raises(Exception):
bielliptic_transfer(1e6, -2e6, 3e6, GM_EARTH)
with pytest.raises(Exception):
bielliptic_transfer(1e6, 2e6, -3e6, GM_EARTH)
def test_equal_radii_rejected(self, leo_radius):
r_intermediate = leo_radius * 2.0
with pytest.raises(Exception):
bielliptic_transfer(leo_radius, leo_radius, r_intermediate, GM_EARTH)
def test_intermediate_too_small_rejected(self, leo_radius, geo_radius):
r_intermediate = (leo_radius + geo_radius) / 2.0
with pytest.raises(Exception):
bielliptic_transfer(leo_radius, geo_radius, r_intermediate, GM_EARTH)
def test_negative_mu_rejected(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 3.0
with pytest.raises(Exception):
bielliptic_transfer(leo_radius, geo_radius, r_intermediate, -GM_EARTH)
class TestPhysicalRealism:
def test_energy_relationships(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 3.0
result = bielliptic_transfer(leo_radius, geo_radius, r_intermediate, GM_EARTH)
epsilon_initial = -GM_EARTH / (2.0 * leo_radius)
epsilon_final = -GM_EARTH / (2.0 * geo_radius)
assert epsilon_final > epsilon_initial
assert result["delta_v_total"] > 0
def test_vis_viva_equation(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 3.0
result = bielliptic_transfer(leo_radius, geo_radius, r_intermediate, GM_EARTH)
v_initial_squared = result["v_initial"] ** 2
expected_v_initial_squared = GM_EARTH / leo_radius
assert abs(v_initial_squared - expected_v_initial_squared) < 1.0
v_final_squared = result["v_final"] ** 2
expected_v_final_squared = GM_EARTH / geo_radius
assert abs(v_final_squared - expected_v_final_squared) < 1.0
class TestNumericalValues:
def test_leo_to_geo_approximate_values(self, leo_radius, geo_radius):
r_intermediate = geo_radius * 3.0
result = bielliptic_transfer(leo_radius, geo_radius, r_intermediate, GM_EARTH)
assert 1000.0 < result["delta_v_total"] < 10000.0
one_day = 86400.0
one_week = 7 * one_day
assert one_day < result["transfer_time"] < one_week
def test_critical_radius_ratio(self):
r_initial = R_MEAN_EARTH + 400e3 r_final = r_initial * 16.0 r_intermediate = r_final * 5.0
result = compare_bielliptic_hohmann(r_initial, r_final, r_intermediate, GM_EARTH)
assert "dv_savings" in result
assert "time_penalty" in result
if __name__ == "__main__":
pytest.main([__file__, "-v"])