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/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
------------------------------
diff --git a/astroquery/solarsystem/pds/__init__.py b/astroquery/solarsystem/pds/__init__.py
new file mode 100644
index 0000000000..fc326f0c62
--- /dev/null
+++ b/astroquery/solarsystem/pds/__init__.py
@@ -0,0 +1,35 @@
+"""
+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
+ 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 (seconds).")
+
+
+conf = Conf()
+
+from .core import RingNode, RingNodeClass
+
+__all__ = [
+ "RingNode",
+ "RingNodeClass",
+ "Conf",
+ "conf",
+]
diff --git a/astroquery/solarsystem/pds/core.py b/astroquery/solarsystem/pds/core.py
new file mode 100644
index 0000000000..c4e5028c85
--- /dev/null
+++ b/astroquery/solarsystem/pds/core.py
@@ -0,0 +1,401 @@
+# 1. standard library imports
+import re
+import os
+
+# 2. third party imports
+from astropy.time import Time
+from astropy.table import QTable, join
+import astropy.units as u
+from astropy.coordinates import EarthLocation, Angle
+from bs4 import BeautifulSoup
+
+# 3. local imports - use relative imports
+# all Query classes should inherit from BaseQuery.
+from ...query import BaseQuery
+from ...utils import async_to_sync
+
+# 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):
+ """
+ a class for querying the Planetary Ring Node ephemeris tools
+
+ """
+
+ def __init__(self, url='', timeout=None):
+ '''
+ Instantiate Planetary Ring Node query
+ '''
+ super().__init__()
+ self.url = url
+ self.timeout = timeout
+
+ @property
+ def _url(self):
+ return self.url or conf.url
+
+ def __str__(self):
+
+ return "PDSRingNode instance"
+
+ def ephemeris_async(self, planet, *, epoch=None, location=None, neptune_arcmodel=3,
+ get_query_payload=False, cache=True):
+ """
+ send query to Planetary Ring Node server
+
+ Parameters
+ ----------
+ 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.
+ 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 geofocus is used.
+ 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
+ a dict, default: False
+ cache : boolean, optional
+ When set to `True` the method caches the download, default: True
+
+
+ Returns
+ -------
+ response : `requests.Response`
+ The response of the HTTP request.
+
+
+ Examples
+ --------
+ >>> from astroquery.solarsystem.pds import RingNode
+ >>> import astropy.units as u
+ >>> 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
+ 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.lower()
+ 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')
+ elif isinstance(epoch, str):
+ epoch = Time(epoch, format='iso')
+ elif epoch is None:
+ epoch = Time.now()
+
+ 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
+ latitude = loc[1].deg
+ 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_value(u.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)"
+ )
+
+ # configure request_payload for ephemeris query
+ request_payload = dict(
+ [
+ ("abbrev", planet[:3]),
+ ("ephem", planet_defaults[planet]["ephem"]),
+ ("time", epoch.utc.iso[:16]),
+ ("fov", 10), # figure option - hardcode ok
+ ("fov_unit", planet.capitalize() + " radii"),
+ ("center", "body"),
+ ("center_body", planet.capitalize()),
+ ("center_ansa", planet_defaults[planet]["center_ansa"]),
+ ("center_ew", "east"),
+ ("center_ra", ""),
+ ("center_ra_type", "hours"),
+ ("center_dec", ""),
+ ("center_star", ""),
+ ("viewpoint", viewpoint),
+ ("observatory", observatory),
+ ("latitude", latitude),
+ ("longitude", longitude),
+ ("lon_dir", "east"),
+ ("altitude", altitude),
+ ("moons", planet_defaults[planet]["moons"]),
+ ("rings", planet_defaults[planet]["rings"]),
+ ("arcmodel", 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"),
+ ]
+ )
+
+ # return request_payload if desired
+ if get_query_payload:
+ return request_payload
+
+ # query and parse
+ response = self._request(
+ "GET", self._url, params=request_payload, timeout=self.timeout, cache=cache
+ )
+
+ return response
+
+ def _parse_result(self, response, verbose=None):
+ """
+ Routine for parsing data from ring node
+
+ Parameters
+ ----------
+ self : RingNodeClass instance
+ response : list
+ raw response from server
+
+
+ Returns
+ -------
+ bodytable : `~astropy.table.QTable`
+ ringtable : `~astropy.table.QTable`
+ """
+
+ soup = BeautifulSoup(response.text, "html5lib")
+ text = soup.get_text()
+ # need regex because some blank lines have spacebar and some do not
+ textgroups = re.split("\n\n|\n \n", text.strip())
+ ringtable = None
+
+ for group in textgroups:
+ group = group.strip()
+
+ # input parameters. only thing needed is epoch
+ if group.startswith("Observation"):
+ epoch = group.splitlines()[0].split("e: ")[-1].strip()
+
+ # 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 = 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))
+
+ # minor body table part 2
+ elif group.startswith("Sub-"):
+
+ 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 = 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"):
+ 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,
+ "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 lines[0]:
+ systemtable["opening_angle"] = (
+ float(re.sub("[a-zA-Z]+", "", lines[1]).strip("()")) * u.deg
+ )
+ 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]+", "", lines[1]).strip("()")) * u.deg
+ )
+ 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"):
+ 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 lines[0]:
+ # this is redundant with observer distance in km
+ pass
+ elif "Sun-planet distance (km)" in lines[0]:
+ systemtable["d_sun"] = (
+ float(lines[1].split("x")[0].strip()) * 1e6 * u.km
+ )
+ elif "Observer-planet distance (km)" in lines[0]:
+ systemtable["d_obs"] = (
+ float(lines[1].split("x")[0].strip()) * 1e6 * u.km
+ )
+ 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 = 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"):
+ 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 = QTable([["F"], [peri], [ascn]],
+ names=ringtable_names,
+ units=(ringtable_units))
+
+ # Neptune ring arcs data
+ elif group.startswith("Courage"):
+ 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]+", "", lines[1]).strip("()").split()
+ ]
+ if i == 0:
+ ringtable_names = ("ring", "min_angle", "max_angle")
+ ringtable_units = [None, u.deg, u.deg]
+ 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])
+
+ # 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 = join(bodytable, bodytable2) # concatenate minor body table
+ bodytable.add_index("Body")
+ bodytable.meta = systemtable
+
+ return bodytable, ringtable
+
+
+RingNode = RingNodeClass()
diff --git a/astroquery/solarsystem/pds/tests/__init__.py b/astroquery/solarsystem/pds/tests/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
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/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/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/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_pds.py b/astroquery/solarsystem/pds/tests/test_pds.py
new file mode 100644
index 0000000000..471a6b10a6
--- /dev/null
+++ b/astroquery/solarsystem/pds/tests/test_pds.py
@@ -0,0 +1,229 @@
+import pytest
+import os
+import numpy as np
+
+import astropy.units as u
+from astropy.table import QTable
+
+from ....query import AstroQuery
+
+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',
+ 'Neptune': 'neptune_ephemeris.html',
+ 'Saturn': 'saturn_ephemeris.html',
+ }
+
+
+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):
+
+ 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
+
+
+# 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_parse_result(patch_request):
+ q = pds.RingNode()
+ # need _last_query to be defined
+ q._last_query = AstroQuery('GET', 'http://dummy')
+ with pytest.raises(ValueError):
+ q.ephemeris('dummy-planet-name')
+
+
+def test_ephemeris_query_Uranus(patch_request):
+
+ 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),
+ )
+ # 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,
+ )
+
+ 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 expected_mab.values_equal(mab)
+
+ # 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_Pluto(patch_request):
+
+ 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",
+ )
+ 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, ],
+ 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 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, ],
+ rtol=1e-2,
+ )
+
+ assert ringtable is None
+
+
+def test_ephemeris_query_Neptune(patch_request):
+ '''Verify that the Neptune ring arcs are queried properly'''
+
+ 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
+ )
+
+ 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):
+ '''Check Saturn F ring is queried properly'''
+ 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",
+ )
+
+ 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():
+ 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,
+ location=(-120.355 * u.deg, 10.0 * u.deg, 1000 * u.m),
+ get_query_payload=True,
+ )
+
+ assert res == dict(
+ [
+ ("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",
+ ),
+ ("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"),
+ ]
+ )
+
+
+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_pds_remote.py b/astroquery/solarsystem/pds/tests/test_pds_remote.py
new file mode 100644
index 0000000000..b7f13c7191
--- /dev/null
+++ b/astroquery/solarsystem/pds/tests/test_pds_remote.py
@@ -0,0 +1,38 @@
+import pytest
+import numpy as np
+import astropy.units as u
+
+from ... import pds
+
+
+@pytest.mark.remote_data
+class TestRingNodeClass:
+ def test_ephemeris_query(self):
+
+ bodytable, ringtable = pds.RingNode.ephemeris(
+ planet="Uranus",
+ epoch="2022-05-03 00:00",
+ location=(-120.355 * u.deg, 10.0 * u.deg, 1000 * u.m),
+ )
+ # 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, ],
+ 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 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,
+ )
+
+ # 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)
diff --git a/docs/solarsystem/pds/pds.rst b/docs/solarsystem/pds/pds.rst
new file mode 100644
index 0000000000..ded553f3b3
--- /dev/null
+++ b/docs/solarsystem/pds/pds.rst
@@ -0,0 +1,109 @@
+.. _astroquery.solarsystem.pds:
+
+***********************************************************************************
+PDS Planetary Ring Node Queries (`astroquery.solarsystem.pds`)
+***********************************************************************************
+
+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.
+
+
+Ephemeris
+-----------
+
+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:
+
+.. 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))
+ >>> 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)
+
+
+.. 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))
+ 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 (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
+---------
+``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)
+
+
+``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)
+
+
+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.:
+
+
+.. doctest-remote-data::
+
+ >>> bodytable, ringtable = RingNode.ephemeris(planet='Neptune', epoch='2022-05-24 00:00')
+ >>> print(ringtable)
+ 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
+
+System-wide data are available as metadata in both ``bodytable`` and ``ringtable`` (if ``ringtable`` exists), e.g.:
+
+.. doctest-remote-data::
+
+ >>> 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
+=============
+
+.. automodapi:: astroquery.solarsystem.pds
+ :no-inheritance-diagram:
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
=============