Repository navigation
Solar system plan94 - #4920
Solar system plan94#4920
Conversation
|
p.s. The tests were simplified somewhat but two classes still contain the same function twice, so it can obviously be improved. Also needs some tests for |
|
|
||
|
|
||
| def get_body_barycentric(time, body): | ||
| def get_body_barycentric(body, time, ephemeris=None): |
There was a problem hiding this comment.
From an API point of view, I'm happy to swap body and time, and also to take the location from the time if none is supplied.
I'm not sure about having the ephemeris as the argument to the functions. If a user makes a series of calls and forgets to set the ephemeris in one, the behaviour might be quite unpredictable. One ought, IMO, to be able to set the state at the start of your code and the same ephemeris will be used throughout.
There was a problem hiding this comment.
I'm a bit fan of "explicit is better than implicit", but maybe that is an argument for not allowing None at all... I hadn't quite realised we had a thing called "science state" -- that may be fine too.
There was a problem hiding this comment.
yes - I was thinking science state may be the way to go. Perhaps we should make all these functions return an error until an ephemeris has been chosen?
There was a problem hiding this comment.
I could get behind that - that they can only be used in a specific state.
|
@eteq, @StuartLittlefair - I added some tests for the Sun. While doing that, I found a strange discrepancy with EDIT: the following does again give a consistent result. Maybe |
|
@StuartLittlefair, @eteq - the comment about inconsistencies in |
| return CartesianRepresentation(barycen_to_body_vector) | ||
|
|
||
|
|
||
| def _get_earth_body_vector(time, body, earth_time=None): |
There was a problem hiding this comment.
For clarification, I combined this in the code below to avoid recalculating the position of the earth many times.
There was a problem hiding this comment.
yes - this is much better
|
@mhvk, @eteq I agree that something supporting |
|
@mhvk -sorry this is a bit out of order, I can't see how to reply to a comment directly. The discrepancy between the different methods for calculating the Sun's position is worrying. Given that the tests against Horizons pass I'm thinking that either the aberration calculation is wrong (I didn't look too closely). Alternatively, the transform between ICRS and GCRS also includes deflection by large bodies in the Solar system. I don't suppose Jupiter is getting in the way at the time you've chosen is it? |
|
@StuartLittlefair - on |
|
@mhvk works well for get_sun, but for the planets you'd have to convert the ICRS planet positions into ecliptic coordinates which might be more work than it's worth. |
|
@StuartLittlefair - actually, |
|
p.s. And |
|
They're in the FK5(equinox=j2000), so almost ICRS (to about 80mas). So although you've got heliocentric coordinates it's not easy to get the coordinates to make a HeliocentricTrueEcliptic frame from them. You could use the coordinates from plan94 directly in a HCRS frame... |
|
Actually, the transformation to equatorial is done at the very end of |
|
Sorry: I did not read right: yes, I had thought about |
|
@mhvk - Hmm, that Another possibility to consider: does HORIZONS include the aberration correction? It might be that their output doesn't include that for whatever reason, which would mean we need to add it back in to alll the ephemerides calculations. And there's also the light deflection question: I can't remember off hand if there's an ICRS->GCRS transform here, but if so, it might be that we're getting the sun's "deflection"? If so, that might mean we need to add some logic into the ICRS->GCRS transform to decide when to use deflection... |
|
Oh, and I like the idea of using
|
|
OK, back in. I'll go ahead and rebase. My general feeling is to keep Also, I agree the state should be something explicit, like a string, so I'll implement that. Finally, to me it makes most sense to actually download the ephemeris when setting the state, as this is the point the user might expect a delay, and not wait until an attempt is made to use the ephemeris. This has the advantage that validation can be done at the setting stage as well. |
|
I got some way towards a science state code that I thought was rather elegant, but students happened so I didn't finish it. Also, I have social commitments this evening, so I won't be able to contribute much. However, I'll post what I had as a comment here. Maybe it will be useful. The idea is that the code would be used as: import astropy.coordinates as coord
from astropy.time import Time
with coord.solar_system_ephemeris.set('plan94'):
print('Allowed bodies ', coord.solar_system_ephemeris.bodies)
pos = coord.get_body_barycentric('venus', Time.now())Anyway, here it is.... __all__ = ["solar_system_ephemeris","get_body_barycentric"]
"""
each value in the BODIES dictionary a list of kernel pairs needed
to find the barycentric position of that object from a JPL kernel.
Every JPL 'planet' ephemeris kernel can be assumed to have these
kernel pairs contained within, so these pairings should work with
all SPK files from JPL.
"""
BODY_NAME_TO_KERNEL_SPEC = OrderedDict(
(('sun', [(0, 10)]),
('mercury', [(0, 1), (1, 199)]),
('venus', [(0, 2), (2, 299)]),
('earth-moon-barycenter', [(0, 3)]),
('earth', [(0, 3), (3, 399)]),
('moon', [(0, 3), (3, 301)]),
('mars', [(0, 4)]),
('jupiter', [(0, 5)]),
('saturn', [(0, 6)]),
('uranus', [(0, 7)]),
('neptune', [(0, 8)]),
('pluto', [(0, 9)]))
)
"""
The ERFA `plan94` routines have a limited subset of objects.
"""
ERFA_BODY_NAME_TO_PLANET_INDEX = OrderedDict(
(('mercury', 1),
('venus', 2),
('earth-moon-barycenter', 3),
('mars', 4),
('jupiter', 5),
('saturn', 6),
('uranus', 7),
('neptune', 8)))
SOLAR_SYSTEM_BODIES = None
class classproperty(object):
def __init__(self, getter):
self.getter = getter
def __get__(self, instance, owner):
return self.getter(owner)
class solar_system_ephemeris(ScienceState):
"""
The ephemeris to use for calculating solar system body positions.
The default is `None`, in which case any attempt to calculate solar system
body positions will raise an Exception.
Valid choices (other than `None`) are:
plan94 : Uses the ERFA routines to find approximate positions of a limited
subset of bodies. Accuracy w.r.t JPL ephemeris is about 1 arcminute.
No moon, or earth positions available.
jpl : Uses the NASA JPL Satellite Planet Kernel (SPK) file DE430. This is ~120 MB
and will be downloaded the first time a position is calculated and
cached from then on. It covers the date range ~1550-2650 CE.
de430 : An alias for jpl.
de432 : Uses the DE432 SPK file instead. This in only 10MB, but covers a limited
date range of ~1950-2050 CE.
url : You can also supply the URL to any valid SPK file.
"""
_value = None
@classmethod
def validate(cls, value):
global KERNEL
if value is None:
KERNEL = None
return value
if value.lower() not in ['jpl', 'de430', 'de432', 'plan94']:
# CUSTOM URL
try:
six.moves.urllib.parse.urlparse(value)
except:
# this is not a valid url or one of the approved items
raise ValueError('{} is not a valid URL, or one of the pre-defined '
'ephemerides'.format(value))
KERNEL = None
return value
@classproperty
def url(cls):
if cls._value is None or cls._value.lower() == 'plan94':
url = None
elif cls._value in ['jpl', 'de430', 'de432']:
if cls._value.lower() == 'jpl':
de_string = 'de430'
else:
de_string = cls._value.lower()
de_number = int(de_string[2:])
url = ('http://naif.jpl.nasa.gov/pub/naif/'
'generic_kernels/spk/planets/de{0:03d}.bsp'.format(de_number))
else:
url = cls._value
return url
@classproperty
def bodies(cls):
if cls._value is None:
return None
if cls._value.lower() == 'plan94':
return tuple(ERFA_BODY_NAME_TO_PLANET_INDEX.keys())
else:
return tuple(BODY_NAME_TO_KERNEL_SPEC.keys())
def _get_kernel():
"""
Try importing jplephem, download/retrieve from cache the Satellite Planet
Kernel corresponding to the given ephemeris.
"""
global KERNEL
if KERNEL is None and solar_system_ephemeris.url is not None:
try:
from jplephem.spk import SPK
except ImportError:
raise ImportError("Solar system JPL ephemeris calculations require "
"the jplephem package "
"(https://pypi.python.org/pypi/jplephem)")
KERNEL = SPK.open(_download_spk_file(solar_system_ephemeris.url))
return KERNEL
def get_body_barycentric(body, time):
"""
Calculate the barycentric position of the solar system body ``body``
in cartesian coordinates.
Parameters
----------
body : str
The solar system body to calculate. The allowed values for ``body``
depend on the ephemeris, but standard ones can be found in
``astropy.coordinates.SOLAR_SYSTEM_BODIES``.
time : `~astropy.time.Time`
Time at which the position should be calculated.
Returns
-------
cartesian_position : `~astropy.coordinates.CartesianRepresentation`
Barycentric (ICRS) position of the body in cartesian coordinates
"""
if solar_system_ephemeris.get() is None:
raise ValueError("""You must set the solar_system_ephemeris first. For example:
>>> from astropy import coordinates as coord
>>> coord.solar_system_ephemeris.set('plan94')""")
jd1, jd2 = get_jd12(time, 'tdb')
body = body.lower()
if solar_system_ephemeris.url is None:
earth_pv_helio, earth_pv_bary = erfa.epv00(jd1, jd2)
if body == 'earth':
cartesian_position_body = earth_pv_bary[..., 0, :]
else:
sun_bary = earth_pv_bary[..., 0, :] - earth_pv_helio[..., 0, :]
if body == 'sun':
cartesian_position_body = sun_bary
else:
body_index = ERFA_BODY_NAME_TO_PLANET_INDEX[body]
body_pv_helio = erfa.plan94(jd1, jd2, body_index)
cartesian_position_body = body_pv_helio[..., 0, :] + sun_bary
barycen_to_body_vector = u.Quantity(
np.rollaxis(cartesian_position_body, -1, 0), u.au)
else:
# Lookup chain for JPL ephemeris.
chain = BODY_NAME_TO_KERNEL_SPEC[body.lower()]
kernel = _get_kernel()
cartesian_position_body = sum([kernel[pair].compute(jd1, jd2)
for pair in chain])
barycen_to_body_vector = u.Quantity(cartesian_position_body, unit=u.km)
return CartesianRepresentation(barycen_to_body_vector) |
e2ee6fc to
2cff402
Compare
|
OK, finally a complete rework. Please have a look... The remote data test may currently fail since the times used in the tests are sufficiently nearby that IERS A is downloaded even for the |
|
@mhvk Is the motivation behind allowing the ephemeris as an argument to My reasoning is that I would expect setting a science state to affect all calls to |
| # Add uncertainty in position of Earth | ||
| dist_tol = dist_tol + 1300 * u.km | ||
|
|
||
| astropy_1980 = get_body(body, self.t, ephemeris='erfa') |
There was a problem hiding this comment.
I guess we should be using astropy_2016 here, or the appropriate date if we change the test
|
Other than the comment above about the ephemeris argument I think this all looks very good! If you want to change the Kitt Peak tests to 2014-09-25T00:00, then the appropriate numbers for Horizons (as generated on 2016-05-17) are: Jupiter: |
|
@eteq - I'm done implementing your other changes. The one I did not do was change 'approximate' to 'erfa'. My main reason is that #4963 will hopefully soon give us approximate positions for the moon, and I think ideally we just use those by default. But since #4963 is not based on |
turns out that textwrap.indent does not exist in py 2.x
|
p.s. I found the problem with |
Since ``set`` is inherited and does not show up by default.
|
Note that the docs build failed because I tried to link to |
|
@mhvk - OK, gotcha re: Other than that, 👍 from me on this now! |
|
Yes, |
|
Tests passed, so am merging this! Thanks @StuartLittlefair and @eteq for the great collaboration in getting this in! |
This adds the option to calculate approximate planet positions using
erfa.plan94, i.e., without downloading a (huge) ephemeris file.Ideally, this gets merged for 1.2.0 since it changes the API for
get_body.@eteq, @StuartLittlefair - comments?