sbident
View Source
import warnings import requests from pandas import DataFrame from astropy.time import Time from astropy.table import Table from astropy.coordinates import SkyCoord from urllib.parse import urlencode class SBIdent: """ A class for querying the JPL Small Body Identification API: https://ssd-api.jpl.nasa.gov/doc/sb_ident.html Attributes ---------- json : dict The full json response from the API query. summary : dict The summary response from the API query. data_field : str The dictionary key that indicates which response type the API returned. results : astropy.table The list of objects returned by the API query. uri : str The URI that is generated by the object instantiation. """ API_VERSION = '1.0' """ The API version this code expects from JPL. """ def __init__(self, location=None, obstime=None, fov=None, precision='high', hwidth=None, elem=False, maglim=None, magreq=True, filters=None, request=True): """ Parameters ---------- location : str or dict, required The observer location. Can be an MPC observatory code string or a dictionary of the following formats specified in the API documentation: geodetic coordinates, parallax, geocentric state vector or heliocentric state vector. obstime : numeric or time, required The time of observation. Can be numeric in which case it is assumed to be a Julian date. Otherwise use a time object. fov : SkyCoord or list-like of SkyCoord or dict, required Field of view. Specify a center-based field of view with a SkyCoord or an edge based field of view with a list (of length 2) of SkyCoords. hwidth : numeric or list-like, optional Half width of the field of view (in degrees) when field of view specified by a center-based SkyCoord. If hwdith is a single numeric value both RA and DEC will have the same width. If hwidth is list-like the RA and DEC width will correspond to the first and second elements of the list respectively. API defaults to 0.5 if not specified. Ignored if not using a center-based field of view. default: None precision : 'low' or 'high', optional Fidelity of the model used to calculate object position. 'low' is a two-body model, 'high' is a numerically integrated force model. default: 'high' elem : bool, optional When true return osculating orbital elements instead of RA and DEC. default: False maglim : numeric, optional Upper bound on the visual magnitude of the objects returned. default: None magreq : bool, optional Only return objects with defined visual magnitudes when true. default: True filters : dict, optional A dictionary that will be passed directly to the API as key=value pairs in order to support SBDB filtering as defined by the API documentation. default : None request : bool, optional When false the API URI is formed but the request is not made. default : True """ # initializing some attributes self.json = None self.summary = None self.data_field = None self.results = Table() # minimal warning format warnings.formatwarning = lambda msg, *args, **kwargs: f'Warning: {msg}\n' # args: elem, magreq, request # test boolean arguments are boolean and set for a in ['elem', 'magreq', 'request']: if not isinstance(eval(a), bool): raise TypeError("'{}' must be either True or False".format(a)) self.elem = elem self.req_elem = {'req-elem': str(elem).lower()} self.magreq = magreq self.mag_required = {'mag-required': str(magreq).lower()} self.request = request # test maglim is numeric or None if isinstance(maglim, (int,float)): self.maglim = float(maglim) self.vmag_lim = {'vmag-lim': self.maglim} elif maglim==None: self.maglim = maglim self.vmag_lim = None else: raise TypeError("'maglim' must be either numeric or None") # test precision is either 'high' or 'low' if precision not in ['high', 'low']: raise TypeError("precision values must be either 'high' or 'low'") self.precision = precision self.two_pass = {'two-pass': str(self.precision=='high').lower()} self.suppress_first_pass = {'suppress-first-pass': str(self.precision=='high').lower()} # test filters is dict if isinstance(filters, dict) or filters==None: self.filters = filters else: raise TypeError("filters must be either a dictionary or None") # parse location if isinstance(location, str): # using observatory mpc-code location self.location = {'mpc-code': location} elif isinstance(location, dict): if ('mpc-code' in location): # using observatory mpc-code location self.location = {'mpc-code': location['mpc-code']} elif ('lat' in location and 'lon' in location and 'alt' in location): # using geodetic location self.location = {k:location[k] for k in ['lat', 'lon', 'alt']} elif ('lon' in location and 'dxy' in location and 'dz' in location): # using parallax location self.location = {k:location[k] for k in ['lon', 'dxy', 'dz']} elif ('xobs' in location): # using geocentric location self.location = {'xobs': location['xobs']} elif ('xobs-hel' in location): # using heliocentric location self.location = {'xobs-hel': location['xobs-hel']} else: # location missing some required fields raise ValueError("'location' argument missing some required fields") else: # location type unexpected raise TypeError("Unexpected argument type for 'location'. Must be string or dictionary.") # parse obstime; handle string, float (jdate), Time or dict if isinstance(obstime, str): # passing UTC string self.obstime = {'obs-time': obstime} elif isinstance(obstime, float): # assumes float is jdate self.obstime = {'obs-time': Time(obstime, format='jd', precision=0).utc.iso} elif isinstance(obstime, Time): # passing Time object obstime.precision = 0 self.obstime = {'obs-time': obstime.utc.iso} elif isinstance(obstime, dict) and 'obs-time' in obstime: self.obstime = obstime else: # obstime type unexpected raise TypeError("Unexpected argument type for 'obstime'. Must be string, float (jdate) or Time.") if isinstance(fov, SkyCoord): # if fov is of type SkyCoord get a center relative fov radec_jpl = self.skycoord_to_jplstr(fov) fov = {'fov-ra-center': radec_jpl[0], 'fov-dec-center': radec_jpl[1]} elif isinstance(fov, (tuple, list)) and len(fov)==2 and all(isinstance(f, SkyCoord) for f in fov): # if fov is a list or tuple of SkyCoords we treat it as an edge type field of view radec_jpl0 = self.skycoord_to_jplstr(fov[0]) radec_jpl1 = self.skycoord_to_jplstr(fov[1]) fov = {'fov-ra-lim': "{},{}".format(radec_jpl0[0],radec_jpl1[0]), 'fov-dec-lim': "{},{}".format(radec_jpl0[1],radec_jpl1[1])} # handle hwidth - this has precedence if it's defined if hwidth is not None: if isinstance(hwidth, (tuple, list)) and len(hwidth)==2: fov['fov-ra-hwidth'] = str(hwidth[0]) fov['fov-dec-hwidth'] = str(hwidth[1]) elif isinstance(hwidth, (int, float)): fov['fov-ra-hwidth'] = str(hwidth) fov['fov-dec-hwidth'] = str(hwidth) else: #unexpected hwidth type raise TypeError("Unexpected argument type for 'hwidth'; must be either numeric or list-like of numerics of length 2") # continued fov parsing if isinstance(fov, dict): if ('fov-ra-center' in fov and 'fov-dec-center' in fov): # center fov method self.fov = {k:fov[k] for k in ['fov-ra-center', 'fov-dec-center']} if 'fov-ra-hwidth' in fov: self.fov['fov-ra-hwidth'] = fov['fov-ra-hwidth'] if 'fov-dec-hwidth' in fov: self.fov['fov-dec-hwidth'] = fov['fov-dec-hwidth'] elif ('fov-ra-lim' in fov and 'fov-dec-lim' in fov): # edge fov method - warn if hwidth defined self.fov = {k:fov[k] for k in ['fov-ra-lim', 'fov-dec-lim']} if hwidth is not None: warnings.warn("'hwidth' ignored when using an edge-based field of view") else: # fov missing some required fields raise ValueError("'fov' argument missing required keys") else: # fov type unexpected raise TypeError("Unexpected argument type for 'fov'; must be either SkyCoord or dictionary") self.form_uri() if request: self.make_request() else: self.request = False def __repr__(self): """ show representation of object in terms that are instantiate-able """ repr = "<SBIdent(location={l}, obstime={t}, fov={fov}, filters={op}, precision='{p}', maglim={ml}, magreq={mr}, request={req})>" return repr.format(l=self.location, t=self.obstime, fov=self.fov, op=self.filters, p=self.precision, ml=self.maglim, mr=self.magreq, req=self.request) def form_uri(self): """ Forms the URI from parameters specified by instantiation. """ base_uri = "https://ssd-api.jpl.nasa.gov/sb_ident.api?" params = {} build = [self.location, self.obstime, self.fov, self.two_pass, self.suppress_first_pass, self.vmag_lim, self.mag_required, self.req_elem] for b in build: if b is not None: params.update(b) self.uri = base_uri + urlencode(params) if self.filters is not None: self.uri = self.uri + "&" + urlencode(self.filters) return self.uri def read_uri(self): """ Makes the API request and checks for warnings or errors in the response. """ # read & set json response = requests.get(self.uri) json = response.json() self.json = json # report API error if one exists if 'code' in self.json: raise SystemError("API error: {}".format(self.json['message'])) # warn if API version is unexpected ver = self.json['signature']['version'] if ver != self.API_VERSION: version_warning = "Expected API version '{ev}' but response indicated version '{v}'. Unexpected behavior possible." warnings.warn(version_warning.format(API_VERSION,ver)) def parse_json(self): """ Parses the json returned for the API and sets attributes for the objects returned. """ # make sure json exists if self.json is None: raise UnboundLocalError("No json to parse in API response") # report API warning if one exists if 'warning' in self.json: warnings.warn("API warning: {}".format(self.json['warning'])) # set summary self.summary = self.json['summary'] # set results to the most precise results present data_fields = ['data_second_pass', 'data_first_pass', 'elem_first_pass', 'elem_second_pass'] for d in data_fields: if d in self.json: self.data_field = d continue # no data returned if self.data_field is None: return # column fields depend on first or second pass selection if 'first' in self.data_field: col_field = 'fields_first' count_field = 'n_first_pass' else: col_field = 'fields_second' count_field = 'n_second_pass' # the actual json parse rows = self.json[self.data_field] columns = self.json[col_field] df = DataFrame(rows, columns=columns) results = Table.from_pandas(df) self.results = results def make_request(self): """ Make the API call and parse return. Usually for when object instantiated with 'request=False' and thus no API request was made initially. """ self.read_uri() self.parse_json() @classmethod def skycoord_to_jplstr(cls,c): """ Class method that converts a SkyCoord to a list with the JPL Small Body Identification RA and DEC strings. Parameters ---------- c : SkyCoord The SkyCoord that you want to convert to a JPL string. """ cstr = c.icrs.to_string('hmsdms', sep=":", precision=2) radec_jpl = cstr.replace('-','M').replace(':','-').split(' ') return radec_jpl
View Source
class SBIdent: """ A class for querying the JPL Small Body Identification API: https://ssd-api.jpl.nasa.gov/doc/sb_ident.html Attributes ---------- json : dict The full json response from the API query. summary : dict The summary response from the API query. data_field : str The dictionary key that indicates which response type the API returned. results : astropy.table The list of objects returned by the API query. uri : str The URI that is generated by the object instantiation. """ API_VERSION = '1.0' """ The API version this code expects from JPL. """ def __init__(self, location=None, obstime=None, fov=None, precision='high', hwidth=None, elem=False, maglim=None, magreq=True, filters=None, request=True): """ Parameters ---------- location : str or dict, required The observer location. Can be an MPC observatory code string or a dictionary of the following formats specified in the API documentation: geodetic coordinates, parallax, geocentric state vector or heliocentric state vector. obstime : numeric or time, required The time of observation. Can be numeric in which case it is assumed to be a Julian date. Otherwise use a time object. fov : SkyCoord or list-like of SkyCoord or dict, required Field of view. Specify a center-based field of view with a SkyCoord or an edge based field of view with a list (of length 2) of SkyCoords. hwidth : numeric or list-like, optional Half width of the field of view (in degrees) when field of view specified by a center-based SkyCoord. If hwdith is a single numeric value both RA and DEC will have the same width. If hwidth is list-like the RA and DEC width will correspond to the first and second elements of the list respectively. API defaults to 0.5 if not specified. Ignored if not using a center-based field of view. default: None precision : 'low' or 'high', optional Fidelity of the model used to calculate object position. 'low' is a two-body model, 'high' is a numerically integrated force model. default: 'high' elem : bool, optional When true return osculating orbital elements instead of RA and DEC. default: False maglim : numeric, optional Upper bound on the visual magnitude of the objects returned. default: None magreq : bool, optional Only return objects with defined visual magnitudes when true. default: True filters : dict, optional A dictionary that will be passed directly to the API as key=value pairs in order to support SBDB filtering as defined by the API documentation. default : None request : bool, optional When false the API URI is formed but the request is not made. default : True """ # initializing some attributes self.json = None self.summary = None self.data_field = None self.results = Table() # minimal warning format warnings.formatwarning = lambda msg, *args, **kwargs: f'Warning: {msg}\n' # args: elem, magreq, request # test boolean arguments are boolean and set for a in ['elem', 'magreq', 'request']: if not isinstance(eval(a), bool): raise TypeError("'{}' must be either True or False".format(a)) self.elem = elem self.req_elem = {'req-elem': str(elem).lower()} self.magreq = magreq self.mag_required = {'mag-required': str(magreq).lower()} self.request = request # test maglim is numeric or None if isinstance(maglim, (int,float)): self.maglim = float(maglim) self.vmag_lim = {'vmag-lim': self.maglim} elif maglim==None: self.maglim = maglim self.vmag_lim = None else: raise TypeError("'maglim' must be either numeric or None") # test precision is either 'high' or 'low' if precision not in ['high', 'low']: raise TypeError("precision values must be either 'high' or 'low'") self.precision = precision self.two_pass = {'two-pass': str(self.precision=='high').lower()} self.suppress_first_pass = {'suppress-first-pass': str(self.precision=='high').lower()} # test filters is dict if isinstance(filters, dict) or filters==None: self.filters = filters else: raise TypeError("filters must be either a dictionary or None") # parse location if isinstance(location, str): # using observatory mpc-code location self.location = {'mpc-code': location} elif isinstance(location, dict): if ('mpc-code' in location): # using observatory mpc-code location self.location = {'mpc-code': location['mpc-code']} elif ('lat' in location and 'lon' in location and 'alt' in location): # using geodetic location self.location = {k:location[k] for k in ['lat', 'lon', 'alt']} elif ('lon' in location and 'dxy' in location and 'dz' in location): # using parallax location self.location = {k:location[k] for k in ['lon', 'dxy', 'dz']} elif ('xobs' in location): # using geocentric location self.location = {'xobs': location['xobs']} elif ('xobs-hel' in location): # using heliocentric location self.location = {'xobs-hel': location['xobs-hel']} else: # location missing some required fields raise ValueError("'location' argument missing some required fields") else: # location type unexpected raise TypeError("Unexpected argument type for 'location'. Must be string or dictionary.") # parse obstime; handle string, float (jdate), Time or dict if isinstance(obstime, str): # passing UTC string self.obstime = {'obs-time': obstime} elif isinstance(obstime, float): # assumes float is jdate self.obstime = {'obs-time': Time(obstime, format='jd', precision=0).utc.iso} elif isinstance(obstime, Time): # passing Time object obstime.precision = 0 self.obstime = {'obs-time': obstime.utc.iso} elif isinstance(obstime, dict) and 'obs-time' in obstime: self.obstime = obstime else: # obstime type unexpected raise TypeError("Unexpected argument type for 'obstime'. Must be string, float (jdate) or Time.") if isinstance(fov, SkyCoord): # if fov is of type SkyCoord get a center relative fov radec_jpl = self.skycoord_to_jplstr(fov) fov = {'fov-ra-center': radec_jpl[0], 'fov-dec-center': radec_jpl[1]} elif isinstance(fov, (tuple, list)) and len(fov)==2 and all(isinstance(f, SkyCoord) for f in fov): # if fov is a list or tuple of SkyCoords we treat it as an edge type field of view radec_jpl0 = self.skycoord_to_jplstr(fov[0]) radec_jpl1 = self.skycoord_to_jplstr(fov[1]) fov = {'fov-ra-lim': "{},{}".format(radec_jpl0[0],radec_jpl1[0]), 'fov-dec-lim': "{},{}".format(radec_jpl0[1],radec_jpl1[1])} # handle hwidth - this has precedence if it's defined if hwidth is not None: if isinstance(hwidth, (tuple, list)) and len(hwidth)==2: fov['fov-ra-hwidth'] = str(hwidth[0]) fov['fov-dec-hwidth'] = str(hwidth[1]) elif isinstance(hwidth, (int, float)): fov['fov-ra-hwidth'] = str(hwidth) fov['fov-dec-hwidth'] = str(hwidth) else: #unexpected hwidth type raise TypeError("Unexpected argument type for 'hwidth'; must be either numeric or list-like of numerics of length 2") # continued fov parsing if isinstance(fov, dict): if ('fov-ra-center' in fov and 'fov-dec-center' in fov): # center fov method self.fov = {k:fov[k] for k in ['fov-ra-center', 'fov-dec-center']} if 'fov-ra-hwidth' in fov: self.fov['fov-ra-hwidth'] = fov['fov-ra-hwidth'] if 'fov-dec-hwidth' in fov: self.fov['fov-dec-hwidth'] = fov['fov-dec-hwidth'] elif ('fov-ra-lim' in fov and 'fov-dec-lim' in fov): # edge fov method - warn if hwidth defined self.fov = {k:fov[k] for k in ['fov-ra-lim', 'fov-dec-lim']} if hwidth is not None: warnings.warn("'hwidth' ignored when using an edge-based field of view") else: # fov missing some required fields raise ValueError("'fov' argument missing required keys") else: # fov type unexpected raise TypeError("Unexpected argument type for 'fov'; must be either SkyCoord or dictionary") self.form_uri() if request: self.make_request() else: self.request = False def __repr__(self): """ show representation of object in terms that are instantiate-able """ repr = "<SBIdent(location={l}, obstime={t}, fov={fov}, filters={op}, precision='{p}', maglim={ml}, magreq={mr}, request={req})>" return repr.format(l=self.location, t=self.obstime, fov=self.fov, op=self.filters, p=self.precision, ml=self.maglim, mr=self.magreq, req=self.request) def form_uri(self): """ Forms the URI from parameters specified by instantiation. """ base_uri = "https://ssd-api.jpl.nasa.gov/sb_ident.api?" params = {} build = [self.location, self.obstime, self.fov, self.two_pass, self.suppress_first_pass, self.vmag_lim, self.mag_required, self.req_elem] for b in build: if b is not None: params.update(b) self.uri = base_uri + urlencode(params) if self.filters is not None: self.uri = self.uri + "&" + urlencode(self.filters) return self.uri def read_uri(self): """ Makes the API request and checks for warnings or errors in the response. """ # read & set json response = requests.get(self.uri) json = response.json() self.json = json # report API error if one exists if 'code' in self.json: raise SystemError("API error: {}".format(self.json['message'])) # warn if API version is unexpected ver = self.json['signature']['version'] if ver != self.API_VERSION: version_warning = "Expected API version '{ev}' but response indicated version '{v}'. Unexpected behavior possible." warnings.warn(version_warning.format(API_VERSION,ver)) def parse_json(self): """ Parses the json returned for the API and sets attributes for the objects returned. """ # make sure json exists if self.json is None: raise UnboundLocalError("No json to parse in API response") # report API warning if one exists if 'warning' in self.json: warnings.warn("API warning: {}".format(self.json['warning'])) # set summary self.summary = self.json['summary'] # set results to the most precise results present data_fields = ['data_second_pass', 'data_first_pass', 'elem_first_pass', 'elem_second_pass'] for d in data_fields: if d in self.json: self.data_field = d continue # no data returned if self.data_field is None: return # column fields depend on first or second pass selection if 'first' in self.data_field: col_field = 'fields_first' count_field = 'n_first_pass' else: col_field = 'fields_second' count_field = 'n_second_pass' # the actual json parse rows = self.json[self.data_field] columns = self.json[col_field] df = DataFrame(rows, columns=columns) results = Table.from_pandas(df) self.results = results def make_request(self): """ Make the API call and parse return. Usually for when object instantiated with 'request=False' and thus no API request was made initially. """ self.read_uri() self.parse_json() @classmethod def skycoord_to_jplstr(cls,c): """ Class method that converts a SkyCoord to a list with the JPL Small Body Identification RA and DEC strings. Parameters ---------- c : SkyCoord The SkyCoord that you want to convert to a JPL string. """ cstr = c.icrs.to_string('hmsdms', sep=":", precision=2) radec_jpl = cstr.replace('-','M').replace(':','-').split(' ') return radec_jpl
A class for querying the JPL Small Body Identification API: https://ssd-api.jpl.nasa.gov/doc/sb_ident.html
Attributes
- json (dict): The full json response from the API query.
- summary (dict): The summary response from the API query.
- data_field (str): The dictionary key that indicates which response type the API returned.
- results (astropy.table): The list of objects returned by the API query.
- uri (str): The URI that is generated by the object instantiation.
#  
SBIdent(
location=None,
obstime=None,
fov=None,
precision='high',
hwidth=None,
elem=False,
maglim=None,
magreq=True,
filters=None,
request=True
)
View Source
def __init__(self, location=None, obstime=None, fov=None, precision='high', hwidth=None, elem=False, maglim=None, magreq=True, filters=None, request=True): """ Parameters ---------- location : str or dict, required The observer location. Can be an MPC observatory code string or a dictionary of the following formats specified in the API documentation: geodetic coordinates, parallax, geocentric state vector or heliocentric state vector. obstime : numeric or time, required The time of observation. Can be numeric in which case it is assumed to be a Julian date. Otherwise use a time object. fov : SkyCoord or list-like of SkyCoord or dict, required Field of view. Specify a center-based field of view with a SkyCoord or an edge based field of view with a list (of length 2) of SkyCoords. hwidth : numeric or list-like, optional Half width of the field of view (in degrees) when field of view specified by a center-based SkyCoord. If hwdith is a single numeric value both RA and DEC will have the same width. If hwidth is list-like the RA and DEC width will correspond to the first and second elements of the list respectively. API defaults to 0.5 if not specified. Ignored if not using a center-based field of view. default: None precision : 'low' or 'high', optional Fidelity of the model used to calculate object position. 'low' is a two-body model, 'high' is a numerically integrated force model. default: 'high' elem : bool, optional When true return osculating orbital elements instead of RA and DEC. default: False maglim : numeric, optional Upper bound on the visual magnitude of the objects returned. default: None magreq : bool, optional Only return objects with defined visual magnitudes when true. default: True filters : dict, optional A dictionary that will be passed directly to the API as key=value pairs in order to support SBDB filtering as defined by the API documentation. default : None request : bool, optional When false the API URI is formed but the request is not made. default : True """ # initializing some attributes self.json = None self.summary = None self.data_field = None self.results = Table() # minimal warning format warnings.formatwarning = lambda msg, *args, **kwargs: f'Warning: {msg}\n' # args: elem, magreq, request # test boolean arguments are boolean and set for a in ['elem', 'magreq', 'request']: if not isinstance(eval(a), bool): raise TypeError("'{}' must be either True or False".format(a)) self.elem = elem self.req_elem = {'req-elem': str(elem).lower()} self.magreq = magreq self.mag_required = {'mag-required': str(magreq).lower()} self.request = request # test maglim is numeric or None if isinstance(maglim, (int,float)): self.maglim = float(maglim) self.vmag_lim = {'vmag-lim': self.maglim} elif maglim==None: self.maglim = maglim self.vmag_lim = None else: raise TypeError("'maglim' must be either numeric or None") # test precision is either 'high' or 'low' if precision not in ['high', 'low']: raise TypeError("precision values must be either 'high' or 'low'") self.precision = precision self.two_pass = {'two-pass': str(self.precision=='high').lower()} self.suppress_first_pass = {'suppress-first-pass': str(self.precision=='high').lower()} # test filters is dict if isinstance(filters, dict) or filters==None: self.filters = filters else: raise TypeError("filters must be either a dictionary or None") # parse location if isinstance(location, str): # using observatory mpc-code location self.location = {'mpc-code': location} elif isinstance(location, dict): if ('mpc-code' in location): # using observatory mpc-code location self.location = {'mpc-code': location['mpc-code']} elif ('lat' in location and 'lon' in location and 'alt' in location): # using geodetic location self.location = {k:location[k] for k in ['lat', 'lon', 'alt']} elif ('lon' in location and 'dxy' in location and 'dz' in location): # using parallax location self.location = {k:location[k] for k in ['lon', 'dxy', 'dz']} elif ('xobs' in location): # using geocentric location self.location = {'xobs': location['xobs']} elif ('xobs-hel' in location): # using heliocentric location self.location = {'xobs-hel': location['xobs-hel']} else: # location missing some required fields raise ValueError("'location' argument missing some required fields") else: # location type unexpected raise TypeError("Unexpected argument type for 'location'. Must be string or dictionary.") # parse obstime; handle string, float (jdate), Time or dict if isinstance(obstime, str): # passing UTC string self.obstime = {'obs-time': obstime} elif isinstance(obstime, float): # assumes float is jdate self.obstime = {'obs-time': Time(obstime, format='jd', precision=0).utc.iso} elif isinstance(obstime, Time): # passing Time object obstime.precision = 0 self.obstime = {'obs-time': obstime.utc.iso} elif isinstance(obstime, dict) and 'obs-time' in obstime: self.obstime = obstime else: # obstime type unexpected raise TypeError("Unexpected argument type for 'obstime'. Must be string, float (jdate) or Time.") if isinstance(fov, SkyCoord): # if fov is of type SkyCoord get a center relative fov radec_jpl = self.skycoord_to_jplstr(fov) fov = {'fov-ra-center': radec_jpl[0], 'fov-dec-center': radec_jpl[1]} elif isinstance(fov, (tuple, list)) and len(fov)==2 and all(isinstance(f, SkyCoord) for f in fov): # if fov is a list or tuple of SkyCoords we treat it as an edge type field of view radec_jpl0 = self.skycoord_to_jplstr(fov[0]) radec_jpl1 = self.skycoord_to_jplstr(fov[1]) fov = {'fov-ra-lim': "{},{}".format(radec_jpl0[0],radec_jpl1[0]), 'fov-dec-lim': "{},{}".format(radec_jpl0[1],radec_jpl1[1])} # handle hwidth - this has precedence if it's defined if hwidth is not None: if isinstance(hwidth, (tuple, list)) and len(hwidth)==2: fov['fov-ra-hwidth'] = str(hwidth[0]) fov['fov-dec-hwidth'] = str(hwidth[1]) elif isinstance(hwidth, (int, float)): fov['fov-ra-hwidth'] = str(hwidth) fov['fov-dec-hwidth'] = str(hwidth) else: #unexpected hwidth type raise TypeError("Unexpected argument type for 'hwidth'; must be either numeric or list-like of numerics of length 2") # continued fov parsing if isinstance(fov, dict): if ('fov-ra-center' in fov and 'fov-dec-center' in fov): # center fov method self.fov = {k:fov[k] for k in ['fov-ra-center', 'fov-dec-center']} if 'fov-ra-hwidth' in fov: self.fov['fov-ra-hwidth'] = fov['fov-ra-hwidth'] if 'fov-dec-hwidth' in fov: self.fov['fov-dec-hwidth'] = fov['fov-dec-hwidth'] elif ('fov-ra-lim' in fov and 'fov-dec-lim' in fov): # edge fov method - warn if hwidth defined self.fov = {k:fov[k] for k in ['fov-ra-lim', 'fov-dec-lim']} if hwidth is not None: warnings.warn("'hwidth' ignored when using an edge-based field of view") else: # fov missing some required fields raise ValueError("'fov' argument missing required keys") else: # fov type unexpected raise TypeError("Unexpected argument type for 'fov'; must be either SkyCoord or dictionary") self.form_uri() if request: self.make_request() else: self.request = False
Parameters
- location (str or dict, required): The observer location. Can be an MPC observatory code string or a dictionary of the following formats specified in the API documentation: geodetic coordinates, parallax, geocentric state vector or heliocentric state vector.
- obstime (numeric or time, required): The time of observation. Can be numeric in which case it is assumed to be a Julian date. Otherwise use a time object.
- fov (SkyCoord or list-like of SkyCoord or dict, required): Field of view. Specify a center-based field of view with a SkyCoord or an edge based field of view with a list (of length 2) of SkyCoords.
- hwidth (numeric or list-like, optional): Half width of the field of view (in degrees) when field of view specified by a center-based SkyCoord. If hwdith is a single numeric value both RA and DEC will have the same width. If hwidth is list-like the RA and DEC width will correspond to the first and second elements of the list respectively. API defaults to 0.5 if not specified. Ignored if not using a center-based field of view. default: None
- precision ('low' or 'high', optional): Fidelity of the model used to calculate object position. 'low' is a two-body model, 'high' is a numerically integrated force model. default: 'high'
- elem (bool, optional): When true return osculating orbital elements instead of RA and DEC. default: False
- maglim (numeric, optional): Upper bound on the visual magnitude of the objects returned. default: None
- magreq (bool, optional): Only return objects with defined visual magnitudes when true. default: True
- filters (dict, optional): A dictionary that will be passed directly to the API as key=value pairs in order to support SBDB filtering as defined by the API documentation. default : None
- request (bool, optional): When false the API URI is formed but the request is not made. default : True
The API version this code expects from JPL.
View Source
def form_uri(self): """ Forms the URI from parameters specified by instantiation. """ base_uri = "https://ssd-api.jpl.nasa.gov/sb_ident.api?" params = {} build = [self.location, self.obstime, self.fov, self.two_pass, self.suppress_first_pass, self.vmag_lim, self.mag_required, self.req_elem] for b in build: if b is not None: params.update(b) self.uri = base_uri + urlencode(params) if self.filters is not None: self.uri = self.uri + "&" + urlencode(self.filters) return self.uri
Forms the URI from parameters specified by instantiation.
View Source
def read_uri(self): """ Makes the API request and checks for warnings or errors in the response. """ # read & set json response = requests.get(self.uri) json = response.json() self.json = json # report API error if one exists if 'code' in self.json: raise SystemError("API error: {}".format(self.json['message'])) # warn if API version is unexpected ver = self.json['signature']['version'] if ver != self.API_VERSION: version_warning = "Expected API version '{ev}' but response indicated version '{v}'. Unexpected behavior possible." warnings.warn(version_warning.format(API_VERSION,ver))
Makes the API request and checks for warnings or errors in the response.
View Source
def parse_json(self): """ Parses the json returned for the API and sets attributes for the objects returned. """ # make sure json exists if self.json is None: raise UnboundLocalError("No json to parse in API response") # report API warning if one exists if 'warning' in self.json: warnings.warn("API warning: {}".format(self.json['warning'])) # set summary self.summary = self.json['summary'] # set results to the most precise results present data_fields = ['data_second_pass', 'data_first_pass', 'elem_first_pass', 'elem_second_pass'] for d in data_fields: if d in self.json: self.data_field = d continue # no data returned if self.data_field is None: return # column fields depend on first or second pass selection if 'first' in self.data_field: col_field = 'fields_first' count_field = 'n_first_pass' else: col_field = 'fields_second' count_field = 'n_second_pass' # the actual json parse rows = self.json[self.data_field] columns = self.json[col_field] df = DataFrame(rows, columns=columns) results = Table.from_pandas(df) self.results = results
Parses the json returned for the API and sets attributes for the objects returned.
View Source
def make_request(self): """ Make the API call and parse return. Usually for when object instantiated with 'request=False' and thus no API request was made initially. """ self.read_uri() self.parse_json()
Make the API call and parse return. Usually for when object instantiated with 'request=False' and thus no API request was made initially.
View Source
@classmethod def skycoord_to_jplstr(cls,c): """ Class method that converts a SkyCoord to a list with the JPL Small Body Identification RA and DEC strings. Parameters ---------- c : SkyCoord The SkyCoord that you want to convert to a JPL string. """ cstr = c.icrs.to_string('hmsdms', sep=":", precision=2) radec_jpl = cstr.replace('-','M').replace(':','-').split(' ') return radec_jpl
Class method that converts a SkyCoord to a list with the JPL Small Body Identification RA and DEC strings.
Parameters
- c (SkyCoord): The SkyCoord that you want to convert to a JPL string.