Nutze Skyfield fuer praezisere Daemmerungsberechnung

This commit is contained in:
2026-07-13 19:35:39 +02:00
parent dc5292735f
commit 97357a88e0
1092 changed files with 370248 additions and 61 deletions
+575
View File
@@ -0,0 +1,575 @@
# -*- encoding: utf-8 -*-
"""Use a JPL ephemeris to predict planet positions.
Cite as: `Astrophysics Source Code Library, record ascl:1112.014
<https://ascl.net/1112.014>`_
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 currently supports binary SPK files (extension
``.bsp``) like `those distributed by the Jet Propulsion Laboratory
<https://ssd.jpl.nasa.gov/ftp/eph/planets/bsp/>`_ that are:
* **Type 2** — positions stored as Chebyshev polynomials, with velocity
derived by computing their derivative.
* **Type 3** — positions and velocities both stored explicitly as
Chebyshev polynomials.
* **Type 9** — a series of discrete positions and velocities, with
separate timestamps that do not need to be equally spaced. Currently
there is only support for linear interpolation: for Type 9 ephemerides
of polynomial degree 1, not of any higher degrees.
Note that even if an ephemeris isn’t one of the above types, you can
still use ``jplephem`` to read its text comment and list the segments
inside, using the subcommands ``comment`` and ``daf`` described below.
Installation
------------
The only third-party package that ``jplephem`` depends on is `NumPy
<http://www.numpy.org/>`_, which ``pip`` will automatically attempt to
install alongside ``pyephem`` when you run::
$ pip install jplephem
If you see NumPy compilation errors, then try downloading and installing
NumPy directly from `its web site <http://www.numpy.org/>`_ or simply
use a distribution of Python with science tools already installed, like
`Anaconda <http://continuum.io/downloads>`_.
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 <http://rhodesmill.org/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:
http://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/
To learn more about SPK files, the official `SPK Required Reading
<http://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/req/spk.html>`_
document is available from the NAIF facility’s web site under the NASA
JPL domain.
Command Line Tool
-----------------
If you have downloaded a ``.bsp`` file, you can run ``jplephem`` from
the command line to display the data inside of it::
python -m jplephem comment de421.bsp
python -m jplephem daf de421.bsp
python -m jplephem spk de421.bsp
python -m jplephem spk -v de421.bsp
You can also take a large ephemeris and produce a smaller excerpt by
limiting the range of dates that it covers::
python -m jplephem excerpt 2018/1/1 2018/4/1 de421.bsp excerpt421.bsp
The comment text of the output ephemeris is copied verbatim from the
input ephemeris, with the addition of a few lines of text at the top
that identify the output file as a mere excerpt, and record the dates
the user asked for.
You will get an error if your starting year is negative, because Unix
commands expect a list of options when they see a dash. The fix is to
provide a special argument ``--`` which says “I’m done passing options,
even if the next argument stars with a dash”::
python -m jplephem excerpt -- -800/1/1 800/1/1 de422.bsp excerpt422.bsp
You can also filter by the integer codes for the targets you need.
Unrecognized targets will not raise an error, to let you apply a master
list of targets to a whole series of SPK files that might or might not
each have all of the targets::
python -m jplephem excerpt --targets 1,2,3 2018/1/1 2018/4/1 de421.bsp excerpt421.bsp
If the input ephemeris is a URL, then ``jplephem`` will try to save
bandwidth by fetching only the blocks of the remote file that are
necessary to cover the dates you have specified. For example, the
Jupiter satellite ephemeris ``jup310.bsp`` is famously large, weighing
in a nearly a gigabyte. But if all you need are Jupiter's satellites
for a few months, you can download considerably less data::
$ python -m jplephem excerpt 2018/1/1 2018/4/1 \\
https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/satellites/jup365.bsp \\
excerpt.bsp
$ ls -lh excerpt.bsp
-rw-r----- 1 brandon brandon 1.2M Feb 11 13:36 excerpt.bsp
In this case only about one-thousandth of the ephemeris's data needed to
be downloaded.
Getting Started With DE421
--------------------------
The DE421 ephemeris is a useful starting point. It weighs in at 17 MB,
but provides predictions over the years 1900–2050:
https://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/a_old_versions/de421.bsp
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('de421.bsp')
>>> print(kernel)
File type DAF/SPK and format LTL-IEEE with 15 segments:
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Mercury Barycenter (1)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Venus Barycenter (2)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Earth Barycenter (3)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Mars Barycenter (4)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Jupiter Barycenter (5)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Saturn Barycenter (6)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Uranus Barycenter (7)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Neptune Barycenter (8)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Pluto Barycenter (9)
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Sun (10)
1899-07-29..2053-10-09 Type 2 Earth Barycenter (3) -> Moon (301)
1899-07-29..2053-10-09 Type 2 Earth Barycenter (3) -> Earth (399)
1899-07-29..2053-10-09 Type 2 Mercury Barycenter (1) -> Mercury (199)
1899-07-29..2053-10-09 Type 2 Venus Barycenter (2) -> Venus (299)
1899-07-29..2053-10-09 Type 2 Mars Barycenter (4) -> Mars (499)
Since the next few examples involve vector output, let’s tell NumPy to
make vector output attractive.
>>> import numpy as np
>>> np.set_printoptions(precision=3)
Each segment of the file lets you predict the position of one body with
respect to another for a given Julian date. A small routine is provided
to convert Gregorian calendar dates to Julian dates:
>>> from jplephem.calendar import compute_julian_date
>>> compute_julian_date(2015, 2, 8)
2457061.5
Here is how to compute the coordinates of Mars (target 4) relative to
the Solar System barycenter (target 0) at midnight 2015 February 8 TDB
(Barycentric Dynamical Time), using the Julian date we just computed:
>>> position = kernel[0,4].compute(2457061.5)
>>> print(position)
[2.057e+08 4.251e+07 1.394e+07]
By contrast, it takes three steps to learn the position of Mars with
respect to the Earth: from Mars to the Solar System barycenter, to the
Earth-Moon barycenter (3), and finally to Earth itself (399).
>>> position = kernel[0,4].compute(2457061.5)
>>> position -= kernel[0,3].compute(2457061.5)
>>> position -= kernel[3,399].compute(2457061.5)
>>> print(position)
[ 3.161e+08 -4.679e+07 -2.476e+07]
You can see that the output of this ephemeris DE421 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:
>>> jd = np.array([2457061.5, 2457062.5, 2457063.5, 2457064.5])
>>> position = kernel[0,4].compute(jd)
>>> print(position)
[[2.057e+08 2.053e+08 2.049e+08 2.045e+08]
[4.251e+07 4.453e+07 4.654e+07 4.855e+07]
[1.394e+07 1.487e+07 1.581e+07 1.674e+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.057e+08 4.251e+07 1.394e+07]
>>> print(velocity)
[-363896.059 2019662.996 936169.773]
The velocity will by default be distance traveled per day, in whatever
units for distance the ephemeris happens to use. To get a velocity per
second, simply divide by the number of seconds in a day:
>>> velocity_per_second = velocity / 86400.0
>>> print(velocity_per_second)
[-4.212 23.376 10.835]
Details of the API
------------------
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())
1899-07-29..2053-10-09 Type 2 Earth Barycenter (3) -> Earth (399)
frame=1 source=DE-0421LE-0421
* Each ``Segment`` loaded from the kernel has a number of attributes
that are loaded from the SPK file:
>>> from jplephem.spk import BaseSegment
>>> help(BaseSegment)
Help on class BaseSegment in module jplephem.spk:
...
| 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
...
* If you want to access the raw coefficients, use the segment
``load_array()`` method. It returns two floats and a NumPy array:
>>> initial_epoch, interval_length, coefficients = segment.load_array()
>>> print(coefficients.shape)
(3, 14080, 13)
* 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.
High-Precision Dates
--------------------
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
<http://jplephem.s3.amazonaws.com/JD_precision_test.pdf>`_.
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!
Support for Binary PCKs
-----------------------
You can also load and produce rotation matrices from a binary PCK file.
Its segments are available through the ``segments`` attributes of the
returned object.
>>> from jplephem.pck import PCK
>>> p = PCK.open('moon_pa_de421_1900-2050.bpc')
>>> p.segments[0].body
31006
>>> p.segments[0].frame
1
>>> p.segments[0].data_type
2
Given a solary system barycenter Julian date, the segment will return
the three angles necessary to build a rotation matrix: right ascension
of the pole, declination of the pole, and cumulative rotation of the
body’s axis. Typically these will all be in radians.
>>> tdb = 2454540.34103
>>> print(p.segments[0].compute(tdb, 0.0, False))
[3.928e-02 3.878e-01 3.253e+03]
You can ask for velocity as well.
>>> r, v = p.segments[0].compute(tdb, 0.0, True)
>>> print(r)
[3.928e-02 3.878e-01 3.253e+03]
>>> print(v)
[6.707e-09 4.838e-10 2.655e-06]
Closing an ephemeris
--------------------
To release all open files and memory maps associated with an ephemeris,
call its ``close()`` method.
>>> kernel.close()
>>> p.close()
Reporting issues
----------------
You can report any issues, bugs, or problems at the GitHub repository:
https://github.com/brandon-rhodes/python-jplephem/
Changelog
---------
**2025 June 22 — Version 2.23**
* An ephemeris created with the ``excerpt`` command, instead of simply
copying verbatim the comments area of the original ephemeris, now adds
text declaring “This is an ephemeris excerpt created by jplephem” and
recording the dates that the user asked for.
* The segments of an excerpt ephemeris now advertise exactly the start
date and end date that the user asked for, even if the underlying
polynomials cover a wider range of dates.
* A new ``-v`` (“verbose”) command-line option to the ``spk``
sub-command prints not only each segment’s descriptor, but the
dimensions and date range of its underlying polynomial array.
**2024 April 24 — Version 2.22**
* When printed, segments now print their start and end dates using the
Gregorian calendar instead of printing raw Julian dates.
* A small ``compute_julian_date`` routine is now provided for converting
calendar dates into Julian dates.
* Fixed the text of the ``ValueError`` that is raised when the PCK
segment ``compute()`` method is given an out-of-range date; it was
reporting incorrectly large numbers for the Julian date range, because
a PCK counts time using seconds before or after J2000, not years.
**2023 December 1 — Version 2.21**
* Tweaked an import to avoid a fatal exception under Python 2, in case
anyone is still using it.
**2023 November 13 — Version 2.20**
* Each segment is now protected by a lock, in case two threads
simultaneously trigger the code that performs the initial load of the
segment’s data; the symptom was a rare exception ``ValueError: cannot
reshape array``.
**2023 September 6 — Version 2.19**
* Fixed a bug in the ``excerpt`` command that was causing it to truncate
its output when the input ephemeris had more than about two dozen
segments. The command’s output should now include all matching
segments from even a very large ephemeris.
* Fixed the ``excerpt`` command so the calendar dates specified on the
command line produce Julian dates ending with the fraction ``.5``,
which makes excerpt endpoints more exact.
**2022 September 28 — Version 2.18**
* Added support for big-endian processors, and created a GitHub Actions
CI build that includes both a big- and a little-endian architecture.
**2021 December 31 — Version 2.17**
* Fixed an ``AttributeError`` in the ``excerpt`` command.
**2021 July 3 — Version 2.16**
* Fixed a ``ValueError`` raised in the ``excerpt`` command when an
ephemeris segment needs to be entirely skipped because it has no
overlap with the user-specified range of dates.
* Added a ``__version__`` constant to the package’s top level.
**2020 September 2 — Version 2.15**
* The ``excerpt`` subcommand now accepts a ``--targets`` option to save
space by copying only matching segments into the output SPK file.
* The Julian day fraction ``tdb2`` is handled even more carefully than
before, providing a smoother delta between successive positions when
the difference between successive times is down around 0.1 µs.
**2020 March 26 — Version 2.14**
* Fall back to plain file I/O on platforms that support ``fileno()`` but
that don’t support ``mmap()``, like the `Pyodide platform
<https://github.com/iodide-project/pyodide>`_.
**2020 February 22 — Version 2.13**
* The exception raised when a segment is given a Julian date outside the
segment’s date range is now an instance of the ``ValueError`` subclass
``OutOfRangeError`` that reminds the caller of the range of dates
supported by the SPK segment, and carries an array attribute
indicating which input dates were at fault.
**2019 December 13 — Version 2.12**
* Replaced use of NumPy ``flip()`` with a reverse slice ``[::-1]`` after
discovering the function was a recent addition that some user installs
of NumPy do not support.
**2019 December 13 — Version 2.11**
* Reverse the order in which Chebyshev polynomials are computed to
slightly increase speed, to simplify the code, and in one case
(comparing PCK output to NASA) to gain a partial digit of extra
precision.
**2019 December 11 — Version 2.10**
* Document and release support for ``.bcp`` binary PCK kernel files
through the new ``jplephem.pck`` module.
**2019 January 3 — Version 2.9**
* Added the ``load_array()`` method to the segment class.
**2018 July 22 — Version 2.8**
* Switched to a making a single memory map of the entire file, to avoid
running out of file descriptors when users load an ephemeris with
hundreds of segments.
**2018 February 11 — Version 2.7**
* Expanded the command line tool, most notably with the ability to fetch
over HTTP only those sections of a large ephemeris that cover a
specific range of dates, producing a smaller ``.bsp`` file.
**2016 December 19 — Version 2.6**
* Fixed the ability to invoke the module from the command line with
``python -m jplephem``, and added a test to keep it fixed.
**2015 November 9 — Version 2.5**
* Move ``fileno()`` call out of the ``DAF`` constructor to support
fetching at least summary information from ``StringIO`` objects.
**2015 November 1 — Version 2.4**
* Add Windows compatibility by switching ``mmap()`` from using
``PAGESIZE`` to ``ALLOCATIONGRANULARITY``.
* Avoid a new NumPy deprecation warning by being careful to use only
integers in the NumPy ``shape`` tuple.
* Add names "TDB" and "TT" to the names database for DE430.
**2015 August 16 — Version 2.3**
* Added auto-detection and support for old NAIF/DAF kernels like
``de405.bsp`` to the main ``DAF`` class itself, instead of requiring
the awkward use of an entirely different alternative class.
**2015 August 5 — Version 2.2**
* You can now invoke ``jplephem`` from the command line.
* Fixes an exception that was raised for SPK segments with a coefficient
count of only 2, like the DE421 and DE430 segments that provide the
offset of Mercury from the Mercury barycenter.
* Supports old NAIF/DAF kernels like ``de405.bsp``.
* The ``SPK()`` constructor is now simpler, taking a ``DAF`` object
instead of an open file. This is considered an internal API change —
the public API is the constructor ``SPK.open()``.
**2015 February 24 — Version 2.1**
* Switched from mapping an entire SPK file into memory at once to
memory-mapping each segment separately on demand.
**2015 February 8 — Version 2.0**
* Added support for SPICE SPK kernel files downloaded directly from
NASA, and designated old Python-packaged ephemerides as “legacy.”
**2013 November 26 — Version 1.2**
* Helge Eichhorn fixed the default for the ``position_and_velocity()``
argument ``tdb2`` so it defaults to zero days instead of 2.0 days.
Tests were added to prevent any future regression.
**2013 July 10 — Version 1.1**
* Deprecates the old ``compute()`` method in favor of separate
``position()`` and ``position_and_velocity()`` methods.
* Supports computing position and velocity in two separate phases by
saving a “bundle” of coefficients returned by ``compute_bundle()``.
* From Marten van Kerkwijk: a second ``tdb2`` time argument, for users
who want to build higher precision dates out of two 64-bit floats.
**2013 January 18 — Version 1.0**
* Initial release
References
----------
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
Equivalent FORTRAN code for using the ephemerides be found at the same
FTP site: ftp://ssd.jpl.nasa.gov/pub/eph/planets/fortran/
"""
from .ephem import Ephemeris, DateError
__version__ = '2.23'
__all__ = ['Ephemeris', 'DateError', '__version__']
+4
View File
@@ -0,0 +1,4 @@
import sys
from .commandline import main
sys.stdout.write(main(sys.argv[1:]))
sys.exit(0)
+57
View File
@@ -0,0 +1,57 @@
"""Support function for parsing JPL ephemeris text files.
This is for parsing a NASA ephemeris text header file, like:
ftp://ssd.jpl.nasa.gov/pub/eph/planets/ascii/de421/header.421
You can use this routine like this::
from jplephem.ascii import parse_header
d = parse_header(open('header.421'))
from pprint import pprint
pprint(d)
pprint(dict(zip(d['names'], d['values'])))
"""
import numpy as np
def parse_header(lines):
lines = iter(lines)
while next(lines).strip() != 'GROUP 1030':
continue
assert next(lines).strip() == ''
jalpha, jomega, jdelta = (float(s) for s in e(next(lines)).split())
while next(lines).strip() != 'GROUP 1040':
continue
assert next(lines).strip() == ''
nconstants = int(next(lines))
names = []
while len(names) < nconstants:
names.extend(next(lines).split())
while next(lines).strip() != 'GROUP 1041':
continue
assert next(lines).strip() == ''
assert int(next(lines)) == nconstants
values = []
while len(values) < nconstants:
values.extend(float(s) for s in e(next(lines)).split())
while next(lines).strip() != 'GROUP 1050':
continue
assert next(lines).strip() == ''
planet_offsets = np.array(next(lines).split(), np.int_)
num_coefficients = np.array(next(lines).split(), np.int_)
coefficient_sets = np.array(next(lines).split(), np.int_)
del lines
return(locals())
def e(s):
"""Convert a string in 0.1D+01 FORTRAN notation into 0.1e+10."""
return s.replace('D', 'e')
+35
View File
@@ -0,0 +1,35 @@
"""Routines for dealing with Julian dates."""
def compute_calendar_date(jd_integer, julian_before=None):
"""Convert Julian day ``jd_integer`` to ``(year, month, day)``.
Uses the proleptic Gregorian calendar unless ``julian_before`` is
set to a specific Julian day, in which case the Julian calendar is
used for dates older than that.
"""
use_gregorian = (julian_before is None) or (jd_integer >= julian_before)
# See the Explanatory Supplement to the Astronomical Almanac 15.11.
f = jd_integer + 1401
f += use_gregorian * ((4 * jd_integer + 274277) // 146097 * 3 // 4 - 38)
e = 4 * f + 3
g = e % 1461 // 4
h = 5 * g + 2
day = h % 153 // 5 + 1
month = (h // 153 + 2) % 12 + 1
year = e // 1461 - 4716 + (12 + 2 - month) // 12
return year, month, day
def compute_julian_date(year, month=1, day=1.0):
"""Given a proleptic Gregorian date, return a Julian date float."""
return compute_julian_day(year, month, day) - 0.5
def compute_julian_day(year, month=1, day=1):
"""Given a proleptic Gregorian date, return a Julian day int."""
janfeb = month < 3
return (+ 1461 * (year + 4800 - janfeb) // 4
+ 367 * (month - 2 + janfeb * 12) // 12
- 3 * ((year + 4900 - janfeb) // 100) // 4
- 32075
+ day)
+185
View File
@@ -0,0 +1,185 @@
"""The `python -m jplephem` command line."""
from __future__ import print_function
import argparse
import sys
from .calendar import compute_calendar_date, compute_julian_date
from .daf import DAF
from .excerpter import RemoteFile, write_excerpt
from .spk import S_PER_DAY, SPK, T0
_DAY = 86400.0
def _jd(seconds):
"""Convert a number of seconds since J2000 to a Julian Date."""
return T0 + seconds / S_PER_DAY
def main(args):
parser = argparse.ArgumentParser(
prog='python -m jplephem',
description='Describe an SPK kernel',
)
subparsers = parser.add_subparsers()
p = subparsers.add_parser(
'comment',
help="Print a file's comment blocks",
)
p.set_defaults(func=comment)
p.add_argument('path', help='Path to a SPICE file')
p = subparsers.add_parser(
'daf',
help="List a file's raw segment descriptors",
)
p.set_defaults(func=daf_segments)
p.add_argument('path', help='Path to a SPICE file')
p = subparsers.add_parser(
'excerpt',
help="Create an SPK covering a narrower range of dates",
)
p.set_defaults(func=excerpt)
p.add_argument('--targets', help='Comma-separated targets to include')
p.add_argument('start_date', help='Start date yyyy/mm/dd', type=parse_date)
p.add_argument('end_date', help='End date yyyy/mm/dd', type=parse_date)
p.add_argument('path_or_url', help='Local filename or remote URL')
p.add_argument('output_path', help='Output file to create')
p = subparsers.add_parser(
'spk',
help="List the segments in an SPK file",
)
p.set_defaults(func=spk_segments)
p.add_argument('path', help='Path to a .bsp SPICE kernel file')
p.add_argument('-v', '--verbose', action='store_true')
args = parser.parse_args(args)
func = getattr(args, 'func', None)
if func is None:
parser.print_help()
sys.exit(2)
lines = list(func(args))
if lines and not lines[-1].endswith('\n'):
lines.append('')
return '\n'.join(lines)
def comment(args):
with open(args.path, 'rb') as f:
d = DAF(f)
yield d.comments()
def daf_segments(args):
with open(args.path, 'rb') as f:
d = DAF(f)
for i, (name, values) in enumerate(d.summaries()):
yield '{:2d} {} {}'.format(i + 1, name.decode('latin-1'),
' '.join(repr(v) for v in values))
def excerpt(args):
for string, jd in args.start_date, args.end_date:
yield 'Date {:10} = JD {}'.format(string, jd)
if args.path_or_url.startswith(('http://', 'https://')):
url = args.path_or_url
f = RemoteFile(url)
else:
path = args.path_or_url
f = open(path, 'rb')
with f:
spk = SPK(DAF(f))
summaries = spk.daf.summaries()
if args.targets:
desired_targets = set(args.targets.split(','))
summaries = [
summary for summary, segment in zip(summaries, spk.segments)
if str(segment.target) in desired_targets
]
with open(args.output_path, 'w+b') as output_file:
write_excerpt(spk, output_file, args.start_date[1],
args.end_date[1], summaries)
yield '\n{!r} written successfully with the following contents\n'.format(
args.output_path)
with open(args.output_path, 'rb') as f:
yield str(SPK(DAF(f)))
def spk_segments(args):
with open(args.path, 'rb') as f:
spk = SPK(DAF(f))
# Snag the first line from the normal str().
output = str(spk)
yield output.split('\n', 1)[0]
# But produce the rest of the lines ourselves, so we can
# optionally honor '-v' by providing more information.
for s in spk.segments:
yield str(s)
if not args.verbose:
continue
for line in _describe_segment_details(s):
yield line
def _describe_segment_details(s):
if s.data_type not in (2, 3):
return
init, intlen, coefficients = s._data
degree, dimensions, record_count = coefficients.shape
days_per = intlen / _DAY
plural = '' if record_count == 1 else 's'
each = '' if record_count == 1 else ' each'
yield ' {} polynomial{} covering {} days{}'.format(
record_count, plural, days_per, each,
)
yield ' x {} coefficients per polynomial'.format(degree)
yield ' x {} coordinates'.format(dimensions)
yield ' = {} double precision floats'.format(coefficients.size)
polynomial_start = init
polynomial_end = init + intlen * record_count
if s.start_second == polynomial_start:
yield ' Polynomial start date matches segment start date'
else:
days = (s.start_second - polynomial_start) / _DAY
jd = _jd(polynomial_start)
y, m, d = compute_calendar_date(int(jd + 0.5))
yield (
' First polynomial starts {:.1f} days earlier'
' than segment start date, on {}-{:02}-{:02}'
.format(days, y, m, d)
)
if s.end_second == polynomial_end:
yield ' Polynomial end date matches segment end date'
else:
days = (polynomial_end - s.end_second) / _DAY
jd = _jd(polynomial_end)
y, m, d = compute_calendar_date(int(jd + 0.5))
yield (
' Final polynomial ends {:.1f} days later'
' than segment end date, on {}-{:02}-{:02}'
.format(days, y, m, d)
)
yield ''
def parse_date(s):
try:
fields = [int(f) for f in s.split('/')]
except ValueError:
fields = []
if len(fields) < 1 or len(fields) > 3:
E = argparse.ArgumentTypeError
raise E('specify each date as YYYY or YYYY/MM or YYYY/MM/DD')
jd = compute_julian_date(*fields)
return s, jd
+276
View File
@@ -0,0 +1,276 @@
"""Access a NASA JPL SPICE Double Precision Array File (DAF).
http://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/req/daf.html
"""
import io
import mmap
import sys
try: # Use low-level module to avoid importing huge 'threading.py'
from _thread import allocate_lock
except:
from thread import allocate_lock
from struct import Struct
from numpy import array as numpy_array, ndarray
FTPSTR = b'FTPSTR:\r:\n:\r\n:\r\x00:\x81:\x10\xce:ENDFTP' # FTP test string
LOCFMT = {b'BIG-IEEE': '>', b'LTL-IEEE': '<'}
K = 1024
class DAF(object):
"""Access to NASA SPICE Double Precision Array Files (DAF).
Provide the constructor with a ``file_object`` for full access to
both the segment summaries and to the numeric arrays. If you pass a
``StringIO`` instead, then you can fetch the summary information but
not access the arrays.
"""
def __init__(self, file_object):
if getattr(file_object, 'encoding', None):
raise ValueError('file_object must be opened in binary "b" mode')
self.file = file_object
self.lock = allocate_lock()
self._map = None
self._array = None
file_record = self.read_record(1)
def unpack():
fmt = self.endian + '8sII60sIII8s603s28s297s'
self.file_record_struct = Struct(fmt)
(locidw, self.nd, self.ni, self.locifn, self.fward, self.bward,
self.free, locfmt, self.prenul, self.ftpstr, self.pstnul
) = self.file_record_struct.unpack(file_record)
self.locidw = file_record[:8].upper().rstrip()
if self.locidw == b'NAIF/DAF':
for self.locfmt, self.endian in LOCFMT.items():
unpack()
if self.nd == 2:
break
else:
raise ValueError('neither a big- nor a little-endian scan'
' of this file produces the expected ND=2')
elif self.locidw.startswith(b'DAF/'):
if file_record[500:1000].strip(b'\0') != FTPSTR:
raise ValueError('this SPK file has been damaged')
self.locfmt = file_record[88:96]
self.endian = LOCFMT.get(self.locfmt)
if self.endian is None:
raise ValueError('unknown format {0!r}'.format(self.locfmt))
unpack()
else:
raise ValueError('file starts with {0!r}, not "NAIF/DAF" or "DAF/"'
.format(self.locidw))
self.locifn_text = self.locifn.rstrip()
summary_format = 'd' * self.nd + 'i' * self.ni
self.summary_control_struct = Struct(self.endian + 'ddd')
self.summary_struct = struct = Struct(self.endian + summary_format)
self.summary_length = length = struct.size
self.summary_step = length + (-length % 8) # pad to 8 bytes
self.summaries_per_record = (1024 - 8 * 3) // self.summary_step
def read_record(self, n):
"""Return record `n` as 1,024 bytes; records are indexed from 1."""
with self.lock:
self.file.seek(n * K - K)
return self.file.read(K)
def write_record(self, n, data):
"""Write `data` to file record `n`; records are indexed from 1."""
with self.lock:
self.file.seek(n * K - K)
return self.file.write(data)
def write_file_record(self):
data = self.file_record_struct.pack(
self.locidw.ljust(8, b' '), self.nd, self.ni, self.locifn,
self.fward, self.bward, self.free, self.locfmt,
self.prenul, self.ftpstr, self.pstnul,
)
self.write_record(1, data)
def map_words(self, start, end):
"""Return a memory-map of the elements `start` through `end`.
The memory map will offer the 8-byte double-precision floats
("elements") in the file from index `start` through to the index
`end`, inclusive, both counting the first float as element 1.
Memory maps must begin on a page boundary, so `skip` returns the
number of extra bytes at the beginning of the return value.
If a memory map is not available on your operating system, then
the segment's bytes are simply read into an array instead.
"""
i, j = 8 * start - 8, 8 * end
try:
fileno = self.file.fileno()
except (AttributeError, io.UnsupportedOperation):
m = None
else:
skip = i % mmap.ALLOCATIONGRANULARITY
r = mmap.ACCESS_READ
try:
m = mmap.mmap(fileno, length=j-i+skip, access=r, offset=i-skip)
except OSError:
m = None
if m is None:
skip = 0
with self.lock:
self.file.seek(i)
m = self.file.read(j - i)
if sys.version_info > (3,):
m = memoryview(m) # so further slicing can return views
return m, skip
def comments(self):
"""Return the text inside the comment area of the file."""
record_numbers = range(2, self.fward)
if not record_numbers:
return ''
data = b''.join(self.read_record(n)[0:1000] for n in record_numbers)
try:
return data[:data.find(b'\4')].decode('ascii').replace('\0', '\n')
except IndexError:
raise ValueError('DAF file comment area is missing its EOT byte')
except UnicodeDecodeError:
raise ValueError('DAF file comment area is not ASCII text')
def read_array(self, start, end):
"""Return floats from `start` to `end` inclusive, indexed from 1.
The entire range of floats is immediately read into memory from
the file, making this efficient for small sequences of floats
whose values are all needed immediately.
"""
f = self.file
length = 1 + end - start
with self.lock:
f.seek(8 * (start - 1))
data = f.read(8 * length)
return ndarray(length, self.endian + 'd', data)
def map_array(self, start, end):
"""Return floats from `start` to `end` inclusive, indexed from 1.
Instead of pausing to load all of the floats into RAM, this
routine creates a memory map which will load data from the file
only as it is accessed, and then will let it expire back out to
disk later. This is very efficient for large data sets to which
you need random access.
"""
if self._array is None:
self._map, skip = self.map_words(1, self.free - 1)
assert skip == 0
self._array = ndarray(self.free - 1, self.endian + 'd', self._map)
return self._array[start - 1 : end]
def summary_records(self):
"""Yield (record_number, n_summaries, record_data) for each record.
Readers will only use the second two values in each tuple.
Writers can update the record using the `record_number`.
"""
record_number = self.fward
unpack = self.summary_control_struct.unpack
while record_number:
data = self.read_record(record_number)
next_number, previous_number, n_summaries = unpack(data[:24])
yield record_number, n_summaries, data
record_number = int(next_number)
def summaries(self):
"""Yield (name, (value, value, ...)) for each summary in the file."""
length = self.summary_length
step = self.summary_step
for record_number, n_summaries, summary_data in self.summary_records():
name_data = self.read_record(record_number + 1)
for i in range(0, int(n_summaries) * step, step):
j = self.summary_control_struct.size + i
name = name_data[i:i+step].strip()
data = summary_data[j:j+length]
values = self.summary_struct.unpack(data)
yield name, values
def map(self, summary_values):
"""Return the array of floats described by a summary.
Instead of pausing to load all of the floats into RAM, this
routine creates a memory map which will load data from the file
only as it is accessed, and then will let it expire back out to
disk later. This is very efficient for large data sets to which
you need random access.
"""
return self.map_array(summary_values[-2], summary_values[-1])
def add_array(self, name, values, array):
"""Add a new array to the DAF file.
The summary will be initialized with the `name` and `values`,
and will have its start word and end word fields set to point to
where the `array` of floats has been appended to the file.
This method is not thread-safe.
"""
f = self.file
scs = self.summary_control_struct
record_number = self.bward
data = bytearray(self.read_record(record_number))
next_record, previous_record, n_summaries = scs.unpack(data[:24])
if n_summaries < self.summaries_per_record:
summary_record = record_number
name_record = summary_record + 1
data[:24] = scs.pack(next_record, previous_record, n_summaries + 1)
self.write_record(summary_record, data)
else:
summary_record = ((self.free - 1) * 8 + 1023) // 1024 + 1
name_record = summary_record + 1
free_record = summary_record + 2
data[:24] = scs.pack(summary_record, previous_record, n_summaries)
self.write_record(record_number, data)
n_summaries = 0
summaries = scs.pack(0, record_number, 1).ljust(1024, b'\0')
names = b'\0' * 1024
self.write_record(summary_record, summaries)
self.write_record(name_record, names)
self.bward = summary_record
self.free = (free_record - 1) * 1024 // 8 + 1
array = numpy_array(array, self.endian + 'f8')
start_word = self.free
f.seek((start_word - 1) * 8)
f.write(array.view())
end_word = f.tell() // 8
self.free = end_word + 1
self.write_file_record()
values = values[:self.nd + self.ni - 2] + (start_word, end_word)
base = 1024 * (summary_record - 1)
offset = int(n_summaries) * self.summary_step
f.seek(base + scs.size + offset)
f.write(self.summary_struct.pack(*values))
f.seek(base + 1024 + offset)
f.write(name[:self.summary_length].ljust(self.summary_step, b' '))
NAIF_DAF = DAF # a separate class supported NAIF/DAF format in jplephem 2.2
+14
View File
@@ -0,0 +1,14 @@
from functools import update_wrapper
class reify(object):
"""Adapted from Pyramid's `reify()` memoizing decorator."""
def __init__(self, method):
self.method = method
update_wrapper(self, method)
def __get__(self, instance, objtype=None):
if instance is None:
return self
value = self.method(instance)
instance.__dict__[self.__name__] = value
return value
+124
View File
@@ -0,0 +1,124 @@
"""Compute positions from an ephemeris installed as a Python package.
Note: This entire module is DEPRECATED. The idea of distributing JPL
ephemerides as Python packages proved to be impractical (they were much
too large for the Python Package Index to easily store and distribute),
and it forced `jplephem` users get their ephemerides from a different
source than mainline astronomers, who use SPICE files.
"""
import os
import numpy as np
class DateError(ValueError):
pass
class Ephemeris(object):
"""[DEPRECATED] JPL planetary ephemeris for computing positions on dates."""
def __init__(self, module):
self.name = module.__name__.upper()
self.dirpath = os.path.dirname(module.__file__)
self.names = tuple(sorted(
name.split('-')[-1].split('.')[0]
for name in os.listdir(self.dirpath)
if not name.startswith('constants') and name.endswith('.npy')
))
path = self.path('constants.npy')
self.__dict__.update((k.decode('ascii'), v) for k, v in np.load(path))
self.earth_share = 1.0 / (1.0 + self.EMRAT)
self.moon_share = self.EMRAT / (1.0 + self.EMRAT)
self.sets = {}
def path(self, filename):
"""[DEPRECATED] Compute the path to a particular file in the ephemeris."""
return os.path.join(self.dirpath, filename)
def load(self, name):
"""[DEPRECATED] Load the polynomial series for `name` and return it."""
s = self.sets.get(name)
if s is None:
self.sets[name] = s = np.load(self.path('jpl-%s.npy' % name))
return s
def position(self, name, tdb, tdb2=0.0):
"""[DEPRECATED] Compute the position of `name` at ``tdb [+ tdb2]``."""
bundle = self.compute_bundle(name, tdb, tdb2)
return self.position_from_bundle(bundle)
def position_and_velocity(self, name, tdb, tdb2=0.0):
"""[DEPRECATED] Compute the position and velocity of `name` at ``tdb [+ tdb2]``."""
bundle = self.compute_bundle(name, tdb, tdb2)
position = self.position_from_bundle(bundle)
velocity = self.velocity_from_bundle(bundle)
return position, velocity
def compute(self, name, tdb):
"""[DEPRECATED] Legacy routine that concatenates position and velocity vectors."""
bundle = self.compute_bundle(name, tdb, 0.0)
position = self.position_from_bundle(bundle)
velocity = self.velocity_from_bundle(bundle)
return np.concatenate((position, velocity))
def compute_bundle(self, name, tdb, tdb2=0.0):
"""[DEPRECATED] Return a tuple of coefficients and parameters for `tdb`."""
input_was_scalar = getattr(tdb, 'shape', ()) == ()
if input_was_scalar:
tdb = np.array((tdb,))
# no need to deal with tdb2; numpy broadcast will add fine below.
coefficient_sets = self.load(name)
number_of_sets, axis_count, coefficient_count = coefficient_sets.shape
jalpha, jomega = self.jalpha, self.jomega
days_per_set = (jomega - jalpha) / number_of_sets
# to keep precision, first subtract, then add
index, offset = divmod((tdb - jalpha) + tdb2, days_per_set)
index = index.astype(int)
if (index < 0).any() or (number_of_sets < index).any():
raise DateError('ephemeris %s only covers dates %.1f through %.1f'
% (self.name, jalpha, jomega))
omegas = (index == number_of_sets)
index[omegas] -= 1
offset[omegas] += days_per_set
coefficients = np.rollaxis(coefficient_sets[index], 1)
# Chebyshev recurrence:
T = np.empty((coefficient_count, len(index)))
T[0] = 1.0
T[1] = t1 = 2.0 * offset / days_per_set - 1.0
twot1 = t1 + t1
for i in range(2, coefficient_count):
T[i] = twot1 * T[i-1] - T[i-2]
bundle = coefficients, days_per_set, T, twot1
return bundle
def position_from_bundle(self, bundle):
"""[DEPRECATED] Return position, given the `coefficient_bundle()` return value."""
coefficients, days_per_set, T, twot1 = bundle
return (T.T * coefficients).sum(axis=2)
def velocity_from_bundle(self, bundle):
"""[DEPRECATED] Return velocity, given the `coefficient_bundle()` return value."""
coefficients, days_per_set, T, twot1 = bundle
coefficient_count = coefficients.shape[2]
# Chebyshev derivative:
dT = np.empty_like(T)
dT[0] = 0.0
dT[1] = 1.0
dT[2] = twot1 + twot1
for i in range(3, coefficient_count):
dT[i] = twot1 * dT[i-1] - dT[i-2] + T[i-1] + T[i-1]
dT *= 2.0
dT /= days_per_set
return (dT.T * coefficients).sum(axis=2)
+16
View File
@@ -0,0 +1,16 @@
"""A set of special exceptions that can be thrown by the jplephem library"""
class OutOfRangeError(ValueError):
"""One or more time values given were out of range for the ephemeris.
This exception is thrown if any input times are out of the range of
times supported by an ephemeris. It has an extra attribute:
- `out_of_range_times`: if the input `tdb` of times is an array,
this provides an array of booleans of the same length where `True`
means the corresponding date is out of range.
"""
def __init__(self, message, out_of_range_times):
self.args = message,
self.out_of_range_times = out_of_range_times
+142
View File
@@ -0,0 +1,142 @@
"""Extract data for a specific date range from an SPK file."""
from sys import stderr
try:
from urllib.request import Request, urlopen
except ImportError:
from urllib2 import Request, urlopen
from numpy import copy
from . import __version__ as jplephem_version
from .calendar import compute_calendar_date
from .daf import DAF, K
from .spk import S_PER_DAY, T0
clip_lower = max
clip_upper = min
_PREFACE = """\
;
; This is an ephemeris excerpt created by jplephem {}, which was
; asked to narrow the ephemeris to Julian dates {:.1f} - {:.1f}
; (proleptic Gregorian dates {}-{:02}-{:02} through {}-{:02}-{:02}).
;
; Here is the comments area from the original ephemeris file:
; ----------------------------------------------------------------------
"""
def _seconds(jd):
"""Convert a Julian Date to a number of seconds since J2000."""
return (jd - T0) * S_PER_DAY
def write_excerpt(input_spk, output_file, start_jd, end_jd, summaries):
start_seconds = _seconds(start_jd)
end_seconds = _seconds(end_jd)
old = input_spk.daf
# Supplement the comment text.
y1, m1, d1 = compute_calendar_date(int(start_jd + 0.5))
y2, m2, d2 = compute_calendar_date(int(end_jd + 0.5))
preface = _PREFACE.format(
jplephem_version, start_jd, end_jd, y1,m1,d1, y2,m2,d2,
)
comment = preface + old.comments()
# Build new comment blocks (which have 1000 text characters each).
data = comment.encode('ascii').replace(b'\n', b'\0') + b'\004'
blocks = [data[i : i + 1000] for i in range(0, len(data), 1000)]
comment_data = b''.join([
block + b' ' * (K - len(block))
for block in blocks
])
# Start the new DAF file with:
# 1. The verbatim first record from the original file.
# 2. The new comment.
# 3. An empty summary block.
# 4. An empty name block.
f = output_file
f.seek(0)
f.truncate()
summary_data = b'\0' * 1024
name_data = b' ' * 1024
f.write(old.read_record(1))
f.write(comment_data)
f.write(summary_data)
f.write(name_data)
# There are now enough blocks to start treating the file as a DAF!
# Set the initial block number indexes.
f.seek(0)
d = DAF(f)
d.fward = d.bward = 2 + len(comment_data) // K
d.free = (d.fward + 1) * (1024 // 8) + 1
d.write_file_record()
# Copy over an excerpt of each array.
for name, values in summaries:
start, end = values[-2], values[-1]
init, intlen, rsize, n = old.read_array(end - 3, end)
rsize = int(rsize)
i = int(clip(0, n, (start_seconds - init) // intlen))
j = int(clip(0, n, (end_seconds - init) // intlen + 1))
if i == j:
continue # Segment has no overlap with user's dates.
init = init + i * intlen
n = j - i
extra = 4 # enough room to rebuild [init intlen rsize n]
excerpt = copy(old.read_array(
start + rsize * i,
start + rsize * j + extra - 1,
))
excerpt[-4:] = (init, intlen, rsize, n)
# Even though the polynomials we selected probably cover a wider
# range of dates, let's only claim that each segment covers the
# range `start_seconds .. end_seconds` that the user requested,
# to avoid confusing them. The `de442s.bsp` ephemeris shows
# that this is also the practice at NASA itself.
values = (start_seconds, end_seconds) + values[2:]
d.add_array(name, values, excerpt)
def clip(lower, upper, n):
return clip_lower(lower, clip_upper(upper, n))
class RemoteFile(object):
def __init__(self, url):
self.url = url
self.filename = url.rstrip('/').rsplit('/', 1)[-1]
self.offset = 0
def seek(self, offset, whence=0):
assert whence == 0
self.offset = offset
def read(self, size):
start = self.offset
end = start + size - 1
assert end > start
byte_range = 'bytes={}-{}'.format(start, end)
stderr.write('Fetching {} bytes from {} using Range: {}\n'
.format(size, self.filename, byte_range))
request = Request(self.url, headers={'Range': byte_range})
data = urlopen(request).read()
assert len(data) == size, (
'asked for "Range: {}" which is {} bytes, but got {} bytes back'
.format(byte_range, size, len(data))
)
self.offset += size
return data
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
pass
+602
View File
@@ -0,0 +1,602 @@
# Standard SPICE target names (generated by parse_zzidmap.awk).
target_name_pairs = [
(0, 'SOLAR_SYSTEM_BARYCENTER'),
(0, 'SSB'),
(0, 'SOLAR SYSTEM BARYCENTER'),
(1, 'MERCURY_BARYCENTER'),
(1, 'MERCURY BARYCENTER'),
(2, 'VENUS_BARYCENTER'),
(2, 'VENUS BARYCENTER'),
(3, 'EARTH_BARYCENTER'),
(3, 'EMB'),
(3, 'EARTH MOON BARYCENTER'),
(3, 'EARTH-MOON BARYCENTER'),
(3, 'EARTH BARYCENTER'),
(4, 'MARS_BARYCENTER'),
(4, 'MARS BARYCENTER'),
(5, 'JUPITER_BARYCENTER'),
(5, 'JUPITER BARYCENTER'),
(6, 'SATURN_BARYCENTER'),
(6, 'SATURN BARYCENTER'),
(7, 'URANUS_BARYCENTER'),
(7, 'URANUS BARYCENTER'),
(8, 'NEPTUNE_BARYCENTER'),
(8, 'NEPTUNE BARYCENTER'),
(9, 'PLUTO_BARYCENTER'),
(9, 'PLUTO BARYCENTER'),
(10, 'SUN'),
(199, 'MERCURY'),
(299, 'VENUS'),
(399, 'EARTH'),
(301, 'MOON'),
(499, 'MARS'),
(401, 'PHOBOS'),
(402, 'DEIMOS'),
(599, 'JUPITER'),
(501, 'IO'),
(502, 'EUROPA'),
(503, 'GANYMEDE'),
(504, 'CALLISTO'),
(505, 'AMALTHEA'),
(506, 'HIMALIA'),
(507, 'ELARA'),
(508, 'PASIPHAE'),
(509, 'SINOPE'),
(510, 'LYSITHEA'),
(511, 'CARME'),
(512, 'ANANKE'),
(513, 'LEDA'),
(514, 'THEBE'),
(515, 'ADRASTEA'),
(516, 'METIS'),
(517, 'CALLIRRHOE'),
(518, 'THEMISTO'),
(519, 'MAGACLITE'),
(520, 'TAYGETE'),
(521, 'CHALDENE'),
(522, 'HARPALYKE'),
(523, 'KALYKE'),
(524, 'IOCASTE'),
(525, 'ERINOME'),
(526, 'ISONOE'),
(527, 'PRAXIDIKE'),
(528, 'AUTONOE'),
(529, 'THYONE'),
(530, 'HERMIPPE'),
(531, 'AITNE'),
(532, 'EURYDOME'),
(533, 'EUANTHE'),
(534, 'EUPORIE'),
(535, 'ORTHOSIE'),
(536, 'SPONDE'),
(537, 'KALE'),
(538, 'PASITHEE'),
(539, 'HEGEMONE'),
(540, 'MNEME'),
(541, 'AOEDE'),
(542, 'THELXINOE'),
(543, 'ARCHE'),
(544, 'KALLICHORE'),
(545, 'HELIKE'),
(546, 'CARPO'),
(547, 'EUKELADE'),
(548, 'CYLLENE'),
(549, 'KORE'),
(550, 'HERSE'),
(699, 'SATURN'),
(601, 'MIMAS'),
(602, 'ENCELADUS'),
(603, 'TETHYS'),
(604, 'DIONE'),
(605, 'RHEA'),
(606, 'TITAN'),
(607, 'HYPERION'),
(608, 'IAPETUS'),
(609, 'PHOEBE'),
(610, 'JANUS'),
(611, 'EPIMETHEUS'),
(612, 'HELENE'),
(613, 'TELESTO'),
(614, 'CALYPSO'),
(615, 'ATLAS'),
(616, 'PROMETHEUS'),
(617, 'PANDORA'),
(618, 'PAN'),
(619, 'YMIR'),
(620, 'PAALIAQ'),
(621, 'TARVOS'),
(622, 'IJIRAQ'),
(623, 'SUTTUNGR'),
(624, 'KIVIUQ'),
(625, 'MUNDILFARI'),
(626, 'ALBIORIX'),
(627, 'SKATHI'),
(628, 'ERRIAPUS'),
(629, 'SIARNAQ'),
(630, 'THRYMR'),
(631, 'NARVI'),
(632, 'METHONE'),
(633, 'PALLENE'),
(634, 'POLYDEUCES'),
(635, 'DAPHNIS'),
(636, 'AEGIR'),
(637, 'BEBHIONN'),
(638, 'BERGELMIR'),
(639, 'BESTLA'),
(640, 'FARBAUTI'),
(641, 'FENRIR'),
(642, 'FORNJOT'),
(643, 'HATI'),
(644, 'HYRROKKIN'),
(645, 'KARI'),
(646, 'LOGE'),
(647, 'SKOLL'),
(648, 'SURTUR'),
(649, 'ANTHE'),
(650, 'JARNSAXA'),
(651, 'GREIP'),
(652, 'TARQEQ'),
(653, 'AEGAEON'),
(799, 'URANUS'),
(701, 'ARIEL'),
(702, 'UMBRIEL'),
(703, 'TITANIA'),
(704, 'OBERON'),
(705, 'MIRANDA'),
(706, 'CORDELIA'),
(707, 'OPHELIA'),
(708, 'BIANCA'),
(709, 'CRESSIDA'),
(710, 'DESDEMONA'),
(711, 'JULIET'),
(712, 'PORTIA'),
(713, 'ROSALIND'),
(714, 'BELINDA'),
(715, 'PUCK'),
(716, 'CALIBAN'),
(717, 'SYCORAX'),
(718, 'PROSPERO'),
(719, 'SETEBOS'),
(720, 'STEPHANO'),
(721, 'TRINCULO'),
(722, 'FRANCISCO'),
(723, 'MARGARET'),
(724, 'FERDINAND'),
(725, 'PERDITA'),
(726, 'MAB'),
(727, 'CUPID'),
(899, 'NEPTUNE'),
(801, 'TRITON'),
(802, 'NEREID'),
(803, 'NAIAD'),
(804, 'THALASSA'),
(805, 'DESPINA'),
(806, 'GALATEA'),
(807, 'LARISSA'),
(808, 'PROTEUS'),
(809, 'HALIMEDE'),
(810, 'PSAMATHE'),
(811, 'SAO'),
(812, 'LAOMEDEIA'),
(813, 'NESO'),
(999, 'PLUTO'),
(901, 'CHARON'),
(902, 'NIX'),
(903, 'HYDRA'),
(904, 'KERBEROS'),
(905, 'STYX'),
(-1, 'GEOTAIL'),
(-3, 'MOM'),
(-3, 'MARS ORBITER MISSION'),
(-5, 'AKATSUKI'),
(-5, 'VCO'),
(-5, 'PLC'),
(-5, 'PLANET-C'),
(-6, 'P6'),
(-6, 'PIONEER-6'),
(-7, 'P7'),
(-7, 'PIONEER-7'),
(-8, 'WIND'),
(-12, 'VENUS ORBITER'),
(-12, 'P12'),
(-12, 'PIONEER 12'),
(-12, 'LADEE'),
(-13, 'POLAR'),
(-18, 'MGN'),
(-18, 'MAGELLAN'),
(-18, 'LCROSS'),
(-20, 'P8'),
(-20, 'PIONEER-8'),
(-21, 'SOHO'),
(-23, 'P10'),
(-23, 'PIONEER-10'),
(-24, 'P11'),
(-24, 'PIONEER-11'),
(-25, 'LP'),
(-25, 'LUNAR PROSPECTOR'),
(-27, 'VK1'),
(-27, 'VIKING 1 ORBITER'),
(-29, 'STARDUST'),
(-29, 'SDU'),
(-29, 'NEXT'),
(-30, 'VK2'),
(-30, 'VIKING 2 ORBITER'),
(-30, 'DS-1'),
(-31, 'VG1'),
(-31, 'VOYAGER 1'),
(-32, 'VG2'),
(-32, 'VOYAGER 2'),
(-40, 'CLEMENTINE'),
(-41, 'MEX'),
(-41, 'MARS EXPRESS'),
(-44, 'BEAGLE2'),
(-44, 'BEAGLE 2'),
(-46, 'MS-T5'),
(-46, 'SAKIGAKE'),
(-47, 'PLANET-A'),
(-47, 'SUISEI'),
(-47, 'GNS'),
(-47, 'GENESIS'),
(-48, 'HUBBLE SPACE TELESCOPE'),
(-48, 'HST'),
(-53, 'MARS PATHFINDER'),
(-53, 'MPF'),
(-53, 'MARS ODYSSEY'),
(-53, 'MARS SURVEYOR 01 ORBITER'),
(-54, 'ARM'),
(-54, 'ASTEROID RETRIEVAL MISSION'),
(-55, 'ULYSSES'),
(-58, 'VSOP'),
(-58, 'HALCA'),
(-59, 'RADIOASTRON'),
(-61, 'JUNO'),
(-64, 'ORX'),
(-64, 'OSIRIS-REX'),
(-66, 'VEGA 1'),
(-67, 'VEGA 2'),
(-68, 'MMO'),
(-68, 'MERCURY MAGNETOSPHERIC ORBITER'),
(-69, 'MPO'),
(-69, 'MERCURY PLANETARY ORBITER'),
(-70, 'DEEP IMPACT IMPACTOR SPACECRAFT'),
(-74, 'MRO'),
(-74, 'MARS RECON ORBITER'),
(-76, 'MSL'),
(-76, 'MARS SCIENCE LABORATORY'),
(-77, 'GLL'),
(-77, 'GALILEO ORBITER'),
(-78, 'GIOTTO'),
(-79, 'SPITZER'),
(-79, 'SPACE INFRARED TELESCOPE FACILITY'),
(-79, 'SIRTF'),
(-81, 'CASSINI ITL'),
(-82, 'CAS'),
(-82, 'CASSINI'),
(-84, 'PHOENIX'),
(-85, 'LRO'),
(-85, 'LUNAR RECON ORBITER'),
(-85, 'LUNAR RECONNAISSANCE ORBITER'),
(-86, 'CH1'),
(-86, 'CHANDRAYAAN-1'),
(-90, 'CASSINI SIMULATION'),
(-93, 'NEAR EARTH ASTEROID RENDEZVOUS'),
(-93, 'NEAR'),
(-94, 'MO'),
(-94, 'MARS OBSERVER'),
(-94, 'MGS'),
(-94, 'MARS GLOBAL SURVEYOR'),
(-95, 'MGS SIMULATION'),
(-96, 'SPP'),
(-96, 'SOLAR PROBE PLUS'),
(-97, 'TOPEX/POSEIDON'),
(-98, 'NEW HORIZONS'),
(-107, 'TROPICAL RAINFALL MEASURING MISSION'),
(-107, 'TRMM'),
(-112, 'ICE'),
(-116, 'MARS POLAR LANDER'),
(-116, 'MPL'),
(-121, 'BEPICOLOMBO'),
(-127, 'MARS CLIMATE ORBITER'),
(-127, 'MCO'),
(-130, 'MUSES-C'),
(-130, 'HAYABUSA'),
(-131, 'SELENE'),
(-131, 'KAGUYA'),
(-135, 'DRTS-W'),
(-140, 'EPOCH'),
(-140, 'DIXI'),
(-140, 'EPOXI'),
(-140, 'DEEP IMPACT FLYBY SPACECRAFT'),
(-142, 'TERRA'),
(-142, 'EOS-AM1'),
(-144, 'SOLO'),
(-144, 'SOLAR ORBITER'),
(-146, 'LUNAR-A'),
(-150, 'CASSINI PROBE'),
(-150, 'HUYGENS PROBE'),
(-150, 'CASP'),
(-151, 'AXAF'),
(-151, 'CHANDRA'),
(-154, 'AQUA'),
(-159, 'EUROPA ORBITER'),
(-164, 'YOHKOH'),
(-164, 'SOLAR-A'),
(-165, 'MAP'),
(-166, 'IMAGE'),
(-170, 'JWST'),
(-170, 'JAMES WEBB SPACE TELESCOPE'),
(-177, 'GRAIL-A'),
(-178, 'PLANET-B'),
(-178, 'NOZOMI'),
(-181, 'GRAIL-B'),
(-183, 'CLUSTER 1'),
(-185, 'CLUSTER 2'),
(-188, 'MUSES-B'),
(-189, 'NSYT'),
(-189, 'INSIGHT'),
(-190, 'SIM'),
(-194, 'CLUSTER 3'),
(-196, 'CLUSTER 4'),
(-198, 'INTEGRAL'),
(-200, 'CONTOUR'),
(-202, 'MAVEN'),
(-203, 'DAWN'),
(-205, 'SOIL MOISTURE ACTIVE AND PASSIVE'),
(-205, 'SMAP'),
(-212, 'STV51'),
(-213, 'STV52'),
(-214, 'STV53'),
(-226, 'ROSETTA'),
(-227, 'KEPLER'),
(-228, 'GLL PROBE'),
(-228, 'GALILEO PROBE'),
(-234, 'STEREO AHEAD'),
(-235, 'STEREO BEHIND'),
(-236, 'MESSENGER'),
(-238, 'SMART1'),
(-238, 'SM1'),
(-238, 'S1'),
(-238, 'SMART-1'),
(-248, 'VEX'),
(-248, 'VENUS EXPRESS'),
(-253, 'OPPORTUNITY'),
(-253, 'MER-1'),
(-254, 'SPIRIT'),
(-254, 'MER-2'),
(-362, 'RADIATION BELT STORM PROBE A'),
(-362, 'RBSP_A'),
(-363, 'RADIATION BELT STORM PROBE B'),
(-363, 'RBSP_B'),
(-500, 'RSAT'),
(-500, 'SELENE Relay Satellite'),
(-500, 'SELENE Rstar'),
(-500, 'Rstar'),
(-502, 'VSAT'),
(-502, 'SELENE VLBI Radio Satellite'),
(-502, 'SELENE VRAD Satellite'),
(-502, 'SELENE Vstar'),
(-502, 'Vstar'),
(-550, 'MARS-96'),
(-550, 'M96'),
(-550, 'MARS 96'),
(-550, 'MARS96'),
(-750, 'SPRINT-A'),
(50000001, 'SHOEMAKER-LEVY 9-W'),
(50000002, 'SHOEMAKER-LEVY 9-V'),
(50000003, 'SHOEMAKER-LEVY 9-U'),
(50000004, 'SHOEMAKER-LEVY 9-T'),
(50000005, 'SHOEMAKER-LEVY 9-S'),
(50000006, 'SHOEMAKER-LEVY 9-R'),
(50000007, 'SHOEMAKER-LEVY 9-Q'),
(50000008, 'SHOEMAKER-LEVY 9-P'),
(50000009, 'SHOEMAKER-LEVY 9-N'),
(50000010, 'SHOEMAKER-LEVY 9-M'),
(50000011, 'SHOEMAKER-LEVY 9-L'),
(50000012, 'SHOEMAKER-LEVY 9-K'),
(50000013, 'SHOEMAKER-LEVY 9-J'),
(50000014, 'SHOEMAKER-LEVY 9-H'),
(50000015, 'SHOEMAKER-LEVY 9-G'),
(50000016, 'SHOEMAKER-LEVY 9-F'),
(50000017, 'SHOEMAKER-LEVY 9-E'),
(50000018, 'SHOEMAKER-LEVY 9-D'),
(50000019, 'SHOEMAKER-LEVY 9-C'),
(50000020, 'SHOEMAKER-LEVY 9-B'),
(50000021, 'SHOEMAKER-LEVY 9-A'),
(50000022, 'SHOEMAKER-LEVY 9-Q1'),
(50000023, 'SHOEMAKER-LEVY 9-P2'),
(1000001, 'AREND'),
(1000002, 'AREND-RIGAUX'),
(1000003, 'ASHBROOK-JACKSON'),
(1000004, 'BOETHIN'),
(1000005, 'BORRELLY'),
(1000006, 'BOWELL-SKIFF'),
(1000007, 'BRADFIELD'),
(1000008, 'BROOKS 2'),
(1000009, 'BRORSEN-METCALF'),
(1000010, 'BUS'),
(1000011, 'CHERNYKH'),
(1000012, '67P/CHURYUMOV-GERASIMENKO (1969 R1)'),
(1000012, 'CHURYUMOV-GERASIMENKO'),
(1000013, 'CIFFREO'),
(1000014, 'CLARK'),
(1000015, 'COMAS SOLA'),
(1000016, 'CROMMELIN'),
(1000017, 'D\'ARREST'),
(1000018, 'DANIEL'),
(1000019, 'DE VICO-SWIFT'),
(1000020, 'DENNING-FUJIKAWA'),
(1000021, 'DU TOIT 1'),
(1000022, 'DU TOIT-HARTLEY'),
(1000023, 'DUTOIT-NEUJMIN-DELPORTE'),
(1000024, 'DUBIAGO'),
(1000025, 'ENCKE'),
(1000026, 'FAYE'),
(1000027, 'FINLAY'),
(1000028, 'FORBES'),
(1000029, 'GEHRELS 1'),
(1000030, 'GEHRELS 2'),
(1000031, 'GEHRELS 3'),
(1000032, 'GIACOBINI-ZINNER'),
(1000033, 'GICLAS'),
(1000034, 'GRIGG-SKJELLERUP'),
(1000035, 'GUNN'),
(1000036, 'HALLEY'),
(1000037, 'HANEDA-CAMPOS'),
(1000038, 'HARRINGTON'),
(1000039, 'HARRINGTON-ABELL'),
(1000040, 'HARTLEY 1'),
(1000041, 'HARTLEY 2'),
(1000042, 'HARTLEY-IRAS'),
(1000043, 'HERSCHEL-RIGOLLET'),
(1000044, 'HOLMES'),
(1000045, 'HONDA-MRKOS-PAJDUSAKOVA'),
(1000046, 'HOWELL'),
(1000047, 'IRAS'),
(1000048, 'JACKSON-NEUJMIN'),
(1000049, 'JOHNSON'),
(1000050, 'KEARNS-KWEE'),
(1000051, 'KLEMOLA'),
(1000052, 'KOHOUTEK'),
(1000053, 'KOJIMA'),
(1000054, 'KOPFF'),
(1000055, 'KOWAL 1'),
(1000056, 'KOWAL 2'),
(1000057, 'KOWAL-MRKOS'),
(1000058, 'KOWAL-VAVROVA'),
(1000059, 'LONGMORE'),
(1000060, 'LOVAS 1'),
(1000061, 'MACHHOLZ'),
(1000062, 'MAURY'),
(1000063, 'NEUJMIN 1'),
(1000064, 'NEUJMIN 2'),
(1000065, 'NEUJMIN 3'),
(1000066, 'OLBERS'),
(1000067, 'PETERS-HARTLEY'),
(1000068, 'PONS-BROOKS'),
(1000069, 'PONS-WINNECKE'),
(1000070, 'REINMUTH 1'),
(1000071, 'REINMUTH 2'),
(1000072, 'RUSSELL 1'),
(1000073, 'RUSSELL 2'),
(1000074, 'RUSSELL 3'),
(1000075, 'RUSSELL 4'),
(1000076, 'SANGUIN'),
(1000077, 'SCHAUMASSE'),
(1000078, 'SCHUSTER'),
(1000079, 'SCHWASSMANN-WACHMANN 1'),
(1000080, 'SCHWASSMANN-WACHMANN 2'),
(1000081, 'SCHWASSMANN-WACHMANN 3'),
(1000082, 'SHAJN-SCHALDACH'),
(1000083, 'SHOEMAKER 1'),
(1000084, 'SHOEMAKER 2'),
(1000085, 'SHOEMAKER 3'),
(1000086, 'SINGER-BREWSTER'),
(1000087, 'SLAUGHTER-BURNHAM'),
(1000088, 'SMIRNOVA-CHERNYKH'),
(1000089, 'STEPHAN-OTERMA'),
(1000090, 'SWIFT-GEHRELS'),
(1000091, 'TAKAMIZAWA'),
(1000092, 'TAYLOR'),
(1000093, 'TEMPEL_1'),
(1000093, 'TEMPEL 1'),
(1000094, 'TEMPEL 2'),
(1000095, 'TEMPEL-TUTTLE'),
(1000096, 'TRITTON'),
(1000097, 'TSUCHINSHAN 1'),
(1000098, 'TSUCHINSHAN 2'),
(1000099, 'TUTTLE'),
(1000100, 'TUTTLE-GIACOBINI-KRESAK'),
(1000101, 'VAISALA 1'),
(1000102, 'VAN BIESBROECK'),
(1000103, 'VAN HOUTEN'),
(1000104, 'WEST-KOHOUTEK-IKEMURA'),
(1000105, 'WHIPPLE'),
(1000106, 'WILD 1'),
(1000107, 'WILD 2'),
(1000108, 'WILD 3'),
(1000109, 'WIRTANEN'),
(1000110, 'WOLF'),
(1000111, 'WOLF-HARRINGTON'),
(1000112, 'LOVAS 2'),
(1000113, 'URATA-NIIJIMA'),
(1000114, 'WISEMAN-SKIFF'),
(1000115, 'HELIN'),
(1000116, 'MUELLER'),
(1000117, 'SHOEMAKER-HOLT 1'),
(1000118, 'HELIN-ROMAN-CROCKETT'),
(1000119, 'HARTLEY 3'),
(1000120, 'PARKER-HARTLEY'),
(1000121, 'HELIN-ROMAN-ALU 1'),
(1000122, 'WILD 4'),
(1000123, 'MUELLER 2'),
(1000124, 'MUELLER 3'),
(1000125, 'SHOEMAKER-LEVY 1'),
(1000126, 'SHOEMAKER-LEVY 2'),
(1000127, 'HOLT-OLMSTEAD'),
(1000128, 'METCALF-BREWINGTON'),
(1000129, 'LEVY'),
(1000130, 'SHOEMAKER-LEVY 9'),
(1000131, 'HYAKUTAKE'),
(1000132, 'HALE-BOPP'),
(1003228, 'C/2013 A1'),
(1003228, 'SIDING SPRING'),
(9511010, 'GASPRA'),
(2431010, 'IDA'),
(2431011, 'DACTYL'),
(2000001, 'CERES'),
(2000002, 'PALLAS'),
(2000004, 'VESTA'),
(2000021, 'LUTETIA'),
(2000216, 'KLEOPATRA'),
(2000433, 'EROS'),
(2000511, 'DAVIDA'),
(2000253, 'MATHILDE'),
(2002867, 'STEINS'),
(2009969, '1992KD'),
(2009969, 'BRAILLE'),
(2004015, 'WILSON-HARRINGTON'),
(2004179, 'TOUTATIS'),
(2025143, 'ITOKAWA'),
(398989, 'NOTO'),
(398990, 'NEW NORCIA'),
(399001, 'GOLDSTONE'),
(399002, 'CANBERRA'),
(399003, 'MADRID'),
(399004, 'USUDA'),
(399005, 'DSS-05'),
(399005, 'PARKES'),
(399012, 'DSS-12'),
(399013, 'DSS-13'),
(399014, 'DSS-14'),
(399015, 'DSS-15'),
(399016, 'DSS-16'),
(399017, 'DSS-17'),
(399023, 'DSS-23'),
(399024, 'DSS-24'),
(399025, 'DSS-25'),
(399026, 'DSS-26'),
(399027, 'DSS-27'),
(399028, 'DSS-28'),
(399033, 'DSS-33'),
(399034, 'DSS-34'),
(399042, 'DSS-42'),
(399043, 'DSS-43'),
(399045, 'DSS-45'),
(399046, 'DSS-46'),
(399049, 'DSS-49'),
(399053, 'DSS-53'),
(399054, 'DSS-54'),
(399055, 'DSS-55'),
(399061, 'DSS-61'),
(399063, 'DSS-63'),
(399064, 'DSS-64'),
(399065, 'DSS-65'),
(399066, 'DSS-66'),
# ftp://ssd.jpl.nasa.gov/pub/eph/planets/bsp/README.txt
(1000000000, 'TDB'),
(1000000001, 'TT'),
]
target_names = dict(target_name_pairs)
+193
View File
@@ -0,0 +1,193 @@
"""Compute things from a NASA SPICE binary PCK kernel file.
ftp://naif.jpl.nasa.gov/pub/naif/toolkit_docs/C/req/pck.html
"""
from numpy import array, rollaxis
from .calendar import compute_calendar_date
from .daf import DAF
from .names import target_names
T0 = 2451545.0
S_PER_DAY = 86400.0
def jd(seconds):
"""Convert a number of seconds since J2000 to a Julian Date."""
return T0 + seconds / S_PER_DAY
class PCK(object):
"""A JPL binary PCK (extension ``.bcp``) kernel.
You can load a binary PCK file by specifying its filename::
kernel = BinaryPCK.open('moon_pa_de421_1900-2050.bpc')
Run ``print(kernel)`` see which segments are inside and iterate
across ``kernel.segments`` to access them each in turn.
To see the text comments, call ``kernel.comments()``.
"""
def __init__(self, daf):
self.daf = daf
self.segments = [Segment(self.daf, source, descriptor)
for source, descriptor in self.daf.summaries()]
@classmethod
def open(cls, path):
"""Open the file at `path` and return a binary PCK instance."""
return cls(DAF(open(path, 'rb')))
def close(self):
"""Close this file."""
self.daf.file.close()
for segment in self.segments:
if hasattr(segment, '_data'):
del segment._data # TODO: explicitly close each memory map
def __str__(self):
daf = self.daf
d = lambda b: b.decode('latin-1')
lines = (str(segment) for segment in self.segments)
return 'File type {0} and format {1} with {2} segments:\n{3}'.format(
d(daf.locidw), d(daf.locfmt), len(self.segments), '\n'.join(lines))
def comments(self):
"""Return the file comments, as a string."""
return self.daf.comments()
class Segment(object):
"""A single segment of a binary PCK file.
There are several items of information about each segment that are
loaded from the underlying PCK file, and made available as object
attributes:
segment.source - official ephemeris name, like 'DE-0430LE-0430'
segment.initial_second - initial epoch, as seconds from J2000
segment.final_second - final epoch, as seconds from J2000
segment.body - integer body 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
"""
def __init__(self, daf, source, descriptor):
self.daf = daf
self.source = source
(self.initial_second, self.final_second, self.body, self.frame,
self.data_type, self.start_i, self.end_i) = descriptor
self.initial_jd = jd(self.initial_second)
self.final_jd = jd(self.final_second)
self._data = None
def __str__(self):
return self.describe(verbose=False)
def describe(self, verbose=True):
"""Return a textual description of the segment."""
body = titlecase(target_names.get(self.body, 'Unknown body'))
text = ('{0.initial_jd:.2f}..{0.final_jd:.2f} frame={0.frame}'
' {1} ({0.body})'.format(self, body))
if verbose:
text += ('\n data_type={0.data_type} source={1}'
.format(self, self.source.decode('ascii')))
return text
def _load(self):
"""Map the coefficients into memory using a NumPy array.
"""
if self.data_type == 2:
component_count = 3
else:
raise ValueError('only binary PCK data type 2 is supported')
init, intlen, rsize, n = self.daf.read_array(self.end_i - 3, self.end_i)
coefficient_count = int(rsize - 2) // component_count
coefficients = self.daf.map_array(self.start_i, self.end_i - 4)
coefficients.shape = (int(n), int(rsize))
coefficients = coefficients[:,2:] # ignore MID and RADIUS elements
coefficients.shape = (int(n), component_count, coefficient_count)
coefficients = rollaxis(coefficients, 1)
coefficients = rollaxis(coefficients, 2)
coefficients = coefficients[::-1]
return init, intlen, coefficients
def compute(self, tdb, tdb2, derivative=True):
"""Generate angles and derivatives for time `tdb` plus `tdb2`.
If ``derivative`` is true, return a tuple containing both the
angle and its derivative; otherwise simply return the angles.
"""
scalar = not getattr(tdb, 'shape', 0) and not getattr(tdb2, 'shape', 0)
if scalar:
tdb = array((tdb,))
data = self._data
if data is None:
self._data = data = self._load()
init, intlen, coefficients = data
coefficient_count, component_count, n = coefficients.shape
# Subtracting init before adding tdb2 affords greater precision.
seconds = (tdb - T0) * S_PER_DAY - init + tdb2 * S_PER_DAY
index, offset = divmod(seconds, intlen)
index = index.astype(int)
if (index < 0).any() or (index > n).any():
raise ValueError(
'segment only covers dates %d-%02d-%02d through %d-%02d-%02d'
% (compute_calendar_date(self.initial_jd + 0.5) +
compute_calendar_date(self.final_jd + 0.5))
)
omegas = (index == n)
index[omegas] -= 1
offset[omegas] += intlen
coefficients = coefficients[:,:,index]
# Chebyshev polynomial.
s = 2.0 * offset / intlen - 1.0
s2 = 2.0 * s
w0 = w1 = dw0 = dw1 = 0.0
for coefficient in coefficients[:-1]:
w2 = w1
w1 = w0
w0 = coefficient + (s2 * w1 - w2)
if derivative: # TODO: defer to a second loop
dw2 = dw1
dw1 = dw0
dw0 = 2.0 * w1 + dw1 * s2 - dw2
components = coefficients[-1] + (s * w0 - w1)
if scalar:
components = components[:,0]
if not derivative:
return components
# Chebyshev differentiation.
rates = w0 + s * dw0 - dw1
rates /= intlen
rates *= 2.0
if scalar:
rates = rates[:,0]
return components, rates
def titlecase(name):
"""Title-case body `name` if it looks safe to do so."""
return name if name.startswith(('1', 'C/', 'DSS-')) else name.title()
+340
View File
@@ -0,0 +1,340 @@
"""Compute positions from a NASA SPICE SPK ephemeris kernel file.
http://naif.jpl.nasa.gov/pub/naif/toolkit_docs/FORTRAN/req/spk.html
"""
from numpy import array, interp, rollaxis
from .calendar import compute_calendar_date
from .daf import DAF
from .descriptorlib import reify
from .exceptions import OutOfRangeError
from .names import target_names
T0 = 2451545.0
S_PER_DAY = 86400.0
def _jd(seconds):
"""Convert a number of seconds since J2000 to a Julian Date."""
return T0 + seconds / S_PER_DAY
class SPK(object):
"""A JPL SPK ephemeris kernel for computing positions and velocities.
You can load an SPK by specifying its filename::
kernel = SPK.open('de431.bsp')
Run ``print(kernel)`` to list the ephemeris segments. You can also
loop across all of the segments in the list ``kernel.segments`` or,
as a convenience, you can select a particular segment by providing a
center and target integer in square brackets. So ``kernel[3,399]``
will select the segment that computes the distance between the
Earth-Moon barycenter (3) and the Earth itself (399).
To extract the text comments from the SPK use ``kernel.comments()``.
"""
def __init__(self, daf):
self.daf = daf
self.segments = [
build_segment(self.daf, source, descriptor)
for source, descriptor in self.daf.summaries()
]
self.pairs = dict(((s.center, s.target), s) for s in self.segments)
@classmethod
def open(cls, path):
"""Open the file at `path` and return an SPK instance."""
f = open(path, 'rb')
try:
return cls(DAF(f))
except Exception:
f.close()
raise
def close(self):
"""Close this SPK file."""
self.daf.file.close()
for segment in self.segments:
if '_data' in segment.__dict__:
del segment._data
self.daf._array = None
self.daf._map = None
def __str__(self):
daf = self.daf
d = lambda b: b.decode('latin-1')
lines = [
'File type {0} and format {1} with {2} segments:'
.format(d(daf.locidw), d(daf.locfmt), len(self.segments))
]
lines.extend(str(segment) for segment in self.segments)
return '\n'.join(lines)
def __getitem__(self, key):
"""Given (center, target) integers, return the last matching segment."""
return self.pairs[key]
def comments(self):
"""Return the file comments, as a string."""
return self.daf.comments()
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
def build_segment(daf, source, descriptor):
data_type = descriptor[5]
cls = _segment_classes.get(data_type, BaseSegment)
return cls(daf, source, descriptor)
class BaseSegment(object):
"""A single segment of an SPK file.
There are several items of information about each segment that are
loaded from the underlying SPK file, and made available as object
attributes:
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
"""
_data = None
def __init__(self, daf, source, descriptor):
self.daf = daf
self.source = source
(self.start_second, self.end_second, self.target, self.center,
self.frame, self.data_type, self.start_i, self.end_i) = descriptor
self.start_jd = _jd(self.start_second)
self.end_jd = _jd(self.end_second)
def __str__(self):
return self.describe(verbose=False)
def describe(self, verbose=True):
"""Return a textual description of the segment."""
start = '%d-%02d-%02d' % compute_calendar_date(self.start_jd + 0.5)
end = '%d-%02d-%02d' % compute_calendar_date(self.end_jd + 0.5)
center = titlecase(target_names.get(self.center, 'Unknown center'))
target = titlecase(target_names.get(self.target, 'Unknown target'))
text = ('{1}..{2} Type {0.data_type}'
' {3} ({0.center}) -> {4} ({0.target})'
.format(self, start, end, center, target))
if verbose:
text += ('\n frame={0.frame} source={1}'
.format(self, self.source.decode('ascii')))
return text
def compute(self, tdb, tdb2=0.0):
"""Compute the component values for the time `tdb` plus `tdb2`."""
raise ValueError(
'jplephem has not yet learned how to compute positions'
' from an ephemeris segment with data type {0}'
.format(self.data_type)
)
def compute_and_differentiate(self, tdb, tdb2=0.0):
"""Compute components and differentials for time `tdb` plus `tdb2`."""
raise ValueError(
'jplephem has not yet learned how to compute positions and'
' velocities from an ephemeris segment with data type {0}'
.format(self.data_type)
)
class Segment(BaseSegment):
# Type 2 or type 3 segment.
def compute(self, tdb, tdb2=0.0):
"""Compute the component values for the time `tdb` plus `tdb2`."""
for position in self.generate(tdb, tdb2):
return position
def compute_and_differentiate(self, tdb, tdb2=0.0):
"""Compute components and differentials for time `tdb` plus `tdb2`."""
return tuple(self.generate(tdb, tdb2))
@reify
def _data(self):
"""Map the coefficients into memory using a NumPy array.
"""
if self.data_type == 2:
component_count = 3
elif self.data_type == 3:
component_count = 6
else:
raise ValueError('this class only supports SPK data types 2 and 3')
init, intlen, rsize, n = self.daf.read_array(self.end_i - 3, self.end_i)
coefficient_count = int(rsize - 2) // component_count
coefficients = self.daf.map_array(self.start_i, self.end_i - 4)
coefficients.shape = (int(n), int(rsize))
coefficients = coefficients[:,2:] # ignore MID and RADIUS elements
coefficients.shape = (int(n), component_count, coefficient_count)
coefficients = rollaxis(coefficients, 1)
coefficients = rollaxis(coefficients, 2)
coefficients = coefficients[::-1]
return init, intlen, coefficients
def load_array(self):
init, intlen, coefficients = self._data
initial_epoch = _jd(init)
interval_length = intlen / S_PER_DAY
coefficients = coefficients[::-1]
coefficients = rollaxis(coefficients, 2)
coefficients = rollaxis(coefficients, 2)
return initial_epoch, interval_length, coefficients
def generate(self, tdb, tdb2):
"""Generate components and differentials for time `tdb` plus `tdb2`.
Most uses will simply want to call the `compute()` method or the
`compute_differentials()` method, for convenience. But in those
cases (see Skyfield) where you want to compute a position and
examine it before deciding whether to proceed with the velocity,
but without losing all of the work that it took to get to that
point, this generator lets you get them as two separate steps.
"""
scalar = not getattr(tdb, 'shape', 0) and not getattr(tdb2, 'shape', 0)
if scalar:
tdb = array((tdb,))
init, intlen, coefficients = self._data
coefficient_count, component_count, n = coefficients.shape
# Keeping fractions strictly separate from whole numbers
# maintains the highest possible precision.
index1, offset1 = divmod((tdb - T0) * S_PER_DAY - init, intlen)
index2, offset2 = divmod(tdb2 * S_PER_DAY, intlen)
index3, offset = divmod(offset1 + offset2, intlen)
index = (index1 + index2 + index3).astype(int)
if (index < 0).any() or (index > n).any():
raise OutOfRangeError(
'segment only covers dates %d-%02d-%02d through %d-%02d-%02d'
% (compute_calendar_date(self.start_jd + 0.5) +
compute_calendar_date(self.end_jd + 0.5)),
out_of_range_times=(index < 0) | (index > n),
)
omegas = (index == n)
index[omegas] -= 1
offset[omegas] += intlen
coefficients = coefficients[:,:,index]
# Chebyshev polynomial.
s = 2.0 * offset / intlen - 1.0
s2 = 2.0 * s
w0 = w1 = 0.0
wlist = []
for coefficient in coefficients[:-1]:
w2 = w1
w1 = w0
w0 = coefficient + (s2 * w1 - w2)
wlist.append(w1)
components = coefficients[-1] + (s * w0 - w1)
if scalar:
components = components[:,0]
yield components
# Chebyshev differentiation.
dw0 = dw1 = 0.0
for coefficient, w1 in zip(coefficients[:-1], wlist):
dw2 = dw1
dw1 = dw0
dw0 = 2.0 * w1 + dw1 * s2 - dw2
rates = w0 + s * dw0 - dw1
rates /= intlen
rates *= 2.0
rates *= S_PER_DAY
if scalar:
rates = rates[:,0]
yield rates
class Type9Segment(BaseSegment):
"""Lagrange Interpolation - Unequal Time Steps"""
def map_arrays(self):
"""Raw coefficients and epochs as memory-mapped NumPy arrays."""
i = self.end_i
polynomial_degree, number_of_states = self.daf.read_array(i - 1, i)
if polynomial_degree != 1:
raise ValueError('jplephem does not yet support Type 9 segments'
' with a polynomial degree of {0}'
.format(polynomial_degree))
number_of_states = int(number_of_states)
i = self.start_i
j = i + 6 * number_of_states - 1
coefficients = self.daf.map_array(i, j)
coefficients.shape = number_of_states, 6
coefficients = coefficients.T
epochs = self.daf.map_array(j + 1, j + number_of_states)
return coefficients, epochs
@reify
def _data(self):
"""Cached arrays that are ready for interpolation."""
coefficients, epochs = self.map_arrays()
# Make iteration faster by pre-creating tuples of separate arrays.
positions = tuple(coefficients[:3])
and_velocities = tuple(coefficients)
epochs = _jd(epochs)
return positions, and_velocities, epochs
def compute(self, tdb, tdb2=0.0):
"""Interpolate [x y z] at time `tdb` plus `tdb2`.
A standard JPL Type 9 ephemerides will return kilometers.
"""
positions, and_velocities, epochs = self._data
return array([interp(tdb, epochs, c) for c in positions])
def compute_and_differentiate(self, tdb, tdb2=0.0):
"""Interpolate [x y z dx dy dz] at time `tdb` plus `tdb2`.
A standard JPL Type 9 ephemerides will return kilometers and
kilometers per second.
"""
positions, and_velocities, epochs = self._data
return array([interp(tdb, epochs, c) for c in and_velocities])
def titlecase(name):
"""Title-case target `name` if it looks safe to do so."""
return name if name.startswith(('1', 'C/', 'DSS-')) else name.title()
_segment_classes = {
2: Segment,
3: Segment,
9: Type9Segment,
}
+627
View File
@@ -0,0 +1,627 @@
"""Tests for ``jplephem``.
See the accompanying ``jpltest`` module for a more intense numerical
test suite that can verify that ``jplephem`` delivers, over hundreds of
examples, the same results as when the ephemerides are run at JPL. This
smaller and more feature-oriented suite can be run with::
python -m unittest discover jplephem
"""
import gc
import mmap
import numpy as np
import sys
import tempfile
import warnings
from doctest import DocTestSuite, ELLIPSIS
from functools import partial
from io import BytesIO
from jplephem import Ephemeris, commandline
from jplephem.exceptions import OutOfRangeError
from jplephem.daf import DAF, FTPSTR, NAIF_DAF
from jplephem.pck import PCK
from jplephem.spk import SPK
from struct import Struct
try:
from unittest import SkipTest, TestCase
except ImportError:
from unittest2 import SkipTest, TestCase
epsilon_m = 0.01
target_names = {
'mercury barycenter': 1, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'venus barycenter': 2, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'earthmoon': 3, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'mars barycenter': 4, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'jupiter': 5, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'saturn': 6, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'uranus': 7, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'neptune': 8, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'pluto': 9, # BARYCENTER w.r.t. 0 SOLAR SYSTEM BARYCENTER
'sun': 10, # w.r.t. 0 SOLAR SYSTEM BARYCENTER
'mercury': 199, # w.r.t. 1 MERCURY BARYCENTER
'venus': 299, # w.r.t. 2 VENUS BARYCENTER
'moon': 301, # w.r.t. 3 EARTH BARYCENTER
'earth': 399, # w.r.t. 3 EARTH BARYCENTER
'mars': 499, # w.r.t. 4 MARS BARYCENTER
}
class TestDAFBytesIO(TestCase):
def sample_daf(self):
word = Struct('<d').pack
integer = Struct('<i').pack
return BytesIO(b''.join([
# Record 1 - File Record
b'DAF/SPK ',
b'\x02\x00\x00\x00', # ND
b'\x03\x00\x00\x00', # NI
b'Internal Name'.ljust(60, b' '), # LOCIFN
b'\x03\x00\x00\x00', # FWARD
b'\x07\x00\x00\x00', # BWARD
b'\x01\x04\x00\x00', # FREE
b'LTL-IEEE', # LOCFMT
b'\0' * 603, # PRENUL
FTPSTR,
b'\0' * 297, # PSTNUL
# Record 2
b'Comment Record'.ljust(1024, b'\0'),
# Record 3 - first Summary Record
b''.join([
word(7), # next summary record
word(0), # previous summary record
word(1), # number of summaries
word(101),
word(202),
integer(303),
integer(1024 * 4 // 8 + 1), # Record 5 start
integer(1024 * 5 // 8), # Record 5 end
integer(0),
]).ljust(1024, b'\0'),
# Record 4 - first Name Record
b'Summary Name 1'.ljust(1024, b' '),
# Record 5
word(1001) * 128,
# Record 6
word(2002) * 128,
# Record 7 - second Summary Record
b''.join([
word(0), # next summary record
word(3), # previous summary record
word(1), # number of summaries
word(111),
word(222),
integer(333),
integer(1024 * 5 // 8 + 1), # Record 6 start
integer(1024 * 6 // 8), # Record 6 end
integer(0),
]).ljust(1024, b'\0'),
# Record 8 - second Name Record
b'Summary Name 2'.ljust(1024, b' '),
]))
def test_header(self):
f = self.sample_daf()
d = DAF(f)
eq = self.assertEqual
eq(d.locidw, b'DAF/SPK')
eq(d.nd, 2)
eq(d.ni, 3)
eq(d.locifn_text, b'Internal Name')
eq(d.fward, 3)
eq(d.bward, 7)
eq(d.free, 0x401)
eq(d.locfmt, b'LTL-IEEE')
def test_segments(self):
f = self.sample_daf()
d = DAF(f)
summaries = list(d.summaries())
eq = self.assertEqual
eq(len(summaries), 2)
eq(summaries[0], (b'Summary Name 1', (101.0, 202.0, 303, 513, 640)))
eq(summaries[1], (b'Summary Name 2', (111.0, 222.0, 333, 641, 768)))
eq = self.assertSequenceEqual
eq(list(d.map(summaries[0][1])), [1001.0] * 128)
eq(list(d.map(summaries[1][1])), [2002.0] * 128)
def test_add_segment(self):
f = self.sample_daf()
d = DAF(f)
d.add_array(b'Summary Name 3', (121.0, 232.0, 343), [3003.0] * 128)
summaries = list(d.summaries())
eq = self.assertEqual
eq(len(summaries), 3)
eq(summaries[0], (b'Summary Name 1', (101.0, 202.0, 303, 513, 640)))
eq(summaries[1], (b'Summary Name 2', (111.0, 222.0, 333, 641, 768)))
eq(summaries[2], (b'Summary Name 3', (121.0, 232.0, 343, 1025, 1152)))
eq = self.assertSequenceEqual
eq(list(d.map(summaries[0][1])), [1001.0] * 128)
eq(list(d.map(summaries[1][1])), [2002.0] * 128)
eq(list(d.map(summaries[2][1])), [3003.0] * 128)
def test_add_segment_when_summary_block_is_full(self):
f = self.sample_daf()
d = DAF(f)
# Update n_summaries of final summary block to full.
d.file.seek(6 * 1024 + 16)
d.file.write(Struct('<d').pack(d.summaries_per_record))
d.add_array(b'Summary Name 3', (121.0, 232.0, 343), [3003.0] * 200)
# Reset n_summaries of that block back to its real value.
d.file.seek(6 * 1024 + 16)
d.file.write(Struct('<d').pack(1))
summaries = list(d.summaries())
eq = self.assertEqual
eq(len(summaries), 3)
eq(summaries[0], (b'Summary Name 1', (101.0, 202.0, 303, 513, 640)))
eq(summaries[1], (b'Summary Name 2', (111.0, 222.0, 333, 641, 768)))
eq(summaries[2], (b'Summary Name 3', (121.0, 232.0, 343, 1281, 1480)))
eq = self.assertSequenceEqual
eq(list(d.map(summaries[0][1])), [1001.0] * 128)
eq(list(d.map(summaries[1][1])), [2002.0] * 128)
eq(list(d.map(summaries[2][1])), [3003.0] * 200)
class TestDAFRealFile(TestDAFBytesIO):
# Where "Real" = "written to disk with a real file descriptor
# instead of an in-memory BytesIO".
def sample_daf(self):
bytes_io = super(TestDAFRealFile, self).sample_daf()
f = tempfile.NamedTemporaryFile(mode='w+b', prefix='jplephem_test')
f.write(bytes_io.getvalue())
f.seek(0)
return f
def fake_mmap_that_raises_OSError(*args, **kw):
raise OSError('mmap() not supported on this platform')
class TestDAFRealFileWithoutMMap(TestDAFRealFile):
# Where "Real" = "written to disk with a real file descriptor
# instead of an in-memory BytesIO". And we turn off mmap() to
# simulate platforms like pyodide.
def setUp(self):
self.mmap = mmap.mmap
mmap.mmap = fake_mmap_that_raises_OSError
def tearDown(self):
mmap.mmap = self.mmap
class _CommonTests(object):
def check0(self, xyz, xyzdot=None):
eq = partial(self.assertAlmostEqual, delta=epsilon_m)
x, y, z = xyz
eq(x, 39705023.28)
eq(y, 131195345.65)
eq(z, 56898495.41)
if xyzdot is None:
return
dx, dy, dz = xyzdot
eq(dx, -2524248.19)
eq(dy, 619970.11)
eq(dz, 268928.26)
def check1(self, xyz, xyzdot=None):
eq = partial(self.assertAlmostEqual, delta=epsilon_m)
x, y, z = xyz
eq(x, -144692624.00)
eq(y, -32707965.14)
eq(z, -14207167.26)
if xyzdot is None:
return
dx, dy, dz = xyzdot
eq(dx, 587334.38)
eq(dy, -2297419.36)
eq(dz, -996628.74)
def test_scalar_tdb(self):
self.check0(self.position('earthmoon', 2414994.0))
self.check1(self.position('earthmoon', 2415112.5))
def test_scalar_tdb2(self):
self.check0(self.position('earthmoon', 2414990.0, 4.0))
self.check1(self.position('earthmoon', 2415110.0, 2.5))
def test_scalar_tdb_keyword(self):
self.check0(self.position('earthmoon', tdb=2414994.0))
self.check1(self.position('earthmoon', tdb=2415112.5))
def test_scalar_tdb2_keyword(self):
self.check0(self.position('earthmoon', tdb=2414990.0, tdb2=4.0))
self.check1(self.position('earthmoon', tdb=2415110.0, tdb2=2.5))
def check_2d_result(self, name, tdb, tdb2):
p = self.position(name, tdb + tdb2)
self.check0(p[:,0])
self.check1(p[:,1])
p = self.position(name, tdb, tdb2)
self.check0(p[:,0])
self.check1(p[:,1])
p, v = self.position_and_velocity(name, tdb + tdb2)
self.check0(p[:,0], v[:,0])
self.check1(p[:,1], v[:,1])
p, v = self.position_and_velocity(name, tdb, tdb2)
self.check0(p[:,0], v[:,0])
self.check1(p[:,1], v[:,1])
def test_array_tdb(self):
tdb = np.array([2414994.0, 2415112.5])
tdb2 = 0.0
self.check_2d_result('earthmoon', tdb, tdb2)
def test_array_tdb_scalar_tdb2(self):
tdb = np.array([2414991.5, 2415110.0])
tdb2 = 2.5
self.check_2d_result('earthmoon', tdb, tdb2)
def test_scalar_tdb_array_tdb2(self):
tdb = 2414990.0
d = 2415112.5 - tdb
tdb2 = np.array([4.0, d])
self.check_2d_result('earthmoon', tdb, tdb2)
def test_array_tdb_array_tdb2(self):
tdb = np.array([2414990.0, 2415110.0])
tdb2 = np.array([4.0, 2.5])
self.check_2d_result('earthmoon', tdb, tdb2)
def test_jitter(self):
usecond = 1e-6 / 24.0 / 3600.0
tdb = np.ones(12) * 2414998.0
tdb2 = np.linspace(1.0 * usecond, 2.0 * usecond, 12)
x, y, z = self.position('earthmoon', tdb, tdb2)
for component in x, y, z:
size = component[0]
relative_jitter = np.diff(np.diff(x)) / size
self.assertLess(max(abs(relative_jitter)), 3e-16)
def test_ephemeris_end_date(self):
x, y, z = self.position('earthmoon', self.jomega)
# These positions are actually from HORIZONS and thus DE431,
# hence the low precision match:
self.assertAlmostEqual(x, 1.442502234663646E+08, delta=1.0)
self.assertAlmostEqual(y, 3.690043031712407E+07, delta=1.0)
self.assertAlmostEqual(z, 1.599543968176661E+07, delta=1.0)
def test_too_early_date(self):
tdb = self.jalpha - 0.01
self.assertRaises(ValueError, self.position, 'earthmoon', tdb)
def test_too_late_date(self):
tdb = self.jomega + 16.01
self.assertRaises(ValueError, self.position, 'earthmoon', tdb)
class SPKTests(_CommonTests, TestCase):
def setUp(self):
try:
self.spk = SPK.open('de421.bsp')
except IOError:
raise SkipTest('the "de421.bsp" SPK file is not available')
segment = self.spk[0,1]
self.jalpha = segment.start_jd
self.jomega = segment.end_jd
def tearDown(self):
self.spk.close()
if sys.version_info < (3,):
return
# With thanks for https://stackoverflow.com/questions/24717027/
with warnings.catch_warnings(record=True) as w:
warnings.resetwarnings()
warnings.simplefilter('always', ResourceWarning)
del self.spk
gc.collect()
self.assertFalse(w and str(w[-1]))
def position(self, name, tdb, tdb2=0.0):
segment = self.spk[0, target_names[name]]
return segment.compute(tdb, tdb2)
def position_and_velocity(self, name, tdb, tdb2=0.0):
segment = self.spk[0, target_names[name]]
return segment.compute_and_differentiate(tdb, tdb2)
def test_segment_with_only_two_coefficients(self):
tdb = 2414990.0
tup = target_names['mercury barycenter'], target_names['mercury']
segment = self.spk[tup]
segment.compute_and_differentiate(tdb)
def test_str(self):
str(self.spk) # just to confirm it does not raise an exception
segment = self.spk[0,4]
self.assertEqual(str(segment), segment.describe(verbose=False))
self.assertEqual(segment.describe(verbose=False),
'1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Mars Barycenter (4)')
self.assertEqual(segment.describe(verbose=True),
'1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Mars Barycenter (4)'
'\n frame=1 source=DE-0421LE-0421')
def test_loading_array(self):
segment = self.spk[0,4]
initial_epoch, interval_length, coefficients = segment.load_array()
self.assertEqual(coefficients.shape, (3, 1760, 11))
def test_out_of_range_dates(self):
segment = self.spk[0,4]
tdb = np.array([-1e3, 0, +1e5]) + 2414990.0
try:
segment.compute_and_differentiate(tdb)
except OutOfRangeError as e:
self.assertEqual(str(e), 'segment only covers dates'
' 1899-07-29 through 2053-10-09')
self.assertIs(type(e.out_of_range_times), np.ndarray)
self.assertEqual(list(e.out_of_range_times), [True, False, True])
def test_whether_bad_ephemeris_leaves_file_open(self):
# This doesn't actually fail if the file object is left open,
# but should produce a ResourceWarning as a side effect.
non_spk_path = __file__
with self.assertRaises(ValueError):
SPK.open(non_spk_path)
class LegacyTests(_CommonTests, TestCase):
def setUp(self):
try:
import de421
except ImportError:
raise SkipTest('the "de421" ephemeris package has not been'
' installed with "pip install de421"')
self.eph = Ephemeris(de421)
self.jalpha = self.eph.jalpha
self.jomega = self.eph.jomega
def position(self, name, tdb, tdb2=0.0):
return self.eph.position(name, tdb, tdb2)
def position_and_velocity(self, name, tdb, tdb2=0.0):
return self.eph.position_and_velocity(name, tdb, tdb2)
def test_names(self):
self.assertEqual(self.eph.names, (
'earthmoon', 'jupiter', 'librations', 'mars', 'mercury',
'moon', 'neptune', 'nutations', 'pluto', 'saturn', 'sun',
'uranus', 'venus',
))
def test_legacy_compute_method(self):
pv = self.eph.compute('earthmoon', 2414994.0)
self.check0(pv[:3], pv[3:])
pv = self.eph.compute('earthmoon', np.array([2414994.0, 2415112.5]))
self.check0(pv[:3,0], pv[3:,0])
self.check1(pv[:3,1], pv[3:,1])
def test_ephemeris_end_date(self):
x, y, z = self.position('earthmoon', self.jomega)
self.assertAlmostEqual(x, -94189805.73967789, delta=epsilon_m)
self.assertAlmostEqual(y, 1.05103857e+08, delta=1.0)
self.assertAlmostEqual(z, 45550861.44383482, delta=epsilon_m)
class PCKTests(TestCase):
def test_out_of_range_date(self):
p = PCK.open('moon_pa_de421_1900-2050.bpc')
segment = p.segments[0]
expect = 'segment only covers dates 1900-01-01 through 2051-01-01'
with self.assertRaisesRegex(ValueError, expect):
segment.compute(0.0, 0.0)
p.close()
class NAIF_DAF_Tests(TestCase):
def test_single_position(self):
with SPK(NAIF_DAF(open('de405.bsp', 'rb'))) as kernel:
x, y, z = kernel[0,4].compute(2457061.5)
# Expect rough agreement with a DE430 position from our README:
self.assertAlmostEqual(x, 2.05700211e+08, delta=2.0)
self.assertAlmostEqual(y, 4.25141646e+07, delta=2.0)
self.assertAlmostEqual(z, 1.39379183e+07, delta=2.0)
class CommandLineTests(TestCase):
maxDiff = 9999
def test_comment_command(self):
output = commandline.main(['comment', 'de405.bsp'])
self.assertEqual(output[:30], '; de405.bsp LOG FILE\n;\n; Creat')
self.assertEqual(output[-30:], "rom Standish's DE405 memo <<<\n")
def test_daf_command(self):
self.assertEqual(commandline.main(['daf', 'de405.bsp']), """\
1 DE-405 -1577879958.8160586 1577880064.1839132 1 0 1 2 1409 202316
2 DE-405 -1577879958.8160586 1577880064.1839132 2 0 1 2 202317 275376
3 DE-405 -1577879958.8160586 1577880064.1839132 3 0 1 2 275377 368983
4 DE-405 -1577879958.8160586 1577880064.1839132 4 0 1 2 368984 408957
5 DE-405 -1577879958.8160586 1577880064.1839132 5 0 1 2 408958 438653
6 DE-405 -1577879958.8160586 1577880064.1839132 6 0 1 2 438654 464923
7 DE-405 -1577879958.8160586 1577880064.1839132 7 0 1 2 464924 487767
8 DE-405 -1577879958.8160586 1577880064.1839132 8 0 1 2 487768 510611
9 DE-405 -1577879958.8160586 1577880064.1839132 9 0 1 2 510612 533455
10 DE-405 -1577879958.8160586 1577880064.1839132 10 0 1 2 533456 613364
11 DE-405 -1577879958.8160586 1577880064.1839132 301 3 1 2 613365 987780
12 DE-405 -1577879958.8160586 1577880064.1839132 399 3 1 2 987781 1362196
13 DE-405 -1577879958.8160586 1577880064.1839132 199 1 1 2 1362197 1362208
14 DE-405 -1577879958.8160586 1577880064.1839132 299 2 1 2 1362209 1362220
15 DE-405 -1577879958.8160586 1577880064.1839132 499 4 1 2 1362221 1362232
""")
def test_spk_command(self):
self.assertEqual(commandline.main(['spk', 'de405.bsp']), """\
File type NAIF/DAF and format BIG-IEEE with 15 segments:
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Mercury Barycenter (1)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Venus Barycenter (2)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Earth Barycenter (3)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Mars Barycenter (4)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Jupiter Barycenter (5)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Saturn Barycenter (6)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Uranus Barycenter (7)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Neptune Barycenter (8)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Pluto Barycenter (9)
1950-01-01..2050-01-01 Type 2 Solar System Barycenter (0) -> Sun (10)
1950-01-01..2050-01-01 Type 2 Earth Barycenter (3) -> Moon (301)
1950-01-01..2050-01-01 Type 2 Earth Barycenter (3) -> Earth (399)
1950-01-01..2050-01-01 Type 2 Mercury Barycenter (1) -> Mercury (199)
1950-01-01..2050-01-01 Type 2 Venus Barycenter (2) -> Venus (299)
1950-01-01..2050-01-01 Type 2 Mars Barycenter (4) -> Mars (499)
""")
def test_verbose_spk_command_with_tidy_ephemeris(self):
output = commandline.main(['spk', '-v', 'de421.bsp'])
pieces = output.split('\n\n')
pieces[1:-2] = ['...']
output = '\n\n'.join(pieces)
self.assertEqual(output, """\
File type DAF/SPK and format LTL-IEEE with 15 segments:
1899-07-29..2053-10-09 Type 2 Solar System Barycenter (0) -> Mercury Barycenter (1)
7040 polynomials covering 8.0 days each
x 14 coefficients per polynomial
x 3 coordinates
= 295680 double precision floats
Polynomial start date matches segment start date
Polynomial end date matches segment end date
...
1899-07-29..2053-10-09 Type 2 Mars Barycenter (4) -> Mars (499)
1 polynomial covering 56320.0 days
x 2 coefficients per polynomial
x 3 coordinates
= 6 double precision floats
Polynomial start date matches segment start date
Polynomial end date matches segment end date
""")
def test_verbose_spk_command_with_untidy_ephemeris(self):
output = commandline.main(['spk', '-v', 'de442s.bsp'])
pieces = output.split('\n\n')
pieces[1:-2] = ['...']
output = '\n\n'.join(pieces)
self.assertEqual(output, """\
File type DAF/SPK and format LTL-IEEE with 14 segments:
1849-12-26..2150-01-22 Type 2 Venus Barycenter (2) -> Venus (299)
1 polynomial covering 401792.0 days
x 2 coefficients per polynomial
x 3 coordinates
= 6 double precision floats
First polynomial starts 109568.0 days earlier than segment start date, on 1549-12-31
Final polynomial ends 182624.0 days later than segment end date, on 2650-01-25
...
1849-12-26..2150-01-22 Type 2 Solar System Barycenter (0) -> Mercury Barycenter (1)
13701 polynomials covering 8.0 days each
x 14 coefficients per polynomial
x 3 coordinates
= 575442 double precision floats
Polynomial start date matches segment start date
Final polynomial ends 8.0 days later than segment end date, on 2150-01-30
""")
def test_excerpt_command(self):
output = commandline.main(['excerpt', '2023/8/23', '2023/8/24',
'de421.bsp', 'de421_excerpt.bsp'])
self.assertEqual(output, """\
Date 2023/8/23 = JD 2460179.5
Date 2023/8/24 = JD 2460180.5
'de421_excerpt.bsp' written successfully with the following contents
File type DAF/SPK and format LTL-IEEE with 15 segments:
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Mercury Barycenter (1)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Venus Barycenter (2)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Earth Barycenter (3)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Mars Barycenter (4)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Jupiter Barycenter (5)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Saturn Barycenter (6)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Uranus Barycenter (7)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Neptune Barycenter (8)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Pluto Barycenter (9)
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Sun (10)
2023-08-23..2023-08-24 Type 2 Earth Barycenter (3) -> Moon (301)
2023-08-23..2023-08-24 Type 2 Earth Barycenter (3) -> Earth (399)
2023-08-23..2023-08-24 Type 2 Mercury Barycenter (1) -> Mercury (199)
2023-08-23..2023-08-24 Type 2 Venus Barycenter (2) -> Venus (299)
2023-08-23..2023-08-24 Type 2 Mars Barycenter (4) -> Mars (499)
""")
preface = """\
;
; This is an ephemeris excerpt created by jplephem 2.23, which was
; asked to narrow the ephemeris to Julian dates 2460179.5 - 2460180.5
; (proleptic Gregorian dates 2023-08-23 through 2023-08-24).
;
; Here is the comments area from the original ephemeris file:
; ----------------------------------------------------------------------
"""
output1 = commandline.main(['comment', 'de421.bsp'])
output2 = commandline.main(['comment', 'de421_excerpt.bsp'])
self.assertEqual(preface + output1, output2)
output = commandline.main(['spk', '-v', 'de421_excerpt.bsp'])
pieces = output.split('\n\n')
pieces[1:-2] = ['...']
output = '\n\n'.join(pieces)
self.assertEqual(output, """\
File type DAF/SPK and format LTL-IEEE with 15 segments:
2023-08-23..2023-08-24 Type 2 Solar System Barycenter (0) -> Mercury Barycenter (1)
1 polynomial covering 8.0 days
x 14 coefficients per polynomial
x 3 coordinates
= 42 double precision floats
First polynomial starts 3.0 days earlier than segment start date, on 2023-08-20
Final polynomial ends 4.0 days later than segment end date, on 2023-08-28
...
2023-08-23..2023-08-24 Type 2 Mars Barycenter (4) -> Mars (499)
1 polynomial covering 56320.0 days
x 2 coefficients per polynomial
x 3 coordinates
= 6 double precision floats
First polynomial starts 45315.0 days earlier than segment start date, on 1899-07-29
Final polynomial ends 11004.0 days later than segment end date, on 2053-10-09
""")
def load_tests(loader, tests, ignore):
"""Run our main documentation as a test."""
# If we are running in CI, where we test against an old version of
# NumPy, skip the doctests since NumPy will print whitespace
# differently (and worse).
version = tuple(int(s) for s in np.__version__.split('.'))
if version < (1, 17):
return tests
# Python 2.6 formats floating-point numbers a bit differently and
# breaks the doctest.
if sys.version_info <= (2, 6):
return tests
tests.addTests(DocTestSuite('jplephem', optionflags=ELLIPSIS))
return tests