Source code for astroquery.besancon.core

# Licensed under a 3-clause BSD style license - see LICENSE.rst

import socket
import time
import copy
import sys
import re
import os
import warnings
from astropy.io import ascii
from urllib.error import URLError
from collections import OrderedDict
from ..query import BaseQuery
from ..utils import commons, prepend_docstr_nosections, async_to_sync
from . import conf

__all__ = ['Besancon', 'BesanconClass', 'parse_besancon_model_string']

keyword_defaults = {
    'rinf': 0.000000,
    'rsup': 50.000000,
    'dist_step_mode': 0,
    'dlr': 0.000,
    'kleg': 1,
    'longit': 10.62,
    'latit': -0.38,
    'soli': 0.0003,  # degrees.  0.00027777 = 1 arcsec
    'kleh': 1,
    'eq1': 2000.0,
    'al0': 200.00,
    'alm': 200.00,
    'dl': 0,
    'ab0': 59.00,
    'abm': 59.00,
    'db': 0,
    'adif': 0.700,
    'ev': [""] * 25,
    'AV': [""] * 25,
    'di': [""] * 25,
    'oo': [-7] + [-99] * 12,
    'ff': [15] + [99] * 12,
    'spectyp_min': 1,
    'subspectyp_min': 0,
    'spectyp_max': 9,
    'subspectyp_max': 5,
    'lumi': list(range(1, 8)),
    'sous_pop': list(range(1, 11)),
    'iband': 1,
    'band0': [8] * 9,
    'bandf': [25] * 9,
    'colind': ["B-V", "U-B", "V-I", "V-K", ],
    'nic': 4,
    'klea': 1,
    'sc': [[0, 0, 0]] * 9,
    'klee': 0,
    'throughform': 'ok',
    'kleb': 3,  # 3 = Catalogue Simulation, 1 = tables and differential counts
    'klec': 1,  # 1 = ubv, 15= cfhtls (photometric system)
    'cinem': 0,  # 0: no kinematics, 1: kinematics
    'outmod': "",
}
keyword_defaults['ff[15]'] = 500
keyword_defaults['oo[15]'] = -500

colors_limits = OrderedDict((ci, (-99, 99))
                            for ci in keyword_defaults['colind'])
mag_limits = {'U': (-99, 99), 'B': (-99, 99), 'V': (10, 18), 'R': (-99, 99),
              'I': (-99, 99), 'J': (-99, 99), 'H': (-99, 99), 'K': (-99, 99),
              'L': (-99, 99)}

mag_order = "VBURIJHKL"


[docs] @async_to_sync class BesanconClass(BaseQuery): # Since these are configuration options, they should probably be used # directly rather than re-stored as local variables. Then again, we need # to refactor this whole project to be class-based, so they should be # set for class instances. url_download = conf.download_url QUERY_URL = conf.model_form ping_delay = conf.ping_delay TIMEOUT = conf.timeout # sample file name: 1340900648.230224.resu result_re = re.compile(r"[0-9]{10}\.[0-9]{6}\.resu") def __init__(self, *, email=None): super().__init__() self.email = email
[docs] def get_besancon_model_file(self, filename, *, verbose=True, timeout=5.0): """ Download a Besancon model from the website. Parameters ---------- filename : string The besancon filename, with format ##########.######.resu verbose : bool Print details about the download process timeout : float Amount of time to wait after pinging the server to see if a file is present. Default 5s, which is probably reasonable. """ url = os.path.join(self.url_download, filename) elapsed_time = 0 t0 = time.time() if verbose: sys.stdout.write("Awaiting Besancon file...\n") while True: if verbose: sys.stdout.write(u"\r") sys.stdout.flush() try: with commons.get_readable_fileobj(url, remote_timeout=timeout, cache=True) as f: results = f.read() break except URLError: if verbose: sys.stdout.write(u"Waiting %0.1fs for model to finish" " (elapsed wait time %0.1fs, total " "wait time %0.1f)\r" % (self.ping_delay, elapsed_time, time.time() - t0)) sys.stdout.flush() time.sleep(self.ping_delay) elapsed_time += self.ping_delay continue except socket.timeout: if verbose: sys.stdout.write(u"Waiting %0.1fs for model to finish " "(elapsed wait time %0.1fs, total wait " "time %0.1f)\r" % (self.ping_delay, elapsed_time, time.time() - t0)) sys.stdout.flush() time.sleep(self.ping_delay) elapsed_time += self.ping_delay continue return parse_besancon_model_string(results)
def _parse_result(self, response, *, verbose=False, retrieve_file=True): """ retrieve_file : bool If True, will try to retrieve the file every 30s until it shows up. Otherwise, just returns the filename (the job is still executed on the remote server, though) """ if verbose: print("Loading request from Besancon server ...") # keep the text stored for possible later use text = response.text try: filename = self.result_re.search(text).group() except AttributeError: # if there are no matches errors = parse_errors(text) raise ValueError("Errors: " + "\n".join(errors)) if verbose: print("File is {0} and can be acquired from {1}" .format(filename, self.url_download + '/' + filename)) if retrieve_file: return self.get_besancon_model_file(filename) else: return filename def _parse_args(self, glon, glat, email, *, smallfield=True, extinction=0.7, area=0.0001, verbose=True, clouds=None, absmag_limits=(-7, 20), mag_limits=copy.copy(mag_limits), colors_limits=copy.copy(colors_limits), **kwargs): """ Perform a query on the Besancon model of the galaxy. https://model.obs-besancon.fr/ Parameters ---------- glon : float glat : float Galactic latitude and longitude at the center email : str A valid e-mail address to send the report of completion to smallfield : bool Small field (True) or Large Field (False) LARGE FIELD NOT SUPPORTED YET extinction : float Extinction per kpc in A_V area : float Area in square degrees absmag_limits : (float,float) Absolute magnitude lower,upper limits colors_limits : dict of (float,float) Should contain 4 elements listing color differences in the valid bands, e.g.: {"J-H":(99,-99),"H-K":(99,-99),"J-K":(99,-99),"V-K":(99,-99)} mag_limits = dict of (float,float) Lower and Upper magnitude difference limits for each magnitude band U B V R I J H K L clouds : list of 2-tuples Up to 25 line-of-sight clouds can be specified in pairs of (A_V, distance in pc) verbose : bool Print out extra error messages? kwargs : dict Can override any argument in the request if you know the name of the POST keyword. Returns ------- Either the filename or the table depending on whether 'retrieve file' is specified """ if email is None and hasattr(self, 'email'): email = self.email if (email is None or not isinstance(email, str) or not commons.validate_email(email)): raise ValueError("Must specify a valid e-mail address.") # create a new keyword dict based on inputs + defaults kwd = copy.copy(keyword_defaults) for key, val in kwargs.items(): if key in keyword_defaults: kwd[key] = val elif verbose and key not in ('retrieve_file',): print("Skipped invalid key %s" % key) kwd['kleg'] = 1 if smallfield else 2 if not smallfield: raise NotImplementedError kwd['longit'] = glon kwd['latit'] = glat kwd['adif'] = extinction kwd['soli'] = area kwd['oo'][0] = absmag_limits[0] kwd['ff'][0] = absmag_limits[1] for index, (key, val) in enumerate(colors_limits.items()): if key[0] in mag_order and key[1] == '-' and key[2] in mag_order: kwd['colind'][index] = key kwd['oo'][index + 9] = val[0] kwd['ff'][index + 9] = val[1] else: raise ValueError('Invalid color %s' % key) for (key, val) in mag_limits.items(): if key in mag_order: kwd['band0'][mag_order.index(key)] = val[0] kwd['bandf'][mag_order.index(key)] = val[1] else: raise ValueError('Invalid band %s' % key) if clouds is not None: for index, (AV, di) in enumerate(clouds): kwd['AV'][index] = AV kwd['di'][index] = di # parse the default dictionary # request_data = parse_besancon_dict(keyword_defaults) request_data = kwd.copy() # convert all array elements to arrays for dummy in range(2): # deal with nested lists for k, v in list(request_data.items()): if (isinstance(v, list) or (isinstance(v, tuple) and len(v) > 1)): if k in request_data: del request_data[k] for index, val in enumerate(v): request_data['%s[%i]' % (k, index)] = val # an e-mail address is required request_data['email'] = email return request_data
[docs] @prepend_docstr_nosections("\n" + _parse_args.__doc__ + _parse_result.__doc__) def query_async(self, *args, **kwargs): """ Returns ------- response : `requests.Response` object The response of the HTTP request. """ data_payload = self._parse_args(*args, **kwargs) if kwargs.get('get_query_payload'): return data_payload response = self._request("POST", url=self.QUERY_URL, data=data_payload, timeout=self.TIMEOUT, stream=True) return response
Besancon = BesanconClass() def parse_besancon_dict(bd): """ Turn a dict like default_keys into a list of tuples. Must be a list of tuples because there are some repeated entries, which dictionaries do not support. .. todo:: In the future, a better way to do this is to make each dict entry a list; requests knows how to deal with this properly """ http_dict = [] for key, val in bd.items(): if isinstance(val, list): if "[]" in key: for listval in val: http_dict.append((key, listval)) else: for index, listval in enumerate(val): if isinstance(listval, list): for inner_index, inner_listval in enumerate(listval): http_dict.append((key + "[%i][%i]" % (index, inner_index), inner_listval)) else: http_dict.append((key + "[%i]" % (index), listval)) else: http_dict.append((key, val)) return http_dict def parse_errors(text): """ Attempt to extract the errors from a Besancon web page with error messages in it. """ try: errors = re.compile(r"""<div\ class="?errorpar"?>\s* <ol>\s* (<li>([a-zA-Z0-9):( \s_-]*)</li>\s*)*\s* </ol>\s* </div>""", re.X) text = errors.search(text).group() except AttributeError: raise ValueError("Regular expression matching to error " "message failed.") text_items = re.split(r"<li>|</li>|\n", errors.search(text).group()) text_items = [t for t in text_items if t != ""] error_list = text_items[2:-2] return error_list
[docs] def parse_besancon_model_file(filename): """ Parse a besancon model from a file on disk """ with open(filename, 'r') as f: contents = f.read() return parse_besancon_model_string(contents)
[docs] def parse_besancon_model_string(bms,): """ Given an entire Besancon model result in *string* form, parse it into an `~astropy.table.Table`. """ header_start = "Dist Mv CL".split() # locate index of data start lines = bms.split('\n') nblanks1 = 0 for index, line in enumerate(lines): if line.strip() == '': nblanks1 += 1 if all([h in line for h in header_start]): break names = line.split() ncols = len(names) header_line = index # data starts 1 line after header first_data_line = lines[header_line + 1] # apparently ascii wants you to start 1 early though data_start = header_line # ascii.read ignores blank lines data_start -= nblanks1 # locate index of data end nblanks2 = 0 for data_index, line in enumerate(lines[::-1]): if "TOTAL NUMBER OF STARS :" in line: nstars = int(line.split()[-1]) if line.strip() == '': nblanks2 += 1 if all([h in line for h in header_start]): break # most likely = -7 data_end = -(data_index - nblanks2 + 1) # note: old col_starts/col_ends were: # (0,7,13,16,21,27,33,36,41,49,56,62,69,76,82,92,102,109) # (6,12,15,20,26,32,35,39,48,55,61,68,75,81,91,101,108,115) space_indices = [first_data_line.find(" ", ii) for ii in range(len(first_data_line))] col_ends = [y for x, y in zip(space_indices[:-1], space_indices[1:]) if y - x > 1] + [len(space_indices)] if not all(x < y for x, y in zip(col_ends[:-1], col_ends[1:])): raise ValueError("Failed to parse Besancon table header.") col_starts = [0] + [c for c in col_ends[:-1]] if len(col_starts) != ncols or len(col_ends) != ncols: raise ValueError("Table parsing error: mismatch between # of " "columns & header") besancon_table = ascii.read(bms, format='fixed_width_no_header', data_start=data_start, data_end=data_end, col_starts=col_starts, col_ends=col_ends, names=names, fast_reader=False) if len(besancon_table) != nstars: raise ValueError("Besancon table did not match reported size") for cn in besancon_table.columns: if besancon_table[cn].dtype.kind in ('s', 'S'): warnings.warn("The Besancon table did not parse properly. " "Some columns are likely to have invalid " "values and others incorrect values. " "Please report this error.") break return besancon_table