import numpy as np
import pytest
from astrora import _core
class TestECEFToGeodetic:
def test_ecef_to_geodetic_equator(self):
wgs84_a = 6378.137 ecef = np.array([wgs84_a, 0.0, 0.0])
result = _core.ecef_to_geodetic(ecef)
assert abs(result["latitude_deg"]) < 0.001
assert abs(result["longitude_deg"]) < 0.001
assert abs(result["altitude_km"]) < 0.01
def test_ecef_to_geodetic_north_pole(self):
wgs84_b = 6356.752314245 ecef = np.array([0.0, 0.0, wgs84_b])
result = _core.ecef_to_geodetic(ecef)
assert abs(result["latitude_deg"] - 90.0) < 0.001
assert abs(result["altitude_km"]) < 0.01
def test_ecef_to_geodetic_south_pole(self):
wgs84_b = 6356.752314245 ecef = np.array([0.0, 0.0, -wgs84_b])
result = _core.ecef_to_geodetic(ecef)
assert abs(result["latitude_deg"] + 90.0) < 0.001
assert abs(result["altitude_km"]) < 0.01
def test_ecef_to_geodetic_with_altitude(self):
wgs84_a = 6378.137 altitude = 500.0
ecef = np.array([wgs84_a + altitude, 0.0, 0.0])
result = _core.ecef_to_geodetic(ecef)
assert abs(result["latitude_deg"]) < 0.001
assert abs(result["longitude_deg"]) < 0.001
assert abs(result["altitude_km"] - altitude) < 0.01
def test_ecef_to_geodetic_western_hemisphere(self):
wgs84_a = 6378.137 ecef = np.array([0.0, -wgs84_a, 0.0])
result = _core.ecef_to_geodetic(ecef)
assert abs(result["latitude_deg"]) < 0.001
assert abs(result["longitude_deg"] + 90.0) < 0.001
assert abs(result["altitude_km"]) < 0.01
def test_ecef_to_geodetic_leo_satellite(self):
ecef = np.array([6700.0, 0.0, 500.0])
result = _core.ecef_to_geodetic(ecef)
assert -90 <= result["latitude_deg"] <= 90
assert -180 <= result["longitude_deg"] <= 180
assert 300 < result["altitude_km"] < 600
def test_ecef_to_geodetic_invalid_input(self):
with pytest.raises(ValueError):
_core.ecef_to_geodetic(np.array([1.0, 2.0]))
with pytest.raises(ValueError):
_core.ecef_to_geodetic(np.array([]))
class TestComputeGroundTrack:
def test_compute_ground_track_basic(self):
positions = np.array(
[
[6778.0, 0.0, 0.0],
[6778.0, 100.0, 50.0],
[6750.0, 200.0, 100.0],
]
)
times = np.array([0.0, 1.0, 2.0])
result = _core.compute_ground_track(positions, times)
assert "latitudes_deg" in result
assert "longitudes_deg" in result
assert "altitudes_km" in result
assert "times_minutes" in result
assert len(result["latitudes_deg"]) == 3
assert len(result["longitudes_deg"]) == 3
assert len(result["altitudes_km"]) == 3
assert len(result["times_minutes"]) == 3
np.testing.assert_array_almost_equal(result["times_minutes"], times)
assert np.all(np.isfinite(result["latitudes_deg"]))
assert np.all(np.isfinite(result["longitudes_deg"]))
assert np.all(np.isfinite(result["altitudes_km"]))
assert np.all(np.abs(result["latitudes_deg"]) <= 90)
assert np.all(np.abs(result["longitudes_deg"]) <= 180)
def test_compute_ground_track_equatorial_orbit(self):
wgs84_a = 6378.137
altitude = 400.0
r = wgs84_a + altitude
angles = np.linspace(0, np.pi / 4, 5)
positions = np.array([[r * np.cos(ang), r * np.sin(ang), 0.0] for ang in angles])
times = np.linspace(0, 10, 5)
result = _core.compute_ground_track(positions, times)
assert np.all(np.abs(result["latitudes_deg"]) < 5.0)
lons = result["longitudes_deg"]
assert np.all(np.diff(lons) > 0)
alts = result["altitudes_km"]
assert np.all(np.abs(alts - altitude) < 50.0)
def test_compute_ground_track_polar_orbit(self):
wgs84_a = 6378.137
altitude = 500.0
r = wgs84_a + altitude
lats_rad = np.linspace(-np.pi / 3, np.pi / 3, 5)
positions = np.array([[r * np.cos(lat), 0.0, r * np.sin(lat)] for lat in lats_rad])
times = np.linspace(0, 15, 5)
result = _core.compute_ground_track(positions, times)
lat_range = np.ptp(result["latitudes_deg"])
assert lat_range > 50
assert np.all(np.abs(result["longitudes_deg"]) < 30.0)
def test_compute_ground_track_invalid_input(self):
positions = np.array([[6700.0, 0.0, 0.0]])
times = np.array([0.0, 1.0])
with pytest.raises(ValueError, match="must match"):
_core.compute_ground_track(positions, times)
positions = np.array([[6700.0, 0.0]]) times = np.array([0.0])
with pytest.raises(ValueError, match="shape"):
_core.compute_ground_track(positions, times)
class TestMaximumGroundRange:
def test_maximum_ground_range_iss(self):
altitude = 400.0
max_range = _core.maximum_ground_range(altitude)
assert 2100 < max_range < 2400
def test_maximum_ground_range_higher_altitude(self):
range_400 = _core.maximum_ground_range(400.0)
range_800 = _core.maximum_ground_range(800.0)
assert range_800 > range_400
def test_maximum_ground_range_scaling(self):
altitudes = [200, 400, 600, 800, 1000]
ranges = [_core.maximum_ground_range(alt) for alt in altitudes]
assert all(r2 > r1 for r1, r2 in zip(ranges[:-1], ranges[1:]))
for r in ranges:
assert 1000 < r < 5000
def test_maximum_ground_range_zero_altitude(self):
max_range = _core.maximum_ground_range(0.0)
assert max_range < 10.0
class TestCalculateSwathWidth:
def test_calculate_swath_width_leo(self):
altitude = 500.0 min_elevation = 10.0
swath = _core.calculate_swath_width(altitude, min_elevation)
assert swath > 0
assert swath < 10000
def test_calculate_swath_width_elevation_effect(self):
altitude = 500.0
swath_5deg = _core.calculate_swath_width(altitude, 5.0)
swath_10deg = _core.calculate_swath_width(altitude, 10.0)
swath_20deg = _core.calculate_swath_width(altitude, 20.0)
assert swath_5deg > swath_10deg > swath_20deg
def test_calculate_swath_width_zero_elevation(self):
altitude = 500.0
swath = _core.calculate_swath_width(altitude, 0.0)
max_range = _core.maximum_ground_range(altitude)
assert swath > 0
assert swath <= 2 * max_range * 1.1
def test_calculate_swath_width_high_elevation(self):
altitude = 500.0
swath = _core.calculate_swath_width(altitude, 80.0)
max_swath = _core.calculate_swath_width(altitude, 0.0)
assert swath < max_swath * 0.3
class TestIntegration:
def test_ecef_to_geodetic_roundtrip_consistency(self):
ecef = np.array([6700.0, 1000.0, 500.0])
result1 = _core.ecef_to_geodetic(ecef)
result2 = _core.ecef_to_geodetic(ecef)
assert result1["latitude_deg"] == result2["latitude_deg"]
assert result1["longitude_deg"] == result2["longitude_deg"]
assert result1["altitude_km"] == result2["altitude_km"]
def test_ground_track_conservation_of_altitude(self):
wgs84_a = 6378.137
altitude = 450.0
r = wgs84_a + altitude
angles = np.linspace(0, 2 * np.pi, 20)
positions = np.array([[r * np.cos(ang), r * np.sin(ang), 0.0] for ang in angles])
times = np.linspace(0, 90, 20)
result = _core.compute_ground_track(positions, times)
alts = result["altitudes_km"]
alt_std = np.std(alts)
assert alt_std < 10.0
assert abs(np.mean(alts) - altitude) < 10.0