Skip to content

Commit 52fd26c

Browse files
Don’t pre-build vector functions for eph segments
To reduce the expense of looking up a planet in an ephemeris, each ephemeris was doing a single scan of its `.segments` in `__init__()` and caching the resulting vector functions. But this meant that users could not usefully edit the `.segments` list — for example, to combine all the segments from two ephemeris files. So let’s do everything on-the-fly.
1 parent 04cddb6 commit 52fd26c

File tree

2 files changed

+73
-23
lines changed

2 files changed

+73
-23
lines changed

skyfield/jpllib.py

Lines changed: 26 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -73,19 +73,12 @@ def __init__(self, path):
7373
self.segments = [SPICESegment(self, s) for s in self.spk.segments]
7474
self.comments = self.spk.comments # deprecated pass-through method
7575

76-
# Pre-compute which segments lead to which targets.
77-
d = defaultdict(list)
78-
for segment in self.segments:
79-
d[segment.target].append(segment)
80-
81-
# Go ahead and build a vector function for each target.
82-
self._vector_functions = {
83-
target: segments[0] if len(segments) == 1 else Stack(segments)
84-
for target, segments in d.items()
85-
}
86-
8776
def __repr__(self):
88-
return '<{0} {1!r}>'.format(type(self).__name__, self.path)
77+
paths = sorted({s.ephemeris.path for s in self.segments})
78+
return '<{} {}>'.format(
79+
type(self).__name__,
80+
' '.join(repr(path) for path in paths),
81+
)
8982

9083
def __str__(self):
9184
lines = []
@@ -94,8 +87,9 @@ def __str__(self):
9487
ephemeris = start = end = None
9588
for s in self.segments:
9689
if ephemeris is not s.ephemeris:
90+
word = 'Segments' if (ephemeris is None) else 'And'
9791
ephemeris = s.ephemeris
98-
a('Segments from kernel file {!r}:'.format(ephemeris.filename))
92+
a('{} from kernel file {!r}:'.format(word, ephemeris.filename))
9993
spk = s.spk_segment
10094
if start != spk.start_jd or end != spk.end_jd:
10195
start, end = spk.start_jd, spk.end_jd
@@ -170,17 +164,28 @@ def decode(self, name):
170164

171165
def __getitem__(self, target):
172166
"""Return a vector function for computing the location of `target`."""
167+
target_arg = target
173168
target = self.decode(target)
174-
vector_functions = self._vector_functions
175-
vf = vector_functions[target]
176-
if vf.center == 0:
177-
return vf
178-
vfs = [vf]
179-
center = vf.center
180-
while center in vector_functions:
181-
vf = vector_functions[center]
169+
center = target
170+
vfs = []
171+
172+
while center != 0:
173+
matches = [s for s in self.segments if s.target == center]
174+
if not matches:
175+
raise KeyError(
176+
'the segments of this ephemeris cannot connect the Solar'
177+
' System Barycenter to the target {!r}'.format(target_arg)
178+
)
179+
elif len(matches) == 1:
180+
vf = matches[0]
181+
else:
182+
vf = Stack(matches)
182183
vfs.append(vf)
183184
center = vf.center
185+
186+
if len(vfs) == 1:
187+
return vfs[0]
188+
184189
return VectorSum(center, target, tuple(reversed(vfs)))
185190

186191
def __contains__(self, name_or_code):

skyfield/tests/test_jpllib.py

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_multiple_non_overlapping_segments_per_target():
3232

3333
# Verify that ephemeris objects let their segments be edited.
3434

35-
def test_removing_segments_from_jpl_ephemeris():
35+
def test_removing_segments_from_ephemeris():
3636
eph = load('de421.bsp')
3737
eph.segments = [s for s in eph.segments if s.target in (3, 4)]
3838

@@ -55,5 +55,50 @@ def test_removing_segments_from_jpl_ephemeris():
5555
0 -> 4 SOLAR SYSTEM BARYCENTER -> MARS BARYCENTER"""
5656

5757
assert type(eph[4]).__name__ == 'ChebyshevPosition'
58-
with assert_raises(KeyError):
58+
with assert_raises(KeyError, 'is missing 5'):
5959
eph[5]
60+
61+
def test_adding_segments_to_ephemeris():
62+
eph = load('de405.bsp')
63+
eph.segments = [s for s in eph.segments if s.target == 3]
64+
65+
eph2 = load('de421.bsp')
66+
eph.segments.extend((s for s in eph2.segments if s.target in (301, 399)))
67+
68+
assert len(eph.segments) == 3
69+
assert 2 not in eph
70+
assert 3 in eph
71+
assert 301 in eph
72+
assert eph.codes == {0, 3, 301, 399}
73+
assert eph.names() == {
74+
0: ['SOLAR_SYSTEM_BARYCENTER', 'SSB', 'SOLAR SYSTEM BARYCENTER'],
75+
3: ['EARTH_BARYCENTER', 'EMB', 'EARTH MOON BARYCENTER',
76+
'EARTH-MOON BARYCENTER', 'EARTH BARYCENTER'],
77+
301: ['MOON'], 399: ['EARTH'],
78+
}
79+
80+
assert repr(eph) == "<SpiceKernel 'de405.bsp' 'de421.bsp'>"
81+
assert str(eph) == """\
82+
Segments from kernel file 'de405.bsp':
83+
JD 2305424.50 - JD 2525008.50 (1599-12-08 through 2201-02-19)
84+
0 -> 3 SOLAR SYSTEM BARYCENTER -> EARTH BARYCENTER
85+
And from kernel file 'de421.bsp':
86+
JD 2414864.50 - JD 2471184.50 (1899-07-28 through 2053-10-08)
87+
3 -> 301 EARTH BARYCENTER -> MOON
88+
3 -> 399 EARTH BARYCENTER -> EARTH"""
89+
90+
vs = eph[399]
91+
assert type(vs).__name__ == 'VectorSum'
92+
assert str(vs) == """\
93+
Sum of 2 vectors:
94+
'de405.bsp' segment 0 SOLAR SYSTEM BARYCENTER -> 3 EARTH BARYCENTER
95+
'de421.bsp' segment 3 EARTH BARYCENTER -> 399 EARTH"""
96+
97+
with assert_raises(KeyError):
98+
eph[4]
99+
100+
def test_ephemeris_lacking_segments_to_connect_to_barycenter():
101+
eph = load('de421.bsp')
102+
eph.segments = [s for s in eph.segments if s.target == 399]
103+
with assert_raises(KeyError, "Barycenter to the target 'Earth'"):
104+
eph['Earth']

0 commit comments

Comments
 (0)