Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
944c9b7
add TEME frame
StuartLittlefair Apr 15, 2020
8db29f2
documentation for TEME frame
StuartLittlefair Apr 15, 2020
00d60e4
add changelog
StuartLittlefair Apr 15, 2020
921872b
newline for PEP8
StuartLittlefair Apr 15, 2020
426cee2
Update CHANGES.rst
StuartLittlefair Apr 15, 2020
144bd26
Update docs/coordinates/satellites.rst
StuartLittlefair Apr 15, 2020
6780557
mark test as remote
StuartLittlefair Apr 16, 2020
cd5eefe
fix time import in docs
StuartLittlefair Apr 16, 2020
3211803
comments and consistency with Vallado06
StuartLittlefair Apr 21, 2020
49b6acc
require sgp4
StuartLittlefair Apr 21, 2020
4e8f4bd
typo
StuartLittlefair Apr 21, 2020
7df10e5
shortened long line
StuartLittlefair Apr 22, 2020
c020bfa
remove unwanted line
StuartLittlefair Apr 22, 2020
0d93ebe
clarify time docstring
StuartLittlefair Apr 24, 2020
65198bf
spell out acronym
StuartLittlefair Apr 24, 2020
5ec9e53
remove non-neutral wording
StuartLittlefair Apr 24, 2020
048b443
add Vallado ref to docstring
StuartLittlefair Apr 24, 2020
274467e
whatsnew entry
StuartLittlefair Apr 24, 2020
9fe06ca
typo in docs
StuartLittlefair Apr 28, 2020
00ecbc6
cleaner use of differentials in docs
StuartLittlefair Apr 28, 2020
80233aa
remove hanging comment
StuartLittlefair Apr 28, 2020
675e025
added discussion of satellite conventions
StuartLittlefair Apr 28, 2020
dcd8fb1
copy-editing
StuartLittlefair Apr 29, 2020
af97427
remove default obstime
StuartLittlefair Apr 30, 2020
4c427a4
typo
StuartLittlefair May 1, 2020
7e3b428
remove duplicate in changes
StuartLittlefair May 1, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ astropy.coordinates
This provides limited support for updating coordinate data values from another
coordinate object of the same class and equivalent frame attributes. [#9857]

- Added the True Equator Mean Equinox (TEME) frame. [#10149]
Comment thread
StuartLittlefair marked this conversation as resolved.
Resolved

astropy.cosmology
^^^^^^^^^^^^^^^^^

Expand All @@ -41,7 +43,7 @@ astropy.io.ascii
astropy.io.misc
^^^^^^^^^^^^^^^
- Added serialization of parameter constraints fixed and bounds. [#10082]
- Added 'functional_models.py' and 'physical_models.py' to asdf/tags/transform,
- Added 'functional_models.py' and 'physical_models.py' to asdf/tags/transform,
with to allow serialization of all functional and physical models. [#10028]

- Fix ASDF serialization of circular model inverses, and remove explicit calls
Expand Down
3 changes: 2 additions & 1 deletion astropy/coordinates/builtin_frames/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
from .cirs import CIRS
from .itrs import ITRS
from .hcrs import HCRS
from .teme import TEME
from .ecliptic import * # there are a lot of these so we don't list them all explicitly
from .skyoffset import SkyOffsetFrame
# need to import transformations so that they get registered in the graph
Expand All @@ -55,7 +56,7 @@
# get included
__all__ = ['ICRS', 'FK5', 'FK4', 'FK4NoETerms', 'Galactic', 'Galactocentric',
'galactocentric_frame_defaults',
'Supergalactic', 'AltAz', 'GCRS', 'CIRS', 'ITRS', 'HCRS',
'Supergalactic', 'AltAz', 'GCRS', 'CIRS', 'ITRS', 'HCRS', 'TEME',
'PrecessedGeocentric', 'GeocentricMeanEcliptic',
'BarycentricMeanEcliptic', 'HeliocentricMeanEcliptic',
'GeocentricTrueEcliptic', 'BarycentricTrueEcliptic',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Contains the transformation functions for getting to/from ITRS, GCRS, and CIRS.
Contains the transformation functions for getting to/from ITRS, TEME, GCRS, and CIRS.
These are distinct from the ICRS and AltAz functions because they are just
rotations without aberration corrections or offsets.
"""
Expand All @@ -16,11 +16,31 @@
from .gcrs import GCRS, PrecessedGeocentric
from .cirs import CIRS
from .itrs import ITRS
from .teme import TEME
from .utils import get_polar_motion, get_jd12

# # first define helper functions


def teme_to_itrs_mat(time):
# Sidereal time, rotates from ITRS to mean equinox
# Use 1982 model for consistency with Vallado et al (2006)
# http://www.celestrak.com/publications/aiaa/2006-6753/AIAA-2006-6753.pdf
gst = erfa.gmst82(*get_jd12(time, 'ut1'))
Comment thread
StuartLittlefair marked this conversation as resolved.
Resolved

# Polar Motion
# Do not include TIO locator s' because it is not used in Vallado 2006
xp, yp = get_polar_motion(time)
pmmat = erfa.pom00(xp, yp, 0)

# rotation matrix
# c2tcio expects a GCRS->CIRS matrix as it's first argument.
# Here, we just set that to an I-matrix, because we're already
# in TEME and the difference between TEME and CIRS is just the
# rotation by the sidereal time rather than the Earth Rotation Angle
return erfa.c2tcio(np.eye(3), gst, pmmat)


def gcrs_to_cirs_mat(time):
# celestial-to-intermediate matrix
return erfa.c2i06a(*get_jd12(time, 'tt'))
Expand Down Expand Up @@ -129,3 +149,26 @@ def precessedgeo_to_gcrs(from_coo, to_frame):

# then move to the GCRS that's actually desired
return gcrs_coo.transform_to(to_frame)


@frame_transform_graph.transform(FunctionTransformWithFiniteDifference, TEME, ITRS)
Comment thread
adrn marked this conversation as resolved.
Resolved
def teme_to_itrs(teme_coo, itrs_frame):
# first get us to TEME at the target obstime
# TODO: self transform?
teme_coo2 = teme_coo.transform_to(TEME(obstime=itrs_frame.obstime))

# now get the pmatrix
pmat = teme_to_itrs_mat(itrs_frame.obstime)
crepr = teme_coo2.cartesian.transform(pmat)
return itrs_frame.realize_frame(crepr)


@frame_transform_graph.transform(FunctionTransformWithFiniteDifference, ITRS, TEME)
def itrs_to_teme(itrs_coo, teme_frame):
# compute the pmatrix, and then multiply by its transpose
pmat = teme_to_itrs_mat(itrs_coo.obstime)
newrepr = itrs_coo.cartesian.transform(matrix_transpose(pmat))
teme = TEME(newrepr, obstime=itrs_coo.obstime)

# now do any needed offsets (no-op if same obstime)
return teme.transform_to(teme_frame)
52 changes: 52 additions & 0 deletions astropy/coordinates/builtin_frames/teme.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
A coordinate or frame in the TEME (True Equator, Mean Equinox) system.

TEME is a True equator, Mean Equinox coordinate frame used in NORAD TLE
satellite files.
Comment thread
StuartLittlefair marked this conversation as resolved.
Resolved
"""
# -*- coding: utf-8 -*-
# Licensed under a 3-clause BSD style license - see LICENSE.rst

from astropy.utils.decorators import format_doc
from astropy.coordinates.representation import CartesianRepresentation, CartesianDifferential
from astropy.coordinates.baseframe import BaseCoordinateFrame, base_doc
from astropy.coordinates.attributes import TimeAttribute
from .utils import DEFAULT_OBSTIME

__all__ = ['TEME']

doc_footer_teme = """
Other parameters
----------------
obstime : `~astropy.time.Time`
The time at which the frame is defined. Used for determining the
position of the Earth.
"""


@format_doc(base_doc, components="", footer=doc_footer_teme)
class TEME(BaseCoordinateFrame):
"""
A coordinate or frame in the True Equator Mean Equinox frame (TEME).

This frame is a geocentric system similar to CIRS or geocentric apparent place,
except that the mean sidereal time is used to rotate from TIRS. TEME coordinates
are most often used in combination with orbital data for satellites in the
two-line-ephemeris format.

Different implementations of the TEME frame exist. For clarity, this frame follows the
conventions and relations to other frames that are set out in Vallado et al (2006).

For more background on TEME, see the references provided in the
:ref:`astropy-coordinates-seealso` section of the documentation.
"""

default_representation = CartesianRepresentation
default_differential = CartesianDifferential

obstime = TimeAttribute()

# Transformation functions for getting to/from TEME and ITRS are in
# intermediate rotation transforms.py
44 changes: 42 additions & 2 deletions astropy/coordinates/tests/test_intermediate_transformations.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
from astropy.time import Time
from astropy.coordinates import (EarthLocation, get_sun, ICRS, GCRS, CIRS, ITRS, AltAz,
PrecessedGeocentric, CartesianRepresentation, SkyCoord,
SphericalRepresentation, UnitSphericalRepresentation,
HCRS, HeliocentricMeanEcliptic)
CartesianDifferential, SphericalRepresentation, UnitSphericalRepresentation,
HCRS, HeliocentricMeanEcliptic, TEME)
from astropy.utils import iers


Expand Down Expand Up @@ -492,6 +492,46 @@ def test_gcrs_self_transform_closeby():
assert_allclose(delta, 0.0*u.m, atol=1*u.m)


@pytest.mark.remote_data
def test_teme_itrf():
"""
Test case transform from TEME to ITRF.

Test case derives from example on appendix C of Vallado, Crawford, Hujsak & Kelso (2006).
See https://celestrak.com/publications/AIAA/2006-6753/AIAA-2006-6753-Rev2.pdf
"""
v_itrf = CartesianDifferential(-3.225636520, -2.872451450, 5.531924446,
unit=u.km/u.s)
p_itrf = CartesianRepresentation(-1033.479383, 7901.2952740, 6380.35659580,
unit=u.km, differentials={'s': v_itrf})
t = Time("2004-04-06T07:51:28.386")

teme = ITRS(p_itrf, obstime=t).transform_to(TEME(obstime=t))
v_teme = CartesianDifferential(-4.746131487, 0.785818041, 5.531931288,
unit=u.km/u.s)
p_teme = CartesianRepresentation(5094.18016210, 6127.64465050, 6380.34453270,
unit=u.km, differentials={'s': v_teme})

assert_allclose(teme.cartesian.without_differentials().xyz,
p_teme.without_differentials().xyz, atol=30*u.cm)

assert_allclose(teme.cartesian.differentials['s'].d_xyz,
p_teme.differentials['s'].d_xyz, atol=1.0*u.cm/u.s)

# test round trip
itrf = teme.transform_to(ITRS(obstime=t))
assert_allclose(
itrf.cartesian.without_differentials().xyz,
p_itrf.without_differentials().xyz,
atol=100*u.cm
)
assert_allclose(
itrf.cartesian.differentials['s'].d_xyz,
p_itrf.differentials['s'].d_xyz,
atol=1*u.cm/u.s
)


@pytest.mark.remote_data
def test_earth_orientation_table():
"""Check that we can set the IERS table used as Earth Reference.
Expand Down
4 changes: 4 additions & 0 deletions docs/coordinates/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ listed below.
skycoord
transforming
solarsystem
satellites
formatting
matchsep
representations
Expand Down Expand Up @@ -496,6 +497,9 @@ coordinate systems implemented here include:
* Meeus, J. "Astronomical Algorithms"
A valuable text describing details of a wide range of coordinate-related
problems and concepts.
* `Revisiting Spacetrack Report #3 <https://celestrak.com/publications/AIAA/2006-6753/AIAA-2006-6753-Rev2.pdf>`_
A discussion of the simplified general pertubation (SGP) for satellite orbits, with a description of
the True Equator Mean Equinox (TEME) coordinate frame.


Built-in Frame Classes
Expand Down
106 changes: 106 additions & 0 deletions docs/coordinates/satellites.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
.. include:: references.txt

.. _astropy-coordinates-satellites:

Working with Earth Satellites Using Astropy Coordinates
*******************************************************

Comment thread
adrn marked this conversation as resolved.
Resolved
Satellite data is normally provided in the Two-Line Element (TLE) format
(see `here <https://www.celestrak.com/NORAD/documentation/tle-fmt.php>`_
for a definition). These datasets are designed to be used in combination
with a theory for orbital propagation model to predict the positions
of satellites.

The history of such models is discussed in detail in
`Vallado et al (2006) <https://celestrak.com/publications/AIAA/2006-6753/AIAA-2006-6753-Rev2.pdf>`_
who also provide a reference implementation of the SGP4 orbital propagation
code, designed to be compatible with the TLE sets provided by the United
States Department of Defense, which are available from a source like
`Celestrak <http://celestrak.com/>`_.

The output coordinate frame of the SGP4 model is the True Equator, Mean Equinox
frame (TEME), which is one of the frames built-in to `astropy.coordinates`.
TEME is an Earth-centered inertial frame (i.e., it does not rotate with respect
to the stars). Several definitions exist; ``astropy`` uses the implementation described
in `Vallado et al (2006) <https://celestrak.com/publications/AIAA/2006-6753/AIAA-2006-6753-Rev2.pdf>`_.

Finding TEME Coordinates from TLE Data
==========================================

There is currently no support in `astropy.coordinates` for computing satellite orbits
from TLE orbital element sets. Full support for handling TLE files is available in
the `Skyfield <http://rhodesmill.org/skyfield/>`_ library, but some advice for dealing
with satellite data in ``astropy`` is below.

.. EXAMPLE START Using sgp4 to get a TEME coordinate

You will need some external library to compute the position and velocity of the satellite from the
TLE orbital elements. The `SGP4 <https://pypi.org/project/sgp4/>`_ library can do this. An example
of using this library to find the `~astropy.coordinates.TEME` coordinates of a satellite is::

.. doctest-requires:: sgp4

>>> from sgp4.api import Satrec
Comment thread
StuartLittlefair marked this conversation as resolved.
Resolved
>>> from sgp4.api import SGP4_ERRORS
>>> s = '1 25544U 98067A 19343.69339541 .00001764 00000-0 38792-4 0 9991'
>>> t = '2 25544 51.6439 211.2001 0007417 17.6667 85.6398 15.50103472202482'
>>> satellite = Satrec.twoline2rv(s, t)

The ``satellite`` object has a method, ``satellite.sgp4``, that will try to compute the TEME position
and velocity at a given time::

.. doctest-requires:: sgp4

>>> from astropy.time import Time
>>> t = Time(2458827.362605, format='jd')
>>> error_code, teme_p, teme_v = satellite.sgp4(t.jd1, t.jd2) # in km and km/s
>>> if error_code != 0:
Comment thread
StuartLittlefair marked this conversation as resolved.
Resolved
... raise RuntimeError(SGP4_ERRORS[error_code])

Now that we have the position and velocity in kilometers and kilometers per second, we can create a
position in the `~astropy.coordinates.TEME` reference frame::

.. doctest-requires:: sgp4

>>> from astropy.coordinates import TEME, CartesianDifferential, CartesianRepresentation
>>> from astropy import units as u
>>> teme_p = CartesianRepresentation(teme_p*u.km)
>>> teme_v = CartesianDifferential(teme_v*u.km/u.s)
>>> teme = TEME(teme_p.with_differentials(teme_v), obstime=t)

.. EXAMPLE END

Note how we are careful to set the observed time of the `~astropy.coordinates.TEME` frame to
the time at which we calculated satellite position.

Comment thread
StuartLittlefair marked this conversation as resolved.
Resolved
Transforming TEME to Other Coordinate Systems
==================================================

Comment thread
StuartLittlefair marked this conversation as resolved.
Resolved
Once you have satellite positions in `~astropy.coordinates.TEME` coordinates they can be transformed
into any `astropy.coordinates` frame.

For example, to find the overhead latitude, longitude, and height of the satellite::

.. EXAMPLE START Transforming TEME

.. doctest-requires:: sgp4

>>> from astropy.coordinates import ITRS
>>> itrs = teme.transform_to(ITRS(obstime=t)) # doctest: +REMOTE_DATA
>>> location = itrs.earth_location # doctest: +REMOTE_DATA
>>> location.geodetic # doctest: +REMOTE_DATA +FLOAT_CMP
GeodeticLocation(lon=<Longitude 160.34199789 deg>, lat=<Latitude -24.6609379 deg>, height=<Quantity 420.17927591 km>)

Or, if you want to find the altitude and azimuth of the satellite from a particular location::

.. doctest-requires:: sgp4

>>> from astropy.coordinates import EarthLocation, AltAz
>>> siding_spring = EarthLocation.of_site('aao') # doctest: +REMOTE_DATA
>>> aa = teme.transform_to(AltAz(obstime=t, location=siding_spring)) # doctest: +REMOTE_DATA

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my understanding is that the examples should be standalone. So while this may pass the doctests for now, in the long term it have to be fixed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK - I've been back and forth on this; no-one seems to know what the right way is!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the example directive is not working yet, but when it does, these examples will be extracted to a separate place, part of a gallery. I would be very surprised if they see outside of their directive container, but the infra is still in the works, so I cannot say this for certain.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it will probably start with them needing to be stand-alone, but might expand to have ways to see wider context? But anyway, I think it doesn't need to block this PR

>>> aa.alt # doctest: +REMOTE_DATA +FLOAT_CMP
<Latitude 10.94798427 deg>
>>> aa.az # doctest: +REMOTE_DATA +FLOAT_CMP
<Longitude 59.28807348 deg>

.. EXAMPLE END
2 changes: 2 additions & 0 deletions docs/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ The following packages can optionally be used when testing:
- `skyfield <https://rhodesmill.org/skyfield/>`_: Used for testing Solar System
coordinates.

- `spgp4 <https://pypi.org/project/sgp4/>`_: Used for testing satellite positions.

- `tox <https://tox.readthedocs.io/en/latest/>`_: Used to automate testing
and documentation builds.

Expand Down
12 changes: 10 additions & 2 deletions docs/whatsnew/4.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@ In particular, this release includes:

2. Support for units on otherwise unitless models via the ``Model.coerce_units`` method.

3. Ability to create a new ``CompoundModel`` by modifying an existing one
with the ``replace_submodel`` method.
3. Added the True Equator Mean Equinox frame.

4. Added the True Equator Mean Equinox frame.

Support for writing Dask arrays to FITS files
=============================================
Expand All @@ -38,3 +38,11 @@ written, which will avoid using excessive memory:
>>> from astropy.io import fits
>>> hdu = fits.PrimaryHDU(data=array)
>>> hdu.writeto('test_dask.fits')

Added True Equator Mean Equinox (TEME) frame for satellite two-line ephemeris data
==================================================================================

The True Equator Mean Equinox (`~astropy.coordinates.TEME`) frame has been added to
the built-in frames within :ref:`astropy.coordinates <astropy-coordinates>`.

For more details, see :ref:`astropy-coordinates-satellites`.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ test =
ipython
coverage
skyfield>=1.20
sgp4
all =
scipy>=0.18
dask[array]
Expand Down