Skip to main content
Glama
test_integrations_frames.py11.7 kB
""" Tests for coordinate frame transformation module. """ import math import pytest from aerospace_mcp.integrations.frames import ( ASTROPY_AVAILABLE, SKYFIELD_AVAILABLE, _manual_ecef_to_geodetic, _manual_geodetic_to_ecef, ecef_to_geodetic, geodetic_to_ecef, get_frame_info, transform_frames, ) class TestGeodeticECEFConversions: """Test geodetic <-> ECEF coordinate conversions.""" def test_manual_conversions_round_trip(self): """Test manual conversions maintain round-trip accuracy.""" # Test with known coordinates lat, lon, alt = 37.7749, -122.4194, 100.0 # San Francisco-ish # Convert to ECEF and back x, y, z = _manual_geodetic_to_ecef(lat, lon, alt) lat_out, lon_out, alt_out = _manual_ecef_to_geodetic(x, y, z) # Should round-trip within reasonable precision assert abs(lat_out - lat) < 1e-9 assert abs(lon_out - lon) < 1e-9 assert abs(alt_out - alt) < 2e-6 # meters (adjusted for numerical precision) def test_known_ecef_values(self): """Test conversion against known ECEF values.""" # Equator, Greenwich meridian, sea level lat, lon, alt = 0.0, 0.0, 0.0 x, y, z = _manual_geodetic_to_ecef(lat, lon, alt) # Should be on equatorial plane at Earth radius assert abs(x - 6378137.0) < 1.0 # WGS84 semi-major axis assert abs(y) < 1e-6 assert abs(z) < 1e-6 # North pole lat, lon, alt = 90.0, 0.0, 0.0 x, y, z = _manual_geodetic_to_ecef(lat, lon, alt) assert abs(x) < 1e-6 assert abs(y) < 1e-6 assert abs(z - 6356752.314245) < 1.0 # WGS84 semi-minor axis def test_geodetic_validation(self): """Test geodetic coordinate validation.""" # Valid coordinates should work result = geodetic_to_ecef(45.0, -90.0, 1000.0) assert result.frame == "ECEF" # At longitude -90°, x should be near zero (on negative y-axis) assert abs(result.x) < 1.0 # Should be essentially zero assert abs(result.y) > 4000000 # Should be large negative y value # Invalid latitude with pytest.raises(ValueError, match="Latitude must be between"): geodetic_to_ecef(91.0, 0.0, 0.0) with pytest.raises(ValueError, match="Latitude must be between"): geodetic_to_ecef(-91.0, 0.0, 0.0) # Invalid longitude with pytest.raises(ValueError, match="Longitude must be between"): geodetic_to_ecef(0.0, 181.0, 0.0) with pytest.raises(ValueError, match="Longitude must be between"): geodetic_to_ecef(0.0, -181.0, 0.0) def test_altitude_effects(self): """Test that altitude changes affect ECEF coordinates properly.""" lat, lon = 45.0, 45.0 # Compare sea level vs high altitude ecef_low = geodetic_to_ecef(lat, lon, 0.0) ecef_high = geodetic_to_ecef(lat, lon, 10000.0) # 10km up # Distance from origin should increase dist_low = math.sqrt(ecef_low.x**2 + ecef_low.y**2 + ecef_low.z**2) dist_high = math.sqrt(ecef_high.x**2 + ecef_high.y**2 + ecef_high.z**2) assert dist_high > dist_low assert abs(dist_high - dist_low - 10000.0) < 1.0 # Should be ~10km difference class TestFrameTransformations: """Test coordinate frame transformations.""" def test_same_frame_no_transform(self): """Test that same frame transformation returns unchanged coordinates.""" xyz = [1000000.0, 2000000.0, 3000000.0] result = transform_frames(xyz, "ECEF", "ECEF") assert result.x == xyz[0] assert result.y == xyz[1] assert result.z == xyz[2] assert result.frame == "ECEF" def test_ecef_geodetic_transforms(self): """Test ECEF <-> GEODETIC transformations.""" # Start with geodetic coordinates lat, lon, alt = 37.7749, -122.4194, 500.0 # Transform to ECEF ecef_result = transform_frames([lat, lon, alt], "GEODETIC", "ECEF") assert ecef_result.frame == "ECEF" assert abs(ecef_result.x) > 1000000 # Reasonable ECEF magnitude # Transform back to geodetic geodetic_result = transform_frames( [ecef_result.x, ecef_result.y, ecef_result.z], "ECEF", "GEODETIC" ) assert geodetic_result.frame == "GEODETIC" assert abs(geodetic_result.x - lat) < 1e-6 # Should round-trip assert abs(geodetic_result.y - lon) < 1e-6 assert abs(geodetic_result.z - alt) < 0.001 def test_coordinate_validation(self): """Test coordinate input validation.""" # Wrong number of coordinates with pytest.raises(ValueError, match="xyz must be a list of 3 coordinates"): transform_frames([1, 2], "ECEF", "ECI") with pytest.raises(ValueError, match="xyz must be a list of 3 coordinates"): transform_frames([1, 2, 3, 4], "ECEF", "ECI") def test_invalid_frames(self): """Test invalid frame names.""" xyz = [1000000.0, 2000000.0, 3000000.0] with pytest.raises(ValueError, match="Frame must be one of"): transform_frames(xyz, "INVALID", "ECEF") with pytest.raises(ValueError, match="Frame must be one of"): transform_frames(xyz, "ECEF", "INVALID") def test_eci_ecef_approximate_transform(self): """Test ECI <-> ECEF approximate transformation.""" xyz = [6500000.0, 0.0, 0.0] # Point on equatorial plane # Transform ECI to ECEF (with astropy, accounts for Earth rotation) result = transform_frames(xyz, "ECI", "ECEF") assert result.frame == "ECEF" # With proper transformation, coordinates will be significantly different due to Earth rotation # Just check that the magnitude is reasonable (should be similar distance from Earth center) original_magnitude = (xyz[0] ** 2 + xyz[1] ** 2 + xyz[2] ** 2) ** 0.5 result_magnitude = (result.x**2 + result.y**2 + result.z**2) ** 0.5 assert ( abs(result_magnitude - original_magnitude) < 1000 ) # Magnitude should be preserved @pytest.mark.skipif(not ASTROPY_AVAILABLE, reason="astropy not available") def test_astropy_integration(self): """Test astropy integration if available.""" xyz = [6500000.0, 1000000.0, 2000000.0] # Should use astropy for high-precision transforms if available result = transform_frames(xyz, "ECI", "ECEF", "2023-01-01T00:00:00") assert result.frame == "ECEF" assert isinstance(result.x, float) assert isinstance(result.y, float) assert isinstance(result.z, float) def test_approximate_transforms(self): """Test that approximate transformations work without high-precision libraries.""" xyz = [1000000.0, 2000000.0, 3000000.0] # Without high-precision libraries, should still provide approximate results if not ASTROPY_AVAILABLE and not SKYFIELD_AVAILABLE: result = transform_frames(xyz, "GCRS", "ITRF") assert result.frame == "ITRF" assert result.x == xyz[0] # Simplified approximation keeps same coordinates assert result.y == xyz[1] assert result.z == xyz[2] class TestConvenienceFunctions: """Test convenience functions.""" def test_ecef_to_geodetic_function(self): """Test ECEF to geodetic convenience function.""" # Known ECEF coordinates (approximately San Francisco) x, y, z = -2699490.0, -4293565.0, 3855273.0 result = ecef_to_geodetic(x, y, z) # Should be reasonable geodetic coordinates assert -90 <= result.latitude_deg <= 90 assert -180 <= result.longitude_deg <= 180 assert result.altitude_m > -1000 # Not too far underground def test_geodetic_to_ecef_function(self): """Test geodetic to ECEF convenience function.""" result = geodetic_to_ecef(37.7749, -122.4194, 100.0) assert result.frame == "ECEF" assert isinstance(result.x, float) assert isinstance(result.y, float) assert isinstance(result.z, float) # Should be reasonable ECEF magnitude for Earth surface distance = math.sqrt(result.x**2 + result.y**2 + result.z**2) assert 6.3e6 < distance < 6.5e6 # Earth radius range class TestFrameInfo: """Test frame information utilities.""" def test_get_frame_info(self): """Test frame information retrieval.""" info = get_frame_info() assert "available_frames" in info assert "ECEF" in info["available_frames"] assert "GEODETIC" in info["available_frames"] assert "ECI" in info["available_frames"] assert "libraries" in info assert "astropy" in info["libraries"] assert "skyfield" in info["libraries"] assert "manual_transforms" in info assert "notes" in info # Check library availability reporting assert info["libraries"]["astropy"] == ASTROPY_AVAILABLE assert info["libraries"]["skyfield"] == SKYFIELD_AVAILABLE assert info["high_precision_available"] == ASTROPY_AVAILABLE class TestNumericalStability: """Test numerical stability and edge cases.""" def test_polar_coordinates(self): """Test coordinate transformations at poles.""" # North pole result = geodetic_to_ecef(90.0, 0.0, 1000.0) # At pole, X and Y should be near zero assert abs(result.x) < 1e-6 assert abs(result.y) < 1e-6 assert result.z > 6350000 # Should be near polar radius + altitude # South pole result = geodetic_to_ecef(-90.0, 180.0, 0.0) assert abs(result.x) < 1e-6 assert abs(result.y) < 1e-6 assert result.z < -6350000 # Should be negative def test_equatorial_coordinates(self): """Test coordinates on equator.""" # Prime meridian on equator result = geodetic_to_ecef(0.0, 0.0, 0.0) assert abs(result.x - 6378137.0) < 1.0 # Earth radius assert abs(result.y) < 1e-6 assert abs(result.z) < 1e-6 # 90° east on equator result = geodetic_to_ecef(0.0, 90.0, 0.0) assert abs(result.x) < 1e-6 assert abs(result.y - 6378137.0) < 1.0 assert abs(result.z) < 1e-6 def test_high_altitude(self): """Test very high altitude coordinates.""" # Satellite altitude result = geodetic_to_ecef(0.0, 0.0, 20200000.0) # GPS orbit altitude # Distance from center should be Earth radius + altitude distance = math.sqrt(result.x**2 + result.y**2 + result.z**2) expected = 6378137.0 + 20200000.0 assert abs(distance - expected) < 1.0 def test_precision_consistency(self): """Test that precision is consistent across different coordinate ranges.""" test_cases = [ (0.0, 0.0, 0.0), (45.0, 45.0, 1000.0), (-45.0, -45.0, 1000.0), (89.9, 179.9, 10000.0), (-89.9, -179.9, 10000.0), ] for lat, lon, alt in test_cases: # Round trip test ecef = geodetic_to_ecef(lat, lon, alt) geodetic = ecef_to_geodetic(ecef.x, ecef.y, ecef.z) # Should maintain precision assert abs(geodetic.latitude_deg - lat) < 1e-9 assert abs(geodetic.longitude_deg - lon) < 1e-9 assert ( abs(geodetic.altitude_m - alt) < 5e-3 ) # Relaxed for high latitude/altitude cases if __name__ == "__main__": pytest.main([__file__])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cheesejaguar/aerospace-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server