Repository navigation
Solar system and lunar ephemerides via jplephem #4746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1cc330a
bbe6b14
7d0679d
e243c9f
c367180
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,204 @@ | ||
| # Licensed under a 3-clause BSD style license - see LICENSE.rst | ||
| """ | ||
| This module contains convenience functions for retrieving solar system | ||
| ephemerides from jplephem. | ||
| """ | ||
|
|
||
| from __future__ import (absolute_import, division, print_function, | ||
| unicode_literals) | ||
|
|
||
| import numpy as np | ||
| from .sky_coordinate import SkyCoord | ||
| from ..utils.data import download_file | ||
| from .. import units as u | ||
| from ..constants import c as speed_of_light | ||
| from .representation import CartesianRepresentation | ||
| from .builtin_frames import GCRS | ||
| from .earth import EarthLocation | ||
|
|
||
|
|
||
| __all__ = ["get_planet", "get_moon"] | ||
|
|
||
| KERNEL = None | ||
|
|
||
|
|
||
| def _download_spk_file(show_progress=True): | ||
| """ | ||
| Get the Satellite Planet Kernel (SPK) file `de430.bsp` from NASA JPL. | ||
|
|
||
| Download the file from the JPL webpage once and subsequently access a | ||
| cached copy. This file is ~120 MB, and covers years ~1550-2650 CE [1]_. | ||
|
|
||
| .. [1] http://naif.jpl.nasa.gov/pub/naif/generic_kernels/spk/planets/aareadme_de430-de431.txt | ||
| """ | ||
| de430_url = ('http://naif.jpl.nasa.gov/pub/naif/' | ||
| 'generic_kernels/spk/planets/de430.bsp') | ||
| return download_file(de430_url, cache=True, show_progress=show_progress) | ||
|
|
||
|
|
||
| def _get_kernel(*args, **kwargs): | ||
| """ | ||
| Try importing jplephem, download/retrieve from cahce the Satellite Planet | ||
| Kernel. | ||
| """ | ||
| global KERNEL | ||
|
|
||
| try: | ||
| from jplephem.spk import SPK | ||
| except ImportError: | ||
| raise ImportError("Solar system ephemeris calculations depend on" | ||
| "jplephem") | ||
|
|
||
| if KERNEL is None: | ||
| KERNEL = SPK.open(_download_spk_file()) | ||
| return KERNEL | ||
|
|
||
|
|
||
| def _get_absolute_planet_position(time, planet_index): | ||
| """ | ||
| Calculate the position of planet ``planet_index`` in cartesian coordinates. | ||
|
|
||
| Uses ``jplephem`` with the DE430 kernel. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| time : `~astropy.time.Time` | ||
| Time of observation | ||
|
|
||
| planet_index : int | ||
| Index of the planet (1-9 for Mercury through Pluto) | ||
|
|
||
| Returns | ||
| ------- | ||
| cartesian_position : `~astropy.units.Quantity` | ||
| GRCS position of the planet in cartesian coordinates | ||
|
|
||
| earth_distance : `~astropy.units.Quantity` | ||
| Distance between Earth and planet. | ||
|
|
||
| Notes | ||
| ----- | ||
|
|
||
| """ | ||
| kernel = _get_kernel() | ||
|
|
||
| # Mercury and Venus have separate barycenter and planet vectors | ||
| if planet_index < 3: | ||
| barycenter_to_planet_ind = 100*planet_index + 99 | ||
| cartesian_position_planet = (kernel[0, planet_index].compute(time.jd) + | ||
| kernel[planet_index, | ||
| barycenter_to_planet_ind].compute(time.jd)) | ||
| # planet_index is 3 gets the moon | ||
| elif planet_index == 3: | ||
| barycenter_to_planet_ind = 301 | ||
| cartesian_position_planet = (kernel[0, planet_index].compute(time.jd) + | ||
| kernel[planet_index, | ||
| barycenter_to_planet_ind].compute(time.jd)) | ||
| else: | ||
| cartesian_position_planet = kernel[0, planet_index].compute(time.jd) | ||
|
|
||
| cartesian_position_earth = (kernel[0, 3].compute(time.jd) + | ||
| kernel[3, 399].compute(time.jd)) | ||
|
|
||
| # Quadrature sum of the difference of the position vectors | ||
| earth_distance = np.sqrt(np.sum((np.array(cartesian_position_planet) - | ||
| np.array(cartesian_position_earth))**2))*u.km | ||
|
|
||
| earth_to_planet_vector = u.Quantity(cartesian_position_planet - | ||
| cartesian_position_earth, | ||
| unit=u.km) | ||
|
|
||
| return earth_to_planet_vector, earth_distance | ||
|
|
||
|
|
||
| def _get_apparent_planet_position(time, planet_index): | ||
| """ | ||
| Calculate the apparent position of planet ``planet_index`` in cartesian | ||
| coordinates, given the approximate light travel time to the object. | ||
|
|
||
| Uses ``jplephem`` with the DE430 kernel. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| time : `~astropy.time.Time` | ||
| Time of observation | ||
|
|
||
| planet_index : int | ||
| Index of the planet (1-9 for Mercury through Pluto) | ||
|
|
||
| Returns | ||
| ------- | ||
| cartesian_position : `~astropy.coordinates.CartesianRepresentation` | ||
| Position of the planet defined as a vector from the Earth. | ||
| """ | ||
| # Get distance of planet at `time` | ||
| earth_to_planet_vector, earth_distance = _get_absolute_planet_position(time, planet_index) | ||
|
|
||
| # The apparent position depends on the time that the light was emitted from | ||
| # the distant planet, so subtract off the light travel time | ||
| light_travel_time = earth_distance/speed_of_light | ||
| emitted_time = time - light_travel_time | ||
|
|
||
| # Calculate position given approximate light travel time. | ||
| # TODO: this should be solved iteratively to converge on precise positions | ||
| earth_to_planet_vector, earth_distance = _get_absolute_planet_position(emitted_time, planet_index) | ||
| x, y, z = earth_to_planet_vector | ||
|
|
||
| return CartesianRepresentation(x=x, y=y, z=z) | ||
|
|
||
|
|
||
| def get_planet(time, planet_index, location=None): | ||
| """ | ||
| Get a `~astropy.coordinates.SkyCoord` for a planet as observed from a | ||
| location on Earth. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| time : `~astropy.time.Time` | ||
| Time of observation | ||
|
|
||
| planet_index : int | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Planet index seems a little obscure. Although most of us have the planet order memorized, what if someone finds Planet X between two other existing ones? Might be more descriptive to use explicit strings like
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I completely agree with this - Pluto has taught us the planet indices are not fixed! String name would be better. Or planet/dwarf planet unicode (☿, ♀, ♁, ♂, ♃, ♄, ♅, ♆, ♇) 😉
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I for once fall on @adrn's side against string parameters, and think it's faster and more convenient to type integer indices, especially for a problem like this where indices are rather obvious (planet If Pluto doesn't sit well, we can limit the input arguments to integers 1-8 and make a separate function to expose Pluto, since you get it for free with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How about a half-way compromise using module constants? MERCURY = 1
VENUS = 2
...This way, user can use
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just have and then in the routine proper something like:
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My quick proposal was just that if index was in the dictionary, it would return the associated value, if not, it would just be kept as is. So, if you pass in an integer, it stays as is. Of course one could be more elaborate And then catch the Anyway, bottom line is that I don't see much of a problem with supporting both strings and integers (as long as the meaning of the integer is the same as it is in the ephemeris file).
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mhvk, fine by me!
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I definitely think we want to disallow 9 though. Also, +1 to supporting strings too (this is not going to dominate performance) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In case it helps: note that there is already a standard set of integer indexes for planets, long maintained as part of the SPICE package from NASA: The
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also +1 to allow strings. That's much more in line with what other pythonic tools do. But no harm in allowing ints or strings. |
||
| Index of the planet (1-9 for Mercury through Pluto), excluding the | ||
| special value 3, which corresponds to the Earth's Moon. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you sure
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will be the first to admit that this interface is confusing. What's going on here is that it would be senseless to create a This design choice isn't a weird relic from the weirdness of the DE430 kernel, it's me trying to write code that's not redundant. I'm open to alternatives to make this clear or more intuitive.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we're using indices that match those used by the JPL kernel, 3 should have the same meaning as those have, otherwise it is just going to be too confusing in my opinion (especially since
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. p.s. It is sensible to get a position of the Earth relative to the solar system barycentre, since this underlies any precise barycentric corrections.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand where you're coming from. I was only designing a function for the purposes of creating
For the purposes of this simple I think part of the reason why it took so long for a PR like this to arrive is because the JPL kernel indices are confusing and messy. Hiding that mess was my top priority in this PR.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, for me, the reason I did only do a kludge myself is that I don't care where the planets are, but do care about very accurate barycentric corrections... Anyway, I'm happy with unambiguous entries, but think it is only confusing if you use something that is almost but not exactly the same as the way the ephemeris files (and |
||
|
|
||
| location : `~astropy.coordinates.EarthLocation` | ||
| Location of observer on the Earth. If none is supplied, set to | ||
| Greenwich. | ||
|
|
||
| Returns | ||
| ------- | ||
| skycoord : `~astropy.coordinates.SkyCoord` | ||
| Coordinate for the planet | ||
| """ | ||
| if location is None: | ||
| location = EarthLocation.of_site('greenwich') | ||
|
|
||
| cartrep = _get_apparent_planet_position(time, planet_index) | ||
|
|
||
| return SkyCoord(GCRS(cartrep, obstime=time, | ||
| obsgeoloc=u.Quantity(location.geocentric, copy=False))) | ||
|
|
||
|
|
||
| def get_moon(time, location=None): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just feel like it is a waste not to include the following in this function: if some_condition:
warnings.warn('That is no moon!', AstropyUserWarning) |
||
| """ | ||
| Get a `~astropy.coordinates.SkyCoord` for the Earth's moon as observed | ||
| from a location on Earth. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| time : `~astropy.time.Time` | ||
| Time of observation | ||
|
|
||
| location : `~astropy.coordinates.EarthLocation` | ||
| Location of observer on the Earth. If none is supplied, set to | ||
| Greenwich. | ||
|
|
||
| planet_index : int | ||
| Index of the planet (1-9 for Mercury through Pluto) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But, Pluto is not a planet anymore...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about this and decided that it's still pretty clear what
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a tricky question. Because Pluto is still a planet in New Mexico. |
||
|
|
||
| Returns | ||
| ------- | ||
| skycoord : `~astropy.coordinates.SkyCoord` | ||
| Coordinate for the planet | ||
| """ | ||
| return get_planet(time, planet_index=3, location=location) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| from __future__ import (absolute_import, division, print_function, | ||
| unicode_literals) | ||
|
|
||
| from ...time import Time | ||
| from ... import units as u | ||
| from ...constants import c | ||
| from ..builtin_frames import GCRS | ||
| from ..earth import EarthLocation | ||
| from ..sky_coordinate import SkyCoord | ||
| from ..solar_system import get_planet, get_moon | ||
| from ...tests.helper import pytest, assert_quantity_allclose, remote_data | ||
|
|
||
| try: | ||
| import jplephem # pylint: disable=W0611 | ||
| except ImportError: | ||
| HAS_JPLEPHEM = False | ||
| else: | ||
| HAS_JPLEPHEM = True | ||
|
|
||
| separation_tolerance_planets = 1*u.arcmin | ||
| separation_tolerance_moon = 1*u.degree | ||
| distance_tolerance = 0.02 | ||
|
|
||
|
|
||
| @remote_data | ||
| @pytest.mark.skipif(str('not HAS_JPLEPHEM')) | ||
| def test_positions_distances_kpno_1980(): | ||
| """ | ||
| Test positions against those generated by JPL Horizons accessed on | ||
| 2016-03-28, with refraction turned on. | ||
| """ | ||
| t = Time('1980-03-25 00:00') | ||
| kitt_peak = EarthLocation.from_geodetic(lon=-111.6*u.deg, | ||
| lat=31.963333333333342*u.deg, | ||
| height=2120*u.m) | ||
| frame = GCRS(obstime=t, obsgeoloc=u.Quantity(kitt_peak.geocentric, | ||
| copy=False)) | ||
|
|
||
| moon_astropy_1980 = get_moon(t, kitt_peak) | ||
| mercury_astropy_1980 = get_planet(t, 1, kitt_peak) | ||
| jupiter_astropy_1980 = get_planet(t, 5, kitt_peak) | ||
|
|
||
| # Results returned by JPL Horizons web interface | ||
| mercury_horizons_1980 = SkyCoord(ra='22h42m51.03s', dec='-08d23m16.3s', | ||
| frame=frame) | ||
| moon_horizons_1980 = SkyCoord(ra='07h35m26.49s', dec='+18d14m43.2s', | ||
| frame=frame) | ||
| jupiter_horizons_1980 = SkyCoord(ra='10h18m15.71s', dec='+11d57m04.4s', | ||
| frame=frame) | ||
| d_mercury_1980 = c*(6.323068*u.min) | ||
| d_moon_1980 = c*(0.021651*u.min) | ||
| d_jupiter_1980 = c*(37.694477*u.min) | ||
|
|
||
| # Assert sky coordinates are close | ||
| assert (mercury_astropy_1980.separation(mercury_horizons_1980) < | ||
| separation_tolerance_planets) | ||
| assert (jupiter_astropy_1980.separation(jupiter_horizons_1980) < | ||
| separation_tolerance_planets) | ||
| assert (moon_astropy_1980.separation(moon_horizons_1980) < | ||
| separation_tolerance_moon) | ||
|
|
||
| # Assert distances are close | ||
| distances_astropy = [mercury_astropy_1980.distance, | ||
| moon_astropy_1980.distance, | ||
| jupiter_astropy_1980.distance] | ||
| distances_horizons = [d_mercury_1980, d_moon_1980, d_jupiter_1980] | ||
|
|
||
| assert_quantity_allclose(distances_astropy, distances_horizons, | ||
| rtol=distance_tolerance) | ||
|
|
||
|
|
||
| @remote_data | ||
| @pytest.mark.skipif(str('not HAS_JPLEPHEM')) | ||
| def test_positions_distances_ctio_2016(): | ||
| """ | ||
| Test positions against those generated by JPL Horizons accessed on | ||
| 2016-03-28, with refraction turned on. | ||
| """ | ||
| t = Time('2016-09-25 00:00') | ||
| ctio = EarthLocation.from_geodetic(lon=-70.815*u.deg, | ||
| lat=-30.165277777777778*u.deg, | ||
| height=2215*u.m) | ||
|
|
||
| frame = GCRS(obstime=t, obsgeoloc=u.Quantity(ctio.geocentric, | ||
| copy=False)) | ||
|
|
||
| moon_astropy_2016 = get_moon(t, ctio) | ||
| mercury_astropy_2016 = get_planet(t, 1, ctio) | ||
| jupiter_astropy_2016 = get_planet(t, 5, ctio) | ||
|
|
||
| # Results returned by JPL Horizons web interface | ||
| mercury_horizons_2016 = SkyCoord(ra='11h06m01.23s', dec='+06d04m49.4s', | ||
| frame=frame) | ||
| moon_horizons_2016 = SkyCoord(ra='07h32m55.22s', dec='+17d49m10.7s', | ||
| frame=frame) | ||
| jupiter_horizons_2016 = SkyCoord(ra='12h13m09.58s', dec='-00d14m05.3s', | ||
| frame=frame) | ||
| d_mercury_1980 = c*(7.063664*u.min) | ||
| d_moon_1980 = c*(0.021536*u.min) | ||
| d_jupiter_1980 = c*(53.675250*u.min) | ||
|
|
||
| # Assert sky coordinates are close | ||
| assert (mercury_astropy_2016.separation(mercury_horizons_2016) < | ||
| separation_tolerance_planets) | ||
| assert (jupiter_astropy_2016.separation(jupiter_horizons_2016) < | ||
| separation_tolerance_planets) | ||
| assert (moon_astropy_2016.separation(moon_horizons_2016) < | ||
| separation_tolerance_moon) | ||
|
|
||
| # Assert distances are close | ||
| distances_astropy = [mercury_astropy_2016.distance, | ||
| moon_astropy_2016.distance, | ||
| jupiter_astropy_2016.distance] | ||
| distances_horizons = [d_mercury_1980, d_moon_1980, d_jupiter_1980] | ||
|
|
||
| assert_quantity_allclose(distances_astropy, distances_horizons, | ||
| rtol=distance_tolerance) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Like this, @bsipocz? Thanks for the help, sorry to be learning by experimentation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, this looks better :)