from typing import Any, Dict
import numpy as np
import pytest
REFERENCE_EPOCHS = {
"earth_perihelion_2020": "2020-01-05 07:47:00", "hyperbolic_epoch": "2015-07-14 07:59", }
CELESTIAL_BODIES = {
"earth": {
"gm": 3.986004418e14, "radius": 6_371_000.0, "angular_velocity": 7.292114e-5, "j2": 1.08262668e-3, "name": "Earth",
},
"jupiter": {
"gm": 1.26712763e17, "radius": 71_492_000.0, "name": "Jupiter",
},
"sun": {
"gm": 1.32712440018e20, "radius": 695_700_000.0, "name": "Sun",
},
"mars": {
"gm": 4.282837e13, "radius": 3_389_500.0, "name": "Mars",
},
}
VALLADO_EXAMPLE_2_4 = {
"description": "Vallado Example 2.4: Orbit propagation",
"source": "Vallado 4th Ed., Example 2.4",
"attractor": "earth",
"initial_state": {
"r": np.array([1131.340e3, -2282.343e3, 6672.423e3]), "v": np.array([-5.64305e3, 4.30333e3, 2.42879e3]), "description": "Initial position and velocity in ECI frame",
},
"time_of_flight": 40.0 * 60.0, "expected_final_state": {
"r": np.array([-4219.7527e3, 4363.0292e3, -3958.7666e3]), "v": np.array([3.689866e3, -1.916735e3, -6.112511e3]), "description": "Expected final state after 40 minutes",
},
"tolerance": {
"position": 1.0, "velocity": 1e-3, },
}
CURTIS_EXAMPLE_4_3 = {
"description": "Curtis Example 4.3: State vectors to orbital elements",
"source": "Curtis 3rd Ed., Example 4.3, p. 200",
"attractor": "earth",
"state": {
"r": np.array([-6045.0e3, -3490.0e3, 2500.0e3]), "v": np.array([-3.457e3, 6.618e3, 2.533e3]), },
"expected_elements": {
"e": 0.1712, "i": 153.25, "raan": 255.28, "argp": 20.07, "nu": 28.45, "p": 8530.47e3, },
"tolerance": {
"e": 0.001,
"angles_deg": 0.1, "p": 100.0, },
}
CURTIS_EXAMPLE_3_5 = {
"description": "Curtis Example 3.5: Hyperbolic orbit propagation",
"source": "Curtis 3rd Ed., Example 3.5",
"attractor": "earth",
"initial_state": {
"r": np.array([6_671_000.0, 0.0, 0.0]), "v": np.array([0.0, 15.0e3, 0.0]), },
"time_of_flight": 14941.0, "expected_final_state": {
"r_magnitude": 163_180e3, "v_magnitude": 10.51e3, },
"tolerance": {
"position": 1000.0, "velocity": 10.0, },
}
CURTIS_PROBLEM_3_15 = {
"description": "Curtis Problem 3.15: Parabolic orbit",
"source": "Curtis 3rd Ed., Problem 3.15",
"attractor": "earth",
"semi_latus_rectum": 13_200e3, "eccentricity": 1.0, "test_cases": [
{
"time": 0.44485 * 3600.0, "expected_nu_deg": 90.0, },
{
"time": 36.0 * 3600.0, "expected_r": 304_700e3, },
],
}
VALLADO75_LAMBERT = {
"description": "Vallado test case 75: Lambert problem",
"source": "Vallado reference implementation, test 75",
"attractor": "earth",
"r1": np.array([15945.34e3, 0.0, 0.0]), "r2": np.array([12214.83399e3, 10249.46731e3, 0.0]), "time_of_flight": 76.0 * 60.0, "expected_velocities": {
"v1": np.array([2.058925e3, 2.915956e3, 0.0]), "v2": np.array([-3.451569e3, 0.910301e3, 0.0]), },
"tolerance": {
"velocity": 1.0, },
}
CURTIS52_LAMBERT = {
"description": "Curtis test case 52: Lambert problem",
"source": "Curtis reference implementation, test 52",
"attractor": "earth",
"r1": np.array([5000.0e3, 10000.0e3, 2100.0e3]), "r2": np.array([-14600.0e3, 2500.0e3, 7000.0e3]), "time_of_flight": 1.0 * 3600.0, "expected_velocities": {
"v1": np.array([-5.9925e3, 1.9254e3, 3.2456e3]), "v2": np.array([-3.3125e3, -4.1966e3, -0.38529e3]), },
"tolerance": {
"velocity": 1.0, },
}
CURTIS53_LAMBERT = {
"description": "Curtis test case 53: Lambert problem (with errata)",
"source": "Curtis reference implementation, test 53",
"attractor": "earth",
"r1": np.array([273378.0e3, 0.0, 0.0]), "r2": np.array([145820.0e3, 12758.0e3, 0.0]), "time_of_flight": 13.5 * 3600.0, "expected_velocities": {
"v1": np.array([-2.4356e3, 0.26741e3, 0.0]), },
"tolerance": {
"velocity": 1.0, },
"notes": "Errata notes positive j-component in v1",
}
HOHMANN_LEO_TO_GEO = {
"description": "Hohmann transfer from LEO to GEO",
"source": "poliastro test suite, standard reference",
"attractor": "earth",
"initial_altitude": 191.34411e3, "final_altitude": 35_781.34857e3, "expected_results": {
"delta_v_total": 3.935224e3, "transfer_time": 5.256713 * 3600.0, "final_eccentricity": 0.0, },
"tolerance": {
"delta_v": 10.0, "time": 60.0, "eccentricity": 1e-12,
},
}
BIELLIPTIC_TRANSFER = {
"description": "Bielliptic transfer maneuver",
"source": "poliastro test suite",
"attractor": "earth",
"initial_altitude": 191.34411e3, "intermediate_altitude": 503_873.0e3, "final_altitude": 376_310.0e3, "expected_results": {
"delta_v_total": 3.904057e3, "transfer_time": 593.919803 * 3600.0, "final_eccentricity": 0.0, },
"tolerance": {
"delta_v": 10.0, "time": 100.0, "eccentricity": 1e-12,
},
}
CIRCULAR_VELOCITY_TEST = {
"description": "Circular velocity calculation",
"source": "poliastro test suite, basic validation",
"gm": 398600e9, "semi_major_axis": 7000e3, "expected_velocity": 7546.0491, "tolerance": 0.001, "formula": "V = sqrt(GM/a)",
}
NEW_HORIZONS_HYPERBOLIC = {
"description": "New Horizons hyperbolic departure",
"source": "poliastro conftest.py, New Horizons mission data",
"epoch": "2015-07-14 07:59", "attractor": "sun",
"state": {
"r": np.array([1.197659243752796e9, -4.443716685978071e9, -1.747610548576734e9]), "v": np.array([5.540549267188614e3, -12.51544669134140e3, -4.848892572767733e3]), },
"orbit_type": "hyperbolic",
"notes": "Sun-centered state at Pluto flyby approach",
}
NEAR_PARABOLIC_ECCENTRICITIES = {
"description": "Near-parabolic orbit test eccentricities",
"source": "poliastro test_propagation.py",
"elliptic": [0.9, 0.99, 0.999, 0.9999, 0.99999],
"hyperbolic": [1.0001, 1.001, 1.01, 1.1],
"notes": "Used for numerical stability validation against Cowell propagator",
}
@pytest.fixture(scope="session")
def all_reference_data() -> Dict[str, Any]:
return {
"epochs": REFERENCE_EPOCHS,
"bodies": CELESTIAL_BODIES,
"propagation": {
"vallado_2_4": VALLADO_EXAMPLE_2_4,
"curtis_3_5": CURTIS_EXAMPLE_3_5,
"curtis_3_15": CURTIS_PROBLEM_3_15,
},
"state_conversion": {
"curtis_4_3": CURTIS_EXAMPLE_4_3,
},
"lambert": {
"vallado75": VALLADO75_LAMBERT,
"curtis52": CURTIS52_LAMBERT,
"curtis53": CURTIS53_LAMBERT,
},
"maneuvers": {
"hohmann_leo_geo": HOHMANN_LEO_TO_GEO,
"bielliptic": BIELLIPTIC_TRANSFER,
},
"basic": {
"circular_velocity": CIRCULAR_VELOCITY_TEST,
},
"mission_data": {
"new_horizons": NEW_HORIZONS_HYPERBOLIC,
},
"stability": {
"near_parabolic": NEAR_PARABOLIC_ECCENTRICITIES,
},
}
@pytest.fixture(scope="session")
def vallado_2_4():
return VALLADO_EXAMPLE_2_4
@pytest.fixture(scope="session")
def curtis_4_3():
return CURTIS_EXAMPLE_4_3
@pytest.fixture(scope="session")
def curtis_3_5():
return CURTIS_EXAMPLE_3_5
@pytest.fixture(scope="session")
def vallado75_lambert():
return VALLADO75_LAMBERT
@pytest.fixture(scope="session")
def curtis52_lambert():
return CURTIS52_LAMBERT
@pytest.fixture(scope="session")
def curtis53_lambert():
return CURTIS53_LAMBERT
@pytest.fixture(scope="session")
def hohmann_leo_geo():
return HOHMANN_LEO_TO_GEO
@pytest.fixture(scope="session")
def bielliptic_transfer():
return BIELLIPTIC_TRANSFER
@pytest.fixture(scope="session")
def new_horizons_hyperbolic():
return NEW_HORIZONS_HYPERBOLIC
class TestReferenceDataIntegrity:
def test_all_lambert_cases_have_required_fields(self, all_reference_data):
lambert_cases = all_reference_data["lambert"]
for name, case in lambert_cases.items():
assert "r1" in case, f"{name} missing r1"
assert "r2" in case, f"{name} missing r2"
assert "time_of_flight" in case, f"{name} missing time_of_flight"
assert "attractor" in case, f"{name} missing attractor"
assert case["r1"].shape == (3,), f"{name} r1 wrong shape"
assert case["r2"].shape == (3,), f"{name} r2 wrong shape"
def test_all_bodies_have_gm(self, all_reference_data):
bodies = all_reference_data["bodies"]
for name, body in bodies.items():
assert "gm" in body, f"{name} missing gm"
assert body["gm"] > 0, f"{name} has non-positive gm"
def test_propagation_cases_have_states(self, all_reference_data):
prop_cases = all_reference_data["propagation"]
for name, case in prop_cases.items():
if "initial_state" in case:
assert "r" in case["initial_state"], f"{name} missing r"
assert "v" in case["initial_state"], f"{name} missing v"
def test_tolerances_are_positive(self, all_reference_data):
def check_tolerances(data):
if isinstance(data, dict):
for key, value in data.items():
if key == "tolerance" and isinstance(value, dict):
for tol_key, tol_val in value.items():
assert tol_val > 0, f"Negative tolerance: {tol_key} = {tol_val}"
else:
check_tolerances(value)
check_tolerances(all_reference_data)