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

Running the Example

python examples/ephemeris_demo.py

See Also