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
Orbital Mechanics - Orbital propagation
Relativistic Effects - Relativistic corrections
Coordinate Systems - Coordinate transformations