import math
import pytest
from astrora._core import (
constants,
coorbital_rendezvous,
coplanar_rendezvous,
phasing_orbit,
)
GM_EARTH = constants.GM_EARTH
R_MEAN_EARTH = constants.R_MEAN_EARTH
class TestPhasingOrbit:
def test_phasing_orbit_catch_up(self):
r = R_MEAN_EARTH + 400e3 phase_change = math.radians(30) num_orbits = 5.0
result = phasing_orbit(r, phase_change, num_orbits, GM_EARTH)
assert "delta_v_total" in result
assert "phasing_time" in result
assert "a_phasing" in result
assert "period_phasing" in result
assert result["delta_v_total"] > 0
assert result["phasing_time"] > 0
assert pytest.approx(result["total_phase_change"]) == phase_change
assert pytest.approx(result["phase_change_per_orbit"]) == phase_change / num_orbits
assert result["a_phasing"] < r
assert result["period_phasing"] < result["period_original"]
def test_phasing_orbit_wait(self):
r = R_MEAN_EARTH + 400e3
phase_change = -math.radians(30) num_orbits = 5.0
result = phasing_orbit(r, phase_change, num_orbits, GM_EARTH)
assert result["a_phasing"] > r
assert result["period_phasing"] > result["period_original"]
assert pytest.approx(result["total_phase_change"]) == phase_change
def test_phasing_orbit_small_change(self):
r = R_MEAN_EARTH + 400e3
phase_change = math.radians(10) num_orbits = 10.0
result = phasing_orbit(r, phase_change, num_orbits, GM_EARTH)
assert result["delta_v_total"] > 0
phase_per_orbit = result["phase_change_per_orbit"]
assert abs(phase_per_orbit) < math.radians(5)
def test_phasing_orbit_symmetric_maneuver(self):
r = R_MEAN_EARTH + 400e3
phase_change = math.radians(20)
num_orbits = 5.0
result = phasing_orbit(r, phase_change, num_orbits, GM_EARTH)
assert pytest.approx(result["delta_v_enter"]) == result["delta_v_exit"]
assert pytest.approx(result["delta_v_total"]) == 2 * result["delta_v_enter"]
def test_phasing_orbit_invalid_inputs(self):
r = R_MEAN_EARTH + 400e3
with pytest.raises(Exception): phasing_orbit(r, 0.0, 2.0, GM_EARTH)
with pytest.raises(Exception):
phasing_orbit(r, math.radians(30), 0.5, GM_EARTH)
with pytest.raises(Exception):
phasing_orbit(-r, math.radians(30), 2.0, GM_EARTH)
class TestCoorbitalRendezvous:
def test_coorbital_rendezvous_basic(self):
r = R_MEAN_EARTH + 400e3
phase_diff = math.radians(45) num_orbits = 10.0
result = coorbital_rendezvous(r, phase_diff, num_orbits, GM_EARTH)
assert "r_orbit" in result
assert "initial_phase_difference" in result
assert "phasing" in result
assert isinstance(result["phasing"], dict)
assert pytest.approx(result["r_orbit"]) == r
assert pytest.approx(result["initial_phase_difference"]) == phase_diff
phasing = result["phasing"]
assert phasing["delta_v_total"] > 0
assert pytest.approx(phasing["total_phase_change"]) == phase_diff
def test_coorbital_rendezvous_90_degrees(self):
r = R_MEAN_EARTH + 400e3
phase_diff = math.radians(90)
num_orbits = 15.0
result = coorbital_rendezvous(r, phase_diff, num_orbits, GM_EARTH)
assert pytest.approx(result["initial_phase_difference"]) == math.radians(90)
assert result["phasing"]["delta_v_total"] > 0
def test_coorbital_rendezvous_phase_normalization(self):
r = R_MEAN_EARTH + 400e3
phase_diff = math.radians(400) num_orbits = 10.0
result = coorbital_rendezvous(r, phase_diff, num_orbits, GM_EARTH)
assert result["initial_phase_difference"] >= 0
assert result["initial_phase_difference"] < 2 * math.pi
class TestCoplanarRendezvous:
def test_coplanar_rendezvous_ascending(self):
r_leo = R_MEAN_EARTH + 300e3 r_iss = R_MEAN_EARTH + 400e3 current_phase = math.radians(45)
result = coplanar_rendezvous(r_leo, r_iss, current_phase, GM_EARTH)
assert "r_chaser" in result
assert "r_target" in result
assert "transfer_time" in result
assert "wait_time" in result
assert "delta_v_total" in result
assert "required_phase_angle" in result
assert "lead_angle" in result
assert pytest.approx(result["r_chaser"]) == r_leo
assert pytest.approx(result["r_target"]) == r_iss
assert result["transfer_time"] > 0
assert result["delta_v_total"] > 0
assert result["wait_time"] >= 0
assert result["a_transfer"] > r_leo
assert result["a_transfer"] < r_iss
def test_coplanar_rendezvous_descending(self):
r_high = R_MEAN_EARTH + 500e3
r_low = R_MEAN_EARTH + 350e3
current_phase = math.radians(60)
result = coplanar_rendezvous(r_high, r_low, current_phase, GM_EARTH)
assert result["delta_v_total"] > 0
assert result["a_transfer"] > r_low
assert result["a_transfer"] < r_high
def test_coplanar_rendezvous_lead_angle(self):
r_chaser = R_MEAN_EARTH + 300e3
r_target = R_MEAN_EARTH + 400e3
current_phase = 0.0
result = coplanar_rendezvous(r_chaser, r_target, current_phase, GM_EARTH)
assert result["lead_angle"] > 0
assert result["lead_angle"] < 2 * math.pi
assert "required_phase_angle" in result
def test_coplanar_rendezvous_wait_orbits(self):
r_chaser = R_MEAN_EARTH + 300e3
r_target = R_MEAN_EARTH + 400e3
current_phase = math.radians(180)
result = coplanar_rendezvous(r_chaser, r_target, current_phase, GM_EARTH)
assert result["wait_orbits"] >= 0
assert result["wait_time"] >= 0
def test_coplanar_rendezvous_invalid_same_orbit(self):
r = R_MEAN_EARTH + 400e3
with pytest.raises(Exception):
coplanar_rendezvous(r, r, math.radians(45), GM_EARTH)
class TestRendezvousPhysics:
def test_energy_conservation_phasing(self):
r = R_MEAN_EARTH + 400e3
phase_change = math.radians(20)
num_orbits = 5.0
result = phasing_orbit(r, phase_change, num_orbits, GM_EARTH)
epsilon_original = -GM_EARTH / (2 * r)
epsilon_phasing = -GM_EARTH / (2 * result["a_phasing"])
assert epsilon_original != epsilon_phasing
v_original_calc = math.sqrt(GM_EARTH / r)
assert pytest.approx(result["v_original"], rel=1e-6) == v_original_calc
def test_delta_v_positive(self):
r = R_MEAN_EARTH + 400e3
phase_change = math.radians(30)
num_orbits = 5.0
result = phasing_orbit(r, phase_change, num_orbits, GM_EARTH)
assert result["delta_v_enter"] > 0
assert result["delta_v_exit"] > 0
assert result["delta_v_total"] > 0
assert result["delta_v_total"] >= result["delta_v_enter"]
assert result["delta_v_total"] >= result["delta_v_exit"]
def test_eccentricity_bounds(self):
r = R_MEAN_EARTH + 400e3
phase_change = math.radians(25)
num_orbits = 8.0
result = phasing_orbit(r, phase_change, num_orbits, GM_EARTH)
assert result["e_phasing"] > 0
assert result["e_phasing"] < 1
class TestRendezvousRealism:
def test_iss_resupply_scenario(self):
r_cargo = R_MEAN_EARTH + 380e3 r_iss = R_MEAN_EARTH + 408e3
current_phase = math.radians(30)
result = coplanar_rendezvous(r_cargo, r_iss, current_phase, GM_EARTH)
transfer_hours = result["transfer_time"] / 3600
assert transfer_hours < 10
assert result["delta_v_total"] < 200 assert result["delta_v_total"] > 10
def test_geostationary_phasing(self):
r_geo = R_MEAN_EARTH + 35_786e3
phase_change = math.radians(10)
num_orbits = 7.0
result = phasing_orbit(r_geo, phase_change, num_orbits, GM_EARTH)
assert result["delta_v_total"] < 100
phasing_days = result["phasing_time"] / 86400
assert pytest.approx(phasing_days, rel=0.1) == 7.0
if __name__ == "__main__":
pytest.main([__file__, "-v"])