astrora_core 0.1.1

Astrora - Rust-backed astrodynamics library - core computational components
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
"""
Pytest configuration and shared fixtures for astrora test suite.

This file provides:
1. Common orbit fixtures (LEO, MEO, GEO, HEO, lunar, interplanetary)
2. Celestial body fixtures with standard gravitational parameters
3. State vector fixtures for various scenarios
4. Time/epoch fixtures
5. Numerical tolerance fixtures
6. Test markers and hooks
7. Reference data fixtures from test_reference_data.py
"""

from typing import Dict

import numpy as np
import pytest

# Import astrora modules
try:
    import astrora._core as core
    from astrora._core import (
        CartesianState,
        Duration,
        Epoch,
        OrbitalElements,
        constants,
    )

    ASTRORA_AVAILABLE = True
except ImportError:
    ASTRORA_AVAILABLE = False
    pytest.skip("astrora not available", allow_module_level=True)

# Register test_reference_data module to make fixtures available
# This is the pytest-recommended way to share fixtures across modules
pytest_plugins = ["tests.test_reference_data"]


# ============================================================================
# Test Configuration Fixtures
# ============================================================================


@pytest.fixture(scope="session")
def numerical_tolerances() -> Dict[str, float]:
    """
    Standard numerical tolerances for different test categories.

    Returns:
        Dictionary of tolerance values for various test types
    """
    return {
        "position_m": 1e-6,  # 1 micrometer position tolerance
        "velocity_m_s": 1e-9,  # 1 nanometer/second velocity tolerance
        "angle_rad": 1e-12,  # Sub-milliarcsecond angular tolerance
        "energy_relative": 1e-10,  # Energy conservation tolerance
        "momentum_relative": 1e-10,  # Angular momentum conservation tolerance
        "time_s": 1e-9,  # 1 nanosecond time tolerance
        "mass_kg": 1e-12,  # Mass tolerance
        # Looser tolerances for integration tests
        "integration_position_m": 1e-3,  # 1 mm for numerical integration
        "integration_velocity_m_s": 1e-6,  # 1 micrometer/s for integration
        # Very loose for validation against external tools
        "validation_position_m": 1.0,  # 1 meter for GMAT/STK comparison
        "validation_velocity_m_s": 1e-3,  # 1 mm/s for external validation
    }


@pytest.fixture(scope="session")
def standard_epochs() -> Dict[str, Epoch]:
    """
    Standard test epochs for various scenarios.

    Returns:
        Dictionary of commonly used epoch values
    """
    return {
        "j2000": Epoch.j2000_epoch(),  # J2000.0 epoch
        "gps_epoch": Epoch.from_midnight_utc(1980, 1, 6),  # GPS epoch
        "unix_epoch": Epoch.from_midnight_utc(1970, 1, 1),  # Unix epoch
        "year_2025": Epoch.from_midnight_utc(2025, 1, 1),
        "year_2030": Epoch.from_midnight_utc(2030, 1, 1),
    }


# ============================================================================
# Celestial Body Fixtures
# ============================================================================


@pytest.fixture(scope="session")
def earth_params() -> Dict[str, float]:
    """Standard Earth parameters for testing."""
    return {
        "gm": constants.GM_EARTH,  # m³/s²
        "radius": constants.R_EARTH,  # m
        "j2": constants.J2_EARTH,
        "angular_velocity": 7.292115e-5,  # rad/s
    }


@pytest.fixture(scope="session")
def sun_params() -> Dict[str, float]:
    """Standard Sun parameters for testing."""
    return {
        "gm": constants.GM_SUN,  # m³/s²
        "radius": constants.R_SUN,  # m
    }


@pytest.fixture(scope="session")
def moon_params() -> Dict[str, float]:
    """Standard Moon parameters for testing."""
    return {
        "gm": constants.GM_MOON,  # m³/s²
        "radius": constants.R_MOON,  # m
    }


# ============================================================================
# Orbit Regime Fixtures - State Vectors
# ============================================================================


@pytest.fixture
def leo_state(earth_params: Dict[str, float]) -> CartesianState:
    """
    Low Earth Orbit (LEO) state vector.

    Circular orbit at 400 km altitude (typical ISS orbit).
    """
    gm = earth_params["gm"]
    r_earth = earth_params["radius"]
    altitude = 400000.0  # 400 km
    r = r_earth + altitude
    v = np.sqrt(gm / r)

    return CartesianState([r, 0.0, 0.0], [0.0, v, 0.0])


@pytest.fixture
def meo_state(earth_params: Dict[str, float]) -> CartesianState:
    """
    Medium Earth Orbit (MEO) state vector.

    Circular orbit at 20,200 km altitude (typical GPS orbit).
    """
    gm = earth_params["gm"]
    r_earth = earth_params["radius"]
    altitude = 20200000.0  # 20,200 km
    r = r_earth + altitude
    v = np.sqrt(gm / r)

    return CartesianState([r, 0.0, 0.0], [0.0, v, 0.0])


@pytest.fixture
def geo_state(earth_params: Dict[str, float]) -> CartesianState:
    """
    Geostationary Orbit (GEO) state vector.

    Circular equatorial orbit at ~35,786 km altitude.
    """
    gm = earth_params["gm"]
    # Geostationary radius
    r = (gm / (earth_params["angular_velocity"] ** 2)) ** (1 / 3)
    v = np.sqrt(gm / r)

    return CartesianState([r, 0.0, 0.0], [0.0, v, 0.0])


@pytest.fixture
def gto_state(earth_params: Dict[str, float]) -> CartesianState:
    """
    Geostationary Transfer Orbit (GTO) state vector.

    Elliptical orbit with perigee at 300 km and apogee at GEO altitude.
    """
    gm = earth_params["gm"]
    r_earth = earth_params["radius"]
    r_perigee = r_earth + 300000.0  # 300 km
    r_apogee = (gm / (earth_params["angular_velocity"] ** 2)) ** (1 / 3)

    a = (r_perigee + r_apogee) / 2
    e = (r_apogee - r_perigee) / (r_apogee + r_perigee)

    # Velocity at perigee
    v = np.sqrt(gm * (2 / r_perigee - 1 / a))

    return CartesianState([r_perigee, 0.0, 0.0], [0.0, v, 0.0])


@pytest.fixture
def heo_state(earth_params: Dict[str, float]) -> CartesianState:
    """
    Highly Elliptical Orbit (HEO) state vector.

    Molniya-type orbit: e=0.74, i=63.4°, period=12h.
    """
    gm = earth_params["gm"]
    r_earth = earth_params["radius"]

    # Molniya orbit parameters
    period = 43200.0  # 12 hours in seconds
    a = (gm * (period / (2 * np.pi)) ** 2) ** (1 / 3)
    e = 0.74
    i = np.radians(63.4)

    # State at perigee
    r_perigee = a * (1 - e)
    v_perigee = np.sqrt(gm * (2 / r_perigee - 1 / a))

    # Apply inclination
    return CartesianState(
        [r_perigee, 0.0, 0.0], [0.0, v_perigee * np.cos(i), v_perigee * np.sin(i)]
    )


@pytest.fixture
def sso_state(earth_params: Dict[str, float]) -> CartesianState:
    """
    Sun-Synchronous Orbit (SSO) state vector.

    Circular orbit at 800 km altitude with 98° inclination.
    """
    gm = earth_params["gm"]
    r_earth = earth_params["radius"]
    altitude = 800000.0  # 800 km
    r = r_earth + altitude
    v = np.sqrt(gm / r)
    i = np.radians(98.0)

    return CartesianState([r, 0.0, 0.0], [0.0, v * np.cos(i), v * np.sin(i)])


@pytest.fixture
def lunar_transfer_state(earth_params: Dict[str, float]) -> CartesianState:
    """
    Lunar transfer orbit state vector.

    Initial state for a Hohmann-like transfer to lunar distance.
    """
    gm = earth_params["gm"]
    r_earth = earth_params["radius"]
    r_moon = 384400000.0  # meters (lunar distance)

    # Hohmann transfer from LEO to lunar distance
    r_departure = r_earth + 200000.0  # 200 km altitude
    a = (r_departure + r_moon) / 2
    v = np.sqrt(gm * (2 / r_departure - 1 / a))

    return CartesianState([r_departure, 0.0, 0.0], [0.0, v, 0.0])


# ============================================================================
# Orbit Regime Fixtures - Orbital Elements
# ============================================================================


@pytest.fixture
def circular_equatorial_elements(earth_params: Dict[str, float]) -> OrbitalElements:
    """Circular equatorial orbit elements (e=0, i=0)."""
    r_earth = earth_params["radius"]
    a = r_earth + 500000.0  # 500 km altitude

    return OrbitalElements(a=a, e=0.0, i=0.0, raan=0.0, argp=0.0, nu=0.0)


@pytest.fixture
def eccentric_inclined_elements(earth_params: Dict[str, float]) -> OrbitalElements:
    """Eccentric inclined orbit elements (e=0.3, i=45°)."""
    r_earth = earth_params["radius"]
    a = r_earth + 10000000.0  # High altitude

    return OrbitalElements(
        a=a,
        e=0.3,
        i=np.radians(45.0),
        raan=np.radians(30.0),
        argp=np.radians(60.0),
        nu=np.radians(90.0),
    )


@pytest.fixture
def polar_orbit_elements(earth_params: Dict[str, float]) -> OrbitalElements:
    """Polar orbit elements (i=90°)."""
    r_earth = earth_params["radius"]
    a = r_earth + 700000.0  # 700 km altitude

    return OrbitalElements(a=a, e=0.001, i=np.radians(90.0), raan=0.0, argp=0.0, nu=0.0)


# ============================================================================
# Test Data Arrays
# ============================================================================


@pytest.fixture
def test_true_anomalies() -> np.ndarray:
    """Array of test true anomaly values covering full orbit."""
    return np.linspace(0, 2 * np.pi, 100)


@pytest.fixture
def test_eccentricities() -> np.ndarray:
    """Array of test eccentricity values (circular to highly elliptical)."""
    return np.array([0.0, 0.1, 0.3, 0.5, 0.7, 0.9, 0.95, 0.99])


@pytest.fixture
def test_inclinations() -> np.ndarray:
    """Array of test inclination values (equatorial to polar)."""
    return np.radians([0, 15, 30, 45, 60, 75, 90, 105, 120, 135, 150, 165, 180])


# ============================================================================
# Propagation Time Fixtures
# ============================================================================


@pytest.fixture
def short_propagation_times() -> np.ndarray:
    """Short propagation times for quick tests (0 to 1 orbit)."""
    return np.linspace(0, 6000, 50)  # Up to ~1.7 hours


@pytest.fixture
def medium_propagation_times() -> np.ndarray:
    """Medium propagation times for validation (0 to 1 day)."""
    return np.linspace(0, 86400, 100)  # Up to 1 day


@pytest.fixture
def long_propagation_times() -> np.ndarray:
    """Long propagation times for stability tests (0 to 7 days)."""
    return np.linspace(0, 604800, 200)  # Up to 7 days


# ============================================================================
# Pytest Hooks
# ============================================================================


def pytest_configure(config):
    """Pytest configuration hook for custom setup."""
    # Register custom markers dynamically if needed
    pass


def pytest_collection_modifyitems(config, items):
    """
    Automatically mark tests based on their module and name.

    This hook automatically applies markers to tests based on patterns:
    - Files starting with 'benchmark_' get the 'benchmark' marker
    - Files containing 'validation' get the 'validation' marker
    - Files containing 'integration' get the 'integration' marker
    - Tests taking > 1 second get the 'slow' marker (via pytest-timeout)
    """
    for item in items:
        # Mark benchmark files
        if "benchmark_" in str(item.fspath):
            item.add_marker(pytest.mark.benchmark)

        # Mark validation tests
        if "validation" in str(item.fspath):
            item.add_marker(pytest.mark.validation)

        # Mark integration tests
        if "integration" in str(item.fspath) or "_integration" in str(item.fspath):
            item.add_marker(pytest.mark.integration)

        # Mark regression tests
        if "regression" in str(item.fspath):
            item.add_marker(pytest.mark.regression)

        # Mark domain-specific tests based on file names
        fspath_str = str(item.fspath)
        if "propagat" in fspath_str:
            item.add_marker(pytest.mark.propagation)
        if "coordinate" in fspath_str or "frame" in fspath_str or "transform" in fspath_str:
            item.add_marker(pytest.mark.coordinates)
        if "maneuver" in fspath_str or "transfer" in fspath_str:
            item.add_marker(pytest.mark.maneuvers)
        if (
            "perturbation" in fspath_str
            or "_j2" in fspath_str
            or "drag" in fspath_str
            or "srp" in fspath_str
        ):
            item.add_marker(pytest.mark.perturbations)
        if "satellite" in fspath_str or "sgp4" in fspath_str or "tle" in fspath_str:
            item.add_marker(pytest.mark.satellite)
        if "plot" in fspath_str or "animation" in fspath_str:
            item.add_marker(pytest.mark.plotting)


def pytest_report_header(config):
    """Add custom header information to pytest output."""
    return [
        f"astrora version: {core.__version__ if hasattr(core, '__version__') else 'unknown'}",
        f"NumPy version: {np.__version__}",
    ]


# ============================================================================
# Benchmark Fixtures (for pytest-benchmark)
# ============================================================================


@pytest.fixture
def benchmark_arrays():
    """Standard array sizes for benchmarking."""
    return {
        "tiny": 10,
        "small": 100,
        "medium": 1000,
        "large": 10000,
        "huge": 100000,
    }