JPL Horizons Integration ========================= ``rust-ephem`` includes support for NASA's JPL Horizons system, providing access to ephemerides for solar system bodies not available in SPICE kernels. This is particularly useful for querying asteroids, comets, spacecraft, and other minor bodies. Overview -------- The JPL Horizons system is NASA's comprehensive solar system ephemeris service. It provides high-accuracy position and velocity data for: - **Planets and moons** (when not in SPICE kernels) - **Asteroids** (including named and numbered asteroids) - **Comets** (periodic and non-periodic) - **Spacecraft** (natural and artificial satellites, space probes) - **Interplanetary objects** (Voyager, New Horizons, etc.) When you set ``use_horizons=True`` in ``get_body()`` or ``Constraint.evaluate_moving_body()``, ``rust-ephem`` automatically falls back to JPL Horizons if the requested body is not found in your local SPICE kernels. This enables seamless querying of a much broader range of bodies without requiring large kernel files or pre-configuration. Setup ----- No additional setup is required beyond installing ``rust-ephem``. The Horizons feature is built-in and uses NASA's public HTTP API. .. code-block:: python import rust_ephem as re from datetime import datetime, timezone # Load default planetary ephemeris (still useful for Sun/Moon/planets) re.ensure_planetary_ephemeris() # Create any ephemeris type begin = datetime(2024, 6, 1, tzinfo=timezone.utc) end = datetime(2024, 6, 2, tzinfo=timezone.utc) ephem = re.TLEEphemeris(norad_id=25544, begin=begin, end=end) Basic Usage ----------- Enable Horizons queries by setting ``use_horizons=True``: .. code-block:: python # Query asteroid Ceres ceres = ephem.get_body("1", use_horizons=True) print(f"Ceres RA: {ceres[0].ra}, Dec: {ceres[0].dec}") # Query by name (case-insensitive) ceres = ephem.get_body("Ceres", use_horizons=True) # Query position/velocity data ceres_pv = ephem.get_body_pv("1", use_horizons=True) print(f"Ceres distance: {ceres_pv.position[0]}") # km print(f"Ceres velocity: {ceres_pv.velocity[0]}") # km/s Fallback Behavior ~~~~~~~~~~~~~~~~~ When ``use_horizons=True``, the lookup process is: 1. **Check SPICE kernels first** — If the body is found in your kernel, use it 2. **Fall back to Horizons** — If not found in SPICE, query JPL Horizons 3. **Raise error if not found** — If neither source has the body, raise an exception This approach gives you the best of both worlds: fast, cached SPICE lookups for frequently-used bodies, with automatic fallback to Horizons for less common objects. .. code-block:: python # These are equivalent when Mars is in SPICE kernel: mars_spice = ephem.get_body("Mars") # Uses SPICE (no network) mars_either = ephem.get_body("Mars", use_horizons=True) # Prefers SPICE # But Horizons is required for lesser-known bodies: apophis = ephem.get_body("99942", use_horizons=True) # Asteroid 99942 (Apophis) # This will fail if use_horizons=False and Apophis isn't in SPICE kernel Body Identifiers ---------------- JPL Horizons accepts many types of body identifiers: Common Objects (NAIF IDs) ~~~~~~~~~~~~~~~~~~~~~~~~~ .. list-table:: :header-rows: 1 :widths: 20 30 50 * - Name - NAIF ID - Notes * - Sun - 10 - Solar center * - Moon - 301 - Earth's moon * - Mercury - 199 - Planet center * - Venus - 299 - Planet center * - Earth - 399 - Planet center * - Mars - 499 - Planet center * - Jupiter - 599 - Planet center (5 = barycenter) * - Saturn - 699 - Planet center (6 = barycenter) * - Uranus - 799 - Planet center (7 = barycenter) * - Neptune - 899 - Planet center (8 = barycenter) Asteroids ~~~~~~~~~ Asteroids can be referenced by: - **NAIF ID** (integer): ``ephem.get_body("1", use_horizons=True)`` → Ceres - **Minor planet number** (integer): ``ephem.get_body("433", use_horizons=True)`` → Eros - **Name** (string): ``ephem.get_body("Ceres", use_horizons=True)`` .. code-block:: python # Common asteroids ceres = ephem.get_body("1", use_horizons=True) # Ceres (dwarf planet) vesta = ephem.get_body("4", use_horizons=True) # Vesta juno = ephem.get_body("3", use_horizons=True) # Juno eros = ephem.get_body("433", use_horizons=True) # Eros apophis = ephem.get_body("99942", use_horizons=True) # Apophis bennu = ephem.get_body("101955", use_horizons=True) # Bennu Comets ~~~~~~ Comets are referenced by name: .. code-block:: python # Some well-known comets halley = ephem.get_body("Halley", use_horizons=True) neowise = ephem.get_body("C/2020 F3", use_horizons=True) # NEOWISE leone = ephem.get_body("67P", use_horizons=True) # Churyumov-Gerasimenko (short form) Spacecraft ~~~~~~~~~~ Many space probes and satellites are available: .. code-block:: python # Natural and artificial objects voyager1 = ephem.get_body("-31", use_horizons=True) # Voyager 1 voyager2 = ephem.get_body("-32", use_horizons=True) # Voyager 2 newhorizons = ephem.get_body("-98", use_horizons=True) # New Horizons probe parker = ephem.get_body("-96", use_horizons=True) # Parker Solar Probe juno = ephem.get_body("-61", use_horizons=True) # Juno orbiter **Note:** Spacecraft are referenced by negative NAIF IDs. Consult JPL's list of spacecraft IDs at `Horizons System `_ for a comprehensive list. Working with Constraints ------------------------ Horizons integration is fully supported in the constraint system, enabling observation planning for any Horizons-accessible body: .. code-block:: python from rust_ephem.constraints import SunConstraint, MoonConstraint # Set up ephemeris begin = datetime(2024, 6, 1, tzinfo=timezone.utc) end = datetime(2024, 6, 2, tzinfo=timezone.utc) ephem = re.TLEEphemeris(norad_id=25544, begin=begin, end=end) # Define constraint: body must be 45° from Sun AND 10° from Moon constraint = SunConstraint(min_angle=45) & MoonConstraint(min_angle=10) # Get visibility for Ceres visibility = constraint.evaluate_moving_body( ephemeris=ephem, body="1", # Ceres use_horizons=True # ← Enable Horizons fallback ) # Print visibility windows for window in visibility.visibility: print(f"Visible: {window.start_time} to {window.end_time}") # Check satisfaction statistics print(f"Total satisfied: {visibility.all_satisfied}") print(f"Per-sample satisfied: {visibility.constraint_array[:5]}") Advanced Examples ----------------- Asteroid Visibility During Approach ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Track an asteroid approaching Earth using Horizons: .. code-block:: python import numpy as np from datetime import datetime, timedelta, timezone # Define extended time range for close approach event begin = datetime(2029, 4, 1, tzinfo=timezone.utc) end = datetime(2029, 4, 14, tzinfo=timezone.utc) # Ground observatory (e.g., Arecibo) obs = re.GroundEphemeris( latitude=18.3461, longitude=-66.7527, height=496, begin=begin, end=end, step_size=3600 # Hourly steps ) # Define constraints for Apophis observation constraint = SunConstraint(min_angle=10) & MoonConstraint(min_angle=20) # Query Apophis during approach result = constraint.evaluate_moving_body( ephemeris=obs, body="99942", # Apophis use_horizons=True ) print(f"Apophis observable for {len(result.visibility)} window(s)") for window in result.visibility: print(f" {window.start_time} to {window.end_time}") Comparing Asteroid Positions Across Observers ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Compare how an asteroid's position changes from different ground stations: .. code-block:: python from astropy.coordinates import SkyCoord import astropy.units as u begin = datetime(2024, 9, 15, tzinfo=timezone.utc) end = datetime(2024, 9, 16, tzinfo=timezone.utc) # Two observatories keck = re.GroundEphemeris( latitude=19.8267, longitude=-155.4730, height=4207, begin=begin, end=end, step_size=60 ) vlt = re.GroundEphemeris( latitude=-24.6276, longitude=-70.4035, height=2635, begin=begin, end=end, step_size=60 ) # Get Ceres from each location ceres_keck = keck.get_body("1", use_horizons=True) ceres_vlt = vlt.get_body("1", use_horizons=True) # Calculate parallax effect sep = ceres_keck.separation(ceres_vlt) print(f"Maximum parallax: {sep[0].max():.4f} degrees") Tracking Multiple Asteroids ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Monitor visibility for a set of potentially hazardous asteroids: .. code-block:: python # PHAs (Potentially Hazardous Asteroids) phas = { "433": "Eros", "1862": "Apollo", "2062": "Aten", "3122": "Florence", "99942": "Apophis", } ephem = re.TLEEphemeris(norad_id=25544, begin=begin, end=end) constraint = SunConstraint(min_angle=30) for naif_id, name in phas.items(): result = constraint.evaluate_moving_body( ephemeris=ephem, body=naif_id, use_horizons=True ) window_count = len(result.visibility) print(f"{name:15s} ({naif_id:5s}): {window_count} visibility window(s)") Performance Considerations -------------------------- Network Requirements ~~~~~~~~~~~~~~~~~~~~ Horizons queries require an active internet connection. Each query makes an HTTP request to NASA's servers. Queries are typically fast (< 1 second), but: - **Network latency** affects query time - **Large time ranges** may take longer to compute - **Query caching** is not implemented (each call hits the network) For repeated queries of the same body and time range, consider caching the results: .. code-block:: python # Cache a lookup ceres_pv = ephem.get_body_pv("1", use_horizons=True) # Reuse the cached result multiple times sun_dist = np.linalg.norm(ceres_pv.position[0]) print(f"Distance: {sun_dist:.0f} km") Time Range Limitations ~~~~~~~~~~~~~~~~~~~~~~ Horizons has limitations on how far into the past and future it can compute: - **Well-established bodies** (planets, Moon, major asteroids): ±thousands of years - **Recently discovered objects** (comets, new asteroids): Much shorter ranges - **Spacecraft**: Limited by mission duration and tracking data If you query beyond the supported range, Horizons will raise an error. Start with smaller time ranges and expand if successful. Accuracy ~~~~~~~~ Horizons positions are typically accurate to within a few kilometers for solar system bodies, with uncertainty increasing for: - Objects far in the past or future - Recently discovered bodies with fewer observations - Comets with uncertain orbital parameters For mission-critical applications, compare Horizons results with other sources or use higher-order accuracy models. Troubleshooting --------------- Body Not Found ~~~~~~~~~~~~~~ If you get a "body not found" error, verify: 1. **Check the body identifier** — Use JPL's `Horizons browser `_ to find the correct NAIF ID or name 2. **Check time range** — The body may not be computable during your time range 3. **Network connectivity** — Ensure your system has internet access 4. **Horizons service** — NASA's servers may occasionally be unavailable .. code-block:: python try: body = ephem.get_body("99999999", use_horizons=True) except Exception as e: print(f"Query failed: {e}") Slow Queries ~~~~~~~~~~~~ If Horizons queries are slow: 1. **Network latency** — Check your internet connection speed 2. **Large time range** — Reduce the step size or query shorter periods 3. **Server load** — Horizons may be experiencing high traffic; retry later For production applications querying many bodies, consider batching queries or using periodic pre-computation to avoid real-time network dependencies. Type Stub Support ----------------- Full type hints are provided for Horizons methods: .. code-block:: python from typing import Optional from rust_ephem import Ephemeris def track_asteroid( ephem: Ephemeris, asteroid_id: str, use_horizons: bool = True ) -> None: """Track an asteroid using SPICE or Horizons.""" body = ephem.get_body(asteroid_id, use_horizons=use_horizons) print(f"Position: {body[0]}") The ``.pyi`` stub files include ``use_horizons`` parameter documentation for IDE autocomplete and static type checkers (mypy, pyright, etc.). Integration with Constraint System ----------------------------------- Horizons support is seamlessly integrated into all constraint types: .. code-block:: python from rust_ephem.constraints import ( AirmassConstraint, EarthLimbConstraint, SunConstraint, MoonConstraint, MoonPhaseConstraint, ) constraint = ( SunConstraint(min_angle=30) & AirmassConstraint(max_airmass=2.0) & EarthLimbConstraint(min_angle=20) ) # Works with any Horizons-accessible body result = constraint.evaluate_moving_body( ephemeris=ephem, body="2", # Pallas asteroid use_horizons=True ) Limitations and Caveats ----------------------- 1. **Network Required** — Unlike SPICE kernel queries, Horizons lookups require internet connectivity 2. **No Caching** — Results are not cached; repeated queries recompute Consider implementing application-level caching if needed 3. **Time Range Constraints** — Some bodies (especially recently discovered ones) have limited computable time ranges 4. **Accuracy Varies** — Position accuracy depends on observational data for the body Check Horizons documentation for specific body accuracy estimates 5. **API Changes** — JPL Horizons is maintained by NASA; future API changes could affect compatibility (current API assumed stable) 6. **Spacecraft Tracking** — Spacecraft positions become unavailable once missions end or tracking ceases; consult NASA's list of active tracking objects Reference --------- **JPL Horizons System** - Main site: https://ssd.jpl.nasa.gov/horizons/ - Web interface: https://ssd.jpl.nasa.gov/horizons/basic.html - NAIF IDs: https://ssd.jpl.nasa.gov/?horizons - Body list: Search the Horizons database for any object **rustephem Implementation** - Horizons module: ``src/utils/horizons.rs`` - Rhorizons crate: https://crates.io/crates/rhorizons (async NASA JPL Horizons client) - Integration: ``src/utils/celestial.rs`` (``calculate_body_by_id_or_name`` function) **Related Documentation** - :doc:`ephemeris_get_body` — Basic body lookups - :doc:`planning_constraints` — Constraint system overview - :doc:`planning_visibility` — Visibility calculations