Umbau auf server
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
"""An interface between JPL ephemerides and Skyfield."""
|
||||
|
||||
import numpy as np
|
||||
import os
|
||||
from collections import defaultdict
|
||||
|
||||
from jplephem.exceptions import OutOfRangeError
|
||||
from jplephem.spk import SPK
|
||||
from jplephem.names import target_name_pairs
|
||||
|
||||
from .constants import AU_KM, DAY_S
|
||||
from .errors import EphemerisRangeError
|
||||
from .timelib import compute_calendar_date
|
||||
from .vectorlib import VectorFunction, VectorSum, _jpl_code_name_dict
|
||||
|
||||
_jpl_name_code_dict = dict(
|
||||
(name, target) for (target, name) in target_name_pairs
|
||||
)
|
||||
|
||||
class SpiceKernel(object):
|
||||
"""Ephemeris file in NASA .bsp format.
|
||||
|
||||
A "Spacecraft and Planet Kernel" (SPK) file from NASA provides
|
||||
|xyz| coordinates for bodies in the Solar System like the Sun,
|
||||
planets, moons, and spacecraft.
|
||||
|
||||
You can download a .bsp file yourself and use this class to open it,
|
||||
or use the Skyfield ``load()`` function to automatically download a
|
||||
popular ephemeris. Once loaded, you can print this object to the
|
||||
screen to see a report on the segments that it includes:
|
||||
|
||||
>>> planets = load('de421.bsp')
|
||||
>>> print(planets)
|
||||
SPICE kernel file 'de421.bsp' has 15 segments
|
||||
JD 2414864.50 - JD 2471184.50 (1899-07-28 through 2053-10-08)
|
||||
0 -> 1 SOLAR SYSTEM BARYCENTER -> MERCURY BARYCENTER
|
||||
0 -> 2 SOLAR SYSTEM BARYCENTER -> VENUS BARYCENTER
|
||||
0 -> 3 SOLAR SYSTEM BARYCENTER -> EARTH BARYCENTER
|
||||
0 -> 4 SOLAR SYSTEM BARYCENTER -> MARS BARYCENTER
|
||||
0 -> 5 SOLAR SYSTEM BARYCENTER -> JUPITER BARYCENTER
|
||||
0 -> 6 SOLAR SYSTEM BARYCENTER -> SATURN BARYCENTER
|
||||
0 -> 7 SOLAR SYSTEM BARYCENTER -> URANUS BARYCENTER
|
||||
0 -> 8 SOLAR SYSTEM BARYCENTER -> NEPTUNE BARYCENTER
|
||||
0 -> 9 SOLAR SYSTEM BARYCENTER -> PLUTO BARYCENTER
|
||||
0 -> 10 SOLAR SYSTEM BARYCENTER -> SUN
|
||||
3 -> 301 EARTH BARYCENTER -> MOON
|
||||
3 -> 399 EARTH BARYCENTER -> EARTH
|
||||
1 -> 199 MERCURY BARYCENTER -> MERCURY
|
||||
2 -> 299 VENUS BARYCENTER -> VENUS
|
||||
4 -> 499 MARS BARYCENTER -> MARS
|
||||
|
||||
To retrieve the one or more vectors necessary to compute the
|
||||
position of a body relative to the Solar System barycenter, look up
|
||||
the body by its name or official SPICE identifying integer:
|
||||
|
||||
>>> planets['earth']
|
||||
<VectorSum of 2 vectors:
|
||||
'de421.bsp' segment 0 SOLAR SYSTEM BARYCENTER -> 3 EARTH BARYCENTER
|
||||
'de421.bsp' segment 3 EARTH BARYCENTER -> 399 EARTH>
|
||||
>>> planets[499]
|
||||
<VectorSum of 2 vectors:
|
||||
'de421.bsp' segment 0 SOLAR SYSTEM BARYCENTER -> 4 MARS BARYCENTER
|
||||
'de421.bsp' segment 4 MARS BARYCENTER -> 499 MARS>
|
||||
|
||||
The result will be a :class:`~skyfield.vectorlib.VectorFunction`
|
||||
instance that you can ask for a position at a given input time.
|
||||
|
||||
"""
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
self.filename = os.path.basename(path)
|
||||
self.spk = SPK.open(path)
|
||||
self.segments = [SPICESegment(self, s) for s in self.spk.segments]
|
||||
self.codes = set(s.center for s in self.segments).union(
|
||||
s.target for s in self.segments)
|
||||
self.comments = self.spk.comments # deprecated pass-through method
|
||||
|
||||
# Pre-compute which segments lead to which targets.
|
||||
d = defaultdict(list)
|
||||
for segment in self.segments:
|
||||
d[segment.target].append(segment)
|
||||
|
||||
# Go ahead and build a vector function for each target.
|
||||
self._vector_functions = {
|
||||
target: segments[0] if len(segments) == 1 else Stack(segments)
|
||||
for target, segments in d.items()
|
||||
}
|
||||
|
||||
def __repr__(self):
|
||||
return '<{0} {1!r}>'.format(type(self).__name__, self.path)
|
||||
|
||||
def __str__(self):
|
||||
segments = self.spk.segments
|
||||
lines = ['SPICE kernel file {0!r} has {1} segments'
|
||||
.format(self.filename, len(segments))]
|
||||
format_date = '{0}-{1:02}-{2:02}'.format
|
||||
start = end = None
|
||||
for s in segments:
|
||||
if start != s.start_jd or end != s.end_jd:
|
||||
start, end = s.start_jd, s.end_jd
|
||||
starts = format_date(*compute_calendar_date(int(start)))
|
||||
ends = format_date(*compute_calendar_date(int(end)))
|
||||
lines.append(' JD {0:.2f} - JD {1:.2f} ({2} through {3})'
|
||||
.format(start, end, starts, ends))
|
||||
lines.append(_format_segment(s))
|
||||
return '\n'.join(lines)
|
||||
|
||||
def close(self):
|
||||
"""Close this ephemeris file."""
|
||||
self.spk.close()
|
||||
|
||||
# In practice, users are not confident the file is really closed
|
||||
# unless the metadata also disappears.
|
||||
del self.segments[:]
|
||||
self.codes.clear()
|
||||
|
||||
def names(self):
|
||||
"""Return all target names that are valid with this kernel.
|
||||
|
||||
>>> pprint(planets.names())
|
||||
{0: ['SOLAR_SYSTEM_BARYCENTER', 'SSB', 'SOLAR SYSTEM BARYCENTER'],
|
||||
1: ['MERCURY_BARYCENTER', 'MERCURY BARYCENTER'],
|
||||
2: ['VENUS_BARYCENTER', 'VENUS BARYCENTER'],
|
||||
3: ['EARTH_BARYCENTER',
|
||||
'EMB',
|
||||
...
|
||||
|
||||
The result is a dictionary with target code keys and name lists
|
||||
as values. The last name in each list is the one that Skyfield
|
||||
uses when printing information about a body.
|
||||
|
||||
"""
|
||||
d = defaultdict(list)
|
||||
for code, name in target_name_pairs:
|
||||
if code in self.codes:
|
||||
d[code].append(name)
|
||||
return dict(d)
|
||||
|
||||
def decode(self, name):
|
||||
"""Translate a target name into its integer code.
|
||||
|
||||
>>> planets.decode('Venus')
|
||||
299
|
||||
|
||||
Raises ``ValueError`` if you supply an unknown name, or
|
||||
``KeyError`` if the target is missing from this kernel. You can
|
||||
supply an integer code if you already have one and just want to
|
||||
check whether it is present in this kernel.
|
||||
|
||||
"""
|
||||
if isinstance(name, int):
|
||||
code = name
|
||||
else:
|
||||
name = name.upper()
|
||||
code = _jpl_name_code_dict.get(name)
|
||||
if code is None:
|
||||
raise ValueError('unknown SPICE target {0!r}'.format(name))
|
||||
if code not in self.codes:
|
||||
targets = ', '.join(_format_code_and_name(c) for c in self.codes)
|
||||
raise KeyError('kernel {0!r} is missing {1!r} -'
|
||||
' the targets it supports are: {2}'
|
||||
.format(self.filename, name, targets))
|
||||
return code
|
||||
|
||||
def __getitem__(self, target):
|
||||
"""Return a vector function for computing the location of `target`."""
|
||||
target = self.decode(target)
|
||||
vector_functions = self._vector_functions
|
||||
vf = vector_functions[target]
|
||||
if vf.center == 0:
|
||||
return vf
|
||||
vfs = [vf]
|
||||
center = vf.center
|
||||
while center in vector_functions:
|
||||
vf = vector_functions[center]
|
||||
vfs.append(vf)
|
||||
center = vf.center
|
||||
return VectorSum(center, target, tuple(reversed(vfs)))
|
||||
|
||||
def __contains__(self, name_or_code):
|
||||
if isinstance(name_or_code, int):
|
||||
code = name_or_code
|
||||
else:
|
||||
code = _jpl_name_code_dict.get(name_or_code.upper())
|
||||
return code in self.codes
|
||||
|
||||
class SPICESegment(VectorFunction):
|
||||
|
||||
def __new__(cls, ephemeris, spk_segment):
|
||||
if spk_segment.data_type == 2:
|
||||
return object.__new__(ChebyshevPosition)
|
||||
if spk_segment.data_type == 3:
|
||||
return object.__new__(ChebyshevPositionVelocity)
|
||||
raise ValueError('SPK data type {0} not yet supported'
|
||||
.format(spk_segment.data_type))
|
||||
|
||||
def __init__(self, ephemeris, spk_segment):
|
||||
self.ephemeris = ephemeris
|
||||
self.center = spk_segment.center
|
||||
self.target = spk_segment.target
|
||||
self.spk_segment = spk_segment
|
||||
|
||||
@property
|
||||
def vector_name(self):
|
||||
return '{0!r} segment'.format(self.ephemeris.path)
|
||||
|
||||
def time_range(self, ts):
|
||||
s = self.spk_segment
|
||||
return ts.tdb_jd(s.start_jd), ts.tdb_jd(s.end_jd)
|
||||
|
||||
class ChebyshevPosition(SPICESegment):
|
||||
def _at(self, t):
|
||||
segment = self.spk_segment
|
||||
try:
|
||||
position, velocity = segment.compute_and_differentiate(
|
||||
t.whole, t.tdb_fraction)
|
||||
except OutOfRangeError as e:
|
||||
start_time, end_time = self.time_range(t.ts)
|
||||
s = '%04d-%02d-%02d' % start_time.tdb_calendar()[:3]
|
||||
t = '%04d-%02d-%02d' % end_time.tdb_calendar()[:3]
|
||||
text = 'ephemeris segment only covers dates %s through %s' % (s, t)
|
||||
mask = e.out_of_range_times
|
||||
segment = self.spk_segment
|
||||
e = EphemerisRangeError(text, start_time, end_time, mask, segment)
|
||||
e.__cause__ = None # avoid exception chaining in Python 3
|
||||
raise e
|
||||
|
||||
return position / AU_KM, velocity / AU_KM, None, None
|
||||
|
||||
class ChebyshevPositionVelocity(SPICESegment):
|
||||
def _at(self, t):
|
||||
pv = self.spk_segment.compute(t.whole, t.tdb_fraction)
|
||||
return pv[:3] / AU_KM, pv[3:] * DAY_S / AU_KM, None, None
|
||||
|
||||
class Stack(VectorFunction):
|
||||
"""Several segments for one target, that might cover different dates."""
|
||||
def __init__(self, segments):
|
||||
self.center = segments[0].center
|
||||
self.target = segments[0].target # all segments have same target
|
||||
self.ephemeris = segments[0].ephemeris
|
||||
|
||||
# Hopefully all segments have the same center, since we
|
||||
# ourselves can only advertise a single `.center`. If not, drop
|
||||
# segments that don't match our arbitrary choice of center.
|
||||
segments[:] = (s for s in segments if s.center == self.center)
|
||||
self.segments = segments
|
||||
|
||||
def _at(self, t):
|
||||
if not t.shape:
|
||||
for segment in reversed(self.segments):
|
||||
spk = segment.spk_segment
|
||||
if spk.start_jd <= t.tdb <= spk.end_jd:
|
||||
break
|
||||
return segment._at(t)
|
||||
|
||||
shape = (3,) + t.shape
|
||||
position = np.empty(shape)
|
||||
velocity = np.empty(shape)
|
||||
position.fill(np.nan)
|
||||
velocity.fill(np.nan)
|
||||
for segment in self.segments:
|
||||
spk = segment.spk_segment
|
||||
matches = (spk.start_jd <= t.tdb) & (t.tdb <= spk.end_jd)
|
||||
indices = matches.nonzero()[0]
|
||||
if len(indices) == 0:
|
||||
continue
|
||||
t_i = t[indices]
|
||||
position_i, velocity_i, _, _ = segment._at(t_i)
|
||||
position[:, indices] = position_i
|
||||
velocity[:, indices] = velocity_i
|
||||
# TODO: check for nan's? or let user do that?
|
||||
return position, velocity, None, None
|
||||
|
||||
def _format_code_and_name(code):
|
||||
name = _jpl_code_name_dict.get(code, None)
|
||||
if name is None:
|
||||
return str(code)
|
||||
return '{0} {1}'.format(code, name)
|
||||
|
||||
def _format_segment(segment):
|
||||
cname = _jpl_code_name_dict.get(segment.center, 'unknown')
|
||||
tname = _jpl_code_name_dict.get(segment.target, 'unknown')
|
||||
return ' {0:3} -> {1:<3} {2} -> {3}'.format(
|
||||
segment.center, segment.target, cname, tname)
|
||||
Reference in New Issue
Block a user