Source code for astroquery.mpc.core

# -*- coding: utf-8 -*-

import json
import re
import warnings

import numpy as np
from bs4 import BeautifulSoup
from astropy.io import ascii
from astropy.time import Time
from astropy.table import Table, QTable, Column
import astropy.units as u
from astropy.coordinates import EarthLocation, Angle, SkyCoord
from erfa import ErfaWarning

from ..query import BaseQuery
from . import conf
from ..utils import async_to_sync, class_or_instance
from ..exceptions import InvalidQueryError, EmptyResponseError


__all__ = ['MPCClass']


[docs] @async_to_sync class MPCClass(BaseQuery): MPC_URL = 'https://' + conf.web_service_server + '/web_service' # The authentication credentials for the MPC web service are publicly # available and can be openly viewed on the documentation page at # https://minorplanetcenter.net/web_service/ MPC_USERNAME = 'mpc_ws' MPC_PASSWORD = 'mpc!!ws' MPES_URL = 'https://' + conf.mpes_server + '/cgi-bin/mpeph2.cgi' OBSERVATORY_CODES_URL = ('https://' + conf.web_service_server + '/iau/lists/ObsCodes.html') MPCOBS_URL = conf.mpcdb_server TIMEOUT = conf.timeout _ephemeris_types = { 'equatorial': 'a', 'heliocentric': 's', 'geocentric': 'G' } _default_number_of_steps = { 'd': '21', 'h': '49', 'm': '121', 's': '301' } _proper_motions = { 'total': 't', 'coordinate': 'c', 'sky': 's' } def __init__(self): super().__init__()
[docs] def query_object_async(self, target_type, *, get_query_payload=False, **kwargs): """ Query around a specific object within a given mission catalog. When searching for a comet, it will return the entry with the latest epoch. The following are valid query parameters for the MPC API search. The params list and description are from https://minorplanetcenter.net/web_service/ and are accurate as of 3/6/2018. Parameters ---------- target_type : str Search for either a comet or an asteroid, with the two valid values being, naturally, "comet" and "asteroid" updated_at : str Date-time when the Orbits table was last updated (YYYY-MM-DDThh:mm:ssZ). Note: the documentation lists this field as "orbit-updated-at", but the service response contained "updated_at", which appears to correlate and can also be used as a query parameter. name : str The object's name; e.g., Eros. This can be queried as 'Eros' or 'eros'. If the object has not yet been named, this field will be 'null'. number : integer The object's number; e.g., 433. If the object has not yet been numbered, this field will be 'null'. designation : str The object's provisional designation (e.g., 2014 AA) if it has not been numbered yet. If the object has been numbered, this number is its permanent designation and is what the 'designation' parameter will return, padded with leading zeroes for a total of 7 digits; e.g., '0000433'. When querying for provisional designations, because white spaces aren't allowed in the query, escape the space with either a '+' or '%20'; e.g., '2014+AA' or '2014%20AA'. epoch : str The date/time of reference for the current orbital parameters. epoch_jd : str The Julian Date of the epoch. period (years) : str Time it takes for the object to complete one orbit around the Sun. semimajor_axis : str a, one half of the longest diameter of the orbital ellipse. (AU) aphelion_distance : str The distance when the object is furthest from the Sun in its orbit. (AU) perihelion_distance : str The distance when the object is nearest to the Sun in its orbit. (AU) perihelion_date : str Date when the object is at perihelion, i.e., reaches its closest point to the Sun. perihelion_date_jd : str The Julian Date of perihelion. argument_of_perihelion (°) : str ω, defines the orientation of the ellipse in the orbital plane and is the angle from the object's ascending node to its perihelion, measured in the direction of motion. Range: 0–360°. ascending_node (°) : str Ω, the longitude of the ascending node, it defines the horizontal orientation of the ellipse with respect to the ecliptic, and is the angle measured counterclockwise (as seen from North of the ecliptic) from the First Point of Aries to the ascending node. Range: 0–360°. inclination (°) : str i, the angle between the object's orbit and the ecliptic. Range: 0–180°. eccentricity : str e, a measure of how far the orbit shape departs from a circle. Range: 0–1, with e = 0 being a perfect circle, intermediate values being ellipses ever more elongated as e increases, and e = 1 describing a parabola. mean_anomaly (°) : str M, is related to the position of the object along its orbit at the given epoch. Range: 0–360°. mean_daily_motion (°/day) : str n, a measure of the average speed of the object along its orbit. absolute_magnitude : str H, apparent magnitude the object would have if it were observed from 1 AU away at zero phase, while it was 1 AU away from the Sun. Note this is geometrically impossible and is equivalent to observing the object from the center of the Sun. phase_slope : str G, slope parameter as calculated or assumed by the MPC. The slope parameter is a measure of how much brighter the object gets as its phase angle decreases. When not known, a value of G = 0.15 is assumed. orbit_type : integer Asteroids are classified from a dynamics perspective by the area of the Solar System in which they orbit. A number identifies each orbit type. 0: Unclassified (mostly Main Belters) 1: Atiras 2: Atens 3: Apollos 4: Amors 5: Mars Crossers 6: Hungarias 7: Phocaeas 8: Hildas 9: Jupiter Trojans 10: Distant Objects delta_v (km/sec) : float Δv, an estimate of the amount of energy necessary to jump from LEO (Low Earth Orbit) to the object's orbit. tisserand_jupiter : float TJ, Tisserand parameter with respect to Jupiter, which is a quasi-invariant value for each object and is frequently used to distinguish objects (typically TJ > 3) from Jupiter-family comets (typically 2 < TJ < 3). neo : bool value = 1 flags Near Earth Objects (NEOs). km_neo : bool value = 1 flags NEOs larger than ~1 km in diameter. pha : bool value = 1 flags Potentially Hazardous Asteroids (PHAs). mercury_moid : float Minimum Orbit Intersection Distance with respect to Mercury. (AU) venus_moid : float Minimum Orbit Intersection Distance with respect to Venus. (AU) earth_moid : float Minimum Orbit Intersection Distance with respect to Earth. (AU) mars_moid : float Minimum Orbit Intersection Distance with respect to Mars. (AU) jupiter_moid : float Minimum Orbit Intersection Distance with respect to Jupiter. (AU) saturn_moid : float Minimum Orbit Intersection Distance with respect to Saturn. (AU) uranus_moid : float Minimum Orbit Intersection Distance with respect to Uranus. (AU) neptune_moid : float Minimum Orbit Intersection Distance with respect to Neptune. (AU) """ self.get_mpc_object_endpoint(target_type) kwargs['limit'] = 1 return self.query_objects_async(target_type, get_query_payload=get_query_payload, **kwargs)
[docs] def query_objects_async(self, target_type, *, get_query_payload=False, **kwargs): """ Query around a specific object within a given mission catalog The following are valid query parameters for the MPC API search. The params list and description are from https://minorplanetcenter.net/web_service/ and are accurate as of 3/6/2018: Parameters ---------- target_type : str Search for either a comet or an asteroid, with the two valid values being, naturally, "comet" and "asteroid" updated_at : str Date-time when the Orbits table was last updated (YYYY-MM-DDThh:mm:ssZ). Note: the documentation lists this field as "orbit-updated-at", but the service response contained "updated_at", which appears to correlate and can also be used as a query parameter. name : str The object's name; e.g., Eros. This can be queried as 'Eros' or 'eros'. If the object has not yet been named, this field will be 'null'. number : integer The object's number; e.g., 433. If the object has not yet been numbered, this field will be 'null'. designation : str The object's provisional designation (e.g., 2014 AA) if it has not been numbered yet. If the object has been numbered, this number is its permanent designation and is what the 'designation' parameter will return, padded with leading zeroes for a total of 7 digits; e.g., '0000433'. When querying for provisional designations, because white spaces aren't allowed in the query, escape the space with either a '+' or '%20'; e.g., '2014+AA' or '2014%20AA'. epoch : str The date/time of reference for the current orbital parameters. epoch_jd : str The Julian Date of the epoch. period (years) : str Time it takes for the object to complete one orbit around the Sun. semimajor_axis : str a, one half of the longest diameter of the orbital ellipse. (AU) aphelion_distance : str The distance when the object is furthest from the Sun in its orbit. (AU) perihelion_distance : str The distance when the object is nearest to the Sun in its orbit. (AU) perihelion_date : str Date when the object is at perihelion, i.e., reaches its closest point to the Sun. perihelion_date_jd : str The Julian Date of perihelion. argument_of_perihelion (°) : str ω, defines the orientation of the ellipse in the orbital plane and is the angle from the object's ascending node to its perihelion, measured in the direction of motion. Range: 0–360°. ascending_node (°) : str Ω, the longitude of the ascending node, it defines the horizontal orientation of the ellipse with respect to the ecliptic, and is the angle measured counterclockwise (as seen from North of the ecliptic) from the First Point of Aries to the ascending node. Range: 0–360°. inclination (°) : str i, the angle between the object's orbit and the ecliptic. Range: 0–180°. eccentricity : str e, a measure of how far the orbit shape departs from a circle. Range: 0–1, with e = 0 being a perfect circle, intermediate values being ellipses ever more elongated as e increases, and e = 1 describing a parabola. mean_anomaly (°) : str M, is related to the position of the object along its orbit at the given epoch. Range: 0–360°. mean_daily_motion (°/day) : str n, a measure of the average speed of the object along its orbit. absolute_magnitude : str H, apparent magnitude the object would have if it were observed from 1 AU away at zero phase, while it was 1 AU away from the Sun. Note this is geometrically impossible and is equivalent to observing the object from the center of the Sun. phase_slope : str G, slope parameter as calculated or assumed by the MPC. The slope parameter is a measure of how much brighter the object gets as its phase angle decreases. When not known, a value of G = 0.15 is assumed. orbit_type : integer Asteroids are classified from a dynamics perspective by the area of the Solar System in which they orbit. A number identifies each orbit type. 0: Unclassified (mostly Main Belters) 1: Atiras 2: Atens 3: Apollos 4: Amors 5: Mars Crossers 6: Hungarias 7: Phocaeas 8: Hildas 9: Jupiter Trojans 10: Distant Objects delta_v (km/sec) : float Δv, an estimate of the amount of energy necessary to jump from LEO (Low Earth Orbit) to the object's orbit. tisserand_jupiter : float TJ, Tisserand parameter with respect to Jupiter, which is a quasi-invariant value for each object and is frequently used to distinguish objects (typically TJ > 3) from Jupiter-family comets (typically 2 < TJ < 3). neo : bool value = 1 flags Near Earth Objects (NEOs). km_neo : bool value = 1 flags NEOs larger than ~1 km in diameter. pha : bool value = 1 flags Potentially Hazardous Asteroids (PHAs). mercury_moid : float Minimum Orbit Intersection Distance with respect to Mercury. (AU) venus_moid : float Minimum Orbit Intersection Distance with respect to Venus. (AU) earth_moid : float Minimum Orbit Intersection Distance with respect to Earth. (AU) mars_moid : float Minimum Orbit Intersection Distance with respect to Mars. (AU) jupiter_moid : float Minimum Orbit Intersection Distance with respect to Jupiter. (AU) saturn_moid : float Minimum Orbit Intersection Distance with respect to Saturn. (AU) uranus_moid : float Minimum Orbit Intersection Distance with respect to Uranus. (AU) neptune_moid : float Minimum Orbit Intersection Distance with respect to Neptune. (AU) limit : integer Limit the number of results to the given value """ mpc_endpoint = self.get_mpc_object_endpoint(target_type) if (target_type == 'comet'): kwargs['order_by_desc'] = "epoch" request_args = self._args_to_object_payload(**kwargs) # Return payload if requested if get_query_payload: return request_args self.query_type = 'object' auth = (self.MPC_USERNAME, self.MPC_PASSWORD) return self._request('GET', mpc_endpoint, params=request_args, auth=auth)
[docs] def get_mpc_object_endpoint(self, target_type): mpc_endpoint = self.MPC_URL if target_type == 'asteroid': mpc_endpoint = mpc_endpoint + '/search_orbits' elif target_type == 'comet': mpc_endpoint = mpc_endpoint + '/search_comet_orbits' return mpc_endpoint
[docs] @class_or_instance def get_ephemeris_async(self, target, *, location='500', start=None, step='1d', number=None, ut_offset=0, eph_type='equatorial', ra_format=None, dec_format=None, proper_motion='total', proper_motion_unit='arcsec/h', suppress_daytime=False, suppress_set=False, perturbed=True, unc_links=False, get_query_payload=False, get_raw_response=False, cache=False): r""" Object ephemerides from the Minor Planet Ephemeris Service. Parameters ---------- target : str Designation of the object of interest. See Notes for acceptable formats. location : str, array-like, or `~astropy.coordinates.EarthLocation`, optional Observer's location as an IAU observatory code, a 3-element array of Earth longitude, latitude, altitude, or a `~astropy.coordinates.EarthLocation`. Longitude and latitude should be anything that initializes an `~astropy.coordinates.Angle` object, and altitude should initialize an `~astropy.units.Quantity` object (with units of length). If ``None``, then the geocenter (code 500) is used. start : str or `~astropy.time.Time`, optional First epoch of the ephemeris as a string (UT), or astropy `~astropy.time.Time`. Strings are parsed by `~astropy.time.Time`. If ``None``, then today is used. Valid dates span the time period 1900 Jan 1 - 2099 Dec 31 [MPES]_. step : str or `~astropy.units.Quantity`, optional The ephemeris step size or interval in units of days, hours, minutes, or seconds. Strings are parsed by `~astropy.units.Quantity`. All inputs are rounded to the nearest integer. Default is 1 day. number : int, optional The number of ephemeris dates to compute. Must be ≤1441. If ``None``, the value depends on the units of ``step``: 21 for days, 49 for hours, 121 for minutes, or 301 for seconds. ut_offset : int, optional Number of hours to offset from 0 UT for daily ephemerides. eph_type : str, optional Specify the type of ephemeris:: equatorial: RA and Dec (default) heliocentric: heliocentric position and velocity vectors geocentric: geocentric position vector ra_format : dict, optional Format the RA column with `~astropy.coordinates.Angle.to_string` using these keyword arguments, e.g., ``{'sep': ':', 'unit': 'hourangle', 'precision': 1}``. dec_format : dict, optional Format the Dec column with `~astropy.coordinates.Angle.to_string` using these keyword arguments, e.g., ``{'sep': ':', 'precision': 0}``. proper_motion : str, optional total: total motion and direction (default) coordinate: separate RA and Dec coordinate motion sky: separate RA and Dec sky motion (i.e., includes a cos(Dec) term). proper_motion_unit : string or Unit, optional Convert proper motion to this unit. Must be an angular rate. Default is 'arcsec/h'. suppress_daytime : bool, optional Suppress output when the Sun is above the local horizon. (default ``False``) suppress_set : bool, optional Suppress output when the object is below the local horizon. (default ``False``) perturbed : bool, optional Generate perturbed ephemerides for unperturbed orbits (default ``True``). unc_links : bool, optional Return columns with uncertainty map and offset links, if available. get_query_payload : bool, optional Return the HTTP request parameters as a dictionary (default: ``False``). get_raw_response : bool, optional Return raw data without parsing into a table (default: ``False``). cache : bool Defaults to False. If set overrides global caching behavior. See :ref:`caching documentation <astroquery_cache>`. Returns ------- response : `requests.Response` The HTTP response returned from the service. Notes ----- See the MPES user's guide [MPES]_ for details on options and implementation. MPES allows azimuths to be measured eastwards from the north meridian, or westwards from the south meridian. However, the `~astropy.coordinates.AltAz` coordinate frame assumes eastwards of north. To remain consistent with Astropy, eastwards of north is used. Acceptable target names [MPES]_ are listed in the tables below. .. attention:: Asteroid designations in the text version of the documentation may be prefixed with a backslash, which should be ignored. This is to force correct rendering of the designation in the rendered versions of the documentation (e.g., HTML). +------------+-----------------------------------+ | Target | Description | +============+===================================+ | \(3202) | Numbered minor planet (3202) | +------------+-----------------------------------+ | 14829 | Numbered minor planet (14829) | +------------+-----------------------------------+ | 1997 XF11 | Unnumbered minor planet 1997 XF11 | +------------+-----------------------------------+ | 1P | Comet 1P/Halley | +------------+-----------------------------------+ | C/2003 A2 | Comet C/2003 A2 (Gleason) | +------------+-----------------------------------+ | P/2003 CP7 | Comet P/2003 CP7 (LINEAR-NEAT) | +------------+-----------------------------------+ For comets, P/ and C/ are interchangable. The designation may also be in a packed format: +------------+-----------------------------------+ | Target | Description | +============+===================================+ | 00233 | Numbered minor planet (233) | +------------+-----------------------------------+ | K03A07A | Unnumbered minor planet 2003 AA7 | +------------+-----------------------------------+ | PK03C07P | Comet P/2003 CP7 (LINEAR-NEAT) | +------------+-----------------------------------+ | 0039P | Comet 39P/Oterma | +------------+-----------------------------------+ You may also search by name: +------------+-----------------------------------+ | Target | Description | +============+===================================+ | Encke | \(9134) Encke | +------------+-----------------------------------+ | Africa | \(1193) Africa | +------------+-----------------------------------+ | Africano | \(6391) Africano | +------------+-----------------------------------+ | P/Encke | 2P/Encke | +------------+-----------------------------------+ | C/Encke | 2P/Encke | +------------+-----------------------------------+ | C/Gleason | C/2003 A2 (Gleason) | +------------+-----------------------------------+ If a comet name is not unique, the first match will be returned. References ---------- .. [MPES] Williams, G. The Minor Planet Ephemeris Service. https://minorplanetcenter.net/iau/info/MPES.pdf (retrieved 2018 June 19). .. IAU Minor Planet Center. List of Observatory codes. https://minorplanetcenter.net/iau/lists/ObsCodesF.html (retrieved 2018 June 19). Examples -------- >>> from astroquery.mpc import MPC >>> tab = astroquery.mpc.MPC.get_ephemeris('(24)', location=568, ... start='2003-02-26', step='100d', number=3) # doctest: +SKIP >>> print(tab) # doctest: +SKIP """ # parameter checks if type(location) not in (str, int, EarthLocation): if hasattr(location, '__iter__'): if len(location) != 3: raise ValueError( "location arrays require three values:" " longitude, latitude, and altitude") else: raise TypeError( "location must be a string, integer, array-like," " or astropy EarthLocation") if start is not None: _start = Time(start) else: _start = None # step must be one of these units, and must be an integer (we # will convert to an integer later). MPES fails for large # integers, so we cannot just convert everything to seconds. _step = u.Quantity(step) if _step.unit not in [u.d, u.h, u.min, u.s]: raise ValueError( 'step must have units of days, hours, minutes, or seconds.') if number is not None: if number > 1441: raise ValueError('number must be <=1441') if eph_type not in self._ephemeris_types.keys(): raise ValueError("eph_type must be one of {}".format( self._ephemeris_types.keys())) if proper_motion not in self._proper_motions.keys(): raise ValueError("proper_motion must be one of {}".format( self._proper_motions.keys())) if not u.Unit(proper_motion_unit).is_equivalent('rad/s'): raise ValueError("proper_motion_unit must be an angular rate.") # setup payload request_args = self._args_to_ephemeris_payload( target=target, ut_offset=ut_offset, suppress_daytime=suppress_daytime, suppress_set=suppress_set, perturbed=perturbed, location=location, start=_start, step=_step, number=number, eph_type=eph_type, proper_motion=proper_motion) # store for retrieval in _parse_result self._ra_format = ra_format self._dec_format = dec_format self._proper_motion_unit = u.Unit(proper_motion_unit) self._unc_links = unc_links if get_query_payload: return request_args self.query_type = 'ephemeris' response = self._request('POST', self.MPES_URL, data=request_args) return response
[docs] @class_or_instance def get_observatory_codes_async(self, *, get_raw_response=False, cache=True): """ Table of observatory codes from the IAU Minor Planet Center. Parameters ---------- get_raw_response : bool, optional Return raw data without parsing into a table (default: `False`). cache : bool Defaults to True. If set overrides global caching behavior. See :ref:`caching documentation <astroquery_cache>`. Returns ------- response : `requests.Response` The HTTP response returned from the service. References ---------- .. IAU Minor Planet Center. List of Observatory codes. https://minorplanetcenter.net/iau/lists/ObsCodesF.html (retrieved 2018 June 19). Examples -------- >>> from astroquery.mpc import MPC >>> obs = MPC.get_observatory_codes() # doctest: +SKIP >>> print(obs[295]) # doctest: +SKIP Code Longitude cos sin Name ---- --------- -------- --------- ------------- 309 289.59569 0.909943 -0.414336 Cerro Paranal """ self.query_type = 'observatory_code' response = self._request('GET', self.OBSERVATORY_CODES_URL, timeout=self.TIMEOUT, cache=cache) return response
[docs] @class_or_instance def get_observatory_location(self, code, *, cache=True): """ IAU observatory location. Parameters ---------- code : string Three-character IAU observatory code. cache : bool Defaults to True. If set overrides global caching behavior. See :ref:`caching documentation <astroquery_cache>`. Returns ------- longitude : Angle Observatory longitude (east of Greenwich). cos : float Parallax constant ``rho * cos(phi)`` where ``rho`` is the geocentric distance in earth radii, and ``phi`` is the geocentric latitude. sin : float Parallax constant ``rho * sin(phi)``. name : string The name of the observatory. Raises ------ LookupError If `code` is not found in the MPC table. Examples -------- >>> from astroquery.mpc import MPC >>> obs = MPC.get_observatory_location('000') >>> print(obs) # doctest: +SKIP (<Angle 0. deg>, 0.62411, 0.77873, 'Greenwich') """ if not isinstance(code, str): raise TypeError('code must be a string') if len(code) != 3: raise ValueError('code must be three charaters long') tab = self.get_observatory_codes(cache=cache) for row in tab: if row[0] == code: return Angle(row[1], 'deg'), row[2], row[3], row[4] raise LookupError('{} not found'.format(code))
def _args_to_object_payload(self, **kwargs): request_args = kwargs kwargs['json'] = 1 return_fields = kwargs.pop('return_fields', None) if return_fields: kwargs['return'] = return_fields return request_args def _args_to_ephemeris_payload(self, **kwargs): request_args = { 'ty': 'e', 'TextArea': str(kwargs['target']), 'uto': str(kwargs['ut_offset']), 'igd': 'y' if kwargs['suppress_daytime'] else 'n', 'ibh': 'y' if kwargs['suppress_set'] else 'n', 'fp': 'y' if kwargs['perturbed'] else 'n', 'adir': 'N', # always measure azimuth eastward from north 'tit': '', # dummy page title 'bu': '' # dummy base URL } location = kwargs['location'] if isinstance(location, str): request_args['c'] = location elif isinstance(location, int): request_args['c'] = '{:03d}'.format(location) elif isinstance(location, EarthLocation): loc = location.geodetic request_args['long'] = loc[0].deg request_args['lat'] = loc[1].deg request_args['alt'] = loc[2].to(u.m).value elif hasattr(location, '__iter__'): request_args['long'] = Angle(location[0]).deg request_args['lat'] = Angle(location[1]).deg request_args['alt'] = u.Quantity(location[2]).to('m').value if kwargs['start'] is None: _start = Time.now() _start.precision = 0 # integer seconds request_args['d'] = _start.iso.replace(':', '') else: _start = Time(kwargs['start'], precision=0, scale='utc') # integer seconds request_args['d'] = _start.iso.replace(':', '') request_args['i'] = str(int(round(kwargs['step'].value))) request_args['u'] = str(kwargs['step'].unit)[:1] if kwargs['number'] is None: request_args['l'] = self._default_number_of_steps[ request_args['u']] else: request_args['l'] = kwargs['number'] request_args['raty'] = self._ephemeris_types[kwargs['eph_type']] request_args['s'] = self._proper_motions[kwargs['proper_motion']] request_args['m'] = 'h' # always return proper_motion as arcsec/hr return request_args
[docs] @class_or_instance def get_observations_async(self, targetid, *, id_type=None, comettype=None, get_mpcformat=False, get_raw_response=False, get_query_payload=False, cache=True): """ Obtain all reported observations for an asteroid or a comet from the `Minor Planet Center observations database <https://minorplanetcenter.net/db_search>`_. Parameters ---------- targetid : int or str Official target number or designation. If a number is provided (either as int or str), the input is interpreted as an asteroid number; asteroid designations are interpreted as such (note that a whitespace between the year and the remainder of the designation is required and no packed designations are allowed). To query a periodic comet number, you have to append ``'P'``, e.g., ``'234P'``. To query any comet designation, the designation has to start with a letter describing the comet type and a slash, e.g., ``'C/2018 E1'``. Comet or asteroid names, Palomar-Leiden Survey designations, and individual comet fragments cannot be queried. id_type : str, optional Manual override for identifier type. If ``None``, the identifier type is derived by parsing ``targetid``; if this automated classification fails, it can be set manually using this parameter. Possible values are ``'asteroid number'``, ``'asteroid designation'``, ``'comet number'``, and ``'comet designation'``. Default: ``None`` get_mpcformat : bool, optional If ``True``, this method will return an `~astropy.table.QTable` with only a single column holding the original MPC 80-column observation format. Default: ``False`` get_raw_response : bool, optional If ``True``, this method will return the raw output from the MPC servers (json). Default: ``False`` get_query_payload : bool, optional Return the HTTP request parameters as a dictionary (default: ``False``). cache : bool Defaults to True. If set overrides global caching behavior. See :ref:`caching documentation <astroquery_cache>`. Raises ------ RuntimeError If query did not return any data. ValueError If target name could not be parsed properly and target type could not be identified. Notes ----- The following quantities are included in the output table +-------------------+--------------------------------------------+ | Column Name | Definition | +===================+============================================+ | ``number`` | official IAU target number (int) | +-------------------+--------------------------------------------+ | ``desig`` | provisional target designation (str) | +-------------------+--------------------------------------------+ | ``discovery`` (*) | target discovery flag (str) | +-------------------+--------------------------------------------+ | ``comettype`` (*) | orbital type of comet (str) | +-------------------+--------------------------------------------+ | ``note1`` (#) | Note1 (str) | +-------------------+--------------------------------------------+ | ``note2`` (#) | Note2 (str) | +-------------------+--------------------------------------------+ | ``epoch`` | epoch of observation (Julian Date, float) | +-------------------+--------------------------------------------+ | ``RA`` | RA reported (J2000, deg, float) | +-------------------+--------------------------------------------+ | ``DEC`` | declination reported (J2000, deg, float) | +-------------------+--------------------------------------------+ | ``mag`` | reported magnitude (mag, float) | +-------------------+--------------------------------------------+ | ``band`` (*) | photometric band for ``mag`` (str) | +-------------------+--------------------------------------------+ | ``phottype`` (*) | comet photometry type (nuclear/total, str) | +-------------------+--------------------------------------------+ | ``catalog`` (!) | star catalog used in the observation (str) | +-------------------+--------------------------------------------+ | ``observatory`` | IAU observatory code (str) | +-------------------+--------------------------------------------+ (*): Column names are optional and depend on whether an asteroid or a comet has been queried. (#): Parameters ``Note1`` and ``Note2`` are defined in the `MPC 80-column format description <https://minorplanetcenter.net/iau/info/OpticalObs.html>`_ (!): `Description of star catalog codes <https://minorplanetcenter.net/iau/info/OpticalObs.html>`_ Examples -------- >>> from astroquery.mpc import MPC >>> MPC.get_observations(12893) # doctest: +SKIP <QTable length=2772> number desig discovery note1 ... band catalog observatory ... int32 str9 str1 str1 ... str1 str1 str3 ------ --------- --------- ----- ... ---- ------- ----------- 12893 1998 QS55 -- -- ... -- -- 413 12893 1998 QS55 -- -- ... -- -- 413 12893 1998 QS55 * 4 ... -- -- 809 12893 1998 QS55 -- 4 ... -- -- 809 12893 1998 QS55 -- 4 ... -- -- 809 ... ... ... ... ... ... ... ... 12893 1998 QS55 -- -- ... o V T05 12893 1998 QS55 -- -- ... o V M22 12893 1998 QS55 -- -- ... o V M22 12893 1998 QS55 -- -- ... o V M22 12893 1998 QS55 -- -- ... o V M22 """ request_payload = {'table': 'observations'} if id_type is None: pat = ('(^[0-9]*$)|' # [0] asteroid number '(^[0-9]{1,3}[PIA]$)' # [1] periodic comet number '(-[1-9A-Z]{0,2})?$|' # [2] fragment '(^[PDCXAI]/[- 0-9A-Za-z]*)' # [3] comet designation '(-[1-9A-Z]{0,2})?$|' # [4] fragment '(^([1A][8-9][0-9]{2}[ _][A-Z]{2}[0-9]{0,3}$|' '^20[0-9]{2}[ _][A-Z]{2}[0-9]{0,3}$)|' '(^[1-9][0-9]{3}[ _](P-L|T-[1-3]))$)' # asteroid designation [5] (old/new/Palomar-Leiden style) ) # comet fragments are extracted here, but the MPC server does # not allow for fragment-based queries m = re.findall(pat, str(targetid)) if len(m) == 0: raise ValueError(('Cannot interpret target ' 'identifier "{}".').format(targetid)) else: m = m[0] request_payload['object_type'] = 'M' if m[1] != '': request_payload['object_type'] = 'P' if m[3] != '': request_payload['object_type'] = m[3][0] if m[0] != '': request_payload['number'] = m[0] # asteroid number elif m[1] != '': request_payload['number'] = m[1][:-1] # per. comet number elif m[3] != '': request_payload['designation'] = m[3] # comet designation elif m[5] != '': request_payload['designation'] = m[5] # ast. designation else: if 'asteroid' in id_type: request_payload['object_type'] = 'M' if 'number' in id_type: request_payload['number'] = str(targetid) elif 'designation' in id_type: request_payload['designation'] = targetid if 'comet' in id_type: pat = ('(^[0-9]{1,3}[PIA])|' # [0] number '(^[PDCXAI]/[- 0-9A-Za-z]*)' # [1] designation ) m = re.findall(pat, str(targetid)) if len(m) == 0: raise ValueError(('Cannot parse comet type ' 'from "{}".').format(targetid)) else: m = m[0] if m[0] != '': request_payload['object_type'] = m[0][-1] elif m[1] != '': request_payload['object_type'] = m[1][0] if 'number' in id_type: request_payload['number'] = targetid[:-1] elif 'designation' in id_type: request_payload['designation'] = targetid self.query_type = 'observations' if get_query_payload: return request_payload response = self._request('GET', url=self.MPCOBS_URL, params=request_payload, auth=(self.MPC_USERNAME, self.MPC_PASSWORD), timeout=self.TIMEOUT, cache=cache) if get_mpcformat: self.obsformat = 'mpc' else: self.obsformat = 'table' if get_raw_response: self.get_raw_response = True else: self.get_raw_response = False return response
def _parse_result(self, result, **kwargs): if self.query_type == 'object': try: data = result.json() except ValueError: raise InvalidQueryError(result.text) return data elif self.query_type == 'observatory_code': root = BeautifulSoup(result.content, 'html.parser') text_table = root.find('pre').text start = text_table.index('000') text_table = text_table[start:] # parse table ourselves to make sure the code column is a # string and that blank cells are masked rows = [] for line in text_table.splitlines(): lon = line[4:13] if len(lon.strip()) == 0: lon = np.nan else: lon = float(lon) c = line[13:21] if len(c.strip()) == 0: c = np.nan else: c = float(c) s = line[21:30] if len(s.strip()) == 0: s = np.nan else: s = float(s) rows.append((line[:3], lon, c, s, line[30:])) tab = Table(rows=rows, names=('Code', 'Longitude', 'cos', 'sin', 'Name'), dtype=(str, float, float, float, str), masked=True) tab['Longitude'].mask = ~np.isfinite(tab['Longitude']) tab['cos'].mask = ~np.isfinite(tab['cos']) tab['sin'].mask = ~np.isfinite(tab['sin']) return tab elif self.query_type == 'ephemeris': content = result.content.decode() table_start = content.find('<pre>') if table_start == -1: raise InvalidQueryError(content) table_end = content.find('</pre>') text_table = content[table_start + 5:table_end] SKY = 'raty=a' in result.request.body HELIOCENTRIC = 'raty=s' in result.request.body GEOCENTRIC = 'raty=G' in result.request.body # columns = '\n'.join(text_table.splitlines()[:2]) # find column headings if SKY: # slurp to newline after "h m s" i = text_table.index('\n', text_table.index('h m s')) + 1 columns = text_table[:i] data_start = columns.count('\n') - 1 else: # slurp to newline after "JD_TT" i = text_table.index('\n', text_table.index('JD_TT')) + 1 columns = text_table[:i] data_start = columns.count('\n') - 1 first_row = text_table.splitlines()[data_start + 1] if SKY: names = ('Date', 'RA', 'Dec', 'Delta', 'r', 'Elongation', 'Phase', 'V') col_starts = (0, 18, 29, 39, 47, 56, 62, 69) col_ends = (17, 28, 38, 46, 55, 61, 68, 72) units = (None, None, None, 'au', 'au', 'deg', 'deg', 'mag') if 's=t' in result.request.body: # total motion names += ('Proper motion', 'Direction') units += ('arcsec/h', 'deg') elif 's=c' in result.request.body: # coord Motion names += ('dRA', 'dDec') units += ('arcsec/h', 'arcsec/h') elif 's=s' in result.request.body: # sky Motion names += ('dRA cos(Dec)', 'dDec') units += ('arcsec/h', 'arcsec/h') col_starts += (73, 81) col_ends += (80, 89) if 'Moon' in columns: # table includes Alt, Az, Sun and Moon geometry names += ('Azimuth', 'Altitude', 'Sun altitude', 'Moon phase', 'Moon distance', 'Moon altitude') col_starts += tuple((col_ends[-1] + offset for offset in (2, 9, 14, 20, 27, 33))) col_ends += tuple((col_ends[-1] + offset for offset in (8, 13, 19, 26, 32, 37))) units += ('deg', 'deg', 'deg', None, 'deg', 'deg') if 'Uncertainty' in columns: names += ('Uncertainty 3sig', 'Unc. P.A.') col_starts += tuple((col_ends[-1] + offset for offset in (2, 11))) col_ends += tuple((col_ends[-1] + offset for offset in (10, 16))) units += ('arcsec', 'deg') if ">Map</a>" in first_row and self._unc_links: names += ('Unc. map', 'Unc. offsets') col_starts += (first_row.index(' / <a') + 3, ) col_starts += ( first_row.index(' / <a', col_starts[-1]) + 3, ) # Unc. offsets is always last col_ends += (col_starts[-1] - 3, first_row.rindex('</a>') + 4) units += (None, None) elif HELIOCENTRIC: names = ('Object', 'JD', 'X', 'Y', 'Z', "X'", "Y'", "Z'") col_starts = (0, 12, 28, 45, 61, 77, 92, 108) col_ends = None units = (None, None, 'au', 'au', 'au', 'au/d', 'au/d', 'au/d') elif GEOCENTRIC: names = ('Object', 'JD', 'X', 'Y', 'Z') col_starts = (0, 12, 28, 45, 61) col_ends = None units = (None, None, 'au', 'au', 'au') tab = ascii.read(text_table, format='fixed_width_no_header', names=names, col_starts=col_starts, col_ends=col_ends, data_start=data_start, fill_values=(('N/A', np.nan),), fast_reader=False) for col, unit in zip(names, units): tab[col].unit = unit # Time for dates, Angle for RA and Dec; convert columns at user's request if SKY: # convert from MPES string to Time, MPES uses UT timescale tab['Date'] = Time(['{}-{}-{} {}:{}:{}'.format( d[:4], d[5:7], d[8:10], d[11:13], d[13:15], d[15:17]) for d in tab['Date']], scale='utc') # convert from MPES string: ra = Angle(tab['RA'], unit='hourangle').to('deg') dec = Angle(tab['Dec'], unit='deg') # optionally convert back to a string if self._ra_format is not None: ra_unit = self._ra_format.get('unit', ra.unit) ra = ra.to_string(**self._ra_format) else: ra_unit = ra.unit if self._dec_format is not None: dec_unit = self._dec_format.get('unit', dec.unit) dec = dec.to_string(**self._dec_format) else: dec_unit = dec.unit # replace columns tab.remove_columns(('RA', 'Dec')) tab.add_column(Column(ra, name='RA', unit=ra_unit), index=1) tab.add_column(Column(dec, name='Dec', unit=dec_unit), index=2) # convert proper motion columns for col in ('Proper motion', 'dRA', 'dRA cos(Dec)', 'dDec'): if col in tab.colnames: tab[col].convert_unit_to(self._proper_motion_unit) else: # convert from MPES string to Time tab['JD'] = Time(tab['JD'], format='jd', scale='tt') return tab elif self.query_type == 'observations': warnings.simplefilter("ignore", ErfaWarning) try: src = json.loads(result.text) except (ValueError, json.decoder.JSONDecodeError): raise RuntimeError( 'Server response not readable: "{}"'.format( result.text)) if len(src) == 0: raise EmptyResponseError(('No data queried. Are the target ' 'identifiers correct? Is the MPC ' 'database search working for your ' 'object? The service is hosted at ' 'https://www.minorplanetcenter.net/' 'search_db')) # return raw response if requested if self.get_raw_response: return src # return raw 80-column observation format if requested if self.obsformat == 'mpc': tab = Table([[o['original_record'] for o in src]]) tab.rename_column('col0', 'obs') return tab if all([o['object_type'] == 'M' for o in src]): # minor planets (asteroids) data = ascii.read("\n".join([o['original_record'] for o in src]), format='fixed_width_no_header', names=('number', 'pdesig', 'discovery', 'note1', 'note2', 'epoch', 'RA', 'DEC', 'mag', 'band', 'catalog', 'observatory'), col_starts=(0, 5, 12, 13, 14, 15, 32, 44, 65, 70, 71, 77), col_ends=(4, 11, 12, 13, 14, 31, 43, 55, 69, 70, 71, 79), fast_reader=False) # convert asteroid designations # old designation style, e.g.: 1989AB ident = data['pdesig'][0] if isinstance(ident, np.ma.masked_array) and ident.mask: ident = '' elif (len(ident) < 7 and ident[:4].isdigit() and ident[4:6].isalpha()): ident = ident[:4]+' '+ident[4:6] # Palomar Survey elif 'PLS' in ident: ident = ident[3:] + " P-L" # Trojan Surveys elif 'T1S' in ident: ident = ident[3:] + " T-1" elif 'T2S' in ident: ident = ident[3:] + " T-2" elif 'T3S' in ident: ident = ident[3:] + " T-3" # standard MPC packed 7-digit designation elif (ident[0].isalpha() and ident[1:3].isdigit() and ident[-1].isalpha() and ident[-2].isdigit()): yr = str(conf.pkd.find(ident[0]))+ident[1:3] let = ident[3]+ident[-1] num = str(conf.pkd.find(ident[4]))+ident[5] num = num.lstrip("0") ident = yr+' '+let+num data.add_column(Column([ident]*len(data), name='desig'), index=1) data.remove_column('pdesig') elif all([o['object_type'] != 'M' for o in src]): # comets data = ascii.read("\n".join([o['original_record'] for o in src]), format='fixed_width_no_header', names=('number', 'comettype', 'desig', 'note1', 'note2', 'epoch', 'RA', 'DEC', 'mag', 'phottype', 'catalog', 'observatory'), col_starts=(0, 4, 5, 13, 14, 15, 32, 44, 65, 70, 71, 77), col_ends=(3, 4, 12, 13, 14, 31, 43, 55, 69, 70, 71, 79), fast_reader=False) # convert comet designations ident = data['desig'][0] if (not isinstance(ident, (np.ma.masked_array, np.ma.core.MaskedConstant)) or not ident.mask): yr = str(conf.pkd.find(ident[0]))+ident[1:3] let = ident[3] # patch to parse asteroid designations if len(ident) == 7 and str.isalpha(ident[6]): let += ident[6] ident = ident[:6] + ident[7:] num = str(conf.pkd.find(ident[4]))+ident[5] num = num.lstrip("0") if len(ident) >= 7: frag = ident[6] if ident[6] != '0' else '' else: frag = '' ident = yr+' '+let+num+frag # remove and add desig column to overcome length limit data.remove_column('desig') data.add_column(Column([ident]*len(data), name='desig'), index=3) else: raise ValueError(('Object type is ambiguous. "{}" ' 'are present.').format( set([o['object_type'] for o in src]))) # convert dates to Julian Dates dates = [d[:10].replace(' ', '-') for d in data['epoch']] times = np.array([float(d[10:]) for d in data['epoch']]) jds = Time(dates, format='iso').jd+times data['epoch'] = jds # convert ra and dec to degrees coo = SkyCoord(ra=data['RA'], dec=data['DEC'], unit=(u.hourangle, u.deg), frame='icrs') data['RA'] = coo.ra.deg data['DEC'] = coo.dec.deg # convert Table to QTable data = QTable(data) data['epoch'].unit = u.d data['RA'].unit = u.deg data['DEC'].unit = u.deg # Masked quantities are not supported in older astropy, warnings are raised for <v5.0 with warnings.catch_warnings(): warnings.filterwarnings('ignore', message='dropping mask in Quantity column', category=UserWarning) data['mag'].unit = u.mag return data
MPC = MPCClass()