Use a JPL ephemeris to predict planet positions.
This package can load and use a Jet Propulsion Laboratory (JPL) ephemeris for predicting the position and velocity of a planet or other Solar System body. It only needs NumPy, which pip will automatically attempt to install alongside pyephem when you run:
$ pip install jplephem
Note that jplephem offers only the logic necessary to produce plain three-dimensional vectors. Most programmers interested in astronomy will want to look at Skyfield instead, which uses jplephem but converts the numbers into more traditional measurements like right ascension and declination.
Most users will use jplephem with the Satellite Planet Kernel (SPK) files that the NAIF facility at NASA JPL offers for use with their own SPICE toolkit. They have collected their most useful kernels beneath the directory:
To learn more about SPK files, the official SPK Required Reading document is available from the NAIF facility’s web site under the NASA JPL domain.
If you have downloaded a .bsp file and want to learn what ephemeris segments are stored inside of it, you can have jplephem print them out by invoking the module directly from the command line:
python -m jplephem de430.bsp
This will print out a summary identical to the one shown in the following section, but without requiring that you type and run any Python code.
The recent DE430 ephemeris is a useful starting point. It weighs in at 115 MB, but provides predictions across the generous range of years 1550–2650:
After the kernel has downloaded, you can use jplephem to load this SPK file and learn about the segments it offers:
>>> from jplephem.spk import SPK >>> kernel = SPK.open('de430.bsp') >>> print(kernel) File type DAF/SPK and format LTL-IEEE with 14 segments: 2287184.50..2688976.50 Solar System Barycenter (0) -> Mercury Barycenter (1) 2287184.50..2688976.50 Solar System Barycenter (0) -> Venus Barycenter (2) 2287184.50..2688976.50 Solar System Barycenter (0) -> Earth Barycenter (3) 2287184.50..2688976.50 Solar System Barycenter (0) -> Mars Barycenter (4) 2287184.50..2688976.50 Solar System Barycenter (0) -> Jupiter Barycenter (5) 2287184.50..2688976.50 Solar System Barycenter (0) -> Saturn Barycenter (6) 2287184.50..2688976.50 Solar System Barycenter (0) -> Uranus Barycenter (7) 2287184.50..2688976.50 Solar System Barycenter (0) -> Neptune Barycenter (8) 2287184.50..2688976.50 Solar System Barycenter (0) -> Pluto Barycenter (9) 2287184.50..2688976.50 Solar System Barycenter (0) -> Sun (10) 2287184.50..2688976.50 Earth Barycenter (3) -> Moon (301) 2287184.50..2688976.50 Earth Barycenter (3) -> Earth (399) 2287184.50..2688976.50 Mercury Barycenter (1) -> Mercury (199) 2287184.50..2688976.50 Venus Barycenter (2) -> Venus (299)
Each segment of the file lets you predict the position of an object with respect to some other reference point. If you want the coordinates of Mars at 2457061.5 (2015 February 8) with respect to the center of the solar system, this ephemeris only requires you to take a single step:
>>> position = kernel[0,4].compute(2457061.5) >>> print(position) [ 2.05700211e+08 4.25141646e+07 1.39379183e+07]
But learning the position of Mars with respect to the Earth takes three steps, from Mars to the Solar System barycenter to the Earth-Moon barycenter and finally to Earth itself:
>>> position = kernel[0,4].compute(2457061.5) >>> position -= kernel[0,3].compute(2457061.5) >>> position -= kernel[3,399].compute(2457061.5) >>> print(position) [ 3.16065185e+08 -4.67929557e+07 -2.47554111e+07]
You can see that the output of this ephemeris is in kilometers. If you use another ephemeris, check its documentation to be sure of the units that it employs.
If you supply the date as a NumPy array, then each component that is returned will itself be a vector as long as your date:
>>> import numpy as np >>> jd = np.array([2457061.5, 2457062.5, 2457063.5, 2457064.5]) >>> position = kernel[0,4].compute(jd) >>> print(position) [[ 2.05700211e+08 2.05325363e+08 2.04928663e+08 2.04510189e+08] [ 4.25141646e+07 4.45315179e+07 4.65441136e+07 4.85517457e+07] [ 1.39379183e+07 1.48733243e+07 1.58071381e+07 1.67392630e+07]]
Some ephemerides include velocity inline by returning a 6-vector instead of a 3-vector. For an ephemeris that does not, you can ask for the Chebyshev polynomial to be differentiated to produce a velocity, which is delivered as a second return value:
>>> position, velocity = kernel[0,4].compute_and_differentiate(2457061.5) >>> print(position) [ 2.05700211e+08 4.25141646e+07 1.39379183e+07] >>> print(velocity) [ -363896.06287889 2019662.99596519 936169.77271558]
Here are a few details for people ready to go beyond the high-level API provided above and read through the code to learn more.
Instead of reading an entire ephemeris into memory, jplephem memory-maps the underlying file so that the operating system can efficiently page into RAM only the data that your code is using.
Once the metadata has been parsed from the binary SPK file, the polynomial coefficients themselves are loaded by building a NumPy array object that has access to the raw binary file contents. Happily, NumPy already knows how to interpret a packed array of double-precision floats. You can learn about the underlying DAF “Double Precision Array File” format, in case you ever need to open other such array files in Python, through the DAF class in the module jplephem.daf.
An SPK file is made of segments. When you first create an SPK kernel object k, it examines the file and creates a list of Segment objects that it keeps in a list under an attribute named k.segments which you are free to examine in your own code by looping over it.
There is more information about each segment beyond the one-line summary that you get when you print out the SPK file, which you can see by asking the segment to print itself verbosely:
>>> segment = kernel[3,399] >>> print(segment.describe()) 2287184.50..2688976.50 Earth Barycenter (3) -> Earth (399) frame=1 data_type=2 source=DE-0430LE-0430
Each Segment loaded from the kernel has a number of attributes that are loaded from the SPK file:
>>> help(segment) Help on Segment in module jplephem.spk object: ... | segment.source - official ephemeris name, like 'DE-0430LE-0430' | segment.start_second - initial epoch, as seconds from J2000 | segment.end_second - final epoch, as seconds from J2000 | segment.start_jd - start_second, converted to a Julian Date | segment.end_jd - end_second, converted to a Julian Date | segment.center - integer center identifier | segment.target - integer target identifier | segment.frame - integer frame identifier | segment.data_type - integer data type identifier | segment.start_i - index where segment starts | segment.end_i - index where segment ends ...
The square-bracket lookup mechanism kernel[3,399] is a non-standard convenience that returns only the last matching segment in the file. While the SPK standard does say that the last segment takes precedence, it also says that earlier segments for a particular center-target pair should be fallen back upon for dates that the last segment does not cover. So, if you ever tackle a complicated kernel, you will need to implement fallback rules that send some dates to the final segment for a given center and target, but that send other dates to earlier segments that are qualified to cover them.
If you are accounting for light travel time and require repeated computation of the position, but then need the velocity at the end, and want to avoid repeating the expensive position calculation, then try out the segment.generate() method - it will let you ask for the position, and then only proceed to the velocity once you are sure that the light-time error is now small enough.
Since all modern Julian dates are numbers larger than 2.4 million, a standard 64-bit Python or NumPy float necessarily leaves only a limited number of bits available for the fractional part. Technical Note 2011-02 from the United States Naval Observatory’s Astronomical Applications Department suggests that the precision possible with a 64-bit floating point Julian date is around 20.1 µs.
If you need to supply times and receive back planetary positions with greater precision than 20.1 µs, then you have two options.
First, you can supply times using the special float96 NumPy type, which is also aliased to the name longfloat. If you provide either a float96 scalar or a float96 array as your tdb parameter to any jplephem routine, you should get back a high-precision result.
Second, you can split your date or dates into two pieces, and supply them as a pair of arguments two tdb and tdb2. One popular approach for how to split your date is to use the tdb float for the integer Julian date, and tdb2 for the fraction that specifies the time of day. Nearly all jplephem routines accept this optional tdb2 argument if you wish to provide it, thanks to the work of Marten van Kerkwijk!
Back before I learned about SPICE and SPK files, I had run across the text-file formatted JPL ephemerides at:
I laboriously assembled the data in these text files into native NumPy array files and wrapped them each in a Python package so that users could install an ephemeris with a simple command:
pip install de421
If you want to use one of these pip-installable ephemerides, you will be using a slightly older API, and will lose the benefit of the efficient memory-mapping that the newer SPK code performs. With the old API, here is how you would load DE421 and compute a position, given a barycentric dynamical time expressed as a Julian date:
import de421 from jplephem import Ephemeris eph = Ephemeris(de421) x, y, z = eph.position('mars', 2444391.5) # 1980.06.01
For more information about the legacy API, consult the jplephem entry on PyPI for the final release of the 1.x series:
The ephemerides that were made available as Python packages (the following links explain the differences between them) are:
You can report any issues, bugs, or problems at the GitHub repository:
2016 December 19 — Version 2.6
2015 November 9 — Version 2.5
2015 November 1 — Version 2.4
2015 August 16 — Version 2.3
2015 August 5 — Version 2.2
2015 February 24 — Version 2.1
2015 February 8 — Version 2.0
2013 November 26 — Version 1.2
2013 July 10 — Version 1.1
2013 January 18 — Version 1.0
The Jet Propulsion Laboratory’s “Solar System Dynamics” page introduces the various options for doing solar system position computations: http://ssd.jpl.nasa.gov/?ephemerides
The plain ASCII format element sets from which the jplephem Python ephemeris packages are built, along with documentation, can be found at: ftp://ssd.jpl.nasa.gov/pub/eph/planets/ascii/
Equivalent FORTRAN code for using the ephemerides be found at the same FTP site: ftp://ssd.jpl.nasa.gov/pub/eph/planets/fortran/