astro query:docs

- Index
- Modules

### Navigation

- astroquery v0.4.11 »
- Module code »
- astroquery.atomic »

# Source code for astroquery.atomic.core

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

    from io import StringIO
    from urllib import parse as urlparse

    from collections import defaultdict

    from astropy.io import ascii
    from astropy.table import Table
    from astropy import units as u
    from bs4 import BeautifulSoup

    from .. import log

    from ..query import BaseQuery
    from ..utils import prepend_docstr_nosections, async_to_sync
    from . import conf
    from .utils import is_valid_transitions_param

    __all__ = ['AtomicLineList', 'AtomicLineListClass']

    @async_to_sync
    class AtomicLineListClass(BaseQuery):
        FORM_URL = conf.url
        TIMEOUT = conf.timeout

        def __init__(self):
            super().__init__()
            self.__default_form_values = None

        def query_object(self, *, wavelength_range=None, wavelength_type=None, wavelength_accuracy=None,
                         element_spectrum=None,
                         minimal_abundance=None, depl_factor=None, lower_level_energy_range=None,
                         upper_level_energy_range=None, nmax=None, multiplet=None, transitions=None,
                         show_fine_structure=None, show_auto_ionizing_transitions=None,
                         output_columns=('spec', 'type', 'conf', 'term', 'angm', 'prob', 'ener'), cache=True,
                         get_query_payload=False):
            """
            Queries Atomic Line List for the given parameters adnd returns the
            result as a `~astropy.table.Table`. All parameters are optional.

            Parameters
            ----------
            wavelength_range : pair of `astropy.units.Unit` values
                Wavelength range. Can be done in two ways: supply a lower and
                upper limit for the region or, supply the central wavelength and
                the 1 sigma error (68 % confidence value) for that line. If the
                first number is smaller than the second number, this implies
                that the first option has been chosen, and otherwise the second
                option.

            wavelength_type : str
                Either ``'Air'`` or ``'Vacuum'``.

            wavelength_accuracy : str
                All wavelengths in the line list have relative accuracies of
                5% or better. The default is to list all lines, irrespective
                of their accuracy. When a relative accuracy in percent is
                given, only those lines with accuracies better than or equal
                to the passed value are included in the search. Values larger
                than 5% will be ignored.

            element_spectrum : str
                Restrict the search to a range of elements and/or ionization
                stages. The elements should be entered by their usual
                symbolic names (e.g. Fe) and the ionization stages by the
                usual spectroscopic notation (e.g. I for neutral, II for
                singly ionized etc.). To pass multiple values, separate them
                by ``\\n`` (newline).

            minimal_abundance : str
                 Impose a lower limit on the abundances of elements to be
                 considered for possible identifications. Default is to
                 consider arbitrary low abundances. The elements are assumed
                 to have standard cosmic abundances.

            depl_factor : str
                For nebular conditions it is not a realistic assumption that
                the elements have standard cosmic abundances since most
                metals will be depleted on grains. To simulate this it is
                possible to supply a depletion factor df. This factor will be
                used to calculate the actual abundance A from the cosmic
                abundance Ac using the formula A(elm) = Ac(elm) - df*sd(elm)
                where sd is the standard depletion for each element.

            lower_level_energy_range : `~astropy.units.Quantity`
                Default is to consider all values for the lower/upper level
                energy to find a possible identification. To restrict the
                search a range of energies can be supplied.
                The supported units are: Ry, eV, 1/cm, J, erg.

            upper_level_energy_range : `~astropy.units.Quantity`
                See parameter ``lower_level_energy_range``.

            nmax : int
                Maximum for principal quantum number n. Default is to
                consider all possible values for the principal quantum number
                n to find possible identifications. However, transitions
                involving electrons with a very high quantum number n tend to
                be weaker and can therefore be less likely identifications.
                These transitions can be suppressed using this parameter.

            multiplet : str
                This option (case sensitive) can be used to find all lines in
                a specific multiplet within a certain wavelength range. The
                lower and upper level term should be entered here exactly as
                they appear in the output of the query. The spectrum to which
                this multiplet belongs should of course also be supplied in
                the ``element_spectrum`` parameter.

            transitions : str`
                Possible values are:
                    - ``'all'``:
                        The default, consider all transition types.
                    - ``'nebular'``:
                        Consider only allowed transitions of Hydrogen or
                        Helium and only magnetic dipole or electric quadrupole
                        transitions of other elements.
                    - A union of the values: One of the following:
                      ``'E1'``, ``'IC'``, ``'M1'``, ``'E2'``
                      Refer to [1]_ for the meaning of these values.

            show_fine_structure : bool
                 If `True`, the fine structure components will be included in
                 the output. Refer to [1]_ for more information.

            show_auto_ionizing_transitions : bool
                If `True`, transitions originating from auto-ionizing levels
                will be included in the output. In this context, all levels
                with energies higher than the ionization potential going to
                the ground state of the next ion are considered auto-ionizing
                levels.

            output_columns : tuple
                A Tuple of strings indicating which output columns are retrieved.
                A subset of ('spec', 'type', 'conf', 'term', 'angm', 'prob',
                'ener') should be used. Where each string corresponds to the
                column titled Spectrum, Transition type, Configuration, Term,
                Angular momentum, Transition probability and Level energies
                respectively.

            cache : bool
                Defaults to True. If set overrides global caching behavior.
                See :ref:`caching documentation <astroquery_cache>`.

            Returns
            -------
            result : `~astropy.table.Table`
                The result of the query as a `~astropy.table.Table` object.

            References
            ----------
            .. [1] https://linelist.pa.uky.edu/atomic/instruction.html
            """
            response = self.query_object_async(
                wavelength_range=wavelength_range, wavelength_type=wavelength_type,
                wavelength_accuracy=wavelength_accuracy,
                element_spectrum=element_spectrum,
                minimal_abundance=minimal_abundance, depl_factor=depl_factor,
                lower_level_energy_range=lower_level_energy_range,
                upper_level_energy_range=upper_level_energy_range, nmax=nmax,
                multiplet=multiplet, transitions=transitions,
                show_fine_structure=show_fine_structure,
                show_auto_ionizing_transitions=show_auto_ionizing_transitions,
                output_columns=output_columns, cache=cache,
                get_query_payload=get_query_payload)
            if get_query_payload:
                return response

            table = self._parse_result(response)
            return table

        @prepend_docstr_nosections(query_object.__doc__)
        def query_object_async(self, *, wavelength_range=None, wavelength_type='', wavelength_accuracy=None,
                               element_spectrum=None, minimal_abundance=None, depl_factor=None,
                               lower_level_energy_range=None, upper_level_energy_range=None, nmax=None, multiplet=None,
                               transitions=None, show_fine_structure=None, show_auto_ionizing_transitions=None,
                               output_columns=('spec', 'type', 'conf', 'term', 'angm', 'prob', 'ener'),
                               cache=True, get_query_payload=False):
            """
            Returns
            -------
            response : `requests.Response`
                The HTTP response returned from the service.
            """
            default_values = self._default_form_values()
            wltype = (wavelength_type or default_values.get('air', '')).lower()
            if wltype in ('air', 'vacuum'):
                air = wltype.capitalize()
            else:
                raise ValueError('parameter wavelength_type must be either "air" '
                                 'or "vacuum".')
            if wavelength_range is not None:
                wlrange = wavelength_range
            else:
                wlrange = []
            if len(wlrange) not in (0, 2):
                raise ValueError('Length of `wavelength_range` must be 2 or 0, '
                                 'but is: {}'.format(len(wlrange)))
            if not is_valid_transitions_param(transitions):
                raise ValueError('Invalid parameter "transitions": {0!r}'
                                 .format(transitions))
            if transitions is None:
                _type = default_values.get('type')
                type2 = default_values.get('type2')
            else:
                s = str(transitions)
                if len(s.split(',')) > 1:
                    _type = 'Sel'
                    type2 = s.split(',')
                else:
                    _type = s
                    type2 = ''
            # convert wavelengths in incoming wavelength range to Angstroms
            wlrange_in_angstroms = (wl.to(u.Angstrom,
                                          equivalencies=u.spectral()).value
                                    for wl in wlrange)

            lower_level_erange = lower_level_energy_range
            if lower_level_erange is not None:
                lower_level_erange = lower_level_erange.to(
                    u.cm ** -1, equivalencies=u.spectral()).value
            upper_level_erange = upper_level_energy_range
            if upper_level_erange is not None:
                upper_level_erange = upper_level_erange.to(
                    u.cm ** -1, equivalencies=u.spectral()).value
            input_payload = {
                'wavl': ' '.join(map(str, wlrange_in_angstroms)),
                'wave': 'Angstrom',
                'air': air,
                'wacc': wavelength_accuracy,
                'elmion': element_spectrum,
                'abun': minimal_abundance,
                'depl': depl_factor,
                'elo': lower_level_erange,
                'ehi': upper_level_erange,
                'ener': 'cm^-1',
                'nmax': nmax,
                'term': multiplet,
                'type': _type,
                'type2': type2,
                'hydr': show_fine_structure,
                'auto': show_auto_ionizing_transitions,
                'form': output_columns,
                'jval': 'usej',
                'tptype': 'as_a',
            }
            if get_query_payload:
                return input_payload
            response = self._submit_form(input_payload, cache=cache)
            return response

        def _parse_result(self, response):

            if 'ERROR: request form contains no information' in response.text:
                raise ValueError("The server returned an error.  Please check the URL."
                                 f"   The full error message is {response.text}")
            elif 'ERROR' in response.text:
                raise ValueError(f"The server returned an error.  The full error message is {response.text}")

            html_pre = BeautifulSoup(response.text, features='html5lib').find('pre')
            if html_pre is None:
                raise ValueError("Data format not recognized.  The <pre> tag was missing from the response.")

            data = StringIO(html_pre.text.strip())
            # `header` is e.g.:
            # "u'-LAMBDA-VAC-ANG-|-SPECTRUM--|TT|--------TERM---------|---J-J---|----LEVEL-ENERGY--CM-1----'"
            # `colnames` is then
            # [u'LAMBDA VAC ANG', u'SPECTRUM', u'TT', u'TERM', u'J J',
            #  u'LEVEL ENERGY  CM 1']

            header = data.readline().strip().strip('|')

            colnames = [colname.strip('-').replace('-', ' ')
                        for colname in header.split('|') if colname.strip()]
            indices = [i for i, c in enumerate(header) if c == '|']
            result_data = []
            for line in data:
                row = []
                for start, end in zip([0] + indices, indices + [None]):
                    # `value` will hold all cell values in the line, so
                    # `u'1.010799'`, `u'Zn XXX'` etc.
                    value = line[start:end].strip()
                    if value:
                        row.append(value)
                    else:
                        # maintain table dimensions when data missing
                        row.append('None')
                if row:
                    result_data.append('\t'.join(row))
            if result_data:
                return ascii.read(result_data, data_start=0, delimiter='\t',
                                  names=colnames, fast_reader=False)
            else:
                # return an empty table if the query yielded no results
                return Table()

        def _submit_form(self, input_data=None, cache=True):
            """Fill out the form of the SkyView site and submit it with the
            values given in ``input_data`` (a dictionary where the keys are the form
            element's names and the values are their respective values).

            """
            if input_data is None:
                input_data = {}
            # only overwrite payload's values if the ``input_data`` value is not None
            # to avoid overwriting of the form's default values
            payload = self._default_form_values().copy()
            for k, v in input_data.items():
                if v is not None:
                    payload[k] = v
            url = self._form_action_url
            log.debug(f"final payload = {payload} from url={url}")
            response = self._request("POST", url=url, data=payload,
                                     timeout=self.TIMEOUT, cache=cache)
            response.raise_for_status()
            log.debug("Retrieved data from POST request")
            return response

        def _default_form_values(self):
            if self.__default_form_values is None:
                response = self._request("GET", url=self.FORM_URL, params={},
                                         timeout=self.TIMEOUT, cache=False)
                bs = BeautifulSoup(response.text, features='html5lib')
                form = bs.find('form')
                default_form_values = self._get_default_form_values(form)
                self._form_action_url = urlparse.urljoin(self.FORM_URL, form.get('action'))
                self.__default_form_values = default_form_values

            return self.__default_form_values

        def _get_default_form_values(self, form):
            """Return the already selected values of a given form (a BeautifulSoup
            form node) as a dict.

            """
            res = defaultdict(list)
            for elem in form.find_all(['input', 'select']):
                key = elem.get('name')
                value = None
                # ignore the submit and reset buttons
                if elem.get('type') in ['submit', 'reset']:
                    continue
                # check boxes: enabled boxes have the value "on" if not specified
                # otherwise. Found out by debugging, perhaps not documented.
                if (elem.get('type') == 'checkbox'
                        and elem.get('checked') in ["", "checked"]):
                    value = elem.get('value', 'on')
                # radio buttons and simple input fields
                if elem.get('type') == 'radio' and\
                        elem.get('checked') in ["", "checked"] or\
                        elem.get('type') in [None, 'text']:
                    value = elem.get('value')
                # dropdown menu, multi-section possible
                if elem.name == 'select':
                    for option in elem.find_all('option'):
                        if option.get('selected') == '':
                            value = option.get('value', option.text.strip())
                if value and value not in [None, u'None', u'null']:
                    res[key].append(value)

            # avoid values with size 1 lists
            d = dict(res)
            for k, v in d.items():
                if len(v) == 1:
                    d[k] = v[0]
            return d

    AtomicLineList = AtomicLineListClass()