Source code for tesliper.glassware.spectra

"""Objects representing spectra."""

import logging as lgg
from typing import Dict, Optional, Sequence, Union

import numpy as np

import tesliper  # absolute import to solve problem of circular imports

from .. import datawork as dw
from .array_base import ArrayProperty

# LOGGER
logger = lgg.getLogger(__name__)


[docs]class SingleSpectrum: """Represents a single spectrum: experimental, averaged from set of conformers, or calculated for only one conformer. Notes ----- Calling ``len()`` on this class' instance will show a number of data points in the spectrum. """ _vibrational_units = { "width": "cm-1", "start": "cm-1", "stop": "cm-1", "step": "cm-1", "x": "Frequency / cm^(-1)", } _electronic_units = { "width": "eV", "start": "nm", "stop": "nm", "step": "nm", "x": "Wavelength / nm", } _units = { "ir": {"y": "Epsilon"}, "uv": {"y": "Epsilon"}, "vcd": {"y": "Delta Epsilon"}, "ecd": {"y": "Delta Epsilon"}, "raman": {"y": "I(R)+I(L)"}, "roa": {"y": "I(R)-I(L)"}, } for u in "ir vcd raman roa".split(" "): _units[u].update(_vibrational_units) for u in ("uv", "ecd"): _units[u].update(_electronic_units) _spectra_type_ref = dict( vcd="vibrational", ir="vibrational", roa="scattering", raman="scattering", ecd="electronic", uv="electronic", ) def __init__( self, genre: str, values: Sequence[float], abscissa: Sequence[float], width: float = 0.0, fitting: str = "n/a", scaling: float = 1.0, offset: float = 0.0, filenames: Optional[Sequence[str]] = None, averaged_by: Optional[str] = None, ): """ Parameters ---------- genre : str Name of data genre that this object represents. values : Sequence[float] List of intensity values for each point on the x-axis. abscissa : Sequence[float] List of x-axis values. width : float, optional Full width at half maximum used to calculate spectrum, if applies. Provided for the record only, by default 0.0. fitting : str, optional Name of the fitting function used to calculate spectrum, if applies. Provided for the record only, by default "n/a". scaling : float, optional Multiplyier for correction of signal intensity, by default 1.0. offset : float, optional Correction of the spectrum's shift. Positive value indicates a bathochromic shift, negative value indicates a hypsochromic shift. By default 0.0. filenames : Optional[Sequence[str]], optional List of identifiers of conformers that were used to calculate average spectrum, if applies. averaged_by : Optional[str], optional Energies genre used to calculate average spectrum, if applies. """ self.genre = genre self.filenames = [] if filenames is None else filenames self.averaged_by = averaged_by self.abscissa = abscissa self.values = values self.start = abscissa[0] self.stop = abscissa[-1] self.step = abs(abscissa[0] - abscissa[1]) self.width = width self.fitting = fitting self.scaling = scaling self.offset = offset filenames = ArrayProperty(check_against=None, dtype=str) abscissa = ArrayProperty(check_against=None) values = ArrayProperty(check_against="abscissa") @property def spectra_type(self): """Returns type of spectra: 'vibrational', 'electronic', or 'scattering'.""" return self._spectra_type_ref[self.genre] @property def units(self) -> Dict[str, str]: """Units in which spectral data is stored. It provides a unit for :attr:`.width`, :attr:`.start`, :attr:`.stop`, :attr:`.step`, :attr:`.x`, and :attr:`.y`. :attr:`.abscissa` and :attr:`~.SingleSpectrum.values` are stored in the same units as :attr:`.x` and :attr:`.y` respectively. """ return self._units[self.genre] @property def scaling(self) -> Union[int, float]: """A factor for correcting the scale of spectra. Setting it to new value changes the :attr:`.y` attribute as well. It should be an ``int`` or ``float``. """ return vars(self)["scaling"] @scaling.setter def scaling(self, factor: Union[int, float]): vars(self)["scaling"] = factor vars(self)["y"] = self.values * factor @property def offset(self) -> Union[int, float]: """A factor for correcting the shift of spectra. Positive value indicates a bathochromic shift, negative value indicates a hypsochromic shift. Setting it to new value changes the :attr:`.x` attribute as well. It should be an ``int`` or ``float``. """ return vars(self)["offset"] @offset.setter def offset(self, offset: Union[int, float]): vars(self)["offset"] = offset vars(self)["x"] = self.abscissa + offset @property def x(self) -> np.ndarray: """Spectra's x-values corrected by adding its :attr:`.offset` to :attr:`.abscissa`.""" return vars(self)["x"] @property def y(self) -> np.ndarray: """Spectra's y-values corrected by multiplying its :attr:`~.SingleSpectrum.values` by :attr:`.scaling`.""" return vars(self)["y"]
[docs] def scale_to(self, spectrum: "SingleSpectrum") -> None: """Establishes a scaling factor to best match a scale of the *spectrum* values. Parameters ---------- spectrum : SingleSpectrum This spectrum's y-axis values will be treated as a reference. If *spectrum* has its own scaling factor, it will be taken into account. """ self.scaling = dw.find_scaling(spectrum.y, self.values)
[docs] def shift_to(self, spectrum: "SingleSpectrum") -> None: """Establishes an offset factor to best match given *spectrum*. Parameters ---------- spectrum : SingleSpectrum This spectrum will be treated as a reference. If *spectrum* has its own offset factor, it will be taken into account. """ self.offset = dw.find_offset(spectrum.x, spectrum.y, self.abscissa, self.values)
def __len__(self): return len(self.abscissa)
[docs]class Spectra(SingleSpectrum): """Represents a collection of spectra calculated for a number of conformers. .. versionchanged:: 0.9.1 Corrected ``len()`` behavior. Notes ----- Calling ``len()`` on this class' instance will show how many conformers' spectra it contains. """ filenames = ArrayProperty(check_against=None, dtype=str) abscissa = ArrayProperty(check_against=None) values = ArrayProperty(check_against="filenames") def __init__( self, genre: str, filenames: Sequence[str], values: Sequence[Sequence[float]], abscissa: Sequence[float], width: float = 0.0, fitting: str = "n/a", scaling: float = 1.0, offset: float = 0.0, allow_data_inconsistency: bool = False, ): """ Parameters ---------- genre : str Name of data genre that this object represents. filenames : Optional[Sequence[str]], optional List of conformers' identifiers that were used to calculate spectra. values : Sequence[float] List of intensity values for each point on the x-axis. abscissa : Sequence[float] List of x-axis values. width : float, optional Full width at half maximum used to calculate spectra. Provided for the record only, by default 0.0. fitting : str, optional Name of the fitting function used to calculate spectra. Provided for the record only, by default "n/a". scaling : float, optional Multiplyier for correction of signal intensity, by default 1.0. offset : float, optional Correction of the spectra's shift. Positive value indicates a bathochromic shift, negative value indicates a hypsochromic shift. By default 0.0. allow_data_inconsistency : bool, optional Flag signalizing if instance should allow data inconsistency (see :class:`.ArrayPropety` for details). """ self.allow_data_inconsistency = allow_data_inconsistency SingleSpectrum.__init__( self, genre, values, abscissa, width, fitting, scaling, offset, filenames )
[docs] def average(self, energies: "tesliper.glassware.Energies") -> SingleSpectrum: """A method for averaging spectra by population of conformers. If this object is empty, averaged spectrum will be a flat line at 0.0 intensity. Parameters ---------- energies : Energies Object with ``populations`` and ``genre`` attributes containing respectively: list of populations values as ``numpy.ndarray`` and string specifying energy genre. Returns ------- SingleSpectrum Averaged spectrum. """ populations = energies.populations energy_type = energies.genre av_spec = dw.calculate_average(self.values, populations) if not av_spec.size: av_spec = np.zeros(self.abscissa.shape) av_spec = SingleSpectrum( self.genre, av_spec, self.abscissa, self.width, self.fitting, scaling=self.scaling, offset=self.offset, filenames=self.filenames, averaged_by=energy_type, ) logger.debug(f"{self.genre} spectrum averaged by {energy_type}.") return av_spec
[docs] def scale_to( self, spectrum: SingleSpectrum, average_by: Optional["tesliper.glassware.Energies"] = None, ) -> None: """Establishes a scaling factor to best match a scale of the *spectrum* values. An average spectrum is calculated prior to calculating the factor. If *average_by* is given, it is used to average by population of each conformer. Otherwise an arithmetic average of spectra is calculated, which may lead to inaccurate results. Parameters ---------- spectrum : SingleSpectrum This spectrum's y-axis values will be treated as a reference. If *spectrum* has its own scaling factor, it will be taken into account. average_by : Energies, optional Energies object, used to calculate average spectrum prior to calculating the factor. If not given, a simple arithmetic average of the spectra will be calculated. """ if average_by is not None: averaged = self.average(energies=average_by) averaged.scale_to(spectrum) factor = averaged.scaling else: logger.warning( "Trying to find optimal scaling factor for spectra, but no Energies " "object given for averaging by population. Results may be inaccurate." ) averaged = np.average(self.values, axis=0) factor = dw.find_scaling(spectrum.y, averaged) self.scaling = factor
[docs] def shift_to( self, spectrum: SingleSpectrum, average_by: Optional["tesliper.glassware.Energies"] = None, ) -> None: """Establishes an offset factor to best match given *spectrum*. An average spectrum is calculated prior to calculating the factor. If *average_by* is given, it is used to average by population of each conformer. Otherwise an arithmetic average of spectra is calculated, which may lead to inaccurate results. Parameters ---------- spectrum : SingleSpectrum This spectrum will be treated as a reference. If *spectrum* has its own offset factor, it will be taken into account. average_by : Energies, optional Energies object, used to calculate average spectrum prior to calculating the factor. If not given, a simple arithmetic average of the spectra will be calculated. """ if average_by is not None: averaged = self.average(energies=average_by) averaged.shift_to(spectrum) factor = averaged.offset else: logger.warning( "Trying to find optimal offset factor for spectra, but no Energies " "object given for averaging by population. Results may be inaccurate." ) averaged = np.average(self.values, axis=0) factor = dw.find_offset(spectrum.x, spectrum.y, self.abscissa, averaged) self.offset = factor
def __len__(self): # must override SingleSpecrum's implementation, because it may have an abscisa # but contain no data for conformers # e.g. when created in calculations of spectra from an empty activities array return len(self.filenames)