diff --git a/astroquery/jplhorizons/__init__.py b/astroquery/jplhorizons/__init__.py index b910dd1de7..8895ab1a7b 100644 --- a/astroquery/jplhorizons/__init__.py +++ b/astroquery/jplhorizons/__init__.py @@ -229,6 +229,36 @@ class Conf(_config.ConfigNamespace): 'VX': ('vx', 'AU/d'), 'VY': ('vy', 'AU/d'), 'VZ': ('vz', 'AU/d'), + + 'X_s': ('x_s', 'AU'), + 'Y_s': ('y_s', 'AU'), + 'Z_s': ('z_s', 'AU'), + 'VX_s': ('vx_s', 'AU/d'), + 'VY_s': ('vy_s', 'AU/d'), + 'VZ_s': ('vz_s', 'AU/d'), + + 'A_s': ('a_s', 'AU'), + 'C_s': ('c_s', 'AU'), + 'N_s': ('n_s', 'AU'), + 'VA_s': ('va_s', 'AU/d'), + 'VC_s': ('vc_s', 'AU/d'), + 'VN_s': ('vn_s', 'AU/d'), + + 'R_s': ('r_s', 'AU'), + 'T_s': ('t_s', 'AU'), + # N_s is repeated here. + 'VR_s': ('vr_s', 'AU/d'), + 'VT_s': ('vt_s', 'AU/d'), + # VN_s is repeated here. + + # Note: A_s is duplicated for POS (p) and ACN (a) uncertainties. It + # is up to the user to differentiate them! (They have the same units.) + 'D_s': ('d_s', 'AU'), + # R_s is repeated here. + 'VA_RA_s': ('va_ra_s', 'AU/d'), + 'VD_DEC_s': ('va_dec_s', 'AU/d'), + # VR_s is repeated here. + 'LT': ('lighttime', 'd'), 'RG': ('range', 'AU'), 'RR': ('range_rate', diff --git a/astroquery/jplhorizons/core.py b/astroquery/jplhorizons/core.py index 34bf0f9f06..4851a45852 100644 --- a/astroquery/jplhorizons/core.py +++ b/astroquery/jplhorizons/core.py @@ -959,8 +959,8 @@ def elements_async(self, *, get_query_payload=False, def vectors_async(self, *, get_query_payload=False, closest_apparition=False, no_fragments=False, get_raw_response=False, cache=True, - refplane='ecliptic', aberrations='geometric', - delta_T=False,): + refplane='ecliptic', vector_table="3", + aberrations='geometric', delta_T=False,): """ Query JPL Horizons for state vectors. @@ -1057,6 +1057,14 @@ def vectors_async(self, *, get_query_payload=False, See :ref:`Horizons Reference Frames ` in the astroquery documentation for details. + + vector_table : string, optional + Selects the table of vectors to be returned. Options are numbers 1-6, + followed by any string of characters in the list [``'x'``, ``'a'``, + ``'r'``, ``'p'``]. Default: ``'3'``. + + See `Horizons User Manual `_ + for details. aberrations : string, optional Aberrations to be accounted for: [``'geometric'``, @@ -1156,6 +1164,7 @@ def vectors_async(self, *, get_query_payload=False, ('VEC_CORR', {'geometric': '"NONE"', 'astrometric': '"LT"', 'apparent': '"LT+S"'}[aberrations]), + ('VEC_TABLE', vector_table), ('VEC_DELTA_T', {True: 'YES', False: 'NO'}[delta_T]), ('OBJ_DATA', 'YES')] ) @@ -1314,16 +1323,19 @@ def _parse_result(self, response, verbose=None): elif (self.query_type == 'elements' and "JDTDB," in line): headerline = str(line).split(',') headerline[-1] = '_dump' - # read in vectors header line - elif (self.query_type == 'vectors' and "JDTDB," in line): - headerline = str(line).split(',') - headerline[-1] = '_dump' # identify end of data block if "$$EOE" in line: data_end_idx = idx # identify start of data block if "$$SOE" in line: data_start_idx = idx + 1 + + # read in vectors header line + # reading like this helps fix issues with commas after JDTDB + if self.query_type == 'vectors': + headerline_raw = str(src[idx - 2]).replace("JDTDB,", "JDTDB") + headerline = [" JDTDB", *str(headerline_raw).split("JDTDB")[1].split(',')] + headerline[-1] = '_dump' # read in targetname if "Target body name" in line: targetname = line[18:50].strip() @@ -1404,6 +1416,20 @@ def _parse_result(self, response, verbose=None): # strip whitespaces from column labels headerline = [h.strip() for h in headerline] + # add numbers to duplicates + headerline_seen = {} # format - column_name: [headerline_idx, count] + dup_col_to_orig = {} # format - remapped_column_name: [original_column_name, index], used for later processing + for i, col in enumerate(headerline): + if col in headerline_seen: + headerline_seen[col][1] += 1 + headerline[headerline_seen[col][0]] = f"{col}_1" + dup_col_to_orig[f"{col}_1"] = [col, 1] + + headerline[i] = f"{col}_{headerline_seen[col][1]}" + dup_col_to_orig[headerline[i]] = [col, headerline_seen[col][1]] + else: + headerline_seen[col] = [i, 1] + # remove all 'Cut-off' messages raw_data = [line for line in src[data_start_idx:data_end_idx] if 'Cut-off' not in line] @@ -1469,14 +1495,22 @@ def _parse_result(self, response, verbose=None): # set column units rename = [] for col in data.columns: - data[col].unit = column_defs[col][1] - if data[col].name != column_defs[col][0]: + # fetch from original definition, not remapped + col_unit = column_defs[dup_col_to_orig[col][0] if col in dup_col_to_orig.keys() else col] + + data[col].unit = col_unit[1] + if data[col].name != col_unit[0]: rename.append(data[col].name) # rename columns for col in rename: try: - data.rename_column(data[col].name, column_defs[col][0]) + if col in dup_col_to_orig.keys(): # preserve index on duplicate columns + to_rename = f"{column_defs[dup_col_to_orig[col][0]][0]}_{dup_col_to_orig[col][1]}" + else: + to_rename = column_defs[col][0] + + data.rename_column(data[col].name, to_rename) except KeyError: pass diff --git a/astroquery/jplhorizons/tests/test_jplhorizons.py b/astroquery/jplhorizons/tests/test_jplhorizons.py index 61440e0db2..b7e17c714d 100644 --- a/astroquery/jplhorizons/tests/test_jplhorizons.py +++ b/astroquery/jplhorizons/tests/test_jplhorizons.py @@ -268,6 +268,7 @@ def test_vectors_query_payload(): ('TP_TYPE', 'ABSOLUTE'), ('VEC_LABELS', 'YES'), ('VEC_CORR', '"NONE"'), + ('VEC_TABLE', '3'), ('VEC_DELTA_T', 'NO'), ('OBJ_DATA', 'YES'), ('CENTER', "'500@10'"), diff --git a/astroquery/jplhorizons/tests/test_jplhorizons_remote.py b/astroquery/jplhorizons/tests/test_jplhorizons_remote.py index 518e50222f..7f9a65d37b 100644 --- a/astroquery/jplhorizons/tests/test_jplhorizons_remote.py +++ b/astroquery/jplhorizons/tests/test_jplhorizons_remote.py @@ -353,6 +353,42 @@ def test_vectors_query(self): res['vz'], res['lighttime'], res['range'], res['range_rate']], rtol=1e-3) + + def test_vectors_query_two(self): + # check values of Ceres for a given epoch, with vector_table="2xarp" to get all possible information + # orbital uncertainty of Ceres is basically zero + res = jplhorizons.Horizons(id='Ceres', location='500@10', + id_type='smallbody', + epochs=2451544.5).vectors(vector_table="2xarp",)[0] + + assert res['targetname'] == "1 Ceres (A801 AA)" + assert res['datetime_str'] == "A.D. 2000-Jan-01 00:00:00.0000" + + assert_quantity_allclose( + [2451544.5, + -2.377530292832982E+00, 8.007772359639206E-01, + 4.628376133882323E-01, -3.605422228805115E-03, + -1.057883336698096E-02, 3.379790443661611E-04, + 1.69636278E-10, 4.38650236E-10, 1.97923277E-10, + 1.94851194E-12, 5.56918627E-13, 2.05288488E-12, + 4.69444042E-10, 2.17944530E-11, 1.98774780E-10, + 8.81447488E-14, 1.83081418E-12, 2.22745222E-12, + 2.54838011E-11, 4.69258226E-10, 1.98774780E-10, + 1.83140694E-12, 7.48243991E-14, 2.22745222E-12, + 4.68686492E-10, 2.00119134E-10, 2.54838011E-11, + 1.17091788E-13, 2.22563061E-12, 1.83140694E-12,], + [res['datetime_jd'], + res['x'], res['y'], + res['z'], res['vx'], + res['vy'], res['vz'], + res['x_s'], res['y_s'], res['z_s'], + res['vx_s'], res['vy_s'], res['vz_s'], + res['a_s_1'], res['c_s'], res['n_s_1'], + res['va_s'], res['vc_s'], res['vn_s_1'], + res['r_s_1'], res['t_s'], res['n_s_2'], + res['vr_s_1'], res['vt_s'], res['vn_s_2'], + res['a_s_2'], res['d_s'], res['r_s_2'], + res['va_ra_s'], res['va_dec_s'], res['vr_s_2'],], rtol=1e-3) def test_vectors_query_raw(self): # deprecated as of #2418