# Licensed under a 3-clause BSD style license - see LICENSE.rst
# -*- coding: utf-8 -*-
"""
Container class for databases of spectral features. This implements
the base class used by :class:`mangadap.par.artifactdb.ArtifactDB`,
:class:`mangadap.par.emissionlinedb.EmissionLineDB`,
:class:`mangadap.par.emissionmomentsdb.EmissionMomentsDB`,
:class:`mangadap.par.absorptionindexdb.AbsorptionIndexDB`, and
:class:`mangadap.par.bandheadindexdb.BandheadIndexDB`.
----
.. include license and copyright
.. include:: ../include/copy.rst
----
.. include common links, assuming primary doc root is up one directory
.. include:: ../include/links.rst
"""
from pathlib import Path
from IPython import embed
import numpy
from ..config import defaults
from .parset import ParDatabase
from ..proc import util
# TODO: Enable searching of local directory
[docs]
class SpectralFeatureDB(ParDatabase):
"""
Basic container class for the parameters databases of spectral
features. This is the base class for all of the following:
- class used by :class:`~mangadap.par.artifactdb.ArtifactDB`
- :class:`~mangadap.par.emissionlinedb.EmissionLineDB`
- :class:`~mangadap.par.emissionmomentsdb.EmissionMomentsDB`
- :class:`~mangadap.par.absorptionindexdb.AbsorptionIndexDB`
- :class:`~mangadap.par.bandheadindexdb.BandheadIndexDB`
See :class:`~mangadap.par.parset.ParDatabase` for additional
attributes.
Each derived class must define its own default directory that
contains the relevant databases, the class that defines the base
:class:`~mangadap.par.parset.ParSet` for each
:class:`~mangadap.par.parset.ParDatabase` entry, and the method
that parses the parameter file into the parameter list.
The primary instantiation requires the SDSS parameter file. To
instantiate using a keyword (and optionally a directory that
holds the parameter files), use :func:`from_key`.
Args:
parfile (:obj:`str`, `Path`_):
The SDSS parameter file with the emission-line database.
Attributes:
key (:obj:`str`):
Database signifying keyword
file (:obj:`str`):
File with the emission-line data
size (:obj:`int`):
Number of elements in the database.
"""
default_data_dir = None
def __init__(self, parfile):
# TODO: The approach here (read using yanny, set to par
# individually, then covert back to record array using
# ParDatabase) is stupid...
_parfile = Path(parfile).resolve()
if not _parfile.exists():
raise FileNotFoundError(f'{_parfile} does not exist!')
self.key = util.get_database_key(str(_parfile))
self.file = str(_parfile)
self.size = None
# _parse_yanny() has to set `size` for each subclass.
ParDatabase.__init__(self, self._parse_yanny())
self._validate()
[docs]
def _validate(self):
# Ensure that all indices are unique
if len(numpy.unique(self.data['index'])) != self.size:
raise ValueError(f'Database indices for {self.key} are not all unique!')
[docs]
def _parse_yanny(self):
raise NotImplementedError(f'_parse_yanny not defined for {self.__class__.__name__}.')
[docs]
@classmethod
def default_path(cls):
"""
Return the default path with the emission-line databases.
"""
if cls.default_data_dir is None:
raise ValueError(f'Default data directory undefined for {cls.__class__.__name__}.')
return defaults.dap_data_root() / cls.default_data_dir
[docs]
@classmethod
def available_databases(cls, directory_path=None):
"""
Return the list of available database files.
Args:
directory_path (:obj:`str`, optional):
Root path with the database files. If None, uses the
default directory defined by :func:`default_path`.
Returns:
:obj:`dict`: A dictionary with the database files and
associated keyword.
Raises:
NotADirectoryError:
Raised if the provided or default directory does not
exist.
ValueError:
Raised if the keywords found for all the ``*.par``
files are not unique.
"""
if directory_path is None:
directory_path = cls.default_path()
if not directory_path.exists():
raise NotADirectoryError(f'{directory_path} not found!')
files = list(directory_path.glob('*.par'))
keys = [util.get_database_key(str(f)) for f in files]
if len(keys) != len(numpy.unique(keys)):
raise ValueError(f'Keys read for par files in {directory_path} are not unique! Names '
'of par files must be case-insensitive and unique.')
return {k: f for k,f in zip(keys, files)}
[docs]
@classmethod
def from_key(cls, key, directory_path=None):
r"""
Instantiate the object using a keyword.
Args:
key (:obj:`str`):
Keyword selecting the database to use.
directory_path (:obj:`str`, optional):
Root path with the database parameter files. If None,
uses the default set by :func:`available_databases`.
Note that the file search includes *any* file with a
``.par`` extension. The root of the file should be
case-insensitive.
"""
databases = cls.available_databases(directory_path=directory_path)
available_keys = list(databases.keys())
if key not in available_keys:
raise KeyError(f'No database found with key {key}. Keywords found '
f'are: {available_keys}')
return cls(databases[key])