Skip to content

Commit 7eeb381

Browse files
authored
Fixes bug where the aberrated coordinates were misaligned with ephemeris produced (#132)
1 parent 16d9421 commit 7eeb381

File tree

2 files changed

+169
-9
lines changed

2 files changed

+169
-9
lines changed

src/adam_core/propagator/propagator.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,24 @@ def _add_light_time(
127127

128128
# Calculate the topocentric distance
129129
rho = np.linalg.norm(orbit_i.coordinates.r - observer_position)
130+
# If rho becomes too large, we are probably simulating a close encounter
131+
# and our propagation will break
132+
if np.isnan(rho) or rho > 1e12:
133+
raise ValueError(
134+
"Distance from observer is NaN or too large and propagation will break."
135+
)
130136

131137
# Calculate the light travel time
132138
lt = rho / C
133139

134140
# Calculate the change in light travel time since the previous iteration
135141
dlt = np.abs(lt - lt_prev)
136142

143+
if np.isnan(lt) or lt > 1e12:
144+
raise ValueError(
145+
"Light travel time is NaN or too large and propagation will break."
146+
)
147+
137148
# Calculate the new epoch and propagate the initial orbit to that epoch
138149
# Should be sufficient to use 2body propagation for this
139150
orbit_i = propagate_2body(
@@ -160,7 +171,24 @@ def _generate_ephemeris(
160171
elif isinstance(orbits, VariantOrbits):
161172
ephemeris_total = VariantEphemeris.empty()
162173

174+
# Sort observers by time and code to ensure consistent ordering
175+
# As further propagation will order by time as well
176+
observers = observers.sort_by(
177+
["coordinates.time.days", "coordinates.time.nanos", "code"]
178+
)
179+
180+
observers_barycentric = observers.set_column(
181+
"coordinates",
182+
transform_coordinates(
183+
observers.coordinates,
184+
CartesianCoordinates,
185+
frame_out="ecliptic",
186+
origin_out=OriginCodes.SOLAR_SYSTEM_BARYCENTER,
187+
),
188+
)
189+
163190
for orbit in orbits:
191+
# Propagate orbits to sorted observer times
164192
propagated_orbits = self.propagate_orbits(orbit, observers.coordinates.time)
165193

166194
# Transform both the orbits and observers to the barycenter if they are not already.
@@ -173,15 +201,7 @@ def _generate_ephemeris(
173201
origin_out=OriginCodes.SOLAR_SYSTEM_BARYCENTER,
174202
),
175203
)
176-
observers_barycentric = observers.set_column(
177-
"coordinates",
178-
transform_coordinates(
179-
observers.coordinates,
180-
CartesianCoordinates,
181-
frame_out="ecliptic",
182-
origin_out=OriginCodes.SOLAR_SYSTEM_BARYCENTER,
183-
),
184-
)
204+
185205
num_orbits = len(propagated_orbits_barycentric.orbit_id.unique())
186206

187207
observer_codes = np.tile(
@@ -248,6 +268,15 @@ def _generate_ephemeris(
248268

249269
ephemeris_total = qv.concatenate([ephemeris_total, ephemeris])
250270

271+
ephemeris_total = ephemeris_total.sort_by(
272+
[
273+
"orbit_id",
274+
"coordinates.time.days",
275+
"coordinates.time.nanos",
276+
"coordinates.origin.code",
277+
]
278+
)
279+
251280
return ephemeris_total
252281

253282
def generate_ephemeris(

src/adam_core/propagator/tests/test_propagator.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import numpy as np
22
import pyarrow as pa
3+
import pyarrow.compute as pc
4+
import pytest
35
import quivr as qv
6+
from adam_assist import ASSISTPropagator
47

58
from ...coordinates.cartesian import CartesianCoordinates
69
from ...coordinates.origin import Origin, OriginCodes
@@ -58,6 +61,8 @@ def _generate_ephemeris(self, orbits: Orbits, observers: Observers) -> Ephemeris
5861
orbit_id=pa.array(np.full(len(coords), orbit.orbit_id[0].as_py())),
5962
object_id=pa.array(np.full(len(coords), orbit.object_id[0].as_py())),
6063
coordinates=coords.to_spherical(),
64+
aberrated_coordinates=coords,
65+
light_time=np.full(len(coords), 0.0),
6166
)
6267
ephemeris_list.append(ephemeris_i)
6368

@@ -157,3 +162,129 @@ def test_propagate_different_origins():
157162
orbit_two_results.coordinates.origin.code.unique()[0].as_py()
158163
== "EARTH_MOON_BARYCENTER"
159164
)
165+
166+
167+
def test_light_time_distance_threshold():
168+
"""
169+
Test that _add_light_time raises a ValueError when an object gets too far from the observer.
170+
"""
171+
# Create a single orbit with very high velocity
172+
orbit = Orbits.from_kwargs(
173+
orbit_id=["1"],
174+
object_id=["1"],
175+
coordinates=CartesianCoordinates.from_kwargs(
176+
x=[1], # Start at origin
177+
y=[1],
178+
z=[1],
179+
vx=[1e9], # Very high velocity (will quickly exceed our limits)
180+
vy=[0],
181+
vz=[0],
182+
time=Timestamp.from_mjd([60000], scale="tdb"),
183+
frame="ecliptic",
184+
origin=Origin.from_kwargs(code=["SOLAR_SYSTEM_BARYCENTER"]),
185+
),
186+
)
187+
188+
# Create an observer at a fixed position
189+
observer = Observers.from_kwargs(
190+
code=["500"], # Arbitrary observer code
191+
coordinates=CartesianCoordinates.from_kwargs(
192+
x=[0],
193+
y=[0],
194+
z=[0],
195+
vx=[0],
196+
vy=[0],
197+
vz=[0],
198+
time=Timestamp.from_mjd([60020], scale="tdb"), # 1 day later
199+
frame="ecliptic",
200+
origin=Origin.from_kwargs(code=["SOLAR_SYSTEM_BARYCENTER"]),
201+
),
202+
)
203+
204+
prop = MockPropagator()
205+
206+
# The object will have moved ~86400 * 1e6 AU in one day, which should trigger the threshold
207+
with pytest.raises(
208+
ValueError,
209+
match="Distance from observer is NaN or too large and propagation will break.",
210+
):
211+
prop._add_light_time(orbit, observer)
212+
213+
214+
@pytest.mark.parametrize("max_processes", [1, 4])
215+
def test_generate_ephemeris_unordered_observers(max_processes):
216+
"""
217+
Test that ephemeris generation works correctly even when observers
218+
are not ordered by time, verifying that physical positions don't get mixed up
219+
between different objects.
220+
"""
221+
# Create two distinct orbits - one near Earth and one near Mars
222+
orbits = Orbits.from_kwargs(
223+
orbit_id=["far", "near"],
224+
object_id=["far", "near"],
225+
coordinates=CartesianCoordinates.from_kwargs(
226+
x=[3.0, 1], # Pick something far from both observers (earth) and sun
227+
y=[3.0, 1],
228+
z=[3.0, 1],
229+
vx=[0.1, 0.1],
230+
vy=[0.0, 0.0], # Different orbital velocities
231+
vz=[0.0, 0.0],
232+
time=Timestamp.from_mjd([59000, 59000], scale="tdb"),
233+
origin=Origin.from_kwargs(code=["SUN", "SUN"]),
234+
frame="ecliptic",
235+
),
236+
)
237+
# Create observers with deliberately unordered times
238+
times = Timestamp.from_mjd([59005, 59004, 59005, 59004, 59006, 59006], scale="utc")
239+
codes = ["500", "500", "X05", "X05", "X05", "500"] # Mix of observatory codes
240+
observers = Observers.from_codes(codes, times)
241+
242+
propagator = ASSISTPropagator()
243+
ephemeris = propagator.generate_ephemeris(
244+
orbits, observers, max_processes=max_processes
245+
)
246+
247+
# Basic ordering checks
248+
assert len(ephemeris) == 12 # 2 objects × 6 times
249+
250+
# Verify that coordinates.time - aberrated_coordinates.time is equal to light_time
251+
time_difference_days, time_difference_nanos = ephemeris.coordinates.time.rescale(
252+
"tdb"
253+
).difference(ephemeris.aberrated_coordinates.time)
254+
fractional_days = pc.divide(time_difference_nanos, 86400 * 1e9)
255+
time_difference = pc.add(time_difference_days, fractional_days)
256+
np.testing.assert_allclose(
257+
time_difference.to_numpy(zero_copy_only=False),
258+
ephemeris.light_time.to_numpy(zero_copy_only=False),
259+
atol=1e-6,
260+
)
261+
262+
# Verify that the near-Earth object is consistently closer than the Mars object
263+
far_ephem = ephemeris.select("orbit_id", "far")
264+
near_ephem = ephemeris.select("orbit_id", "near")
265+
266+
# make sure the far object is always further than the near object
267+
far_positions = np.linalg.norm(
268+
far_ephem.aberrated_coordinates.values[:, :3], axis=1
269+
)
270+
near_positions = np.linalg.norm(
271+
near_ephem.aberrated_coordinates.values[:, :3], axis=1
272+
)
273+
assert np.all(
274+
far_positions > near_positions
275+
), "Far aberrated positions should be consistently further than near positions"
276+
277+
# Verify observer codes match the expected order after sorting for each object
278+
sorted_observers = observers.sort_by(
279+
["coordinates.time.days", "coordinates.time.nanos", "code"]
280+
)
281+
for orbit_id in ["far", "near"]:
282+
orbit_ephem = ephemeris.select("orbit_id", orbit_id)
283+
np.testing.assert_array_equal(
284+
orbit_ephem.coordinates.origin.code.to_numpy(zero_copy_only=False),
285+
sorted_observers.code.to_numpy(zero_copy_only=False),
286+
)
287+
288+
# Link back to observers to verify correct correspondence
289+
linkage = ephemeris.link_to_observers(observers)
290+
assert len(linkage.all_unique_values) == len(observers)

0 commit comments

Comments
 (0)