astrora_core 0.1.1

Astrora - Rust-backed astrodynamics library - core computational components
Documentation
"""
Tests for simple modules to improve coverage - no external dependencies.

Focuses on units, coordinates, maneuver functions that don't require
Plotly, Cartopy, or other optional dependencies.
"""

import numpy as np
import pytest
from astropy import units as u
from astrora._core import Epoch
from astrora.bodies import Earth


class TestUnitsModuleComprehensive:
    """Comprehensive tests for units module."""

    def test_to_si_velocity(self):
        """Test velocity conversion to SI (m/s)."""
        from astrora.units import to_si_velocity

        # Test with raw number (assume m/s)
        result = to_si_velocity(7500)
        assert result == 7500

        # Test with quantity
        v_kms = 7.5 * (u.km / u.s)
        result = to_si_velocity(v_kms)
        assert abs(result - 7500) < 1e-6

        # Test with array
        v_array = np.array([1000, 2000, 3000])
        result = to_si_velocity(v_array)
        assert np.allclose(result, v_array)

    def test_to_si_angle(self):
        """Test angle conversion to SI (radians)."""
        from astrora.units import to_si_angle

        # Test with raw number (assume radians)
        result = to_si_angle(np.pi)
        assert result == np.pi

        # Test with degrees
        angle_deg = 180 * u.deg
        result = to_si_angle(angle_deg)
        assert abs(result - np.pi) < 1e-10

        # Test with array
        angles = np.array([0, np.pi / 2, np.pi])
        result = to_si_angle(angles)
        assert np.allclose(result, angles)

    def test_to_si_time(self):
        """Test time conversion to SI (seconds)."""
        from astrora.units import to_si_time

        # Test with raw number (assume seconds)
        result = to_si_time(3600)
        assert result == 3600

        # Test with hours
        t_hours = 1 * u.hour
        result = to_si_time(t_hours)
        assert abs(result - 3600) < 1e-6

        # Test with array
        times = np.array([60, 120, 180])
        result = to_si_time(times)
        assert np.allclose(result, times)

    def test_to_dimensionless(self):
        """Test dimensionless conversion."""
        from astrora.units import to_dimensionless

        # Test with raw number
        result = to_dimensionless(0.5)
        assert result == 0.5

        # Test with dimensionless quantity
        q = 0.7 * u.dimensionless_unscaled
        result = to_dimensionless(q)
        assert abs(result - 0.7) < 1e-10

    def test_to_si_length(self):
        """Test length/distance conversion to SI (m)."""
        from astrora.units import to_si_length

        # Test with raw number
        result = to_si_length(7000000)
        assert result == 7000000

        # Test with km
        d_km = 7000 * u.km
        result = to_si_length(d_km)
        assert abs(result - 7000000) < 1

    def test_to_si_position(self):
        """Test position vector conversion to SI."""
        from astrora.units import to_si_position

        # Test with raw array (assume m)
        pos = np.array([7000e3, 0, 0])
        result = to_si_position(pos)
        assert np.allclose(result, pos)

        # Test with quantity
        pos_km = np.array([7000, 0, 0]) * u.km
        result = to_si_position(pos_km)
        assert np.allclose(result, [7000e3, 0, 0])

    def test_as_quantity_functions(self):
        """Test as_quantity conversion functions."""
        from astrora.units import as_quantity_angle, as_quantity_length, as_quantity_velocity

        # Test length
        length_q = as_quantity_length(7000e3)  # meters to km
        assert length_q.unit == u.km
        assert abs(length_q.value - 7000) < 0.1

        # Test velocity
        vel_q = as_quantity_velocity(7546)  # m/s to km/s
        assert vel_q.unit == (u.km / u.s)
        assert abs(vel_q.value - 7.546) < 0.001

        # Test angle
        angle_q = as_quantity_angle(np.pi / 2)  # radians
        assert angle_q.unit == u.rad
        assert abs(angle_q.value - np.pi / 2) < 1e-10


class TestCoordinatesModuleComprehensive:
    """Comprehensive tests for coordinates module."""

    def test_check_astropy_function(self):
        """Test the astropy check function."""
        from astrora.coordinates import _check_astropy

        # Should not raise error
        _check_astropy()

    def test_coordinate_frame_classes(self):
        """Test coordinate frame classes."""
        from astrora.coordinates import AstroraGCRS, AstroraICRS, AstroraITRS

        # These are coordinate frame classes - they exist
        assert AstroraICRS is not None
        assert AstroraGCRS is not None
        assert AstroraITRS is not None

        # They are classes that can be instantiated
        # (exact constructor signature may vary)
        assert callable(AstroraICRS)
        assert callable(AstroraGCRS)
        assert callable(AstroraITRS)

    def test_astropy_integration_functions(self):
        """Test astropy integration if available."""
        try:
            from astropy.time import Time
            from astrora.coordinates import to_astropy_coord

            r = np.array([7000e3, 0, 0])
            v = np.array([0, 7546, 0])
            obstime = Time("2025-01-01T00:00:00")

            # This may work or may require different parameters
            # Just test that the function exists
            assert callable(to_astropy_coord)
        except ImportError:
            pytest.skip("Astropy coordinate integration not available")


class TestManeuverModuleComprehensive:
    """Comprehensive tests for maneuver module."""

    def test_maneuver_creation(self):
        """Test basic maneuver creation."""
        from astrora.maneuver import Maneuver

        # Create a simple impulsive maneuver
        dv = np.array([100, 0, 0])  # 100 m/s prograde
        maneuver = Maneuver((0, dv))

        assert maneuver is not None
        assert len(maneuver.impulses) == 1

        # Create a two-impulse maneuver
        maneuver2 = Maneuver((0, dv), (3600, -dv))
        assert len(maneuver2.impulses) == 2

    def test_maneuver_from_hohmann_direct(self):
        """Test Hohmann transfer using direct Rust function."""
        try:
            from astrora._core import hohmann_transfer

            # LEO to GEO transfer
            r_leo = 7000e3  # 7000 km
            r_geo = 42164e3  # 42164 km
            mu = Earth.mu  # Already in m^3/s^2

            result = hohmann_transfer(r_leo, r_geo, mu)

            # Should have delta_v values (dict returned from Rust)
            assert isinstance(result, dict)
            # Check for expected keys (may vary)
            assert len(result) > 0
        except ImportError:
            pytest.skip("Rust _core module not available")

    def test_maneuver_from_bielliptic_direct(self):
        """Test bielliptic transfer using direct Rust function."""
        try:
            from astrora._core import bielliptic_transfer

            r_i = 7000e3  # Initial orbit (LEO)
            r_f = 42164e3  # Final orbit (GEO)
            r_b = 80000e3  # Intermediate orbit (must be > max(r_i, r_f))
            mu = Earth.mu  # Already in m^3/s^2

            # Try the function - it will validate parameters
            try:
                result = bielliptic_transfer(r_i, r_b, r_f, mu)
                # Should return a dict
                assert isinstance(result, dict)
                assert len(result) > 0
            except ValueError as e:
                # If parameters are invalid, that's OK - the function exists
                # and does parameter validation
                assert "Invalid parameter" in str(e)
        except ImportError:
            pytest.skip("Rust _core module not available")


class TestTimeModuleComprehensive:
    """Comprehensive tests for time module."""

    def test_epoch_to_astropy_time(self):
        """Test Epoch to astropy Time conversion."""
        try:
            from astrora._core import Epoch
            from astrora.time import epoch_to_astropy_time

            # Create an epoch using available method
            epoch = Epoch.from_midnight_utc(2025, 1, 1)

            # Convert to astropy Time
            astropy_time = epoch_to_astropy_time(epoch)
            assert astropy_time is not None
        except ImportError:
            pytest.skip("Astropy Time integration not available")

    def test_astropy_time_to_epoch(self):
        """Test astropy Time to Epoch conversion."""
        try:
            from astropy.time import Time
            from astrora.time import astropy_time_to_epoch

            # Create astropy Time
            t = Time("2025-01-01T00:00:00", scale="utc")

            # Convert to Epoch
            epoch = astropy_time_to_epoch(t)
            assert epoch is not None
        except ImportError:
            pytest.skip("Astropy Time integration not available")

    def test_to_epoch_function(self):
        """Test to_epoch conversion function."""
        from astrora.time import to_epoch

        # Test with None
        result = to_epoch(None)
        assert result is None

        # Test with Epoch (should return same)
        epoch = Epoch.from_midnight_utc(2025, 1, 1)
        result = to_epoch(epoch)
        assert result is epoch

    def test_to_astropy_time_function(self):
        """Test to_astropy_time conversion function."""
        try:
            from astrora._core import Epoch
            from astrora.time import to_astropy_time

            # Test with Epoch
            epoch = Epoch.from_midnight_utc(2025, 1, 1)
            astropy_time = to_astropy_time(epoch)
            assert astropy_time is not None
        except ImportError:
            pytest.skip("Astropy Time integration not available")

    def test_epoch_direct_usage(self):
        """Test direct Epoch usage from _core."""

        # Create epoch using available method
        epoch = Epoch.from_midnight_utc(2025, 6, 15)
        assert epoch is not None

        # Test that it has expected methods/attributes
        assert hasattr(epoch, "jd_utc") or hasattr(epoch, "jd_tt")

        # Test accessing JD
        jd = epoch.jd_utc
        assert jd > 0


class TestUtilModuleComprehensive:
    """Comprehensive tests for util module."""

    def test_time_range_function(self):
        """Test time_range utility."""
        from astropy.time import Time
        from astrora.util import time_range

        # Create time range with start, periods, and end
        start_time = Time("2025-01-01T00:00:00")
        end_time = Time("2025-01-02T00:00:00")

        times = time_range(start_time, periods=100, end=end_time)
        assert len(times) == 100
        assert times[0].jd == start_time.jd
        assert times[-1].jd == end_time.jd

    def test_wrap_angle_function(self):
        """Test wrap_angle function."""
        from astrora.util import wrap_angle

        # Default wraps to [-180, 180) degrees
        angle_deg = wrap_angle(270)  # degrees
        assert -180 <= angle_deg < 180

        # Test with radians
        angle_rad = wrap_angle(3 * np.pi * u.rad, limit=np.pi * u.rad)
        # Should wrap to [-π, π)
        assert angle_rad.value >= -np.pi
        assert angle_rad.value < np.pi

    def test_alinspace_function(self):
        """Test alinspace (angle linspace)."""
        from astrora.util import alinspace

        # Create angle range
        angles = alinspace(0, 2 * np.pi, num=100)
        assert len(angles) == 100
        assert abs(angles[0]) < 1e-10
        assert abs(angles[-1] - 2 * np.pi) < 1e-6

    def test_find_closest_value(self):
        """Test find_closest_value utility."""
        from astrora.util import find_closest_value

        array = np.array([1, 3, 5, 7, 9])
        value = 6

        idx = find_closest_value(array, value)
        assert idx == 2  # Index of 5, which is closest to 6


class TestInitModule:
    """Tests for __init__.py to improve coverage."""

    def test_version_attribute(self):
        """Test that version is accessible."""
        import astrora

        assert hasattr(astrora, "__version__")
        assert isinstance(astrora.__version__, str)

    def test_core_imports(self):
        """Test that core modules can be imported."""
        import astrora

        assert hasattr(astrora, "Orbit")
        assert hasattr(astrora, "Maneuver")
        assert hasattr(astrora, "bodies")
        assert hasattr(astrora, "twobody")

        # Test that Earth can be accessed via bodies
        from astrora import bodies

        assert hasattr(bodies, "Earth")

    def test_submodule_imports(self):
        """Test that submodules are accessible."""
        from astrora import bodies, maneuver, plotting, twobody

        assert twobody is not None
        assert bodies is not None
        assert plotting is not None
        assert maneuver is not None


if __name__ == "__main__":
    pytest.main([__file__, "-v"])