
===============
 Kepler Orbits
===============

Skyfield now offers basic support for computing the position
of a comet or minor planet
whose elliptical, parabolic, or hyperbolic orbit
is provided as Kepler orbital elements.

Beware that the internal routines supporting Kepler orbits
are rudimentary and subject to change —
only the interface documented here
is guaranteed to work in future Skyfield versions.

Skyfield loads orbital elements from text files using the Pandas library.
Install it before trying any of the the examples below::

    pip install pandas

Comets
======

The `IAU Minor Planet Center <https://www.minorplanetcenter.net/>`_
distributes a
`CometEls.txt <https://www.minorplanetcenter.net/iau/MPCORB/CometEls.txt>`_
file
of orbital elements for predicting comet positions.
The file is plain text,
so feel free to open it with a text editor
to see the comets for which it offers orbital elements.
To build a dataframe of comets:

.. testsetup::

    def len(x):
        return 864

.. testcode::

    from skyfield.api import load
    from skyfield.data import mpc

    with load.open(mpc.COMET_URL) as f:
        comets = mpc.load_comets_dataframe(f)

    print(len(comets), 'comets loaded')

.. testoutput::

    864 comets loaded

Since the comets file has no explicit expiration date,
``load.open()`` will only download the file once.
Subsequent calls re-open the copy of the file already on your filesystem.
To force a fresh download and receive updated orbits and new comets,
pass ``reload=True``.

To generate a comet’s position,
first select its row from dataframe.
There are several Pandas techniques for selecting rows,
but most Skyfield users will simply index their dataframe by comet designation.

.. testcode::

    # Keep only the most recent orbit for each comet,
    # and index by designation for fast lookup.
    comets = (comets.sort_values('reference')
              .groupby('designation', as_index=False).last()
              .set_index('designation', drop=False))

    # Sample lookups.
    row = comets.loc['1P/Halley']
    row = comets.loc['C/1995 O1 (Hale-Bopp)']

When computing the position of a comet from Earth,
there is a complication:
cometary orbits are not measured from the Solar System barycenter
but are instead centered on the Sun.
You will therefore need to add the *barycenter→Sun* vector
to the *Sun→comet* vector
to produce a position that you can pass to the ``observe()`` method,
which always measures positions from the Solar System barycenter.

.. testcode::

    # Generating a position.

    from skyfield.constants import GM_SUN_DE440_km3_s2 as GM_SUN

    ts = load.timescale()
    eph = load('de421.bsp')
    sun, earth = eph['sun'], eph['earth']

    comet = sun + mpc.comet_orbit(row, ts, GM_SUN)

    t = ts.utc(2020, 5, 31)
    ra, dec, distance = earth.at(t).observe(comet).radec()
    print(ra)
    print(dec)

.. testoutput::

    23h 59m 16.85s
    -84deg 46' 57.8"

Hopefully Skyfield will in the future support generating positions
for whole arrays of comets in a single efficient operation,
but for now your code should expect to operate on one comet at a time.

Minor Planets
=============

There are nearly a million minor planets
in the `IAU Minor Planet Center <https://www.minorplanetcenter.net/>`_’s
database of orbital elements,
thanks to the prodigious output of automated sky surveys
over the past few decades.

The database can be downloaded as a single ``MPCORB`` —
“Minor Planet Center orbits” —
file that offers each minor planet’s orbital elements as plain text.
But the raw file requires a bit of preprocessing
before Skyfield is ready to load it:

* The first 43 lines of the file are paragraphs that explain its contents,
  state the terms under which software programs may include the data,
  and provide links to documentation.
  Skyfield will need these non-data lines ignored or removed.

* While an uncompressed version of the file is available for download,
  most users opt to download the 55 MB compressed version
  from the Minor Planet Center
  to save bandwidth and storage.
  Decompressing the full 190 MB of data stored inside
  can require more than 1 second of computing time
  depending on your platform and processing speed.

* The complete catalog lists nearly 1 million objects,
  which can take several seconds to load and index.

For all of these reasons,
it usually makes the most sense to download, uncompress, and filter the file
before starting your application.

If your operating system provides tools for pattern matching,
they might be the fastest tool for selecting specific orbits.
Here’s how to extract the orbits
for the first four asteroids to be discovered —
(1) Ceres, (2) Pallas, (3) Juno, and (4) Vesta —
on a Linux system::

    zgrep -P '^(00001|00002|00003|00004) ' MPCORB.DAT.gz > MPCORB.excerpt.DAT

If your operating system lacks such tools,
you can build them yourself using Python.
Note that mass operations that Python implements in C,
like reading an entire file’s contents with ``read()``
and scanning the full contents with a regular expression ``findall()``,
will be much faster than using a Python loop to read every line.
Here’s an example script for performing the same search
as the ``zgrep`` command shown above:

.. include:: ../design/mpc_make_excerpt.py
   :literal:

The same four asteroid orbits could then be extracted with::

    python mpc_make_excerpt.py 00001 00002 00003 00004 > MPCORB.excerpt.DAT

Note that the minor planets file has no explicit expiration date,
so ``load.open()`` in the above script
will only download the file once.
Subsequent calls re-open the copy of the file already on your filesystem.
To force a fresh download, pass ``reload=True``.

In either case, the resulting file — shorn of its text header,
and containing only minor planet orbits —
is ready for Skyfield to load.

.. testcode::

    with load.open('MPCORB.excerpt.DAT') as f:
        minor_planets = mpc.load_mpcorb_dataframe(f)

    print(minor_planets.shape[0], 'minor planets loaded')

.. testoutput::

    4 minor planets loaded

Some Skyfield users have encountered Minor Planet Center files
with bodies whose `orbital elements are incomplete
<https://github.com/skyfielders/python-skyfield/issues/449>`_,
presumably because their orbits are still being determined.
To avoid receiving an ``EphemerisRangeError`` exception
when Skyfield tries to compute a position for these bodies,
you can ask Pandas to filter them out of your dataframe:

.. testcode::

    # Filtering the orbits dataframe to avoid triggering
    # an `EphemerisRangeError` on ill-defined orbits.

    bad_orbits = minor_planets.semimajor_axis_au.isnull()
    minor_planets = minor_planets[~bad_orbits]

As was demonstrated in the previous section on comets,
you can ask Pandas to index the dataframe by minor planet designation
for quick lookup.

.. testcode::

    # Index by designation for fast lookup.
    minor_planets = minor_planets.set_index('designation', drop=False)

    # Sample lookups.
    row = minor_planets.loc['(1) Ceres']
    row = minor_planets.loc['(4) Vesta']

Finally,
generating a position involves the same maneuver necessary for comets:
since minor planet orbits are centered on the Sun,
the Sun’s position vector must be added to theirs
to build a complete position.

.. testcode::

    ceres = sun + mpc.mpcorb_orbit(row, ts, GM_SUN)
    ra, dec, distance = earth.at(t).observe(ceres).radec()
    print(ra)
    print(dec)

.. testoutput::

    05h 51m 45.85s
    +22deg 38' 50.2"
