From 6722d7af91393273f4a042cc9c9a5546bead4958 Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Thu, 7 Apr 2022 21:12:58 -0700 Subject: [PATCH 01/25] started building infrastructure for solarsystem.pds.RingNode --- astroquery/solarsystem/pds/__init__.py | 55 +++++ astroquery/solarsystem/pds/core.py | 206 +++++++++++++++++++ astroquery/solarsystem/pds/tests/__init__.py | 0 3 files changed, 261 insertions(+) create mode 100644 astroquery/solarsystem/pds/__init__.py create mode 100644 astroquery/solarsystem/pds/core.py create mode 100644 astroquery/solarsystem/pds/tests/__init__.py diff --git a/astroquery/solarsystem/pds/__init__.py b/astroquery/solarsystem/pds/__init__.py new file mode 100644 index 0000000000..9c4adc8ea7 --- /dev/null +++ b/astroquery/solarsystem/pds/__init__.py @@ -0,0 +1,55 @@ +''' +RingNode +-------- + +:author: Ned Molter (emolter@berkeley.edu) +''' + +from astropy import config as _config + + +class Conf(_config.ConfigNamespace): + """ + Configuration parameters for `astroquery.solarsystem.pds`. + """ + + # server settings + pds_server = _config.ConfigItem( + ['https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?', ], + 'Ring Node') + + # implement later: other pds tools + + timeout = _config.ConfigItem( + 30, + 'Time limit for connecting to PDS servers.') + + # PDS settings - put hardcoded dictionaries of any kind here + + planet_defaults = {'jupiter':{ + 'ephem':'000 URA111 %2B+URA115+%2B+DE440', + 'moons':'727+All+inner+moons+%28U1-U15%2CU25-U27%29&' + }, + 'saturn':{ + 'ephem':'000+URA111+%2B+URA115+%2B+DE440', + 'moons':'727+All+inner+moons+%28U1-U15%2CU25-U27%29&' + }, + 'uranus':{ + 'ephem':'000 URA111 + URA115 + DE440', + 'moons':'727 All inner moons (U1-U15,U25-U27)' + }, + 'neptune':{ + 'ephem':'000+URA111+%2B+URA115+%2B+DE440', + 'moons':'727+All+inner+moons+%28U1-U15%2CU25-U27%29&' + } + } + + + +conf = Conf() + +#from .core import RingNode, RingNodeClass + +#__all__ = ['RingNode', 'RingNodeClass', +# 'Conf', 'conf', +# ] \ No newline at end of file diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py new file mode 100644 index 0000000000..d60b4d9342 --- /dev/null +++ b/astroquery/solarsystem/pds/core.py @@ -0,0 +1,206 @@ + +# 1. standard library imports +import numpy as np +from collections import OrderedDict + +# 2. third party imports +from astropy.time import Time +from astropy.table import Table, Column + +# 3. local imports - use relative imports +# commonly required local imports shown below as example +# all Query classes should inherit from BaseQuery. +## CHANGE TO RELATIVE IMPORTS ONCE THIS ALL WORKS RIGHT +from astroquery.query import BaseQuery +# import configurable items declared in __init__.py, e.g. hardcoded dictionaries +from __init__ import conf +from astroquery.utils import async_to_sync + + +@async_to_sync +class RingNodeClass(BaseQuery): + ''' + for querying the Planetary Ring Node ephemeris tools + + + ''' + + TIMEOUT = conf.timeout + + def __init__(self, planet=None, obstime=None): + '''Instantiate Planetary Ring Node query + + Parameters + ---------- + planet : str, required. one of Jupiter, Saturn, Uranus, or Neptune + Name, number, or designation of the object to be queried. + obstime : str, in JD or MJD format. If no obstime is provided, the current + time is used. + ''' + + super().__init__() + self.planet = planet + if self.planet is not None: + self.planet = self.planet.lower() + if self.planet not in ['jupiter', 'saturn', 'uranus', 'neptune']: + raise ValueError("illegal value for 'planet' parameter (must be Jupiter, Saturn, Uranus, or Neptune") + + self.obstime = obstime + + + def __str__(self): + ''' + String representation of RingNodeClass object instance' + + Examples + -------- + >>> from astroquery.solarsystem.pds import RingNode + >>> uranus = Horizons(planet='Uranus', + ... obstime='2017-01-01 00:00 + >>> print(uranus) # doctest: +SKIP + PDSRingNode instance "Uranus"; obstime=2017-01-01 00:00 + ''' + return ('PDSRingNode instance \"{:s}\"; obstime={:s}').format( + str(self.planet), + str(self.obstime)) + # --- pretty stuff above this line, get it working below this line --- + + def ephemeris_async(self, + get_query_payload=False, + get_raw_response=False, + cache=True): + ''' + send query to server + + note this interacts with utils.async_to_sync to be called as ephemeris() + + Returns + ------- + response : `requests.Response` + The response of the HTTP request. + + Examples + -------- + >>> from astroquery.planetary.pds import RingNode + >>> uranus = Horizons(planet='Uranus', + ... obstime='2017-01-01 00:00 + >>> eph = obj.ephemeris() # doctest: +SKIP + >>> print(eph) # doctest: +SKIP + table here... + ''' + + URL = conf.pds_server + #URL = 'https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?' + + # check for required information + if self.planet is None: + raise ValueError("'planet' parameter not set. Query aborted.") + if self.obstime is None: + self.obstime = Time.now().jd + + # configure request_payload for ephemeris query + # start with successful query and incrementally de-hardcode stuff + request_payload = OrderedDict([ + ('abbrev', self.planet[:3]), + ('ephem', conf.planet_defaults[self.planet]['ephem']), # change hardcoding for other planets + ('time', self.obstime), + ('fov', 10), #for now do not care about the diagram. next few hardcoded + ('fov_unit', self.planet.capitalize()+' radii'), + ('center', 'body'), + ('center_body', self.planet.capitalize()), + ('center_ansa', 'Epsilon Ring'), + ('center_ew', 'east'), + ('center_ra', ''), + ('center_ra_type', 'hours'), + ('center_dec', ''), + ('center_star', ''), + ('viewpoint', 'observatory'), # de-hardcode later! + ('observatory', "Earth's center"), # de-hardcode later! + ('latitude',''), # de-hardcode later! + ('longitude',''), # de-hardcode later! + ('lon_dir',''), # de-hardcode later! + ('altitude',''), # de-hardcode later! + ('moons',conf.planet_defaults[self.planet]['moons']), # change hardcoding for other planets + ('rings','All rings'), # check if this works for other planets + ('extra_ra',''), # diagram stuff. next few can be hardcoded + ('extra_ra_type','hours'), + ('extra_dec',''), + ('extra_name',''), + ('title',''), + ('labels','Small (6 points)'), + ('moonpts','0'), + ('blank', 'No'), + ('peris', 'None'), + ('peripts', '4'), + ('meridians', 'Yes'), + ('output', 'html') + ]) + + # return request_payload if desired + if get_query_payload: + return request_payload + + # set return_raw flag, if raw response desired + if get_raw_response: + self.return_raw = True + + # query and parse + response = self._request('GET', URL, params=request_payload, + timeout=self.TIMEOUT, cache=cache) + self.uri = response.url + + ## questions + # where does the Horizons one actually call the parser? I don't get it... is parse_response somehow put inside self._request? need to look at base class. + # I guess this is supposed to return response as requests.Request object + # but then when is parse_response used? + # how does the Horizons one know to call ephemerides_async when it asks for ephemerides()? + + return response + + def _parse_ringnode(self, src): + ''' + Routine for parsing data from ring node + ''' + + self.raw_response = src + + + + return src + + def _parse_result(self, response, verbose = None): + ''' + Routine for managing parser calls + note this MUST be named exactly _parse_result so it interacts with async_to_sync properly + + Parameters + ---------- + self : RingNodeClass instance + response : string + raw response from server + + Returns + ------- + data : `astropy.Table` + ''' + self.last_response = response + try: + data = self._parse_ringnode(response.text) + except: + try: + self._last_query.remove_cache_file(self.cache_location) + except OSError: + # this is allowed: if `cache` was set to False, this + # won't be needed + pass + raise + return data #astropy table + +RingNode = RingNodeClass() + +if __name__ == "__main__": + + uranus = RingNodeClass('Uranus', '2019-10-28 00:00') + response = uranus.ephemeris() + print(response) + \ No newline at end of file diff --git a/astroquery/solarsystem/pds/tests/__init__.py b/astroquery/solarsystem/pds/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From d96fa2e9512ce0b7e029224801751acf60fea2d1 Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Fri, 8 Apr 2022 14:13:31 -0700 Subject: [PATCH 02/25] minimum working example. parses basic query for all six targets --- astroquery/solarsystem/pds/__init__.py | 38 +++-- astroquery/solarsystem/pds/core.py | 195 ++++++++++++++++++++++--- 2 files changed, 201 insertions(+), 32 deletions(-) diff --git a/astroquery/solarsystem/pds/__init__.py b/astroquery/solarsystem/pds/__init__.py index 9c4adc8ea7..05c10f9ab5 100644 --- a/astroquery/solarsystem/pds/__init__.py +++ b/astroquery/solarsystem/pds/__init__.py @@ -26,22 +26,42 @@ class Conf(_config.ConfigNamespace): # PDS settings - put hardcoded dictionaries of any kind here - planet_defaults = {'jupiter':{ - 'ephem':'000 URA111 %2B+URA115+%2B+DE440', - 'moons':'727+All+inner+moons+%28U1-U15%2CU25-U27%29&' + planet_defaults = {'mars':{ + 'ephem':'000 MAR097 + DE440', + 'moons':'402 Phobos, Deimos', + 'center_ansa':'Phobos Ring', + 'rings':'Phobos, Deimos', + }, + 'jupiter':{ + 'ephem':'000 JUP365 + DE440', + 'moons':'516 All inner moons (J1-J5,J14-J16)', + 'center_ansa':'Main Ring', + 'rings':'Main & Gossamer', }, 'saturn':{ - 'ephem':'000+URA111+%2B+URA115+%2B+DE440', - 'moons':'727+All+inner+moons+%28U1-U15%2CU25-U27%29&' + 'ephem':'000 SAT389 + SAT393 + SAT427 + DE440', + 'moons':'653 All inner moons (S1-S18,S32-S35,S49,S53)', + 'center_ansa':'A', + 'rings':'A,B,C,F,G,E', }, 'uranus':{ 'ephem':'000 URA111 + URA115 + DE440', - 'moons':'727 All inner moons (U1-U15,U25-U27)' + 'moons':'727 All inner moons (U1-U15,U25-U27)', + 'center_ansa':'Epsilon', + 'rings':'All rings', }, 'neptune':{ - 'ephem':'000+URA111+%2B+URA115+%2B+DE440', - 'moons':'727+All+inner+moons+%28U1-U15%2CU25-U27%29&' - } + 'ephem':'000 NEP081 + NEP095 + DE440', + 'moons':'814 All inner moons (N1-N8,N14)', + 'center_ansa':'Adams Ring', + 'rings':'Galle, LeVerrier, Arago, Adams', + }, + 'pluto':{ + 'ephem':'000 PLU058 + DE440', + 'moons':'905 All moons (P1-P5)', + 'center_ansa':'Hydra', + 'rings':'Styx, Nix, Kerberos, Hydra', + } } diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index d60b4d9342..adc9a56259 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -2,10 +2,13 @@ # 1. standard library imports import numpy as np from collections import OrderedDict +import re # 2. third party imports from astropy.time import Time -from astropy.table import Table, Column +from astropy import table +from astropy.io import ascii +from bs4 import BeautifulSoup # 3. local imports - use relative imports # commonly required local imports shown below as example @@ -27,14 +30,14 @@ class RingNodeClass(BaseQuery): TIMEOUT = conf.timeout - def __init__(self, planet=None, obstime=None): + def __init__(self, planet=None, obs_time=None): '''Instantiate Planetary Ring Node query Parameters ---------- planet : str, required. one of Jupiter, Saturn, Uranus, or Neptune Name, number, or designation of the object to be queried. - obstime : str, in JD or MJD format. If no obstime is provided, the current + obs_time : str, in JD or MJD format. If no obs_time is provided, the current time is used. ''' @@ -42,10 +45,10 @@ def __init__(self, planet=None, obstime=None): self.planet = planet if self.planet is not None: self.planet = self.planet.lower() - if self.planet not in ['jupiter', 'saturn', 'uranus', 'neptune']: - raise ValueError("illegal value for 'planet' parameter (must be Jupiter, Saturn, Uranus, or Neptune") + if self.planet not in ['mars', 'jupiter', 'saturn', 'uranus', 'neptune', 'pluto']: + raise ValueError("illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto'") - self.obstime = obstime + self.obs_time = obs_time def __str__(self): @@ -56,13 +59,13 @@ def __str__(self): -------- >>> from astroquery.solarsystem.pds import RingNode >>> uranus = Horizons(planet='Uranus', - ... obstime='2017-01-01 00:00 + ... obs_time='2017-01-01 00:00 >>> print(uranus) # doctest: +SKIP - PDSRingNode instance "Uranus"; obstime=2017-01-01 00:00 + PDSRingNode instance "Uranus"; obs_time=2017-01-01 00:00 ''' - return ('PDSRingNode instance \"{:s}\"; obstime={:s}').format( + return ('PDSRingNode instance \"{:s}\"; obs_time={:s}').format( str(self.planet), - str(self.obstime)) + str(self.obs_time)) # --- pretty stuff above this line, get it working below this line --- def ephemeris_async(self, @@ -83,7 +86,7 @@ def ephemeris_async(self, -------- >>> from astroquery.planetary.pds import RingNode >>> uranus = Horizons(planet='Uranus', - ... obstime='2017-01-01 00:00 + ... obs_time='2017-01-01 00:00 >>> eph = obj.ephemeris() # doctest: +SKIP >>> print(eph) # doctest: +SKIP table here... @@ -95,20 +98,25 @@ def ephemeris_async(self, # check for required information if self.planet is None: raise ValueError("'planet' parameter not set. Query aborted.") - if self.obstime is None: - self.obstime = Time.now().jd + if self.obs_time is None: + self.obs_time = Time.now().jd + + ''' + https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?abbrev=jup&ephem=000+JUP365+%2B+DE440&time=2021-10-07+07%3A25&fov=10&fov_unit=Jupiter+radii¢er=body¢er_body=Jupiter¢er_ansa=Main+Ring¢er_ew=east¢er_ra=¢er_ra_type=hours¢er_dec=¢er_star=&viewpoint=observatory&observatory=Earth%27s+center&latitude=&longitude=&lon_dir=east&altitude=&moons=516+All+inner+moons+%28J1-J5%2CJ14-J16%29&rings=Main+%26+Gossamer&torus_inc=6.8&torus_rad=422000&extra_ra=&extra_ra_type=hours&extra_dec=&extra_name=&title=&labels=Small+%286+points%29&moonpts=0&blank=No&meridians=Yes&output=HTML + ''' # configure request_payload for ephemeris query # start with successful query and incrementally de-hardcode stuff + # thankfully, adding extra planet-specific keywords here does not break query for other planets request_payload = OrderedDict([ ('abbrev', self.planet[:3]), ('ephem', conf.planet_defaults[self.planet]['ephem']), # change hardcoding for other planets - ('time', self.obstime), + ('time', self.obs_time), ('fov', 10), #for now do not care about the diagram. next few hardcoded ('fov_unit', self.planet.capitalize()+' radii'), ('center', 'body'), ('center_body', self.planet.capitalize()), - ('center_ansa', 'Epsilon Ring'), + ('center_ansa', conf.planet_defaults[self.planet]['center_ansa']), ('center_ew', 'east'), ('center_ra', ''), ('center_ra_type', 'hours'), @@ -121,7 +129,8 @@ def ephemeris_async(self, ('lon_dir',''), # de-hardcode later! ('altitude',''), # de-hardcode later! ('moons',conf.planet_defaults[self.planet]['moons']), # change hardcoding for other planets - ('rings','All rings'), # check if this works for other planets + ('rings',conf.planet_defaults[self.planet]['rings']), # check if this works for other planets + ('arcmodel','#3 (820.1121 deg/day)'), # check if this works for other planets ('extra_ra',''), # diagram stuff. next few can be hardcoded ('extra_ra_type','hours'), ('extra_dec',''), @@ -130,8 +139,10 @@ def ephemeris_async(self, ('labels','Small (6 points)'), ('moonpts','0'), ('blank', 'No'), + ('opacity', 'Transparent'), ('peris', 'None'), ('peripts', '4'), + ('arcpts', '4'), ('meridians', 'Yes'), ('output', 'html') ]) @@ -160,13 +171,134 @@ def ephemeris_async(self, def _parse_ringnode(self, src): ''' Routine for parsing data from ring node + + Parameters + ---------- + self : HorizonsClass instance + src : list + raw response from server + + + Returns + ------- + data : `astropy.Table` ''' self.raw_response = src + soup = BeautifulSoup(src, 'html.parser') + text = soup.get_text() + #print(repr(text)) + textgroups = re.split('\n\n|\n \n', text) + ringtable = None + for group in textgroups: + group = group.strip(', \n') + + #input parameters. only thing needed is obs_time + if group.startswith('Observation'): + obs_time = group.split('\n')[0].split('e: ')[-1].strip(', \n') + self.obs_time = obs_time + + #minor body table part 1 + elif group.startswith('Body'): + group = 'NAIF '+group #fixing lack of header for NAIF ID + bodytable = ascii.read(group, format='fixed_width', + col_starts=(0, 4, 18, 35, 54, 68, 80, 91), + col_ends= (4, 18, 35, 54, 68, 80, 91, 102), + names = ('NAIF ID', 'Body', 'RA', 'Dec', 'RA (deg)', 'Dec (deg)', 'dRA', 'dDec') + ) + #minor body table part 2 + elif group.startswith('Sub-'): + group = '\n'.join(group.split('\n')[1:]) #fixing two-row header + group = 'NAIF'+group[4:] + bodytable2 = ascii.read(group, format='fixed_width', + col_starts=(0, 4, 18, 28, 37, 49, 57, 71), + col_ends= (4, 18, 28, 37, 49, 57, 71, 90), + names = ('NAIF ID', 'Body', 'sub_obs_lon', 'sub_obs_lat', 'sub_sun_lon', 'sub_sun_lat', 'phase', 'distance') + ) + ## to do: add units!! + + #ring plane data + elif group.startswith('Ring s'): + lines = group.split('\n') + for line in lines: + l = line.split(':') + if 'Ring sub-solar latitude' in l[0]: + [sub_sun_lat, sub_sun_lat_min, sub_sun_lat_max] = [float(s.strip(', \n()')) for s in re.split('\(|to', l[1])] + systemtable = table.Table([['sub_sun_lat','sub_sun_lat_max','sub_sun_lat_min'], + [sub_sun_lat, sub_sun_lat_max, sub_sun_lat_min]], + names = ('Parameter', 'Value')) + + elif 'Ring plane opening angle' in l[0]: + systemtable.add_row(['opening_angle', float(re.sub('[a-zA-Z]+', '', l[1]).strip(', \n()'))]) + elif 'Ring center phase angle' in l[0]: + systemtable.add_row(['phase_angle', float(l[1].strip(', \n'))]) + elif 'Sub-solar longitude' in l[0]: + systemtable.add_row(['sub_sun_lon', float(re.sub('[a-zA-Z]+', '', l[1]).strip(', \n()'))]) + elif 'Sub-observer longitude' in l[0]: + systemtable.add_row(['sub_obs_lon', float(l[1].strip(', \n'))]) + else: + pass + ## to do: add units? + + #basic info about the planet + elif group.startswith('Sun-planet'): + lines = group.split('\n') + for line in lines: + l = line.split(':') + if 'Sun-planet distance (AU)' in l[0]: + systemtable.add_row(['d_sun_AU', float(l[1].strip(', \n'))]) + elif 'Observer-planet distance (AU)' in l[0]: + systemtable.add_row(['d_obs_AU', float(l[1].strip(', \n'))]) + elif 'Sun-planet distance (km)' in l[0]: + systemtable.add_row(['d_sun_km', float(l[1].split('x')[0].strip(', \n'))*1e6]) + elif 'Observer-planet distance (km)' in l[0]: + systemtable.add_row(['d_obs_km', float(l[1].split('x')[0].strip(', \n'))*1e6]) + elif 'Light travel time' in l[0]: + systemtable.add_row(['light_time', float(l[1].strip(', \n'))]) + else: + pass + + ## to do: add units? + + # --------- below this line, planet-specific info ------------ + # Uranus individual rings data + elif group.startswith('Ring '): + ringtable = ascii.read(' '+group, format='fixed_width', + col_starts=(5, 18, 29), + col_ends= (18, 29, 36), + names = ('ring', 'pericenter', 'ascending node') + ) + + # Saturn F-ring data - NEEDS TESTING + elif group.startswith('F Ring'): + lines = group.split('\n') + for line in lines: + l = line.split(':') + if 'F Ring pericenter' in l[0]: + peri = float(re.sub('[a-zA-Z]+', '', l[1]).strip(', \n()')) + elif 'F Ring ascending node' in l[0]: + ascn = float(l[1].strip(', \n')) + ringtable = table.Table([['F'], [peri], [ascn]], names=('ring', 'pericenter', 'ascending node')) + + # Neptune ring arcs data - NEEDS TESTING + elif group.startswith('Courage'): + lines = group.split('\n') + for i in range(len(lines)): + l = lines[i].split(':') + ring = l[0].split('longitude')[0].strip(', \n') + [min_angle, max_angle] = [float(s.strip(', \n')) for s in re.sub('[a-zA-Z]+', '', l[1]).strip(', \n()').split()] + if i == 0: + ringtable = table.Table([[ring], [min_angle], [max_angle]], names=('ring', 'min_angle', 'max_angle')) + else: + ringtable.add_row([ring, min_angle, max_angle]) + + else: + pass + # concatenate minor body table + bodytable = table.join(bodytable, bodytable2) - - return src + return systemtable, bodytable, ringtable def _parse_result(self, response, verbose = None): ''' @@ -185,7 +317,7 @@ def _parse_result(self, response, verbose = None): ''' self.last_response = response try: - data = self._parse_ringnode(response.text) + systemtable, bodytable, ringtable = self._parse_ringnode(response.text) except: try: self._last_query.remove_cache_file(self.cache_location) @@ -194,13 +326,30 @@ def _parse_result(self, response, verbose = None): # won't be needed pass raise - return data #astropy table + return systemtable, bodytable, ringtable #astropy table, astropy table, astropy table RingNode = RingNodeClass() if __name__ == "__main__": - uranus = RingNodeClass('Uranus', '2019-10-28 00:00') - response = uranus.ephemeris() - print(response) + # single basic query + neptune = RingNodeClass('Neptune', '2019-10-28 00:00') + systemtable, bodytable, ringtable = neptune.ephemeris() + + ''' + # basic query for all six targets + for major_body in ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto']: + nodequery = RingNode(major_body, '2022-05-03 00:00') + systemtable, bodytable, ringtable = nodequery.ephemeris() + + print(' ') + print(' ') + print('~'*40) + print(major_body) + print('~'*40) + print(systemtable) + print(bodytable) + print(ringtable) + ''' + \ No newline at end of file From 21e146c373d8cd4d6a1ce18226e43aedb286f15d Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Fri, 8 Apr 2022 16:29:05 -0700 Subject: [PATCH 03/25] allowed option to pass observatory coordinates --- astroquery/solarsystem/pds/__init__.py | 4 ++ astroquery/solarsystem/pds/core.py | 59 ++++++++++++++++++-------- 2 files changed, 46 insertions(+), 17 deletions(-) diff --git a/astroquery/solarsystem/pds/__init__.py b/astroquery/solarsystem/pds/__init__.py index 05c10f9ab5..9934725e78 100644 --- a/astroquery/solarsystem/pds/__init__.py +++ b/astroquery/solarsystem/pds/__init__.py @@ -63,6 +63,10 @@ class Conf(_config.ConfigNamespace): 'rings':'Styx, Nix, Kerberos, Hydra', } } + + neptune_arcmodels = {1:'#1 (820.1194 deg/day)', + 2:'#2 (820.1118 deg/day)', + 3:'#3 (820.1121 deg/day)'} diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index adc9a56259..f4dd57761f 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -69,6 +69,8 @@ def __str__(self): # --- pretty stuff above this line, get it working below this line --- def ephemeris_async(self, + observer_coords = None, + neptune_arcmodel = 3, get_query_payload=False, get_raw_response=False, cache=True): @@ -76,6 +78,11 @@ def ephemeris_async(self, send query to server note this interacts with utils.async_to_sync to be called as ephemeris() + + Parameters + ---------- + self : + observer_coords : three-element list/array/tuple of format (lat (deg), lon (deg east), altitude (m)) Returns ------- @@ -95,14 +102,28 @@ def ephemeris_async(self, URL = conf.pds_server #URL = 'https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?' - # check for required information + # check inputs and set defaults for optional inputs if self.planet is None: raise ValueError("'planet' parameter not set. Query aborted.") if self.obs_time is None: self.obs_time = Time.now().jd + if observer_coords is None: + viewpoint = 'observatory' + latitude, longitude, altitude = '', '', '' + else: + viewpoint = 'latlon' + try: + latitude, longitude, altitude = [float(j) for j in observer_coords] + assert -90. <= latitude <= 90. + assert -360. <= longitude <= 360. + except: + raise ValueError(f"Illegal observatory coordinates {observer_coords}. must be of format [lat(deg), lon(deg east), alt(m)]") + if neptune_arcmodel not in [1,2,3]: + raise ValueError(f"Illegal Neptune arc model {neptune_arcmodel}. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details)") - ''' - https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?abbrev=jup&ephem=000+JUP365+%2B+DE440&time=2021-10-07+07%3A25&fov=10&fov_unit=Jupiter+radii¢er=body¢er_body=Jupiter¢er_ansa=Main+Ring¢er_ew=east¢er_ra=¢er_ra_type=hours¢er_dec=¢er_star=&viewpoint=observatory&observatory=Earth%27s+center&latitude=&longitude=&lon_dir=east&altitude=&moons=516+All+inner+moons+%28J1-J5%2CJ14-J16%29&rings=Main+%26+Gossamer&torus_inc=6.8&torus_rad=422000&extra_ra=&extra_ra_type=hours&extra_dec=&extra_name=&title=&labels=Small+%286+points%29&moonpts=0&blank=No&meridians=Yes&output=HTML + + ''' + https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?abbrev=nep&ephem=000+NEP081+%2B+NEP095+%2B+DE440&time=2020-01-01+00%3A00&fov=10&fov_unit=Neptune+radii¢er=body¢er_body=Neptune¢er_ansa=Adams+Ring¢er_ew=east¢er_ra=¢er_ra_type=hours¢er_dec=¢er_star=&observatory=Earth%27s+center&viewpoint=latlon&latitude=19.827&longitude=-155.472&lon_dir=east&altitude=4216&moons=814+All+inner+moons+%28N1-N8%2CN14%29&rings=Galle%2C+LeVerrier%2C+Arago%2C+Adams&arcmodel=%233+%28820.1121+deg%2Fday%29&extra_ra=&extra_ra_type=hours&extra_dec=&extra_name=&title=&labels=Small+%286+points%29&moonpts=0&blank=No&arcpts=4&meridians=Yes&output=HTML ''' # configure request_payload for ephemeris query @@ -112,7 +133,7 @@ def ephemeris_async(self, ('abbrev', self.planet[:3]), ('ephem', conf.planet_defaults[self.planet]['ephem']), # change hardcoding for other planets ('time', self.obs_time), - ('fov', 10), #for now do not care about the diagram. next few hardcoded + ('fov', 10), #next few are figure options, can be hardcoded and ignored ('fov_unit', self.planet.capitalize()+' radii'), ('center', 'body'), ('center_body', self.planet.capitalize()), @@ -122,16 +143,16 @@ def ephemeris_async(self, ('center_ra_type', 'hours'), ('center_dec', ''), ('center_star', ''), - ('viewpoint', 'observatory'), # de-hardcode later! - ('observatory', "Earth's center"), # de-hardcode later! - ('latitude',''), # de-hardcode later! - ('longitude',''), # de-hardcode later! - ('lon_dir',''), # de-hardcode later! - ('altitude',''), # de-hardcode later! - ('moons',conf.planet_defaults[self.planet]['moons']), # change hardcoding for other planets - ('rings',conf.planet_defaults[self.planet]['rings']), # check if this works for other planets - ('arcmodel','#3 (820.1121 deg/day)'), # check if this works for other planets - ('extra_ra',''), # diagram stuff. next few can be hardcoded + ('viewpoint', viewpoint), + ('observatory', "Earth's center"), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories + ('latitude',latitude), + ('longitude',longitude), + ('lon_dir','east'), + ('altitude',altitude), + ('moons',conf.planet_defaults[self.planet]['moons']), + ('rings',conf.planet_defaults[self.planet]['rings']), + ('arcmodel',conf.neptune_arcmodels[neptune_arcmodel]), + ('extra_ra',''), # figure options below this line, can all be hardcoded and ignored ('extra_ra_type','hours'), ('extra_dec',''), ('extra_name',''), @@ -269,7 +290,7 @@ def _parse_ringnode(self, src): names = ('ring', 'pericenter', 'ascending node') ) - # Saturn F-ring data - NEEDS TESTING + # Saturn F-ring data elif group.startswith('F Ring'): lines = group.split('\n') for line in lines: @@ -280,7 +301,7 @@ def _parse_ringnode(self, src): ascn = float(l[1].strip(', \n')) ringtable = table.Table([['F'], [peri], [ascn]], names=('ring', 'pericenter', 'ascending node')) - # Neptune ring arcs data - NEEDS TESTING + # Neptune ring arcs data elif group.startswith('Courage'): lines = group.split('\n') for i in range(len(lines)): @@ -334,7 +355,11 @@ def _parse_result(self, response, verbose = None): # single basic query neptune = RingNodeClass('Neptune', '2019-10-28 00:00') - systemtable, bodytable, ringtable = neptune.ephemeris() + systemtable, bodytable, ringtable = neptune.ephemeris(neptune_arcmodel = 2, observer_coords = (10.0, -120.355, 1000)) + print(systemtable) + print(bodytable) + print(ringtable) + ''' # basic query for all six targets From ae97eb9830154b724b385fb1c5bd657f877e67f6 Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Mon, 11 Apr 2022 21:03:46 -0700 Subject: [PATCH 04/25] added some tests --- astroquery/solarsystem/pds/__init__.py | 111 ++-- astroquery/solarsystem/pds/core.py | 543 ++++++++++-------- .../solarsystem/pds/tests/test_ringnode.py | 110 ++++ 3 files changed, 476 insertions(+), 288 deletions(-) create mode 100644 astroquery/solarsystem/pds/tests/test_ringnode.py diff --git a/astroquery/solarsystem/pds/__init__.py b/astroquery/solarsystem/pds/__init__.py index 9934725e78..94eb705999 100644 --- a/astroquery/solarsystem/pds/__init__.py +++ b/astroquery/solarsystem/pds/__init__.py @@ -1,9 +1,9 @@ -''' +""" RingNode -------- :author: Ned Molter (emolter@berkeley.edu) -''' +""" from astropy import config as _config @@ -15,65 +15,68 @@ class Conf(_config.ConfigNamespace): # server settings pds_server = _config.ConfigItem( - ['https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?', ], - 'Ring Node') + ["https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?",], "Ring Node" + ) # implement later: other pds tools - timeout = _config.ConfigItem( - 30, - 'Time limit for connecting to PDS servers.') + timeout = _config.ConfigItem(30, "Time limit for connecting to PDS servers.") # PDS settings - put hardcoded dictionaries of any kind here - - planet_defaults = {'mars':{ - 'ephem':'000 MAR097 + DE440', - 'moons':'402 Phobos, Deimos', - 'center_ansa':'Phobos Ring', - 'rings':'Phobos, Deimos', - }, - 'jupiter':{ - 'ephem':'000 JUP365 + DE440', - 'moons':'516 All inner moons (J1-J5,J14-J16)', - 'center_ansa':'Main Ring', - 'rings':'Main & Gossamer', - }, - 'saturn':{ - 'ephem':'000 SAT389 + SAT393 + SAT427 + DE440', - 'moons':'653 All inner moons (S1-S18,S32-S35,S49,S53)', - 'center_ansa':'A', - 'rings':'A,B,C,F,G,E', - }, - 'uranus':{ - 'ephem':'000 URA111 + URA115 + DE440', - 'moons':'727 All inner moons (U1-U15,U25-U27)', - 'center_ansa':'Epsilon', - 'rings':'All rings', - }, - 'neptune':{ - 'ephem':'000 NEP081 + NEP095 + DE440', - 'moons':'814 All inner moons (N1-N8,N14)', - 'center_ansa':'Adams Ring', - 'rings':'Galle, LeVerrier, Arago, Adams', - }, - 'pluto':{ - 'ephem':'000 PLU058 + DE440', - 'moons':'905 All moons (P1-P5)', - 'center_ansa':'Hydra', - 'rings':'Styx, Nix, Kerberos, Hydra', - } - } - - neptune_arcmodels = {1:'#1 (820.1194 deg/day)', - 2:'#2 (820.1118 deg/day)', - 3:'#3 (820.1121 deg/day)'} - + + planet_defaults = { + "mars": { + "ephem": "000 MAR097 + DE440", + "moons": "402 Phobos, Deimos", + "center_ansa": "Phobos Ring", + "rings": "Phobos, Deimos", + }, + "jupiter": { + "ephem": "000 JUP365 + DE440", + "moons": "516 All inner moons (J1-J5,J14-J16)", + "center_ansa": "Main Ring", + "rings": "Main & Gossamer", + }, + "saturn": { + "ephem": "000 SAT389 + SAT393 + SAT427 + DE440", + "moons": "653 All inner moons (S1-S18,S32-S35,S49,S53)", + "center_ansa": "A", + "rings": "A,B,C,F,G,E", + }, + "uranus": { + "ephem": "000 URA111 + URA115 + DE440", + "moons": "727 All inner moons (U1-U15,U25-U27)", + "center_ansa": "Epsilon", + "rings": "All rings", + }, + "neptune": { + "ephem": "000 NEP081 + NEP095 + DE440", + "moons": "814 All inner moons (N1-N8,N14)", + "center_ansa": "Adams Ring", + "rings": "Galle, LeVerrier, Arago, Adams", + }, + "pluto": { + "ephem": "000 PLU058 + DE440", + "moons": "905 All moons (P1-P5)", + "center_ansa": "Hydra", + "rings": "Styx, Nix, Kerberos, Hydra", + }, + } + + neptune_arcmodels = { + 1: "#1 (820.1194 deg/day)", + 2: "#2 (820.1118 deg/day)", + 3: "#3 (820.1121 deg/day)", + } conf = Conf() -#from .core import RingNode, RingNodeClass +from .core import RingNode, RingNodeClass -#__all__ = ['RingNode', 'RingNodeClass', -# 'Conf', 'conf', -# ] \ No newline at end of file +__all__ = [ + "RingNode", + "RingNodeClass", + "Conf", + "conf", +] diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index f4dd57761f..85c9acda2b 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -1,8 +1,8 @@ - # 1. standard library imports import numpy as np from collections import OrderedDict import re +import warnings # 2. third party imports from astropy.time import Time @@ -13,25 +13,41 @@ # 3. local imports - use relative imports # commonly required local imports shown below as example # all Query classes should inherit from BaseQuery. -## CHANGE TO RELATIVE IMPORTS ONCE THIS ALL WORKS RIGHT -from astroquery.query import BaseQuery +from ...query import BaseQuery +from ...utils import async_to_sync + # import configurable items declared in __init__.py, e.g. hardcoded dictionaries -from __init__ import conf -from astroquery.utils import async_to_sync +from . import conf @async_to_sync class RingNodeClass(BaseQuery): - ''' + """ for querying the Planetary Ring Node ephemeris tools - ''' - TIMEOUT = conf.timeout + # basic query for all six targets + for major_body in ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto']: + nodequery = RingNode(major_body, '2022-05-03 00:00') + systemtable, bodytable, ringtable = nodequery.ephemeris() + + print(' ') + print(' ') + print('~'*40) + print(major_body) + print('~'*40) + print(systemtable) + print(bodytable) + print(ringtable) + + """ + + TIMEOUT = conf.timeout + def __init__(self, planet=None, obs_time=None): - '''Instantiate Planetary Ring Node query + """Instantiate Planetary Ring Node query Parameters ---------- @@ -39,42 +55,39 @@ def __init__(self, planet=None, obs_time=None): Name, number, or designation of the object to be queried. obs_time : str, in JD or MJD format. If no obs_time is provided, the current time is used. - ''' - + """ + super().__init__() self.planet = planet - if self.planet is not None: - self.planet = self.planet.lower() - if self.planet not in ['mars', 'jupiter', 'saturn', 'uranus', 'neptune', 'pluto']: - raise ValueError("illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto'") - self.obs_time = obs_time - - + def __str__(self): - ''' + """ String representation of RingNodeClass object instance' Examples -------- >>> from astroquery.solarsystem.pds import RingNode - >>> uranus = Horizons(planet='Uranus', - ... obs_time='2017-01-01 00:00 + >>> uranus = RingNode(planet='Uranus', + ... obs_time='2017-01-01 00:00') >>> print(uranus) # doctest: +SKIP - PDSRingNode instance "Uranus"; obs_time=2017-01-01 00:00 - ''' - return ('PDSRingNode instance \"{:s}\"; obs_time={:s}').format( - str(self.planet), - str(self.obs_time)) - # --- pretty stuff above this line, get it working below this line --- - - def ephemeris_async(self, - observer_coords = None, - neptune_arcmodel = 3, - get_query_payload=False, - get_raw_response=False, - cache=True): - ''' + PDSRingNode instance "Uranus"; obs_time='2017-01-01 00:00' + """ + return ('PDSRingNode instance "{:s}"; obs_time={:s}').format( + str(self.planet), str(self.obs_time) + ) + + # --- pretty stuff above this line, get it working below this line --- + + def ephemeris_async( + self, + observer_coords=None, + neptune_arcmodel=3, + get_query_payload=False, + get_raw_response=False, + cache=True, + ): + """ send query to server note this interacts with utils.async_to_sync to be called as ephemeris() @@ -91,83 +104,124 @@ def ephemeris_async(self, Examples -------- - >>> from astroquery.planetary.pds import RingNode - >>> uranus = Horizons(planet='Uranus', - ... obs_time='2017-01-01 00:00 + >>> from astroquery.solarsystem.pds import RingNode + >>> uranus = RingNode(planet='Uranus', + ... obs_time='2017-01-01 00:00') >>> eph = obj.ephemeris() # doctest: +SKIP >>> print(eph) # doctest: +SKIP table here... - ''' - + """ + URL = conf.pds_server - #URL = 'https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?' - + # URL = 'https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?' + # check inputs and set defaults for optional inputs if self.planet is None: raise ValueError("'planet' parameter not set. Query aborted.") + else: + self.planet = self.planet.lower() + if self.planet not in [ + "mars", + "jupiter", + "saturn", + "uranus", + "neptune", + "pluto", + ]: + raise ValueError( + "illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto'" + ) + if self.obs_time is None: - self.obs_time = Time.now().jd + self.obs_time = Time.now().strftime("%Y-%m-%d %H:%M") + warnings.warn("obs_time not set. using current time instead.") + else: + try: + Time.strptime(self.obs_time, "%Y-%m-%d %H:%M").jd + except: + raise ValueError( + "illegal value for 'obs_time' parameter. must have format 'yyyy-mm-dd hh:mm'" + ) + if observer_coords is None: - viewpoint = 'observatory' - latitude, longitude, altitude = '', '', '' + viewpoint = "observatory" + latitude, longitude, altitude = "", "", "" + print("Observatory coordinates not set. Using center of Earth.") else: - viewpoint = 'latlon' + viewpoint = "latlon" try: latitude, longitude, altitude = [float(j) for j in observer_coords] - assert -90. <= latitude <= 90. - assert -360. <= longitude <= 360. + assert -90.0 <= latitude <= 90.0 + assert -360.0 <= longitude <= 360.0 except: - raise ValueError(f"Illegal observatory coordinates {observer_coords}. must be of format [lat(deg), lon(deg east), alt(m)]") - if neptune_arcmodel not in [1,2,3]: - raise ValueError(f"Illegal Neptune arc model {neptune_arcmodel}. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details)") - - - ''' + raise ValueError( + f"Illegal observatory coordinates {observer_coords}. must be of format [lat(deg), lon(deg east), alt(m)]" + ) + if int(neptune_arcmodel) not in [1, 2, 3]: + raise ValueError( + f"Illegal Neptune arc model {neptune_arcmodel}. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details)" + ) + + """ https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?abbrev=nep&ephem=000+NEP081+%2B+NEP095+%2B+DE440&time=2020-01-01+00%3A00&fov=10&fov_unit=Neptune+radii¢er=body¢er_body=Neptune¢er_ansa=Adams+Ring¢er_ew=east¢er_ra=¢er_ra_type=hours¢er_dec=¢er_star=&observatory=Earth%27s+center&viewpoint=latlon&latitude=19.827&longitude=-155.472&lon_dir=east&altitude=4216&moons=814+All+inner+moons+%28N1-N8%2CN14%29&rings=Galle%2C+LeVerrier%2C+Arago%2C+Adams&arcmodel=%233+%28820.1121+deg%2Fday%29&extra_ra=&extra_ra_type=hours&extra_dec=&extra_name=&title=&labels=Small+%286+points%29&moonpts=0&blank=No&arcpts=4&meridians=Yes&output=HTML - ''' - + """ + # configure request_payload for ephemeris query # start with successful query and incrementally de-hardcode stuff # thankfully, adding extra planet-specific keywords here does not break query for other planets - request_payload = OrderedDict([ - ('abbrev', self.planet[:3]), - ('ephem', conf.planet_defaults[self.planet]['ephem']), # change hardcoding for other planets - ('time', self.obs_time), - ('fov', 10), #next few are figure options, can be hardcoded and ignored - ('fov_unit', self.planet.capitalize()+' radii'), - ('center', 'body'), - ('center_body', self.planet.capitalize()), - ('center_ansa', conf.planet_defaults[self.planet]['center_ansa']), - ('center_ew', 'east'), - ('center_ra', ''), - ('center_ra_type', 'hours'), - ('center_dec', ''), - ('center_star', ''), - ('viewpoint', viewpoint), - ('observatory', "Earth's center"), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories - ('latitude',latitude), - ('longitude',longitude), - ('lon_dir','east'), - ('altitude',altitude), - ('moons',conf.planet_defaults[self.planet]['moons']), - ('rings',conf.planet_defaults[self.planet]['rings']), - ('arcmodel',conf.neptune_arcmodels[neptune_arcmodel]), - ('extra_ra',''), # figure options below this line, can all be hardcoded and ignored - ('extra_ra_type','hours'), - ('extra_dec',''), - ('extra_name',''), - ('title',''), - ('labels','Small (6 points)'), - ('moonpts','0'), - ('blank', 'No'), - ('opacity', 'Transparent'), - ('peris', 'None'), - ('peripts', '4'), - ('arcpts', '4'), - ('meridians', 'Yes'), - ('output', 'html') - ]) - + request_payload = OrderedDict( + [ + ("abbrev", self.planet[:3]), + ( + "ephem", + conf.planet_defaults[self.planet]["ephem"], + ), # change hardcoding for other planets + ("time", self.obs_time), + ( + "fov", + 10, + ), # next few are figure options, can be hardcoded and ignored + ("fov_unit", self.planet.capitalize() + " radii"), + ("center", "body"), + ("center_body", self.planet.capitalize()), + ("center_ansa", conf.planet_defaults[self.planet]["center_ansa"]), + ("center_ew", "east"), + ("center_ra", ""), + ("center_ra_type", "hours"), + ("center_dec", ""), + ("center_star", ""), + ("viewpoint", viewpoint), + ( + "observatory", + "Earth's center", + ), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories + ("latitude", latitude), + ("longitude", longitude), + ("lon_dir", "east"), + ("altitude", altitude), + ("moons", conf.planet_defaults[self.planet]["moons"]), + ("rings", conf.planet_defaults[self.planet]["rings"]), + ("arcmodel", conf.neptune_arcmodels[int(neptune_arcmodel)]), + ( + "extra_ra", + "", + ), # figure options below this line, can all be hardcoded and ignored + ("extra_ra_type", "hours"), + ("extra_dec", ""), + ("extra_name", ""), + ("title", ""), + ("labels", "Small (6 points)"), + ("moonpts", "0"), + ("blank", "No"), + ("opacity", "Transparent"), + ("peris", "None"), + ("peripts", "4"), + ("arcpts", "4"), + ("meridians", "Yes"), + ("output", "html"), + ] + ) + # return request_payload if desired if get_query_payload: return request_payload @@ -175,27 +229,22 @@ def ephemeris_async(self, # set return_raw flag, if raw response desired if get_raw_response: self.return_raw = True - + # query and parse - response = self._request('GET', URL, params=request_payload, - timeout=self.TIMEOUT, cache=cache) + response = self._request( + "GET", URL, params=request_payload, timeout=self.TIMEOUT, cache=cache + ) self.uri = response.url - - ## questions - # where does the Horizons one actually call the parser? I don't get it... is parse_response somehow put inside self._request? need to look at base class. - # I guess this is supposed to return response as requests.Request object - # but then when is parse_response used? - # how does the Horizons one know to call ephemerides_async when it asks for ephemerides()? - + return response - + def _parse_ringnode(self, src): - ''' + """ Routine for parsing data from ring node Parameters ---------- - self : HorizonsClass instance + self : RingNodeClass instance src : list raw response from server @@ -203,126 +252,175 @@ def _parse_ringnode(self, src): Returns ------- data : `astropy.Table` - ''' - + """ + self.raw_response = src - soup = BeautifulSoup(src, 'html.parser') + soup = BeautifulSoup(src, "html.parser") text = soup.get_text() - #print(repr(text)) - textgroups = re.split('\n\n|\n \n', text) + # print(repr(text)) + textgroups = re.split("\n\n|\n \n", text) ringtable = None for group in textgroups: - group = group.strip(', \n') - - #input parameters. only thing needed is obs_time - if group.startswith('Observation'): - obs_time = group.split('\n')[0].split('e: ')[-1].strip(', \n') + group = group.strip(", \n") + + # input parameters. only thing needed is obs_time + if group.startswith("Observation"): + obs_time = group.split("\n")[0].split("e: ")[-1].strip(", \n") self.obs_time = obs_time - #minor body table part 1 - elif group.startswith('Body'): - group = 'NAIF '+group #fixing lack of header for NAIF ID - bodytable = ascii.read(group, format='fixed_width', - col_starts=(0, 4, 18, 35, 54, 68, 80, 91), - col_ends= (4, 18, 35, 54, 68, 80, 91, 102), - names = ('NAIF ID', 'Body', 'RA', 'Dec', 'RA (deg)', 'Dec (deg)', 'dRA', 'dDec') - ) - #minor body table part 2 - elif group.startswith('Sub-'): - group = '\n'.join(group.split('\n')[1:]) #fixing two-row header - group = 'NAIF'+group[4:] - bodytable2 = ascii.read(group, format='fixed_width', - col_starts=(0, 4, 18, 28, 37, 49, 57, 71), - col_ends= (4, 18, 28, 37, 49, 57, 71, 90), - names = ('NAIF ID', 'Body', 'sub_obs_lon', 'sub_obs_lat', 'sub_sun_lon', 'sub_sun_lat', 'phase', 'distance') - ) + # minor body table part 1 + elif group.startswith("Body"): + group = "NAIF " + group # fixing lack of header for NAIF ID + bodytable = ascii.read( + group, + format="fixed_width", + col_starts=(0, 4, 18, 35, 54, 68, 80, 91), + col_ends=(4, 18, 35, 54, 68, 80, 91, 102), + names=( + "NAIF ID", + "Body", + "RA", + "Dec", + "RA (deg)", + "Dec (deg)", + "dRA", + "dDec", + ), + ) + # minor body table part 2 + elif group.startswith("Sub-"): + group = "\n".join(group.split("\n")[1:]) # fixing two-row header + group = "NAIF" + group[4:] + bodytable2 = ascii.read( + group, + format="fixed_width", + col_starts=(0, 4, 18, 28, 37, 49, 57, 71), + col_ends=(4, 18, 28, 37, 49, 57, 71, 90), + names=( + "NAIF ID", + "Body", + "sub_obs_lon", + "sub_obs_lat", + "sub_sun_lon", + "sub_sun_lat", + "phase", + "distance", + ), + ) ## to do: add units!! - - #ring plane data - elif group.startswith('Ring s'): - lines = group.split('\n') + + # ring plane data + elif group.startswith("Ring s"): + lines = group.split("\n") for line in lines: - l = line.split(':') - if 'Ring sub-solar latitude' in l[0]: - [sub_sun_lat, sub_sun_lat_min, sub_sun_lat_max] = [float(s.strip(', \n()')) for s in re.split('\(|to', l[1])] - systemtable = table.Table([['sub_sun_lat','sub_sun_lat_max','sub_sun_lat_min'], - [sub_sun_lat, sub_sun_lat_max, sub_sun_lat_min]], - names = ('Parameter', 'Value')) - - elif 'Ring plane opening angle' in l[0]: - systemtable.add_row(['opening_angle', float(re.sub('[a-zA-Z]+', '', l[1]).strip(', \n()'))]) - elif 'Ring center phase angle' in l[0]: - systemtable.add_row(['phase_angle', float(l[1].strip(', \n'))]) - elif 'Sub-solar longitude' in l[0]: - systemtable.add_row(['sub_sun_lon', float(re.sub('[a-zA-Z]+', '', l[1]).strip(', \n()'))]) - elif 'Sub-observer longitude' in l[0]: - systemtable.add_row(['sub_obs_lon', float(l[1].strip(', \n'))]) + l = line.split(":") + if "Ring sub-solar latitude" in l[0]: + [sub_sun_lat, sub_sun_lat_min, sub_sun_lat_max] = [ + float(s.strip(", \n()")) for s in re.split("\(|to", l[1]) + ] + # systemtable = table.Table([[sub_sun_lat], [sub_sun_lat_max], [sub_sun_lat_min]], + # names = ('sub_sun_lat', 'sub_sun_lat_max', 'sub_sun_lat_min')) + systemtable = { + "sub_sun_lat": sub_sun_lat, + "sub_sun_lat_min": sub_sun_lat_min, + "sub_sun_lat_max": sub_sun_lat_min, + } + + elif "Ring plane opening angle" in l[0]: + systemtable["opening_angle"] = float( + re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()") + ) + elif "Ring center phase angle" in l[0]: + systemtable["phase_angle"] = float(l[1].strip(", \n")) + elif "Sub-solar longitude" in l[0]: + systemtable["sub_sun_lon"] = float( + re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()") + ) + elif "Sub-observer longitude" in l[0]: + systemtable["sub_obs_lon"] = float(l[1].strip(", \n")) else: pass ## to do: add units? - - #basic info about the planet - elif group.startswith('Sun-planet'): - lines = group.split('\n') + + # basic info about the planet + elif group.startswith("Sun-planet"): + lines = group.split("\n") for line in lines: - l = line.split(':') - if 'Sun-planet distance (AU)' in l[0]: - systemtable.add_row(['d_sun_AU', float(l[1].strip(', \n'))]) - elif 'Observer-planet distance (AU)' in l[0]: - systemtable.add_row(['d_obs_AU', float(l[1].strip(', \n'))]) - elif 'Sun-planet distance (km)' in l[0]: - systemtable.add_row(['d_sun_km', float(l[1].split('x')[0].strip(', \n'))*1e6]) - elif 'Observer-planet distance (km)' in l[0]: - systemtable.add_row(['d_obs_km', float(l[1].split('x')[0].strip(', \n'))*1e6]) - elif 'Light travel time' in l[0]: - systemtable.add_row(['light_time', float(l[1].strip(', \n'))]) + l = line.split(":") + if "Sun-planet distance (AU)" in l[0]: + systemtable["d_sun_AU"] = float(l[1].strip(", \n")) + elif "Observer-planet distance (AU)" in l[0]: + systemtable["d_obs_AU"] = float(l[1].strip(", \n")) + elif "Sun-planet distance (km)" in l[0]: + systemtable["d_sun_km"] = ( + float(l[1].split("x")[0].strip(", \n")) * 1e6 + ) + elif "Observer-planet distance (km)" in l[0]: + systemtable["d_obs_km"] = ( + float(l[1].split("x")[0].strip(", \n")) * 1e6 + ) + elif "Light travel time" in l[0]: + systemtable["light_time"] = float(l[1].strip(", \n")) else: pass - + ## to do: add units? - - # --------- below this line, planet-specific info ------------ - # Uranus individual rings data - elif group.startswith('Ring '): - ringtable = ascii.read(' '+group, format='fixed_width', - col_starts=(5, 18, 29), - col_ends= (18, 29, 36), - names = ('ring', 'pericenter', 'ascending node') - ) - + + # --------- below this line, planet-specific info ------------ + # Uranus individual rings data + elif group.startswith("Ring "): + ringtable = ascii.read( + " " + group, + format="fixed_width", + col_starts=(5, 18, 29), + col_ends=(18, 29, 36), + names=("ring", "pericenter", "ascending node"), + ) + ringtable.add_index("ring") + # Saturn F-ring data - elif group.startswith('F Ring'): - lines = group.split('\n') + elif group.startswith("F Ring"): + lines = group.split("\n") for line in lines: - l = line.split(':') - if 'F Ring pericenter' in l[0]: - peri = float(re.sub('[a-zA-Z]+', '', l[1]).strip(', \n()')) - elif 'F Ring ascending node' in l[0]: - ascn = float(l[1].strip(', \n')) - ringtable = table.Table([['F'], [peri], [ascn]], names=('ring', 'pericenter', 'ascending node')) - - # Neptune ring arcs data - elif group.startswith('Courage'): - lines = group.split('\n') + l = line.split(":") + if "F Ring pericenter" in l[0]: + peri = float(re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()")) + elif "F Ring ascending node" in l[0]: + ascn = float(l[1].strip(", \n")) + ringtable = table.Table( + [["F"], [peri], [ascn]], + names=("ring", "pericenter", "ascending node"), + ) + + # Neptune ring arcs data + elif group.startswith("Courage"): + lines = group.split("\n") for i in range(len(lines)): - l = lines[i].split(':') - ring = l[0].split('longitude')[0].strip(', \n') - [min_angle, max_angle] = [float(s.strip(', \n')) for s in re.sub('[a-zA-Z]+', '', l[1]).strip(', \n()').split()] + l = lines[i].split(":") + ring = l[0].split("longitude")[0].strip(", \n") + [min_angle, max_angle] = [ + float(s.strip(", \n")) + for s in re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()").split() + ] if i == 0: - ringtable = table.Table([[ring], [min_angle], [max_angle]], names=('ring', 'min_angle', 'max_angle')) + ringtable = table.Table( + [[ring], [min_angle], [max_angle]], + names=("ring", "min_angle", "max_angle"), + ) else: ringtable.add_row([ring, min_angle, max_angle]) - + else: pass - - # concatenate minor body table + + # concatenate minor body table bodytable = table.join(bodytable, bodytable2) - + bodytable.add_index("Body") + return systemtable, bodytable, ringtable - - def _parse_result(self, response, verbose = None): - ''' + + def _parse_result(self, response, verbose=None): + """ Routine for managing parser calls note this MUST be named exactly _parse_result so it interacts with async_to_sync properly @@ -335,7 +433,7 @@ def _parse_result(self, response, verbose = None): Returns ------- data : `astropy.Table` - ''' + """ self.last_response = response try: systemtable, bodytable, ringtable = self._parse_ringnode(response.text) @@ -347,34 +445,11 @@ def _parse_result(self, response, verbose = None): # won't be needed pass raise - return systemtable, bodytable, ringtable #astropy table, astropy table, astropy table - -RingNode = RingNodeClass() + return ( + systemtable, + bodytable, + ringtable, + ) # astropy table, astropy table, astropy table -if __name__ == "__main__": - - # single basic query - neptune = RingNodeClass('Neptune', '2019-10-28 00:00') - systemtable, bodytable, ringtable = neptune.ephemeris(neptune_arcmodel = 2, observer_coords = (10.0, -120.355, 1000)) - print(systemtable) - print(bodytable) - print(ringtable) - - - ''' - # basic query for all six targets - for major_body in ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto']: - nodequery = RingNode(major_body, '2022-05-03 00:00') - systemtable, bodytable, ringtable = nodequery.ephemeris() - - print(' ') - print(' ') - print('~'*40) - print(major_body) - print('~'*40) - print(systemtable) - print(bodytable) - print(ringtable) - ''' - - \ No newline at end of file + +RingNode = RingNodeClass() diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py new file mode 100644 index 0000000000..09bbc5d5e7 --- /dev/null +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -0,0 +1,110 @@ +import pytest +from collections import OrderedDict +from astropy.tests.helper import assert_quantity_allclose + +from ... import pds + + +# Horizons has machinery here to mock request from a text file +# is that necessary? why is that done? +# wouldn't we want to know if the website we are querying changes something that makes code fail? + + +# --------------------------------- actual test functions + + +@pytest.mark.remote_data +class TestRingNodeClass: + def test_ephemeris_query(): + + systemtable, bodytable, ringtable = pds.RingNode( + planet="Uranus", obs_time="2022-05-03 00:00" + ).ephemeris(observer_coords=(10.0, -120.355, 1000)) + + # check system table + systemclose = assert_quantity_allclose( + [ + -56.12233, + -56.13586, + -56.13586, + -56.01577, + 0.10924, + 354.11072, + 354.12204, + 19.70547, + 20.71265, + 2947896667.0, + 3098568884.0, + 10335.713263, + ], + [ + systemtable["sub_sun_lat"], + systemtable["sub_sun_lat_min"], + systemtable["sub_sun_lat_max"], + systemtable["opening_angle"], + systemtable["phase_angle"], + systemtable["sub_sun_lon"], + systemtable["sub_obs_lon"], + systemtable["d_sun_AU"], + systemtable["d_obs_AU"], + systemtable["d_sun_km"], + systemtable["d_obs_km"], + systemtable["light_time"], + ], + rtol=1e-3, + ) + + # check a moon in body table + mab = bodytable[bodytable.loc_indices["Mab"]] + assert mab["NAIF ID"] == 726 + assert mab["Body"] == "Mab" + mabclose = assert_quantity_allclose( + [ + 42.011201, + 15.801323, + 5.368, + 0.623, + 223.976, + 55.906, + 223.969, + 56.013, + 0.10932, + 3098.514, + ], + [ + mab["RA (deg)"], + mab["Dec (deg)"], + mab["dRA"], + mab["dDec"], + mab["sub_obs_lon"], + mab["sub_obs_lat"], + mab["sub_sun_lon"], + mab["sub_sun_lat"], + mab["phase"], + mab["distance"], + ], + rtol=1e-3, + ) + + # check a ring in ringtable + beta = ringtable[ringtable.loc_indices["Beta"]] + assert np.isclose(beta["pericenter"], 231.051, rtol=1e-3) + assert np.isclose(beta["ascending node"], 353.6, rtol=1e-2) + + def test_bad_query_exception_throw(): + + with pytest.raises(ValueError): + pds.RingNode(planet="Mercury", obs_time="2022-05-03 00:00").ephemeris() + + with pytest.raises(ValueError): + pds.RingNode(planet="Uranus", obs_time="2022-13-03 00:00").ephemeris() + + with pytest.raises(ValueError): + pds.RingNode(planet="Neptune", obs_time="2022-05-03 00:00").ephemeris( + observer_coords=(1000, 10.0, -120.355) + ) + + with pytest.raises(ValueError): + pds.RingNode(planet="Neptune", obs_time="2022-05-03 00:00").ephemeris( + observer_coords=(10.0, -120.355, 1000), neptune_arcmodel=0 + ) From b7d275104bff387bdb5e130a04e37247911ecd4d Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Mon, 11 Apr 2022 21:23:45 -0700 Subject: [PATCH 05/25] attempted to fix all pep8 problems --- astroquery/solarsystem/pds/__init__.py | 2 +- astroquery/solarsystem/pds/core.py | 46 ++++++++----------- .../solarsystem/pds/tests/test_ringnode.py | 6 +-- 3 files changed, 23 insertions(+), 31 deletions(-) diff --git a/astroquery/solarsystem/pds/__init__.py b/astroquery/solarsystem/pds/__init__.py index 94eb705999..7e46473cf9 100644 --- a/astroquery/solarsystem/pds/__init__.py +++ b/astroquery/solarsystem/pds/__init__.py @@ -15,7 +15,7 @@ class Conf(_config.ConfigNamespace): # server settings pds_server = _config.ConfigItem( - ["https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?",], "Ring Node" + ["https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?"], "Ring Node" ) # implement later: other pds tools diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 85c9acda2b..472ee9a5e6 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -1,5 +1,5 @@ # 1. standard library imports -import numpy as np + from collections import OrderedDict import re import warnings @@ -25,13 +25,13 @@ class RingNodeClass(BaseQuery): """ for querying the Planetary Ring Node ephemeris tools - - + + # basic query for all six targets for major_body in ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto']: nodequery = RingNode(major_body, '2022-05-03 00:00') systemtable, bodytable, ringtable = nodequery.ephemeris() - + print(' ') print(' ') print('~'*40) @@ -40,15 +40,15 @@ class RingNodeClass(BaseQuery): print(systemtable) print(bodytable) print(ringtable) - - + + """ TIMEOUT = conf.timeout def __init__(self, planet=None, obs_time=None): """Instantiate Planetary Ring Node query - + Parameters ---------- planet : str, required. one of Jupiter, Saturn, Uranus, or Neptune @@ -64,7 +64,7 @@ def __init__(self, planet=None, obs_time=None): def __str__(self): """ String representation of RingNodeClass object instance' - + Examples -------- >>> from astroquery.solarsystem.pds import RingNode @@ -89,19 +89,19 @@ def ephemeris_async( ): """ send query to server - + note this interacts with utils.async_to_sync to be called as ephemeris() - + Parameters ---------- - self : + self : RingNodeClass instance observer_coords : three-element list/array/tuple of format (lat (deg), lon (deg east), altitude (m)) - + Returns ------- response : `requests.Response` The response of the HTTP request. - + Examples -------- >>> from astroquery.solarsystem.pds import RingNode @@ -138,7 +138,7 @@ def ephemeris_async( else: try: Time.strptime(self.obs_time, "%Y-%m-%d %H:%M").jd - except: + except Exception as e: raise ValueError( "illegal value for 'obs_time' parameter. must have format 'yyyy-mm-dd hh:mm'" ) @@ -153,7 +153,7 @@ def ephemeris_async( latitude, longitude, altitude = [float(j) for j in observer_coords] assert -90.0 <= latitude <= 90.0 assert -360.0 <= longitude <= 360.0 - except: + except Exception as e: raise ValueError( f"Illegal observatory coordinates {observer_coords}. must be of format [lat(deg), lon(deg east), alt(m)]" ) @@ -162,10 +162,6 @@ def ephemeris_async( f"Illegal Neptune arc model {neptune_arcmodel}. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details)" ) - """ - https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?abbrev=nep&ephem=000+NEP081+%2B+NEP095+%2B+DE440&time=2020-01-01+00%3A00&fov=10&fov_unit=Neptune+radii¢er=body¢er_body=Neptune¢er_ansa=Adams+Ring¢er_ew=east¢er_ra=¢er_ra_type=hours¢er_dec=¢er_star=&observatory=Earth%27s+center&viewpoint=latlon&latitude=19.827&longitude=-155.472&lon_dir=east&altitude=4216&moons=814+All+inner+moons+%28N1-N8%2CN14%29&rings=Galle%2C+LeVerrier%2C+Arago%2C+Adams&arcmodel=%233+%28820.1121+deg%2Fday%29&extra_ra=&extra_ra_type=hours&extra_dec=&extra_name=&title=&labels=Small+%286+points%29&moonpts=0&blank=No&arcpts=4&meridians=Yes&output=HTML - """ - # configure request_payload for ephemeris query # start with successful query and incrementally de-hardcode stuff # thankfully, adding extra planet-specific keywords here does not break query for other planets @@ -241,7 +237,7 @@ def ephemeris_async( def _parse_ringnode(self, src): """ Routine for parsing data from ring node - + Parameters ---------- self : RingNodeClass instance @@ -307,7 +303,6 @@ def _parse_ringnode(self, src): "distance", ), ) - ## to do: add units!! # ring plane data elif group.startswith("Ring s"): @@ -340,7 +335,6 @@ def _parse_ringnode(self, src): systemtable["sub_obs_lon"] = float(l[1].strip(", \n")) else: pass - ## to do: add units? # basic info about the planet elif group.startswith("Sun-planet"): @@ -364,8 +358,6 @@ def _parse_ringnode(self, src): else: pass - ## to do: add units? - # --------- below this line, planet-specific info ------------ # Uranus individual rings data elif group.startswith("Ring "): @@ -423,13 +415,13 @@ def _parse_result(self, response, verbose=None): """ Routine for managing parser calls note this MUST be named exactly _parse_result so it interacts with async_to_sync properly - + Parameters ---------- self : RingNodeClass instance response : string raw response from server - + Returns ------- data : `astropy.Table` @@ -437,7 +429,7 @@ def _parse_result(self, response, verbose=None): self.last_response = response try: systemtable, bodytable, ringtable = self._parse_ringnode(response.text) - except: + except Exception as e: try: self._last_query.remove_cache_file(self.cache_location) except OSError: diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index 09bbc5d5e7..d187b5673c 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -1,6 +1,6 @@ import pytest -from collections import OrderedDict from astropy.tests.helper import assert_quantity_allclose +import numpy as np from ... import pds @@ -22,7 +22,7 @@ def test_ephemeris_query(): ).ephemeris(observer_coords=(10.0, -120.355, 1000)) # check system table - systemclose = assert_quantity_allclose( + assert_quantity_allclose( [ -56.12233, -56.13586, @@ -58,7 +58,7 @@ def test_ephemeris_query(): mab = bodytable[bodytable.loc_indices["Mab"]] assert mab["NAIF ID"] == 726 assert mab["Body"] == "Mab" - mabclose = assert_quantity_allclose( + assert_quantity_allclose( [ 42.011201, 15.801323, From d4b1f020910a154938d8909e2ac4dad0da89137d Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Fri, 6 May 2022 18:49:05 -0700 Subject: [PATCH 06/25] added units, some fixes suggested by mkelley --- astroquery/solarsystem/pds/core.py | 217 +- .../solarsystem/pds/tests/test_ringnode.py | 75 +- coverage.xml | 18155 ++++++++++++++++ docs/solarsystem/pds/pds.rst | 494 + 4 files changed, 18817 insertions(+), 124 deletions(-) create mode 100644 coverage.xml create mode 100644 docs/solarsystem/pds/pds.rst diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 472ee9a5e6..57f0d6c299 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -8,6 +8,8 @@ from astropy.time import Time from astropy import table from astropy.io import ascii +import astropy.units as u +from astropy.coordinates import EarthLocation, Angle from bs4 import BeautifulSoup # 3. local imports - use relative imports @@ -26,22 +28,6 @@ class RingNodeClass(BaseQuery): for querying the Planetary Ring Node ephemeris tools - - # basic query for all six targets - for major_body in ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto']: - nodequery = RingNode(major_body, '2022-05-03 00:00') - systemtable, bodytable, ringtable = nodequery.ephemeris() - - print(' ') - print(' ') - print('~'*40) - print(major_body) - print('~'*40) - print(systemtable) - print(bodytable) - print(ringtable) - - """ TIMEOUT = conf.timeout @@ -51,37 +37,31 @@ def __init__(self, planet=None, obs_time=None): Parameters ---------- - planet : str, required. one of Jupiter, Saturn, Uranus, or Neptune - Name, number, or designation of the object to be queried. - obs_time : str, in JD or MJD format. If no obs_time is provided, the current - time is used. + """ super().__init__() - self.planet = planet - self.obs_time = obs_time def __str__(self): """ - String representation of RingNodeClass object instance' + String representation of RingNodeClass object instance Examples -------- >>> from astroquery.solarsystem.pds import RingNode - >>> uranus = RingNode(planet='Uranus', - ... obs_time='2017-01-01 00:00') - >>> print(uranus) # doctest: +SKIP - PDSRingNode instance "Uranus"; obs_time='2017-01-01 00:00' + >>> nodeobj = RingNode() + >>> print(nodeobj) # doctest: +SKIP + PDSRingNode instance """ - return ('PDSRingNode instance "{:s}"; obs_time={:s}').format( - str(self.planet), str(self.obs_time) - ) + return 'PDSRingNode instance' # --- pretty stuff above this line, get it working below this line --- def ephemeris_async( self, - observer_coords=None, + planet, + obs_time=None, + location=None, neptune_arcmodel=3, get_query_payload=False, get_raw_response=False, @@ -95,7 +75,22 @@ def ephemeris_async( Parameters ---------- self : RingNodeClass instance - observer_coords : three-element list/array/tuple of format (lat (deg), lon (deg east), altitude (m)) + planet : str, required. one of Mars, Jupiter, Saturn, Uranus, Neptune, or Pluto + obs_time : astropy.Time object, or str in format YYYY-MM-DD hh:mm, optional. + If str is provided then UTC is assumed. If no obs_time is provided, + the current time is used. + location : array-like, or `~astropy.coordinates.EarthLocation`, optional + Observer's location as a + 3-element array of Earth longitude, latitude, altitude, or + a `~astropy.coordinates.EarthLocation`. Longitude and + latitude should be anything that initializes an + `~astropy.coordinates.Angle` object, and altitude should + initialize an `~astropy.units.Quantity` object (with units + of length). If ``None``, then the geocenter (code 500) is + used. + neptune_arcmodel : float, optional. which ephemeris to assume for Neptune's ring arcs + must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details) + has no effect if planet != 'Neptune' Returns ------- @@ -105,22 +100,24 @@ def ephemeris_async( Examples -------- >>> from astroquery.solarsystem.pds import RingNode - >>> uranus = RingNode(planet='Uranus', - ... obs_time='2017-01-01 00:00') - >>> eph = obj.ephemeris() # doctest: +SKIP + >>> nodeobj = RingNode() + >>> eph = obj.ephemeris(planet='Uranus', + ... obs_time='2017-01-01 00:00') # doctest: +SKIP >>> print(eph) # doctest: +SKIP table here... """ + planet = planet + obs_time = obs_time URL = conf.pds_server # URL = 'https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?' # check inputs and set defaults for optional inputs - if self.planet is None: + if planet is None: raise ValueError("'planet' parameter not set. Query aborted.") else: - self.planet = self.planet.lower() - if self.planet not in [ + planet = planet.lower() + if planet not in [ "mars", "jupiter", "saturn", @@ -132,31 +129,50 @@ def ephemeris_async( "illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto'" ) - if self.obs_time is None: - self.obs_time = Time.now().strftime("%Y-%m-%d %H:%M") + if obs_time is None: + obs_time = Time.now().strftime("%Y-%m-%d %H:%M") warnings.warn("obs_time not set. using current time instead.") - else: + elif type(obs_time) == str: try: - Time.strptime(self.obs_time, "%Y-%m-%d %H:%M").jd + Time.strptime(obs_time, "%Y-%m-%d %H:%M").jd except Exception as e: raise ValueError( - "illegal value for 'obs_time' parameter. must have format 'yyyy-mm-dd hh:mm'" + "illegal value for 'obs_time' parameter. string must have format 'yyyy-mm-dd hh:mm'" + ) + elif type(obs_time) == Time: + try: + obs_time = obs_time.utc.to_value('iso', subfmt = 'date_hm') + except Exception as e: + raise ValueError( + "illegal value for 'obs_time' parameter. could not parse astropy.time.core.Time object into format 'yyyy-mm-dd hh:mm' (UTC)" ) - if observer_coords is None: + if location is None: viewpoint = "observatory" latitude, longitude, altitude = "", "", "" print("Observatory coordinates not set. Using center of Earth.") else: viewpoint = "latlon" - try: - latitude, longitude, altitude = [float(j) for j in observer_coords] - assert -90.0 <= latitude <= 90.0 - assert -360.0 <= longitude <= 360.0 - except Exception as e: - raise ValueError( - f"Illegal observatory coordinates {observer_coords}. must be of format [lat(deg), lon(deg east), alt(m)]" - ) + if type(location) != EarthLocation: + if hasattr(location, '__iter__'): + if len(location) != 3: + raise ValueError( + "location arrays require three values:" + " longitude, latitude, and altitude") + else: + raise TypeError( + "location must be array-like or astropy EarthLocation") + + if isinstance(location, EarthLocation): + loc = location.geodetic + longitude = loc[0].deg + latitude = loc[1].deg + altitude = loc[2].to(u.m).value + elif hasattr(location, '__iter__'): + longitude = Angle(location[0]).deg + latitude = Angle(location[1]).deg + altitude = u.Quantity(location[2]).to('m').value + if int(neptune_arcmodel) not in [1, 2, 3]: raise ValueError( f"Illegal Neptune arc model {neptune_arcmodel}. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details)" @@ -167,20 +183,20 @@ def ephemeris_async( # thankfully, adding extra planet-specific keywords here does not break query for other planets request_payload = OrderedDict( [ - ("abbrev", self.planet[:3]), + ("abbrev", planet[:3]), ( "ephem", - conf.planet_defaults[self.planet]["ephem"], + conf.planet_defaults[planet]["ephem"], ), # change hardcoding for other planets - ("time", self.obs_time), + ("time", obs_time), #UTC. this should be enforced when checking inputs ( "fov", 10, ), # next few are figure options, can be hardcoded and ignored - ("fov_unit", self.planet.capitalize() + " radii"), + ("fov_unit", planet.capitalize() + " radii"), ("center", "body"), - ("center_body", self.planet.capitalize()), - ("center_ansa", conf.planet_defaults[self.planet]["center_ansa"]), + ("center_body", planet.capitalize()), + ("center_ansa", conf.planet_defaults[planet]["center_ansa"]), ("center_ew", "east"), ("center_ra", ""), ("center_ra_type", "hours"), @@ -195,8 +211,8 @@ def ephemeris_async( ("longitude", longitude), ("lon_dir", "east"), ("altitude", altitude), - ("moons", conf.planet_defaults[self.planet]["moons"]), - ("rings", conf.planet_defaults[self.planet]["rings"]), + ("moons", conf.planet_defaults[planet]["moons"]), + ("rings", conf.planet_defaults[planet]["rings"]), ("arcmodel", conf.neptune_arcmodels[int(neptune_arcmodel)]), ( "extra_ra", @@ -262,7 +278,6 @@ def _parse_ringnode(self, src): # input parameters. only thing needed is obs_time if group.startswith("Observation"): obs_time = group.split("\n")[0].split("e: ")[-1].strip(", \n") - self.obs_time = obs_time # minor body table part 1 elif group.startswith("Body"): @@ -282,7 +297,18 @@ def _parse_ringnode(self, src): "dRA", "dDec", ), - ) + ) + units_list = [None, + None, + None, + None, + u.deg, + u.deg, + u.arcsec, + u.arcsec] + bodytable = table.QTable(bodytable, units = units_list) + #for i in range(len(bodytable.colnames)): + # bodytable[bodytable.colnames[i]].unit = units_list[i] # minor body table part 2 elif group.startswith("Sub-"): group = "\n".join(group.split("\n")[1:]) # fixing two-row header @@ -301,8 +327,19 @@ def _parse_ringnode(self, src): "sub_sun_lat", "phase", "distance", - ), - ) + )) + units_list=[ + None, + None, + u.deg, + u.deg, + u.deg, + u.deg, + u.deg, + u.km * 1e6] + bodytable2 = table.QTable(bodytable2, units = units_list) + #for i in range(len(bodytable2.colnames)): + # bodytable2[bodytable2.colnames[i]].unit = units_list[i] # ring plane data elif group.startswith("Ring s"): @@ -316,23 +353,23 @@ def _parse_ringnode(self, src): # systemtable = table.Table([[sub_sun_lat], [sub_sun_lat_max], [sub_sun_lat_min]], # names = ('sub_sun_lat', 'sub_sun_lat_max', 'sub_sun_lat_min')) systemtable = { - "sub_sun_lat": sub_sun_lat, - "sub_sun_lat_min": sub_sun_lat_min, - "sub_sun_lat_max": sub_sun_lat_min, + "sub_sun_lat": sub_sun_lat * u.deg, + "sub_sun_lat_min": sub_sun_lat_min * u.deg, + "sub_sun_lat_max": sub_sun_lat_min * u.deg, } elif "Ring plane opening angle" in l[0]: systemtable["opening_angle"] = float( re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()") - ) + ) * u.deg elif "Ring center phase angle" in l[0]: - systemtable["phase_angle"] = float(l[1].strip(", \n")) + systemtable["phase_angle"] = float(l[1].strip(", \n")) * u.deg elif "Sub-solar longitude" in l[0]: systemtable["sub_sun_lon"] = float( re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()") - ) + ) * u.deg elif "Sub-observer longitude" in l[0]: - systemtable["sub_obs_lon"] = float(l[1].strip(", \n")) + systemtable["sub_obs_lon"] = float(l[1].strip(", \n")) * u.deg else: pass @@ -342,19 +379,21 @@ def _parse_ringnode(self, src): for line in lines: l = line.split(":") if "Sun-planet distance (AU)" in l[0]: - systemtable["d_sun_AU"] = float(l[1].strip(", \n")) + #systemtable["d_sun_AU"] = float(l[1].strip(", \n")) + pass elif "Observer-planet distance (AU)" in l[0]: - systemtable["d_obs_AU"] = float(l[1].strip(", \n")) + #systemtable["d_obs_AU"] = float(l[1].strip(", \n")) + pass elif "Sun-planet distance (km)" in l[0]: - systemtable["d_sun_km"] = ( - float(l[1].split("x")[0].strip(", \n")) * 1e6 + systemtable["d_sun"] = ( + float(l[1].split("x")[0].strip(", \n")) * 1e6 * u.km ) elif "Observer-planet distance (km)" in l[0]: - systemtable["d_obs_km"] = ( - float(l[1].split("x")[0].strip(", \n")) * 1e6 + systemtable["d_obs"] = ( + float(l[1].split("x")[0].strip(", \n")) * 1e6 * u.km ) elif "Light travel time" in l[0]: - systemtable["light_time"] = float(l[1].strip(", \n")) + systemtable["light_time"] = float(l[1].strip(", \n")) * u.second else: pass @@ -366,10 +405,11 @@ def _parse_ringnode(self, src): format="fixed_width", col_starts=(5, 18, 29), col_ends=(18, 29, 36), - names=("ring", "pericenter", "ascending node"), - ) - ringtable.add_index("ring") - + names=("ring", "pericenter", "ascending node")) + + units_list=[None, u.deg, u.deg] + ringtable = table.QTable(ringtable, units = units_list) + # Saturn F-ring data elif group.startswith("F Ring"): lines = group.split("\n") @@ -382,6 +422,7 @@ def _parse_ringnode(self, src): ringtable = table.Table( [["F"], [peri], [ascn]], names=("ring", "pericenter", "ascending node"), + units=(None, u.deg, u.deg) ) # Neptune ring arcs data @@ -398,6 +439,7 @@ def _parse_ringnode(self, src): ringtable = table.Table( [[ring], [min_angle], [max_angle]], names=("ring", "min_angle", "max_angle"), + units=(None, u.deg, u.deg) ) else: ringtable.add_row([ring, min_angle, max_angle]) @@ -405,9 +447,16 @@ def _parse_ringnode(self, src): else: pass - # concatenate minor body table - bodytable = table.join(bodytable, bodytable2) + # + ## do some cleanup from the parsing job + # + ringtable.add_index("ring") + + bodytable = table.join(bodytable, bodytable2) # concatenate minor body table bodytable.add_index("Body") + + systemtable["obs_time"] = Time(obs_time, format = 'iso', scale = 'utc') # add obs time to systemtable + return systemtable, bodytable, ringtable diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index d187b5673c..a659530062 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -1,6 +1,7 @@ import pytest from astropy.tests.helper import assert_quantity_allclose import numpy as np +import astropy.units as u from ... import pds @@ -15,12 +16,10 @@ @pytest.mark.remote_data class TestRingNodeClass: - def test_ephemeris_query(): - - systemtable, bodytable, ringtable = pds.RingNode( - planet="Uranus", obs_time="2022-05-03 00:00" - ).ephemeris(observer_coords=(10.0, -120.355, 1000)) + def test_ephemeris_query(self): + systemtable, bodytable, ringtable = pds.RingNode().ephemeris( + planet="Uranus", obs_time="2022-05-03 00:00",location=(10.0*u.deg, -120.355*u.deg, 1000*u.m)) # check system table assert_quantity_allclose( [ @@ -31,27 +30,23 @@ def test_ephemeris_query(): 0.10924, 354.11072, 354.12204, - 19.70547, - 20.71265, 2947896667.0, 3098568884.0, 10335.713263, ], [ - systemtable["sub_sun_lat"], - systemtable["sub_sun_lat_min"], - systemtable["sub_sun_lat_max"], - systemtable["opening_angle"], - systemtable["phase_angle"], - systemtable["sub_sun_lon"], - systemtable["sub_obs_lon"], - systemtable["d_sun_AU"], - systemtable["d_obs_AU"], - systemtable["d_sun_km"], - systemtable["d_obs_km"], - systemtable["light_time"], + systemtable["sub_sun_lat"].to(u.deg).value, + systemtable["sub_sun_lat_min"].to(u.deg).value, + systemtable["sub_sun_lat_max"].to(u.deg).value, + systemtable["opening_angle"].to(u.deg).value, + systemtable["phase_angle"].to(u.deg).value, + systemtable["sub_sun_lon"].to(u.deg).value, + systemtable["sub_obs_lon"].to(u.deg).value, + systemtable["d_sun"].to(u.km).value, + systemtable["d_obs"].to(u.km).value, + systemtable["light_time"].to(u.second).value, ], - rtol=1e-3, + rtol=1e-2, ) # check a moon in body table @@ -72,39 +67,39 @@ def test_ephemeris_query(): 3098.514, ], [ - mab["RA (deg)"], - mab["Dec (deg)"], - mab["dRA"], - mab["dDec"], - mab["sub_obs_lon"], - mab["sub_obs_lat"], - mab["sub_sun_lon"], - mab["sub_sun_lat"], - mab["phase"], - mab["distance"], + mab["RA (deg)"].to(u.deg).value, + mab["Dec (deg)"].to(u.deg).value, + mab["dRA"].to(u.arcsec).value, + mab["dDec"].to(u.arcsec).value, + mab["sub_obs_lon"].to(u.deg).value, + mab["sub_obs_lat"].to(u.deg).value, + mab["sub_sun_lon"].to(u.deg).value, + mab["sub_sun_lat"].to(u.deg).value, + mab["phase"].to(u.deg).value, + mab["distance"].to(u.km * 1e6).value, ], - rtol=1e-3, + rtol=1e-2, ) # check a ring in ringtable beta = ringtable[ringtable.loc_indices["Beta"]] - assert np.isclose(beta["pericenter"], 231.051, rtol=1e-3) - assert np.isclose(beta["ascending node"], 353.6, rtol=1e-2) + assert np.isclose(beta["pericenter"].to(u.deg).value, 231.051, rtol=1e-3) + assert np.isclose(beta["ascending node"].to(u.deg).value, 353.6, rtol=1e-2) - def test_bad_query_exception_throw(): + def test_bad_query_exception_throw(self): with pytest.raises(ValueError): - pds.RingNode(planet="Mercury", obs_time="2022-05-03 00:00").ephemeris() + pds.RingNode().ephemeris(planet="Mercury", obs_time="2022-05-03 00:00") with pytest.raises(ValueError): - pds.RingNode(planet="Uranus", obs_time="2022-13-03 00:00").ephemeris() + pds.RingNode().ephemeris(planet="Uranus", obs_time="2022-13-03 00:00") with pytest.raises(ValueError): - pds.RingNode(planet="Neptune", obs_time="2022-05-03 00:00").ephemeris( - observer_coords=(1000, 10.0, -120.355) + pds.RingNode().ephemeris( + planet="Neptune", obs_time="2022-05-03 00:00", location=(10.0*u.deg, -120.355*u.deg) ) with pytest.raises(ValueError): - pds.RingNode(planet="Neptune", obs_time="2022-05-03 00:00").ephemeris( - observer_coords=(10.0, -120.355, 1000), neptune_arcmodel=0 + pds.RingNode().ephemeris( + planet="Neptune", obs_time="2022-05-03 00:00", location=(10.0*u.deg, -120.355*u.deg, 1000*u.m), neptune_arcmodel=0 ) diff --git a/coverage.xml b/coverage.xml new file mode 100644 index 0000000000..e8dd5f4f48 --- /dev/null +++ b/coverage.xml @@ -0,0 +1,18155 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst new file mode 100644 index 0000000000..6c784cac46 --- /dev/null +++ b/docs/solarsystem/pds/pds.rst @@ -0,0 +1,494 @@ +.. doctest-skip-all + +.. _astroquery.solarsystem.pds: + +*********************************************************************************** +PDS Planetary Ring Node Queries (`astroquery.solarsystem.pds`) +*********************************************************************************** + +Overview +======== + + +The :class:`~astroquery.solarsystem.pds.RingNodeClass` class provides an +interface to services provided by the `Planetary Data System's Ring Node System hosted by SETI institute`_. + +In order to query information for a specific Solar System body, a +``RingNode`` object has to be instantiated: + +.. code-block:: python + + >>> from astroquery.solarsystem.pds import RingNode + >>> obj = RingNode(planet='Uranus', obs_time='2022-05-03 11:55') + >>> print(obj) + PDSRingNode instance "Uranus"; obs_time=2022-05-03 11:55 + +``planet`` must be one of ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto'] (case-insensitive) + +``obs_time`` is the UTC datetime to query in format 'YYYY-MM-DD HH:MM'. If not set, will assume the current time. Unlike the Horizons tool, the Planetary Ring Node unfortunately does not support querying multiple epochs at once. + + + +Ephemeris +----------- + +:meth:`~astroquery.solarsystem.pds.RingNodeClass.ephemeris` returns ephemeris information +for rings and small bodies around the given planet at a given observer location (``observer_coords``) and epoch +(``obs_time``) in the form of astropy tables. The following example queries the +ephemerides of the rings and small moons around Uranus as viewed from ALMA: + +.. code-block:: python + + >>> from astroquery.solarsystem.pds import RingNode + >>> obj = RingNode(planet='Uranus', obs_time='2022-05-03 11:55') + >>> systemtable, bodytable, ringtable = obj.ephemeris(observer_coords=(-23.029, -67.755, 5000)) + >>> print(eph) ## edited to here + targetname datetime_str datetime_jd ... GlxLat RA_3sigma DEC_3sigma + --- --- d ... deg arcsec arcsec + ---------- ----------------- ----------- ... --------- --------- ---------- + 1 Ceres 2010-Jan-01 00:00 2455197.5 ... 24.120057 0.0 0.0 + 1 Ceres 2010-Jan-11 00:00 2455207.5 ... 20.621496 0.0 0.0 + 1 Ceres 2010-Jan-21 00:00 2455217.5 ... 17.229529 0.0 0.0 + 1 Ceres 2010-Jan-31 00:00 2455227.5 ... 13.97264 0.0 0.0 + 1 Ceres 2010-Feb-10 00:00 2455237.5 ... 10.877201 0.0 0.0 + 1 Ceres 2010-Feb-20 00:00 2455247.5 ... 7.976737 0.0 0.0 + + +The following fields are available for each ephemerides query: + +.. code-block:: python + + >>> print(eph.columns) + + +The values in these columns are the same as those defined in the Horizons +`Definition of Observer Table Quantities`_; names have been simplified in a few +cases. Quantities ``H`` and ``G`` are the target's Solar System absolute +magnitude and photometric phase curve slope, respectively. In the case of +comets, ``H`` and ``G`` are replaced by ``M1``, ``M2``, ``k1``, ``k2``, and +``phasecoeff``; please refer to the `Horizons documentation`_ for definitions. + +Optional parameters of :meth:`~astroquery.jplhorizons.HorizonsClass.ephemerides` +correspond to optional features of the Horizons system: ``airmass_lessthan`` +sets an upper limit to airmass, ``solar_elongation`` enables the definition of a +solar elongation range, ``max_hour_angle`` sets a cutoff of the hour angle, +``skip_daylight=True`` rejects epochs during daylight, ``rate_cutoff`` rejects +targets with sky motion rates higher than provided (in units of arcsec/h), +``refraction`` accounts for refraction in the computation of the ephemerides +(disabled by default), and ``refsystem`` defines the coordinate reference system +used (ICRF by default). For comets, the options ``closest_apparition`` and +``no_fragments`` are available, which selects the closest apparition in time and +limits fragment matching (73P-B would only match 73P-B), respectively. Note +that these options should only be used for comets and will crash the query for +other object types. Extra precision in the queried properties can be requested +using the ``extra_precision`` option. Furthermore, ``get_query_payload=True`` +skips the query and only returns the query payload, whereas +``get_raw_response=True`` returns the raw query response instead of the astropy +table. + +:meth:`~astroquery.jplhorizons.HorizonsClass.ephemerides` queries by default all +available quantities from the JPL Horizons servers. This might take a while. If +you are only interested in a subset of the available quantities, you can query +only those. The corresponding optional parameter to be set is ``quantities``. +This parameter uses the same numerical codes as JPL Horizons defined in the `JPL +Horizons User Manual Definition of Observer Table Quantities +`_. For instance, +if you only want to query astrometric RA and Dec, you can use ``quantities=1``; +if you only want the heliocentric and geocentric distances, you can use +``quantities='19,20'`` (note that in this case a string with comma-separated +codes has to be provided). + + +Orbital elements +---------------- + +:meth:`~astroquery.jplhorizons.HorizonsClass.elements` returns orbital elements +relative to some Solar System body (``location``, referred to as "CENTER" in +Horizons) and for a given epoch or a range of epochs (``epochs``) in the form of +an astropy table. The following example queries the osculating elements of +asteroid (433) Eros for a given date relative to the Sun: + +.. code-block:: python + + >>> from astroquery.jplhorizons import Horizons + >>> obj = Horizons(id='433', location='500@10', + ... epochs=2458133.33546) + >>> el = obj.elements() + >>> print(el) + targetname datetime_jd ... Q P + --- d ... AU d + ------------------ ------------- ... ------------- ------------ + 433 Eros (A898 PA) 2458133.33546 ... 1.78244263804 642.93873484 + + +The following fields are queried: + +.. code-block:: python + + >>> print(el.columns) + + +Optional parameters of :meth:`~astroquery.jplhorizons.HorizonsClass.elements` +include ``refsystem``, which defines the coordinate reference system used (ICRF +by default), ``refplane`` which defines the reference plane of the orbital +elements queried, and ``tp_type``, which switches between a relative and +absolute representation of the time of perihelion passage. For comets, the +options ``closest_apparition`` and ``no_fragments`` are available, which select +the closest apparition in time and reject fragments, respectively. Note that +these options should only be used for comets and will crash the query for other +object types. Also available are ``get_query_payload=True``, which skips the +query and only returns the query payload, and ``get_raw_response=True``, which +returns the raw query response instead of the astropy table. + +Vectors +------- + +:meth:`~astroquery.jplhorizons.HorizonsClass.vectors` returns the +state vector of the target body in cartesian coordinates relative to +some Solar System body (``location``, referred to as "CENTER" in +Horizons) and for a given epoch or a range of epochs (``epochs``) in +the form of an astropy table. The following example queries the state +vector of asteroid 2012 TC4 as seen from Goldstone for a range of +epochs: + +.. code-block:: python + + >>> from astroquery.jplhorizons import Horizons + >>> obj = Horizons(id='2012 TC4', location='257', + ... epochs={'start':'2017-10-01', 'stop':'2017-10-02', + ... 'step':'10m'}) + >>> vec = obj.vectors() + >>> print(vec) + targetname datetime_jd ... range range_rate + --- d ... AU AU / d + ---------- ------------- ... --------------- ----------------- + (2012 TC4) 2458027.5 ... 0.0429332099306 -0.00408018711862 + (2012 TC4) 2458027.50694 ... 0.0429048742906 -0.00408040726527 + (2012 TC4) 2458027.51389 ... 0.0428765385796 -0.00408020747595 + (2012 TC4) 2458027.52083 ... 0.0428482057142 -0.0040795878561 + (2012 TC4) 2458027.52778 ... 0.042819878607 -0.00407854931543 + (2012 TC4) 2458027.53472 ... 0.0427915601617 -0.0040770935665 + ... ... ... ... ... + (2012 TC4) 2458028.45833 ... 0.0392489462501 -0.00405496595173 + (2012 TC4) 2458028.46528 ... 0.03922077771 -0.00405750632914 + (2012 TC4) 2458028.47222 ... 0.039192592935 -0.00405964084539 + (2012 TC4) 2458028.47917 ... 0.039164394759 -0.00406136516755 + (2012 TC4) 2458028.48611 ... 0.0391361860433 -0.00406267574646 + (2012 TC4) 2458028.49306 ... 0.0391079696711 -0.0040635698239 + (2012 TC4) 2458028.5 ... 0.0390797485422 -0.00406404543822 + Length = 145 rows + +The following fields are queried: + + >>> print(vec.columns) + + + +Similar to the other :class:`~astroquery.jplhorizons.HorizonsClass` functions, +optional parameters of :meth:`~astroquery.jplhorizons.HorizonsClass.vectors` are +``get_query_payload=True``, which skips the query and only returns the query +payload, and ``get_raw_response=True``, which returns the raw query response +instead of the astropy table. For comets, the options ``closest_apparation`` and +``no_fragments`` are available, which select the closest apparition in time and +reject fragments, respectively. Note that these options should only be used for +comets and will crash the query for other object types. Options ``aberrations`` +and ``delta_T`` provide different choices for aberration corrections as well as +a measure for time-varying differences between TDB and UT time-scales, +respectively. + + +How to Use the Query Tables +=========================== + +`astropy table`_ objects created by the query functions are extremely versatile +and easy to use. Since all query functions return the same type of table, they +can all be used in the same way. + +We provide some examples to illustrate how to use them based on the following +JPL Horizons ephemerides query of near-Earth asteroid (3552) Don Quixote since +its year of Discovery: + +.. code-block:: python + + >>> from astroquery.jplhorizons import Horizons + >>> obj = Horizons(id='3552', location='568', + ... epochs={'start':'2010-01-01', 'stop':'2019-12-31', + ... 'step':'1y'}) + >>> eph = obj.ephemerides() + +As we have seen before, we can display a truncated version of table +``eph`` by simply using + +.. code-block:: python + + >>> print(eph) + targetname datetime_str ... PABLon PABLat + --- --- ... deg deg + -------------------------- ----------------- ... -------- -------- + 3552 Don Quixote (1983 SA) 2010-Jan-01 00:00 ... 8.0371 18.9349 + 3552 Don Quixote (1983 SA) 2011-Jan-01 00:00 ... 85.4082 34.5611 + 3552 Don Quixote (1983 SA) 2012-Jan-01 00:00 ... 109.2959 30.3834 + 3552 Don Quixote (1983 SA) 2013-Jan-01 00:00 ... 123.0777 26.136 + 3552 Don Quixote (1983 SA) 2014-Jan-01 00:00 ... 133.9392 21.8962 + 3552 Don Quixote (1983 SA) 2015-Jan-01 00:00 ... 144.2701 17.1908 + 3552 Don Quixote (1983 SA) 2016-Jan-01 00:00 ... 156.1007 11.1447 + 3552 Don Quixote (1983 SA) 2017-Jan-01 00:00 ... 174.0245 1.3487 + 3552 Don Quixote (1983 SA) 2018-Jan-01 00:00 ... 228.9956 -21.6723 + 3552 Don Quixote (1983 SA) 2019-Jan-01 00:00 ... 45.1979 32.3885 + + +Please note the formatting of this table, which is done automatically. Above the +dashes in the first two lines, you have the column name and its unit. Every +column is assigned a unit from `astropy units`_. We will learn later how to use +these units. + + +Columns +------- + +We can get at list of all the columns in this table with: + +.. code-block:: python + + >>> print(eph.columns) + + +We can address each column individually by indexing it using its name as +provided in this list. For instance, we can get all RAs for Don Quixote by using + +.. code-block:: python + + >>> print(eph['RA']) + RA + deg + --------- + 345.50204 + 78.77158 + 119.85659 + 136.60021 + 147.44947 + 156.58967 + 166.32129 + 180.6992 + 232.11974 + 16.1066 + + +This column is formatted like the entire table; it has a column name and a unit. +We can select several columns at a time, for instance RA and DEC for each epoch + +.. code-block:: python + + >>> print(eph['datetime_str', 'RA', 'DEC']) + datetime_str RA DEC + --- deg deg + ----------------- --------- -------- + 2010-Jan-01 00:00 345.50204 13.43621 + 2011-Jan-01 00:00 78.77158 61.48831 + 2012-Jan-01 00:00 119.85659 54.21955 + 2013-Jan-01 00:00 136.60021 45.82409 + 2014-Jan-01 00:00 147.44947 37.79876 + 2015-Jan-01 00:00 156.58967 29.23058 + 2016-Jan-01 00:00 166.32129 18.48174 + 2017-Jan-01 00:00 180.6992 1.20453 + 2018-Jan-01 00:00 232.11974 -37.9554 + 2019-Jan-01 00:00 16.1066 45.50296 + + +We can use the same representation to do math with these columns. For instance, +let's calculate the total rate of the object by summing 'RA_rate' and 'DEC_rate' +in quadrature: + +.. code-block:: python + + >>> import numpy as np + >>> print(np.sqrt(eph['RA_rate']**2 + eph['DEC_rate']**2)) + dRA*cosD + ------------------ + 86.18728612153883 + 26.337249029653798 + 21.520859656742434 + 17.679843758686584 + 14.775809055378625 + 11.874886005626538 + 7.183281978025435 + 7.295600209387093 + 94.84824546372009 + 23.952470898018017 + + +Please note that the column name is wrong (copied from the name of the first +column used), and that the unit is lost. + +Units +----- + +Columns have units assigned to them. For instance, the ``RA`` column has +the unit ``deg`` assigned to it, i.e., degrees. More complex units are +available, too, e.g., the ``RA_rate`` column is expressed in ``arcsec / +h`` - arcseconds per hour: + +.. code-block:: python + + >>> print(eph['RA_rate']) + RA_rate + arcsec / h + ---------- + 72.35438 + -23.8239 + -20.7151 + -15.5509 + -12.107 + -9.32616 + -5.80004 + 3.115853 + 85.22719 + 19.02548 + + +The unit of this column can be easily converted to any other unit describing the +same dimensions. For instance, we can turn ``RA_rate`` into ``arcsec / s``: + +.. code-block:: python + + >>> eph['RA_rate'].convert_unit_to('arcsec/s') + >>> print(eph['RA_rate']) + RA_rate + arcsec / s + ---------------------- + 0.02009843888888889 + -0.0066177499999999995 + -0.005754194444444445 + -0.004319694444444445 + -0.0033630555555555553 + -0.0025905999999999998 + -0.0016111222222222222 + 0.0008655147222222222 + 0.023674219444444443 + 0.005284855555555556 + + +Please refer to the `astropy table`_ and `astropy units`_ documentations for +more information. + +Hints and Tricks +================ + +Checking the original JPL Horizons output +----------------------------------------- + +Once either of the query methods has been called, the retrieved raw response is +stored in the attribute ``raw_response``. Inspecting this response can help to +understand issues with your query, or you can process the results differently. + +For all query types, the query URI (the URI is what you would put into the URL +field of your web browser) that is used to request the data from the JPL +Horizons server can be obtained from the +:class:`~astroquery.jplhorizons.HorizonsClass` object after a query has been +performed (before the query only ``None`` would be returned): + + >>> print(obj.uri) + https://ssd.jpl.nasa.gov/api/horizons.api?format=text&EPHEM_TYPE=OBSERVER&QUANTITIES=%271%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20%2C21%2C22%2C23%2C24%2C25%2C26%2C27%2C28%2C29%2C30%2C31%2C32%2C33%2C34%2C35%2C36%2C37%2C38%2C39%2C40%2C41%2C42%2C43%27&COMMAND=%223552%22&SOLAR_ELONG=%220%2C180%22&LHA_CUTOFF=0&CSV_FORMAT=YES&CAL_FORMAT=BOTH&ANG_FORMAT=DEG&APPARENT=AIRLESS&REF_SYSTEM=ICRF&EXTRA_PREC=NO&CENTER=%27568%27&START_TIME=%222010-01-01%22&STOP_TIME=%222019-12-31%22&STEP_SIZE=%221y%22&SKIP_DAYLT=NO + +If your query failed, it might be useful for you to put the URI into a web +browser to get more information why it failed. Please note that ``uri`` is an +attribute of :class:`~astroquery.jplhorizons.HorizonsClass` and not the results +table. + +Date Formats +------------ + +JPL Horizons puts somewhat strict guidelines on the date formats: individual +epochs have to be provided as Julian dates, whereas epoch ranges have to be +provided as ISO dates (YYYY-MM-DD HH-MM UT). If you have your epoch dates in one +of these formats but you need the other format, make use of +:class:`astropy.time.Time` for the conversion. An example is provided here: + +.. doctest-requires:: astropy + + >>> from astropy.time import Time + >>> mydate_fromiso = Time('2018-07-23 15:55:23') # pass date as string + >>> print(mydate_fromiso.jd) # convert Time object to Julian date + 2458323.163460648 + >>> mydate_fromjd = Time(2458323.163460648, format='jd') + >>> print(mydate_fromjd.iso) # convert Time object to ISO + 2018-07-23 15:55:23.000 + +:class:`astropy.time.Time` allows you to convert dates across a wide range of +formats. Please note that when reading in Julian dates, you have to specify the +date format as ``'jd'``, as number passed to :class:`~astropy.time.Time` is +ambiguous. + +Keep Queries Short +------------------ + +Keep in mind that queries are sent as URIs to the Horizons server. If +you query a large number of epochs (in the form of a list), this list +might be truncated as URIs are typically expected to be shorter than +2,000 symbols and your results might be compromised. If your query URI +is longer than this limit, a warning is given. In that case, please +try using a range of dates instead of a list of individual dates. + +.. _jpl-horizons-reference-frames: + +Reference Frames +---------------- + +The coordinate reference frame for Horizons output is controlled by the +``refplane`` and ``refsystem`` keyword arguments. See the `Horizons +documentation`_ for details. Some output reference frames are included in +astropy's `~astropy.coordinates`: + ++----------------+--------------+----------------+----------------+---------------------------------+ +| Method | ``location`` | ``refplane`` | ``refsystem`` | astropy frame | ++================+==============+================+================+=================================+ +| ``.vectors()`` | ``'@0'`` | ``'ecliptic'`` | N/A | ``'custombarycentricecliptic'`` | ++----------------+--------------+----------------+----------------+---------------------------------+ +| ``.vectors()`` | ``'@0'`` | ``'earth'`` | N/A | ``'icrs'`` | ++----------------+--------------+----------------+----------------+---------------------------------+ +| ``.vectors()`` | ``'@10'`` | ``'ecliptic'`` | N/A | ``'heliocentriceclipticiau76'`` | ++----------------+--------------+----------------+----------------+---------------------------------+ + +For example, get the barycentric coordinates of Jupiter as an astropy +`~astropy.coordinates.SkyCoord` object: + +.. code-block:: python + + >>> from astropy.coordinates import SkyCoord + >>> from astropy.time import Time + >>> from astroquery.jplhorizons import Horizons + >>> epoch = Time('2021-01-01') + >>> q = Horizons('599', location='@0', epochs=epoch.tdb.jd) + >>> tab = q.vectors(refplane='earth') + >>> c = SkyCoord(tab['x'].quantity, tab['y'].quantity, tab['z'].quantity, + ... representation_type='cartesian', frame='icrs', + ... obstime=epoch) + >>> print(c) + + + + + +Acknowledgements +================ + +This submodule makes use of the `JPL Horizons +`_ system. + +The development of this submodule is in part funded through NASA PDART Grant No. +80NSSC18K0987 to the `sbpy project `_. + + +Reference/API +============= + +.. automodapi:: astroquery.jplhorizons + :no-inheritance-diagram: + +.. _Solar System Dynamics group at the Jet Propulation Laboratory: http://ssd.jpl.nasa.gov/ +.. _MPC Observatory codes: http://minorplanetcenter.net/iau/lists/ObsCodesF.html +.. _astropy table: http://docs.astropy.org/en/stable/table/index.html +.. _astropy units: http://docs.astropy.org/en/stable/units/index.html +.. _Definition of Observer Table Quantities: https://ssd.jpl.nasa.gov/horizons/manual.html#observer-table +.. _Horizons documentation: https://ssd.jpl.nasa.gov/horizons/manual.html#observer-table \ No newline at end of file From 72a34be073c27d7f4590ac4a7a0fe92452223bec Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Sun, 8 May 2022 20:27:22 -0700 Subject: [PATCH 07/25] added offline test --- astroquery/solarsystem/pds/core.py | 117 ++++---- .../pds/tests/data/uranus_ephemeris.html | 118 +++++++++ .../solarsystem/pds/tests/setup_package.py | 7 + .../solarsystem/pds/tests/test_ringnode.py | 250 +++++++++++------- .../pds/tests/test_ringnode_remote.py | 113 ++++++++ coverage.xml | 2 +- 6 files changed, 447 insertions(+), 160 deletions(-) create mode 100644 astroquery/solarsystem/pds/tests/data/uranus_ephemeris.html create mode 100644 astroquery/solarsystem/pds/tests/setup_package.py create mode 100644 astroquery/solarsystem/pds/tests/test_ringnode_remote.py diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 57f0d6c299..8a4d6d36b9 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -37,7 +37,7 @@ def __init__(self, planet=None, obs_time=None): Parameters ---------- - + """ super().__init__() @@ -53,7 +53,7 @@ def __str__(self): >>> print(nodeobj) # doctest: +SKIP PDSRingNode instance """ - return 'PDSRingNode instance' + return "PDSRingNode instance" # --- pretty stuff above this line, get it working below this line --- @@ -76,8 +76,8 @@ def ephemeris_async( ---------- self : RingNodeClass instance planet : str, required. one of Mars, Jupiter, Saturn, Uranus, Neptune, or Pluto - obs_time : astropy.Time object, or str in format YYYY-MM-DD hh:mm, optional. - If str is provided then UTC is assumed. If no obs_time is provided, + obs_time : astropy.Time object, or str in format YYYY-MM-DD hh:mm, optional. + If str is provided then UTC is assumed. If no obs_time is provided, the current time is used. location : array-like, or `~astropy.coordinates.EarthLocation`, optional Observer's location as a @@ -141,7 +141,7 @@ def ephemeris_async( ) elif type(obs_time) == Time: try: - obs_time = obs_time.utc.to_value('iso', subfmt = 'date_hm') + obs_time = obs_time.utc.to_value("iso", subfmt="date_hm") except Exception as e: raise ValueError( "illegal value for 'obs_time' parameter. could not parse astropy.time.core.Time object into format 'yyyy-mm-dd hh:mm' (UTC)" @@ -154,25 +154,27 @@ def ephemeris_async( else: viewpoint = "latlon" if type(location) != EarthLocation: - if hasattr(location, '__iter__'): + if hasattr(location, "__iter__"): if len(location) != 3: raise ValueError( "location arrays require three values:" - " longitude, latitude, and altitude") + " longitude, latitude, and altitude" + ) else: raise TypeError( - "location must be array-like or astropy EarthLocation") - + "location must be array-like or astropy EarthLocation" + ) + if isinstance(location, EarthLocation): loc = location.geodetic longitude = loc[0].deg latitude = loc[1].deg altitude = loc[2].to(u.m).value - elif hasattr(location, '__iter__'): - longitude = Angle(location[0]).deg - latitude = Angle(location[1]).deg - altitude = u.Quantity(location[2]).to('m').value - + elif hasattr(location, "__iter__"): + latitude = Angle(location[0]).deg + longitude = Angle(location[1]).deg + altitude = u.Quantity(location[2]).to("m").value + if int(neptune_arcmodel) not in [1, 2, 3]: raise ValueError( f"Illegal Neptune arc model {neptune_arcmodel}. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details)" @@ -183,12 +185,9 @@ def ephemeris_async( # thankfully, adding extra planet-specific keywords here does not break query for other planets request_payload = OrderedDict( [ - ("abbrev", planet[:3]), - ( - "ephem", - conf.planet_defaults[planet]["ephem"], - ), # change hardcoding for other planets - ("time", obs_time), #UTC. this should be enforced when checking inputs + ("abbrev", planet.lower()[:3]), + ("ephem", conf.planet_defaults[planet]["ephem"],), + ("time", obs_time), # UTC. this should be enforced when checking inputs ( "fov", 10, @@ -297,17 +296,10 @@ def _parse_ringnode(self, src): "dRA", "dDec", ), - ) - units_list = [None, - None, - None, - None, - u.deg, - u.deg, - u.arcsec, - u.arcsec] - bodytable = table.QTable(bodytable, units = units_list) - #for i in range(len(bodytable.colnames)): + ) + units_list = [None, None, None, None, u.deg, u.deg, u.arcsec, u.arcsec] + bodytable = table.QTable(bodytable, units=units_list) + # for i in range(len(bodytable.colnames)): # bodytable[bodytable.colnames[i]].unit = units_list[i] # minor body table part 2 elif group.startswith("Sub-"): @@ -327,18 +319,11 @@ def _parse_ringnode(self, src): "sub_sun_lat", "phase", "distance", - )) - units_list=[ - None, - None, - u.deg, - u.deg, - u.deg, - u.deg, - u.deg, - u.km * 1e6] - bodytable2 = table.QTable(bodytable2, units = units_list) - #for i in range(len(bodytable2.colnames)): + ), + ) + units_list = [None, None, u.deg, u.deg, u.deg, u.deg, u.deg, u.km * 1e6] + bodytable2 = table.QTable(bodytable2, units=units_list) + # for i in range(len(bodytable2.colnames)): # bodytable2[bodytable2.colnames[i]].unit = units_list[i] # ring plane data @@ -359,15 +344,15 @@ def _parse_ringnode(self, src): } elif "Ring plane opening angle" in l[0]: - systemtable["opening_angle"] = float( - re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()") - ) * u.deg + systemtable["opening_angle"] = ( + float(re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()")) * u.deg + ) elif "Ring center phase angle" in l[0]: systemtable["phase_angle"] = float(l[1].strip(", \n")) * u.deg elif "Sub-solar longitude" in l[0]: - systemtable["sub_sun_lon"] = float( - re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()") - ) * u.deg + systemtable["sub_sun_lon"] = ( + float(re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()")) * u.deg + ) elif "Sub-observer longitude" in l[0]: systemtable["sub_obs_lon"] = float(l[1].strip(", \n")) * u.deg else: @@ -379,10 +364,10 @@ def _parse_ringnode(self, src): for line in lines: l = line.split(":") if "Sun-planet distance (AU)" in l[0]: - #systemtable["d_sun_AU"] = float(l[1].strip(", \n")) + # systemtable["d_sun_AU"] = float(l[1].strip(", \n")) pass elif "Observer-planet distance (AU)" in l[0]: - #systemtable["d_obs_AU"] = float(l[1].strip(", \n")) + # systemtable["d_obs_AU"] = float(l[1].strip(", \n")) pass elif "Sun-planet distance (km)" in l[0]: systemtable["d_sun"] = ( @@ -405,11 +390,12 @@ def _parse_ringnode(self, src): format="fixed_width", col_starts=(5, 18, 29), col_ends=(18, 29, 36), - names=("ring", "pericenter", "ascending node")) - - units_list=[None, u.deg, u.deg] - ringtable = table.QTable(ringtable, units = units_list) - + names=("ring", "pericenter", "ascending node"), + ) + + units_list = [None, u.deg, u.deg] + ringtable = table.QTable(ringtable, units=units_list) + # Saturn F-ring data elif group.startswith("F Ring"): lines = group.split("\n") @@ -422,7 +408,7 @@ def _parse_ringnode(self, src): ringtable = table.Table( [["F"], [peri], [ascn]], names=("ring", "pericenter", "ascending node"), - units=(None, u.deg, u.deg) + units=(None, u.deg, u.deg), ) # Neptune ring arcs data @@ -439,7 +425,7 @@ def _parse_ringnode(self, src): ringtable = table.Table( [[ring], [min_angle], [max_angle]], names=("ring", "min_angle", "max_angle"), - units=(None, u.deg, u.deg) + units=(None, u.deg, u.deg), ) else: ringtable.add_row([ring, min_angle, max_angle]) @@ -447,16 +433,15 @@ def _parse_ringnode(self, src): else: pass - # - ## do some cleanup from the parsing job - # + # do some cleanup from the parsing job ringtable.add_index("ring") - - bodytable = table.join(bodytable, bodytable2) # concatenate minor body table + + bodytable = table.join(bodytable, bodytable2) # concatenate minor body table bodytable.add_index("Body") - - systemtable["obs_time"] = Time(obs_time, format = 'iso', scale = 'utc') # add obs time to systemtable - + + systemtable["obs_time"] = Time( + obs_time, format="iso", scale="utc" + ) # add obs time to systemtable return systemtable, bodytable, ringtable diff --git a/astroquery/solarsystem/pds/tests/data/uranus_ephemeris.html b/astroquery/solarsystem/pds/tests/data/uranus_ephemeris.html new file mode 100644 index 0000000000..03db30f5c0 --- /dev/null +++ b/astroquery/solarsystem/pds/tests/data/uranus_ephemeris.html @@ -0,0 +1,118 @@ + + +Uranus Viewer 3.0 Results + +

Uranus Viewer 3.0 Results

+

+

+Input Parameters
+----------------
+ 
+  Observation time: 2022-05-03 00:00
+         Ephemeris: URA111 + URA115 + DE440
+     Field of view: 10 (Uranus radii)
+    Diagram center: Uranus
+         Viewpoint: Lat = 10 (deg)
+                    Lon = -120.355 (deg east)
+                    Alt = 1000 (m)
+    Moon selection: All inner moons (U1-U15,U25-U27)
+    Ring selection: Nine major rings
+    Standard stars: No
+   Additional star: No
+      Other bodies: None
+             Title: 
+       Moon labels: Small (6 points)
+  Moon enlargement: 0 (points)
+       Blank disks: No
+Pericenter markers: None
+       Marker size: 4 (points)
+   Prime meridians: Yes
+ 
+ 
+Field of View Description (J2000)
+---------------------------------
+ 
+     Body          RA                 Dec                 RA (deg)    Dec (deg)    dRA (")     dDec (")
+ 799 Uranus         2h 48m 02.3164s    15d 48m 04.141s     42.009651   15.801150      0.000      0.000
+ 701 Ariel          2h 48m 02.6985s    15d 48m 14.802s     42.011244   15.804112      5.515     10.661
+ 702 Umbriel        2h 48m 03.0454s    15d 48m 16.080s     42.012689   15.804467     10.522     11.939
+ 703 Titania        2h 48m 02.9240s    15d 47m 36.893s     42.012184   15.793581      8.771    -27.248
+ 704 Oberon         2h 48m 04.1962s    15d 47m 42.409s     42.017484   15.795114     27.133    -21.732
+ 705 Miranda        2h 48m 02.7625s    15d 48m 07.862s     42.011510   15.802184      6.439      3.722
+ 706 Cordelia       2h 48m 02.2509s    15d 48m 07.282s     42.009379   15.802023     -0.944      3.141
+ 707 Ophelia        2h 48m 02.2438s    15d 48m 00.861s     42.009349   15.800239     -1.047     -3.280
+ 708 Bianca         2h 48m 02.0990s    15d 48m 05.324s     42.008746   15.801479     -3.137      1.183
+ 709 Cressida       2h 48m 02.3358s    15d 48m 00.038s     42.009732   15.800011      0.280     -4.102
+ 710 Desdemona      2h 48m 02.4006s    15d 48m 08.008s     42.010003   15.802224      1.217      3.867
+ 711 Juliet         2h 48m 02.1052s    15d 48m 06.438s     42.008772   15.801788     -3.047      2.298
+ 712 Portia         2h 48m 02.3709s    15d 48m 08.408s     42.009879   15.802336      0.787      4.267
+ 713 Rosalind       2h 48m 02.5516s    15d 48m 06.258s     42.010632   15.801738      3.395      2.117
+ 714 Belinda        2h 48m 02.4108s    15d 47m 59.372s     42.010045   15.799825      1.363     -4.769
+ 715 Puck           2h 48m 02.1358s    15d 48m 09.016s     42.008899   15.802505     -2.605      4.876
+ 725 Perdita        2h 48m 02.3249s    15d 48m 09.230s     42.009687   15.802564      0.124      5.089
+ 726 Mab            2h 48m 02.6883s    15d 48m 04.764s     42.011201   15.801323      5.368      0.623
+ 727 Cupid          2h 48m 02.5963s    15d 48m 04.777s     42.010818   15.801327      4.040      0.636
+ 
+                   Sub-Observer         Sub-Solar     
+     Body          Lon(degE) Lat(deg)   Lon(degE) Lat(deg)   Phase(deg)   Distance(10^6 km)
+ 799 Uranus         24.025    56.016     24.014    56.122      0.10924    3098.568884
+ 701 Ariel         124.977    56.141    124.967    56.249      0.10934    3098.505814
+ 702 Umbriel       139.323    56.082    139.313    56.190      0.10941    3098.454209
+ 703 Titania       251.999    56.036    251.987    56.141      0.10931    3098.498297
+ 704 Oberon        216.747    55.830    216.739    55.936      0.10959    3098.307219
+ 705 Miranda       151.067    54.505    151.037    54.611      0.10934    3098.502972
+ 706 Cordelia      308.108    56.084    308.095    56.194      0.10923    3098.576275
+ 707 Ophelia        89.619    56.085     89.605    56.188      0.10922    3098.581622
+ 708 Bianca        334.349    55.945    334.331    56.053      0.10919    3098.599901
+ 709 Cressida      280.863    56.045    280.852    56.149      0.10923    3098.568767
+ 710 Desdemona     145.642    55.889    145.633    55.998      0.10926    3098.554071
+ 711 Juliet         45.698    55.990     45.681    56.098      0.10920    3098.598161
+ 712 Portia        105.643    55.932    105.633    56.041      0.10926    3098.557986
+ 713 Rosalind      147.213    55.738    147.207    55.846      0.10929    3098.533106
+ 714 Belinda       223.015    56.048    223.006    56.152      0.10925    3098.558348
+ 715 Puck           55.434    56.246     55.421    56.355      0.10921    3098.591319
+ 725 Perdita       163.507    55.992    163.496    56.101      0.10925    3098.564229
+ 726 Mab           223.976    55.906    223.969    56.013      0.10932    3098.514000
+ 727 Cupid         246.256    55.885    246.250    55.992      0.10930    3098.527432
+
+     Ring          Pericenter   Ascending Node (deg, from ring plane ascending node)
+     Six            58.012      283.243
+     Five          300.684      245.829
+     Four          128.182      177.661
+     Alpha          13.729       62.922
+     Beta          231.051      353.609
+     Eta             0.000        0.000
+     Gamma         200.019        0.000
+     Delta           0.000        0.000
+     Epsilon        13.383        0.000
+ 
+   Ring sub-solar latitude (deg): -56.12233  (-56.13586  to -56.10881)
+  Ring plane opening angle (deg): -56.01577  (lit)
+   Ring center phase angle (deg):   0.10924
+       Sub-solar longitude (deg): 354.11072  from ring plane ascending node
+    Sub-observer longitude (deg): 354.12204
+
+        Sun-planet distance (AU):  19.70547
+   Observer-planet distance (AU):  20.71265
+        Sun-planet distance (km):  2947.896667 x 10^6
+   Observer-planet distance (km):  3098.568884 x 10^6
+         Light travel time (sec): 10335.713263
+
+
+
Preview:

+
+Click +here +to download diagram (PDF, 21116 bytes).

+Click +here +to download diagram (JPEG format, 185169 bytes).

+Click +here +to download diagram (PostScript format, 62562 bytes). +


+Uranus Viewer Form | +RMS Node Tools | +Ring-Moon Systems Home + + diff --git a/astroquery/solarsystem/pds/tests/setup_package.py b/astroquery/solarsystem/pds/tests/setup_package.py new file mode 100644 index 0000000000..a82c3e3dd9 --- /dev/null +++ b/astroquery/solarsystem/pds/tests/setup_package.py @@ -0,0 +1,7 @@ +import os + + +def get_package_data(): + paths = [os.path.join("data", "*.html")] # etc, add other extensions + + return {"astroquery.solarsystem.pds.tests": paths} diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index a659530062..ecd926a0de 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -1,105 +1,169 @@ import pytest -from astropy.tests.helper import assert_quantity_allclose +import os +from collections import OrderedDict import numpy as np + +from astropy.tests.helper import assert_quantity_allclose import astropy.units as u +from astroquery.utils.mocks import MockResponse from ... import pds -# Horizons has machinery here to mock request from a text file -# is that necessary? why is that done? -# wouldn't we want to know if the website we are querying changes something that makes code fail? +def data_path(filename): + data_dir = os.path.join(os.path.dirname(__file__), "data") + return os.path.join(data_dir, filename) + + +# monkeypatch replacement request function +def nonremote_request(self, request_type, url, **kwargs): + + with open(data_path("uranus_ephemeris.html"), "rb") as f: + response = MockResponse(content=f.read(), url=url) + + return response + + +# use a pytest fixture to create a dummy 'requests.get' function, +# that mocks(monkeypatches) the actual 'requests.get' function: +@pytest.fixture +def patch_request(request): + mp = request.getfixturevalue("monkeypatch") + + mp.setattr(pds.core.RingNodeClass, "_request", nonremote_request) + return mp # --------------------------------- actual test functions +def test_ephemeris_query(patch_request): + + systemtable, bodytable, ringtable = pds.RingNode().ephemeris( + planet="Uranus", + obs_time="2022-05-03 00:00", + location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), + ) + # check system table + assert_quantity_allclose( + [ + -56.12233, + -56.13586, + -56.13586, + -56.01577, + 0.10924, + 354.11072, + 354.12204, + 2947896667.0, + 3098568884.0, + 10335.713263, + ], + [ + systemtable["sub_sun_lat"].to(u.deg).value, + systemtable["sub_sun_lat_min"].to(u.deg).value, + systemtable["sub_sun_lat_max"].to(u.deg).value, + systemtable["opening_angle"].to(u.deg).value, + systemtable["phase_angle"].to(u.deg).value, + systemtable["sub_sun_lon"].to(u.deg).value, + systemtable["sub_obs_lon"].to(u.deg).value, + systemtable["d_sun"].to(u.km).value, + systemtable["d_obs"].to(u.km).value, + systemtable["light_time"].to(u.second).value, + ], + rtol=1e-2, + ) + + # check a moon in body table + mab = bodytable[bodytable.loc_indices["Mab"]] + assert mab["NAIF ID"] == 726 + assert mab["Body"] == "Mab" + assert_quantity_allclose( + [ + 42.011201, + 15.801323, + 5.368, + 0.623, + 223.976, + 55.906, + 223.969, + 56.013, + 0.10932, + 3098.514, + ], + [ + mab["RA (deg)"].to(u.deg).value, + mab["Dec (deg)"].to(u.deg).value, + mab["dRA"].to(u.arcsec).value, + mab["dDec"].to(u.arcsec).value, + mab["sub_obs_lon"].to(u.deg).value, + mab["sub_obs_lat"].to(u.deg).value, + mab["sub_sun_lon"].to(u.deg).value, + mab["sub_sun_lat"].to(u.deg).value, + mab["phase"].to(u.deg).value, + mab["distance"].to(u.km * 1e6).value, + ], + rtol=1e-2, + ) + + # check a ring in ringtable + beta = ringtable[ringtable.loc_indices["Beta"]] + assert np.isclose(beta["pericenter"].to(u.deg).value, 231.051, rtol=1e-3) + assert np.isclose(beta["ascending node"].to(u.deg).value, 353.6, rtol=1e-2) + +def test_ephemeris_query_payload(): + res = pds.RingNode().ephemeris( + planet="Neptune", + obs_time="2022-05-03 00:00", + neptune_arcmodel=1, + location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), + get_query_payload=True, + ) -@pytest.mark.remote_data -class TestRingNodeClass: - def test_ephemeris_query(self): - - systemtable, bodytable, ringtable = pds.RingNode().ephemeris( - planet="Uranus", obs_time="2022-05-03 00:00",location=(10.0*u.deg, -120.355*u.deg, 1000*u.m)) - # check system table - assert_quantity_allclose( - [ - -56.12233, - -56.13586, - -56.13586, - -56.01577, - 0.10924, - 354.11072, - 354.12204, - 2947896667.0, - 3098568884.0, - 10335.713263, - ], - [ - systemtable["sub_sun_lat"].to(u.deg).value, - systemtable["sub_sun_lat_min"].to(u.deg).value, - systemtable["sub_sun_lat_max"].to(u.deg).value, - systemtable["opening_angle"].to(u.deg).value, - systemtable["phase_angle"].to(u.deg).value, - systemtable["sub_sun_lon"].to(u.deg).value, - systemtable["sub_obs_lon"].to(u.deg).value, - systemtable["d_sun"].to(u.km).value, - systemtable["d_obs"].to(u.km).value, - systemtable["light_time"].to(u.second).value, - ], - rtol=1e-2, - ) - - # check a moon in body table - mab = bodytable[bodytable.loc_indices["Mab"]] - assert mab["NAIF ID"] == 726 - assert mab["Body"] == "Mab" - assert_quantity_allclose( - [ - 42.011201, - 15.801323, - 5.368, - 0.623, - 223.976, - 55.906, - 223.969, - 56.013, - 0.10932, - 3098.514, - ], - [ - mab["RA (deg)"].to(u.deg).value, - mab["Dec (deg)"].to(u.deg).value, - mab["dRA"].to(u.arcsec).value, - mab["dDec"].to(u.arcsec).value, - mab["sub_obs_lon"].to(u.deg).value, - mab["sub_obs_lat"].to(u.deg).value, - mab["sub_sun_lon"].to(u.deg).value, - mab["sub_sun_lat"].to(u.deg).value, - mab["phase"].to(u.deg).value, - mab["distance"].to(u.km * 1e6).value, - ], - rtol=1e-2, - ) - - # check a ring in ringtable - beta = ringtable[ringtable.loc_indices["Beta"]] - assert np.isclose(beta["pericenter"].to(u.deg).value, 231.051, rtol=1e-3) - assert np.isclose(beta["ascending node"].to(u.deg).value, 353.6, rtol=1e-2) - - def test_bad_query_exception_throw(self): - - with pytest.raises(ValueError): - pds.RingNode().ephemeris(planet="Mercury", obs_time="2022-05-03 00:00") - - with pytest.raises(ValueError): - pds.RingNode().ephemeris(planet="Uranus", obs_time="2022-13-03 00:00") - - with pytest.raises(ValueError): - pds.RingNode().ephemeris( - planet="Neptune", obs_time="2022-05-03 00:00", location=(10.0*u.deg, -120.355*u.deg) - ) - - with pytest.raises(ValueError): - pds.RingNode().ephemeris( - planet="Neptune", obs_time="2022-05-03 00:00", location=(10.0*u.deg, -120.355*u.deg, 1000*u.m), neptune_arcmodel=0 - ) + assert res == OrderedDict( + [ + ("abbrev", "nep"), + ("ephem", "000 NEP081 + NEP095 + DE440"), + ( + "time", + "2022-05-03 00:00", + ), # UTC. this should be enforced when checking inputs + ("fov", 10), # next few are figure options, can be hardcoded and ignored + ("fov_unit", "Neptune radii"), + ("center", "body"), + ("center_body", "Neptune"), + ("center_ansa", "Adams Ring"), + ("center_ew", "east"), + ("center_ra", ""), + ("center_ra_type", "hours"), + ("center_dec", ""), + ("center_star", ""), + ("viewpoint", "latlon"), + ( + "observatory", + "Earth's center", + ), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories + ("latitude", 10), + ("longitude", -120.355), + ("lon_dir", "east"), + ("altitude", 1000), + ("moons", "814 All inner moons (N1-N8,N14)"), + ("rings", "Galle, LeVerrier, Arago, Adams"), + ("arcmodel", "#1 (820.1194 deg/day)"), + ( + "extra_ra", + "", + ), # figure options below this line, can all be hardcoded and ignored + ("extra_ra_type", "hours"), + ("extra_dec", ""), + ("extra_name", ""), + ("title", ""), + ("labels", "Small (6 points)"), + ("moonpts", "0"), + ("blank", "No"), + ("opacity", "Transparent"), + ("peris", "None"), + ("peripts", "4"), + ("arcpts", "4"), + ("meridians", "Yes"), + ("output", "html"), + ] + ) diff --git a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py new file mode 100644 index 0000000000..a60a93d06f --- /dev/null +++ b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py @@ -0,0 +1,113 @@ +import pytest +from astropy.tests.helper import assert_quantity_allclose +import numpy as np +import astropy.units as u + +from ... import pds + + +# Horizons has machinery here to mock request from a text file +# is that necessary? why is that done? +# wouldn't we want to know if the website we are querying changes something that makes code fail? + + +# --------------------------------- actual test functions + + +@pytest.mark.remote_data +class TestRingNodeClass: + def test_ephemeris_query(self): + + systemtable, bodytable, ringtable = pds.RingNode().ephemeris( + planet="Uranus", + obs_time="2022-05-03 00:00", + location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), + ) + # check system table + assert_quantity_allclose( + [ + -56.12233, + -56.13586, + -56.13586, + -56.01577, + 0.10924, + 354.11072, + 354.12204, + 2947896667.0, + 3098568884.0, + 10335.713263, + ], + [ + systemtable["sub_sun_lat"].to(u.deg).value, + systemtable["sub_sun_lat_min"].to(u.deg).value, + systemtable["sub_sun_lat_max"].to(u.deg).value, + systemtable["opening_angle"].to(u.deg).value, + systemtable["phase_angle"].to(u.deg).value, + systemtable["sub_sun_lon"].to(u.deg).value, + systemtable["sub_obs_lon"].to(u.deg).value, + systemtable["d_sun"].to(u.km).value, + systemtable["d_obs"].to(u.km).value, + systemtable["light_time"].to(u.second).value, + ], + rtol=1e-2, + ) + + # check a moon in body table + mab = bodytable[bodytable.loc_indices["Mab"]] + assert mab["NAIF ID"] == 726 + assert mab["Body"] == "Mab" + assert_quantity_allclose( + [ + 42.011201, + 15.801323, + 5.368, + 0.623, + 223.976, + 55.906, + 223.969, + 56.013, + 0.10932, + 3098.514, + ], + [ + mab["RA (deg)"].to(u.deg).value, + mab["Dec (deg)"].to(u.deg).value, + mab["dRA"].to(u.arcsec).value, + mab["dDec"].to(u.arcsec).value, + mab["sub_obs_lon"].to(u.deg).value, + mab["sub_obs_lat"].to(u.deg).value, + mab["sub_sun_lon"].to(u.deg).value, + mab["sub_sun_lat"].to(u.deg).value, + mab["phase"].to(u.deg).value, + mab["distance"].to(u.km * 1e6).value, + ], + rtol=1e-2, + ) + + # check a ring in ringtable + beta = ringtable[ringtable.loc_indices["Beta"]] + assert np.isclose(beta["pericenter"].to(u.deg).value, 231.051, rtol=1e-3) + assert np.isclose(beta["ascending node"].to(u.deg).value, 353.6, rtol=1e-2) + + def test_bad_query_exception_throw(self): + + with pytest.raises(ValueError): + pds.RingNode().ephemeris(planet="Mercury", obs_time="2022-05-03 00:00") + + with pytest.raises(ValueError): + pds.RingNode().ephemeris(planet="Uranus", obs_time="2022-13-03 00:00") + + with pytest.raises(ValueError): + pds.RingNode().ephemeris( + planet="Neptune", + obs_time="2022-05-03 00:00", + location=(10.0 * u.deg, -120.355 * u.deg), + ) + + with pytest.raises(ValueError): + pds.RingNode().ephemeris( + planet="Neptune", + obs_time="2022-05-03 00:00", + location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), + neptune_arcmodel=0, + ) diff --git a/coverage.xml b/coverage.xml index e8dd5f4f48..c7c07e2b91 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,5 +1,5 @@ - + From 2dfa1000b090bd5e0074006c9cc0220b329ad318 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Fri, 13 May 2022 11:24:52 -0700 Subject: [PATCH 08/25] small fixes to tests, improved documentation --- astroquery/solarsystem/pds/core.py | 81 +++++++++++-------- .../solarsystem/pds/tests/test_ringnode.py | 27 ++++++- .../pds/tests/test_ringnode_remote.py | 23 ------ 3 files changed, 72 insertions(+), 59 deletions(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 8a4d6d36b9..7a5bb46b40 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -13,7 +13,6 @@ from bs4 import BeautifulSoup # 3. local imports - use relative imports -# commonly required local imports shown below as example # all Query classes should inherit from BaseQuery. from ...query import BaseQuery from ...utils import async_to_sync @@ -25,26 +24,21 @@ @async_to_sync class RingNodeClass(BaseQuery): """ - for querying the Planetary Ring Node ephemeris tools + a class for querying the Planetary Ring Node ephemeris tools - """ TIMEOUT = conf.timeout - def __init__(self, planet=None, obs_time=None): - """Instantiate Planetary Ring Node query - - Parameters - ---------- - - """ - + def __init__(self): + ''' + Instantiate Planetary Ring Node query + ''' super().__init__() def __str__(self): """ - String representation of RingNodeClass object instance + String representation of `~RingNodeClass` object instance Examples -------- @@ -68,46 +62,61 @@ def ephemeris_async( cache=True, ): """ - send query to server - - note this interacts with utils.async_to_sync to be called as ephemeris() + send query to Planetary Ring Node server Parameters ---------- - self : RingNodeClass instance + self : `~RingNodeClass` instance planet : str, required. one of Mars, Jupiter, Saturn, Uranus, Neptune, or Pluto - obs_time : astropy.Time object, or str in format YYYY-MM-DD hh:mm, optional. - If str is provided then UTC is assumed. If no obs_time is provided, - the current time is used. + obs_time : `~astropy.Time` object, or str in format YYYY-MM-DD hh:mm, optional. + If str is provided then UTC is assumed. + If no obs_time is provided, the current time is used. location : array-like, or `~astropy.coordinates.EarthLocation`, optional Observer's location as a 3-element array of Earth longitude, latitude, altitude, or - a `~astropy.coordinates.EarthLocation`. Longitude and + `~astropy.coordinates.EarthLocation`. Longitude and latitude should be anything that initializes an `~astropy.coordinates.Angle` object, and altitude should initialize an `~astropy.units.Quantity` object (with units - of length). If ``None``, then the geocenter (code 500) is - used. + of length). If ``None``, then the geocenter is used. neptune_arcmodel : float, optional. which ephemeris to assume for Neptune's ring arcs must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details) has no effect if planet != 'Neptune' + get_query_payload : boolean, optional + When set to `True` the method returns the HTTP request parameters as + a dict, default: False + get_raw_response : boolean, optional + Return raw data as obtained by the Planetary Ring Node without parsing the data + into a table, default: False + Returns ------- response : `requests.Response` The response of the HTTP request. + Examples -------- >>> from astroquery.solarsystem.pds import RingNode - >>> nodeobj = RingNode() - >>> eph = obj.ephemeris(planet='Uranus', - ... obs_time='2017-01-01 00:00') # doctest: +SKIP - >>> print(eph) # doctest: +SKIP - table here... + >>> systemtable, bodytable, ringtable = RingNode().ephemeris(planet='Uranus', + ... obs_time='2024-05-08 22:39', + ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +SKIP + >>> print(ringtable) # doctest: +SKIP + ring pericenter ascending node + deg deg + ------- ---------- -------------- + Six 293.129 52.0 + Five 109.438 81.1 + Four 242.882 66.9 + Alpha 184.498 253.9 + Beta 287.66 299.2 + Eta 0.0 0.0 + Gamma 50.224 0.0 + Delta 0.0 0.0 + Lambda 0.0 0.0 + Epsilon 298.022 0.0 """ - planet = planet - obs_time = obs_time URL = conf.pds_server # URL = 'https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?' @@ -126,7 +135,7 @@ def ephemeris_async( "pluto", ]: raise ValueError( - "illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto'" + "illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto')" ) if obs_time is None: @@ -181,7 +190,6 @@ def ephemeris_async( ) # configure request_payload for ephemeris query - # start with successful query and incrementally de-hardcode stuff # thankfully, adding extra planet-specific keywords here does not break query for other planets request_payload = OrderedDict( [ @@ -262,7 +270,9 @@ def _parse_ringnode(self, src): Returns ------- - data : `astropy.Table` + systemtable : dict + bodytable : `astropy.Table` + ringtable : `astropy.Table` """ self.raw_response = src @@ -448,7 +458,6 @@ def _parse_ringnode(self, src): def _parse_result(self, response, verbose=None): """ Routine for managing parser calls - note this MUST be named exactly _parse_result so it interacts with async_to_sync properly Parameters ---------- @@ -458,7 +467,9 @@ def _parse_result(self, response, verbose=None): Returns ------- - data : `astropy.Table` + systemtable : dict + bodytable : `astropy.Table` + ringtable : `astropy.Table` """ self.last_response = response try: @@ -475,7 +486,7 @@ def _parse_result(self, response, verbose=None): systemtable, bodytable, ringtable, - ) # astropy table, astropy table, astropy table + ) RingNode = RingNodeClass() diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index ecd926a0de..42c63b7f56 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -35,6 +35,7 @@ def patch_request(request): # --------------------------------- actual test functions + def test_ephemeris_query(patch_request): systemtable, bodytable, ringtable = pds.RingNode().ephemeris( @@ -140,7 +141,7 @@ def test_ephemeris_query_payload(): ( "observatory", "Earth's center", - ), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories + ), ("latitude", 10), ("longitude", -120.355), ("lon_dir", "east"), @@ -167,3 +168,27 @@ def test_ephemeris_query_payload(): ("output", "html"), ] ) + + +def test_bad_query_exception_throw(): + + with pytest.raises(ValueError): + pds.RingNode().ephemeris(planet="Mercury", obs_time="2022-05-03 00:00") + + with pytest.raises(ValueError): + pds.RingNode().ephemeris(planet="Uranus", obs_time="2022-13-03 00:00") + + with pytest.raises(ValueError): + pds.RingNode().ephemeris( + planet="Neptune", + obs_time="2022-05-03 00:00", + location=(10.0 * u.deg, -120.355 * u.deg), + ) + + with pytest.raises(ValueError): + pds.RingNode().ephemeris( + planet="Neptune", + obs_time="2022-05-03 00:00", + location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), + neptune_arcmodel=0, + ) diff --git a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py index a60a93d06f..fce9f12122 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py @@ -88,26 +88,3 @@ def test_ephemeris_query(self): beta = ringtable[ringtable.loc_indices["Beta"]] assert np.isclose(beta["pericenter"].to(u.deg).value, 231.051, rtol=1e-3) assert np.isclose(beta["ascending node"].to(u.deg).value, 353.6, rtol=1e-2) - - def test_bad_query_exception_throw(self): - - with pytest.raises(ValueError): - pds.RingNode().ephemeris(planet="Mercury", obs_time="2022-05-03 00:00") - - with pytest.raises(ValueError): - pds.RingNode().ephemeris(planet="Uranus", obs_time="2022-13-03 00:00") - - with pytest.raises(ValueError): - pds.RingNode().ephemeris( - planet="Neptune", - obs_time="2022-05-03 00:00", - location=(10.0 * u.deg, -120.355 * u.deg), - ) - - with pytest.raises(ValueError): - pds.RingNode().ephemeris( - planet="Neptune", - obs_time="2022-05-03 00:00", - location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), - neptune_arcmodel=0, - ) From c8f7b4c6cf73b4a83adc2903fd01cb0f41a9a143 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Fri, 13 May 2022 14:30:01 -0700 Subject: [PATCH 09/25] added test for Pluto, wrote an rst file --- astroquery/solarsystem/pds/core.py | 9 +- .../pds/tests/data/pluto_ephemeris.html | 77 +++ .../solarsystem/pds/tests/test_ringnode.py | 85 ++- coverage.xml | 251 +++++---- docs/solarsystem/pds/pds.rst | 482 +++--------------- 5 files changed, 349 insertions(+), 555 deletions(-) create mode 100644 astroquery/solarsystem/pds/tests/data/pluto_ephemeris.html diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 7a5bb46b40..1bf3fe2989 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -144,14 +144,14 @@ def ephemeris_async( elif type(obs_time) == str: try: Time.strptime(obs_time, "%Y-%m-%d %H:%M").jd - except Exception as e: + except Exception as ex: raise ValueError( "illegal value for 'obs_time' parameter. string must have format 'yyyy-mm-dd hh:mm'" ) elif type(obs_time) == Time: try: obs_time = obs_time.utc.to_value("iso", subfmt="date_hm") - except Exception as e: + except Exception as ex: raise ValueError( "illegal value for 'obs_time' parameter. could not parse astropy.time.core.Time object into format 'yyyy-mm-dd hh:mm' (UTC)" ) @@ -444,7 +444,8 @@ def _parse_ringnode(self, src): pass # do some cleanup from the parsing job - ringtable.add_index("ring") + if ringtable is not None: + ringtable.add_index("ring") bodytable = table.join(bodytable, bodytable2) # concatenate minor body table bodytable.add_index("Body") @@ -474,7 +475,7 @@ def _parse_result(self, response, verbose=None): self.last_response = response try: systemtable, bodytable, ringtable = self._parse_ringnode(response.text) - except Exception as e: + except Exception as ex: try: self._last_query.remove_cache_file(self.cache_location) except OSError: diff --git a/astroquery/solarsystem/pds/tests/data/pluto_ephemeris.html b/astroquery/solarsystem/pds/tests/data/pluto_ephemeris.html new file mode 100644 index 0000000000..43d48e25ea --- /dev/null +++ b/astroquery/solarsystem/pds/tests/data/pluto_ephemeris.html @@ -0,0 +1,77 @@ + + +Pluto Viewer 3.0 Results + +

Pluto Viewer 3.0 Results

+

+

+Input Parameters
+----------------
+ 
+  Observation time: 2021-10-07 07:25
+         Ephemeris: PLU058 + DE440
+     Field of view: 10 (Pluto-Charon separations (19,571 km))
+    Diagram center: Pluto
+         Viewpoint: Earth's center
+    Moon selection: All moons (P1-P5)
+    Ring selection: None
+    Standard stars: No
+   Additional star: No
+      Other bodies: None
+             Title: 
+       Moon labels: Small (6 points)
+  Moon enlargement: 0 (points)
+       Blank disks: No
+   Prime meridians: Yes
+ 
+ 
+Field of View Description (J2000)
+---------------------------------
+ 
+     Body          RA                 Dec                 RA (deg)    Dec (deg)    dRA (")     dDec (")
+ 999 Pluto         19h 44m 51.0347s   -22d 56m 05.931s    296.212645  -22.934981      0.000      0.000
+ 901 Charon        19h 44m 51.0870s   -22d 56m 05.830s    296.212862  -22.934953      0.722      0.101
+ 902 Nix           19h 44m 50.9270s   -22d 56m 04.738s    296.212196  -22.934650     -1.488      1.192
+ 903 Hydra         19h 44m 51.0560s   -22d 56m 08.227s    296.212733  -22.935619      0.294     -2.297
+ 904 Kerberos      19h 44m 50.9309s   -22d 56m 07.195s    296.212212  -22.935332     -1.434     -1.264
+ 905 Styx          19h 44m 50.9944s   -22d 56m 07.189s    296.212477  -22.935330     -0.557     -1.259
+ 
+                   Sub-Observer         Sub-Solar     
+     Body          Lon(degW) Lat(deg)   Lon(degW) Lat(deg)   Phase(deg)   Distance(10^6 km)
+ 999 Pluto          45.545    56.505     47.823    57.577      1.64048    5114.486814
+ 901 Charon        225.545    56.505    227.823    57.578      1.64048    5114.479237
+ 902 Nix           283.749   -41.845    285.264   -43.043      1.64048    5114.482774
+ 903 Hydra           7.786   -11.624      6.718   -12.887      1.64047    5114.516518
+ 904 Kerberos       12.108    56.881     14.388    57.967      1.64047    5114.516443
+ 905 Styx          350.443    56.472    352.720    57.544      1.64047    5114.509238
+ 
+   Ring sub-solar latitude (deg):  57.57737  ( 57.56961  to  57.58512)
+  Ring plane opening angle (deg):  56.50534  (lit)
+   Ring center phase angle (deg):   1.64048
+       Sub-solar longitude (deg): 116.55873  from ring plane ascending node
+    Sub-observer longitude (deg): 118.83690
+
+        Sun-planet distance (AU):  34.37680
+   Observer-planet distance (AU):  34.18823
+        Sun-planet distance (km):  5142.695995 x 10^6
+   Observer-planet distance (km):  5114.486814 x 10^6
+         Light travel time (sec): 17060.091666
+
+
+
Preview:

+
+Click +here +to download diagram (PDF, 11875 bytes).

+Click +here +to download diagram (JPEG format, 122052 bytes).

+Click +here +to download diagram (PostScript format, 23630 bytes). +


+Pluto Viewer Form | +RMS Node Tools | +Ring-Moon Systems Home + + diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index 42c63b7f56..6cf8153d9b 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -9,6 +9,11 @@ from astroquery.utils.mocks import MockResponse from ... import pds +# files in data/ for different planets +DATA_FILES = {'Uranus': 'uranus_ephemeris.html', + 'Pluto': 'pluto_ephemeris.html', + } + def data_path(filename): data_dir = os.path.join(os.path.dirname(__file__), "data") @@ -18,7 +23,8 @@ def data_path(filename): # monkeypatch replacement request function def nonremote_request(self, request_type, url, **kwargs): - with open(data_path("uranus_ephemeris.html"), "rb") as f: + planet_name = kwargs['params']['center_body'] + with open(data_path(DATA_FILES[planet_name.capitalize()]), "rb") as f: response = MockResponse(content=f.read(), url=url) return response @@ -36,7 +42,7 @@ def patch_request(request): # --------------------------------- actual test functions -def test_ephemeris_query(patch_request): +def test_ephemeris_query_Uranus(patch_request): systemtable, bodytable, ringtable = pds.RingNode().ephemeris( planet="Uranus", @@ -110,6 +116,79 @@ def test_ephemeris_query(patch_request): assert np.isclose(beta["ascending node"].to(u.deg).value, 353.6, rtol=1e-2) +def test_ephemeris_query_Pluto(patch_request): + + systemtable, bodytable, ringtable = pds.RingNode().ephemeris( + planet="Pluto", + obs_time="2021-10-07 07:25", + ) + print(systemtable) + print(bodytable[bodytable.loc_indices["Styx"]]) + # check system table + assert_quantity_allclose( + [ + 57.57737, + 57.56961, + 57.56961, + 56.50534, + 1.64048, + 116.55873, + 118.8369, + 5142696000, + 5114486810, + 17060.091666, + ], + [ + systemtable["sub_sun_lat"].to(u.deg).value, + systemtable["sub_sun_lat_min"].to(u.deg).value, + systemtable["sub_sun_lat_max"].to(u.deg).value, + systemtable["opening_angle"].to(u.deg).value, + systemtable["phase_angle"].to(u.deg).value, + systemtable["sub_sun_lon"].to(u.deg).value, + systemtable["sub_obs_lon"].to(u.deg).value, + systemtable["d_sun"].to(u.km).value, + systemtable["d_obs"].to(u.km).value, + systemtable["light_time"].to(u.second).value, + ], + rtol=1e-2, + ) + + # check a moon in body table + styx = bodytable[bodytable.loc_indices["Styx"]] + assert styx["NAIF ID"] == 905 + assert styx["Body"] == "Styx" + assert_quantity_allclose( + [ + 296.212477, + -22.93533, + -0.557, + -1.259, + 350.443, + 56.472, + 352.72, + 57.544, + 1.64047, + 5114.509238, + ], + [ + styx["RA (deg)"].to(u.deg).value, + styx["Dec (deg)"].to(u.deg).value, + styx["dRA"].to(u.arcsec).value, + styx["dDec"].to(u.arcsec).value, + styx["sub_obs_lon"].to(u.deg).value, + styx["sub_obs_lat"].to(u.deg).value, + styx["sub_sun_lon"].to(u.deg).value, + styx["sub_sun_lat"].to(u.deg).value, + styx["phase"].to(u.deg).value, + styx["distance"].to(u.km * 1e6).value, + ], + rtol=1e-2, + ) + + # check ringtable is None + assert ringtable is None + + def test_ephemeris_query_payload(): res = pds.RingNode().ephemeris( planet="Neptune", @@ -141,7 +220,7 @@ def test_ephemeris_query_payload(): ( "observatory", "Earth's center", - ), + ), ("latitude", 10), ("longitude", -120.355), ("lon_dir", "east"), diff --git a/coverage.xml b/coverage.xml index c7c07e2b91..2f591e642e 100644 --- a/coverage.xml +++ b/coverage.xml @@ -1,5 +1,5 @@ - + @@ -12636,9 +12636,9 @@ - + - + @@ -12650,159 +12650,158 @@ + - - + + - + - - - - - - - - - - - - + + + + + + + - - - - - + - - + - - - - + + + + + - - - - + + - + + + + + - - - - - - - - - + + + + + + + + + - - - - + + + + + - + - - - - - - - + + + + + - - - - + + + + + + + - + + + + + + + + - - - - - + + + - + - - + + - + + - + - - + - - - + + - - + + - - + + + + - - - - - + + + + + - - - - - - - + + + + + + + - - - - - - - - + + + + + + + + - - - - - - + + + @@ -13224,7 +13223,7 @@ - + @@ -13559,38 +13558,38 @@ - + - - - - - - - - + + + + + + + + - - - - + + + + - + - + - - - + + + - + @@ -13605,7 +13604,7 @@ - + diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index 6c784cac46..e38081dbb3 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -11,484 +11,122 @@ Overview The :class:`~astroquery.solarsystem.pds.RingNodeClass` class provides an -interface to services provided by the `Planetary Data System's Ring Node System hosted by SETI institute`_. - -In order to query information for a specific Solar System body, a -``RingNode`` object has to be instantiated: - -.. code-block:: python - - >>> from astroquery.solarsystem.pds import RingNode - >>> obj = RingNode(planet='Uranus', obs_time='2022-05-03 11:55') - >>> print(obj) - PDSRingNode instance "Uranus"; obs_time=2022-05-03 11:55 - -``planet`` must be one of ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto'] (case-insensitive) - -``obs_time`` is the UTC datetime to query in format 'YYYY-MM-DD HH:MM'. If not set, will assume the current time. Unlike the Horizons tool, the Planetary Ring Node unfortunately does not support querying multiple epochs at once. - - +interface to the ephemeris tools provided by the `NASA Planetary Data System's Ring Node System`_ hosted by SETI institute. Ephemeris ----------- -:meth:`~astroquery.solarsystem.pds.RingNodeClass.ephemeris` returns ephemeris information -for rings and small bodies around the given planet at a given observer location (``observer_coords``) and epoch -(``obs_time``) in the form of astropy tables. The following example queries the +In order to query information for a specific Solar System body, a +``RingNode`` object is instantiated and the :meth:`~astroquery.solarsystem.pds.RingNodeClass.ephemeris` method is called. The following example queries the ephemerides of the rings and small moons around Uranus as viewed from ALMA: .. code-block:: python >>> from astroquery.solarsystem.pds import RingNode - >>> obj = RingNode(planet='Uranus', obs_time='2022-05-03 11:55') - >>> systemtable, bodytable, ringtable = obj.ephemeris(observer_coords=(-23.029, -67.755, 5000)) - >>> print(eph) ## edited to here - targetname datetime_str datetime_jd ... GlxLat RA_3sigma DEC_3sigma - --- --- d ... deg arcsec arcsec - ---------- ----------------- ----------- ... --------- --------- ---------- - 1 Ceres 2010-Jan-01 00:00 2455197.5 ... 24.120057 0.0 0.0 - 1 Ceres 2010-Jan-11 00:00 2455207.5 ... 20.621496 0.0 0.0 - 1 Ceres 2010-Jan-21 00:00 2455217.5 ... 17.229529 0.0 0.0 - 1 Ceres 2010-Jan-31 00:00 2455227.5 ... 13.97264 0.0 0.0 - 1 Ceres 2010-Feb-10 00:00 2455237.5 ... 10.877201 0.0 0.0 - 1 Ceres 2010-Feb-20 00:00 2455247.5 ... 7.976737 0.0 0.0 - - -The following fields are available for each ephemerides query: - -.. code-block:: python - - >>> print(eph.columns) - - -The values in these columns are the same as those defined in the Horizons -`Definition of Observer Table Quantities`_; names have been simplified in a few -cases. Quantities ``H`` and ``G`` are the target's Solar System absolute -magnitude and photometric phase curve slope, respectively. In the case of -comets, ``H`` and ``G`` are replaced by ``M1``, ``M2``, ``k1``, ``k2``, and -``phasecoeff``; please refer to the `Horizons documentation`_ for definitions. - -Optional parameters of :meth:`~astroquery.jplhorizons.HorizonsClass.ephemerides` -correspond to optional features of the Horizons system: ``airmass_lessthan`` -sets an upper limit to airmass, ``solar_elongation`` enables the definition of a -solar elongation range, ``max_hour_angle`` sets a cutoff of the hour angle, -``skip_daylight=True`` rejects epochs during daylight, ``rate_cutoff`` rejects -targets with sky motion rates higher than provided (in units of arcsec/h), -``refraction`` accounts for refraction in the computation of the ephemerides -(disabled by default), and ``refsystem`` defines the coordinate reference system -used (ICRF by default). For comets, the options ``closest_apparition`` and -``no_fragments`` are available, which selects the closest apparition in time and -limits fragment matching (73P-B would only match 73P-B), respectively. Note -that these options should only be used for comets and will crash the query for -other object types. Extra precision in the queried properties can be requested -using the ``extra_precision`` option. Furthermore, ``get_query_payload=True`` -skips the query and only returns the query payload, whereas -``get_raw_response=True`` returns the raw query response instead of the astropy -table. - -:meth:`~astroquery.jplhorizons.HorizonsClass.ephemerides` queries by default all -available quantities from the JPL Horizons servers. This might take a while. If -you are only interested in a subset of the available quantities, you can query -only those. The corresponding optional parameter to be set is ``quantities``. -This parameter uses the same numerical codes as JPL Horizons defined in the `JPL -Horizons User Manual Definition of Observer Table Quantities -`_. For instance, -if you only want to query astrometric RA and Dec, you can use ``quantities=1``; -if you only want the heliocentric and geocentric distances, you can use -``quantities='19,20'`` (note that in this case a string with comma-separated -codes has to be provided). - - -Orbital elements ----------------- - -:meth:`~astroquery.jplhorizons.HorizonsClass.elements` returns orbital elements -relative to some Solar System body (``location``, referred to as "CENTER" in -Horizons) and for a given epoch or a range of epochs (``epochs``) in the form of -an astropy table. The following example queries the osculating elements of -asteroid (433) Eros for a given date relative to the Sun: - -.. code-block:: python - - >>> from astroquery.jplhorizons import Horizons - >>> obj = Horizons(id='433', location='500@10', - ... epochs=2458133.33546) - >>> el = obj.elements() - >>> print(el) - targetname datetime_jd ... Q P - --- d ... AU d - ------------------ ------------- ... ------------- ------------ - 433 Eros (A898 PA) 2458133.33546 ... 1.78244263804 642.93873484 - - -The following fields are queried: - -.. code-block:: python - - >>> print(el.columns) - - -Optional parameters of :meth:`~astroquery.jplhorizons.HorizonsClass.elements` -include ``refsystem``, which defines the coordinate reference system used (ICRF -by default), ``refplane`` which defines the reference plane of the orbital -elements queried, and ``tp_type``, which switches between a relative and -absolute representation of the time of perihelion passage. For comets, the -options ``closest_apparition`` and ``no_fragments`` are available, which select -the closest apparition in time and reject fragments, respectively. Note that -these options should only be used for comets and will crash the query for other -object types. Also available are ``get_query_payload=True``, which skips the -query and only returns the query payload, and ``get_raw_response=True``, which -returns the raw query response instead of the astropy table. - -Vectors -------- - -:meth:`~astroquery.jplhorizons.HorizonsClass.vectors` returns the -state vector of the target body in cartesian coordinates relative to -some Solar System body (``location``, referred to as "CENTER" in -Horizons) and for a given epoch or a range of epochs (``epochs``) in -the form of an astropy table. The following example queries the state -vector of asteroid 2012 TC4 as seen from Goldstone for a range of -epochs: - -.. code-block:: python - - >>> from astroquery.jplhorizons import Horizons - >>> obj = Horizons(id='2012 TC4', location='257', - ... epochs={'start':'2017-10-01', 'stop':'2017-10-02', - ... 'step':'10m'}) - >>> vec = obj.vectors() - >>> print(vec) - targetname datetime_jd ... range range_rate - --- d ... AU AU / d - ---------- ------------- ... --------------- ----------------- - (2012 TC4) 2458027.5 ... 0.0429332099306 -0.00408018711862 - (2012 TC4) 2458027.50694 ... 0.0429048742906 -0.00408040726527 - (2012 TC4) 2458027.51389 ... 0.0428765385796 -0.00408020747595 - (2012 TC4) 2458027.52083 ... 0.0428482057142 -0.0040795878561 - (2012 TC4) 2458027.52778 ... 0.042819878607 -0.00407854931543 - (2012 TC4) 2458027.53472 ... 0.0427915601617 -0.0040770935665 - ... ... ... ... ... - (2012 TC4) 2458028.45833 ... 0.0392489462501 -0.00405496595173 - (2012 TC4) 2458028.46528 ... 0.03922077771 -0.00405750632914 - (2012 TC4) 2458028.47222 ... 0.039192592935 -0.00405964084539 - (2012 TC4) 2458028.47917 ... 0.039164394759 -0.00406136516755 - (2012 TC4) 2458028.48611 ... 0.0391361860433 -0.00406267574646 - (2012 TC4) 2458028.49306 ... 0.0391079696711 -0.0040635698239 - (2012 TC4) 2458028.5 ... 0.0390797485422 -0.00406404543822 - Length = 145 rows - -The following fields are queried: - - >>> print(vec.columns) - - - -Similar to the other :class:`~astroquery.jplhorizons.HorizonsClass` functions, -optional parameters of :meth:`~astroquery.jplhorizons.HorizonsClass.vectors` are -``get_query_payload=True``, which skips the query and only returns the query -payload, and ``get_raw_response=True``, which returns the raw query response -instead of the astropy table. For comets, the options ``closest_apparation`` and -``no_fragments`` are available, which select the closest apparition in time and -reject fragments, respectively. Note that these options should only be used for -comets and will crash the query for other object types. Options ``aberrations`` -and ``delta_T`` provide different choices for aberration corrections as well as -a measure for time-varying differences between TDB and UT time-scales, -respectively. - - -How to Use the Query Tables -=========================== - -`astropy table`_ objects created by the query functions are extremely versatile -and easy to use. Since all query functions return the same type of table, they -can all be used in the same way. - -We provide some examples to illustrate how to use them based on the following -JPL Horizons ephemerides query of near-Earth asteroid (3552) Don Quixote since -its year of Discovery: - -.. code-block:: python - - >>> from astroquery.jplhorizons import Horizons - >>> obj = Horizons(id='3552', location='568', - ... epochs={'start':'2010-01-01', 'stop':'2019-12-31', - ... 'step':'1y'}) - >>> eph = obj.ephemerides() - -As we have seen before, we can display a truncated version of table -``eph`` by simply using - -.. code-block:: python - - >>> print(eph) - targetname datetime_str ... PABLon PABLat - --- --- ... deg deg - -------------------------- ----------------- ... -------- -------- - 3552 Don Quixote (1983 SA) 2010-Jan-01 00:00 ... 8.0371 18.9349 - 3552 Don Quixote (1983 SA) 2011-Jan-01 00:00 ... 85.4082 34.5611 - 3552 Don Quixote (1983 SA) 2012-Jan-01 00:00 ... 109.2959 30.3834 - 3552 Don Quixote (1983 SA) 2013-Jan-01 00:00 ... 123.0777 26.136 - 3552 Don Quixote (1983 SA) 2014-Jan-01 00:00 ... 133.9392 21.8962 - 3552 Don Quixote (1983 SA) 2015-Jan-01 00:00 ... 144.2701 17.1908 - 3552 Don Quixote (1983 SA) 2016-Jan-01 00:00 ... 156.1007 11.1447 - 3552 Don Quixote (1983 SA) 2017-Jan-01 00:00 ... 174.0245 1.3487 - 3552 Don Quixote (1983 SA) 2018-Jan-01 00:00 ... 228.9956 -21.6723 - 3552 Don Quixote (1983 SA) 2019-Jan-01 00:00 ... 45.1979 32.3885 + >>> systemtable, bodytable, ringtable = RingNode().ephemeris(planet='Uranus', + ... obs_time='2024-05-08 22:39', + ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +SKIP + >>> print(ringtable) + ring pericenter ascending node + deg deg + ------- ---------- -------------- + Six 293.129 52.0 + Five 109.438 81.1 + Four 242.882 66.9 + Alpha 184.498 253.9 + Beta 287.66 299.2 + Eta 0.0 0.0 + Gamma 50.224 0.0 + Delta 0.0 0.0 + Lambda 0.0 0.0 + Epsilon 298.022 0.0 +``planet`` must be one of ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto'] (case-insensitive) -Please note the formatting of this table, which is done automatically. Above the -dashes in the first two lines, you have the column name and its unit. Every -column is assigned a unit from `astropy units`_. We will learn later how to use -these units. +``obs_time`` is the datetime to query. Accepts a string in format 'YYYY-MM-DD HH:MM' (UTC assumed), or an ```astropy.Time`` object. If no obs_time is provided, the current time is used. +``location`` is the observer's location. Accepts an ``astropy.coordinates.EarthLocation``, or any 3-element array-like (e.g. list, tuple) of format (latitude, longitude, elevation). Longitude and latitude should be anything that initializes an ``astropy.coordinates.Angle`` object, and altitude should initialize an ``astropy.units.Quantity`` object (with units of length). If ``None``, then the geocenter is used. -Columns -------- +``neptune_arcmodel`` is the choice of which ephemeris to assume for Neptune's ring arcs. accepts a float. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details). default 3. has no effect if planet != 'Neptune' -We can get at list of all the columns in this table with: +Outputs +--------- +``systemtable`` is a dictionary containing system-wide ephemeris data. Every value is an `astropy Quantity`_ object. We can get a list of all the keys in this dictionary with: .. code-block:: python - >>> print(eph.columns) - + >>> print(systemtable.keys()) + dict_keys(['sub_sun_lat', 'sub_sun_lat_min', 'sub_sun_lat_max', 'opening_angle', 'phase_angle', 'sub_sun_lon', 'sub_obs_lon', 'd_sun', 'd_obs', 'light_time', 'obs_time']) -We can address each column individually by indexing it using its name as -provided in this list. For instance, we can get all RAs for Don Quixote by using +``bodytable`` is an `astropy table`_ containing ephemeris information on the moons in the planetary system. Every column is assigned a unit from `astropy units`_. We can get a list of all the columns in this table with: .. code-block:: python - >>> print(eph['RA']) - RA - deg - --------- - 345.50204 - 78.77158 - 119.85659 - 136.60021 - 147.44947 - 156.58967 - 166.32129 - 180.6992 - 232.11974 - 16.1066 - - -This column is formatted like the entire table; it has a column name and a unit. -We can select several columns at a time, for instance RA and DEC for each epoch - -.. code-block:: python + >>> print(bodytable.columns) + - >>> print(eph['datetime_str', 'RA', 'DEC']) - datetime_str RA DEC - --- deg deg - ----------------- --------- -------- - 2010-Jan-01 00:00 345.50204 13.43621 - 2011-Jan-01 00:00 78.77158 61.48831 - 2012-Jan-01 00:00 119.85659 54.21955 - 2013-Jan-01 00:00 136.60021 45.82409 - 2014-Jan-01 00:00 147.44947 37.79876 - 2015-Jan-01 00:00 156.58967 29.23058 - 2016-Jan-01 00:00 166.32129 18.48174 - 2017-Jan-01 00:00 180.6992 1.20453 - 2018-Jan-01 00:00 232.11974 -37.9554 - 2019-Jan-01 00:00 16.1066 45.50296 - - -We can use the same representation to do math with these columns. For instance, -let's calculate the total rate of the object by summing 'RA_rate' and 'DEC_rate' -in quadrature: +``ringtable`` is an `astropy table`_ containing ephemeris information on the individual rings in the planetary system. Every column is assigned a unit from `astropy units`_. We can get a list of all the columns in this table with: .. code-block:: python - >>> import numpy as np - >>> print(np.sqrt(eph['RA_rate']**2 + eph['DEC_rate']**2)) - dRA*cosD - ------------------ - 86.18728612153883 - 26.337249029653798 - 21.520859656742434 - 17.679843758686584 - 14.775809055378625 - 11.874886005626538 - 7.183281978025435 - 7.295600209387093 - 94.84824546372009 - 23.952470898018017 - - -Please note that the column name is wrong (copied from the name of the first -column used), and that the unit is lost. - -Units ------ - -Columns have units assigned to them. For instance, the ``RA`` column has -the unit ``deg`` assigned to it, i.e., degrees. More complex units are -available, too, e.g., the ``RA_rate`` column is expressed in ``arcsec / -h`` - arcseconds per hour: + >>> print(ringtable.columns) + + +Note that the behavior of ``ringtable`` changes depending on the planet you query. For Uranus and Saturn the table columns are as above. For Jupiter, Mars, and Pluto, there are no individual named rings returned by the Ring Node, so ``ringtable`` returns None; ephemeris for the ring systems of these bodies is still contained in ``systemtable`` as usual. For Neptune, the ring table shows the minimum and maximum longitudes (from the ring plane ascending node) of the five ring arcs according to the orbital evolution assumed by ``neptune_arcmodel``, e.g.: .. code-block:: python - >>> print(eph['RA_rate']) - RA_rate - arcsec / h - ---------- - 72.35438 - -23.8239 - -20.7151 - -15.5509 - -12.107 - -9.32616 - -5.80004 - 3.115853 - 85.22719 - 19.02548 - - -The unit of this column can be easily converted to any other unit describing the -same dimensions. For instance, we can turn ``RA_rate`` into ``arcsec / s``: + >>> systemtable, bodytable, ringtable = RingNode().ephemeris(planet='Neptune') + >>> print(ringtable) + ring min_angle max_angle + deg deg + ---------- --------- --------- + Courage 46.39438 47.39438 + Liberte 37.59439 41.69437 + Egalite A 26.79437 27.79437 + Egalite B 22.99439 25.99439 + Fraternite 8.99439 18.59439 -.. code-block:: python - >>> eph['RA_rate'].convert_unit_to('arcsec/s') - >>> print(eph['RA_rate']) - RA_rate - arcsec / s - ---------------------- - 0.02009843888888889 - -0.0066177499999999995 - -0.005754194444444445 - -0.004319694444444445 - -0.0033630555555555553 - -0.0025905999999999998 - -0.0016111222222222222 - 0.0008655147222222222 - 0.023674219444444443 - 0.005284855555555556 - - -Please refer to the `astropy table`_ and `astropy units`_ documentations for -more information. Hints and Tricks ================ -Checking the original JPL Horizons output +Checking the original RingNode output ----------------------------------------- -Once either of the query methods has been called, the retrieved raw response is +Once the query method has been called, the retrieved raw response is stored in the attribute ``raw_response``. Inspecting this response can help to understand issues with your query, or you can process the results differently. -For all query types, the query URI (the URI is what you would put into the URL -field of your web browser) that is used to request the data from the JPL -Horizons server can be obtained from the -:class:`~astroquery.jplhorizons.HorizonsClass` object after a query has been +The query URI (the URI is what you would put into the URL +field of your web browser) that is used to request the data from the Planetary Ring Node server can be obtained from the +:class:`~astroquery.solarsystem.pds.RingNode` object after a query has been performed (before the query only ``None`` would be returned): >>> print(obj.uri) - https://ssd.jpl.nasa.gov/api/horizons.api?format=text&EPHEM_TYPE=OBSERVER&QUANTITIES=%271%2C2%2C3%2C4%2C5%2C6%2C7%2C8%2C9%2C10%2C11%2C12%2C13%2C14%2C15%2C16%2C17%2C18%2C19%2C20%2C21%2C22%2C23%2C24%2C25%2C26%2C27%2C28%2C29%2C30%2C31%2C32%2C33%2C34%2C35%2C36%2C37%2C38%2C39%2C40%2C41%2C42%2C43%27&COMMAND=%223552%22&SOLAR_ELONG=%220%2C180%22&LHA_CUTOFF=0&CSV_FORMAT=YES&CAL_FORMAT=BOTH&ANG_FORMAT=DEG&APPARENT=AIRLESS&REF_SYSTEM=ICRF&EXTRA_PREC=NO&CENTER=%27568%27&START_TIME=%222010-01-01%22&STOP_TIME=%222019-12-31%22&STEP_SIZE=%221y%22&SKIP_DAYLT=NO + https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?abbrev=ura&ephem=000+URA111+%2B+URA115+%2B+DE440&time=2024-05-08+22%3A39&fov=10&fov_unit=Uranus+radii¢er=body¢er_body=Uranus¢er_ansa=Epsilon¢er_ew=east¢er_ra=¢er_ra_type=hours¢er_dec=¢er_star=&viewpoint=latlon&observatory=Earth%27s+center&latitude=-67.75499999999998&longitude=-23.028999999999996&lon_dir=east&altitude=4999.999999999843&moons=727+All+inner+moons+%28U1-U15%2CU25-U27%29&rings=All+rings&arcmodel=%233+%28820.1121+deg%2Fday%29&extra_ra=&extra_ra_type=hours&extra_dec=&extra_name=&title=&labels=Small+%286+points%29&moonpts=0&blank=No&opacity=Transparent&peris=None&peripts=4&arcpts=4&meridians=Yes&output=html If your query failed, it might be useful for you to put the URI into a web browser to get more information why it failed. Please note that ``uri`` is an -attribute of :class:`~astroquery.jplhorizons.HorizonsClass` and not the results +attribute of :class:`~astroquery.solarsystem.pds.RingNode` and not the results table. -Date Formats ------------- - -JPL Horizons puts somewhat strict guidelines on the date formats: individual -epochs have to be provided as Julian dates, whereas epoch ranges have to be -provided as ISO dates (YYYY-MM-DD HH-MM UT). If you have your epoch dates in one -of these formats but you need the other format, make use of -:class:`astropy.time.Time` for the conversion. An example is provided here: - -.. doctest-requires:: astropy - - >>> from astropy.time import Time - >>> mydate_fromiso = Time('2018-07-23 15:55:23') # pass date as string - >>> print(mydate_fromiso.jd) # convert Time object to Julian date - 2458323.163460648 - >>> mydate_fromjd = Time(2458323.163460648, format='jd') - >>> print(mydate_fromjd.iso) # convert Time object to ISO - 2018-07-23 15:55:23.000 - -:class:`astropy.time.Time` allows you to convert dates across a wide range of -formats. Please note that when reading in Julian dates, you have to specify the -date format as ``'jd'``, as number passed to :class:`~astropy.time.Time` is -ambiguous. - -Keep Queries Short ------------------- - -Keep in mind that queries are sent as URIs to the Horizons server. If -you query a large number of epochs (in the form of a list), this list -might be truncated as URIs are typically expected to be shorter than -2,000 symbols and your results might be compromised. If your query URI -is longer than this limit, a warning is given. In that case, please -try using a range of dates instead of a list of individual dates. - -.. _jpl-horizons-reference-frames: - -Reference Frames ----------------- - -The coordinate reference frame for Horizons output is controlled by the -``refplane`` and ``refsystem`` keyword arguments. See the `Horizons -documentation`_ for details. Some output reference frames are included in -astropy's `~astropy.coordinates`: - -+----------------+--------------+----------------+----------------+---------------------------------+ -| Method | ``location`` | ``refplane`` | ``refsystem`` | astropy frame | -+================+==============+================+================+=================================+ -| ``.vectors()`` | ``'@0'`` | ``'ecliptic'`` | N/A | ``'custombarycentricecliptic'`` | -+----------------+--------------+----------------+----------------+---------------------------------+ -| ``.vectors()`` | ``'@0'`` | ``'earth'`` | N/A | ``'icrs'`` | -+----------------+--------------+----------------+----------------+---------------------------------+ -| ``.vectors()`` | ``'@10'`` | ``'ecliptic'`` | N/A | ``'heliocentriceclipticiau76'`` | -+----------------+--------------+----------------+----------------+---------------------------------+ - -For example, get the barycentric coordinates of Jupiter as an astropy -`~astropy.coordinates.SkyCoord` object: - -.. code-block:: python - - >>> from astropy.coordinates import SkyCoord - >>> from astropy.time import Time - >>> from astroquery.jplhorizons import Horizons - >>> epoch = Time('2021-01-01') - >>> q = Horizons('599', location='@0', epochs=epoch.tdb.jd) - >>> tab = q.vectors(refplane='earth') - >>> c = SkyCoord(tab['x'].quantity, tab['y'].quantity, tab['z'].quantity, - ... representation_type='cartesian', frame='icrs', - ... obstime=epoch) - >>> print(c) - - - - Acknowledgements ================ -This submodule makes use of the `JPL Horizons -`_ system. - -The development of this submodule is in part funded through NASA PDART Grant No. -80NSSC18K0987 to the `sbpy project `_. +This submodule makes use of the NASA Planetary Data System's `Planetary Ring Node +`_ . Reference/API ============= -.. automodapi:: astroquery.jplhorizons +.. automodapi:: astroquery.solarsystem.pds :no-inheritance-diagram: -.. _Solar System Dynamics group at the Jet Propulation Laboratory: http://ssd.jpl.nasa.gov/ -.. _MPC Observatory codes: http://minorplanetcenter.net/iau/lists/ObsCodesF.html +.. _NASA Planetary Data System's Ring Node System: https://pds-rings.seti.org/ +.. _astropy Quantity: https://docs.astropy.org/en/stable/units/quantity.html .. _astropy table: http://docs.astropy.org/en/stable/table/index.html -.. _astropy units: http://docs.astropy.org/en/stable/units/index.html -.. _Definition of Observer Table Quantities: https://ssd.jpl.nasa.gov/horizons/manual.html#observer-table -.. _Horizons documentation: https://ssd.jpl.nasa.gov/horizons/manual.html#observer-table \ No newline at end of file +.. _astropy units: http://docs.astropy.org/en/stable/units/index.html \ No newline at end of file From 44dc8f79c921333e4f98d5bd7658e4eab88f408a Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Fri, 13 May 2022 14:51:50 -0700 Subject: [PATCH 10/25] small fixes as suggested by mkelley, and trying to fix docs --- astroquery/solarsystem/pds/core.py | 6 +- .../solarsystem/pds/tests/test_ringnode.py | 14 +- .../pds/tests/test_ringnode_remote.py | 2 +- coverage.xml | 18154 ---------------- docs/solarsystem/pds/pds.rst | 8 +- docs/solarsystem/solarsystem.rst | 1 + 6 files changed, 16 insertions(+), 18169 deletions(-) delete mode 100644 coverage.xml diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 1bf3fe2989..f44ef9f686 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -43,7 +43,7 @@ def __str__(self): Examples -------- >>> from astroquery.solarsystem.pds import RingNode - >>> nodeobj = RingNode() + >>> nodeobj = RingNode >>> print(nodeobj) # doctest: +SKIP PDSRingNode instance """ @@ -68,7 +68,7 @@ def ephemeris_async( ---------- self : `~RingNodeClass` instance planet : str, required. one of Mars, Jupiter, Saturn, Uranus, Neptune, or Pluto - obs_time : `~astropy.Time` object, or str in format YYYY-MM-DD hh:mm, optional. + obs_time : `~astropy.time.Time` object, or str in format YYYY-MM-DD hh:mm, optional. If str is provided then UTC is assumed. If no obs_time is provided, the current time is used. location : array-like, or `~astropy.coordinates.EarthLocation`, optional @@ -99,7 +99,7 @@ def ephemeris_async( Examples -------- >>> from astroquery.solarsystem.pds import RingNode - >>> systemtable, bodytable, ringtable = RingNode().ephemeris(planet='Uranus', + >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Uranus', ... obs_time='2024-05-08 22:39', ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +SKIP >>> print(ringtable) # doctest: +SKIP diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index 6cf8153d9b..5f171874d7 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -44,7 +44,7 @@ def patch_request(request): def test_ephemeris_query_Uranus(patch_request): - systemtable, bodytable, ringtable = pds.RingNode().ephemeris( + systemtable, bodytable, ringtable = pds.RingNode.ephemeris( planet="Uranus", obs_time="2022-05-03 00:00", location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), @@ -118,7 +118,7 @@ def test_ephemeris_query_Uranus(patch_request): def test_ephemeris_query_Pluto(patch_request): - systemtable, bodytable, ringtable = pds.RingNode().ephemeris( + systemtable, bodytable, ringtable = pds.RingNode.ephemeris( planet="Pluto", obs_time="2021-10-07 07:25", ) @@ -190,7 +190,7 @@ def test_ephemeris_query_Pluto(patch_request): def test_ephemeris_query_payload(): - res = pds.RingNode().ephemeris( + res = pds.RingNode.ephemeris( planet="Neptune", obs_time="2022-05-03 00:00", neptune_arcmodel=1, @@ -252,20 +252,20 @@ def test_ephemeris_query_payload(): def test_bad_query_exception_throw(): with pytest.raises(ValueError): - pds.RingNode().ephemeris(planet="Mercury", obs_time="2022-05-03 00:00") + pds.RingNode.ephemeris(planet="Mercury", obs_time="2022-05-03 00:00") with pytest.raises(ValueError): - pds.RingNode().ephemeris(planet="Uranus", obs_time="2022-13-03 00:00") + pds.RingNode.ephemeris(planet="Uranus", obs_time="2022-13-03 00:00") with pytest.raises(ValueError): - pds.RingNode().ephemeris( + pds.RingNode.ephemeris( planet="Neptune", obs_time="2022-05-03 00:00", location=(10.0 * u.deg, -120.355 * u.deg), ) with pytest.raises(ValueError): - pds.RingNode().ephemeris( + pds.RingNode.ephemeris( planet="Neptune", obs_time="2022-05-03 00:00", location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), diff --git a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py index fce9f12122..2aed624341 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py @@ -18,7 +18,7 @@ class TestRingNodeClass: def test_ephemeris_query(self): - systemtable, bodytable, ringtable = pds.RingNode().ephemeris( + systemtable, bodytable, ringtable = pds.RingNode.ephemeris( planet="Uranus", obs_time="2022-05-03 00:00", location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), diff --git a/coverage.xml b/coverage.xml deleted file mode 100644 index 2f591e642e..0000000000 --- a/coverage.xml +++ /dev/null @@ -1,18154 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index e38081dbb3..b6e9c49769 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -23,7 +23,7 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: .. code-block:: python >>> from astroquery.solarsystem.pds import RingNode - >>> systemtable, bodytable, ringtable = RingNode().ephemeris(planet='Uranus', + >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Uranus', ... obs_time='2024-05-08 22:39', ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +SKIP >>> print(ringtable) @@ -76,7 +76,7 @@ Note that the behavior of ``ringtable`` changes depending on the planet you quer .. code-block:: python - >>> systemtable, bodytable, ringtable = RingNode().ephemeris(planet='Neptune') + >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Neptune') >>> print(ringtable) ring min_angle max_angle deg deg @@ -101,7 +101,7 @@ understand issues with your query, or you can process the results differently. The query URI (the URI is what you would put into the URL field of your web browser) that is used to request the data from the Planetary Ring Node server can be obtained from the -:class:`~astroquery.solarsystem.pds.RingNode` object after a query has been +:class:`~astroquery.solarsystem.pds.RingNodeClass` object after a query has been performed (before the query only ``None`` would be returned): >>> print(obj.uri) @@ -109,7 +109,7 @@ performed (before the query only ``None`` would be returned): If your query failed, it might be useful for you to put the URI into a web browser to get more information why it failed. Please note that ``uri`` is an -attribute of :class:`~astroquery.solarsystem.pds.RingNode` and not the results +attribute of :class:`~astroquery.solarsystem.pds.RingNodeClass` and not the results table. diff --git a/docs/solarsystem/solarsystem.rst b/docs/solarsystem/solarsystem.rst index 013f8e6139..6730328c14 100644 --- a/docs/solarsystem/solarsystem.rst +++ b/docs/solarsystem/solarsystem.rst @@ -19,6 +19,7 @@ The currently available service providers and services are: imcce/imcce.rst jpl/jpl.rst mpc/mpc.rst + pds/pds.rst Reference/API ============= From d6e7edfc8ee183fec8d596d993eed0b44fbfe517 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Fri, 20 May 2022 10:21:20 -0400 Subject: [PATCH 11/25] tests for Saturn, Neptune special cases --- astroquery/solarsystem/pds/core.py | 6 +- .../pds/tests/data/neptune_ephemeris.html | 93 ++++++++++++++ .../pds/tests/data/saturn_ephemeris.html | 121 ++++++++++++++++++ .../solarsystem/pds/tests/test_ringnode.py | 55 ++++++++ 4 files changed, 272 insertions(+), 3 deletions(-) create mode 100644 astroquery/solarsystem/pds/tests/data/neptune_ephemeris.html create mode 100644 astroquery/solarsystem/pds/tests/data/saturn_ephemeris.html diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index f44ef9f686..98e804401a 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -415,7 +415,7 @@ def _parse_ringnode(self, src): peri = float(re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()")) elif "F Ring ascending node" in l[0]: ascn = float(l[1].strip(", \n")) - ringtable = table.Table( + ringtable = table.QTable( [["F"], [peri], [ascn]], names=("ring", "pericenter", "ascending node"), units=(None, u.deg, u.deg), @@ -432,13 +432,13 @@ def _parse_ringnode(self, src): for s in re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()").split() ] if i == 0: - ringtable = table.Table( + ringtable = table.QTable( [[ring], [min_angle], [max_angle]], names=("ring", "min_angle", "max_angle"), units=(None, u.deg, u.deg), ) else: - ringtable.add_row([ring, min_angle, max_angle]) + ringtable.add_row([ring, min_angle*u.deg, max_angle*u.deg]) else: pass diff --git a/astroquery/solarsystem/pds/tests/data/neptune_ephemeris.html b/astroquery/solarsystem/pds/tests/data/neptune_ephemeris.html new file mode 100644 index 0000000000..74f3b2894a --- /dev/null +++ b/astroquery/solarsystem/pds/tests/data/neptune_ephemeris.html @@ -0,0 +1,93 @@ + + +Neptune Viewer 3.0 Results + +

Neptune Viewer 3.0 Results

+

+

+Input Parameters
+----------------
+ 
+Observation time: 2021-10-07 07:25
+       Ephemeris: NEP081 + NEP095 + DE440
+   Field of view: 10 (seconds of arc)
+  Diagram center: Neptune
+       Viewpoint: Earth's center
+    Moon selection: All inner moons (N1-N8,N14)
+  Ring selection: LeVerrier, Adams
+       Arc model: #2 (820.1118 deg/day)
+  Standard stars: No
+ Additional star: No
+    Other bodies: None
+           Title: 
+     Moon labels: Small (6 points)
+Moon enlargement: 0 (points)
+     Blank disks: No
+      Arc weight: 4 (points)
+ Prime meridians: Yes
+ 
+ 
+Field of View Description (J2000)
+---------------------------------
+ 
+     Body          RA                 Dec                 RA (deg)    Dec (deg)    dRA (")     dDec (")
+ 899 Neptune       23h 28m 17.7510s    -4d 41m 40.978s    352.073962   -4.694716      0.000      0.000
+ 801 Triton        23h 28m 17.0094s    -4d 41m 52.414s    352.070872   -4.697893    -11.087    -11.436
+ 802 Nereid        23h 28m 42.2572s    -4d 38m 35.593s    352.176072   -4.643220    366.360    185.385
+ 803 Naiad         23h 28m 17.8057s    -4d 41m 41.639s    352.074190   -4.694900      0.818     -0.661
+ 804 Thalassa      23h 28m 17.6286s    -4d 41m 42.501s    352.073452   -4.695139     -1.830     -1.523
+ 805 Despina       23h 28m 17.8727s    -4d 41m 40.367s    352.074470   -4.694546      1.820      0.611
+ 806 Galatea       23h 28m 17.8035s    -4d 41m 41.811s    352.074181   -4.694948      0.785     -0.833
+ 807 Larissa       23h 28m 17.5622s    -4d 41m 42.983s    352.073176   -4.695273     -2.822     -2.005
+ 808 Proteus       23h 28m 17.9990s    -4d 41m 40.206s    352.074996   -4.694502      3.708      0.772
+ 814 Hippocamp     23h 28m 17.7896s    -4d 41m 42.997s    352.074123   -4.695277      0.577     -2.018
+ 
+                   Sub-Observer         Sub-Solar     
+     Body          Lon(degW) Lat(deg)   Lon(degW) Lat(deg)   Phase(deg)   Distance(10^6 km)
+ 899 Neptune       119.256   -22.782    118.466   -22.610      0.75134    4338.354329
+ 801 Triton        243.335   -35.865    242.578   -35.437      0.75148    4338.237612
+ 802 Nereid        100.118     3.726     99.371     3.746      0.74807    4336.800925
+ 803 Naiad         199.350   -26.858    198.530   -26.720      0.75133    4338.397183
+ 804 Thalassa      236.105   -22.976    235.314   -22.804      0.75136    4338.355604
+ 805 Despina        59.009   -22.769     58.222   -22.594      0.75131    4338.387909
+ 806 Galatea       343.647   -22.825    342.861   -22.651      0.75132    4338.411414
+ 807 Larissa       252.931   -22.983    252.139   -22.813      0.75137    4338.343394
+ 808 Proteus        34.218   -23.003     33.431   -22.822      0.75129    4338.440907
+ 814 Hippocamp     287.004   -22.973    286.217   -22.798      0.75132    4338.449959
+ 
+   Ring sub-solar latitude (deg): -22.61024  (-22.61915  to -22.60134)
+  Ring plane opening angle (deg): -22.78248  (lit)
+   Ring center phase angle (deg):   0.75134
+       Sub-solar longitude (deg): 149.96998  from ring plane ascending node
+    Sub-observer longitude (deg): 149.17958
+
+        Sun-planet distance (AU):  29.92193
+   Observer-planet distance (AU):  29.00011
+        Sun-planet distance (km):  4476.256293 x 10^6
+   Observer-planet distance (km):  4338.354329 x 10^6
+         Light travel time (sec): 14471.192364
+
+  
+       Courage longitude (deg):  63.81977  to  64.81977  from ring plane ascending node
+       Liberte longitude (deg):  55.01978  to  59.11976
+     Egalite A longitude (deg):  44.21976  to  45.21976
+     Egalite B longitude (deg):  40.41978  to  43.41978
+    Fraternite longitude (deg):  26.41978  to  36.01978
+
+
Preview:

+
+Click +here +to download diagram (PDF, 17055 bytes).

+Click +here +to download diagram (JPEG format, 164693 bytes).

+Click +here +to download diagram (PostScript format, 42562 bytes). +


+Neptune Viewer Form | +RMS Node Tools | +Ring-Moon Systems Home + + diff --git a/astroquery/solarsystem/pds/tests/data/saturn_ephemeris.html b/astroquery/solarsystem/pds/tests/data/saturn_ephemeris.html new file mode 100644 index 0000000000..74f67b2f4f --- /dev/null +++ b/astroquery/solarsystem/pds/tests/data/saturn_ephemeris.html @@ -0,0 +1,121 @@ + + +Saturn Viewer 3.0 Results + +

Saturn Viewer 3.0 Results

+

+

+Input Parameters
+----------------
+ 
+  Observation time: 2021-10-07 07:25
+         Ephemeris: SAT389 + SAT393 + SAT427 + DE440
+     Field of view: 10 (Saturn radii)
+    Diagram center: Saturn
+         Viewpoint: Earth's center
+    Moon selection: All inner moons (S1-S18,S32-S35,S49,S53)
+    Ring selection: A,B,C,F,G,E
+    Standard stars: No
+   Additional star: No
+      Other bodies: None
+             Title: 
+       Moon labels: Small (6 points)
+  Moon enlargement: 0 (points)
+       Blank disks: No
+    Ring plot type: Transparent
+Pericenter markers: None
+       Marker size: 4 (points)
+   Prime meridians: Yes
+ 
+ 
+Field of View Description (J2000)
+---------------------------------
+ 
+     Body          RA                 Dec                 RA (deg)    Dec (deg)    dRA (")     dDec (")
+ 699 Saturn        20h 36m 46.6876s   -19d 25m 00.415s    309.194532  -19.416782      0.000      0.000
+ 601 Mimas         20h 36m 48.5158s   -19d 25m 05.162s    309.202149  -19.418101     25.864     -4.747
+ 602 Enceladus     20h 36m 44.5496s   -19d 24m 51.656s    309.185623  -19.414349    -30.246      8.759
+ 603 Tethys        20h 36m 49.1034s   -19d 24m 54.793s    309.204598  -19.415220     34.177      5.623
+ 604 Dione         20h 36m 43.8042s   -19d 25m 08.188s    309.182518  -19.418941    -40.791     -7.772
+ 605 Rhea          20h 36m 49.3735s   -19d 25m 26.733s    309.205723  -19.424092     37.997    -26.318
+ 606 Titan         20h 36m 52.7285s   -19d 24m 20.454s    309.219702  -19.405682     85.460     39.961
+ 607 Hyperion      20h 36m 37.9308s   -19d 23m 59.499s    309.158045  -19.399861   -123.882     60.917
+ 608 Iapetus       20h 36m 35.8802s   -19d 24m 17.459s    309.149501  -19.404850   -152.891     42.956
+ 609 Phoebe        20h 39m 09.0594s   -19d 18m 51.608s    309.787748  -19.314336   2014.117    368.807
+ 610 Janus         20h 36m 47.0356s   -19d 25m 08.132s    309.195982  -19.418926      4.923     -7.717
+ 611 Epimetheus    20h 36m 47.6785s   -19d 25m 07.559s    309.198660  -19.418766     14.018     -7.143
+ 612 Helene        20h 36m 43.0674s   -19d 24m 49.232s    309.179448  -19.413676    -51.214     11.183
+ 613 Telesto       20h 36m 49.4320s   -19d 25m 10.566s    309.205966  -19.419602     38.824    -10.151
+ 614 Calypso       20h 36m 46.3698s   -19d 24m 45.877s    309.193208  -19.412743     -4.495     14.539
+ 615 Atlas         20h 36m 45.7895s   -19d 24m 53.916s    309.190790  -19.414977    -12.705      6.500
+ 616 Prometheus    20h 36m 45.9304s   -19d 25m 04.996s    309.191377  -19.418054    -10.712     -4.580
+ 617 Pandora       20h 36m 47.7201s   -19d 24m 57.123s    309.198834  -19.415867     14.607      3.293
+ 618 Pan           20h 36m 47.7763s   -19d 25m 05.950s    309.199068  -19.418319     15.402     -5.534
+ 632 Methone       20h 36m 47.8837s   -19d 25m 09.747s    309.199515  -19.419374     16.921     -9.332
+ 633 Pallene       20h 36m 48.8120s   -19d 25m 05.768s    309.203383  -19.418269     30.054     -5.352
+ 634 Polydeuces    20h 36m 48.6362s   -19d 25m 18.814s    309.202651  -19.421893     27.567    -18.398
+ 635 Daphnis       20h 36m 46.2492s   -19d 25m 06.047s    309.192705  -19.418346     -6.202     -5.632
+ 649 Anthe         20h 36m 44.6814s   -19d 24m 58.258s    309.186173  -19.416183    -28.381      2.157
+ 653 Aegaeon       20h 36m 48.3925s   -19d 25m 03.255s    309.201635  -19.417571     24.119     -2.839
+ 
+                   Sub-Observer         Sub-Solar     
+     Body          Lon(degW) Lat(deg)   Lon(degW) Lat(deg)   Phase(deg)   Distance(10^6 km)
+ 699 Saturn        210.795    19.443    205.615    17.398      5.32671    1422.644849
+ 601 Mimas          95.037    20.038     89.784    18.112      5.32655    1422.602603
+ 602 Enceladus     299.701    19.450    294.523    17.405      5.32670    1422.744310
+ 603 Tethys         52.165    19.663     46.940    17.711      5.32564    1422.817372
+ 604 Dione         227.249    19.455    222.064    17.409      5.32813    1422.398436
+ 605 Rhea          150.469    19.711    145.269    17.682      5.32782    1422.224763
+ 606 Titan          25.966    19.303     20.800    17.250      5.32194    1423.637413
+ 607 Hyperion      317.655    18.677    312.471    16.700      5.32461    1423.610877
+ 608 Iapetus       348.552     4.329    343.467     2.760      5.31647    1426.011713
+ 609 Phoebe        155.216    10.809    150.314     8.596      5.31799    1417.532721
+ 610 Janus          66.966    19.590     61.780    17.532      5.32716    1422.507868
+ 611 Epimetheus    167.130    19.801    161.935    17.750      5.32693    1422.539360
+ 612 Helene         50.542    19.469     45.369    17.408      5.32695    1422.746473
+ 613 Telesto       107.882    20.476    102.645    18.479      5.32661    1422.543474
+ 614 Calypso         2.436    19.096    357.210    17.191      5.32573    1422.920167
+ 615 Atlas         203.028    19.441    197.852    17.397      5.32650    1422.741315
+ 616 Prometheus    258.847    19.444    253.660    17.399      5.32727    1422.531082
+ 617 Pandora        54.959    19.442     49.784    17.395      5.32616    1422.742504
+ 618 Pan           304.258    19.446    299.076    17.396      5.32679    1422.573404
+ 632 Methone       129.442    19.445    124.257    17.398      5.32703    1422.503412
+ 633 Pallene        99.503    19.444     94.323    17.396      5.32646    1422.612979
+ 634 Polydeuces    155.947    19.447    150.763    17.400      5.32746    1422.353842
+ 635 Daphnis       304.258    19.444    299.071    17.399      5.32725    1422.521175
+ 649 Anthe         139.723    19.451    134.541    17.406      5.32715    1422.620381
+ 653 Aegaeon        99.504    19.444     94.325    17.396      5.32641    1422.646000
+ 
+   Ring sub-solar latitude (deg):  17.39754  ( 17.37071  to  17.42436)
+  Ring plane opening angle (deg):  19.44270  (lit)
+   Ring center phase angle (deg):   5.32671
+       Sub-solar longitude (deg):   6.08926  from ring plane ascending node
+    Sub-observer longitude (deg):   0.90868
+
+        Sun-planet distance (AU):   9.93730
+   Observer-planet distance (AU):   9.50979
+        Sun-planet distance (km):  1486.598500 x 10^6
+   Observer-planet distance (km):  1422.644849 x 10^6
+         Light travel time (sec):  4745.432417
+
+  
+       F Ring pericenter (deg): 249.23097  from ring plane ascending node
+   F Ring ascending node (deg): 250.34081
+
+
Preview:

+
+Click +here +to download diagram (PDF, 24266 bytes).

+Click +here +to download diagram (JPEG format, 198407 bytes).

+Click +here +to download diagram (PostScript format, 85890 bytes). +


+Saturn Viewer Form | +RMS Node Tools | +Ring-Moon Systems Home + + diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index 5f171874d7..e13bc1a052 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -12,6 +12,8 @@ # files in data/ for different planets DATA_FILES = {'Uranus': 'uranus_ephemeris.html', 'Pluto': 'pluto_ephemeris.html', + 'Neptune': 'neptune_ephemeris.html', + 'Saturn': 'saturn_ephemeris.html', } @@ -189,6 +191,59 @@ def test_ephemeris_query_Pluto(patch_request): assert ringtable is None +def test_ephemeris_query_Neptune(patch_request): + '''Verify that the Neptune ring arcs are queried properly''' + + systemtable, bodytable, ringtable = pds.RingNode.ephemeris( + planet="Neptune", + obs_time="2021-10-07 07:25", + neptune_arcmodel=2 + ) + + print(ringtable[ringtable.loc_indices["Courage"]]) + + assert_quantity_allclose( + [63.81977, + 55.01978, + 44.21976, + 40.41978, + 26.41978, + 64.81977, + 59.11976, + 45.21976, + 43.41978, + 36.01978], + [ringtable[ringtable.loc_indices["Courage"]]["min_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Liberte"]]["min_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Egalite A"]]["min_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Egalite B"]]["min_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Fraternite"]]["min_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Courage"]]["max_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Liberte"]]["max_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Egalite A"]]["max_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Egalite B"]]["max_angle"].to(u.deg).value, + ringtable[ringtable.loc_indices["Fraternite"]]["max_angle"].to(u.deg).value, + ], rtol=1e-3) + + +def test_ephemeris_query_Saturn(patch_request): + '''Check Saturn F ring is queried properly''' + systemtable, bodytable, ringtable = pds.RingNode.ephemeris( + planet="Saturn", + obs_time="2021-10-07 07:25", + ) + print(ringtable) + + assert_quantity_allclose( + [249.23097, + 250.34081 + ], + [ + ringtable[ringtable.loc_indices["F"]]["pericenter"].to(u.deg).value, + ringtable[ringtable.loc_indices["F"]]["ascending node"].to(u.deg).value + ], rtol=1e-3) + + def test_ephemeris_query_payload(): res = pds.RingNode.ephemeris( planet="Neptune", From eb6a1790e9836b3e8bbc4acd3f8e72c1367f1e73 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Tue, 24 May 2022 13:24:19 -0700 Subject: [PATCH 12/25] made (hopefully) all changes from code review --- .gitignore | 1 + astroquery/solarsystem/pds/core.py | 249 +++++------------- .../solarsystem/pds/tests/test_ringnode.py | 37 +-- .../pds/tests/test_ringnode_remote.py | 2 +- docs/solarsystem/pds/pds.rst | 64 ++--- 5 files changed, 89 insertions(+), 264 deletions(-) diff --git a/.gitignore b/.gitignore index c614a2d241..b8e4fd0e05 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ MANIFEST pip-wheel-metadata .hypothesis doctests.py +coverage.xml # Sphinx _build diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 98e804401a..d24081dbbe 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -11,6 +11,7 @@ import astropy.units as u from astropy.coordinates import EarthLocation, Angle from bs4 import BeautifulSoup +from astroquery import log # 3. local imports - use relative imports # all Query classes should inherit from BaseQuery. @@ -35,6 +36,8 @@ def __init__(self): Instantiate Planetary Ring Node query ''' super().__init__() + self.URL = conf.pds_server + self.planet_defaults = conf.planet_defaults def __str__(self): """ @@ -49,28 +52,17 @@ def __str__(self): """ return "PDSRingNode instance" - # --- pretty stuff above this line, get it working below this line --- - - def ephemeris_async( - self, - planet, - obs_time=None, - location=None, - neptune_arcmodel=3, - get_query_payload=False, - get_raw_response=False, - cache=True, - ): + def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel=3, + get_query_payload=False, get_raw_response=False, cache=True): """ send query to Planetary Ring Node server Parameters ---------- - self : `~RingNodeClass` instance planet : str, required. one of Mars, Jupiter, Saturn, Uranus, Neptune, or Pluto - obs_time : `~astropy.time.Time` object, or str in format YYYY-MM-DD hh:mm, optional. + epoch : `~astropy.time.Time` object, or str in format YYYY-MM-DD hh:mm, optional. If str is provided then UTC is assumed. - If no obs_time is provided, the current time is used. + If no epoch is provided, the current time is used. location : array-like, or `~astropy.coordinates.EarthLocation`, optional Observer's location as a 3-element array of Earth longitude, latitude, altitude, or @@ -85,9 +77,6 @@ def ephemeris_async( get_query_payload : boolean, optional When set to `True` the method returns the HTTP request parameters as a dict, default: False - get_raw_response : boolean, optional - Return raw data as obtained by the Planetary Ring Node without parsing the data - into a table, default: False Returns @@ -99,10 +88,11 @@ def ephemeris_async( Examples -------- >>> from astroquery.solarsystem.pds import RingNode + >>> import astropy.units as u >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Uranus', - ... obs_time='2024-05-08 22:39', - ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +SKIP - >>> print(ringtable) # doctest: +SKIP + ... epoch='2024-05-08 22:39', + ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA + >>> print(ringtable) # doctest: +REMOTE_DATA ring pericenter ascending node deg deg ------- ---------- -------------- @@ -118,62 +108,26 @@ def ephemeris_async( Epsilon 298.022 0.0 """ - URL = conf.pds_server - # URL = 'https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?' + planet = planet.lower() + if planet not in ["mars", "jupiter", "saturn", "uranus", "neptune", "pluto", ]: + raise ValueError( + "illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto')" + ) - # check inputs and set defaults for optional inputs - if planet is None: - raise ValueError("'planet' parameter not set. Query aborted.") - else: - planet = planet.lower() - if planet not in [ - "mars", - "jupiter", - "saturn", - "uranus", - "neptune", - "pluto", - ]: - raise ValueError( - "illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto')" - ) - - if obs_time is None: - obs_time = Time.now().strftime("%Y-%m-%d %H:%M") - warnings.warn("obs_time not set. using current time instead.") - elif type(obs_time) == str: - try: - Time.strptime(obs_time, "%Y-%m-%d %H:%M").jd - except Exception as ex: - raise ValueError( - "illegal value for 'obs_time' parameter. string must have format 'yyyy-mm-dd hh:mm'" - ) - elif type(obs_time) == Time: - try: - obs_time = obs_time.utc.to_value("iso", subfmt="date_hm") - except Exception as ex: - raise ValueError( - "illegal value for 'obs_time' parameter. could not parse astropy.time.core.Time object into format 'yyyy-mm-dd hh:mm' (UTC)" - ) + if isinstance(epoch, (int, float)): + epoch = Time(epoch, format='jd') + elif isinstance(epoch, str): + epoch = Time(epoch, format='iso') + elif epoch is None: + epoch = Time.now() + log.warning("Observation time not set. Using current time.") if location is None: viewpoint = "observatory" latitude, longitude, altitude = "", "", "" - print("Observatory coordinates not set. Using center of Earth.") + log.warning("Observatory coordinates not set. Using center of Earth.") else: viewpoint = "latlon" - if type(location) != EarthLocation: - if hasattr(location, "__iter__"): - if len(location) != 3: - raise ValueError( - "location arrays require three values:" - " longitude, latitude, and altitude" - ) - else: - raise TypeError( - "location must be array-like or astropy EarthLocation" - ) - if isinstance(location, EarthLocation): loc = location.geodetic longitude = loc[0].deg @@ -193,38 +147,29 @@ def ephemeris_async( # thankfully, adding extra planet-specific keywords here does not break query for other planets request_payload = OrderedDict( [ - ("abbrev", planet.lower()[:3]), - ("ephem", conf.planet_defaults[planet]["ephem"],), - ("time", obs_time), # UTC. this should be enforced when checking inputs - ( - "fov", - 10, - ), # next few are figure options, can be hardcoded and ignored + ("abbrev", planet[:3]), + ("ephem", self.planet_defaults[planet]["ephem"]), + ("time", epoch.utc.to_value("iso", subfmt="date_hm")), # UTC. this should be enforced when checking inputs + ("fov", 10), # next few are figure options, can be hardcoded and ignored ("fov_unit", planet.capitalize() + " radii"), ("center", "body"), ("center_body", planet.capitalize()), - ("center_ansa", conf.planet_defaults[planet]["center_ansa"]), + ("center_ansa", self.planet_defaults[planet]["center_ansa"]), ("center_ew", "east"), ("center_ra", ""), ("center_ra_type", "hours"), ("center_dec", ""), ("center_star", ""), ("viewpoint", viewpoint), - ( - "observatory", - "Earth's center", - ), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories + ("observatory", "Earth's center"), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories ("latitude", latitude), ("longitude", longitude), ("lon_dir", "east"), ("altitude", altitude), - ("moons", conf.planet_defaults[planet]["moons"]), - ("rings", conf.planet_defaults[planet]["rings"]), + ("moons", self.planet_defaults[planet]["moons"]), + ("rings", self.planet_defaults[planet]["rings"]), ("arcmodel", conf.neptune_arcmodels[int(neptune_arcmodel)]), - ( - "extra_ra", - "", - ), # figure options below this line, can all be hardcoded and ignored + ("extra_ra", ""), # figure options below this line, can all be hardcoded and ignored ("extra_ra_type", "hours"), ("extra_dec", ""), ("extra_name", ""), @@ -245,26 +190,21 @@ def ephemeris_async( if get_query_payload: return request_payload - # set return_raw flag, if raw response desired - if get_raw_response: - self.return_raw = True - # query and parse response = self._request( - "GET", URL, params=request_payload, timeout=self.TIMEOUT, cache=cache + "GET", self.URL, params=request_payload, timeout=self.TIMEOUT, cache=cache ) - self.uri = response.url return response - def _parse_ringnode(self, src): + def _parse_result(self, response, verbose=None): """ Routine for parsing data from ring node Parameters ---------- self : RingNodeClass instance - src : list + response : list raw response from server @@ -274,9 +214,15 @@ def _parse_ringnode(self, src): bodytable : `astropy.Table` ringtable : `astropy.Table` """ + self.last_response = response + try: + self._last_query.remove_cache_file(self.cache_location) + except OSError: + # this is allowed: if `cache` was set to False, this + # won't be needed + pass - self.raw_response = src - soup = BeautifulSoup(src, "html.parser") + soup = BeautifulSoup(response.text, "html.parser") text = soup.get_text() # print(repr(text)) textgroups = re.split("\n\n|\n \n", text) @@ -284,57 +230,28 @@ def _parse_ringnode(self, src): for group in textgroups: group = group.strip(", \n") - # input parameters. only thing needed is obs_time + # input parameters. only thing needed is epoch if group.startswith("Observation"): - obs_time = group.split("\n")[0].split("e: ")[-1].strip(", \n") + epoch = group.split("\n")[0].split("e: ")[-1].strip(", \n") # minor body table part 1 elif group.startswith("Body"): group = "NAIF " + group # fixing lack of header for NAIF ID - bodytable = ascii.read( - group, - format="fixed_width", - col_starts=(0, 4, 18, 35, 54, 68, 80, 91), - col_ends=(4, 18, 35, 54, 68, 80, 91, 102), - names=( - "NAIF ID", - "Body", - "RA", - "Dec", - "RA (deg)", - "Dec (deg)", - "dRA", - "dDec", - ), - ) - units_list = [None, None, None, None, u.deg, u.deg, u.arcsec, u.arcsec] - bodytable = table.QTable(bodytable, units=units_list) - # for i in range(len(bodytable.colnames)): - # bodytable[bodytable.colnames[i]].unit = units_list[i] + bodytable = table.QTable.read(group, format="ascii.fixed_width", + col_starts=(0, 4, 18, 35, 54, 68, 80, 91), + col_ends=(4, 18, 35, 54, 68, 80, 91, 102), + names=("NAIF ID", "Body", "RA", "Dec", "RA (deg)", "Dec (deg)", "dRA", "dDec"), + units=([None, None, None, None, u.deg, u.deg, u.arcsec, u.arcsec])) + # minor body table part 2 elif group.startswith("Sub-"): group = "\n".join(group.split("\n")[1:]) # fixing two-row header group = "NAIF" + group[4:] - bodytable2 = ascii.read( - group, - format="fixed_width", - col_starts=(0, 4, 18, 28, 37, 49, 57, 71), - col_ends=(4, 18, 28, 37, 49, 57, 71, 90), - names=( - "NAIF ID", - "Body", - "sub_obs_lon", - "sub_obs_lat", - "sub_sun_lon", - "sub_sun_lat", - "phase", - "distance", - ), - ) - units_list = [None, None, u.deg, u.deg, u.deg, u.deg, u.deg, u.km * 1e6] - bodytable2 = table.QTable(bodytable2, units=units_list) - # for i in range(len(bodytable2.colnames)): - # bodytable2[bodytable2.colnames[i]].unit = units_list[i] + bodytable2 = table.QTable.read(group, format="ascii.fixed_width", + col_starts=(0, 4, 18, 28, 37, 49, 57, 71), + col_ends=(4, 18, 28, 37, 49, 57, 71, 90), + names=("NAIF ID", "Body", "sub_obs_lon", "sub_obs_lat", "sub_sun_lon", "sub_sun_lat", "phase", "distance"), + units=([None, None, u.deg, u.deg, u.deg, u.deg, u.deg, u.km * 1e6])) # ring plane data elif group.startswith("Ring s"): @@ -345,8 +262,6 @@ def _parse_ringnode(self, src): [sub_sun_lat, sub_sun_lat_min, sub_sun_lat_max] = [ float(s.strip(", \n()")) for s in re.split("\(|to", l[1]) ] - # systemtable = table.Table([[sub_sun_lat], [sub_sun_lat_max], [sub_sun_lat_min]], - # names = ('sub_sun_lat', 'sub_sun_lat_max', 'sub_sun_lat_min')) systemtable = { "sub_sun_lat": sub_sun_lat * u.deg, "sub_sun_lat_min": sub_sun_lat_min * u.deg, @@ -374,10 +289,10 @@ def _parse_ringnode(self, src): for line in lines: l = line.split(":") if "Sun-planet distance (AU)" in l[0]: - # systemtable["d_sun_AU"] = float(l[1].strip(", \n")) + # this is redundant with sun distance in km pass elif "Observer-planet distance (AU)" in l[0]: - # systemtable["d_obs_AU"] = float(l[1].strip(", \n")) + # this is redundant with observer distance in km pass elif "Sun-planet distance (km)" in l[0]: systemtable["d_sun"] = ( @@ -395,16 +310,11 @@ def _parse_ringnode(self, src): # --------- below this line, planet-specific info ------------ # Uranus individual rings data elif group.startswith("Ring "): - ringtable = ascii.read( - " " + group, - format="fixed_width", + ringtable = table.QTable.read(" " + group, format="ascii.fixed_width", col_starts=(5, 18, 29), col_ends=(18, 29, 36), names=("ring", "pericenter", "ascending node"), - ) - - units_list = [None, u.deg, u.deg] - ringtable = table.QTable(ringtable, units=units_list) + units=([None, u.deg, u.deg])) # Saturn F-ring data elif group.startswith("F Ring"): @@ -450,44 +360,9 @@ def _parse_ringnode(self, src): bodytable = table.join(bodytable, bodytable2) # concatenate minor body table bodytable.add_index("Body") - systemtable["obs_time"] = Time( - obs_time, format="iso", scale="utc" - ) # add obs time to systemtable + systemtable["epoch"] = Time(epoch, format="iso", scale="utc") # add obs time to systemtable return systemtable, bodytable, ringtable - def _parse_result(self, response, verbose=None): - """ - Routine for managing parser calls - - Parameters - ---------- - self : RingNodeClass instance - response : string - raw response from server - - Returns - ------- - systemtable : dict - bodytable : `astropy.Table` - ringtable : `astropy.Table` - """ - self.last_response = response - try: - systemtable, bodytable, ringtable = self._parse_ringnode(response.text) - except Exception as ex: - try: - self._last_query.remove_cache_file(self.cache_location) - except OSError: - # this is allowed: if `cache` was set to False, this - # won't be needed - pass - raise - return ( - systemtable, - bodytable, - ringtable, - ) - RingNode = RingNodeClass() diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index e13bc1a052..dc69fb258c 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -1,6 +1,5 @@ import pytest import os -from collections import OrderedDict import numpy as np from astropy.tests.helper import assert_quantity_allclose @@ -48,7 +47,7 @@ def test_ephemeris_query_Uranus(patch_request): systemtable, bodytable, ringtable = pds.RingNode.ephemeris( planet="Uranus", - obs_time="2022-05-03 00:00", + epoch="2022-05-03 00:00", location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), ) # check system table @@ -122,7 +121,7 @@ def test_ephemeris_query_Pluto(patch_request): systemtable, bodytable, ringtable = pds.RingNode.ephemeris( planet="Pluto", - obs_time="2021-10-07 07:25", + epoch="2021-10-07 07:25", ) print(systemtable) print(bodytable[bodytable.loc_indices["Styx"]]) @@ -196,7 +195,7 @@ def test_ephemeris_query_Neptune(patch_request): systemtable, bodytable, ringtable = pds.RingNode.ephemeris( planet="Neptune", - obs_time="2021-10-07 07:25", + epoch="2021-10-07 07:25", neptune_arcmodel=2 ) @@ -230,7 +229,7 @@ def test_ephemeris_query_Saturn(patch_request): '''Check Saturn F ring is queried properly''' systemtable, bodytable, ringtable = pds.RingNode.ephemeris( planet="Saturn", - obs_time="2021-10-07 07:25", + epoch="2021-10-07 07:25", ) print(ringtable) @@ -247,13 +246,13 @@ def test_ephemeris_query_Saturn(patch_request): def test_ephemeris_query_payload(): res = pds.RingNode.ephemeris( planet="Neptune", - obs_time="2022-05-03 00:00", + epoch="2022-05-03 00:00", neptune_arcmodel=1, location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), get_query_payload=True, ) - assert res == OrderedDict( + assert res == dict( [ ("abbrev", "nep"), ("ephem", "000 NEP081 + NEP095 + DE440"), @@ -302,27 +301,3 @@ def test_ephemeris_query_payload(): ("output", "html"), ] ) - - -def test_bad_query_exception_throw(): - - with pytest.raises(ValueError): - pds.RingNode.ephemeris(planet="Mercury", obs_time="2022-05-03 00:00") - - with pytest.raises(ValueError): - pds.RingNode.ephemeris(planet="Uranus", obs_time="2022-13-03 00:00") - - with pytest.raises(ValueError): - pds.RingNode.ephemeris( - planet="Neptune", - obs_time="2022-05-03 00:00", - location=(10.0 * u.deg, -120.355 * u.deg), - ) - - with pytest.raises(ValueError): - pds.RingNode.ephemeris( - planet="Neptune", - obs_time="2022-05-03 00:00", - location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), - neptune_arcmodel=0, - ) diff --git a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py index 2aed624341..b683a3c64a 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py @@ -20,7 +20,7 @@ def test_ephemeris_query(self): systemtable, bodytable, ringtable = pds.RingNode.ephemeris( planet="Uranus", - obs_time="2022-05-03 00:00", + epoch="2022-05-03 00:00", location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), ) # check system table diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index b6e9c49769..eee748b5b5 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -1,5 +1,3 @@ -.. doctest-skip-all - .. _astroquery.solarsystem.pds: *********************************************************************************** @@ -10,7 +8,7 @@ Overview ======== -The :class:`~astroquery.solarsystem.pds.RingNodeClass` class provides an +The :class:`~astroquery.solarsystem.pds.RingNodeClass` provides an interface to the ephemeris tools provided by the `NASA Planetary Data System's Ring Node System`_ hosted by SETI institute. Ephemeris @@ -23,9 +21,10 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: .. code-block:: python >>> from astroquery.solarsystem.pds import RingNode + >>> import astropy.units as u >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Uranus', - ... obs_time='2024-05-08 22:39', - ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +SKIP + ... epoch='2024-05-08 22:39', + ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA >>> print(ringtable) ring pericenter ascending node deg deg @@ -43,29 +42,29 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: ``planet`` must be one of ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto'] (case-insensitive) -``obs_time`` is the datetime to query. Accepts a string in format 'YYYY-MM-DD HH:MM' (UTC assumed), or an ```astropy.Time`` object. If no obs_time is provided, the current time is used. +``epoch`` is the datetime to query. Accepts a string in format 'YYYY-MM-DD HH:MM' (UTC assumed), or a `~astropy.time.Time` object. If no epoch is provided, the current time is used. -``location`` is the observer's location. Accepts an ``astropy.coordinates.EarthLocation``, or any 3-element array-like (e.g. list, tuple) of format (latitude, longitude, elevation). Longitude and latitude should be anything that initializes an ``astropy.coordinates.Angle`` object, and altitude should initialize an ``astropy.units.Quantity`` object (with units of length). If ``None``, then the geocenter is used. +``location`` is the observer's location. Accepts an `~astropy.coordinates.EarthLocation`, or any 3-element array-like (e.g. list, tuple) of format (latitude, longitude, elevation). Longitude and latitude should be anything that initializes an `~astropy.coordinates.Angle` object, and altitude should initialize a `~astropy.units.Quantity` object (with units of length). If ``None``, then the geocenter is used. ``neptune_arcmodel`` is the choice of which ephemeris to assume for Neptune's ring arcs. accepts a float. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details). default 3. has no effect if planet != 'Neptune' Outputs --------- -``systemtable`` is a dictionary containing system-wide ephemeris data. Every value is an `astropy Quantity`_ object. We can get a list of all the keys in this dictionary with: +``systemtable`` is a dict containing system-wide ephemeris data. Every value is a `~astropy.units.Quantity`. We can get a list of all the keys in this dictionary with: .. code-block:: python >>> print(systemtable.keys()) - dict_keys(['sub_sun_lat', 'sub_sun_lat_min', 'sub_sun_lat_max', 'opening_angle', 'phase_angle', 'sub_sun_lon', 'sub_obs_lon', 'd_sun', 'd_obs', 'light_time', 'obs_time']) + dict_keys(['sub_sun_lat', 'sub_sun_lat_min', 'sub_sun_lat_max', 'opening_angle', 'phase_angle', 'sub_sun_lon', 'sub_obs_lon', 'd_sun', 'd_obs', 'light_time', 'epoch']) -``bodytable`` is an `astropy table`_ containing ephemeris information on the moons in the planetary system. Every column is assigned a unit from `astropy units`_. We can get a list of all the columns in this table with: +``bodytable`` is a `~astropy.table.QTable` containing ephemeris information on the moons in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: .. code-block:: python >>> print(bodytable.columns) -``ringtable`` is an `astropy table`_ containing ephemeris information on the individual rings in the planetary system. Every column is assigned a unit from `astropy units`_. We can get a list of all the columns in this table with: +``ringtable`` is a `~astropy.table.QTable` containing ephemeris information on the individual rings in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: .. code-block:: python @@ -76,41 +75,16 @@ Note that the behavior of ``ringtable`` changes depending on the planet you quer .. code-block:: python - >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Neptune') + >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Neptune', epoch='2022-05-24 00:00') # doctest: +REMOTE_DATA >>> print(ringtable) - ring min_angle max_angle - deg deg - ---------- --------- --------- - Courage 46.39438 47.39438 - Liberte 37.59439 41.69437 - Egalite A 26.79437 27.79437 - Egalite B 22.99439 25.99439 - Fraternite 8.99439 18.59439 - - - -Hints and Tricks -================ - -Checking the original RingNode output ------------------------------------------ - -Once the query method has been called, the retrieved raw response is -stored in the attribute ``raw_response``. Inspecting this response can help to -understand issues with your query, or you can process the results differently. - -The query URI (the URI is what you would put into the URL -field of your web browser) that is used to request the data from the Planetary Ring Node server can be obtained from the -:class:`~astroquery.solarsystem.pds.RingNodeClass` object after a query has been -performed (before the query only ``None`` would be returned): - - >>> print(obj.uri) - https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?abbrev=ura&ephem=000+URA111+%2B+URA115+%2B+DE440&time=2024-05-08+22%3A39&fov=10&fov_unit=Uranus+radii¢er=body¢er_body=Uranus¢er_ansa=Epsilon¢er_ew=east¢er_ra=¢er_ra_type=hours¢er_dec=¢er_star=&viewpoint=latlon&observatory=Earth%27s+center&latitude=-67.75499999999998&longitude=-23.028999999999996&lon_dir=east&altitude=4999.999999999843&moons=727+All+inner+moons+%28U1-U15%2CU25-U27%29&rings=All+rings&arcmodel=%233+%28820.1121+deg%2Fday%29&extra_ra=&extra_ra_type=hours&extra_dec=&extra_name=&title=&labels=Small+%286+points%29&moonpts=0&blank=No&opacity=Transparent&peris=None&peripts=4&arcpts=4&meridians=Yes&output=html - -If your query failed, it might be useful for you to put the URI into a web -browser to get more information why it failed. Please note that ``uri`` is an -attribute of :class:`~astroquery.solarsystem.pds.RingNodeClass` and not the results -table. + ring min_angle max_angle + deg deg + ---------- --------- --------- + Courage 53.4818 54.4818 + Liberte 44.68181 48.78178 + Egalite A 33.88179 34.88179 + Egalite B 30.0818 33.0818 + Fraternite 16.0818 25.68181 Acknowledgements From b8845133ce9ebbaf05d45d382bb287e58e1e4bf6 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Wed, 25 May 2022 11:09:12 -0700 Subject: [PATCH 13/25] made all changes suggested by @eerovaher --- astroquery/solarsystem/pds/__init__.py | 2 +- astroquery/solarsystem/pds/core.py | 48 +++++++------------ .../solarsystem/pds/tests/test_ringnode.py | 41 +++++++++------- .../pds/tests/test_ringnode_remote.py | 18 ++----- docs/solarsystem/pds/pds.rst | 32 ++++++++----- 5 files changed, 67 insertions(+), 74 deletions(-) diff --git a/astroquery/solarsystem/pds/__init__.py b/astroquery/solarsystem/pds/__init__.py index 7e46473cf9..d452ddd2bf 100644 --- a/astroquery/solarsystem/pds/__init__.py +++ b/astroquery/solarsystem/pds/__init__.py @@ -15,7 +15,7 @@ class Conf(_config.ConfigNamespace): # server settings pds_server = _config.ConfigItem( - ["https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?"], "Ring Node" + "https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?", "Ring Node" ) # implement later: other pds tools diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index d24081dbbe..4ae0c0075e 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -1,6 +1,5 @@ # 1. standard library imports -from collections import OrderedDict import re import warnings @@ -11,7 +10,6 @@ import astropy.units as u from astropy.coordinates import EarthLocation, Angle from bs4 import BeautifulSoup -from astroquery import log # 3. local imports - use relative imports # all Query classes should inherit from BaseQuery. @@ -29,27 +27,17 @@ class RingNodeClass(BaseQuery): """ - TIMEOUT = conf.timeout - - def __init__(self): + def __init__(self, timeout=None): ''' Instantiate Planetary Ring Node query ''' super().__init__() - self.URL = conf.pds_server + self.url = conf.pds_server + self.timeout = conf.timeout self.planet_defaults = conf.planet_defaults def __str__(self): - """ - String representation of `~RingNodeClass` object instance - Examples - -------- - >>> from astroquery.solarsystem.pds import RingNode - >>> nodeobj = RingNode - >>> print(nodeobj) # doctest: +SKIP - PDSRingNode instance - """ return "PDSRingNode instance" def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel=3, @@ -89,7 +77,7 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel -------- >>> from astroquery.solarsystem.pds import RingNode >>> import astropy.units as u - >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Uranus', + >>> bodytable, ringtable = RingNode.ephemeris(planet='Uranus', ... epoch='2024-05-08 22:39', ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA >>> print(ringtable) # doctest: +REMOTE_DATA @@ -120,12 +108,10 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel epoch = Time(epoch, format='iso') elif epoch is None: epoch = Time.now() - log.warning("Observation time not set. Using current time.") if location is None: viewpoint = "observatory" latitude, longitude, altitude = "", "", "" - log.warning("Observatory coordinates not set. Using center of Earth.") else: viewpoint = "latlon" if isinstance(location, EarthLocation): @@ -134,8 +120,8 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel latitude = loc[1].deg altitude = loc[2].to(u.m).value elif hasattr(location, "__iter__"): - latitude = Angle(location[0]).deg - longitude = Angle(location[1]).deg + longitude = Angle(location[0]).deg + latitude = Angle(location[1]).deg altitude = u.Quantity(location[2]).to("m").value if int(neptune_arcmodel) not in [1, 2, 3]: @@ -145,11 +131,11 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel # configure request_payload for ephemeris query # thankfully, adding extra planet-specific keywords here does not break query for other planets - request_payload = OrderedDict( + request_payload = dict( [ ("abbrev", planet[:3]), ("ephem", self.planet_defaults[planet]["ephem"]), - ("time", epoch.utc.to_value("iso", subfmt="date_hm")), # UTC. this should be enforced when checking inputs + ("time", epoch.utc.iso[:16]), ("fov", 10), # next few are figure options, can be hardcoded and ignored ("fov_unit", planet.capitalize() + " radii"), ("center", "body"), @@ -192,7 +178,7 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel # query and parse response = self._request( - "GET", self.URL, params=request_payload, timeout=self.TIMEOUT, cache=cache + "GET", self.url, params=request_payload, timeout=self.timeout, cache=cache ) return response @@ -210,21 +196,19 @@ def _parse_result(self, response, verbose=None): Returns ------- - systemtable : dict - bodytable : `astropy.Table` - ringtable : `astropy.Table` + bodytable : `astropy.QTable` + ringtable : `astropy.QTable` """ self.last_response = response try: self._last_query.remove_cache_file(self.cache_location) - except OSError: + except FileNotFoundError: # this is allowed: if `cache` was set to False, this # won't be needed pass soup = BeautifulSoup(response.text, "html.parser") text = soup.get_text() - # print(repr(text)) textgroups = re.split("\n\n|\n \n", text) ringtable = None for group in textgroups: @@ -354,15 +338,17 @@ def _parse_result(self, response, verbose=None): pass # do some cleanup from the parsing job + # and make system-wide parameters metadata of bodytable and ringtable + systemtable["epoch"] = Time(epoch, format="iso", scale="utc") # add obs time to systemtable if ringtable is not None: ringtable.add_index("ring") + ringtable.meta = systemtable bodytable = table.join(bodytable, bodytable2) # concatenate minor body table bodytable.add_index("Body") + bodytable.meta = systemtable - systemtable["epoch"] = Time(epoch, format="iso", scale="utc") # add obs time to systemtable - - return systemtable, bodytable, ringtable + return bodytable, ringtable RingNode = RingNodeClass() diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index dc69fb258c..c567c5cbd5 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -2,7 +2,6 @@ import os import numpy as np -from astropy.tests.helper import assert_quantity_allclose import astropy.units as u from astroquery.utils.mocks import MockResponse @@ -45,13 +44,14 @@ def patch_request(request): def test_ephemeris_query_Uranus(patch_request): - systemtable, bodytable, ringtable = pds.RingNode.ephemeris( + bodytable, ringtable = pds.RingNode.ephemeris( planet="Uranus", epoch="2022-05-03 00:00", - location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), + location=(-120.355 * u.deg, 10.0 * u.deg, 1000 * u.m), ) # check system table - assert_quantity_allclose( + systemtable = bodytable.meta + assert np.allclose( [ -56.12233, -56.13586, @@ -83,7 +83,7 @@ def test_ephemeris_query_Uranus(patch_request): mab = bodytable[bodytable.loc_indices["Mab"]] assert mab["NAIF ID"] == 726 assert mab["Body"] == "Mab" - assert_quantity_allclose( + assert np.allclose( [ 42.011201, 15.801323, @@ -119,14 +119,13 @@ def test_ephemeris_query_Uranus(patch_request): def test_ephemeris_query_Pluto(patch_request): - systemtable, bodytable, ringtable = pds.RingNode.ephemeris( + bodytable, ringtable = pds.RingNode.ephemeris( planet="Pluto", epoch="2021-10-07 07:25", ) - print(systemtable) - print(bodytable[bodytable.loc_indices["Styx"]]) + systemtable = bodytable.meta # check system table - assert_quantity_allclose( + assert np.allclose( [ 57.57737, 57.56961, @@ -158,7 +157,7 @@ def test_ephemeris_query_Pluto(patch_request): styx = bodytable[bodytable.loc_indices["Styx"]] assert styx["NAIF ID"] == 905 assert styx["Body"] == "Styx" - assert_quantity_allclose( + assert np.allclose( [ 296.212477, -22.93533, @@ -193,15 +192,13 @@ def test_ephemeris_query_Pluto(patch_request): def test_ephemeris_query_Neptune(patch_request): '''Verify that the Neptune ring arcs are queried properly''' - systemtable, bodytable, ringtable = pds.RingNode.ephemeris( + bodytable, ringtable = pds.RingNode.ephemeris( planet="Neptune", epoch="2021-10-07 07:25", neptune_arcmodel=2 ) - print(ringtable[ringtable.loc_indices["Courage"]]) - - assert_quantity_allclose( + assert np.allclose( [63.81977, 55.01978, 44.21976, @@ -227,13 +224,12 @@ def test_ephemeris_query_Neptune(patch_request): def test_ephemeris_query_Saturn(patch_request): '''Check Saturn F ring is queried properly''' - systemtable, bodytable, ringtable = pds.RingNode.ephemeris( + bodytable, ringtable = pds.RingNode.ephemeris( planet="Saturn", epoch="2021-10-07 07:25", ) - print(ringtable) - assert_quantity_allclose( + assert np.allclose( [249.23097, 250.34081 ], @@ -248,7 +244,7 @@ def test_ephemeris_query_payload(): planet="Neptune", epoch="2022-05-03 00:00", neptune_arcmodel=1, - location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), + location=(-120.355 * u.deg, 10.0 * u.deg, 1000 * u.m), get_query_payload=True, ) @@ -301,3 +297,12 @@ def test_ephemeris_query_payload(): ("output", "html"), ] ) + + +def test_bad_query_raise(): + + with pytest.raises(ValueError): + bodytable, ringtable = pds.RingNode.ephemeris( + planet="Venus", + epoch="2021-10-07 07:25", + ) diff --git a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py index b683a3c64a..157e95093b 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode_remote.py @@ -1,30 +1,22 @@ import pytest -from astropy.tests.helper import assert_quantity_allclose import numpy as np import astropy.units as u from ... import pds -# Horizons has machinery here to mock request from a text file -# is that necessary? why is that done? -# wouldn't we want to know if the website we are querying changes something that makes code fail? - - -# --------------------------------- actual test functions - - @pytest.mark.remote_data class TestRingNodeClass: def test_ephemeris_query(self): - systemtable, bodytable, ringtable = pds.RingNode.ephemeris( + bodytable, ringtable = pds.RingNode.ephemeris( planet="Uranus", epoch="2022-05-03 00:00", - location=(10.0 * u.deg, -120.355 * u.deg, 1000 * u.m), + location=(-120.355 * u.deg, 10.0 * u.deg, 1000 * u.m), ) # check system table - assert_quantity_allclose( + systemtable = bodytable.meta + assert np.allclose( [ -56.12233, -56.13586, @@ -56,7 +48,7 @@ def test_ephemeris_query(self): mab = bodytable[bodytable.loc_indices["Mab"]] assert mab["NAIF ID"] == 726 assert mab["Body"] == "Mab" - assert_quantity_allclose( + assert np.allclose( [ 42.011201, 15.801323, diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index eee748b5b5..a79d980a5f 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -22,9 +22,9 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: >>> from astroquery.solarsystem.pds import RingNode >>> import astropy.units as u - >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Uranus', + >>> bodytable, ringtable = RingNode.ephemeris(planet='Uranus', ... epoch='2024-05-08 22:39', - ... location = (-23.029 * u.deg, -67.755 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA + ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA >>> print(ringtable) ring pericenter ascending node deg deg @@ -42,21 +42,23 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: ``planet`` must be one of ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto'] (case-insensitive) +.. code-block:: python + + >>> bodytable, ringtable = RingNode.ephemeris(planet='Venus', + ... epoch='2024-05-08 22:39', + ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + ValueError: illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto') + ``epoch`` is the datetime to query. Accepts a string in format 'YYYY-MM-DD HH:MM' (UTC assumed), or a `~astropy.time.Time` object. If no epoch is provided, the current time is used. -``location`` is the observer's location. Accepts an `~astropy.coordinates.EarthLocation`, or any 3-element array-like (e.g. list, tuple) of format (latitude, longitude, elevation). Longitude and latitude should be anything that initializes an `~astropy.coordinates.Angle` object, and altitude should initialize a `~astropy.units.Quantity` object (with units of length). If ``None``, then the geocenter is used. +``location`` is the observer's location. Accepts an `~astropy.coordinates.EarthLocation`, or any 3-element array-like (e.g. list, tuple) of format (longitude, latitude, elevation). Longitude and latitude should be anything that initializes an `~astropy.coordinates.Angle` object, and altitude should initialize a `~astropy.units.Quantity` object (with units of length). If ``None``, then the geocenter is used. ``neptune_arcmodel`` is the choice of which ephemeris to assume for Neptune's ring arcs. accepts a float. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details). default 3. has no effect if planet != 'Neptune' Outputs --------- -``systemtable`` is a dict containing system-wide ephemeris data. Every value is a `~astropy.units.Quantity`. We can get a list of all the keys in this dictionary with: - -.. code-block:: python - - >>> print(systemtable.keys()) - dict_keys(['sub_sun_lat', 'sub_sun_lat_min', 'sub_sun_lat_max', 'opening_angle', 'phase_angle', 'sub_sun_lon', 'sub_obs_lon', 'd_sun', 'd_obs', 'light_time', 'epoch']) - ``bodytable`` is a `~astropy.table.QTable` containing ephemeris information on the moons in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: .. code-block:: python @@ -75,7 +77,7 @@ Note that the behavior of ``ringtable`` changes depending on the planet you quer .. code-block:: python - >>> systemtable, bodytable, ringtable = RingNode.ephemeris(planet='Neptune', epoch='2022-05-24 00:00') # doctest: +REMOTE_DATA + >>> bodytable, ringtable = RingNode.ephemeris(planet='Neptune', epoch='2022-05-24 00:00') # doctest: +REMOTE_DATA >>> print(ringtable) ring min_angle max_angle deg deg @@ -85,6 +87,14 @@ Note that the behavior of ``ringtable`` changes depending on the planet you quer Egalite A 33.88179 34.88179 Egalite B 30.0818 33.0818 Fraternite 16.0818 25.68181 + +System-wide data are available as metadata in both ``bodytable`` and ``ringtable`` (if ``ringtable`` exists), e.g.: + +.. code-block:: python + + >>> systemtable = bodytable.meta + >>> print(systemtable.keys()) + dict_keys(['sub_sun_lat', 'sub_sun_lat_min', 'sub_sun_lat_max', 'opening_angle', 'phase_angle', 'sub_sun_lon', 'sub_obs_lon', 'd_sun', 'd_obs', 'light_time', 'epoch']) Acknowledgements From 1d2621dbe7256989f31275c7153b82683353a42c Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Wed, 25 May 2022 13:17:27 -0700 Subject: [PATCH 14/25] attempted fix to AttributeError: 'RingNodeClass' object has no attribute '_last_query' --- astroquery/solarsystem/pds/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 4ae0c0075e..d951ee4915 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -202,7 +202,7 @@ def _parse_result(self, response, verbose=None): self.last_response = response try: self._last_query.remove_cache_file(self.cache_location) - except FileNotFoundError: + except (FileNotFoundError, AttributeError): # this is allowed: if `cache` was set to False, this # won't be needed pass From 95c9570b89ba1c678a4c58685054f31af2fb420f Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Sun, 29 May 2022 17:28:58 -0700 Subject: [PATCH 15/25] complying with template in pull request #2341 --- astroquery/solarsystem/pds/__init__.py | 51 +----------------- astroquery/solarsystem/pds/core.py | 74 ++++++++++++++++++++++---- docs/solarsystem/pds/pds.rst | 2 +- 3 files changed, 66 insertions(+), 61 deletions(-) diff --git a/astroquery/solarsystem/pds/__init__.py b/astroquery/solarsystem/pds/__init__.py index d452ddd2bf..fc326f0c62 100644 --- a/astroquery/solarsystem/pds/__init__.py +++ b/astroquery/solarsystem/pds/__init__.py @@ -14,60 +14,13 @@ class Conf(_config.ConfigNamespace): """ # server settings - pds_server = _config.ConfigItem( + url = _config.ConfigItem( "https://pds-rings.seti.org/cgi-bin/tools/viewer3_xxx.pl?", "Ring Node" ) # implement later: other pds tools - timeout = _config.ConfigItem(30, "Time limit for connecting to PDS servers.") - - # PDS settings - put hardcoded dictionaries of any kind here - - planet_defaults = { - "mars": { - "ephem": "000 MAR097 + DE440", - "moons": "402 Phobos, Deimos", - "center_ansa": "Phobos Ring", - "rings": "Phobos, Deimos", - }, - "jupiter": { - "ephem": "000 JUP365 + DE440", - "moons": "516 All inner moons (J1-J5,J14-J16)", - "center_ansa": "Main Ring", - "rings": "Main & Gossamer", - }, - "saturn": { - "ephem": "000 SAT389 + SAT393 + SAT427 + DE440", - "moons": "653 All inner moons (S1-S18,S32-S35,S49,S53)", - "center_ansa": "A", - "rings": "A,B,C,F,G,E", - }, - "uranus": { - "ephem": "000 URA111 + URA115 + DE440", - "moons": "727 All inner moons (U1-U15,U25-U27)", - "center_ansa": "Epsilon", - "rings": "All rings", - }, - "neptune": { - "ephem": "000 NEP081 + NEP095 + DE440", - "moons": "814 All inner moons (N1-N8,N14)", - "center_ansa": "Adams Ring", - "rings": "Galle, LeVerrier, Arago, Adams", - }, - "pluto": { - "ephem": "000 PLU058 + DE440", - "moons": "905 All moons (P1-P5)", - "center_ansa": "Hydra", - "rings": "Styx, Nix, Kerberos, Hydra", - }, - } - - neptune_arcmodels = { - 1: "#1 (820.1194 deg/day)", - 2: "#2 (820.1118 deg/day)", - 3: "#3 (820.1121 deg/day)", - } + timeout = _config.ConfigItem(30, "Time limit for connecting to PDS servers (seconds).") conf = Conf() diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index d951ee4915..dadd6af7be 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -19,6 +19,51 @@ # import configurable items declared in __init__.py, e.g. hardcoded dictionaries from . import conf +planet_defaults = { + "mars": { + "ephem": "000 MAR097 + DE440", + "moons": "402 Phobos, Deimos", + "center_ansa": "Phobos Ring", + "rings": "Phobos, Deimos", + }, + "jupiter": { + "ephem": "000 JUP365 + DE440", + "moons": "516 All inner moons (J1-J5,J14-J16)", + "center_ansa": "Main Ring", + "rings": "Main & Gossamer", + }, + "saturn": { + "ephem": "000 SAT389 + SAT393 + SAT427 + DE440", + "moons": "653 All inner moons (S1-S18,S32-S35,S49,S53)", + "center_ansa": "A", + "rings": "A,B,C,F,G,E", + }, + "uranus": { + "ephem": "000 URA111 + URA115 + DE440", + "moons": "727 All inner moons (U1-U15,U25-U27)", + "center_ansa": "Epsilon", + "rings": "All rings", + }, + "neptune": { + "ephem": "000 NEP081 + NEP095 + DE440", + "moons": "814 All inner moons (N1-N8,N14)", + "center_ansa": "Adams Ring", + "rings": "Galle, LeVerrier, Arago, Adams", + }, + "pluto": { + "ephem": "000 PLU058 + DE440", + "moons": "905 All moons (P1-P5)", + "center_ansa": "Hydra", + "rings": "Styx, Nix, Kerberos, Hydra", + }, +} + +neptune_arcmodels = { + 1: "#1 (820.1194 deg/day)", + 2: "#2 (820.1118 deg/day)", + 3: "#3 (820.1121 deg/day)", +} + @async_to_sync class RingNodeClass(BaseQuery): @@ -27,14 +72,21 @@ class RingNodeClass(BaseQuery): """ - def __init__(self, timeout=None): + def __init__(self, url='', timeout=None): ''' Instantiate Planetary Ring Node query ''' super().__init__() - self.url = conf.pds_server - self.timeout = conf.timeout - self.planet_defaults = conf.planet_defaults + self.url = url + self.timeout = timeout + + @property + def _url(self): + return self.url or conf.url + + @property + def _timeout(self): + return conf.timeout if self.timeout is None else self.timeout def __str__(self): @@ -134,13 +186,13 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel request_payload = dict( [ ("abbrev", planet[:3]), - ("ephem", self.planet_defaults[planet]["ephem"]), + ("ephem", planet_defaults[planet]["ephem"]), ("time", epoch.utc.iso[:16]), ("fov", 10), # next few are figure options, can be hardcoded and ignored ("fov_unit", planet.capitalize() + " radii"), ("center", "body"), ("center_body", planet.capitalize()), - ("center_ansa", self.planet_defaults[planet]["center_ansa"]), + ("center_ansa", planet_defaults[planet]["center_ansa"]), ("center_ew", "east"), ("center_ra", ""), ("center_ra_type", "hours"), @@ -152,9 +204,9 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel ("longitude", longitude), ("lon_dir", "east"), ("altitude", altitude), - ("moons", self.planet_defaults[planet]["moons"]), - ("rings", self.planet_defaults[planet]["rings"]), - ("arcmodel", conf.neptune_arcmodels[int(neptune_arcmodel)]), + ("moons", planet_defaults[planet]["moons"]), + ("rings", planet_defaults[planet]["rings"]), + ("arcmodel", neptune_arcmodels[int(neptune_arcmodel)]), ("extra_ra", ""), # figure options below this line, can all be hardcoded and ignored ("extra_ra_type", "hours"), ("extra_dec", ""), @@ -178,7 +230,7 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel # query and parse response = self._request( - "GET", self.url, params=request_payload, timeout=self.timeout, cache=cache + "GET", self._url, params=request_payload, timeout=self._timeout, cache=cache ) return response @@ -202,7 +254,7 @@ def _parse_result(self, response, verbose=None): self.last_response = response try: self._last_query.remove_cache_file(self.cache_location) - except (FileNotFoundError, AttributeError): + except FileNotFoundError: # this is allowed: if `cache` was set to False, this # won't be needed pass diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index a79d980a5f..dfc5efe6f3 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -46,7 +46,7 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: >>> bodytable, ringtable = RingNode.ephemeris(planet='Venus', ... epoch='2024-05-08 22:39', - ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +IGNORE_EXCEPTION_DETAIL + ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) Traceback (most recent call last): ... ValueError: illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto') From 66c87f268f2dfbed4bb393c57c98cf8ee88640a0 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Mon, 30 May 2022 10:55:52 -0700 Subject: [PATCH 16/25] fixes suggested by @eerovaher --- astroquery/solarsystem/pds/tests/test_ringnode.py | 9 +++++++++ docs/solarsystem/pds/pds.rst | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_ringnode.py index c567c5cbd5..bc1a7dfb46 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_ringnode.py @@ -3,6 +3,7 @@ import numpy as np import astropy.units as u +from ....query import AstroQuery from astroquery.utils.mocks import MockResponse from ... import pds @@ -42,6 +43,14 @@ def patch_request(request): # --------------------------------- actual test functions +def test_parse_result(patch_request): + q = pds.RingNode() + # need _last_query to be defined + q._last_query = AstroQuery('GET', 'http://dummy') + with pytest.raises(ValueError): + res = q.ephemeris('dummy-planet-name') + + def test_ephemeris_query_Uranus(patch_request): bodytable, ringtable = pds.RingNode.ephemeris( diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index dfc5efe6f3..158dd5e51e 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -25,7 +25,7 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: >>> bodytable, ringtable = RingNode.ephemeris(planet='Uranus', ... epoch='2024-05-08 22:39', ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA - >>> print(ringtable) + >>> print(ringtable) # doctest: +REMOTE_DATA ring pericenter ascending node deg deg ------- ---------- -------------- From 65f5167812d702d15cc987f6e7e777e23c5b3381 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Fri, 24 Jun 2022 15:06:28 -0700 Subject: [PATCH 17/25] changes suggested by @bsipocz --- astroquery/solarsystem/pds/core.py | 11 ++++----- .../tests/{test_ringnode.py => test_pds.py} | 20 ++++++++++++---- ..._ringnode_remote.py => test_pds_remote.py} | 0 docs/solarsystem/pds/pds.rst | 24 +++++++++---------- 4 files changed, 31 insertions(+), 24 deletions(-) rename astroquery/solarsystem/pds/tests/{test_ringnode.py => test_pds.py} (93%) rename astroquery/solarsystem/pds/tests/{test_ringnode_remote.py => test_pds_remote.py} (100%) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index dadd6af7be..dd0a2cdf8f 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -84,16 +84,12 @@ def __init__(self, url='', timeout=None): def _url(self): return self.url or conf.url - @property - def _timeout(self): - return conf.timeout if self.timeout is None else self.timeout - def __str__(self): return "PDSRingNode instance" def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel=3, - get_query_payload=False, get_raw_response=False, cache=True): + get_query_payload=False, cache=True): """ send query to Planetary Ring Node server @@ -117,6 +113,8 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel get_query_payload : boolean, optional When set to `True` the method returns the HTTP request parameters as a dict, default: False + cache : boolean, optional + When set to `True` the method caches the download, default: True Returns @@ -230,7 +228,7 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel # query and parse response = self._request( - "GET", self._url, params=request_payload, timeout=self._timeout, cache=cache + "GET", self._url, params=request_payload, timeout=self.timeout, cache=cache ) return response @@ -251,7 +249,6 @@ def _parse_result(self, response, verbose=None): bodytable : `astropy.QTable` ringtable : `astropy.QTable` """ - self.last_response = response try: self._last_query.remove_cache_file(self.cache_location) except FileNotFoundError: diff --git a/astroquery/solarsystem/pds/tests/test_ringnode.py b/astroquery/solarsystem/pds/tests/test_pds.py similarity index 93% rename from astroquery/solarsystem/pds/tests/test_ringnode.py rename to astroquery/solarsystem/pds/tests/test_pds.py index bc1a7dfb46..5663a0492c 100644 --- a/astroquery/solarsystem/pds/tests/test_ringnode.py +++ b/astroquery/solarsystem/pds/tests/test_pds.py @@ -53,7 +53,9 @@ def test_parse_result(patch_request): def test_ephemeris_query_Uranus(patch_request): - bodytable, ringtable = pds.RingNode.ephemeris( + pds_inst = pds.RingNode() + pds_inst._last_query = AstroQuery('GET', 'http://dummy') + bodytable, ringtable = pds_inst.ephemeris( planet="Uranus", epoch="2022-05-03 00:00", location=(-120.355 * u.deg, 10.0 * u.deg, 1000 * u.m), @@ -128,7 +130,9 @@ def test_ephemeris_query_Uranus(patch_request): def test_ephemeris_query_Pluto(patch_request): - bodytable, ringtable = pds.RingNode.ephemeris( + pds_inst = pds.RingNode() + pds_inst._last_query = AstroQuery('GET', 'http://dummy') + bodytable, ringtable = pds_inst.ephemeris( planet="Pluto", epoch="2021-10-07 07:25", ) @@ -201,7 +205,9 @@ def test_ephemeris_query_Pluto(patch_request): def test_ephemeris_query_Neptune(patch_request): '''Verify that the Neptune ring arcs are queried properly''' - bodytable, ringtable = pds.RingNode.ephemeris( + pds_inst = pds.RingNode() + pds_inst._last_query = AstroQuery('GET', 'http://dummy') + bodytable, ringtable = pds_inst.ephemeris( planet="Neptune", epoch="2021-10-07 07:25", neptune_arcmodel=2 @@ -233,7 +239,9 @@ def test_ephemeris_query_Neptune(patch_request): def test_ephemeris_query_Saturn(patch_request): '''Check Saturn F ring is queried properly''' - bodytable, ringtable = pds.RingNode.ephemeris( + pds_inst = pds.RingNode() + pds_inst._last_query = AstroQuery('GET', 'http://dummy') + bodytable, ringtable = pds_inst.ephemeris( planet="Saturn", epoch="2021-10-07 07:25", ) @@ -249,7 +257,9 @@ def test_ephemeris_query_Saturn(patch_request): def test_ephemeris_query_payload(): - res = pds.RingNode.ephemeris( + pds_inst = pds.RingNode() + pds_inst._last_query = AstroQuery('GET', 'http://dummy') + res = pds_inst.ephemeris( planet="Neptune", epoch="2022-05-03 00:00", neptune_arcmodel=1, diff --git a/astroquery/solarsystem/pds/tests/test_ringnode_remote.py b/astroquery/solarsystem/pds/tests/test_pds_remote.py similarity index 100% rename from astroquery/solarsystem/pds/tests/test_ringnode_remote.py rename to astroquery/solarsystem/pds/tests/test_pds_remote.py diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index 158dd5e51e..2679052439 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -18,13 +18,13 @@ In order to query information for a specific Solar System body, a ``RingNode`` object is instantiated and the :meth:`~astroquery.solarsystem.pds.RingNodeClass.ephemeris` method is called. The following example queries the ephemerides of the rings and small moons around Uranus as viewed from ALMA: -.. code-block:: python +.. code-block:: python >>> from astroquery.solarsystem.pds import RingNode >>> import astropy.units as u >>> bodytable, ringtable = RingNode.ephemeris(planet='Uranus', ... epoch='2024-05-08 22:39', - ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA + ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA >>> print(ringtable) # doctest: +REMOTE_DATA ring pericenter ascending node deg deg @@ -42,11 +42,11 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: ``planet`` must be one of ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto'] (case-insensitive) -.. code-block:: python +.. code-block:: python >>> bodytable, ringtable = RingNode.ephemeris(planet='Venus', ... epoch='2024-05-08 22:39', - ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) + ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA Traceback (most recent call last): ... ValueError: illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto') @@ -61,24 +61,24 @@ Outputs --------- ``bodytable`` is a `~astropy.table.QTable` containing ephemeris information on the moons in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: -.. code-block:: python +.. code-block:: python - >>> print(bodytable.columns) + >>> print(bodytable.columns) # doctest: +REMOTE_DATA ``ringtable`` is a `~astropy.table.QTable` containing ephemeris information on the individual rings in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: .. code-block:: python - >>> print(ringtable.columns) + >>> print(ringtable.columns) # doctest: +REMOTE_DATA Note that the behavior of ``ringtable`` changes depending on the planet you query. For Uranus and Saturn the table columns are as above. For Jupiter, Mars, and Pluto, there are no individual named rings returned by the Ring Node, so ``ringtable`` returns None; ephemeris for the ring systems of these bodies is still contained in ``systemtable`` as usual. For Neptune, the ring table shows the minimum and maximum longitudes (from the ring plane ascending node) of the five ring arcs according to the orbital evolution assumed by ``neptune_arcmodel``, e.g.: -.. code-block:: python +.. code-block:: python >>> bodytable, ringtable = RingNode.ephemeris(planet='Neptune', epoch='2022-05-24 00:00') # doctest: +REMOTE_DATA - >>> print(ringtable) + >>> print(ringtable) # doctest: +REMOTE_DATA ring min_angle max_angle deg deg ---------- --------- --------- @@ -90,10 +90,10 @@ Note that the behavior of ``ringtable`` changes depending on the planet you quer System-wide data are available as metadata in both ``bodytable`` and ``ringtable`` (if ``ringtable`` exists), e.g.: -.. code-block:: python +.. code-block:: python - >>> systemtable = bodytable.meta - >>> print(systemtable.keys()) + >>> systemtable = bodytable.meta # doctest: +REMOTE_DATA + >>> print(systemtable.keys()) # doctest: +REMOTE_DATA dict_keys(['sub_sun_lat', 'sub_sun_lat_min', 'sub_sun_lat_max', 'opening_angle', 'phase_angle', 'sub_sun_lon', 'sub_obs_lon', 'd_sun', 'd_obs', 'light_time', 'epoch']) From 3c922545af243448b4b5004b5cdc05085efc7f57 Mon Sep 17 00:00:00 2001 From: Ned Molter Date: Fri, 24 Jun 2022 16:04:27 -0700 Subject: [PATCH 18/25] made adding units with QTable back-compatible with 3.7 --- astroquery/solarsystem/pds/core.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index dd0a2cdf8f..c6682d3bc0 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -270,21 +270,29 @@ def _parse_result(self, response, verbose=None): # minor body table part 1 elif group.startswith("Body"): group = "NAIF " + group # fixing lack of header for NAIF ID + bodytable_names = ("NAIF ID", "Body", "RA", "Dec", "RA (deg)", "Dec (deg)", "dRA", "dDec") + bodytable_units = [None, None, None, None, u.deg, u.deg, u.arcsec, u.arcsec] bodytable = table.QTable.read(group, format="ascii.fixed_width", col_starts=(0, 4, 18, 35, 54, 68, 80, 91), col_ends=(4, 18, 35, 54, 68, 80, 91, 102), - names=("NAIF ID", "Body", "RA", "Dec", "RA (deg)", "Dec (deg)", "dRA", "dDec"), - units=([None, None, None, None, u.deg, u.deg, u.arcsec, u.arcsec])) + names=bodytable_names) + # units=(bodytable_units)) # this much cleaner way of adding units is supported in later versions but not in 3.7 + for name, unit in zip(bodytable_names, bodytable_units): + bodytable[name].unit = unit # minor body table part 2 elif group.startswith("Sub-"): group = "\n".join(group.split("\n")[1:]) # fixing two-row header group = "NAIF" + group[4:] + bodytable2_names = ("NAIF ID", "Body", "sub_obs_lon", "sub_obs_lat", "sub_sun_lon", "sub_sun_lat", "phase", "distance") + bodytable2_units = [None, None, u.deg, u.deg, u.deg, u.deg, u.deg, u.km * 1e6] bodytable2 = table.QTable.read(group, format="ascii.fixed_width", col_starts=(0, 4, 18, 28, 37, 49, 57, 71), col_ends=(4, 18, 28, 37, 49, 57, 71, 90), - names=("NAIF ID", "Body", "sub_obs_lon", "sub_obs_lat", "sub_sun_lon", "sub_sun_lat", "phase", "distance"), - units=([None, None, u.deg, u.deg, u.deg, u.deg, u.deg, u.km * 1e6])) + names=bodytable2_names) + # units=(bodytable_units)) # this much cleaner way of adding units is supported in later versions but not in 3.7 + for name, unit in zip(bodytable2_names, bodytable2_units): + bodytable2[name].unit = unit # ring plane data elif group.startswith("Ring s"): @@ -293,7 +301,7 @@ def _parse_result(self, response, verbose=None): l = line.split(":") if "Ring sub-solar latitude" in l[0]: [sub_sun_lat, sub_sun_lat_min, sub_sun_lat_max] = [ - float(s.strip(", \n()")) for s in re.split("\(|to", l[1]) + float(s.strip(", \n()")) for s in re.split(r"\(|to", l[1]) ] systemtable = { "sub_sun_lat": sub_sun_lat * u.deg, @@ -343,11 +351,15 @@ def _parse_result(self, response, verbose=None): # --------- below this line, planet-specific info ------------ # Uranus individual rings data elif group.startswith("Ring "): + ringtable_names = ("ring", "pericenter", "ascending node") + ringtable_units = [None, u.deg, u.deg] ringtable = table.QTable.read(" " + group, format="ascii.fixed_width", col_starts=(5, 18, 29), col_ends=(18, 29, 36), - names=("ring", "pericenter", "ascending node"), - units=([None, u.deg, u.deg])) + names=ringtable_names) + # units=(ringtable_units)) # this much cleaner way of adding units is supported in later versions but not in 3.7 + for name, unit in zip(ringtable_names, ringtable_units): + ringtable[name].unit = unit # Saturn F-ring data elif group.startswith("F Ring"): From 550bd9575c192f434dbeed4729ea41f0c8a13bd5 Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Tue, 6 Sep 2022 09:37:37 -0700 Subject: [PATCH 19/25] fixing oldest dependencies table read duplicate column error --- astroquery/solarsystem/pds/core.py | 28 +++++++++++--------- astroquery/solarsystem/pds/tests/test_pds.py | 2 +- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index c6682d3bc0..f1d21836a7 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -1,12 +1,9 @@ # 1. standard library imports - import re -import warnings # 2. third party imports from astropy.time import Time from astropy import table -from astropy.io import ascii import astropy.units as u from astropy.coordinates import EarthLocation, Angle from bs4 import BeautifulSoup @@ -282,14 +279,15 @@ def _parse_result(self, response, verbose=None): # minor body table part 2 elif group.startswith("Sub-"): - group = "\n".join(group.split("\n")[1:]) # fixing two-row header - group = "NAIF" + group[4:] + + group = "\n".join(group.split("\n")[2:]) # removing two-row header entirely bodytable2_names = ("NAIF ID", "Body", "sub_obs_lon", "sub_obs_lat", "sub_sun_lon", "sub_sun_lat", "phase", "distance") bodytable2_units = [None, None, u.deg, u.deg, u.deg, u.deg, u.deg, u.km * 1e6] bodytable2 = table.QTable.read(group, format="ascii.fixed_width", col_starts=(0, 4, 18, 28, 37, 49, 57, 71), col_ends=(4, 18, 28, 37, 49, 57, 71, 90), - names=bodytable2_names) + names=bodytable2_names, + data_start=0) # units=(bodytable_units)) # this much cleaner way of adding units is supported in later versions but not in 3.7 for name, unit in zip(bodytable2_names, bodytable2_units): bodytable2[name].unit = unit @@ -370,11 +368,14 @@ def _parse_result(self, response, verbose=None): peri = float(re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()")) elif "F Ring ascending node" in l[0]: ascn = float(l[1].strip(", \n")) + ringtable_names = ("ring", "pericenter", "ascending node") + ringtable_units = [None, u.deg, u.deg] ringtable = table.QTable( [["F"], [peri], [ascn]], - names=("ring", "pericenter", "ascending node"), - units=(None, u.deg, u.deg), - ) + names=ringtable_names) + # units=(ringtable_units) # this much cleaner way of adding units is supported in later versions but not in 3.7 + for name, unit in zip(ringtable_names, ringtable_units): + ringtable[name].unit = unit # Neptune ring arcs data elif group.startswith("Courage"): @@ -387,11 +388,14 @@ def _parse_result(self, response, verbose=None): for s in re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()").split() ] if i == 0: + ringtable_names = ("ring", "min_angle", "max_angle") + ringtable_units = [None, u.deg, u.deg] ringtable = table.QTable( [[ring], [min_angle], [max_angle]], - names=("ring", "min_angle", "max_angle"), - units=(None, u.deg, u.deg), - ) + names=ringtable_names) + for name, unit in zip(ringtable_names, ringtable_units): + ringtable[name].unit = unit + # units=(ringtable_units) # this much cleaner way of adding units is supported in later versions but not in 3.7 else: ringtable.add_row([ring, min_angle*u.deg, max_angle*u.deg]) diff --git a/astroquery/solarsystem/pds/tests/test_pds.py b/astroquery/solarsystem/pds/tests/test_pds.py index 5663a0492c..99b6c3fc2c 100644 --- a/astroquery/solarsystem/pds/tests/test_pds.py +++ b/astroquery/solarsystem/pds/tests/test_pds.py @@ -48,7 +48,7 @@ def test_parse_result(patch_request): # need _last_query to be defined q._last_query = AstroQuery('GET', 'http://dummy') with pytest.raises(ValueError): - res = q.ephemeris('dummy-planet-name') + q.ephemeris('dummy-planet-name') def test_ephemeris_query_Uranus(patch_request): From a33753b511b6db6d98778e4603d1abfa7b01ee60 Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Thu, 27 Oct 2022 14:22:07 +0200 Subject: [PATCH 20/25] added support for jwst --- astroquery/solarsystem/pds/core.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index f1d21836a7..b4ae5d1bfa 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -96,14 +96,16 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel epoch : `~astropy.time.Time` object, or str in format YYYY-MM-DD hh:mm, optional. If str is provided then UTC is assumed. If no epoch is provided, the current time is used. - location : array-like, or `~astropy.coordinates.EarthLocation`, optional - Observer's location as a - 3-element array of Earth longitude, latitude, altitude, or + location : str, or array-like, or `~astropy.coordinates.EarthLocation`, optional + If str, named observeratory supported by the ring node, e.g. JWST. + If array-like, observer's location as a + 3-element array of Earth longitude, latitude, altitude + that istantiates an `~astropy.coordinates.EarthLocation`. Longitude and latitude should be anything that initializes an `~astropy.coordinates.Angle` object, and altitude should initialize an `~astropy.units.Quantity` object (with units - of length). If ``None``, then the geocenter is used. + of length). If ``None``, then the geofocus is used. neptune_arcmodel : float, optional. which ephemeris to assume for Neptune's ring arcs must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details) has no effect if planet != 'Neptune' @@ -158,9 +160,15 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel if location is None: viewpoint = "observatory" + observatory = "Earth's center" + latitude, longitude, altitude = "", "", "" + elif isinstance(location, str): + viewpoint = "observatory" + observatory = location latitude, longitude, altitude = "", "", "" else: viewpoint = "latlon" + observatory = "Earth's center" if isinstance(location, EarthLocation): loc = location.geodetic longitude = loc[0].deg @@ -194,7 +202,7 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel ("center_dec", ""), ("center_star", ""), ("viewpoint", viewpoint), - ("observatory", "Earth's center"), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories + ("observatory", observatory), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories ("latitude", latitude), ("longitude", longitude), ("lon_dir", "east"), @@ -255,6 +263,10 @@ def _parse_result(self, response, verbose=None): soup = BeautifulSoup(response.text, "html.parser") text = soup.get_text() + if "\n" in text: + text = text.replace("\r", "") + else: + text = text.replace("\r", "\n") textgroups = re.split("\n\n|\n \n", text) ringtable = None for group in textgroups: From 95709326b5c81053bbe2c54d4e36f2aa67fdba6c Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Thu, 17 Nov 2022 15:18:21 -0800 Subject: [PATCH 21/25] forgot to test before push :facepalm: --- astroquery/solarsystem/pds/core.py | 54 ++++--- astroquery/solarsystem/pds/tests/test_pds.py | 140 ++---------------- .../solarsystem/pds/tests/test_pds_remote.py | 52 +------ docs/solarsystem/pds/pds.rst | 52 +++---- 4 files changed, 69 insertions(+), 229 deletions(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index b4ae5d1bfa..1b28063962 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -1,5 +1,6 @@ # 1. standard library imports import re +import os # 2. third party imports from astropy.time import Time @@ -185,13 +186,13 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel ) # configure request_payload for ephemeris query - # thankfully, adding extra planet-specific keywords here does not break query for other planets + request_payload = dict( [ ("abbrev", planet[:3]), ("ephem", planet_defaults[planet]["ephem"]), ("time", epoch.utc.iso[:16]), - ("fov", 10), # next few are figure options, can be hardcoded and ignored + ("fov", 10), # figure option - hardcode ok ("fov_unit", planet.capitalize() + " radii"), ("center", "body"), ("center_body", planet.capitalize()), @@ -202,7 +203,7 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel ("center_dec", ""), ("center_star", ""), ("viewpoint", viewpoint), - ("observatory", observatory), # has no effect if viewpoint != observatory so can hardcode. no plans to implement calling observatories by name since ring node only names like 8 observatories + ("observatory", observatory), ("latitude", latitude), ("longitude", longitude), ("lon_dir", "east"), @@ -263,18 +264,15 @@ def _parse_result(self, response, verbose=None): soup = BeautifulSoup(response.text, "html.parser") text = soup.get_text() - if "\n" in text: - text = text.replace("\r", "") - else: - text = text.replace("\r", "\n") - textgroups = re.split("\n\n|\n \n", text) + # need regex because some blank lines have spacebar and some do not + textgroups = re.split(2*os.linesep+"|"+os.linesep+" "+os.linesep, text) ringtable = None for group in textgroups: - group = group.strip(", \n") + group = group.strip() # input parameters. only thing needed is epoch if group.startswith("Observation"): - epoch = group.split("\n")[0].split("e: ")[-1].strip(", \n") + epoch = group.splitlines()[0].split("e: ")[-1].strip() # minor body table part 1 elif group.startswith("Body"): @@ -292,7 +290,7 @@ def _parse_result(self, response, verbose=None): # minor body table part 2 elif group.startswith("Sub-"): - group = "\n".join(group.split("\n")[2:]) # removing two-row header entirely + group = os.linesep.join(group.splitlines()[2:]) # removing two-row header entirely bodytable2_names = ("NAIF ID", "Body", "sub_obs_lon", "sub_obs_lat", "sub_sun_lon", "sub_sun_lat", "phase", "distance") bodytable2_units = [None, None, u.deg, u.deg, u.deg, u.deg, u.deg, u.km * 1e6] bodytable2 = table.QTable.read(group, format="ascii.fixed_width", @@ -306,12 +304,12 @@ def _parse_result(self, response, verbose=None): # ring plane data elif group.startswith("Ring s"): - lines = group.split("\n") + lines = group.splitlines() for line in lines: l = line.split(":") if "Ring sub-solar latitude" in l[0]: [sub_sun_lat, sub_sun_lat_min, sub_sun_lat_max] = [ - float(s.strip(", \n()")) for s in re.split(r"\(|to", l[1]) + float(s.strip("()")) for s in re.split(r"\(|to", l[1]) ] systemtable = { "sub_sun_lat": sub_sun_lat * u.deg, @@ -321,22 +319,22 @@ def _parse_result(self, response, verbose=None): elif "Ring plane opening angle" in l[0]: systemtable["opening_angle"] = ( - float(re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()")) * u.deg + float(re.sub("[a-zA-Z]+", "", l[1]).strip("()")) * u.deg ) elif "Ring center phase angle" in l[0]: - systemtable["phase_angle"] = float(l[1].strip(", \n")) * u.deg + systemtable["phase_angle"] = float(l[1].strip()) * u.deg elif "Sub-solar longitude" in l[0]: systemtable["sub_sun_lon"] = ( - float(re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()")) * u.deg + float(re.sub("[a-zA-Z]+", "", l[1]).strip("()")) * u.deg ) elif "Sub-observer longitude" in l[0]: - systemtable["sub_obs_lon"] = float(l[1].strip(", \n")) * u.deg + systemtable["sub_obs_lon"] = float(l[1].strip()) * u.deg else: pass # basic info about the planet elif group.startswith("Sun-planet"): - lines = group.split("\n") + lines = group.splitlines() for line in lines: l = line.split(":") if "Sun-planet distance (AU)" in l[0]: @@ -347,14 +345,14 @@ def _parse_result(self, response, verbose=None): pass elif "Sun-planet distance (km)" in l[0]: systemtable["d_sun"] = ( - float(l[1].split("x")[0].strip(", \n")) * 1e6 * u.km + float(l[1].split("x")[0].strip()) * 1e6 * u.km ) elif "Observer-planet distance (km)" in l[0]: systemtable["d_obs"] = ( - float(l[1].split("x")[0].strip(", \n")) * 1e6 * u.km + float(l[1].split("x")[0].strip()) * 1e6 * u.km ) elif "Light travel time" in l[0]: - systemtable["light_time"] = float(l[1].strip(", \n")) * u.second + systemtable["light_time"] = float(l[1].strip()) * u.second else: pass @@ -373,13 +371,13 @@ def _parse_result(self, response, verbose=None): # Saturn F-ring data elif group.startswith("F Ring"): - lines = group.split("\n") + lines = group.splitlines() for line in lines: l = line.split(":") if "F Ring pericenter" in l[0]: - peri = float(re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()")) + peri = float(re.sub("[a-zA-Z]+", "", l[1]).strip("()")) elif "F Ring ascending node" in l[0]: - ascn = float(l[1].strip(", \n")) + ascn = float(l[1].strip()) ringtable_names = ("ring", "pericenter", "ascending node") ringtable_units = [None, u.deg, u.deg] ringtable = table.QTable( @@ -391,13 +389,13 @@ def _parse_result(self, response, verbose=None): # Neptune ring arcs data elif group.startswith("Courage"): - lines = group.split("\n") + lines = group.split(os.linesep)[:5] #requires explicit split by os.linesep because windows and macos handle this text block differently for i in range(len(lines)): l = lines[i].split(":") - ring = l[0].split("longitude")[0].strip(", \n") + ring = l[0].split("longitude")[0].strip() [min_angle, max_angle] = [ - float(s.strip(", \n")) - for s in re.sub("[a-zA-Z]+", "", l[1]).strip(", \n()").split() + float(s) + for s in re.sub("[a-zA-Z]+", "", l[1]).split() ] if i == 0: ringtable_names = ("ring", "min_angle", "max_angle") diff --git a/astroquery/solarsystem/pds/tests/test_pds.py b/astroquery/solarsystem/pds/tests/test_pds.py index 99b6c3fc2c..49a3fe8246 100644 --- a/astroquery/solarsystem/pds/tests/test_pds.py +++ b/astroquery/solarsystem/pds/tests/test_pds.py @@ -63,30 +63,8 @@ def test_ephemeris_query_Uranus(patch_request): # check system table systemtable = bodytable.meta assert np.allclose( - [ - -56.12233, - -56.13586, - -56.13586, - -56.01577, - 0.10924, - 354.11072, - 354.12204, - 2947896667.0, - 3098568884.0, - 10335.713263, - ], - [ - systemtable["sub_sun_lat"].to(u.deg).value, - systemtable["sub_sun_lat_min"].to(u.deg).value, - systemtable["sub_sun_lat_max"].to(u.deg).value, - systemtable["opening_angle"].to(u.deg).value, - systemtable["phase_angle"].to(u.deg).value, - systemtable["sub_sun_lon"].to(u.deg).value, - systemtable["sub_obs_lon"].to(u.deg).value, - systemtable["d_sun"].to(u.km).value, - systemtable["d_obs"].to(u.km).value, - systemtable["light_time"].to(u.second).value, - ], + [-56.12233, -56.13586, -56.13586, -56.01577, 0.10924, 354.11072, 354.12204, 2947896667.0, 3098568884.0, 10335.713263, ], + [systemtable["sub_sun_lat"].to(u.deg).value, systemtable["sub_sun_lat_min"].to(u.deg).value, systemtable["sub_sun_lat_max"].to(u.deg).value, systemtable["opening_angle"].to(u.deg).value, systemtable["phase_angle"].to(u.deg).value, systemtable["sub_sun_lon"].to(u.deg).value, systemtable["sub_obs_lon"].to(u.deg).value, systemtable["d_sun"].to(u.km).value, systemtable["d_obs"].to(u.km).value, systemtable["light_time"].to(u.second).value, ], rtol=1e-2, ) @@ -95,30 +73,8 @@ def test_ephemeris_query_Uranus(patch_request): assert mab["NAIF ID"] == 726 assert mab["Body"] == "Mab" assert np.allclose( - [ - 42.011201, - 15.801323, - 5.368, - 0.623, - 223.976, - 55.906, - 223.969, - 56.013, - 0.10932, - 3098.514, - ], - [ - mab["RA (deg)"].to(u.deg).value, - mab["Dec (deg)"].to(u.deg).value, - mab["dRA"].to(u.arcsec).value, - mab["dDec"].to(u.arcsec).value, - mab["sub_obs_lon"].to(u.deg).value, - mab["sub_obs_lat"].to(u.deg).value, - mab["sub_sun_lon"].to(u.deg).value, - mab["sub_sun_lat"].to(u.deg).value, - mab["phase"].to(u.deg).value, - mab["distance"].to(u.km * 1e6).value, - ], + [42.011201, 15.801323, 5.368, 0.623, 223.976, 55.906, 223.969, 56.013, 0.10932, 3098.514, ], + [mab["RA (deg)"].to(u.deg).value, mab["Dec (deg)"].to(u.deg).value, mab["dRA"].to(u.arcsec).value, mab["dDec"].to(u.arcsec).value, mab["sub_obs_lon"].to(u.deg).value, mab["sub_obs_lat"].to(u.deg).value, mab["sub_sun_lon"].to(u.deg).value, mab["sub_sun_lat"].to(u.deg).value, mab["phase"].to(u.deg).value, mab["distance"].to(u.km * 1e6).value, ], rtol=1e-2, ) @@ -139,30 +95,8 @@ def test_ephemeris_query_Pluto(patch_request): systemtable = bodytable.meta # check system table assert np.allclose( - [ - 57.57737, - 57.56961, - 57.56961, - 56.50534, - 1.64048, - 116.55873, - 118.8369, - 5142696000, - 5114486810, - 17060.091666, - ], - [ - systemtable["sub_sun_lat"].to(u.deg).value, - systemtable["sub_sun_lat_min"].to(u.deg).value, - systemtable["sub_sun_lat_max"].to(u.deg).value, - systemtable["opening_angle"].to(u.deg).value, - systemtable["phase_angle"].to(u.deg).value, - systemtable["sub_sun_lon"].to(u.deg).value, - systemtable["sub_obs_lon"].to(u.deg).value, - systemtable["d_sun"].to(u.km).value, - systemtable["d_obs"].to(u.km).value, - systemtable["light_time"].to(u.second).value, - ], + [57.57737, 57.56961, 57.56961, 56.50534, 1.64048, 116.55873, 118.8369, 5142696000, 5114486810, 17060.091666, ], + [systemtable["sub_sun_lat"].to(u.deg).value, systemtable["sub_sun_lat_min"].to(u.deg).value, systemtable["sub_sun_lat_max"].to(u.deg).value, systemtable["opening_angle"].to(u.deg).value, systemtable["phase_angle"].to(u.deg).value, systemtable["sub_sun_lon"].to(u.deg).value, systemtable["sub_obs_lon"].to(u.deg).value, systemtable["d_sun"].to(u.km).value, systemtable["d_obs"].to(u.km).value, systemtable["light_time"].to(u.second).value, ], rtol=1e-2, ) @@ -171,30 +105,8 @@ def test_ephemeris_query_Pluto(patch_request): assert styx["NAIF ID"] == 905 assert styx["Body"] == "Styx" assert np.allclose( - [ - 296.212477, - -22.93533, - -0.557, - -1.259, - 350.443, - 56.472, - 352.72, - 57.544, - 1.64047, - 5114.509238, - ], - [ - styx["RA (deg)"].to(u.deg).value, - styx["Dec (deg)"].to(u.deg).value, - styx["dRA"].to(u.arcsec).value, - styx["dDec"].to(u.arcsec).value, - styx["sub_obs_lon"].to(u.deg).value, - styx["sub_obs_lat"].to(u.deg).value, - styx["sub_sun_lon"].to(u.deg).value, - styx["sub_sun_lat"].to(u.deg).value, - styx["phase"].to(u.deg).value, - styx["distance"].to(u.km * 1e6).value, - ], + [296.212477, -22.93533, -0.557, -1.259, 350.443, 56.472, 352.72, 57.544, 1.64047, 5114.509238, ], + [styx["RA (deg)"].to(u.deg).value, styx["Dec (deg)"].to(u.deg).value, styx["dRA"].to(u.arcsec).value, styx["dDec"].to(u.arcsec).value, styx["sub_obs_lon"].to(u.deg).value, styx["sub_obs_lat"].to(u.deg).value, styx["sub_sun_lon"].to(u.deg).value, styx["sub_sun_lat"].to(u.deg).value, styx["phase"].to(u.deg).value, styx["distance"].to(u.km * 1e6).value, ], rtol=1e-2, ) @@ -214,27 +126,10 @@ def test_ephemeris_query_Neptune(patch_request): ) assert np.allclose( - [63.81977, - 55.01978, - 44.21976, - 40.41978, - 26.41978, - 64.81977, - 59.11976, - 45.21976, - 43.41978, - 36.01978], - [ringtable[ringtable.loc_indices["Courage"]]["min_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Liberte"]]["min_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Egalite A"]]["min_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Egalite B"]]["min_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Fraternite"]]["min_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Courage"]]["max_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Liberte"]]["max_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Egalite A"]]["max_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Egalite B"]]["max_angle"].to(u.deg).value, - ringtable[ringtable.loc_indices["Fraternite"]]["max_angle"].to(u.deg).value, - ], rtol=1e-3) + [63.81977, 55.01978, 44.21976, 40.41978, 26.41978, 64.81977, 59.11976, 45.21976, 43.41978, 36.01978], + [ringtable[ringtable.loc_indices["Courage"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Liberte"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Egalite A"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Egalite B"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Fraternite"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Courage"]]["max_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Liberte"]]["max_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Egalite A"]]["max_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Egalite B"]]["max_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Fraternite"]]["max_angle"].to(u.deg).value, ], + rtol=1e-3 + ) def test_ephemeris_query_Saturn(patch_request): @@ -247,13 +142,10 @@ def test_ephemeris_query_Saturn(patch_request): ) assert np.allclose( - [249.23097, - 250.34081 - ], - [ - ringtable[ringtable.loc_indices["F"]]["pericenter"].to(u.deg).value, - ringtable[ringtable.loc_indices["F"]]["ascending node"].to(u.deg).value - ], rtol=1e-3) + [249.23097, 250.34081], + [ringtable[ringtable.loc_indices["F"]]["pericenter"].to(u.deg).value, ringtable[ringtable.loc_indices["F"]]["ascending node"].to(u.deg).value], + rtol=1e-3 + ) def test_ephemeris_query_payload(): diff --git a/astroquery/solarsystem/pds/tests/test_pds_remote.py b/astroquery/solarsystem/pds/tests/test_pds_remote.py index 157e95093b..b7f13c7191 100644 --- a/astroquery/solarsystem/pds/tests/test_pds_remote.py +++ b/astroquery/solarsystem/pds/tests/test_pds_remote.py @@ -17,30 +17,8 @@ def test_ephemeris_query(self): # check system table systemtable = bodytable.meta assert np.allclose( - [ - -56.12233, - -56.13586, - -56.13586, - -56.01577, - 0.10924, - 354.11072, - 354.12204, - 2947896667.0, - 3098568884.0, - 10335.713263, - ], - [ - systemtable["sub_sun_lat"].to(u.deg).value, - systemtable["sub_sun_lat_min"].to(u.deg).value, - systemtable["sub_sun_lat_max"].to(u.deg).value, - systemtable["opening_angle"].to(u.deg).value, - systemtable["phase_angle"].to(u.deg).value, - systemtable["sub_sun_lon"].to(u.deg).value, - systemtable["sub_obs_lon"].to(u.deg).value, - systemtable["d_sun"].to(u.km).value, - systemtable["d_obs"].to(u.km).value, - systemtable["light_time"].to(u.second).value, - ], + [-56.12233, -56.13586, -56.13586, -56.01577, 0.10924, 354.11072, 354.12204, 2947896667.0, 3098568884.0, 10335.713263, ], + [systemtable["sub_sun_lat"].to(u.deg).value, systemtable["sub_sun_lat_min"].to(u.deg).value, systemtable["sub_sun_lat_max"].to(u.deg).value, systemtable["opening_angle"].to(u.deg).value, systemtable["phase_angle"].to(u.deg).value, systemtable["sub_sun_lon"].to(u.deg).value, systemtable["sub_obs_lon"].to(u.deg).value, systemtable["d_sun"].to(u.km).value, systemtable["d_obs"].to(u.km).value, systemtable["light_time"].to(u.second).value, ], rtol=1e-2, ) @@ -49,30 +27,8 @@ def test_ephemeris_query(self): assert mab["NAIF ID"] == 726 assert mab["Body"] == "Mab" assert np.allclose( - [ - 42.011201, - 15.801323, - 5.368, - 0.623, - 223.976, - 55.906, - 223.969, - 56.013, - 0.10932, - 3098.514, - ], - [ - mab["RA (deg)"].to(u.deg).value, - mab["Dec (deg)"].to(u.deg).value, - mab["dRA"].to(u.arcsec).value, - mab["dDec"].to(u.arcsec).value, - mab["sub_obs_lon"].to(u.deg).value, - mab["sub_obs_lat"].to(u.deg).value, - mab["sub_sun_lon"].to(u.deg).value, - mab["sub_sun_lat"].to(u.deg).value, - mab["phase"].to(u.deg).value, - mab["distance"].to(u.km * 1e6).value, - ], + [42.011201, 15.801323, 5.368, 0.623, 223.976, 55.906, 223.969, 56.013, 0.10932, 3098.514, ], + [mab["RA (deg)"].to(u.deg).value, mab["Dec (deg)"].to(u.deg).value, mab["dRA"].to(u.arcsec).value, mab["dDec"].to(u.arcsec).value, mab["sub_obs_lon"].to(u.deg).value, mab["sub_obs_lat"].to(u.deg).value, mab["sub_sun_lon"].to(u.deg).value, mab["sub_sun_lat"].to(u.deg).value, mab["phase"].to(u.deg).value, mab["distance"].to(u.km * 1e6).value, ], rtol=1e-2, ) diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index 2679052439..e86cc92183 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -9,7 +9,8 @@ Overview The :class:`~astroquery.solarsystem.pds.RingNodeClass` provides an -interface to the ephemeris tools provided by the `NASA Planetary Data System's Ring Node System`_ hosted by SETI institute. +interface to the ephemeris tools provided by the `NASA Planetary Data System's Ring Node System `_ hosted by SETI institute. + Ephemeris ----------- @@ -18,14 +19,14 @@ In order to query information for a specific Solar System body, a ``RingNode`` object is instantiated and the :meth:`~astroquery.solarsystem.pds.RingNodeClass.ephemeris` method is called. The following example queries the ephemerides of the rings and small moons around Uranus as viewed from ALMA: -.. code-block:: python +.. doctest-remote-data:: >>> from astroquery.solarsystem.pds import RingNode >>> import astropy.units as u >>> bodytable, ringtable = RingNode.ephemeris(planet='Uranus', ... epoch='2024-05-08 22:39', - ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA - >>> print(ringtable) # doctest: +REMOTE_DATA + ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) + >>> print(ringtable) ring pericenter ascending node deg deg ------- ---------- -------------- @@ -42,11 +43,12 @@ ephemerides of the rings and small moons around Uranus as viewed from ALMA: ``planet`` must be one of ['mars', 'jupiter', 'uranus', 'saturn', 'neptune', 'pluto'] (case-insensitive) -.. code-block:: python + +.. doctest-remote-data:: >>> bodytable, ringtable = RingNode.ephemeris(planet='Venus', ... epoch='2024-05-08 22:39', - ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) # doctest: +REMOTE_DATA + ... location = (-67.755 * u.deg, -23.029 * u.deg, 5000 * u.m)) Traceback (most recent call last): ... ValueError: illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto') @@ -61,47 +63,42 @@ Outputs --------- ``bodytable`` is a `~astropy.table.QTable` containing ephemeris information on the moons in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: -.. code-block:: python +.. doctest-remote-data:: - >>> print(bodytable.columns) # doctest: +REMOTE_DATA + >>> print(bodytable.columns) ``ringtable`` is a `~astropy.table.QTable` containing ephemeris information on the individual rings in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: -.. code-block:: python +.. doctest-remote-data:: - >>> print(ringtable.columns) # doctest: +REMOTE_DATA + >>> print(ringtable.columns) - + Note that the behavior of ``ringtable`` changes depending on the planet you query. For Uranus and Saturn the table columns are as above. For Jupiter, Mars, and Pluto, there are no individual named rings returned by the Ring Node, so ``ringtable`` returns None; ephemeris for the ring systems of these bodies is still contained in ``systemtable`` as usual. For Neptune, the ring table shows the minimum and maximum longitudes (from the ring plane ascending node) of the five ring arcs according to the orbital evolution assumed by ``neptune_arcmodel``, e.g.: -.. code-block:: python - >>> bodytable, ringtable = RingNode.ephemeris(planet='Neptune', epoch='2022-05-24 00:00') # doctest: +REMOTE_DATA - >>> print(ringtable) # doctest: +REMOTE_DATA +.. doctest-remote-data:: + + >>> bodytable, ringtable = RingNode.ephemeris(planet='Neptune', epoch='2022-05-24 00:00') + >>> print(ringtable) ring min_angle max_angle - deg deg + deg deg ---------- --------- --------- Courage 53.4818 54.4818 Liberte 44.68181 48.78178 Egalite A 33.88179 34.88179 Egalite B 30.0818 33.0818 Fraternite 16.0818 25.68181 - -System-wide data are available as metadata in both ``bodytable`` and ``ringtable`` (if ``ringtable`` exists), e.g.: -.. code-block:: python - - >>> systemtable = bodytable.meta # doctest: +REMOTE_DATA - >>> print(systemtable.keys()) # doctest: +REMOTE_DATA - dict_keys(['sub_sun_lat', 'sub_sun_lat_min', 'sub_sun_lat_max', 'opening_angle', 'phase_angle', 'sub_sun_lon', 'sub_obs_lon', 'd_sun', 'd_obs', 'light_time', 'epoch']) +System-wide data are available as metadata in both ``bodytable`` and ``ringtable`` (if ``ringtable`` exists), e.g.: -Acknowledgements -================ +.. doctest-remote-data:: -This submodule makes use of the NASA Planetary Data System's `Planetary Ring Node -`_ . + >>> systemtable = bodytable.meta + >>> print(systemtable.keys()) + dict_keys(['sub_sun_lat', 'sub_sun_lat_min', 'sub_sun_lat_max', 'opening_angle', 'phase_angle', 'sub_sun_lon', 'sub_obs_lon', 'd_sun', 'd_obs', 'light_time', 'epoch']) Reference/API @@ -111,6 +108,3 @@ Reference/API :no-inheritance-diagram: .. _NASA Planetary Data System's Ring Node System: https://pds-rings.seti.org/ -.. _astropy Quantity: https://docs.astropy.org/en/stable/units/quantity.html -.. _astropy table: http://docs.astropy.org/en/stable/table/index.html -.. _astropy units: http://docs.astropy.org/en/stable/units/index.html \ No newline at end of file From 55cea03d82d635f7e73031b6e323572525ad61b2 Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Thu, 17 Nov 2022 14:59:14 -0800 Subject: [PATCH 22/25] changes suggested by @bsipocz, most importantly adding platform-independent newline handling --- astroquery/solarsystem/pds/core.py | 7 +++---- docs/solarsystem/pds/pds.rst | 5 ++--- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index 1b28063962..d27640bc6d 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -186,7 +186,6 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel ) # configure request_payload for ephemeris query - request_payload = dict( [ ("abbrev", planet[:3]), @@ -389,13 +388,13 @@ def _parse_result(self, response, verbose=None): # Neptune ring arcs data elif group.startswith("Courage"): - lines = group.split(os.linesep)[:5] #requires explicit split by os.linesep because windows and macos handle this text block differently + lines = group.splitlines() for i in range(len(lines)): l = lines[i].split(":") ring = l[0].split("longitude")[0].strip() [min_angle, max_angle] = [ - float(s) - for s in re.sub("[a-zA-Z]+", "", l[1]).split() + float(s.strip()) + for s in re.sub("[a-zA-Z]+", "", l[1]).strip("()").split() ] if i == 0: ringtable_names = ("ring", "min_angle", "max_angle") diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst index e86cc92183..ded553f3b3 100644 --- a/docs/solarsystem/pds/pds.rst +++ b/docs/solarsystem/pds/pds.rst @@ -63,6 +63,7 @@ Outputs --------- ``bodytable`` is a `~astropy.table.QTable` containing ephemeris information on the moons in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: + .. doctest-remote-data:: >>> print(bodytable.columns) @@ -70,6 +71,7 @@ Outputs ``ringtable`` is a `~astropy.table.QTable` containing ephemeris information on the individual rings in the planetary system. Every column is assigned a unit from `~astropy.units`. We can get a list of all the columns in this table with: + .. doctest-remote-data:: >>> print(ringtable.columns) @@ -93,7 +95,6 @@ Note that the behavior of ``ringtable`` changes depending on the planet you quer System-wide data are available as metadata in both ``bodytable`` and ``ringtable`` (if ``ringtable`` exists), e.g.: - .. doctest-remote-data:: >>> systemtable = bodytable.meta @@ -106,5 +107,3 @@ Reference/API .. automodapi:: astroquery.solarsystem.pds :no-inheritance-diagram: - -.. _NASA Planetary Data System's Ring Node System: https://pds-rings.seti.org/ From 6cc374980a4fa3d10210a41246a822ce730c4728 Mon Sep 17 00:00:00 2001 From: Edward Molter Date: Fri, 18 Nov 2022 14:12:11 -0800 Subject: [PATCH 23/25] changelog entry --- CHANGES.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index e3ff32a360..9a167d2d44 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -23,6 +23,12 @@ esa.hubble - Status and maintenance messages from eHST TAP when the module is instantiated. get_status_messages method to retrieve them. [#2597] - Optional parameters in all methods are kwargs keyword only. [#2597] +solarsystem.pds +^^^^^^^^^^^^^^^ + +- New module to access the Planetary Data System's Ring Node System. [#2358] + + Service fixes and enhancements ------------------------------ From 6448ab871ec071a4f111fe151daf57900f71b86b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sipo=CC=8Bcz?= Date: Mon, 12 Dec 2022 02:10:01 -0800 Subject: [PATCH 24/25] Addressing final review items --- astroquery/solarsystem/pds/core.py | 165 ++++++++----------- astroquery/solarsystem/pds/tests/test_pds.py | 54 +++--- 2 files changed, 101 insertions(+), 118 deletions(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index d27640bc6d..ecf2ac5309 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -4,7 +4,7 @@ # 2. third party imports from astropy.time import Time -from astropy import table +from astropy.table import QTable, join import astropy.units as u from astropy.coordinates import EarthLocation, Angle from bs4 import BeautifulSoup @@ -93,7 +93,8 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel Parameters ---------- - planet : str, required. one of Mars, Jupiter, Saturn, Uranus, Neptune, or Pluto + planet : str + One of "Mars", "Jupiter", "Saturn", "Uranus", "Neptune", or "Pluto". epoch : `~astropy.time.Time` object, or str in format YYYY-MM-DD hh:mm, optional. If str is provided then UTC is assumed. If no epoch is provided, the current time is used. @@ -107,8 +108,9 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel `~astropy.coordinates.Angle` object, and altitude should initialize an `~astropy.units.Quantity` object (with units of length). If ``None``, then the geofocus is used. - neptune_arcmodel : float, optional. which ephemeris to assume for Neptune's ring arcs - must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details) + neptune_arcmodel : int, optional. + which ephemeris to assume for Neptune's ring arcs + Must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details) has no effect if planet != 'Neptune' get_query_payload : boolean, optional When set to `True` the method returns the HTTP request parameters as @@ -147,10 +149,9 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel """ planet = planet.lower() - if planet not in ["mars", "jupiter", "saturn", "uranus", "neptune", "pluto", ]: - raise ValueError( - "illegal value for 'planet' parameter (must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto')" - ) + if planet not in planet_defaults: + raise ValueError("illegal value for 'planet' parameter " + "(must be 'Mars', 'Jupiter', 'Saturn', 'Uranus', 'Neptune', or 'Pluto')") if isinstance(epoch, (int, float)): epoch = Time(epoch, format='jd') @@ -174,13 +175,13 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel loc = location.geodetic longitude = loc[0].deg latitude = loc[1].deg - altitude = loc[2].to(u.m).value + altitude = loc[2].to_value(u.m) elif hasattr(location, "__iter__"): longitude = Angle(location[0]).deg latitude = Angle(location[1]).deg - altitude = u.Quantity(location[2]).to("m").value + altitude = u.Quantity(location[2]).to_value(u.m) - if int(neptune_arcmodel) not in [1, 2, 3]: + if neptune_arcmodel not in [1, 2, 3]: raise ValueError( f"Illegal Neptune arc model {neptune_arcmodel}. must be one of 1, 2, or 3 (see https://pds-rings.seti.org/tools/viewer3_nep.shtml for details)" ) @@ -209,7 +210,7 @@ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel ("altitude", altitude), ("moons", planet_defaults[planet]["moons"]), ("rings", planet_defaults[planet]["rings"]), - ("arcmodel", neptune_arcmodels[int(neptune_arcmodel)]), + ("arcmodel", neptune_arcmodels[neptune_arcmodel]), ("extra_ra", ""), # figure options below this line, can all be hardcoded and ignored ("extra_ra_type", "hours"), ("extra_dec", ""), @@ -251,15 +252,9 @@ def _parse_result(self, response, verbose=None): Returns ------- - bodytable : `astropy.QTable` - ringtable : `astropy.QTable` + bodytable : `~astropy.table.QTable` + ringtable : `~astropy.table.QTable` """ - try: - self._last_query.remove_cache_file(self.cache_location) - except FileNotFoundError: - # this is allowed: if `cache` was set to False, this - # won't be needed - pass soup = BeautifulSoup(response.text, "html.parser") text = soup.get_text() @@ -278,13 +273,11 @@ def _parse_result(self, response, verbose=None): group = "NAIF " + group # fixing lack of header for NAIF ID bodytable_names = ("NAIF ID", "Body", "RA", "Dec", "RA (deg)", "Dec (deg)", "dRA", "dDec") bodytable_units = [None, None, None, None, u.deg, u.deg, u.arcsec, u.arcsec] - bodytable = table.QTable.read(group, format="ascii.fixed_width", + bodytable = QTable.read(group, format="ascii.fixed_width", col_starts=(0, 4, 18, 35, 54, 68, 80, 91), col_ends=(4, 18, 35, 54, 68, 80, 91, 102), - names=bodytable_names) - # units=(bodytable_units)) # this much cleaner way of adding units is supported in later versions but not in 3.7 - for name, unit in zip(bodytable_names, bodytable_units): - bodytable[name].unit = unit + names=bodytable_names, + units=(bodytable_units)) # minor body table part 2 elif group.startswith("Sub-"): @@ -292,23 +285,20 @@ def _parse_result(self, response, verbose=None): group = os.linesep.join(group.splitlines()[2:]) # removing two-row header entirely bodytable2_names = ("NAIF ID", "Body", "sub_obs_lon", "sub_obs_lat", "sub_sun_lon", "sub_sun_lat", "phase", "distance") bodytable2_units = [None, None, u.deg, u.deg, u.deg, u.deg, u.deg, u.km * 1e6] - bodytable2 = table.QTable.read(group, format="ascii.fixed_width", - col_starts=(0, 4, 18, 28, 37, 49, 57, 71), - col_ends=(4, 18, 28, 37, 49, 57, 71, 90), - names=bodytable2_names, - data_start=0) - # units=(bodytable_units)) # this much cleaner way of adding units is supported in later versions but not in 3.7 - for name, unit in zip(bodytable2_names, bodytable2_units): - bodytable2[name].unit = unit + bodytable2 = QTable.read(group, format="ascii.fixed_width", + col_starts=(0, 4, 18, 28, 37, 49, 57, 71), + col_ends=(4, 18, 28, 37, 49, 57, 71, 90), + names=bodytable2_names, + data_start=0, + units=(bodytable2_units)) # ring plane data elif group.startswith("Ring s"): - lines = group.splitlines() - for line in lines: - l = line.split(":") - if "Ring sub-solar latitude" in l[0]: - [sub_sun_lat, sub_sun_lat_min, sub_sun_lat_max] = [ - float(s.strip("()")) for s in re.split(r"\(|to", l[1]) + for line in group.splitlines(): + lines = line.split(":") + if "Ring sub-solar latitude" in lines[0]: + sub_sun_lat, sub_sun_lat_min, sub_sun_lat_max = [ + float(s.strip("()")) for s in re.split(r"\(|to", lines[1]) ] systemtable = { "sub_sun_lat": sub_sun_lat * u.deg, @@ -316,100 +306,83 @@ def _parse_result(self, response, verbose=None): "sub_sun_lat_max": sub_sun_lat_min * u.deg, } - elif "Ring plane opening angle" in l[0]: + elif "Ring plane opening angle" in lines[0]: systemtable["opening_angle"] = ( - float(re.sub("[a-zA-Z]+", "", l[1]).strip("()")) * u.deg + float(re.sub("[a-zA-Z]+", "", lines[1]).strip("()")) * u.deg ) - elif "Ring center phase angle" in l[0]: - systemtable["phase_angle"] = float(l[1].strip()) * u.deg - elif "Sub-solar longitude" in l[0]: + elif "Ring center phase angle" in lines[0]: + systemtable["phase_angle"] = float(lines[1].strip()) * u.deg + elif "Sub-solar longitude" in lines[0]: systemtable["sub_sun_lon"] = ( - float(re.sub("[a-zA-Z]+", "", l[1]).strip("()")) * u.deg + float(re.sub("[a-zA-Z]+", "", lines[1]).strip("()")) * u.deg ) - elif "Sub-observer longitude" in l[0]: - systemtable["sub_obs_lon"] = float(l[1].strip()) * u.deg - else: - pass + elif "Sub-observer longitude" in lines[0]: + systemtable["sub_obs_lon"] = float(lines[1].strip()) * u.deg # basic info about the planet elif group.startswith("Sun-planet"): - lines = group.splitlines() - for line in lines: - l = line.split(":") - if "Sun-planet distance (AU)" in l[0]: + for line in group.splitlines(): + lines = line.split(":") + if "Sun-planet distance (AU)" in lines[0]: # this is redundant with sun distance in km pass - elif "Observer-planet distance (AU)" in l[0]: + elif "Observer-planet distance (AU)" in lines[0]: # this is redundant with observer distance in km pass - elif "Sun-planet distance (km)" in l[0]: + elif "Sun-planet distance (km)" in lines[0]: systemtable["d_sun"] = ( - float(l[1].split("x")[0].strip()) * 1e6 * u.km + float(lines[1].split("x")[0].strip()) * 1e6 * u.km ) - elif "Observer-planet distance (km)" in l[0]: + elif "Observer-planet distance (km)" in lines[0]: systemtable["d_obs"] = ( - float(l[1].split("x")[0].strip()) * 1e6 * u.km + float(lines[1].split("x")[0].strip()) * 1e6 * u.km ) - elif "Light travel time" in l[0]: - systemtable["light_time"] = float(l[1].strip()) * u.second - else: - pass + elif "Light travel time" in lines[0]: + systemtable["light_time"] = float(lines[1].strip()) * u.second # --------- below this line, planet-specific info ------------ # Uranus individual rings data elif group.startswith("Ring "): ringtable_names = ("ring", "pericenter", "ascending node") ringtable_units = [None, u.deg, u.deg] - ringtable = table.QTable.read(" " + group, format="ascii.fixed_width", - col_starts=(5, 18, 29), - col_ends=(18, 29, 36), - names=ringtable_names) - # units=(ringtable_units)) # this much cleaner way of adding units is supported in later versions but not in 3.7 - for name, unit in zip(ringtable_names, ringtable_units): - ringtable[name].unit = unit + ringtable = QTable.read(" " + group, format="ascii.fixed_width", + col_starts=(5, 18, 29), + col_ends=(18, 29, 36), + names=ringtable_names, + units=(ringtable_units)) # Saturn F-ring data elif group.startswith("F Ring"): - lines = group.splitlines() - for line in lines: - l = line.split(":") - if "F Ring pericenter" in l[0]: - peri = float(re.sub("[a-zA-Z]+", "", l[1]).strip("()")) - elif "F Ring ascending node" in l[0]: - ascn = float(l[1].strip()) + for line in group.splitlines(): + lines = line.split(":") + if "F Ring pericenter" in lines[0]: + peri = float(re.sub("[a-zA-Z]+", "", lines[1]).strip("()")) + elif "F Ring ascending node" in lines[0]: + ascn = float(lines[1].strip()) ringtable_names = ("ring", "pericenter", "ascending node") ringtable_units = [None, u.deg, u.deg] - ringtable = table.QTable( - [["F"], [peri], [ascn]], - names=ringtable_names) - # units=(ringtable_units) # this much cleaner way of adding units is supported in later versions but not in 3.7 - for name, unit in zip(ringtable_names, ringtable_units): - ringtable[name].unit = unit + ringtable = QTable([["F"], [peri], [ascn]], + names=ringtable_names, + units=(ringtable_units)) # Neptune ring arcs data elif group.startswith("Courage"): - lines = group.splitlines() - for i in range(len(lines)): - l = lines[i].split(":") - ring = l[0].split("longitude")[0].strip() + for i, line in enumerate(group.splitlines()): + lines = line.split(":") + ring = lines[0].split("longitude")[0].strip() [min_angle, max_angle] = [ float(s.strip()) - for s in re.sub("[a-zA-Z]+", "", l[1]).strip("()").split() + for s in re.sub("[a-zA-Z]+", "", lines[1]).strip("()").split() ] if i == 0: ringtable_names = ("ring", "min_angle", "max_angle") ringtable_units = [None, u.deg, u.deg] - ringtable = table.QTable( - [[ring], [min_angle], [max_angle]], - names=ringtable_names) - for name, unit in zip(ringtable_names, ringtable_units): - ringtable[name].unit = unit - # units=(ringtable_units) # this much cleaner way of adding units is supported in later versions but not in 3.7 + ringtable = QTable([[ring], [min_angle], [max_angle]], + names=ringtable_names, + units=ringtable_units) else: ringtable.add_row([ring, min_angle*u.deg, max_angle*u.deg]) - else: - pass # do some cleanup from the parsing job # and make system-wide parameters metadata of bodytable and ringtable @@ -418,7 +391,7 @@ def _parse_result(self, response, verbose=None): ringtable.add_index("ring") ringtable.meta = systemtable - bodytable = table.join(bodytable, bodytable2) # concatenate minor body table + bodytable = join(bodytable, bodytable2) # concatenate minor body table bodytable.add_index("Body") bodytable.meta = systemtable diff --git a/astroquery/solarsystem/pds/tests/test_pds.py b/astroquery/solarsystem/pds/tests/test_pds.py index 49a3fe8246..471a6b10a6 100644 --- a/astroquery/solarsystem/pds/tests/test_pds.py +++ b/astroquery/solarsystem/pds/tests/test_pds.py @@ -3,6 +3,8 @@ import numpy as np import astropy.units as u +from astropy.table import QTable + from ....query import AstroQuery from astroquery.utils.mocks import MockResponse @@ -62,21 +64,27 @@ def test_ephemeris_query_Uranus(patch_request): ) # check system table systemtable = bodytable.meta + assert isinstance(systemtable, dict) + assert np.allclose( [-56.12233, -56.13586, -56.13586, -56.01577, 0.10924, 354.11072, 354.12204, 2947896667.0, 3098568884.0, 10335.713263, ], [systemtable["sub_sun_lat"].to(u.deg).value, systemtable["sub_sun_lat_min"].to(u.deg).value, systemtable["sub_sun_lat_max"].to(u.deg).value, systemtable["opening_angle"].to(u.deg).value, systemtable["phase_angle"].to(u.deg).value, systemtable["sub_sun_lon"].to(u.deg).value, systemtable["sub_obs_lon"].to(u.deg).value, systemtable["d_sun"].to(u.km).value, systemtable["d_obs"].to(u.km).value, systemtable["light_time"].to(u.second).value, ], rtol=1e-2, ) - # check a moon in body table - mab = bodytable[bodytable.loc_indices["Mab"]] + expected_mab = """ +NAIF ID,Body, RA,Dec,RA (deg),Dec (deg),dRA,dDec,sub_obs_lon,sub_obs_lat,sub_sun_lon,sub_sun_lat,phase,distance +726,Mab,2h 48m 02.6883s,15d 48m 04.764s,42.011201,15.801323,5.368,0.6233,223.976,55.906,223.969,56.013,0.10932,3098.514 + """ + expected_mab = QTable.read(expected_mab, format='ascii.csv', + units=[None] * 4 + ['deg'] * 2 + ['arcsec'] * 2 + ['deg'] * 5 + ['Gm']) + + # check the moon Mab in body table. Slicing to make sure we get a 1 long QTable instead of a Row + mab = bodytable[16:17] + assert mab["NAIF ID"] == 726 assert mab["Body"] == "Mab" - assert np.allclose( - [42.011201, 15.801323, 5.368, 0.623, 223.976, 55.906, 223.969, 56.013, 0.10932, 3098.514, ], - [mab["RA (deg)"].to(u.deg).value, mab["Dec (deg)"].to(u.deg).value, mab["dRA"].to(u.arcsec).value, mab["dDec"].to(u.arcsec).value, mab["sub_obs_lon"].to(u.deg).value, mab["sub_obs_lat"].to(u.deg).value, mab["sub_sun_lon"].to(u.deg).value, mab["sub_sun_lat"].to(u.deg).value, mab["phase"].to(u.deg).value, mab["distance"].to(u.km * 1e6).value, ], - rtol=1e-2, - ) + assert expected_mab.values_equal(mab) # check a ring in ringtable beta = ringtable[ringtable.loc_indices["Beta"]] @@ -110,7 +118,6 @@ def test_ephemeris_query_Pluto(patch_request): rtol=1e-2, ) - # check ringtable is None assert ringtable is None @@ -125,11 +132,16 @@ def test_ephemeris_query_Neptune(patch_request): neptune_arcmodel=2 ) - assert np.allclose( - [63.81977, 55.01978, 44.21976, 40.41978, 26.41978, 64.81977, 59.11976, 45.21976, 43.41978, 36.01978], - [ringtable[ringtable.loc_indices["Courage"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Liberte"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Egalite A"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Egalite B"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Fraternite"]]["min_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Courage"]]["max_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Liberte"]]["max_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Egalite A"]]["max_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Egalite B"]]["max_angle"].to(u.deg).value, ringtable[ringtable.loc_indices["Fraternite"]]["max_angle"].to(u.deg).value, ], - rtol=1e-3 - ) + expected = """ring,min_angle,max_angle +Courage,63.81977,64.81977 +Liberte,55.01978,59.11976 +Egalite A,44.21976,45.21976 +Egalite B,40.41978,43.41978 +Fraternite,26.41978,36.01978 + """ + expected = QTable.read(expected, format='ascii.csv', units=(None, 'deg', 'deg')) + + assert (expected == ringtable).all() def test_ephemeris_query_Saturn(patch_request): @@ -141,11 +153,12 @@ def test_ephemeris_query_Saturn(patch_request): epoch="2021-10-07 07:25", ) - assert np.allclose( - [249.23097, 250.34081], - [ringtable[ringtable.loc_indices["F"]]["pericenter"].to(u.deg).value, ringtable[ringtable.loc_indices["F"]]["ascending node"].to(u.deg).value], - rtol=1e-3 - ) + expected = """ring,pericenter,ascending node +F,249.23097,250.34081 + """ + + expected = QTable.read(expected, format='ascii.csv', units=(None, 'deg', 'deg')) + assert (expected == ringtable).all() def test_ephemeris_query_payload(): @@ -213,7 +226,4 @@ def test_ephemeris_query_payload(): def test_bad_query_raise(): with pytest.raises(ValueError): - bodytable, ringtable = pds.RingNode.ephemeris( - planet="Venus", - epoch="2021-10-07 07:25", - ) + bodytable, ringtable = pds.RingNode.ephemeris(planet="Venus", epoch="2021-10-07 07:25") From 51ead5f11b9fce9d68c5b5df5e1821662f5ad7a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brigitta=20Sip=C5=91cz?= Date: Wed, 4 Jan 2023 20:24:00 -0800 Subject: [PATCH 25/25] Fix windows parsing --- astroquery/solarsystem/pds/core.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py index ecf2ac5309..c4e5028c85 100644 --- a/astroquery/solarsystem/pds/core.py +++ b/astroquery/solarsystem/pds/core.py @@ -256,11 +256,12 @@ def _parse_result(self, response, verbose=None): ringtable : `~astropy.table.QTable` """ - soup = BeautifulSoup(response.text, "html.parser") + soup = BeautifulSoup(response.text, "html5lib") text = soup.get_text() # need regex because some blank lines have spacebar and some do not - textgroups = re.split(2*os.linesep+"|"+os.linesep+" "+os.linesep, text) + textgroups = re.split("\n\n|\n \n", text.strip()) ringtable = None + for group in textgroups: group = group.strip() @@ -383,7 +384,6 @@ def _parse_result(self, response, verbose=None): else: ringtable.add_row([ring, min_angle*u.deg, max_angle*u.deg]) - # do some cleanup from the parsing job # and make system-wide parameters metadata of bodytable and ringtable systemtable["epoch"] = Time(epoch, format="iso", scale="utc") # add obs time to systemtable