Skip to content

Commit 76b8c62

Browse files
authored
Add query_neocc (#141)
* Add query_neocc * Remove try/except in query_neocc * Fix linting issues due to updated black (or ruff) versions * Fix timescale declaration in query_neocc
1 parent 6d0458e commit 76b8c62

File tree

7 files changed

+612
-0
lines changed

7 files changed

+612
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
# flake8: noqa: F401
22
from .horizons import query_horizons
3+
from .neocc import query_neocc
34
from .sbdb import query_sbdb
5+
from .scout import query_scout

src/adam_core/orbits/query/neocc.py

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
from typing import Any, Dict, List, Literal, Union
2+
3+
import numpy as np
4+
import numpy.typing as npt
5+
import quivr as qv
6+
import requests
7+
8+
from ...coordinates import CoordinateCovariances, KeplerianCoordinates, Origin
9+
from ...orbits import Orbits
10+
from ...time import Timestamp
11+
12+
13+
def _upper_triangular_to_full(
14+
upper_triangular: npt.NDArray[np.float64],
15+
) -> npt.NDArray[np.float64]:
16+
"""
17+
Convert an upper triangular matrix containing 21 elements to a full 6x6 matrix.
18+
"""
19+
assert len(upper_triangular) == 21
20+
21+
full = np.zeros((6, 6))
22+
full[np.triu_indices(6)] = upper_triangular
23+
full[np.tril_indices(6, -1)] = full.T[np.tril_indices(6, -1)]
24+
return full
25+
26+
27+
def _parse_oef(data: str) -> Dict[str, Any]:
28+
"""
29+
Parse a OEF file and return the stored orbital elements.
30+
31+
Parameters
32+
----------
33+
data: str
34+
The content of the OEF file.
35+
36+
Returns
37+
-------
38+
Dict[str, Any]
39+
Dictionary containing the parsed orbital elements and metadata.
40+
41+
42+
Examples
43+
--------
44+
format = 'OEF2.0' ! file format
45+
rectype = 'ML' ! record type (1L/ML)
46+
refsys = ECLM J2000 ! default reference system
47+
END_OF_HEADER
48+
2024YR4
49+
! Keplerian elements: a, e, i, long. node, arg. peric., mean anomaly
50+
KEP 2.5158127507489616E+00 6.6154036821914619E-01 3.4081393687180 271.3655954496424 134.3614240204325 4.0403920526717883E+01
51+
MJD 60800.000000000 TDT
52+
MAG 23.876 0.150
53+
! Non-gravitational parameters: model used, number of model parameters, dimension
54+
LSP 0 0 6
55+
! PERIHELION 8.5150105724807035E-01
56+
! APHELION 4.1801244442498522E+00
57+
! ANODE 1.6132920678553648E+00
58+
! DNODE -1.6139338737144644E-02
59+
! MOID 2.8281976977061222E-03
60+
! PERIOD 1.4575246142278593E+03
61+
! PHA F
62+
! VINFTY 14.2161102117779
63+
! U_PAR 5.5
64+
! ORB_TYPE Apollo
65+
! RMS 1.45945E-04 2.08511E-05 7.80533E-05 1.08159E-05 9.07220E-05 3.57225E-03
66+
COV 2.129990103626278E-08 3.043103695236090E-09 1.138994073085263E-08
67+
COV -1.297300567885438E-09 -1.321094812357632E-08 -5.213516734886736E-07
68+
COV 4.347664336862408E-10 1.627274457493249E-09 -1.853526029216412E-10
69+
COV -1.887473042074563E-09 -7.448518590163292E-08 6.092321667952164E-09
70+
COV -6.879908552449990E-10 -7.069797588501348E-09 -2.787885242390572E-07
71+
COV 1.169847013231039E-10 7.781995171805923E-10 3.175091792990313E-08
72+
COV 8.230474520730192E-09 3.233601008844550E-07 1.276097850815426E-05
73+
COR 1.000000000000000E+00 9.999998967955239E-01 9.998647520507938E-01
74+
COR -8.218400094356861E-01 -9.977753189147101E-01 -9.999999781092901E-01
75+
COR 9.999999999999999E-01 9.998650578225570E-01 -8.218757198089662E-01
76+
COR -9.977926872410703E-01 -9.999998019539900E-01 9.999999999999998E-01
77+
COR -8.149420314930980E-01 -9.983966836512449E-01 -9.998652884032685E-01
78+
COR 1.000000000000000E+00 7.930744967922404E-01 8.217690139449345E-01
79+
COR 1.000000000000000E+00 9.977735927640640E-01 1.000000000000000E+00
80+
81+
"""
82+
lines = data.strip().split("\n")
83+
result = {}
84+
85+
# Parse header
86+
header = {}
87+
for line in lines:
88+
line = line.strip()
89+
if line == "END_OF_HEADER":
90+
break
91+
if "=" in line:
92+
key, value = line.split("=", 1)
93+
header[key.strip()] = value.split("!")[0].strip().strip("'")
94+
result["header"] = header
95+
96+
# Find object ID
97+
for line in lines:
98+
if not line.startswith(
99+
("!", " ", "format", "rectype", "refsys", "END_OF_HEADER")
100+
):
101+
result["object_id"] = line.strip()
102+
break
103+
104+
# Parse Keplerian elements
105+
for line in lines:
106+
if line.strip().startswith("KEP"):
107+
elements = line.split()[1:]
108+
result["elements"] = {
109+
"a": float(elements[0]), # semi-major axis
110+
"e": float(elements[1]), # eccentricity
111+
"i": float(elements[2]), # inclination
112+
"node": float(elements[3]), # longitude of ascending node
113+
"peri": float(elements[4]), # argument of perihelion
114+
"M": float(elements[5]), # mean anomaly
115+
}
116+
117+
# Parse epoch
118+
for line in lines:
119+
if line.strip().startswith("MJD"):
120+
result["epoch"] = float(line.split()[1])
121+
result["time_system"] = line.split()[2]
122+
123+
# Parse magnitude
124+
for line in lines:
125+
if line.strip().startswith("MAG"):
126+
mag_data = line.split()[1:]
127+
result["magnitude"] = {
128+
"value": float(mag_data[0]),
129+
"uncertainty": float(mag_data[1]),
130+
}
131+
132+
# Parse derived parameters (marked with !)
133+
derived = {}
134+
for line in lines:
135+
if line.strip().startswith("!") and len(line.split()) >= 3:
136+
key = line.split()[1].lower()
137+
try:
138+
value = float(line.split()[2])
139+
derived[key] = value
140+
except ValueError:
141+
derived[key] = line.split()[2]
142+
result["derived"] = derived
143+
144+
# Parse covariance matrix
145+
cov_matrix = []
146+
for line in lines:
147+
if line.strip().startswith("COV"):
148+
cov_matrix.extend([float(x) for x in line.split()[1:]])
149+
if cov_matrix:
150+
# Upper triangular matrix with order
151+
# (1,1) (1,2) (1,3)
152+
# (1,4) (1,5) (1,6)
153+
# (2,2) (2,3) (2,4)
154+
# (2,5) (2,6) (3,3)
155+
# (3,4) (3,5) (3,6)
156+
# (4,4) (4,5) (4,6)
157+
# (5,5) (5,6) (6,6)
158+
result["covariance"] = _upper_triangular_to_full(np.array(cov_matrix))
159+
160+
# Parse correlation matrix
161+
cor_matrix = []
162+
for line in lines:
163+
if line.strip().startswith("COR"):
164+
cor_matrix.extend([float(x) for x in line.split()[1:]])
165+
if cor_matrix:
166+
result["correlation"] = _upper_triangular_to_full(np.array(cor_matrix))
167+
168+
return result
169+
170+
171+
def query_neocc(
172+
object_ids: Union[List, npt.ArrayLike],
173+
orbit_type: Literal["ke", "eq"] = "ke",
174+
orbit_epoch: Literal["middle", "present-day"] = "present-day",
175+
) -> Orbits:
176+
"""
177+
Query ESA's Near-Earth Object Coordination Centre (NEOCC) database for orbital elements of the specified NEOs.
178+
179+
Parameters
180+
----------
181+
object_ids : Union[List, npt.ArrayLike]
182+
Object IDs / designations recognizable by NEOCC.
183+
orbit_type : ["ke", "eq"]
184+
Type of orbital elements to query.
185+
orbit_epoch : ["middle", "present-day"]
186+
Epoch of the orbital elements to query.
187+
188+
Returns
189+
-------
190+
orbits : `~adam_core.orbits.Orbits`
191+
Orbits object containing the orbital elements of the specified NEOs.
192+
"""
193+
base_url = "https://neo.ssa.esa.int/PSDB-portlet/download"
194+
195+
if orbit_type == "eq":
196+
raise NotImplementedError("Equinoctial elements are not supported yet.")
197+
198+
if orbit_epoch == "middle":
199+
orbit_epoch = 0
200+
elif orbit_epoch == "present-day":
201+
orbit_epoch = 1
202+
else:
203+
raise ValueError(f"Invalid orbit epoch: {orbit_epoch}")
204+
205+
orbits = Orbits.empty()
206+
207+
for object_id in object_ids:
208+
209+
# Clean object ID so that there are no spaces
210+
object_id = object_id.replace(" ", "")
211+
212+
params = {"file": f"{object_id}.{orbit_type}{orbit_epoch}"}
213+
214+
response = requests.get(base_url, params=params)
215+
response.raise_for_status()
216+
217+
data = _parse_oef(response.text)
218+
if orbit_type == "ke":
219+
220+
time_scale = data["time_system"]
221+
if time_scale == "TDT":
222+
time_scale = "tt"
223+
else:
224+
raise ValueError(f"Unsupported time scale: {time_scale}")
225+
226+
if data["header"]["refsys"] != "ECLM J2000":
227+
raise ValueError(
228+
f"Unsupported reference system: {data['header']['refsys']}"
229+
)
230+
231+
orbit = Orbits.from_kwargs(
232+
orbit_id=[data["object_id"]],
233+
object_id=[data["object_id"]],
234+
coordinates=KeplerianCoordinates.from_kwargs(
235+
a=[data["elements"]["a"]],
236+
e=[data["elements"]["e"]],
237+
i=[data["elements"]["i"]],
238+
raan=[data["elements"]["node"]],
239+
ap=[data["elements"]["peri"]],
240+
M=[data["elements"]["M"]],
241+
time=Timestamp.from_mjd([data["epoch"]], scale=time_scale),
242+
covariance=CoordinateCovariances.from_matrix(
243+
data["covariance"].reshape(
244+
1,
245+
6,
246+
6,
247+
)
248+
),
249+
frame="ecliptic",
250+
origin=Origin.from_kwargs(code=["SUN"]),
251+
).to_cartesian(),
252+
)
253+
orbits = qv.concatenate([orbits, orbit])
254+
255+
return orbits

0 commit comments

Comments
 (0)