1
1
# Licensed under a 3-clause BSD style license - see LICENSE.rst
2
2
3
-
4
3
# 1. standard library imports
5
- from numpy import nan
6
- from numpy import isnan
7
- from numpy import ndarray
8
4
from collections import OrderedDict
5
+ from typing import Mapping
9
6
import warnings
10
7
11
8
# 2. third party imports
12
9
from requests .exceptions import HTTPError
10
+ from numpy import nan
11
+ from numpy import isnan
12
+ from numpy import ndarray
13
13
from astropy .table import Table , Column
14
14
from astropy .io import ascii
15
15
from astropy .time import Time
@@ -51,8 +51,16 @@ def __init__(self, id=None, *, location=None, epochs=None,
51
51
Parameters
52
52
----------
53
53
54
- id : str, required
55
- Name, number, or designation of the object to be queried.
54
+ id : str or dict, required
55
+ Name, number, or designation of target object. Uses the same codes
56
+ as JPL Horizons. Arbitrary topocentric coordinates can be added
57
+ in a dict. The dict has to be of the form
58
+ {``'lon'``: longitude in deg (East positive, West
59
+ negative), ``'lat'``: latitude in deg (North positive, South
60
+ negative), ``'elevation'``: elevation in km above the reference
61
+ ellipsoid, [``'body'``: Horizons body ID of the central body;
62
+ optional; if this value is not provided it is assumed that this
63
+ location is on Earth]}.
56
64
57
65
location : str or dict, optional
58
66
Observer's location for ephemerides queries or center body name for
@@ -108,9 +116,16 @@ def __init__(self, id=None, *, location=None, epochs=None,
108
116
"""
109
117
110
118
super ().__init__ ()
111
- self .id = id
112
- self .location = location
113
-
119
+ # check & format coordinate dictionaries for id and location; simply
120
+ # treat other values as given
121
+ if isinstance (id , Mapping ):
122
+ self .id = self ._prep_loc_dict (dict (id ), "id" )
123
+ else :
124
+ self .id = id
125
+ if isinstance (location , Mapping ):
126
+ self .location = self ._prep_loc_dict (dict (location ), "location" )
127
+ else :
128
+ self .location = location
114
129
# check for epochs to be dict or list-like; else: make it a list
115
130
if epochs is not None :
116
131
if isinstance (epochs , (list , tuple , ndarray )):
@@ -535,16 +550,22 @@ def ephemerides_async(self, *, airmass_lessthan=99,
535
550
536
551
URL = conf .horizons_server
537
552
538
- # check for required information
553
+ # check for required information and assemble commanddline stub
539
554
if self .id is None :
540
555
raise ValueError ("'id' parameter not set. Query aborted." )
556
+ elif isinstance (self .id , dict ):
557
+ commandline = (
558
+ f"g:{ self .id ['lon' ]} ,{ self .id ['lat' ]} ,"
559
+ f"{ self .id ['elevation' ]} @{ self .id ['body' ]} "
560
+ )
561
+ else :
562
+ commandline = str (self .id )
541
563
if self .location is None :
542
564
self .location = '500@399'
543
565
if self .epochs is None :
544
566
self .epochs = Time .now ().jd
567
+ # expand commandline based on self.id_type
545
568
546
- # assemble commandline based on self.id_type
547
- commandline = str (self .id )
548
569
if self .id_type in ['designation' , 'name' ,
549
570
'asteroid_name' , 'comet_name' ]:
550
571
commandline = ({'designation' : 'DES=' ,
@@ -580,19 +601,7 @@ def ephemerides_async(self, *, airmass_lessthan=99,
580
601
('EXTRA_PREC' , {True : 'YES' , False : 'NO' }[extra_precision ])])
581
602
582
603
if isinstance (self .location , dict ):
583
- if ('lon' not in self .location or 'lat' not in self .location or
584
- 'elevation' not in self .location ):
585
- raise ValueError (("'location' must contain lon, lat, "
586
- "elevation" ))
587
-
588
- if 'body' not in self .location :
589
- self .location ['body' ] = '399'
590
- request_payload ['CENTER' ] = 'coord@{:s}' .format (
591
- str (self .location ['body' ]))
592
- request_payload ['COORD_TYPE' ] = 'GEODETIC'
593
- request_payload ['SITE_COORD' ] = "'{:f},{:f},{:f}'" .format (
594
- self .location ['lon' ], self .location ['lat' ],
595
- self .location ['elevation' ])
604
+ request_payload = dict (** request_payload , ** self ._location_to_params (self .location ))
596
605
else :
597
606
request_payload ['CENTER' ] = "'" + str (self .location ) + "'"
598
607
@@ -1032,17 +1041,18 @@ def vectors_async(self, *, get_query_payload=False,
1032
1041
1033
1042
URL = conf .horizons_server
1034
1043
1035
- # check for required information
1044
+ # check for required information and assemble commandline stub
1036
1045
if self .id is None :
1037
1046
raise ValueError ("'id' parameter not set. Query aborted." )
1047
+ elif isinstance (self .id , dict ):
1048
+ commandline = "g:{lon},{lat},{elevation}@{body}" .format (** self .id )
1049
+ else :
1050
+ commandline = str (self .id )
1038
1051
if self .location is None :
1039
1052
self .location = '500@10'
1040
1053
if self .epochs is None :
1041
1054
self .epochs = Time .now ().jd
1042
-
1043
- # assemble commandline based on self.id_type
1044
- commandline = str (self .id )
1045
-
1055
+ # expand commandline based on self.id_type
1046
1056
if self .id_type in ['designation' , 'name' ,
1047
1057
'asteroid_name' , 'comet_name' ]:
1048
1058
commandline = ({'designation' : 'DES=' ,
@@ -1060,18 +1070,12 @@ def vectors_async(self, *, get_query_payload=False,
1060
1070
commandline += ' CAP{:s};' .format (closest_apparition )
1061
1071
if no_fragments :
1062
1072
commandline += ' NOFRAG;'
1063
-
1064
- if isinstance (self .location , dict ):
1065
- raise ValueError (('cannot use topographic position in state'
1066
- 'vectors query' ))
1067
-
1068
- # configure request_payload for ephemerides query
1073
+ # configure request_payload for vectors query
1069
1074
request_payload = OrderedDict ([
1070
1075
('format' , 'text' ),
1071
1076
('EPHEM_TYPE' , 'VECTORS' ),
1072
1077
('OUT_UNITS' , 'AU-D' ),
1073
1078
('COMMAND' , '"' + commandline + '"' ),
1074
- ('CENTER' , ("'" + str (self .location ) + "'" )),
1075
1079
('CSV_FORMAT' , ('"YES"' )),
1076
1080
('REF_PLANE' , {'ecliptic' : 'ECLIPTIC' ,
1077
1081
'earth' : 'FRAME' ,
@@ -1086,7 +1090,12 @@ def vectors_async(self, *, get_query_payload=False,
1086
1090
('VEC_DELTA_T' , {True : 'YES' , False : 'NO' }[delta_T ]),
1087
1091
('OBJ_DATA' , 'YES' )]
1088
1092
)
1089
-
1093
+ if isinstance (self .location , dict ):
1094
+ request_payload = dict (
1095
+ ** request_payload , ** self ._location_to_params (self .location )
1096
+ )
1097
+ else :
1098
+ request_payload ['CENTER' ] = "'" + str (self .location ) + "'"
1090
1099
# parse self.epochs
1091
1100
if isinstance (self .epochs , (list , tuple , ndarray )):
1092
1101
request_payload ['TLIST' ] = "\n " .join ([str (epoch ) for epoch in
@@ -1132,6 +1141,30 @@ def vectors_async(self, *, get_query_payload=False,
1132
1141
return response
1133
1142
1134
1143
# ---------------------------------- parser functions
1144
+ @staticmethod
1145
+ def _prep_loc_dict (loc_dict , attr_name ):
1146
+ """prepare coord specification dict for 'location' or 'id'"""
1147
+ if {'lat' , 'lon' , 'elevation' } - loc_dict .keys ():
1148
+ raise ValueError (
1149
+ f"dict values for '{ attr_name } ' must contain 'lat', 'lon', "
1150
+ "'elevation' (and optionally 'body')"
1151
+ )
1152
+ if 'body' not in loc_dict :
1153
+ loc_dict ['body' ] = 399
1154
+ return loc_dict
1155
+
1156
+ @staticmethod
1157
+ def _location_to_params (loc_dict ):
1158
+ """translate a 'location' dict to a dict of request parameters"""
1159
+ loc_dict = {
1160
+ "CENTER" : f"coord@{ loc_dict ['body' ]} " ,
1161
+ "COORD_TYPE" : "GEODETIC" ,
1162
+ "SITE_COORD" : "," .join (
1163
+ str (float (loc_dict [k ])) for k in ['lon' , 'lat' , 'elevation' ]
1164
+ )
1165
+ }
1166
+ loc_dict ["SITE_COORD" ] = f"'{ loc_dict ['SITE_COORD' ]} '"
1167
+ return loc_dict
1135
1168
1136
1169
def _parse_result (self , response , verbose = None ):
1137
1170
"""
@@ -1181,14 +1214,18 @@ def _parse_result(self, response, verbose=None):
1181
1214
H , G = nan , nan
1182
1215
M1 , M2 , k1 , k2 , phcof = nan , nan , nan , nan , nan
1183
1216
headerline = []
1217
+ centername = ''
1184
1218
for idx , line in enumerate (src ):
1185
1219
# read in ephemerides header line; replace some field names
1186
1220
if (self .query_type == 'ephemerides' and
1187
1221
"Date__(UT)__HR:MN" in line ):
1188
1222
headerline = str (line ).split (',' )
1189
1223
headerline [2 ] = 'solar_presence'
1190
- headerline [3 ] = 'flags'
1224
+ headerline [3 ] = "lunar_presence" if "Earth" in centername else "interfering_body"
1191
1225
headerline [- 1 ] = '_dump'
1226
+ if isinstance (self .id , dict ) or str (self .id ).startswith ('g:' ):
1227
+ headerline [4 ] = 'nearside_flag'
1228
+ headerline [5 ] = 'illumination_flag'
1192
1229
# read in elements header line
1193
1230
elif (self .query_type == 'elements' and
1194
1231
"JDTDB," in line ):
@@ -1208,6 +1245,9 @@ def _parse_result(self, response, verbose=None):
1208
1245
# read in targetname
1209
1246
if "Target body name" in line :
1210
1247
targetname = line [18 :50 ].strip ()
1248
+ # read in center body name
1249
+ if "Center body name" in line :
1250
+ centername = line [18 :50 ].strip ()
1211
1251
# read in H and G (if available)
1212
1252
if "rotational period in hours)" in line :
1213
1253
HGline = src [idx + 2 ].split ('=' )
0 commit comments