High-Precision Ephemeris

This example demonstrates using the JPL Development Ephemeris (DE) to compute high-precision positions of the Sun, Moon, and planets.

Overview

Planetary ephemerides provide high-accuracy positions essential for:

  • Deep space navigation: Spacecraft trajectory planning

  • Astronomy: Telescope pointing and observation scheduling

  • Satellite operations: Eclipse and conjunction predictions

  • Time systems: Planetary aberration corrections

Ephemeris Versions

DE405 (1997)

JPL Planetary Ephemeris covering 1997-2050

DE430 (2013)

Extended coverage from 1550-2650

DE440 (2020)

Latest JPL ephemeris with improved accuracy

Examples Covered

Sun Position
  • Heliocentric distance variation (Earth’s orbital eccentricity)

  • Perihelion and aphelion distances

  • Position in ICRF (International Celestial Reference Frame)

Moon Position
  • Earth-centered position and velocity

  • Perigee and apogee variations

  • Lunar orbital ellipticity

Planetary Positions
  • All major planets: Mercury through Neptune

  • Heliocentric coordinates

  • Ecliptic longitude and latitude

Solar System Barycenter
  • Center of mass of the solar system

  • Jupiter’s gravitational influence

  • Reference for high-precision astrometry

Reference Frames
  • ICRF (default inertial frame)

  • Ecliptic frame transformations

  • Earth-centered coordinates

Code Highlights

The example demonstrates:

  • Sun position queries with sun_position()

  • Moon position queries with moon_position()

  • Planet positions with planet_position()

  • Barycenter calculations with barycenter_position()

  • Julian date conversions with jd_to_cal()

  • Ephemeris version selection with DEEphemeris()

Source Code

  1"""High-precision ephemeris queries for celestial bodies.
  2
  3This example demonstrates using the JPL Development Ephemeris (DE) to compute
  4high-precision positions of the Sun, Moon, and planets. The ephemeris kernel
  5data provides accuracy to within kilometers for major solar system bodies.
  6
  7The example covers:
  81. Basic Sun and Moon position queries
  92. Planet position queries for all major planets
 103. Barycenter calculations for multi-body systems
 114. Different reference frame options (ICRF, ecliptic, Earth-centered)
 125. Comparing different ephemeris versions (DE405, DE430, DE440)
 136. Computing distances and velocities
 14"""
 15
 16import os
 17from pathlib import Path
 18
 19import numpy as np
 20import plotly.graph_objects as go
 21from plotly.subplots import make_subplots
 22
 23from pytcl.astronomical import (
 24    DEEphemeris,
 25    barycenter_position,
 26    jd_to_cal,
 27    moon_position,
 28    planet_position,
 29    sun_position,
 30)
 31from pytcl.astronomical.relativity import AU
 32
 33SHOW_PLOTS = os.environ.get("PYTCL_SHOW_PLOTS", "1") != "0"
 34OUTPUT_DIR = Path("docs/_static/images/examples")
 35OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
 36
 37
 38def plot_sun_earth_moon_positions(
 39    jd: float, title: str = "Sun-Earth-Moon System Configuration"
 40) -> None:
 41    """Plot the Sun-Earth-Moon geometry at a given epoch.
 42
 43    The two distances involved differ by a factor of about 390, so a single
 44    3D scene cannot show both: at a scale where the Earth-Sun line is
 45    visible, the Moon sits on top of the Earth. Hence two panels, one per
 46    scale.
 47    """
 48    # sun_position and moon_position return barycentric (SSB) ICRF positions
 49    # in AU. Asking moon_position for the 'earth_centered' frame gives the
 50    # Moon relative to the Earth, so the Earth's own barycentric position is
 51    # exactly the difference of the two.
 52    r_sun, _ = sun_position(jd)
 53    r_moon_bary, _ = moon_position(jd)
 54    r_moon_geo, _ = moon_position(jd, "earth_centered")
 55    r_earth = r_moon_bary - r_moon_geo
 56
 57    km_per_au = AU / 1e3
 58    earth_sun_au = float(np.linalg.norm(r_earth - r_sun))
 59    earth_moon_km = float(np.linalg.norm(r_moon_geo) * km_per_au)
 60
 61    fig = make_subplots(
 62        rows=1,
 63        cols=2,
 64        specs=[[{"type": "scatter3d"}, {"type": "scatter3d"}]],
 65        subplot_titles=(
 66            f"Barycentric view — Earth-Sun {earth_sun_au:.4f} AU",
 67            f"Geocentric view — Earth-Moon {earth_moon_km:,.0f} km",
 68        ),
 69    )
 70
 71    # Panel 1: solar-system scale, positions relative to the barycenter.
 72    fig.add_trace(
 73        go.Scatter3d(
 74            x=[r_sun[0]],
 75            y=[r_sun[1]],
 76            z=[r_sun[2]],
 77            mode="markers+text",
 78            marker=dict(size=12, color="gold"),
 79            text=["Sun"],
 80            textposition="top center",
 81            name="Sun",
 82            hovertemplate=f"<b>Sun</b><br>{np.linalg.norm(r_sun):.5f} AU "
 83            "from barycenter<extra></extra>",
 84        ),
 85        row=1,
 86        col=1,
 87    )
 88    fig.add_trace(
 89        go.Scatter3d(
 90            x=[r_earth[0]],
 91            y=[r_earth[1]],
 92            z=[r_earth[2]],
 93            mode="markers+text",
 94            marker=dict(size=8, color="royalblue"),
 95            text=["Earth"],
 96            textposition="top center",
 97            name="Earth",
 98            hovertemplate=f"<b>Earth</b><br>{earth_sun_au:.5f} AU "
 99            "from the Sun<extra></extra>",
100        ),
101        row=1,
102        col=1,
103    )
104    fig.add_trace(
105        go.Scatter3d(
106            x=[r_sun[0], r_earth[0]],
107            y=[r_sun[1], r_earth[1]],
108            z=[r_sun[2], r_earth[2]],
109            mode="lines",
110            line=dict(color="orange", width=3),
111            hoverinfo="skip",
112            showlegend=False,
113        ),
114        row=1,
115        col=1,
116    )
117
118    # Panel 2: Earth-Moon scale, Earth at the origin, axes in thousands of km.
119    moon_kkm = r_moon_geo * km_per_au / 1e3
120    fig.add_trace(
121        go.Scatter3d(
122            x=[0.0],
123            y=[0.0],
124            z=[0.0],
125            mode="markers+text",
126            marker=dict(size=10, color="royalblue"),
127            text=["Earth"],
128            textposition="top center",
129            name="Earth (origin)",
130            hovertemplate="<b>Earth</b><extra></extra>",
131        ),
132        row=1,
133        col=2,
134    )
135    fig.add_trace(
136        go.Scatter3d(
137            x=[moon_kkm[0]],
138            y=[moon_kkm[1]],
139            z=[moon_kkm[2]],
140            mode="markers+text",
141            marker=dict(size=6, color="lightgray"),
142            text=["Moon"],
143            textposition="top center",
144            name="Moon",
145            hovertemplate=f"<b>Moon</b><br>{earth_moon_km:,.0f} km "
146            "from the Earth<extra></extra>",
147        ),
148        row=1,
149        col=2,
150    )
151    fig.add_trace(
152        go.Scatter3d(
153            x=[0.0, moon_kkm[0]],
154            y=[0.0, moon_kkm[1]],
155            z=[0.0, moon_kkm[2]],
156            mode="lines",
157            line=dict(color="lightblue", width=2, dash="dash"),
158            hoverinfo="skip",
159            showlegend=False,
160        ),
161        row=1,
162        col=2,
163    )
164
165    fig.update_layout(
166        title=title,
167        scene=dict(
168            xaxis_title="X (AU)",
169            yaxis_title="Y (AU)",
170            zaxis_title="Z (AU)",
171            aspectmode="data",
172        ),
173        scene2=dict(
174            xaxis_title="X (1000 km)",
175            yaxis_title="Y (1000 km)",
176            zaxis_title="Z (1000 km)",
177            aspectmode="data",
178        ),
179        hovermode="closest",
180        height=600,
181        showlegend=True,
182    )
183
184    if SHOW_PLOTS:
185        fig.show()
186    else:
187        fig.write_html(
188            str(OUTPUT_DIR / "ephemeris_demo.html"),
189            include_plotlyjs="cdn",
190            div_id="ephemeris_demo",
191        )
192
193
194def plot_orbital_distances(
195    body_name: str, jd_start: float, num_points: int = 100
196) -> None:
197    """Plot distance variation over one year."""
198    if body_name.lower() == "sun":
199        pos_func = sun_position
200        title = "Sun's Distance from Earth (Eccentricity Effect)"
201    elif body_name.lower() == "moon":
202        pos_func = moon_position
203        title = "Moon's Distance from Earth (Orbital Variation)"
204    else:
205
206        def pos_func(jd: float) -> tuple:
207            """Get planet position for given Julian date."""
208            return planet_position(body_name, jd)
209
210        title = f"{body_name.capitalize()}'s Distance from Earth"
211
212    jd_array = np.linspace(jd_start, jd_start + 365.25, num_points)
213    distances = []
214    dates = []
215
216    for jd in jd_array:
217        r, _ = pos_func(jd)
218        distances.append(
219            np.linalg.norm(r) / AU
220            if body_name.lower() != "moon"
221            else np.linalg.norm(r) / 1e6
222        )
223        year, month, day, _, _, _ = jd_to_cal(jd)
224        dates.append(f"{year:04d}-{month:02d}-{day:02d}")
225
226    fig = go.Figure()
227
228    fig.add_trace(
229        go.Scatter(
230            x=dates,
231            y=distances,
232            mode="lines+markers",
233            name=f"{body_name} Distance",
234            line=dict(color="steelblue", width=2),
235            marker=dict(size=4),
236            hovertemplate="<b>Date:</b> %{x}<br><b>Distance:</b> %{y:.3f} "
237            + ("AU" if body_name.lower() != "moon" else "km")
238            + "<extra></extra>",
239        )
240    )
241
242    y_label = "Distance (AU)" if body_name.lower() != "moon" else "Distance (km)"
243    fig.update_layout(
244        title=title,
245        xaxis_title="Date",
246        yaxis_title=y_label,
247        hovermode="x unified",
248        height=500,
249        plot_bgcolor="rgba(240,240,240,0.5)",
250        xaxis_tickangle=-45,
251    )
252
253    if SHOW_PLOTS:
254        fig.show()
255    else:
256        fig.write_html(
257            str(OUTPUT_DIR / "ephemeris_demo_distance.html"),
258            include_plotlyjs="cdn",
259            div_id="ephemeris_demo_distance",
260        )
261
262
263def example_sun_position():
264    """Query the Sun's position at specific times."""
265    print("=" * 70)
266    print("EXAMPLE 1: Sun Position Queries")
267    print("=" * 70)
268
269    # Create ephemeris object (defaults to DE440)
270    eph = DEEphemeris()
271
272    # J2000.0 epoch (January 1, 2000, 12:00 UT)
273    jd_j2000 = 2451545.0
274
275    # Query Sun's position
276    r_sun, v_sun = sun_position(jd_j2000)
277
278    print(f"\nJ2000.0 Epoch: JD {jd_j2000}")
279    print(f"Sun Position (ICRF):")
280    print(f"  X: {r_sun[0]:15.3f} m = {r_sun[0] / AU:10.6f} AU")
281    print(f"  Y: {r_sun[1]:15.3f} m = {r_sun[1] / AU:10.6f} AU")
282    print(f"  Z: {r_sun[2]:15.3f} m = {r_sun[2] / AU:10.6f} AU")
283    print(f"  Distance: {np.linalg.norm(r_sun) / AU:.6f} AU")
284
285    print(f"\nSun Velocity (ICRF):")
286    print(f"  VX: {v_sun[0]:12.3f} m/s")
287    print(f"  VY: {v_sun[1]:12.3f} m/s")
288    print(f"  VZ: {v_sun[2]:12.3f} m/s")
289    print(f"  Speed: {np.linalg.norm(v_sun):.3f} m/s")
290
291    # Compute Sun's distance variation over a year
292    print("\n" + "-" * 70)
293    print("Sun's Distance Throughout 2000:")
294    print("-" * 70)
295
296    distances = []
297    julian_dates = np.linspace(jd_j2000, jd_j2000 + 365.25, 13)
298
299    for jd in julian_dates:
300        year, month, day, _, _, _ = jd_to_cal(jd)
301        r, _ = sun_position(jd)
302        dist_au = np.linalg.norm(r) / AU
303        distances.append(dist_au)
304        print(f"  {year:4d}-{month:2d}-{day:2d}  Distance: {dist_au:.6f} AU")
305
306    print(f"\nPerigee (minimum): {min(distances):.6f} AU")
307    print(f"Apogee (maximum):  {max(distances):.6f} AU")
308    print(
309        f"Variation:         {max(distances) - min(distances):.6f} AU "
310        f"({100 * (max(distances) - min(distances)) / np.mean(distances):.2f}%)"
311    )
312
313    # Visualize the Sun's orbital distance throughout the year
314    plot_orbital_distances("sun", jd_j2000, num_points=365)
315
316
317def example_moon_position():
318    """Query the Moon's position and properties."""
319    print("\n" + "=" * 70)
320    print("EXAMPLE 2: Moon Position Queries")
321    print("=" * 70)
322
323    jd_j2000 = 2451545.0
324
325    # Query Moon's position
326    r_moon, v_moon = moon_position(jd_j2000)
327
328    print(f"\nJ2000.0 Epoch: JD {jd_j2000}")
329    print(f"Moon Position (Earth-centered ICRF):")
330    print(f"  X: {r_moon[0]:12.3f} m = {r_moon[0] / 1e6:10.1f} km")
331    print(f"  Y: {r_moon[1]:12.3f} m = {r_moon[1] / 1e6:10.1f} km")
332    print(f"  Z: {r_moon[2]:12.3f} m = {r_moon[2] / 1e6:10.1f} km")
333    print(f"  Distance: {np.linalg.norm(r_moon) / 1e6:.1f} km")
334
335    print(f"\nMoon Velocity (Earth-centered ICRF):")
336    print(
337        f"  Speed: {np.linalg.norm(v_moon):.3f} m/s = {np.linalg.norm(v_moon) * 86400 / 1e3:.1f} km/day"
338    )
339
340    # Lunar distance variation (orbital ellipticity)
341    print("\n" + "-" * 70)
342    print("Moon's Distance Variation (showing orbital ellipticity):")
343    print("-" * 70)
344
345    distances = []
346    times = np.linspace(0, 27.32, 27)  # ~lunar month in days
347    julian_dates = jd_j2000 + times
348
349    for jd in julian_dates:
350        r, _ = moon_position(jd)
351        dist_km = np.linalg.norm(r) / 1e6
352        distances.append(dist_km)
353
354    print(f"Perigee (closest):  {min(distances):.1f} km")
355    print(f"Apogee (farthest):  {max(distances):.1f} km")
356    print(f"Mean distance:      {np.mean(distances):.1f} km")
357    print(
358        f"Variation:          {max(distances) - min(distances):.1f} km "
359        f"({100 * (max(distances) - min(distances)) / np.mean(distances):.1f}%)"
360    )
361
362    # Visualize the Sun-Earth-Moon configuration
363    jd_j2000 = 2451545.0
364    plot_sun_earth_moon_positions(jd_j2000)
365
366    # Visualize the Moon's orbital distance variation
367    plot_orbital_distances("moon", jd_j2000, num_points=365)
368
369
370def example_planet_positions():
371    """Query positions of all major planets."""
372    print("\n" + "=" * 70)
373    print("EXAMPLE 3: Planetary Positions")
374    print("=" * 70)
375
376    jd_j2000 = 2451545.0
377
378    planets = [
379        "mercury",
380        "venus",
381        "mars",
382        "jupiter",
383        "saturn",
384        "uranus",
385        "neptune",
386    ]
387
388    print(f"\nPlanetary Heliocentric Positions at J2000.0:")
389    print("-" * 70)
390    print(f"{'Planet':<10} {'Distance (AU)':<16} {'Longitude':<12} {'Latitude':<12}")
391    print("-" * 70)
392
393    # Ask for the ecliptic frame, since the table reports ecliptic longitude
394    # and latitude. Reading those angles off the ICRF (equatorial) vectors
395    # instead put Mercury at -25 degrees latitude, which no planet can reach:
396    # the ecliptic latitudes of the planets stay within about 7 degrees.
397    r_sun_ecl, _ = sun_position(jd_j2000, "ecliptic")
398
399    for planet_name in planets:
400        r_bary, _ = planet_position(planet_name, jd_j2000, "ecliptic")
401
402        # planet_position returns barycentric positions, already in AU. The
403        # heliocentric vector is the difference from the Sun; the distance
404        # must not be divided by AU again, which is what left every entry in
405        # this column reading 0.000000.
406        r = r_bary - r_sun_ecl
407        dist_au = np.linalg.norm(r)
408
409        lon = np.arctan2(r[1], r[0])
410        lat = np.arcsin(r[2] / dist_au)
411
412        print(
413            f"{planet_name:<10} {dist_au:<16.6f} "
414            f"{np.degrees(lon):>10.2f}° {np.degrees(lat):>10.2f}°"
415        )
416
417
418def example_barycenter():
419    """Compute solar system barycenter positions."""
420    print("\n" + "=" * 70)
421    print("EXAMPLE 4: Solar System Barycenter")
422    print("=" * 70)
423
424    jd_j2000 = 2451545.0
425
426    # Get Sun position relative to solar system barycenter
427    r_barycenter, v_barycenter = barycenter_position("sun", jd_j2000)
428
429    print(f"\nSolar System Barycenter at J2000.0:")
430    print(f"  X: {r_barycenter[0]:12.3f} m")
431    print(f"  Y: {r_barycenter[1]:12.3f} m")
432    print(f"  Z: {r_barycenter[2]:12.3f} m")
433    print(f"  Distance from origin: {np.linalg.norm(r_barycenter):.3f} m")
434    print(f"  Velocity magnitude: {np.linalg.norm(v_barycenter):.6f} m/s")
435
436    # Compare with Jupiter position
437    r_jupiter, _ = planet_position("jupiter", jd_j2000)
438    print(f"\nComparison with Jupiter position:")
439    print(
440        f"  Jupiter distance from barycenter: {np.linalg.norm(r_jupiter - r_barycenter):.3f} m"
441    )
442    print(f"  This shows Jupiter's significant gravitational influence")
443
444
445def example_frame_transformations():
446    """Demonstrate reference frame options."""
447    print("\n" + "=" * 70)
448    print("EXAMPLE 5: Reference Frame Transformations")
449    print("=" * 70)
450
451    jd_j2000 = 2451545.0
452    eph = DEEphemeris()
453
454    print(f"\nSun's position in different reference frames at J2000.0:")
455    print("-" * 70)
456
457    # ICRF (default)
458    r_icrf, _ = eph.sun_position(jd_j2000, frame="ICRF")
459    print(f"ICRF Frame (International Celestial Reference Frame):")
460    print(f"  X: {r_icrf[0] / AU:10.6f} AU")
461    print(f"  Y: {r_icrf[1] / AU:10.6f} AU")
462    print(f"  Z: {r_icrf[2] / AU:10.6f} AU")
463
464    # Ecliptic frame
465    try:
466        r_ecliptic, _ = eph.sun_position(jd_j2000, frame="ecliptic")
467        print(f"\nEcliptic Frame:")
468        print(f"  X: {r_ecliptic[0] / AU:10.6f} AU")
469        print(f"  Y: {r_ecliptic[1] / AU:10.6f} AU")
470        print(f"  Z: {r_ecliptic[2] / AU:10.6f} AU (small, as expected)")
471    except NotImplementedError:
472        print("\nEcliptic frame transformation would be applied here")
473
474
475def example_time_series():
476    """Generate time series of object positions (useful for animation)."""
477    print("\n" + "=" * 70)
478    print("EXAMPLE 6: Time Series for Visualization")
479    print("=" * 70)
480
481    jd_start = 2451545.0  # J2000
482    dates = jd_start + np.linspace(0, 365, 13)  # Monthly positions
483
484    sun_positions = []
485    moon_positions = []
486
487    print("\nComputing 12-month ephemeris...")
488    for jd in dates:
489        r_sun, _ = sun_position(jd)
490        r_moon, _ = moon_position(jd)
491        sun_positions.append(r_sun)
492        moon_positions.append(r_moon)
493
494    sun_positions = np.array(sun_positions)
495    moon_positions = np.array(moon_positions)
496
497    print(f"Computed {len(dates)} positions for Sun and Moon")
498    print(f"\nSun orbit statistics:")
499    print(
500        f"  Min distance: {np.min(np.linalg.norm(sun_positions, axis=1)) / AU:.6f} AU"
501    )
502    print(
503        f"  Max distance: {np.max(np.linalg.norm(sun_positions, axis=1)) / AU:.6f} AU"
504    )
505    print(f"  Orbit plane:")
506    print(f"    Min Z: {np.min(sun_positions[:, 2]) / AU:.8f} AU")
507    print(f"    Max Z: {np.max(sun_positions[:, 2]) / AU:.8f} AU")
508
509    print(f"\nMoon orbit statistics:")
510    print(
511        f"  Min distance: {np.min(np.linalg.norm(moon_positions, axis=1)) / 1e6:.1f} km"
512    )
513    print(
514        f"  Max distance: {np.max(np.linalg.norm(moon_positions, axis=1)) / 1e6:.1f} km"
515    )
516
517
518def example_ephemeris_versions():
519    """Compare different ephemeris versions."""
520    print("\n" + "=" * 70)
521    print("EXAMPLE 7: Ephemeris Version Comparison")
522    print("=" * 70)
523
524    jd_test = 2451545.0
525
526    print(f"\nNote: Different ephemeris versions available:")
527    print(f"  DE405: JPL Planetary Ephemeris, 1997-2050")
528    print(f"  DE430: JPL Planetary Ephemeris, 1550-2650")
529    print(f"  DE432s: Short version for limited time range")
530    print(f"  DE440: Latest JPL ephemeris, 1550-2650")
531    print(f"\nDepending on jplephem version, different kernels can be loaded")
532    print(f"Default used in this example: DE440 (if available)")
533
534    eph = DEEphemeris(version="DE440")
535    r_sun, _ = eph.sun_position(jd_test)
536    print(f"\nSun position (DE440 at J2000.0): {np.linalg.norm(r_sun) / AU:.6f} AU")
537
538
539if __name__ == "__main__":
540    """Run all examples."""
541    print("\n")
542    print("+" + "=" * 68 + "+")
543    print("|" + " " * 68 + "|")
544    print("|" + "  High-Precision Ephemeris Demonstrations".center(68) + "|")
545    print("|" + " " * 68 + "|")
546    print("+" + "=" * 68 + "+")
547
548    # Run examples
549    example_sun_position()
550    example_moon_position()
551    example_planet_positions()
552    example_barycenter()
553    example_frame_transformations()
554    example_time_series()
555    example_ephemeris_versions()
556
557    OUTPUT_DIR = Path("docs/_static/images/examples")
558    OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
559
560    print("\n" + "=" * 70)
561    print("All ephemeris examples completed successfully!")
562    print("=" * 70 + "\n")

Running the Example

python examples/ephemeris_demo.py

See Also