Nutze Skyfield fuer praezisere Daemmerungsberechnung
This commit is contained in:
+575
@@ -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
@@ -0,0 +1,4 @@
|
||||
import sys
|
||||
from .commandline import main
|
||||
sys.stdout.write(main(sys.argv[1:]))
|
||||
sys.exit(0)
|
||||
Vendored
+57
@@ -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
@@ -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
@@ -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
|
||||
Vendored
+276
@@ -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
@@ -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
|
||||
Vendored
+124
@@ -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
@@ -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
@@ -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
|
||||
Vendored
+602
@@ -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)
|
||||
Vendored
+193
@@ -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()
|
||||
Vendored
+340
@@ -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,
|
||||
}
|
||||
Vendored
+627
@@ -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
|
||||
Reference in New Issue
Block a user