# Licensed under a 3-clause BSD style license - see LICENSE.rst
# -*- coding: utf-8 -*-
"""
Container class for a database of emission-line parameters, as well as
support classes and functions.
Class usage examples
--------------------
To define an emission line::
from mangadap.par.emissionlinedb import EmissionLinePar
p = EmissionLinePar(index=44, name='Ha', restwave=6564.632, action='f', line='l',
flux=1.0, vel=0.0, sig=10., mode='f')
More often, however, you will want to define an emission-line
database using an SDSS parameter file. To do so, you can use one of
the default set of available emission-line databases::
from mangadap.par.emissionlinedb import EmissionLineDB
print(EmissionLineDB.available_databases())
emldb = EmissionLineDB.from_key('ELPMPL9')
The above call uses the
:func:`~mangadap.par.spectralfeaturedb.SpectralFeatureDB.from_key`
method to define the database using its keyword and the database
provided with the MaNGA DAP source distribution. You can also define
the database directly for an SDSS-style parameter file::
from mangadap.par.emissionlinedb import EmissionLineDB
emldb = EmissionLineDB('/path/to/emission/line/database/myeml.par')
The above will read the file and set the database keyword to
'MYEML' (i.e., the capitalized root name of the ``*.par`` file).
See :ref:`emissionlines` for the format of the parameter file.
----
.. include license and copyright
.. include:: ../include/copy.rst
----
.. include common links, assuming primary doc root is up one directory
.. include:: ../include/links.rst
"""
import os
import glob
from IPython import embed
import numpy
from pydl.goddard.astro import airtovac
from pydl.pydlutils.yanny import yanny
from .parset import KeywordParSet
from .spectralfeaturedb import SpectralFeatureDB
from ..proc import util
from ..util.datatable import DataTable
[docs]
class EmissionLinePar(KeywordParSet):
r"""
Metadata used to define an emission line for the DAP to fit
parametrically.
These parameters are only for a single emission line; see
:class:`~mangadap.par.emissionlinedb.EmissionLineDB` for setting a full
line list of parameters.
.. todo::
- Point to/add a description the paramters.
"""
def __init__(self, index=None, name=None, restwave=None, action=None, tie_index=None,
tie_par=None, blueside=None, redside=None):
in_fl = [ int, float ]
action_options = [ 'i', 'f', 'm', 's']
arr_like = [ numpy.ndarray, list ]
_name = None if name is None else name.strip()
_action = None if action is None else action.strip()
pars = ['index', 'name', 'restwave', 'action', 'tie_index', 'tie_par', 'blueside',
'redside']
values = [index, _name, restwave, _action, tie_index, tie_par, blueside, redside]
defaults = [None, None, None, 'f', None, None, None, None]
options = [None, None, None, action_options, None, None, None, None]
dtypes = [int, str, in_fl, str, arr_like, arr_like, arr_like, arr_like]
descr = ['An index used to refer to the line for tying parameters; must be >0.',
'A name for the line.',
'The rest wavelength of the line in angstroms *in vacuum*.',
'Describes how the line should be treated. See ' \
':ref:`emission-line-modeling-action`. Default is ``f``.',
'Indices the lines to which parameters for this line are tied; one index per ' \
'(3) Gaussian parameters. See :ref:`emission-line-modeling-tying`.',
'Details of how each model parameter for this line is tied to the reference ' \
'line. See :ref:`emission-line-modeling-tying`.',
'A two-element vector with the starting and ending wavelength for a bandpass ' \
'blueward of the emission line, which is used to set the continuum level ' \
'near the emission line when calculating the equivalent width.',
'A two-element vector with the starting and ending wavelength for a bandpass ' \
'redward of the emission line, which is used to set the continuum level ' \
'near the emission line when calculating the equivalent width.']
super().__init__(pars, values=values, defaults=defaults, options=options, dtypes=dtypes,
descr=descr)
self._check()
[docs]
def _check(self):
if self.data['index'] is not None and self.data['index'] <= 0:
raise ValueError('Index must be larger than 0.')
[docs]
class EmissionLineDefinitionTable(DataTable):
"""
A :class:`~mangadap.util.datatable.DataTable` with the data from an
:class:`~mangadap.par.emissionlinedb.EmissionLineDB` object.
Table includes:
.. include:: ../tables/emissionlinedefinitiontable.rst
Args:
name_len (:obj:`int`, optional):
The maximum length of any of the emission line names.
tie_len (:obj:`int`, optional):
The maximum length of a tying string. Not sure if this is needed.
shape (:obj:`int`, :obj:`tuple`, optional):
The shape of the initial array. If None, the data array
will not be instantiated; use
:func:`~mangadap.util.datatable.DataTable.init` to
initialize the data array after instantiation.
"""
def __init__(self, name_len=1, tie_len=1, shape=None):
# NOTE: This requires python 3.7 to make sure that this is an "ordered"
# dictionary.
datamodel = dict(ID=dict(typ=int, shape=None, descr='Emission line ID number'),
NAME=dict(typ='<U{0:d}'.format(name_len), shape=None,
descr='Name of the emission line'),
RESTWAVE=dict(typ=float, shape=None,
descr='Rest wavelength of the emission line'),
ACTION=dict(typ='<U1', shape=None,
descr='Action to take for this emission line; see '
':ref:`emission-line-modeling-action`'),
TIE_ID=dict(typ=int, shape=(3,),
descr='ID of the line to which this one is tied.'),
TIE_FLUX=dict(typ='<U{0:d}'.format(tie_len), shape=None,
descr='Tying parameter for the flux of each line.'),
TIE_VEL=dict(typ='<U{0:d}'.format(tie_len), shape=None,
descr='Tying parameter for the velocity of each line.'),
TIE_SIG=dict(typ='<U{0:d}'.format(tie_len), shape=None,
descr='Tying parameter for the dispersion of each line.'))
keys = list(datamodel.keys())
super().__init__(keys, [datamodel[k]['typ'] for k in keys],
element_shapes=[datamodel[k]['shape'] for k in keys],
descr=[datamodel[k]['descr'] for k in keys], shape=shape)
[docs]
class EmissionLineDB(SpectralFeatureDB):
r"""
Basic container class for the database of emission-line parameters.
Each row of the database is parsed using
:class:`~mangadap.par.emissionlinedb.EmissionLinePar`. For the format of
the input file, see :ref:`emissionlines-modeling`.
The primary instantiation requires the SDSS parameter file with the
emission-line data. To instantiate using a keyword, use the
:func:`~mangadap.par.spectralfeaturedb.SpectralFeatureDB.from_key` method
of the base class; see the base class for instantiation and additional
attributes.
Attributes:
key (:obj:`str`):
Database signifying keyword
file (:obj:`str`):
File with the emission-line data
size (:obj:`int`):
Number of emission lines in the database.
"""
default_data_dir = 'emission_lines'
"""
Name of the directory in the mangadap data path with the distributed
emission-line databases.
"""
[docs]
def _parse_yanny(self):
"""
Parse the yanny file (provided by :attr:`file`) for the emission-line
database.
Returns:
:obj:`list`: The list of :class:`~mangadap.par.parset.ParSet`
instances for each line of the database.
"""
# Read the yanny file
par = yanny(filename=self.file, raw=True)
if len(par['DAPEML']['index']) == 0:
raise ValueError('Could not find DAPEML entries in {0}!'.format(self.file))
# Setup the array of emission line database parameters
self.size = len(par['DAPEML']['index'])
parlist = []
for i in range(self.size):
invac = par['DAPEML']['waveref'][i] == 'vac'
tie_index = numpy.full(3, -1, dtype=int)
tie_par = numpy.empty(3, dtype=object)
for j, tie in enumerate(['tie_f', 'tie_v', 'tie_s']):
tie_index[j] = -1 if par['DAPEML'][tie][i][0] == 'None' \
else int(par['DAPEML'][tie][i][0])
tie_par[j] = None if par['DAPEML'][tie][i][1] == 'None' \
else par['DAPEML'][tie][i][1]
parlist += [EmissionLinePar(index=par['DAPEML']['index'][i],
name=par['DAPEML']['name'][i],
restwave=par['DAPEML']['restwave'][i] if invac
else airtovac(par['DAPEML']['restwave'][i]),
action=par['DAPEML']['action'][i], tie_index=tie_index,
tie_par=tie_par,
blueside=par['DAPEML']['blueside'][i] if invac else \
airtovac(numpy.array(par['DAPEML']['blueside'][i])),
redside=par['DAPEML']['redside'][i] if invac else \
airtovac(numpy.array(par['DAPEML']['redside'][i])))]
return parlist
[docs]
def _validate(self):
super()._validate()
# Ensure that no lines are tied to themselves
indx = self.data['index'][:,None] == self.data['tie_index']
if numpy.any(indx):
indx = numpy.any(indx, axis=1)
raise ValueError('Cannot tie lines to themselves! Fix tying index for line '
f'{",".join([str(x) for x in self.data["index"][indx]])}.')
[docs]
def channel_names(self, dicttype=True):
"""
Return a dictionary with the channel names as the dictionary
key and the channel number as the dictionary value. If
``dicttype`` is False, a list is returned with just the
string keys.
"""
channels = ['{0}-{1}'.format(self.data['name'][i], int(self.data['restwave'][i]))
for i in range(self.size)]
return {n:i for i,n in enumerate(channels)} if dicttype else channels
[docs]
def to_datatable(self, quiet=False):
"""
Constuct an
:class:`~mangadap.par.emissionlinedb.EmissionLineDefinitionTable`
instance with the line database.
Args:
quiet (:obj:`bool`, optional):
Suppress terminal output
Returns:
:class:`~mangadap.par.emissionlinedb.EmissionLineDefinitionTable`:
Table with the emission-line metadata.
"""
name_len = numpy.amax([len(n) for n in self.data['name']])
tie_len = numpy.amax([0 if t is None else len(t)
for t in numpy.ravel(self.data['tie_par'])])
# Instatiate the table data that will be saved defining the set
# of emission-line moments measured
db = EmissionLineDefinitionTable(name_len=name_len, tie_len=tie_len, shape=self.size)
hk = ['ID', 'NAME', 'RESTWAVE', 'ACTION', 'TIE_ID']
mk = ['index', 'name', 'restwave', 'action', 'tie_index']
for _hk, _mk in zip(hk,mk):
db[_hk] = self.data[_mk]
db['TIE_FLUX'] = numpy.array([str(d) for d in self.data['tie_par'][:,0]])
db['TIE_VEL'] = numpy.array([str(d) for d in self.data['tie_par'][:,1]])
db['TIE_SIG'] = numpy.array([str(d) for d in self.data['tie_par'][:,2]])
return db
[docs]
def tie_index_match(self):
"""
Return an array with the row indices for the tied parameters, which
is more useful than the ID number of the tied line.
"""
tie_indx = numpy.full(self.data['tie_index'].shape, -1, dtype=int)
indx = self.data['tie_index'] > 0
tie_indx[indx] = [numpy.where(self.data['index'] == i)[0][0]
for i in self.data['tie_index'][indx]]
return tie_indx
@property
def neml(self):
"""
Number of emission lines in the database.
"""
return self.nsets