Source code for mangadap.par.spectralfeaturedb

# 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 pydl.pydlutils.yanny import yanny

from ..config import defaults
from .parset import ParDatabase
from ..util.parser import DefaultConfig
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])