
===========================
 Stars and Distant Objects
===========================

.. currentmodule:: skyfield.api

Skyfield can generate positions for stars or and other distant objects
that you either load from a star catalog, or else for which you can
provide barycentric ICRS coordinates.

The Hipparcos Catalog
=====================

One of the most popular star catalogs is the Hipparcos catalog, compiled
from the observations of a 1989–1993 European Space Agency satellite
telescope.  The spelling is deliberate; the ancient Greek astronomer’s
name was “Hipparchus” but the name of the observatory and resulting
catalog are an acronym for “High Precision Parallax Collecting
Satellite.”

To load this large star catalog with Skyfield requires the `Pandas
<https://pandas.pydata.org/>`_ data library.  If you use the Anaconda
scientific Python distribution, you can install Pandas with::

    conda install pandas

Otherwise you can try installing it with either the standard ``pip``
Python package installation tool::

    pip install pandas

Whichever way you install it, the Pandas library gives Skyfield the
ability to load the Hipparcos catalog as a 118,218 row Pandas dataframe.
You can filter individual stars out of the dataframe using the Pandas
``loc[]`` operation with the star’s Hipparcos catalog number.  For
example, Barnard’s Star — the star moving fastest across our sky — has
the Hipparcos designation “HIP 87937” and would be accessed like this:

.. testsetup::

   __import__('skyfield.tests.fixes').tests.fixes.setup(
       (2014, 1, 18, 23, 10, 9))

   import matplotlib
   matplotlib.use('Agg')  # to avoid “no display name” error on Travis CI

.. testcode::

    from skyfield.api import Star, load
    from skyfield.data import hipparcos

    with load.open(hipparcos.URL) as f:
        df = hipparcos.load_dataframe(f)

    barnards_star = Star.from_dataframe(df.loc[87937])

To generate the position of a star in the Earth’s sky on a given date,
compute the Earth’s position in the Solar System and then ``observe()``
the star:

.. testcode::

    planets = load('de421.bsp')
    earth = planets['earth']

    ts = load.timescale()
    t = ts.now()
    astrometric = earth.at(t).observe(barnards_star)
    ra, dec, distance = astrometric.radec()
    print(ra)
    print(dec)

.. testoutput::

    17h 57m 47.77s
    +04deg 44' 01.1"

The position will properly reflect the star’s proper motion as measured
by the HIPPARCOS mission.

Stars with “nan” positions
==========================

The Hipparcos catalog, as pointed out in its official publication `The
Hipparcos and Tycho Catalogues`_, includes 263 target stars for which no
reliable position could be computed.  These stars have blanks for their
right ascension and declination in the text of the catalog itself.  In
Skyfield they will always return coordinates with the value ``nan`` (the
floating point value “not a number”).

If you want to avoid ``nan`` coordinates, you can filter them out of
your dataframe with:

.. testcode::

    df = df[df['ra_degrees'].notnull()]

The result will be the same if you filter by the ``dec_degrees`` column
instead.

.. _The Hipparcos and Tycho Catalogues: https://www.cosmos.esa.int/documents/532822/552851/vol1_all.pdf/99adf6e3-6893-4824-8fc2-8d3c9cbba2b5

Filtering the star catalog
==========================

In addition to selecting individual stars using their HIP number, you
can build a :class:`~skyfield.starlib.Star` object that contains all of
the stars in a dataframe.  You can combine this with the standard Pandas
dataframe filtering techniques to select only stars above a certain
brightness.

For example, let’s imagine that we wanted to plot the stars in the
constellation Orion — but only stars that are at least magnitude 2.5.
Pandas will let us filter the Hipparcos catalog dataframe in a single
line of code:

.. testcode::

    df = df[df['magnitude'] <= 2.5]
    print('After filtering, there are {} stars'.format(len(df)))

.. testoutput::

    After filtering, there are 93 stars

You can use this dataframe to build a :class:`~skyfield.starlib.Star`
object that will compute the positions of all 93 stars at once.

.. testcode::

   bright_stars = Star.from_dataframe(df)

   t = ts.utc(2018, 9, 3)
   astrometric = earth.at(t).observe(bright_stars)
   ra, dec, distance = astrometric.radec()

   print('There are {} right ascensions'.format(len(ra.hours)))
   print('and {} declinations'.format(len(dec.degrees)))

.. testoutput::

    There are 93 right ascensions
    and 93 declinations

Each element of the right ascension array and the declination array
corresponds to one of the 93 selected stars.  Their position can be
combined with their magnitude to produce a plot.

.. testcode::

   from matplotlib import pyplot as plt
   plt.scatter(ra.hours, dec.degrees, 8 - df['magnitude'], 'k')
   plt.xlim(7.0, 4.0)
   plt.ylim(-20, 20)
   plt.savefig('bright_stars.png')

The result of the simple filtering and plotting is an (admittedly
primitive) rendering of Orion!

.. image:: bright_stars.png

Skyfield does not yet offer star chart routines, but hopefully they will
appear before the end of the year.  For now you can peek at the Skyfield
file ``charting.py`` and experiment on your own.

Building a single star from its coordinates
===========================================

If instead of loading up a whole star catalog you have the coordinates
for one particular star you are interested in, you can instantiate a
:class:`~skyfield.starlib.Star` directly using keyword arguments.  Right
ascension and declination can be specified as traditional base-60
coordinates, where the fractions are “minutes” and “seconds”, if you
pass tuples instead of floats:

.. testcode::

    from skyfield.api import Star, load
    planets = load('de421.bsp')
    earth = planets['earth']

    barnard = Star(ra_hours=(17, 57, 48.49803),
                   dec_degrees=(4, 41, 36.2072))

    ts = load.timescale()
    t = ts.now()
    astrometric = earth.at(t).observe(barnard)
    ra, dec, distance = astrometric.radec()
    print(ra)
    print(dec)

.. testoutput::

    17h 57m 48.50s
    +04deg 41' 36.2"

Of course, this has simply returned the same position.  More interesting
is that we can ask for the position to be expressed in the dynamical
reference system of the moving Earth instead of in the same fixed ICRS
system in which we provided it:

.. testcode::

    ra, dec, distance = astrometric.radec(epoch=ts.now())
    print(ra)
    print(dec)

.. testoutput::

    17h 58m 30.80s
    +04deg 41' 41.9"

Distances to stars
==================

The distance that Skyfield computes to a particular star
might or might not be meaningful,
depending both on which star catalog you are using
and on which particular star you observe.

Skyfield has no way to even guess the distance to a star
if it has only been given its right ascension and declination,
as is the case for Barnard’s Star as we defined it above.
To represent this vector of unknown length,
Skyfield generates a vector exactly one gigaparsec long:

.. testcode::

    print(distance)  # exactly 1 gigaparsec

.. testoutput::

    2.06265e+14 au

This is so very far outside of our galaxy
that such positions should be easy
for you to tell apart from real distances to stars.
Real distances will all be within a few hundred parsecs or less,
because our telescopes cannot currently measure the parallax of stars
that are any further away than that.

In the next section,
we will redefine Barnard’s Star
and this time supply a real value for its parallax
from a recent star catalog.
You will see in that its ``distance``
switches to a realistic estimate.

Proper motion and parallax
==========================

There are other ways that a :class:`Star` object can generate a position
different than the plain old ICRS right ascension and declination with
which it is initialized.  First, Skyfield will recognize that an object
should change position over the years if we specify its proper motion in
milliarcseconds (“mas”) per year.  Second, Skyfield will shift its
apparent position relative to the location from which you observe it if
you provide a parallax indicating that the star is close enough to the
Solar System that we have been able to measure its distance.  Finally,
there is even a tiny effect from its radial velocity if it is known.

Here are all three effects applied to Barnard’s star:

.. testcode::

    from skyfield.api import T0

    barnard = Star(ra_hours=(17, 57, 48.49803),
                   dec_degrees=(4, 41, 36.2072),
                   ra_mas_per_year=-798.71,
                   dec_mas_per_year=+10337.77,
                   parallax_mas=545.4,
                   radial_km_per_s=-110.6)

    astrometric = earth.at(t).observe(barnard)
    ra, dec, distance = astrometric.radec()
    print(ra)
    print(dec)
    print(distance)

.. testoutput::

    17h 57m 47.76s
    +04deg 44' 01.3"
    3.77863e+05 au

Note that the above position is different than the input right ascension
and declination but not because we have asked for dynamic coordinates.
This position is in fixed ICRS coordinates and indicates real motion on
the part of Barnard’s star across our sky.

See the guide to :doc:`positions` to learn the operations that you can
perform with these astrometric positions after using a :class:`Star`
object to generate them.

Position at an epoch besides J2000
==================================

Some star catalogs provide positions at an epoch besides J2000.
For example, the catalog of stars observed by the Hipparcos space telescope
provides their positions as of J1991.25.

The epoch can be provided as an argument ``epoch`` to :class:`Star`.
Here are the right ascension and declination given for Barnard’s Star
in the Hipparcos catalog:

.. testcode::

    hipparcos_epoch = ts.tt(1991.25)
    barnard2 = Star(ra_hours=(17, 57, 48.97),
                    dec_degrees=(4, 40, 5.8),
                    ra_mas_per_year=-797.84,
                    dec_mas_per_year=+10326.93,
                    epoch=hipparcos_epoch)

    ra, dec, distance = earth.at(t).observe(barnard2).radec()
    print(ra)
    print(dec)

.. testoutput::

    17h 57m 47.75s
    +04deg 44' 01.3"

As you can see, the position returned for the date ``t`` is the same,
even though we initialized this :class:`Star` object
with a position for Barnard’s Star more than 8 years earlier
than the position we used in our first example.

.. testcleanup::

   __import__('skyfield.tests.fixes').tests.fixes.teardown()

.. The tiny hip_main.dat.gz file in this directory that powers these
   examples was generated by running the following in the main Skyfield
   source directory:

   zless hip_main.dat.gz | awk -F\| '$6 <= 3.2 || $2 == 87937' |gzip -9c > skyfield/documentation/hip_main.dat.gz
