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.
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:
# 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:
Check SPICE kernels first — If the body is found in your kernel, use it
Fall back to Horizons — If not found in SPICE, query JPL Horizons
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.
# 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)
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)→ CeresMinor planet number (integer):
ephem.get_body("433", use_horizons=True)→ ErosName (string):
ephem.get_body("Ceres", use_horizons=True)
# 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:
# 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:
# 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:
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:
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:
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:
# 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:
# 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:
Check the body identifier — Use JPL’s Horizons browser to find the correct NAIF ID or name
Check time range — The body may not be computable during your time range
Network connectivity — Ensure your system has internet access
Horizons service — NASA’s servers may occasionally be unavailable
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:
Network latency — Check your internet connection speed
Large time range — Reduce the step size or query shorter periods
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:
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:
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
Network Required — Unlike SPICE kernel queries, Horizons lookups require internet connectivity
No Caching — Results are not cached; repeated queries recompute Consider implementing application-level caching if needed
Time Range Constraints — Some bodies (especially recently discovered ones) have limited computable time ranges
Accuracy Varies — Position accuracy depends on observational data for the body Check Horizons documentation for specific body accuracy estimates
API Changes — JPL Horizons is maintained by NASA; future API changes could affect compatibility (current API assumed stable)
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.rsRhorizons crate: https://crates.io/crates/rhorizons (async NASA JPL Horizons client)
Integration:
src/utils/celestial.rs(calculate_body_by_id_or_namefunction)
- Related Documentation
Using get_body for Celestial Bodies — Basic body lookups
Using Constraints — Constraint system overview
Calculating Visibility Windows — Visibility calculations