"""
Copyright Schrodinger, LLC. All rights reserved.
"""
from collections import OrderedDict
from past.utils import old_div
import numpy
from scipy import sparse
from scipy import constants
from scipy.signal import savgol_filter
from scipy.sparse.linalg import spsolve
from scipy.spatial import ConvexHull
from schrodinger.application.matsci import xrd
from schrodinger.application.matsci.nano.xtal import get_pymatgen_structure
from schrodinger.infra import mm
from schrodinger.infra import table
from schrodinger.utils import fileutils
DEFAULT_BANDWIDTH = 8
DEFAULT_BANDWIDTH_MIN = 0
DEFAULT_UV_BANDWIDTH = 3000
DEFAULT_UV_BANDWIDTH_MIN = 0
DEFAULT_UVNM_BANDWIDTH = 40
DEFAULT_UVNM_BANDWIDTH_MIN = 0
DEFAULT_VCD_BANDWIDTH = 4
DEFAULT_VCD_THEO_INTENSITY = 0.02
DEFAULT_VCD_EXP_INTENSITY = 1.0e-6
DEFAULT_IRRAMAN_THEO_INTENSITY = 0.05
DEFAULT_IRRAMAN_EXP_INTENSITY = 0.05
JAG_MAEFILE_PROP = 's_j_Jaguar_mae_file'
IR_OR_RAMAN = 'IR/Raman'
VCD = 'VCD'
ECD = 'ECD'
UVVIS = 'UV/Vis'
VIB_SPECTRUM_MIN = 0
VIB_SPECTRUM_MAX = 4000
UV_SPECTRUM_MIN = 12500  # 800 nm
UV_SPECTRUM_MAX = 100000  # 100 nm
# data is min, max, step
X_RANGE_MAP = {
    IR_OR_RAMAN: (VIB_SPECTRUM_MIN, VIB_SPECTRUM_MAX, 1),
    VCD: (VIB_SPECTRUM_MIN, VIB_SPECTRUM_MAX, 1),
    ECD: (UV_SPECTRUM_MIN, UV_SPECTRUM_MAX, 10),
    UVVIS: (UV_SPECTRUM_MIN, UV_SPECTRUM_MAX, 10)
}
GAUSSIAN = 'Gaussian'
LORENTZIAN = 'Lorentzian'
HARTREE_TO_EV = constants.value('hartree-electron volt relationship')
C_ATOMIC_UNITS = constants.value('inverse fine-structure constant')
BOHR_TO_NM = constants.giga * constants.value('Bohr radius')
# E(ev) = 1239.8419739885849 / lambda(nm)
NM_TO_EV = HARTREE_TO_EV * 2 * constants.pi * C_ATOMIC_UNITS * BOHR_TO_NM
# E(ev) = 0.0001239841973988585 * v_bar(cm-1)
WAVENUMBER_TO_EV = NM_TO_EV * constants.hecto / constants.giga
PDP_DEFAULT_JOB_NAME = 'powder_diffraction_pattern'
PDP_FILE_EXT = '.pdp.spm'
POLYORDER = 'polyorder'
WINDOW_WIDTH = 'win_width'
LAMBDA = 'lambda_val'
WTFACTOR = 'wt_factor'
DEFAULT_LAMBDA = 5.0
DEFAULT_WT_FACTOR = 1e-5
[docs]def rubberband_method(x_data, y_data, parameters):
    """
    Apply a rubberband to the data
    :type x_data: list
    :param x_data: list of 2 theta values
    :type y_data: list
    :param y_data: list of intensity values
    :type parameters: SimpleNamespace
    :param parameters: all the tunable parameters for the method
    :rtype: list
    :return: list of intensity values
    """
    # Find rough base line curve
    vert = ConvexHull(numpy.array(list(zip(x_data, y_data)))).vertices
    # Rotate the base line to make it flat
    vert = numpy.roll(vert, -vert.argmin())
    vert = vert[:vert.argmax()]
    # Interpret the y values for the rotate curve for the original x data
    return numpy.interp(x_data, x_data[vert], y_data[vert]) 
[docs]def least_square_method(x_data, y_data, parameters):
    """
    Smooth data using least sqaure method
    :type x_data: list
    :param x_data: list of 2 theta values
    :type y_data: list
    :param y_data: list of intensity values
    :type parameters: SimpleNamespace
    :param parameters: all the tunable parameters for the method
    :rtype: list
    :return: list of intensity values
    """
    lamda_val = (10**getattr(parameters, LAMBDA))
    wt_factor = getattr(parameters, WTFACTOR)
    data_len = len(y_data)
    diag_mat = sparse.diags([1, -2, 1], [0, -1, -2],
                            shape=(data_len, data_len - 2))
    wts = numpy.ones(data_len)
    # Iterate multiple times to smooth data
    for index in range(10):
        spdiags_mat = sparse.spdiags(wts, 0, data_len, data_len)
        back_mat = spdiags_mat + lamda_val * diag_mat.dot(diag_mat.transpose())
        back_sub = spsolve(back_mat, wts * y_data)
        wts = wt_factor * (y_data > back_sub) + (1 - wt_factor) * (y_data <
                                                                   back_sub)
    return back_sub 
[docs]def savgol_method(x_data, y_data, parameters):
    """
    Apply a Savitzky-Golay filter to the data
    :type x_data: list
    :param x_data: list of 2 theta values
    :type y_data: list
    :param y_data: list of intensity values
    :type parameters: SimpleNamespace
    :param parameters: all the tunable parameters for the method
    :rtype: list
    :return: list of intensity values
    """
    window_size = getattr(parameters, WINDOW_WIDTH)
    polyorder = getattr(parameters, POLYORDER)
    return savgol_filter(y_data, window_size, polyorder, mode='mirror') 
[docs]def convert_nm_and_wavenumber(value):
    """
    Convert value in wavenumbers to nanometers or vice versa, the conversion
    is the same.
    :type value: int or float
    :param value: Number to convert
    :rtype: float
    :return: value converted from wavenumbers to nanometers or vice versa
    """
    return old_div(10000000.0, float(value)) 
[docs]def get_file_data(filename):
    """
    Read the data from the file
    :type name: str
    :param name: The name for the spectrum.  If none, it will be derived
        from the file name.
    :rtype: tuple of (str, str, schrodinger.infra.table.Table)
    :return: First two members of the tuple are the s_j_x_lable and s_j_y_label
        properties.  The third member of the tuple is a Table object from the
        schrodinger.infra.table module.  This object holds the actual data.
    """
    # Grab the X and Y axis labels
    mm.m2io_initialize(mm.error_handler)
    file_handle = mm.m2io_open_file(filename, mm.M2IO_READ_FORWARD)
    mm.m2io_goto_next_block(file_handle, 'f_m_table')
    xaxis_prop = mm.m2io_get_string(file_handle, ['s_j_x_label'])[0]
    intensity_prop = mm.m2io_get_string(file_handle, ['s_j_y_label'])[0]
    mm.m2io_close_file(file_handle)
    mytable = table.Table(filename)
    return xaxis_prop, intensity_prop, mytable 
[docs]def gen_xvals(smin, smax, stride, stride_nm):
    """
    Generates a set of x-values (in wavenumbers) for use in generating a curve from stick spectra.
    Can specify constant intervals in either wavenumber or nm space via stride_nm arg.
    :type smin: int
    :param smin: The minimum x-value in wavenumbers.
    :type smax: int
    :param smax: The maximum x-value in wavenumbers.
    :type stride: float
    :param stride: Compute the intensity every stride values of x. Can be in wavenumbers or nm. See stride_nm arg.
    :type stride_nm: bool
    :param stride_nm: if True, "stride" is given in nm. Default is wavenumbers. Results in a series of x-values
                      that have non-constant intervals in wavenumber space, but constant in nm space.
    :rtype xvals: Numpy ndarray, type float
    :return xvals: Specifies wavenumbers at which to calculate y-values
    """
    if stride_nm:
        # Note that smin/smax are reversed order in nm space since in nm, lower is higher energy (opposite of wavenumber space).
        smin_nm = convert_nm_and_wavenumber(smin)  #convert to nm
        smin_closest_int_nm = round(smin_nm)
        # Create xvals in nm space
        smax_nm = convert_nm_and_wavenumber(smax)
        smax_closest_int_nm = round(smax_nm)
        xvals_nm = numpy.arange(smax_closest_int_nm, smin_closest_int_nm,
                                stride)  # Note smax first due to being in nm
        # Convert xvals to wavenumber space and reverse order so is from low to high energy
        xvals_tmp = [convert_nm_and_wavenumber(x) for x in xvals_nm]
        xvals_tmp.reverse()
        xvals = numpy.asarray(xvals_tmp)
    else:
        xvals = numpy.arange(smin, smax, stride)
    return xvals 
[docs]def find_closest_xval(pos, xvals, stride, stride_nm):
    """
    Finds index of xvals array corresponding to the x-value closest
    to peak position.
    :type pos: float
    :param pos: Position of peak in wavenumbers
    :type xvals: Numpy ndarray, type float
    :param xvals: Array of x-value in units of wavenumbers
    :type stride: float
    :param stride: Distance between each x-value in xvals. Given in either wavenumber or nm, see stride_nm arg.
    :type stride_nm: bool
    :param stride_nm: if True, "stride" is given in nm. Default is wavenumbers. Results in a series of x-values
                      that have non-constant intervals in wavenumber space, but constant in nm space.
    :rtype spot: int
    :return spot: index of xvals array corresponding to the x-value closest to peak position
    """
    if stride_nm:
        # Find closest xval - brute force since with stride_nm, the
        # intervals in xvals array (which is in wavenumber space) are non-constant
        if pos < xvals[0] or pos > xvals[-1]:
            spot = -1
        else:
            spot = 0
            min_diff = abs(pos - xvals[0])
            for cnt, x in enumerate(xvals):
                diff = abs(pos - x)
                if diff <= min_diff:
                    min_diff = diff
                    spot = cnt
                else:
                    # To avoid going through the entire xvals
                    break
    else:
        # The formula below finds the index of the closest x value
        # assuming constant intervals in the xvals array
        spot = int(round((pos - xvals[0]) / stride))
    return spot 
[docs]def generate_curve(mytable,
                   line_width,
                   xprop,
                   intensity,
                   x_scale=1.0,
                   uvvis=False,
                   smin=None,
                   smax=None,
                   stride=None,
                   stride_nm=False,
                   function=LORENTZIAN,
                   line_width_nm=False,
                   offsets=None):
    """
    Generate a full curve for a series of frequency/intensity lines.  The
    curve is generated by broadening the lines with Gaussian or Lorentzian
    curves centered on each line, and summing the curves together.
    Broadening is done in wavenumber space and so returns xvalues in
    wavenumber space. Assumes input data's x-values are in wavenumbers
    UNLESS uvvis=True. If uvvis=True, assumes input x-values are in eV and
    converts them to wavenumbers for curve generation.
    :type mytable: table.Table object
    :param mytable: table of raw data
    :type line_width: float
    :param line_width: the half-bandwidth to use when generating spectrum
    :type x_scale: float
    :param x_scale: the x-axis scale factor
    :type intensity: str
    :param intensity: The label of the intensity property in mytable
    :type xprop: str
    :param xprop: The label of the x-axis property in mytable
    :type uvvis: bool
    :param uvvis: True if this is a uv/vis spectrum, False if not (default
        is False, vibrational spectrum)
    :type smin: int
    :param smin: The minimum x-value in wavenumbers.
    :type smax: int
    :param smax: The maximum x-value in wavenumbers.
    :type stride: float
    :param stride: Compute the intensity every stride values of x.
    :type stride_nm: bool
    :param stride_nm: if True, "stride" is given in nm. Default is wavenumbers. Results in a series of x-values
                      that have non-constant intervals in wavenumber space, but constant in nm space.
    :type function: str
    :param function: The function used to broaden singular intensity values
        to a full spectrum curve.  Default is Lorentzian, other option is
        Gaussian.  Use the LORENTZIAN or GAUSSIAN module constants.
    :type line_width_nm: bool
    :param line_width_nm: Linewidth is given in nm. Default is
        wavenumbers. Not used for non-UV/Vis spectra.
    :type offsets: list
    :param offsets: A list of floats whose length is equal to the number of
        rows in the table. These floats will be used to shift the values of
        x-positions of the curve. Defaults to `None`, where no offsets are
        used.
    :rtype: tuple of 1-D numpy arrays
    :return: (xvalues, yvalues) with xvalues running from smin to smax with Xn =
        X(n-1) + stride.  Note that the X unit will be wavenumbers for all spectra.
    """
    # Assign default argument values
    if uvvis:
        if not stride:
            stride = 10
        if not smin:
            smin = UV_SPECTRUM_MIN
        if not smax:
            smax = UV_SPECTRUM_MAX
    else:
        if not stride:
            stride = 5
        if not smin:
            smin = VIB_SPECTRUM_MIN
        if not smax:
            smax = VIB_SPECTRUM_MAX
    xvals = gen_xvals(smin, smax, stride, stride_nm)
    yvals = numpy.zeros(len(xvals))
    if offsets is None:
        offsets = [0.0] * len(mytable)
    half_original_linewidth = line_width * 0.5
    # Simulate the spectrum:
    is_gaussian = function == GAUSSIAN
    for irow in range(1, len(mytable) + 1):
        value = mytable[irow][xprop]
        pos = (value + offsets[irow - 1])
        if uvvis:
            pos = old_div(pos, WAVENUMBER_TO_EV)
            if line_width_nm:
                # We compute the spectrum in wavenumber space, but a
                # bandwidth in NM is not linear (or constant) in this space
                # Figure out what the bandwidth should be by converting the
                # peak to nanometers, compute the long and short sides of
                # the bandwidth, then convert those back to wavenumbers and
                # take half the difference.
                nm_pos = convert_nm_and_wavenumber(pos)
                nm_short = nm_pos - half_original_linewidth
                nm_long = nm_pos + half_original_linewidth
                wave_short = convert_nm_and_wavenumber(nm_short)
                wave_long = convert_nm_and_wavenumber(nm_long)
                line_width = old_div((wave_short - wave_long), 2.0)
                pos /= x_scale
            else:
                pos *= x_scale
        try:
            height = mytable[irow][intensity]
        except KeyError:
            return None, None
        if line_width == 0:
            # No line-broadening, find index of closest x value
            spot = find_closest_xval(pos, xvals, stride, stride_nm)
            if spot >= 0:
                try:
                    yvals[spot] = height
                except IndexError:
                    pass
        elif is_gaussian:
            yvals += height * numpy.exp(
                old_div(-(xvals - pos)**2, (2 * line_width**2)))
        else:
            # Lorentzian:
            yvals += old_div(height, (1 + (old_div(
                (xvals - pos), line_width))**2))
    return xvals, yvals 
[docs]class PowderDiffractionException(Exception):
    pass 
[docs]def get_powder_diffraction_pattern(st,
                                   wave_length=None,
                                   debye_waller_factors=None,
                                   two_theta_range=(0, 90),
                                   compute_intensities=True):
    """
    Get a pymatgen powder diffraction pattern.
    :type st: `schrodinger.structure.Structure`
    :param st: the structure
    :type wave_length: float
    :param wave_length: the wave length in Ang.
    :type debye_waller_factors: dict
    :param debye_waller_factors: the temperature dependent Debye-Waller factors,
        keys are elemental symbols, values are factors
        in Ang.^2
    :type two_theta_range: tuple or None
    :param two_theta_range: (min, max) pair tuple specifying the x-axis two theta
        range in degrees over which to calculate the powder diffraction pattern
        or None if it is to be calculated at all diffracted beams within the
        limiting sphere of 2 * radius / wave_length
    :type compute_intensities: bool
    :param compute_intensities: If True, compute peaks intensities
        (requires atoms in the structure), otherwise only peak locations
    :raise: PowderDiffractionException if there is an issue
    :rtype: pymatgen.analysis.diffraction.core.DiffractionPattern
    :return: the pymatgen powder diffraction pattern
    """
    if compute_intensities:
        assert st.atom_total
    pymatgen_st = get_pymatgen_structure(st)
    if wave_length is None:
        wave_length = xrd.WAVELENGTHS['CuKa']
    obj = xrd.XRDCalculator(wavelength=wave_length,
                            symprec=0,
                            debye_waller_factors=debye_waller_factors)
    # see MATSCI-7468 where the input parameters result in zero peaks
    try:
        pattern = obj.get_pattern(pymatgen_st,
                                  scaled=False,
                                  two_theta_range=two_theta_range,
                                  compute_intensities=compute_intensities)
    except ValueError as err:
        raise PowderDiffractionException(str(err))
    return pattern 
[docs]class SpectrumFile(object):
    """
    Manage a spectrum file.
    """
    # the following need to be defined in subclasses
    HEADERS = OrderedDict()
    COLUMNS = OrderedDict()
[docs]    def __init__(self, data_file_name, spm_file_name, spectrum_data=None):
        """
        Create an instance.
        :type data_file_name: str
        :param data_file_name: the text file containing the data
        :type spm_file_name: str
        :param spm_file_name: the name of the `*spm` file to create
        :type spectrum_data: list of lists
        :param spectrum_data: Spectrum data. If provided, used to fill table/write file. Otherwise, data obtained from data_file_name
        """
        self.data_file_name = data_file_name
        self.spm_file_name = spm_file_name
        self.table_obj = None
        self.spectrum_data = spectrum_data 
[docs]    @staticmethod
    def getData(data_file_name, separator=None, types=None):
        """
        Return the data from the given data file.
        :type data_file_name: str
        :param data_file_name: the text file containing the data
        :type separator: str
        :param separator: the data separator
        :type types: list
        :param types: a list of types used to type cast the data
        :rtype: list
        :return: contains data tuples
        """
        with open(data_file_name, 'r') as afile:
            lines = afile.readlines()
        data = []
        for aline in lines:
            values = []
            for idx, value in enumerate(aline.split(separator)):
                if types:
                    value = types[idx](value)
                else:
                    value = float(value)
                values.append(value)
            data.append(tuple(values))
        return data 
    def _setUp(self):
        """
        Set up.
        """
        mm.mmtable_initialize(mm.error_handler)
        mm.mmerr_level(mm.error_handler, mm.MMERR_OFF)
        handle = mm.mmtable_new()
        self.table_obj = table.Table(table_handle=handle)
    def _buildTable(self):
        """
        Build the table. If spectrum_data provided in init(), fill table with that data. Otherwise, read data from self.data_file_name.
        """
        # emulate the *spm files obtained by running Jaguar
        for key, value in self.HEADERS.items():
            mm.mmtable_set_string(self.table_obj, key, value)
        for acol, aname in self.COLUMNS.items():
            self.table_obj.insertColumn(mm.MMTABLE_END, acol)
            idx = self.table_obj.getColumnIndex(acol)
            self.table_obj.columnSetName(idx, aname)
        #grab data for filling table
        if self.spectrum_data:
            data = self.spectrum_data
        else:
            data = self.getData(self.data_file_name)
        # table columns and rows are indexed from 1
        for idx, values in enumerate(data, 1):
            self.table_obj.insertRow(mm.MMTABLE_END, 0)
            for jdx, value in enumerate(values, 1):
                self.table_obj[idx][jdx] = value
    def _createFile(self):
        """
        Create the `*spm` file.
        """
        mm.mmtable_m2io_write(self.spm_file_name, self.table_obj, True)
    def _tearDown(self):
        """
        Tear down.
        """
        mm.mmerr_level(mm.error_handler, mm.MMERR_WARNING)
        mm.mmtable_terminate()
[docs]    def write(self):
        """
        Write the `*spm` file.
        """
        self._setUp()
        self._buildTable()
        self._createFile()
        self._tearDown()  
[docs]class PowderDiffractionFile(SpectrumFile):
    """
    Manage a powder diffraction pattern file.
    """
    TWO_THETA_KEY = 'r_matsci_Two_Theta_(degrees)'
    TWO_THETA_TITLE = '2*Theta/deg.'
    INTENSITY_KEY = 'r_matsci_Intensity'
    INTENSITY_TITLE = 'Intensity'
    HKLS_KEY = 's_matsci_HKLs'
    HKLS_TITLE = 'HKLs'
    MULTIPLICITIES_KEY = 's_matsci_HKL_Multiplicities'
    MULTIPLICITIES_TITLE = 'Mults'
    INTERPLANAR_SPACING_KEY = 'r_matsci_Interplanar_Spacing_(Ang.)'
    INTERPLANAR_SPACING_TITLE = 'd_HKL/Ang.'
    # use _j_ properties so that spectrum plot automatically
    # recognizes the file
    SPECTRUM_KEY = 's_j_spectrum_type'
    SPECTRUM_TITLE = 'Powder Diffraction Pattern'
    X_KEY = 's_j_x_label'
    X_ALIAS = TWO_THETA_KEY
    Y_KEY = 's_j_y_label'
    Y_ALIAS = INTENSITY_KEY
    HEADERS = OrderedDict([
        (SPECTRUM_KEY, SPECTRUM_TITLE),
        (X_KEY, X_ALIAS),
        (Y_KEY, Y_ALIAS)
    ]) # yapf: disable
    COLUMNS = OrderedDict([
        (TWO_THETA_KEY, TWO_THETA_TITLE),
        (INTENSITY_KEY, INTENSITY_TITLE),
        (HKLS_KEY, HKLS_TITLE),
        (MULTIPLICITIES_KEY, MULTIPLICITIES_TITLE),
        (INTERPLANAR_SPACING_KEY, INTERPLANAR_SPACING_TITLE)
    ]) # yapf: disable
    SEPARATOR = '; '
    # column types
    TYPES = [float, float, str, str, float]
[docs]    @staticmethod
    def getData(data_file_name, separator=None, types=None):
        """
        Return the data from the given data file.
        :type data_file_name: str
        :param data_file_name: the text file containing the data
        :type separator: str
        :param separator: the data separator
        :type types: list
        :param types: a list of types used to type cast the data
        :rtype: list
        :return: contains data tuples
        """
        separator = PowderDiffractionFile.SEPARATOR
        types = PowderDiffractionFile.TYPES
        return SpectrumFile.getData(data_file_name,
                                    separator=separator,
                                    types=types)  
[docs]def write_powder_diffraction_pattern(st,
                                     wave_length=None,
                                     debye_waller_factors=None,
                                     two_theta_range=(0, 90),
                                     file_name=None):
    """
    Write a powder diffraction pattern file.
    :type st: `schrodinger.structure.Structure`
    :param st: the structure
    :type wave_length: float
    :param wave_length: the wave length in Ang.
    :type debye_waller_factors: dict
    :param debye_waller_factors: the temperature dependent Debye-Waller factors,
        keys are elemental symbols, values are factors
        in Ang.^2
    :type two_theta_range: tuple or None
    :param two_theta_range: (min, max) pair tuple specifying the x-axis two theta
        range in degrees over which to calculate the powder diffraction pattern
        or None if it is to be calculated at all diffracted beams within the
        limiting sphere of 2 * radius / wave_length
    :type file_name: str
    :param file_name: the file name to which the pattern will be written
    :rtype: str
    :return: the file name to which the pattern was written
    """
    pdp_obj = get_powder_diffraction_pattern(
        st,
        wave_length=wave_length,
        debye_waller_factors=debye_waller_factors,
        two_theta_range=two_theta_range)
    data = zip(pdp_obj.x, pdp_obj.y, pdp_obj.hkls, pdp_obj.d_hkls)
    if not file_name:
        file_name = PDP_DEFAULT_JOB_NAME + PDP_FILE_EXT
    line = ['{two_theta}', '{intensity}', '{hkls}', '{mults}', '{d_hkl}\n']
    line = PowderDiffractionFile.SEPARATOR.join(line)
    with fileutils.tempfilename(prefix='data', suffix='.txt') as tmp_path:
        with open(tmp_path, 'w') as afile:
            for two_theta, intensity, hkl_dicts, d_hkl in data:
                hkls, mults = [], []
                for hkl_dict in hkl_dicts:
                    # the following 'hkl' and 'multiplicity' keys are
                    # from pymatgen
                    hkls.append(str(hkl_dict['hkl']))
                    mults.append(str(hkl_dict['multiplicity']))
                hkls, mults = ', '.join(hkls), ', '.join(mults)
                afile.write(
                    line.format(two_theta=two_theta,
                                intensity=intensity,
                                hkls=hkls,
                                mults=mults,
                                d_hkl=d_hkl))
        obj = PowderDiffractionFile(tmp_path, file_name)
        obj.write()
    return file_name 
[docs]class VCD_Spectrum(SpectrumFile):
    """
    Manage a VCD (Vibrational Circular Dichroism) file.
    Note that this class expects its data to be provided upon initialization
    via SpectrumFile's init() fxn's spectrum_data argument,
    as opposed to filled after initialization via reading a file.
    In the future, if we want to initialize via reading a file, we'll have to
    implement a getData() as in PowderDiffractionFile.
    """
    FREQ_KEY = 'r_j_Frequency_(cm-1)'
    FREQ_TITLE = 'Frequency (cm-1)'
    ROT_STR_KEY = 'r_j_Rotational_Strength_(10**-40_esu**2_cm**2)'
    ROT_STR_TITLE = 'Rotational Strength (10**-40 esu**2 cm**2)'
    # use _j_ properties so that spectrum plot automatically
    # recognizes the file
    SPECTRUM_KEY = 's_j_spectrum_type'
    SPECTRUM_TITLE = 'Vibrational Circular Dichroism'
    X_KEY = 's_j_x_label'
    X_ALIAS = FREQ_KEY
    Y_KEY = 's_j_y_label'
    Y_ALIAS = ROT_STR_KEY
    HEADERS = OrderedDict([
        (SPECTRUM_KEY, SPECTRUM_TITLE),
        (X_KEY, X_ALIAS),
        (Y_KEY, Y_ALIAS)
    ]) # yapf: disable
    COLUMNS = OrderedDict([
        (FREQ_KEY, FREQ_TITLE),
        (ROT_STR_KEY, ROT_STR_TITLE),
        ('s_j_Symmetry', 'Symmetry'),
        ('r_j_edtm_x', 'edtm x'),
        ('r_j_edtm_y', 'edtm y'),
        ('r_j_edtm_z', 'edtm z'),
        ('r_j_mdtm_x', 'mdtm x'),
        ('r_j_mdtm_y', 'mdtm y'),
        ('r_j_mdtm_z', 'mdtm z'),
    ]) # yapf: disable
    # column types
    TYPES = [float, float, str, float, float, float, float, float, float] 
[docs]class ECD_Spectrum(SpectrumFile):
    """
    Manage a ECD (Electronic Circular Dichroism) file.
    Note that this class expects its data to be provided upon initialization
    via SpectrumFile's init() fxn's spectrum_data argument,
    as opposed to filled after initialization via reading a file.
    In the future, if we want to initialize via reading a file, we'll have
    to implement a getData() as in PowderDiffractionFile.
    """
    ENERGY_KEY = 'r_j_Electronic_Circular_Dichroism_Energy_(eV)'
    ENERGY_TITLE = 'ECD Energy (eV)'
    INTENSITY_KEY = 'r_j_Molar_Circular_Dichroism_(L_mol-1_cm-1)'
    INTENSITY_TITLE = 'Molar Circular Dichroism (L mol-1 cm-1)'
    # use _j_ properties so that spectrum plot automatically
    # recognizes the file
    SPECTRUM_KEY = 's_j_spectrum_type'
    SPECTRUM_TITLE = 'Electronic Circular Dichroism'
    X_KEY = 's_j_x_label'
    X_ALIAS = ENERGY_KEY
    Y_KEY = 's_j_y_label'
    Y_ALIAS = INTENSITY_KEY
    HEADERS = OrderedDict([
        (SPECTRUM_KEY, SPECTRUM_TITLE),
        (X_KEY, X_ALIAS),
        (Y_KEY, Y_ALIAS)
    ]) # yapf: disable
    COLUMNS = OrderedDict([
        (ENERGY_KEY, ENERGY_TITLE),
        (INTENSITY_KEY, INTENSITY_TITLE),
        ('s_j_Symmetry', 'Symmetry'),
    ]) # yapf: disable
    # column types
    TYPES = [float, float, str] 
[docs]class IR_Spectrum(SpectrumFile):
    """
    Manage an IR (Infrared/Vibrational) file.
    Note that this class expects its data to be provided upon initialization
    via SpectrumFile's init() fxn's spectrum_data argument,
    as opposed to filled after initialization via reading a file.
    In the future, if we want to initialize via reading a file, we'll have
    to implement a getData() as in PowderDiffractionFile.
    """
    FREQ_KEY = 'r_j_Frequency_(cm-1)'
    FREQ_TITLE = 'Frequency (cm-1)'
    INTENSITY_KEY = 'r_j_Intensity_(km/mol)'
    INTENSITY_TITLE = 'Intensity (km/mol)'
    # use _j_ properties so that spectrum plot automatically
    # recognizes the file
    SPECTRUM_KEY = 's_j_spectrum_type'
    SPECTRUM_TITLE = 'Infrared Vibrational Frequencies'
    X_KEY = 's_j_x_label'
    X_ALIAS = FREQ_KEY
    Y_KEY = 's_j_y_label'
    Y_ALIAS = INTENSITY_KEY
    HEADERS = OrderedDict([
        (SPECTRUM_KEY, SPECTRUM_TITLE),
        (X_KEY, X_ALIAS),
        (Y_KEY, Y_ALIAS)
    ]) # yapf: disable
    COLUMNS = OrderedDict([
        (FREQ_KEY, FREQ_TITLE),
        (INTENSITY_KEY, INTENSITY_TITLE),
        ('s_j_Symmetry', 'Symmetry'),
    ]) # yapf: disable
    # column types
    TYPES = [float, float, str] 
[docs]class RamanSpectrum(IR_Spectrum):
    """
    Manage a Raman vibrational spectrum file.
    Note the inheritance from IR_Spectrum class. Some of the variables
    referenced below are defined there.
    Note that this class expects its data to be provided upon initialization
    via SpectrumFile's init() fxn's spectrum_data argument,
    as opposed to filled after initialization via reading a file.
    In the future, if we want to initialize via reading a file, we'll have
    to implement a getData() as in PowderDiffractionFile.
    """
    INTENSITY_KEY = 'r_j_Intensity_(Angstrom**4)'
    INTENSITY_TITLE = 'Intensity (Angstrom**4)'
    SPECTRUM_TITLE = 'Raman Vibrational Frequencies'
    Y_ALIAS = INTENSITY_KEY
    HEADERS = OrderedDict([
        (IR_Spectrum.SPECTRUM_KEY, SPECTRUM_TITLE),
        (IR_Spectrum.X_KEY, IR_Spectrum.X_ALIAS),
        (IR_Spectrum.Y_KEY, Y_ALIAS)
    ]) # yapf: disable
    COLUMNS = OrderedDict([
        (IR_Spectrum.FREQ_KEY, IR_Spectrum.FREQ_TITLE),
        (INTENSITY_KEY, INTENSITY_TITLE),
        ('s_j_Symmetry', 'Symmetry'),
    ]) # yapf: disable 
    ## column types inherited from IR_Spectrum