import math
import pytest
from astrora import _core
pure_plane_change = _core.pure_plane_change
combined_plane_change = _core.combined_plane_change
optimal_plane_change_location = _core.optimal_plane_change_location
GM_EARTH = _core.constants.GM_EARTH
R_MEAN_EARTH = _core.constants.R_MEAN_EARTH
class TestPurePlaneChange:
def test_basic_5_degree_change(self):
v = 7800.0 angle = math.radians(5.0)
result = pure_plane_change(v, angle)
assert "velocity" in result
assert "delta_angle" in result
assert "delta_v" in result
assert abs(result["delta_v"] - 680.0) < 10.0
assert result["velocity"] == v
assert result["delta_angle"] == angle
def test_45_degree_change(self):
v = 7800.0
angle = math.radians(45.0)
result = pure_plane_change(v, angle)
assert abs(result["delta_v"] - 5969.0) < 10.0
def test_180_degree_orbit_reversal(self):
v = 7800.0
angle = math.pi
result = pure_plane_change(v, angle)
assert abs(result["delta_v"] - 2.0 * v) < 0.1
def test_zero_plane_change(self):
v = 7800.0
angle = 0.0
result = pure_plane_change(v, angle)
assert abs(result["delta_v"]) < 1e-6
def test_small_angle_scaling(self):
v = 7800.0
angle1 = math.radians(1.0)
angle2 = math.radians(2.0)
result1 = pure_plane_change(v, angle1)
result2 = pure_plane_change(v, angle2)
ratio = result2["delta_v"] / result1["delta_v"]
assert abs(ratio - 2.0) < 0.01
def test_velocity_scaling(self):
angle = math.radians(10.0)
v1 = 5000.0
v2 = 10000.0
result1 = pure_plane_change(v1, angle)
result2 = pure_plane_change(v2, angle)
ratio = result2["delta_v"] / result1["delta_v"]
assert abs(ratio - 2.0) < 1e-6
def test_error_negative_velocity(self):
with pytest.raises(Exception): pure_plane_change(-100.0, 0.1)
def test_error_invalid_angle(self):
with pytest.raises(Exception): pure_plane_change(7800.0, 4.0)
def test_error_negative_angle(self):
with pytest.raises(Exception): pure_plane_change(7800.0, -0.1)
class TestCombinedPlaneChange:
def test_hohmann_plus_plane_change(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = 42164e3
v_leo = math.sqrt(GM_EARTH / r_leo)
a_transfer = (r_leo + r_geo) / 2.0
v_transfer_leo = math.sqrt((2.0 * GM_EARTH / r_leo) - (GM_EARTH / a_transfer))
angle = math.radians(28.5)
result = combined_plane_change(v_leo, v_transfer_leo, angle)
assert "v_initial" in result
assert "v_final" in result
assert "delta_angle" in result
assert "delta_v" in result
assert "delta_v_orbit_only" in result
assert "delta_v_plane_only" in result
assert "delta_v_penalty" in result
assert result["delta_v"] > result["delta_v_orbit_only"]
assert result["delta_v_penalty"] > 0.0
assert result["delta_v_penalty"] > 1000.0
def test_coplanar_reduces_to_simple_difference(self):
v1 = 7800.0
v2 = 10000.0
angle = 0.0
result = combined_plane_change(v1, v2, angle)
assert abs(result["delta_v"] - abs(v2 - v1)) < 0.1
assert abs(result["delta_v_penalty"]) < 1.0
def test_90_degree_perpendicular_planes(self):
v1 = 7800.0
v2 = 7800.0 angle = math.pi / 2.0
result = combined_plane_change(v1, v2, angle)
expected = v1 * math.sqrt(2.0)
assert abs(result["delta_v"] - expected) < 0.1
def test_penalty_calculation(self):
v1 = 7800.0
v2 = 8000.0
angle = math.radians(10.0)
result = combined_plane_change(v1, v2, angle)
expected_penalty = result["delta_v"] - result["delta_v_orbit_only"]
assert abs(result["delta_v_penalty"] - expected_penalty) < 1e-6
def test_symmetry_in_velocity(self):
v1 = 7800.0
v2 = 8000.0
angle = math.radians(15.0)
result1 = combined_plane_change(v1, v2, angle)
result2 = combined_plane_change(v2, v1, angle)
assert abs(result1["delta_v"] - result2["delta_v"]) < 1e-6
class TestOptimalPlaneChangeLocation:
def test_leo_to_geo_transfer(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = 42164e3
v_leo = math.sqrt(GM_EARTH / r_leo)
v_geo = math.sqrt(GM_EARTH / r_geo)
a_transfer = (r_leo + r_geo) / 2.0
v_transfer_leo = math.sqrt((2.0 * GM_EARTH / r_leo) - (GM_EARTH / a_transfer))
v_transfer_geo = math.sqrt((2.0 * GM_EARTH / r_geo) - (GM_EARTH / a_transfer))
angle = math.radians(28.5)
result = optimal_plane_change_location(v_leo, v_geo, v_transfer_leo, v_transfer_geo, angle)
assert "total_angle" in result
assert "angle_at_first" in result
assert "angle_at_second" in result
assert "v_first" in result
assert "v_second" in result
assert "delta_v_total" in result
assert "delta_v_first" in result
assert "delta_v_second" in result
assert "delta_v_saved" in result
assert "delta_v_saved_vs_low" in result
assert result["angle_at_second"] > result["angle_at_first"]
assert result["delta_v_saved"] > 0.0
assert result["delta_v_saved_vs_low"] > result["delta_v_saved"]
assert (
abs(result["delta_v_total"] - (result["delta_v_first"] + result["delta_v_second"]))
< 0.1
)
assert abs(result["angle_at_first"] + result["angle_at_second"] - angle) < 1e-6
def test_small_angle_split(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = 42164e3
v_leo = math.sqrt(GM_EARTH / r_leo)
v_geo = math.sqrt(GM_EARTH / r_geo)
a_transfer = (r_leo + r_geo) / 2.0
v_transfer_leo = math.sqrt((2.0 * GM_EARTH / r_leo) - (GM_EARTH / a_transfer))
v_transfer_geo = math.sqrt((2.0 * GM_EARTH / r_geo) - (GM_EARTH / a_transfer))
angle = math.radians(5.0)
result = optimal_plane_change_location(v_leo, v_geo, v_transfer_leo, v_transfer_geo, angle)
assert result["angle_at_first"] > 0.0
assert result["delta_v_saved"] >= 0.0
def test_zero_plane_change(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = 42164e3
v_leo = math.sqrt(GM_EARTH / r_leo)
v_geo = math.sqrt(GM_EARTH / r_geo)
a_transfer = (r_leo + r_geo) / 2.0
v_transfer_leo = math.sqrt((2.0 * GM_EARTH / r_leo) - (GM_EARTH / a_transfer))
v_transfer_geo = math.sqrt((2.0 * GM_EARTH / r_geo) - (GM_EARTH / a_transfer))
angle = 0.0
result = optimal_plane_change_location(v_leo, v_geo, v_transfer_leo, v_transfer_geo, angle)
assert abs(result["angle_at_first"]) < 1e-6
assert abs(result["angle_at_second"]) < 1e-6
assert abs(result["delta_v_saved"]) < 1e-6
def test_large_angle_all_at_high(self):
r_leo = R_MEAN_EARTH + 400e3
r_geo = 42164e3
v_leo = math.sqrt(GM_EARTH / r_leo)
v_geo = math.sqrt(GM_EARTH / r_geo)
a_transfer = (r_leo + r_geo) / 2.0
v_transfer_leo = math.sqrt((2.0 * GM_EARTH / r_leo) - (GM_EARTH / a_transfer))
v_transfer_geo = math.sqrt((2.0 * GM_EARTH / r_geo) - (GM_EARTH / a_transfer))
angle = math.radians(60.0)
result = optimal_plane_change_location(v_leo, v_geo, v_transfer_leo, v_transfer_geo, angle)
ratio_at_high = result["angle_at_second"] / angle
assert ratio_at_high > 0.9
class TestValidationAgainstLiterature:
def test_leo_geo_28_5_degrees_case_1(self):
r_leo = R_MEAN_EARTH + 300e3
r_geo = 42164e3
v_leo = math.sqrt(GM_EARTH / r_leo)
v_geo = math.sqrt(GM_EARTH / r_geo)
a_transfer = (r_leo + r_geo) / 2.0
v_transfer_leo = math.sqrt((2.0 * GM_EARTH / r_leo) - (GM_EARTH / a_transfer))
v_transfer_geo = math.sqrt((2.0 * GM_EARTH / r_geo) - (GM_EARTH / a_transfer))
angle = math.radians(28.6)
burn1 = combined_plane_change(v_leo, v_transfer_leo, angle)
burn2 = combined_plane_change(v_transfer_geo, v_geo, 0.0)
total_dv = burn1["delta_v"] + burn2["delta_v"]
assert abs(total_dv - 6469.0) < 100.0
def test_leo_geo_28_5_degrees_case_2(self):
r_leo = R_MEAN_EARTH + 300e3
r_geo = 42164e3
v_leo = math.sqrt(GM_EARTH / r_leo)
v_geo = math.sqrt(GM_EARTH / r_geo)
a_transfer = (r_leo + r_geo) / 2.0
v_transfer_leo = math.sqrt((2.0 * GM_EARTH / r_leo) - (GM_EARTH / a_transfer))
v_transfer_geo = math.sqrt((2.0 * GM_EARTH / r_geo) - (GM_EARTH / a_transfer))
angle = math.radians(28.6)
burn1 = combined_plane_change(v_leo, v_transfer_leo, 0.0)
burn2 = combined_plane_change(v_transfer_geo, v_geo, angle)
total_dv = burn1["delta_v"] + burn2["delta_v"]
assert abs(total_dv - 4258.0) < 100.0
def test_optimal_split_leo_geo_28_5(self):
r_leo = R_MEAN_EARTH + 300e3
r_geo = 42164e3
v_leo = math.sqrt(GM_EARTH / r_leo)
v_geo = math.sqrt(GM_EARTH / r_geo)
a_transfer = (r_leo + r_geo) / 2.0
v_transfer_leo = math.sqrt((2.0 * GM_EARTH / r_leo) - (GM_EARTH / a_transfer))
v_transfer_geo = math.sqrt((2.0 * GM_EARTH / r_geo) - (GM_EARTH / a_transfer))
angle = math.radians(28.6)
result = optimal_plane_change_location(v_leo, v_geo, v_transfer_leo, v_transfer_geo, angle)
assert abs(result["delta_v_total"] - 4233.0) < 100.0
assert result["delta_v_saved"] > 0.0
assert result["delta_v_saved"] < 50.0
if __name__ == "__main__":
pytest.main([__file__, "-v"])