Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions services/meteor_tracking/neo_feed_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

import logging
from dataclasses import dataclass
from datetime import date, datetime, timedelta
from datetime import date, datetime, timedelta, timezone
from typing import List, Optional

import aiohttp
Expand Down Expand Up @@ -81,7 +81,9 @@ async def fetch_neo_feed(
List of CloseApproach objects (same type as CAD client)
"""
if start_date is None:
start_date = date.today()
# Use UTC (the CAD client and all NEO timing are UTC), so the feed
# window cannot shift a day relative to close-approach queries.
start_date = datetime.now(timezone.utc).date()
if end_date is None:
end_date = start_date + timedelta(days=7)

Expand Down Expand Up @@ -145,11 +147,22 @@ def _parse_neo_object(self, obj: dict) -> CloseApproach | None:
d_min = meters.get('estimated_diameter_min')
d_max = meters.get('estimated_diameter_max')

# Distance
# Distance — prefer the astronomical value, fall back to kilometres.
# A missing distance must NOT default to 0: distance_ld == 0 reads as an
# extremely close pass and would trip a false ALERT, so skip such objects.
miss_distance = ca.get('miss_distance', {})
dist_au = float(miss_distance.get('astronomical', 0))
dist_km = float(miss_distance.get('kilometers', 0))
dist_ld = dist_au / LD_TO_AU if dist_au else 0
au_raw = miss_distance.get('astronomical')
km_raw = miss_distance.get('kilometers')
if au_raw is not None:
dist_au = float(au_raw)
dist_km = float(km_raw) if km_raw is not None else dist_au * AU_TO_KM
elif km_raw is not None:
dist_km = float(km_raw)
dist_au = dist_km / AU_TO_KM
else:
logger.debug("NEO object missing miss_distance; skipping")
return None
dist_ld = dist_au / LD_TO_AU

# Velocity
rel_velocity = ca.get('relative_velocity', {})
Expand Down
50 changes: 50 additions & 0 deletions tests/unit/test_neo_feed_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,56 @@ async def test_parse_neo_feed_no_approach_data(self):
approaches = client._parse_neo_feed(response)
assert len(approaches) == 0

@pytest.mark.asyncio
async def test_missing_miss_distance_is_skipped_not_alerted(self):
"""A NEO with no miss_distance must be SKIPPED, not defaulted to 0
(distance 0 would read as an extremely close pass and trip a false ALERT)."""
client = NEOFeedClient()
response = {
'near_earth_objects': {
'2026-03-23': [{
'neo_reference_id': 'no-distance',
'name': 'no-distance',
'absolute_magnitude_h': 22.0,
'estimated_diameter': {'meters': {
'estimated_diameter_min': 10, 'estimated_diameter_max': 20}},
'is_potentially_hazardous_asteroid': False,
'close_approach_data': [{
'close_approach_date': '2026-03-23',
'relative_velocity': {'kilometers_per_second': '12.0'},
# miss_distance intentionally absent
}],
}]
}
}
approaches = client._parse_neo_feed(response)
assert approaches == [] # skipped, not a phantom ALERT at distance 0

@pytest.mark.asyncio
async def test_kilometers_only_distance_derives_au(self):
"""When only kilometres are present, AU/LD are derived (not defaulted to 0)."""
client = NEOFeedClient()
response = {
'near_earth_objects': {
'2026-03-23': [{
'neo_reference_id': 'km-only',
'name': 'km-only',
'absolute_magnitude_h': 22.0,
'estimated_diameter': {'meters': {
'estimated_diameter_min': 10, 'estimated_diameter_max': 20}},
'is_potentially_hazardous_asteroid': False,
'close_approach_data': [{
'close_approach_date': '2026-03-23',
'relative_velocity': {'kilometers_per_second': '12.0'},
'miss_distance': {'kilometers': '14959787.07'}, # 0.1 AU
}],
}]
}
}
approaches = client._parse_neo_feed(response)
assert len(approaches) == 1
assert approaches[0].distance_au == pytest.approx(0.1, rel=1e-3)

@pytest.mark.asyncio
async def test_close_no_session(self):
"""Test closing with no active session."""
Expand Down
Loading