diff --git a/CHANGES.rst b/CHANGES.rst index 529a5408a2..8f8d833d4e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -9,6 +9,13 @@ ipac.irsa - New class to access the Moving Object Search Tool (MOST) added. [#2660] +- The IRSA module's backend has been refactored to favour VO services and to + run the queries through TAP rather than Gator. + New method ``query_tap`` is added to enable ADQL queries, async-named + methods have been removed. The ``selcols`` kwarg has been renamed to + ``columns``, and the ``cache`` and ``verbose`` kwargs have been + deprecated as they have no effect. [#2823] + gaia ^^^^ diff --git a/astroquery/ipac/irsa/__init__.py b/astroquery/ipac/irsa/__init__.py index eea64d252f..8ccf7d6d06 100644 --- a/astroquery/ipac/irsa/__init__.py +++ b/astroquery/ipac/irsa/__init__.py @@ -4,8 +4,8 @@ =============== This module contains various methods for querying the -IRSA Catalog Query Service(CatQuery) and the Moving -Object Search Tool (MOST). +IRSA Services. + """ from astropy import config as _config @@ -14,13 +14,6 @@ class Conf(_config.ConfigNamespace): """ Configuration parameters for `astroquery.ipac.irsa`. """ - - irsa_server = _config.ConfigItem( - 'https://irsa.ipac.caltech.edu/cgi-bin/Gator/nph-query', - 'Name of the IRSA mirror to use.') - gator_list_catalogs = _config.ConfigItem( - 'https://irsa.ipac.caltech.edu/cgi-bin/Gator/nph-scan', - 'URL from which to list all the public catalogs in IRSA.') most_server = _config.ConfigItem( 'https://irsa.ipac.caltech.edu/cgi-bin/MOST/nph-most', 'URL address of the MOST service.') @@ -34,6 +27,7 @@ class Conf(_config.ConfigNamespace): timeout = _config.ConfigItem( 60, 'Time limit for connecting to the IRSA server.') + tap_url = _config.ConfigItem('https://irsa.ipac.caltech.edu/TAP', 'IRSA TAP URL') conf = Conf() diff --git a/astroquery/ipac/irsa/core.py b/astroquery/ipac/irsa/core.py index 250ac1c04b..6f86407a11 100644 --- a/astroquery/ipac/irsa/core.py +++ b/astroquery/ipac/irsa/core.py @@ -3,156 +3,88 @@ IRSA ==== -API from - https://irsa.ipac.caltech.edu/applications/Gator/GatorAid/irsa/catsearch.html - -The URL of the IRSA catalog query service, CatQuery, is - - https://irsa.ipac.caltech.edu/cgi-bin/Gator/nph-query - -The service accepts the following keywords, which are analogous to the search -fields on the Gator search form: - - -spatial Required Type of spatial query: Cone, Box, Polygon, and NONE - -polygon Convex polygon of ra dec pairs, separated by comma(,) - Required if spatial=polygon - -radius Cone search radius - Optional if spatial=Cone, otherwise ignore it - (default 10 arcsec) - -radunits Units of a Cone search: arcsec, arcmin, deg. - Optional if spatial=Cone - (default='arcsec') - -size Width of a box in arcsec - Required if spatial=Box. - -objstr Target name or coordinate of the center of a spatial - search center. Target names must be resolved by - SIMBAD or NED. - - Required only when spatial=Cone or spatial=Box. - - Examples: 'M31' - '00 42 44.3 -41 16 08' - '00h42m44.3s -41d16m08s' - -catalog Required Catalog name in the IRSA database management system. - -selcols Optional Target column list with value separated by a comma(,) - - The input list always overwrites default selections - defined by a data dictionary. Full lists of columns - can be found at the IRSA catalogs website, e.g. - https://irsa.ipac.caltech.edu/cgi-bin/Gator/nph-dd?catalog=allsky_4band_p1bs_psd - To access the full list of columns, press - the "Long Form" button at the top of the Columns - table. - -outfmt Optional Defines query's output format. - 6 - returns a program interface in XML - 3 - returns a VO Table (XML) - 2 - returns SVC message - 1 - returns an ASCII table - 0 - returns Gator Status Page in HTML (default) - -desc Optional Short description of a specific catalog, which will - appear in the result page. - -order Optional Results ordered by this column. - -constraint Optional User defined query constraint(s) - Note: The constraint should follow SQL syntax. - -onlist Optional 1 - catalog is visible through Gator web interface - (default) - - 0 - catalog has been ingested into IRSA but not yet - visible through web interface. - - This parameter will generally only be set to 0 when - users are supporting testing and evaluation of new - catalogs at IRSA's request. - -If onlist=0, the following parameters are required: - - server Symbolic DataBase Management Server (DBMS) name - - database Name of Database. - - ddfile The data dictionary file is used to get column - information for a specific catalog. - - selcols Target column list with value separated by a comma(,) - - The input list always overwrites default selections - defined by a data dictionary. - - outrows Number of rows retrieved from database. - - The retrieved row number outrows is always less than or - equal to available to be retrieved rows under the same - constraints. +Module to query the IRSA archive. """ import warnings -from io import BytesIO -import xml.etree.ElementTree as tree - -import astropy.units as u -import astropy.coordinates as coord -import astropy.io.votable as votable - +from astropy.coordinates import SkyCoord, Angle +from astropy import units as u +from astropy.utils.decorators import deprecated_renamed_argument +from pyvo.dal import TAPService +from astroquery import log from astroquery.query import BaseQuery -from astroquery.utils import commons, async_to_sync +from astroquery.utils.commons import parse_coordinates from astroquery.ipac.irsa import conf -from astroquery.exceptions import TableParseError, NoResultsWarning, InvalidQueryError +from astroquery.exceptions import InvalidQueryError __all__ = ['Irsa', 'IrsaClass'] -@async_to_sync class IrsaClass(BaseQuery): - IRSA_URL = conf.irsa_server - GATOR_LIST_URL = conf.gator_list_catalogs - TIMEOUT = conf.timeout - ROW_LIMIT = conf.row_limit - def query_region_async(self, coordinates=None, *, catalog=None, - spatial='Cone', radius=10 * u.arcsec, width=None, - polygon=None, get_query_payload=False, - selcols=None, verbose=False, cache=True): + def __init__(self): + super().__init__() + self.tap_url = conf.tap_url + self._tap = None + + @property + def tap(self): + if not self._tap: + self._tap = TAPService(baseurl=self.tap_url) + return self._tap + + def query_tap(self, query, *, maxrec=None): + """ + Send query to IRSA TAP. Results in `~pyvo.dal.TAPResults` format. + result.to_qtable in `~astropy.table.QTable` format + + Parameters + ---------- + query : str + ADQL query to be executed + maxrec : int + maximum number of records to return + + Returns + ------- + result : `~pyvo.dal.TAPResults` + TAP query result. + result.to_table : `~astropy.table.Table` + TAP query result as `~astropy.table.Table` + result.to_qtable : `~astropy.table.QTable` + TAP query result as `~astropy.table.QTable` + + """ + log.debug(f'TAP query: {query}') + return self.tap.search(query, language='ADQL', maxrec=maxrec) + + @deprecated_renamed_argument(("selcols", "cache", "verbose"), ("columns", None, None), since="0.4.7") + def query_region(self, coordinates=None, *, catalog=None, spatial='Cone', + radius=10 * u.arcsec, width=None, polygon=None, + get_query_payload=False, columns=None, + verbose=False, cache=True): """ - This function serves the same purpose as - :meth:`~astroquery.ipac.irsa.IrsaClass.query_region`, but returns the raw - HTTP response rather than the results in a `~astropy.table.Table`. + Queries the IRSA TAP server around a coordinate and returns a `~astropy.table.Table` object. Parameters ---------- coordinates : str, `astropy.coordinates` object - Gives the position of the center of the cone or box if - performing a cone or box search. The string can give coordinates - in various coordinate systems, or the name of a source that will - be resolved on the server (see `here - `_ for more - details). Required if spatial is ``'Cone'`` or ``'Box'``. Optional - if spatial is ``'Polygon'``. + Gives the position of the center of the cone or box if performing a cone or box search. + Required if spatial is ``'Cone'`` or ``'Box'``. Ignored if spatial is ``'Polygon'`` or + ``'All-Sky'``. catalog : str The catalog to be used. To list the available catalogs, use - :meth:`~astroquery.ipac.irsa.IrsaClass.print_catalogs`. + :meth:`~astroquery.ipac.irsa.IrsaClass.list_catalogs`. spatial : str Type of spatial query: ``'Cone'``, ``'Box'``, ``'Polygon'``, and - ``'All-Sky'``. If missing then defaults to ``'Cone'``. + ``'All-Sky'``. Defaults to ``'Cone'``. radius : str or `~astropy.units.Quantity` object, [optional for spatial is ``'Cone'``] The string must be parsable by `~astropy.coordinates.Angle`. The appropriate `~astropy.units.Quantity` object from `astropy.units` may also be used. Defaults to 10 arcsec. - width : str, `~astropy.units.Quantity` object [Required for spatial is ``'Polygon'``.] + width : str, `~astropy.units.Quantity` object [Required for spatial is ``'Box'``.] The string must be parsable by `~astropy.coordinates.Angle`. The appropriate `~astropy.units.Quantity` object from `astropy.units` may also be used. @@ -164,299 +96,88 @@ def query_region_async(self, coordinates=None, *, catalog=None, get_query_payload : bool, optional If `True` then returns the dictionary sent as the HTTP request. Defaults to `False`. - selcols : str, optional + columns : str, optional Target column list with value separated by a comma(,) - verbose : bool, optional. - If `True` then displays warnings when the returned VOTable does not - conform to the standard. Defaults to `False`. - cache : bool, optional - Use local cache when set to `True`. Returns ------- - response : `requests.Response` - The HTTP response returned from the service + table : A `~astropy.table.Table` object. """ if catalog is None: raise InvalidQueryError("Catalog name is required!") - request_payload = self._args_to_payload(catalog, selcols=selcols) - request_payload.update(self._parse_spatial(spatial=spatial, - coordinates=coordinates, - radius=radius, width=width, - polygon=polygon)) - - if get_query_payload: - return request_payload - response = self._request("GET", url=Irsa.IRSA_URL, - params=request_payload, timeout=Irsa.TIMEOUT, - cache=cache) - return response - - def _parse_spatial(self, spatial, coordinates, radius=None, width=None, - polygon=None): - """ - Parse the spatial component of a query - - Parameters - ---------- - spatial : str - The type of spatial query. Must be one of: ``'Cone'``, ``'Box'``, - ``'Polygon'``, and ``'All-Sky'``. - coordinates : str, `astropy.coordinates` object - Gives the position of the center of the cone or box if - performing a cone or box search. The string can give coordinates - in various coordinate systems, or the name of a source that will - be resolved on the server (see `here - `_ for more - details). Required if spatial is ``'Cone'`` or ``'Box'``. Optional - if spatial is ``'Polygon'``. - radius : str or `~astropy.units.Quantity` object, [optional for spatial is ``'Cone'``] - The string must be parsable by `~astropy.coordinates.Angle`. The - appropriate `~astropy.units.Quantity` object from `astropy.units` - may also be used. Defaults to 10 arcsec. - width : str, `~astropy.units.Quantity` object [Required for spatial is ``'Polygon'``.] - The string must be parsable by `~astropy.coordinates.Angle`. The - appropriate `~astropy.units.Quantity` object from `astropy.units` - may also be used. - polygon : list, [Required for spatial is ``'Polygon'``] - A list of ``(ra, dec)`` pairs as tuples of - `astropy.coordinates.Angle`s outlining the polygon to search in. - It can also be a list of `astropy.coordinates` object or strings - that can be parsed by `astropy.coordinates.ICRS`. - - Returns - ------- - payload_dict : dict - """ + if columns is None: + columns = '*' - request_payload = {} + adql = f'SELECT {columns} FROM {catalog}' if spatial == 'All-Sky': - spatial = 'NONE' - elif spatial in ['Cone', 'Box']: - if not commons._is_coordinate(coordinates): - request_payload['objstr'] = coordinates - else: - request_payload['objstr'] = _parse_coordinates(coordinates) - if spatial == 'Cone': - radius = _parse_dimension(radius) - request_payload['radius'] = radius.value - request_payload['radunits'] = radius.unit.to_string() - else: - width = _parse_dimension(width) - request_payload['size'] = width.to(u.arcsec).value + where = '' elif spatial == 'Polygon': - if coordinates is not None: - if commons._is_coordinate(coordinates): - request_payload['objstr'] = _parse_coordinates(coordinates) - else: - request_payload['objstr'] = coordinates try: - coordinates_list = [_parse_coordinates(c) for c in polygon] - except (ValueError, TypeError): - coordinates_list = [_format_decimal_coords(*_pair_to_deg(pair)) - for pair in polygon] - request_payload['polygon'] = ','.join(coordinates_list) + coordinates_list = [parse_coordinates(coord).icrs for coord in polygon] + except TypeError: + # to handle the input cases that worked before + try: + coordinates_list = [SkyCoord(*coord).icrs for coord in polygon] + except u.UnitTypeError: + warnings.warn("Polygon endpoints are being interpreted as " + "RA/Dec pairs specified in decimal degree units.") + coordinates_list = [SkyCoord(*coord, unit='deg').icrs for coord in polygon] + + coordinates_str = [f'{coord.ra.deg},{coord.dec.deg}' for coord in coordinates_list] + where = (" WHERE CONTAINS(POINT('ICRS',ra,dec)," + f"POLYGON('ICRS',{','.join(coordinates_str)}))=1") else: - raise ValueError("Unrecognized spatial query type. Must be one of " - "'Cone', 'Box', 'Polygon', or 'All-Sky'.") - - request_payload['spatial'] = spatial + coords_icrs = parse_coordinates(coordinates).icrs + ra, dec = coords_icrs.ra.deg, coords_icrs.dec.deg - return request_payload - - def _args_to_payload(self, catalog, selcols=None): - """ - Sets the common parameters for all cgi -queries - - Parameters - ---------- - catalog : str - The name of the catalog to query. - selcols : str, optional - Target column list with value separated by a comma(,) - - Returns - ------- - request_payload : dict - """ - if selcols is None: - selcols = '' - request_payload = dict(catalog=catalog, - outfmt=3, - outrows=Irsa.ROW_LIMIT, - selcols=selcols) - return request_payload - - def _parse_result(self, response, verbose=False): - """ - Parses the results form the HTTP response to `~astropy.table.Table`. - - Parameters - ---------- - response : `requests.Response` - The HTTP response object - verbose : bool, optional - Defaults to `False`. When true it will display warnings whenever - the VOtable returned from the Service doesn't conform to the - standard. - - Returns - ------- - table : `~astropy.table.Table` - """ - if not verbose: - commons.suppress_vo_warnings() - - content = response.text - - # Check if results were returned - if 'The catalog is not on the list' in content: - raise ValueError("Invalid Catalog specified") - - # Check that object name was not malformed - if 'Either wrong or missing coordinate/object name' in content: - raise ValueError("Malformed coordinate/object name") - - # Check to see that output table size limit hasn't been exceeded - if 'Exceeding output table size limit' in content: - raise TableParseError("Exceeded output table size - reduce number " - "of output columns and/or limit search area") - - # Check to see that the query engine is working - if 'SQLConnect failed' in content: - raise TimeoutError("The IRSA server is currently down") - - # Check that the results are not of length zero - if len(content) == 0: - warnings.warn("The IRSA server sent back an empty reply", - NoResultsWarning) - - # Read it in using the astropy VO table reader - try: - first_table = votable.parse(BytesIO(response.content), - verify='warn').get_first_table() - except Exception as ex: - self.response = response - self.table_parse_error = ex - raise TableParseError("Failed to parse IRSA votable! The raw " - "response can be found in self.response, " - "and the error in self.table_parse_error.") + if spatial == 'Cone': + if isinstance(radius, str): + radius = Angle(radius) + where = (" WHERE CONTAINS(POINT('ICRS',ra,dec)," + f"CIRCLE('ICRS',{ra},{dec},{radius.to(u.deg).value}))=1") + elif spatial == 'Box': + if isinstance(width, str): + width = Angle(width) + where = (" WHERE CONTAINS(POINT('ICRS',ra,dec)," + f"BOX('ICRS',{ra},{dec},{width.to(u.deg).value},{width.to(u.deg).value}))=1") + else: + raise ValueError("Unrecognized spatial query type. Must be one of " + "'Cone', 'Box', 'Polygon', or 'All-Sky'.") - # Convert to astropy.table.Table instance - table = first_table.to_table() + adql += where - # Check if table is empty - if len(table) == 0: - warnings.warn("Query returned no results, so the table will " - "be empty", NoResultsWarning) + if get_query_payload: + return adql + response = self.query_tap(query=adql) - return table + return response.to_table() - def list_catalogs(self, cache=False): + @deprecated_renamed_argument("cache", None, since="0.4.7") + def list_catalogs(self, full=False, cache=False): """ - Return a dictionary of the catalogs in the IRSA Gator tool. + Return information of available IRSA catalogs. Parameters ---------- - cache : bool - Use local cache when set to `True`. Default is `False`. - - Returns - ------- - catalogs : dict - A dictionary of catalogs where the key indicates the catalog - name to be used in query functions, and the value is the verbose - description of the catalog. + full : bool + If True returns the full schema VOTable. If False returns a dictionary of the table names and + their description. """ - response = self._request("GET", url=Irsa.GATOR_LIST_URL, - params=dict(mode='xml'), cache=cache, - timeout=Irsa.TIMEOUT) + tap_tables = Irsa.query_tap("SELECT * FROM TAP_SCHEMA.tables") - root = tree.fromstring(response.content) - catalogs = {} - for catalog in root.findall('catalog'): - catname = catalog.find('catname').text - desc = catalog.find('desc').text - catalogs[catname] = desc - - return catalogs + if full: + return tap_tables + else: + return {tap_table['table_name']: tap_table['description'] for tap_table in tap_tables} - def print_catalogs(self, cache=False): - """ - Display a table of the catalogs in the IRSA Gator tool. - """ - catalogs = self.list_catalogs(cache=cache) + # TODO, deprecate this as legacy + def print_catalogs(self): + catalogs = self.list_catalogs() for catname in catalogs: print("{:30s} {:s}".format(catname, catalogs[catname])) Irsa = IrsaClass() - - -def _parse_coordinates(coordinates): - # borrowed from commons.parse_coordinates as from_name wasn't required in - # this case - if isinstance(coordinates, str): - try: - c = coord.SkyCoord(coordinates, frame='icrs') - warnings.warn("Coordinate string is being interpreted as an " - "ICRS coordinate.") - except u.UnitsError as ex: - warnings.warn("Only ICRS coordinates can be entered as strings\n" - "For other systems please use the appropriate " - "astropy.coordinates object") - raise ex - elif isinstance(coordinates, commons.CoordClasses): - c = coordinates - else: - raise TypeError("Argument cannot be parsed as a coordinate") - c_icrs = c.transform_to(coord.ICRS) - formatted_coords = _format_decimal_coords(c_icrs.ra.degree, - c_icrs.dec.degree) - return formatted_coords - - -def _pair_to_deg(pair): - """ - Turn a pair of floats, Angles, or Quantities into pairs of float - degrees - """ - - # unpack - lon, lat = pair - - if hasattr(lon, 'degree') and hasattr(lat, 'degree'): - pair = (lon.degree, lat.degree) - elif hasattr(lon, 'to') and hasattr(lat, 'to'): - pair = [lon, lat] - for ii, ang in enumerate((lon, lat)): - if ang.unit.is_equivalent(u.degree): - pair[ii] = ang.to(u.degree).value - else: - warnings.warn("Polygon endpoints are being interpreted as " - "RA/Dec pairs specified in decimal degree units.") - return tuple(pair) - - -def _format_decimal_coords(ra, dec): - """ - Print *decimal degree* RA/Dec values in an IPAC-parseable form - """ - return '{0} {1:+}'.format(ra, dec) - - -def _parse_dimension(dim): - if (isinstance(dim, u.Quantity) and dim.unit in u.deg.find_equivalent_units()): - if dim.unit not in ['arcsec', 'arcmin', 'deg']: - dim = dim.to(u.degree) - # otherwise must be an Angle or be specified in hours... - else: - try: - new_dim = coord.Angle(dim) - dim = u.Quantity(new_dim.degree, u.Unit('degree')) - except (u.UnitsError, coord.errors.UnitsError, AttributeError): - raise u.UnitsError("Dimension not in proper units") - return dim diff --git a/astroquery/ipac/irsa/tests/test_irsa.py b/astroquery/ipac/irsa/tests/test_irsa.py index 6e67dcdac9..369e29101e 100644 --- a/astroquery/ipac/irsa/tests/test_irsa.py +++ b/astroquery/ipac/irsa/tests/test_irsa.py @@ -1,121 +1,40 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst -import os -import re -import numpy as np - import pytest from astropy.coordinates import SkyCoord -from astropy.table import Table import astropy.units as u -from astroquery.utils.mocks import MockResponse -from astroquery.ipac.irsa import Irsa, conf -from astroquery.ipac import irsa - -DATA_FILES = {'Cone': 'Cone.xml', - 'Box': 'Box.xml', - 'Polygon': 'Polygon.xml'} +from astroquery.ipac.irsa import Irsa +from astroquery.exceptions import InvalidQueryError -OBJ_LIST = ["m31", "00h42m44.330s +41d16m07.50s", +OBJ_LIST = ["00h42m44.330s +41d16m07.50s", SkyCoord(l=121.1743 * u.deg, b=-21.5733 * u.deg, frame="galactic")] +SIZE_LIST = [2 * u.arcmin, '0d2m0s'] -def data_path(filename): - data_dir = os.path.join(os.path.dirname(__file__), 'data') - return os.path.join(data_dir, filename) - - -@pytest.fixture -def patch_get(request): - mp = request.getfixturevalue("monkeypatch") - - mp.setattr(Irsa, '_request', get_mockreturn) - return mp - - -def get_mockreturn(method, url, params=None, timeout=10, cache=False, **kwargs): - filename = data_path(DATA_FILES[params['spatial']]) - with open(filename, 'rb') as infile: - content = infile.read() - return MockResponse(content, **kwargs) - - -@pytest.mark.parametrize(('dim'), - ['5d0m0s', 0.3 * u.rad, '5h0m0s', 2 * u.arcmin]) -def test_parse_dimension(dim): - # check that the returned dimension is always in units of 'arcsec', - # 'arcmin' or 'deg' - new_dim = irsa.core._parse_dimension(dim) - assert new_dim.unit in ['arcsec', 'arcmin', 'deg'] - - -@pytest.mark.parametrize(('ra', 'dec', 'expected'), - [(10, 10, '10 +10'), - (10.0, -11, '10.0 -11') - ]) -def test_format_decimal_coords(ra, dec, expected): - out = irsa.core._format_decimal_coords(ra, dec) - assert out == expected - - -@pytest.mark.parametrize(('coordinates', 'expected'), - [("5h0m0s 0d0m0s", "75.0 +0.0") - ]) -def test_parse_coordinates(coordinates, expected): - out = irsa.core._parse_coordinates(coordinates) - for a, b in zip(out.split(), expected.split()): - try: - a = float(a) - b = float(b) - np.testing.assert_almost_equal(a, b) - except ValueError: - assert a == b +@pytest.mark.parametrize("coordinates", OBJ_LIST) +@pytest.mark.parametrize("radius", SIZE_LIST) +def test_query_region_cone(coordinates, radius): + query = Irsa.query_region(coordinates, catalog='fp_psc', spatial='Cone', radius=radius, + get_query_payload=True) -def test_args_to_payload(): - out = Irsa._args_to_payload("fp_psc") - assert out == dict(catalog='fp_psc', outfmt=3, outrows=conf.row_limit, - selcols='') + # We don't fully float compare in this string, there are slight differences due to the name-coordinate + # resolution and conversions + assert "SELECT * FROM fp_psc WHERE CONTAINS(POINT('ICRS',ra,dec),CIRCLE('ICRS',10.68" in query + assert ",41.26" in query + assert ",0.0333" in query -@pytest.mark.parametrize(("coordinates"), OBJ_LIST) -def test_query_region_cone_async(coordinates, patch_get): - response = Irsa.query_region_async( - coordinates, catalog='fp_psc', spatial='Cone', - radius=2 * u.arcmin, get_query_payload=True) - assert response['radius'] == 2 - assert response['radunits'] == 'arcmin' - response = Irsa.query_region_async( - coordinates, catalog='fp_psc', spatial='Cone', radius=2 * u.arcmin) - assert response is not None +@pytest.mark.parametrize("coordinates", OBJ_LIST) +@pytest.mark.parametrize("width", SIZE_LIST) +def test_query_region_box(coordinates, width): + query = Irsa.query_region(coordinates, catalog='fp_psc', spatial='Box', width=2 * u.arcmin, + get_query_payload=True) - -@pytest.mark.parametrize(("coordinates"), OBJ_LIST) -def test_query_region_cone(coordinates, patch_get): - result = Irsa.query_region( - coordinates, catalog='fp_psc', spatial='Cone', radius=2 * u.arcmin) - - assert isinstance(result, Table) - - -@pytest.mark.parametrize(("coordinates"), OBJ_LIST) -def test_query_region_box_async(coordinates, patch_get): - response = Irsa.query_region_async( - coordinates, catalog='fp_psc', spatial='Box', - width=2 * u.arcmin, get_query_payload=True) - assert response['size'] == 120 - response = Irsa.query_region_async( - coordinates, catalog='fp_psc', spatial='Box', width=2 * u.arcmin) - assert response is not None - - -@pytest.mark.parametrize(("coordinates"), OBJ_LIST) -def test_query_region_box(coordinates, patch_get): - result = Irsa.query_region( - coordinates, catalog='fp_psc', spatial='Box', width=2 * u.arcmin) - - assert isinstance(result, Table) + assert "SELECT * FROM fp_psc WHERE CONTAINS(POINT('ICRS',ra,dec),BOX('ICRS',10.68" in query + assert ",41.26" in query + assert ",0.0333" in query poly1 = [SkyCoord(ra=10.1 * u.deg, dec=10.1 * u.deg), @@ -125,51 +44,34 @@ def test_query_region_box(coordinates, patch_get): (10.0 * u.deg, 10.0 * u.deg)] -@pytest.mark.parametrize(("polygon"), [poly1, poly2]) -def test_query_region_async_polygon(polygon, patch_get): - response = Irsa.query_region_async( - "m31", catalog="fp_psc", spatial="Polygon", - polygon=polygon, get_query_payload=True) +@pytest.mark.parametrize("polygon", [poly1, poly2]) +def test_query_region_polygon(polygon): + query1 = Irsa.query_region(catalog="fp_psc", spatial="Polygon", polygon=polygon, + get_query_payload=True) + query2 = Irsa.query_region("m31", catalog="fp_psc", spatial="Polygon", polygon=polygon, + get_query_payload=True) - for a, b in zip(re.split("[ ,]", response["polygon"]), - re.split("[ ,]", "10.1 +10.1,10.0 +10.1,10.0 +10.0")): - for a1, b1 in zip(a.split(), b.split()): - a1 = float(a1) - b1 = float(b1) - np.testing.assert_almost_equal(a1, b1) + assert query1 == query2 + assert query1 == ("SELECT * FROM fp_psc " + "WHERE CONTAINS(POINT('ICRS',ra,dec),POLYGON('ICRS',10.1,10.1,10.0,10.1,10.0,10.0))=1") - response = Irsa.query_region_async( - "m31", catalog="fp_psc", spatial="Polygon", polygon=polygon) - assert response is not None +def test_query_allsky(): + query1 = Irsa.query_region(catalog="fp_psc", spatial="All-Sky", get_query_payload=True) + query2 = Irsa.query_region("m31", catalog="fp_psc", spatial="All-Sky", get_query_payload=True) + assert query1 == query2 == "SELECT * FROM fp_psc" -@pytest.mark.parametrize(("polygon"), - [poly1, - poly2, - ]) -def test_query_region_polygon(polygon, patch_get): - result = Irsa.query_region( - "m31", catalog="fp_psc", spatial="Polygon", polygon=polygon) - assert isinstance(result, Table) - - -@pytest.mark.parametrize(('spatial', 'result'), - zip(('Cone', 'Box', 'Polygon', 'All-Sky'), - ('Cone', 'Box', 'Polygon', 'NONE'))) -def test_spatial_valdi(spatial, result): - out = Irsa._parse_spatial( - spatial, coordinates='m31', radius=5 * u.deg, width=5 * u.deg, - polygon=[(5 * u.hour, 5 * u.deg)] * 3) - assert out['spatial'] == result - - -@pytest.mark.parametrize(('spatial'), [('cone', 'box', 'polygon', 'all-Sky', - 'All-sky', 'invalid', 'blah')]) +@pytest.mark.parametrize('spatial', ['cone', 'box', 'polygon', 'all-Sky', 'All-sky', 'invalid']) def test_spatial_invalid(spatial): with pytest.raises(ValueError): - Irsa._parse_spatial(spatial, coordinates='m31') + Irsa.query_region(OBJ_LIST[0], catalog='invalid_spatial', spatial=spatial) + + +def test_no_catalog(): + with pytest.raises(InvalidQueryError): + Irsa.query_region("m31", spatial='Cone') def test_deprecated_namespace_import_warning(): diff --git a/astroquery/ipac/irsa/tests/test_irsa_remote.py b/astroquery/ipac/irsa/tests/test_irsa_remote.py index 0ce397d8a3..f2f0e800c0 100644 --- a/astroquery/ipac/irsa/tests/test_irsa_remote.py +++ b/astroquery/ipac/irsa/tests/test_irsa_remote.py @@ -1,62 +1,80 @@ # Licensed under a 3-clause BSD style license - see LICENSE.rst - import pytest import astropy.units as u from astropy.table import Table from astropy.coordinates import SkyCoord +from astropy.utils.exceptions import AstropyDeprecationWarning + +try: + # This requires pyvo 1.4 + from pyvo.dal.exceptions import DALOverflowWarning +except ImportError: + pass from astroquery.ipac.irsa import Irsa OBJ_LIST = ["m31", "00h42m44.330s +41d16m07.50s", - SkyCoord(l=121.1743, b=-21.5733, unit=(u.deg, u.deg), # noqa - frame='galactic')] + SkyCoord(l=121.1743, b=-21.5733, unit=(u.deg, u.deg), frame='galactic')] @pytest.mark.remote_data class TestIrsa: - def test_query_region_cone_async(self): - response = Irsa.query_region_async( - 'm31', catalog='fp_psc', spatial='Cone', radius=2 * u.arcmin, cache=False) - assert response is not None - def test_query_region_cone(self): - result = Irsa.query_region( - 'm31', catalog='fp_psc', spatial='Cone', radius=2 * u.arcmin, cache=False) + @pytest.mark.parametrize("coordinates", OBJ_LIST) + def test_query_region_cone(self, coordinates): + """ + Test multiple ways of specifying coordinates for a conesearch + """ + result = Irsa.query_region(coordinates, catalog='fp_psc', spatial='Cone') assert isinstance(result, Table) + assert len(result) == 19 + # assert all columns are returned + assert len(result.colnames) == 64 - def test_query_region_box_async(self): - response = Irsa.query_region_async( - "00h42m44.330s +41d16m07.50s", catalog='fp_psc', spatial='Box', - width=2 * u.arcmin, cache=False) - assert response is not None + def test_query_selcols_deprecated(self): + """ + Test renamed selcols + """ + with pytest.warns(AstropyDeprecationWarning, match='"selcols" was deprecated in version'): + result = Irsa.query_region("m31", catalog='fp_psc', selcols='ra,dec,j_m') + assert result.colnames == ['ra', 'dec', 'j_m'] + + def test_query_columns_radius(self): + """ + Test selection of only a few columns, and using a bigger radius + """ + result = Irsa.query_region("m31", catalog='fp_psc', columns='ra,dec,j_m', radius=0.5 * u.arcmin) + assert len(result) == 84 + # assert only selected columns are returned + assert result.colnames == ['ra', 'dec', 'j_m'] + + @pytest.mark.skip("Upstream TAP doesn't support Box geometry yet") def test_query_region_box(self): result = Irsa.query_region( "00h42m44.330s +41d16m07.50s", catalog='fp_psc', spatial='Box', width=2 * u.arcmin, cache=False) assert isinstance(result, Table) - def test_query_region_async_polygon(self): - polygon = [SkyCoord(ra=10.1, dec=10.1, unit=(u.deg, u.deg)), - SkyCoord(ra=10.0, dec=10.1, unit=(u.deg, u.deg)), - SkyCoord(ra=10.0, dec=10.0, unit=(u.deg, u.deg))] - response = Irsa.query_region_async( - "m31", catalog="fp_psc", spatial="Polygon", polygon=polygon, cache=False) - - assert response is not None - def test_query_region_polygon(self): polygon = [(10.1, 10.1), (10.0, 10.1), (10.0, 10.0)] with pytest.warns(UserWarning, match='Polygon endpoints are being interpreted'): - result = Irsa.query_region("m31", catalog="fp_psc", spatial="Polygon", - polygon=polygon, cache=False) + result = Irsa.query_region("m31", catalog="fp_psc", spatial="Polygon", polygon=polygon) assert isinstance(result, Table) def test_list_catalogs(self): - catalogs = Irsa.list_catalogs(cache=False) + catalogs = Irsa.list_catalogs() # Number of available catalogs may change over time, test only for significant drop. - # (at the time of writing there are 587 catalogs in the list). - assert len(catalogs) > 500 + # (at the time of writing there are 933 tables in the list). + assert len(catalogs) > 900 + + def test_tap(self): + query = "SELECT TOP 5 ra,dec FROM cosmos2015" + with pytest.warns(expected_warning=DALOverflowWarning, + match="Partial result set. Potential causes MAXREC, async storage space, etc."): + result = Irsa.query_tap(query=query) + assert len(result) == 5 + assert result.to_table().colnames == ['ra', 'dec'] diff --git a/docs/ipac/irsa/irsa.rst b/docs/ipac/irsa/irsa.rst index 8c7abb0637..fabbde2c19 100644 --- a/docs/ipac/irsa/irsa.rst +++ b/docs/ipac/irsa/irsa.rst @@ -37,19 +37,20 @@ a look at all the available catalogs: ... 'xmm_cat_s05': "SWIRE XMM_LSS Region Spring '05 Spitzer Catalog"} -This returns a dictionary of catalog names with their description. If you would -rather just print out this information: +To access the full VOTable of the catalog information, use the ``full`` keyword argument. .. doctest-remote-data:: >>> from astroquery.ipac.irsa import Irsa - >>> Irsa.print_catalogs() - allwise_p3as_psd AllWISE Source Catalog - allwise_p3as_mep AllWISE Multiepoch Photometry Table - allwise_p3as_psr AllWISE Reject Table + >>> Irsa.list_catalogs(full=True) # doctest: +IGNORE_OUTPUT + + table_index schema_name table_name description ... irsa_access_flag irsa_nrows irsa_odbc_datasource irsa_spatial_idx_name + int32 object object object ... int32 int64 object object + ----------- ----------- ---------------------------------- --------------------------------------------- ... ---------------- ---------- -------------------- --------------------- + 303 spitzer spitzer.m31irac_image M31IRAC Images ... 30 4 postgres + 304 spitzer mipslg MIPS Local Galaxies Catalog ... 30 240 spitzer SPT_IND_MIPSLG + 305 spitzer spitzer.mips_lg_images MIPS Local Galaxies Images ... 30 606 postgres ... - wisegalhii WISE Catalog of Galactic HII Regions v2.2 - denis3 DENIS 3rd Release (Sep. 2005) Performing a cone search @@ -68,70 +69,42 @@ entered as a string that is parsable by `~astropy.coordinates.Angle`. >>> import astropy.units as u >>> table = Irsa.query_region("m31", catalog="fp_psc", spatial="Cone", ... radius=2 * u.arcmin) - >>> print(table) # doctest: +IGNORE_OUTPUT - ra dec clon clat ... angle j_h h_k j_k - deg deg ... deg - ---------- ---------- ------------ ------------ ... ---------- ----- ----- ----- - 10.684737 41.269035 00h42m44.34s 41d16m08.53s ... 4.072939 0.785 0.193 0.978 - 10.683469 41.268585 00h42m44.03s 41d16m06.91s ... 259.968952 -- -- -- - ... ... ... ... ... ... ... ... ... - 10.725972 41.280636 00h42m54.23s 41d16m50.29s ... 69.015201 -- -- -- - 10.656898 41.294655 00h42m37.66s 41d17m40.76s ... 321.112765 1.237 -- -- - 10.647116 41.286366 00h42m35.31s 41d17m10.92s ... 301.956547 -- -- -- - Length = 500 rows - + >>> print(table) + ra dec err_maj err_min ... coadd_key coadd htm20 + deg deg arcsec arcsec ... + ---------- ---------- ------- ------- ... --------- ----- ------------------- + 10.692216 41.260162 0.10 0.09 ... 1590591 33 4805203678124326400 + 10.700059 41.263481 0.31 0.30 ... 1590591 33 4805203678125364736 + 10.699131 41.263248 0.28 0.20 ... 1590591 33 4805203678125474304 + ... ... ... ... ... ... ... ... + 10.661414 41.242363 0.21 0.20 ... 1590591 33 4805203679644192256 + 10.665184 41.240238 0.14 0.13 ... 1590591 33 4805203679647824896 + 10.663245 41.240646 0.24 0.21 ... 1590591 33 4805203679649555456 + Length = 774 rows The coordinates of the center may be specified rather than using the target -name. The coordinates can be specified using the appropriate -`astropy.coordinates` object. ICRS coordinates may also be entered directly as -a string, as specified by `astropy.coordinates`: +name. The coordinates can be specified using a `~astropy.coordinates.SkyCoord` +object or a string resolvable by the `~astropy.coordinates.SkyCoord` constructor. .. doctest-remote-data:: >>> from astroquery.ipac.irsa import Irsa - >>> import astropy.coordinates as coord - >>> table = Irsa.query_region(coord.SkyCoord(121.1743, - ... -21.5733, unit=(u.deg,u.deg), - ... frame='galactic'), + >>> from astropy.coordinates import SkyCoord + >>> coord = SkyCoord(121.1743, -21.5733, unit='deg', frame='galactic') + >>> table = Irsa.query_region(coordinates=coord, ... catalog='fp_psc', radius='0d2m0s') >>> print(table) - ra dec clon clat ... angle j_h h_k j_k - deg deg ... deg - ---------- ---------- ------------ ------------ ... ---------- ----- ----- ----- - 10.684737 41.269035 00h42m44.34s 41d16m08.53s ... 10.37715 0.785 0.193 0.978 - 10.683469 41.268585 00h42m44.03s 41d16m06.91s ... 259.028985 -- -- -- - 10.685657 41.269550 00h42m44.56s 41d16m10.38s ... 43.199247 -- -- -- - ... ... ... ... ... ... ... ... ... - 10.656898 41.294655 00h42m37.66s 41d17m40.76s ... 321.14224 1.237 -- -- - 10.647116 41.286366 00h42m35.31s 41d17m10.92s ... 301.969315 -- -- -- - Length = 500 rows - - -Performing a box search ------------------------ - -The box queries have a syntax similar to the cone queries. In this case the -``spatial`` keyword argument must be set to ``Box``. Also the width of the box -region is required. The width may be specified in the same way as the radius -for cone search queries, above - so it may be set using the appropriate -`~astropy.units.Quantity` object or a string parsable by `~astropy.coordinates.Angle`. - -.. doctest-remote-data:: - - >>> from astroquery.ipac.irsa import Irsa - >>> import astropy.units as u - >>> table = Irsa.query_region("00h42m44.330s +41d16m07.50s", - ... catalog='fp_psc', spatial='Box', - ... width=5 * u.arcsec) - >>> print(table) - ra dec clon clat ... ext_key j_h h_k j_k - deg deg ... - ---------- ---------- ------------ ------------ ... ------- ----- ----- ----- - 10.684737 41.269035 00h42m44.34s 41d16m08.53s ... -- 0.785 0.193 0.978 - -Note that in this case we directly passed ICRS coordinates as a string to the -:meth:`~astroquery.ipac.irsa.IrsaClass.query_region`. - + ra dec err_maj err_min ... coadd_key coadd htm20 + deg deg arcsec arcsec ... + ---------- ---------- ------- ------- ... --------- ----- ------------------- + 10.692216 41.260162 0.10 0.09 ... 1590591 33 4805203678124326400 + 10.700059 41.263481 0.31 0.30 ... 1590591 33 4805203678125364736 + 10.699131 41.263248 0.28 0.20 ... 1590591 33 4805203678125474304 + ... ... ... ... ... ... ... ... + 10.661414 41.242363 0.21 0.20 ... 1590591 33 4805203679644192256 + 10.665184 41.240238 0.14 0.13 ... 1590591 33 4805203679647824896 + 10.663245 41.240646 0.24 0.21 ... 1590591 33 4805203679649555456 + Length = 774 rows Queries over a polygon ---------------------- @@ -140,9 +113,8 @@ Polygon queries can be performed by setting ``spatial='Polygon'``. The search center is optional in this case. One additional parameter that must be set for these queries is ``polygon``. This is a list of coordinate pairs that define a convex polygon. The coordinates may be specified as usual by using the -appropriate `astropy.coordinates` object (Again ICRS coordinates may be -directly passed as properly formatted strings). In addition to using a list of -`astropy.coordinates` objects, one additional convenient means of specifying +appropriate `~astropy.coordinates.SkyCoord` object. In addition to using a list of +`~astropy.coordinates.SkyCoord` objects, one additional convenient means of specifying the coordinates is also available - Coordinates may also be entered as a list of tuples, each tuple containing the ra and dec values in degrees. Each of these options is illustrated below: @@ -157,16 +129,16 @@ options is illustrated below: ... coordinates.SkyCoord(ra=10.0, dec=10.0, unit=(u.deg, u.deg), frame='icrs') ... ]) >>> print(table) - ra dec clon clat ... ext_key j_h h_k j_k - deg deg ... - ---------- ---------- ------------ ------------ ... ------- ----- ----- ----- - 10.015839 10.038061 00h40m03.80s 10d02m17.02s ... -- 0.552 0.313 0.865 - 10.015696 10.099228 00h40m03.77s 10d05m57.22s ... -- 0.602 0.154 0.756 - 10.011170 10.093903 00h40m02.68s 10d05m38.05s ... -- 0.378 0.602 0.98 - 10.031016 10.063082 00h40m07.44s 10d03m47.10s ... -- 0.809 0.291 1.1 - 10.036776 10.060278 00h40m08.83s 10d03m37.00s ... -- 0.468 0.372 0.84 - 10.059964 10.085445 00h40m14.39s 10d05m07.60s ... -- 0.697 0.273 0.97 - 10.005549 10.018401 00h40m01.33s 10d01m06.24s ... -- 0.662 0.566 1.228 + ra dec err_maj err_min ... coadd_key coadd htm20 + deg deg arcsec arcsec ... + ---------- ---------- ------- ------- ... --------- ----- ------------------- + 10.015839 10.038061 0.09 0.06 ... 1443005 91 4805087709670704640 + 10.015696 10.099228 0.10 0.07 ... 1443005 91 4805087709940635648 + 10.011170 10.093903 0.23 0.21 ... 1443005 91 4805087710032524288 + 10.031016 10.063082 0.19 0.18 ... 1443005 91 4805087710169327616 + 10.036776 10.060278 0.11 0.06 ... 1443005 91 4805087710175392768 + 10.059964 10.085445 0.23 0.20 ... 1443005 91 4805087710674674176 + 10.005549 10.018401 0.16 0.14 ... 1443005 91 4805087784811171840 Another way to specify the polygon is directly as a list of tuples - each tuple is an ra, dec pair expressed in degrees: @@ -177,25 +149,24 @@ is an ra, dec pair expressed in degrees: >>> table = Irsa.query_region("m31", catalog="fp_psc", spatial="Polygon", ... polygon = [(10.1, 10.1), (10.0, 10.1), (10.0, 10.0)]) # doctest: +IGNORE_WARNINGS >>> print(table) - ra dec clon clat ... ext_key j_h h_k j_k - deg deg ... - ---------- ---------- ------------ ------------ ... ------- ----- ----- ----- - 10.015839 10.038061 00h40m03.80s 10d02m17.02s ... -- 0.552 0.313 0.865 - 10.015696 10.099228 00h40m03.77s 10d05m57.22s ... -- 0.602 0.154 0.756 - 10.011170 10.093903 00h40m02.68s 10d05m38.05s ... -- 0.378 0.602 0.98 - 10.031016 10.063082 00h40m07.44s 10d03m47.10s ... -- 0.809 0.291 1.1 - 10.036776 10.060278 00h40m08.83s 10d03m37.00s ... -- 0.468 0.372 0.84 - 10.059964 10.085445 00h40m14.39s 10d05m07.60s ... -- 0.697 0.273 0.97 - 10.005549 10.018401 00h40m01.33s 10d01m06.24s ... -- 0.662 0.566 1.228 - + ra dec err_maj err_min ... coadd_key coadd htm20 + deg deg arcsec arcsec ... + ---------- ---------- ------- ------- ... --------- ----- ------------------- + 10.015839 10.038061 0.09 0.06 ... 1443005 91 4805087709670704640 + 10.015696 10.099228 0.10 0.07 ... 1443005 91 4805087709940635648 + 10.011170 10.093903 0.23 0.21 ... 1443005 91 4805087710032524288 + 10.031016 10.063082 0.19 0.18 ... 1443005 91 4805087710169327616 + 10.036776 10.060278 0.11 0.06 ... 1443005 91 4805087710175392768 + 10.059964 10.085445 0.23 0.20 ... 1443005 91 4805087710674674176 + 10.005549 10.018401 0.16 0.14 ... 1443005 91 4805087784811171840 Selecting Columns --------------------- +----------------- The IRSA service allows to query either a subset of the default columns for a given table, or additional columns that are not present by default. This can be done by listing all the required columns separated by a comma (,) in -a string with the ``selcols`` argument. +a string with the ``columns`` argument. An example where the AllWISE Source Catalog needs to be queried around the @@ -205,39 +176,48 @@ star HIP 12 with just the ra, dec and w1mpro columns would be: .. doctest-remote-data:: >>> from astroquery.ipac.irsa import Irsa - >>> table = Irsa.query_region("HIP 12", catalog="allwise_p3as_psd", spatial="Cone", selcols="ra,dec,w1mpro") + >>> table = Irsa.query_region("HIP 12", catalog="allwise_p3as_psd", spatial="Cone", columns="ra,dec,w1mpro") >>> print(table) - ra dec clon clat w1mpro dist angle - deg deg mag arcsec deg - ----------- ----------- ------------ ------------- ------- -------- ---------- - 0.0407905 -35.9602605 00h00m09.79s -35d57m36.94s 4.837 0.350806 245.442148 + ra dec w1mpro + deg deg mag + ----------- ----------- ------- + 0.0407905 -35.9602605 4.837 A list of available columns for each catalog can be found at https://irsa.ipac.caltech.edu/holdings/catalogs.html. The "Long Form" button at the top of the column names table must be clicked to access a full list of all available columns. +Direct TAP query to the IRSA server +----------------------------------- -Changing the precision of ascii output --------------------------------------- - -The precision of the table display of each column is set upstream by the archive, -and appears as the ``.format`` attribute of individual columns. This attribute affects -not only the display of columns, but also the precision that is output when the table -is written in ``ascii.ipac`` or ``ascii.csv`` formats. The ``.format`` attribute of -individual columns may be set to increase the precision. +The `~astroquery.ipac.irsa.IrsaClass.query_tap` method allows for a rich variety of queries. ADQL queries +provided via the ``query`` parameter is sent directly to the IRSA TAP server, and the result is +returned as a `~pyvo.dal.TAPResults` object. Its ``to_table`` or ``to_qtable`` method convert the result to a +`~astropy.table.Table` or `~astropy.table.QTable` object. .. doctest-remote-data:: >>> from astroquery.ipac.irsa import Irsa - >>> table = Irsa.query_region("HIP 12", catalog="allwise_p3as_psd", spatial="Cone", selcols="ra,dec,w1mpro") - >>> table['ra'].format = '{:10.6f}' - >>> table['dec'].format = '{:10.6f}' - >>> print(table) - ra dec clon clat w1mpro dist angle - deg deg mag arcsec deg - ---------- ---------- ------------ ------------- ------- -------- ---------- - 0.040791 -35.960260 00h00m09.79s -35d57m36.94s 4.837 0.350806 245.442148 + >>> query = ("SELECT TOP 10 ra,dec,j_m,j_msigcom,h_m,h_msigcom,k_m,k_msigcom,ph_qual,cc_flg " + ... "FROM fp_psc WHERE CONTAINS(POINT('ICRS',ra, dec), CIRCLE('ICRS',202.48417,47.23056,0.4))=1") + >>> results = Irsa.query_tap(query=query).to_qtable() # doctest: +IGNORE_WARNINGS + >>> results + + ra dec j_m j_msigcom ... k_m k_msigcom ph_qual cc_flg + deg deg mag mag ... mag mag + float64 float64 float32 float32 ... float32 float32 object object + ---------- ---------- ------- --------- ... ------- --------- ------- ------ + 202.900750 46.961285 16.168 0.096 ... 15.180 0.158 ABC 000 + 202.951614 47.024986 15.773 0.072 ... 15.541 0.234 ABD 000 + 202.922589 47.024452 14.628 0.032 ... 14.036 0.059 AAA 000 + 202.911833 47.011093 13.948 0.025 ... 13.318 0.036 AAA 000 + 202.925932 47.004223 16.461 0.131 ... 17.007 ——— BCU 000 + 202.515450 46.929302 15.967 0.088 ... 15.077 0.140 AAB 000 + 202.532240 46.931587 16.575 0.145 ... 15.888 ——— BDU 000 + 202.607930 46.932255 16.658 0.147 ... 15.430 0.193 BUC 000 + 202.823902 47.011593 16.555 0.143 ... 16.136 ——— BBU 000 + 202.809023 46.964558 15.874 0.081 ... 15.322 0.188 AAC 000 Other Configurations