"""Objects representing pure chemical components and pseudo-components.

Attributes
----------
COMP_FAM : list
    Pre-defined component family pick list based on definitions in _[1] and _[2].

Notes
-----

References
----------
[1] Perry's Chemical Engineers' Handbook; Perry, R. H., Southard, M. Z., Eds.; McGraw-Hill Education: New York, 2019.
[2] Tihic, A.; Kontogeorgis, G. M.; von Solms, N.;Michelsen, M. L. Applications of the simplified perturbed-chain SAFT
equation of state using an extended parameter table. Fluid Phase Equilib. 2006, 248, 29-43.
"""

COMP_FAM = ['Alkanes', 'Alkenes', 'Alkynes', 'Cycloalkanes' 'Aromatics', 'Polynuclear Aromatics', 'Aldehydes',
            'Ketones', 'Heterocyclics', 'Elements', 'Alcohols', 'Phenols', 'Ethers', 'Acids', 'Esters', 'Amines',
            'Amides', 'Nitriles', 'Nitro Compounds', 'Isocyanates', 'Mercaptans', 'Sulfides',
            'Halogenated Hydrocarbons', 'Silanes', 'Inorganics', 'Multifunctional']

import numpy as np
from utilities import *

class Comp(object):
    """A pure chemical component."""
    def __init__(self, name=None):
        """
        Parameters
        ----------
        name : str
        """
        # General Attributes
        if name is None:
            raise ValueError("Comp object must be initialized with a name and molecular weight.")
        else:
            # Metadata and constants.
            self.name = name
            self.cas_no = None
            self.formula = None
            self.family = None
            self.mw = None
            self.vdwv = None
            self.vdwa = None
            self.rgyr = None
            self.dipole = None
            self.quadrupole = None
            self.acentric = None
            self.tc = None
            self.pc = None
            self.vc = None
            self.rhoc = None
            self.tt = None
            self.pt = None
            self.bp = None
            self.mp = None
            self.hfus = None
            self.hsub = None
            self.ig_hform = None
            self.ig_gform = None
            self.ig_entr = None
            self.hcomb = None

            # Temperature dependent properties.
            self.pvap_l = None
            self.hvap_l = None
            self.den_s = None
            self.den_l = None
            self.beta_s = None
            self.beta_l = None
            self.cp_s = None
            self.cp_l = None
            self.cp_ig = None
            self.visc_l = None
            self.visc_ig = None
            self.tcond_s = None
            self.tcond_l = None
            self.tcond_ig = None
            self.sigma = None

            # TODO: Ensure CEOS physical terms are built with getter/setter checks for CEOS objects.
            # Cubic EOS physical parameter dictionaries.
            self.srk_phys = {'a': None, 'b': None, 'm': None}
            self.cpa_phys = {'a0': None, 'b': None, 'c1': None}
            self.pr_phys = {'a': None, 'b': None, 'm': None}
            self.gpr_phys = {'a': None, 'b': None, 'a': None, 'b': None, 'c': None}
            self.tpr_phys = {'a': None, 'b': None, 'l': None, 'm': None, 'n': None}

            # SAFT EOS physical parameter dictionaries.
            self.spc_saft_phys = {'m': None, 'sig': None, 'eps': None}
            self.pc_saft_phys = {'m': None, 'sig': None, 'eps': None}

            # SAFT EOS association parameter objects.
            self.assoc_sites = None
            self.cpa_assoc = None
            self.spc_saft_assoc = None
            self.pc_saft_assoc = None

    @property
    def name(self):
        """str : Name of chemical compound.

        Value can only be set whe creating a new Comp instance.
        """
        return self._name

    @name.setter
    def name(self, value):
        try:
            self._name
        except AttributeError:
            if isinstance(value, str):
                self._name = value
            else:
                raise TypeError("name must be a string.")

    @property
    def cas_no(self):
        """str: Chemical Abstracts Service Registry Number."""
        return self._cas_no

    @cas_no.setter
    def cas_no(self, value):
        if isinstance(value, str) or value is None:
            self._cas_no = value
        else:
            raise TypeError("cas_no must be a string.")

    @property
    def formula(self):
        """str : Chemical formula."""
        return self._formula

    @formula.setter
    def formula(self, value):
        if isinstance(value, str) or value is None:
            self._formula = value
        else:
            raise TypeError("formula must be a string.")

    @property
    def family(self):
        """str : Chemical family."""
        return self._family

    @family.setter
    def family(self, value):
        if isinstance(value, str) or value is None:
            if value in COMP_FAM or value is None:
                self._family = value
            else:
                raise ValueError("family must be one of the pre-defined values.")
        else:
            raise TypeError("family must be a string.")

    @property
    def mw(self):
        """float : Molecular weight, g/mol."""
        return self._mw

    @mw.setter
    def mw(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._mw = value
        else:
            raise TypeError("mw must be a positive float.")

    @property
    def vdwv(self):
        """float : Van der Waal's volume, unit TBD."""
        return self._vdwv

    @vdwv.setter
    def vdwv(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._vdwv = value
        else:
            raise TypeError("vdwv must be a positive float.")

    @property
    def vdwa(self):
        """float : Van der Waal's surface area, unit TBD."""
        return self._vdwa

    @vdwa.setter
    def vdwa(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._vdwa = value
        else:
            raise TypeError("vdwa must be a positive float.")

    @property
    def rgyr(self):
        """float : Radius of gyration, unit TBD."""
        return self._rgyr

    @rgyr.setter
    def rgyr(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._rgyr = value
        else:
            raise TypeError("rgyr must be a positive float.")

    @property
    def dipole(self):
        """float : Gas phase dipole moment, unit TBD."""
        return self._dipole

    @dipole.setter
    def dipole(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._dipole = value
        else:
            raise TypeError("dipole must be a positive float.")

    @property
    def quadrupole(self):
        """float : Gas phase quadrupole moment, unit TBD."""
        return self._quadrupole

    @quadrupole.setter
    def quadrupole(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._quadrupole = value
        else:
            raise TypeError("quadrupole must be a positive float.")

    @property
    def acentric(self):
        """float : Acentric factor, dimensionless."""
        return self._acentric

    @acentric.setter
    def acentric(self, value):
        if isinstance(value, float) or value is None:
            self._acentric = value
        else:
            raise TypeError("acentric must be a float.")

    @property
    def tc(self):
        """float : Critical temperature, K."""
        return self._tc

    @tc.setter
    def tc(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._tc = value
        else:
            raise TypeError("tc must be a positive float.")

    @property
    def pc(self):
        """float : Critical pressure, Pa."""
        return self._pc

    @pc.setter
    def pc(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._pc = value
        else:
            raise TypeError("pc must be a positive float.")

    @property
    def vc(self):
        """float : Critical volume, m**3/mol."""
        return self._vc

    @vc.setter
    def vc(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._vc = value
        else:
            raise TypeError("vc must be a positive float.")

    @property
    def rhoc(self):
        """float : Critical density, mol/m**3"""
        return self._rhoc

    @rhoc.setter
    def rhoc(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._rhoc = value
        else:
            raise TypeError("rhoc must be a positive float.")

    @property
    def tt(self):
        """float : Triple point temperature, K."""
        return self._tt

    @tt.setter
    def tt(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._tt = value
        else:
            raise TypeError("tt must be a positive float.")

    @property
    def pt(self):
        """float : Triple point pressure, Pa."""
        return self._pt

    @pt.setter
    def pt(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._pt = value
        else:
            raise TypeError("pt must be a positive float.")

    @property
    def bp(self):
        """float : Boiling point, K."""
        return self._bp

    @bp.setter
    def bp(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._bp = value
        else:
            raise TypeError("bp must be a positive float.")

    @property
    def mp(self):
        """float : Melting point, K."""
        return self._mp

    @mp.setter
    def mp(self, value):
        if (isinstance(value, float) and value >= 0.0) or value is None:
            self._mp = value
        else:
            raise TypeError("mp must be a positive float.")

    @property
    def hfus(self):
        """float : Enthalpy of solid-liquid fusion, J/mol."""
        return self._hfus

    @hfus.setter
    def hfus(self, value):
        if isinstance(value, float) or value is None:
            self._hfus = value
        else:
            raise TypeError("hfus must be a float.")

    @property
    def hsub(self):
        """float : Enthalpy of sublimation, J/mol."""
        return self._hsub

    @hsub.setter
    def hsub(self, value):
        if isinstance(value, float) or value is None:
            self._hsub = value
        else:
            raise TypeError("hsub must be a float.")

    @property
    def ig_hform(self):
        """float : Ideal gas enthalpy of formation at 298.15, unit TBD.

        The compounds are considered to be formed from the elements in their standard states at 298.15K and 1 bar.
        These include  C (graphite) and S (rhombic).
        """
        return self._ig_hform

    @ig_hform.setter
    def ig_hform(self, value):
        if isinstance(value, float) or value is None:
            self._ig_hform = value
        else:
            raise TypeError("ig_hform must be a float.")

    @property
    def ig_gform(self):
        """float : Ideal gas gibbs energy of formation at 298.15, unit TBD."""
        return self._ig_gform

    @ig_gform.setter
    def ig_gform(self, value):
        if isinstance(value, float) or value is None:
            self._ig_gform = value
        else:
            raise TypeError("ig_gform must be a float.")

    @property
    def ig_entr(self):
        """float : Ideal gas gibbs entropy, unit TBD."""
        return self._ig_entr

    @ig_entr.setter
    def ig_entr(self, value):
        if isinstance(value, float) or value is None:
            self._ig_entr = value
        else:
            raise TypeError("ig_entr must be a float.")

    @property
    def hcomb(self):
        """float : Standard net enthalpy of combustion, unit TBD.

        Enthalpy of combustion is the net value for the compound in its standard state at 298.15K and 1 bar.  Products
        of combustion are taken to be CO2 (gas), H2O (gas), F2 (gas), Cl2 (gas), Br2 (gas), I2 (gas), SO2 (gas), N2
        (gas), P4O10 (crystalline), SiO2 (crystobalite), and Al2O3 (crystal, alpha).
        """
        return self._hcomb

    @hcomb.setter
    def hcomb(self, value):
        if isinstance(value, float) or value is None:
            self._hcomb = value
        else:
            raise TypeError("hcomb must be a float.")

    @property
    def pvap_l(self):
        """float : Liquid vapor pressure, Pa."""
        return self._pvap_l

    @pvap_l.setter
    def pvap_l(self, value):
        if isinstance(value, Corel) or value is None:
            self._pvap_l = value
        else:
            raise TypeError("pvap_l must be an instance of Corel.")

    @property
    def hvap_l(self):
        """float : Enthalpy of saturated liquid vaporization, J/mol."""
        return self._hvap_l

    @hvap_l.setter
    def hvap_l(self, value):
        if isinstance(value, Corel) or value is None:
            self._hvap_l = value
        else:
            raise TypeError("hvap_l must be an instance of Corel.")

    @property
    def den_s(self):
        """float : Solid density, kg/m3 or mol/m3."""
        return self._den_s

    @den_s.setter
    def den_s(self, value):
        if isinstance(value, Corel) or value is None:
            self._den_s = value
        else:
            raise TypeError("den_S must be an instance of Corel.")

    @property
    def den_l(self):
        """float : Liquid density, kg/m3 or mol/m3."""
        return self._den_l

    @den_l.setter
    def den_l(self, value):
        if isinstance(value, Corel) or value is None:
            self._den_l = value
        else:
            raise TypeError("den_l must be an instance of Corel.")

    @property
    def beta_s(self):
        """float : Isothermal solid compressibility, Unit TBD.

        beta = -(1/V)*(dV/dP) = (1/rho)*(drho/dP), sign change due to conversion from V to rho with chain rule."""
        return self._beta_s

    @beta_s.setter
    def beta_s(self, value):
        if isinstance(value, Corel) or value is None:
            self._beta_s = value
        else:
            raise TypeError("beta_s must be an instance of Corel.")

    @property
    def beta_l(self):
        """float : Isothermal liquid compressibility, Unit TBD.

        beta = -(1/V)*(dV/dP) = (1/rho)*(drho/dP), sign change due to conversion from V to rho with chain rule."""
        return self._beta_l

    @beta_l.setter
    def beta_l(self, value):
        if isinstance(value, Corel) or value is None:
            self._beta_l = value
        else:
            raise TypeError("beta_l must be an instance of Corel.")

    @property
    def cp_s(self):
        """float : Solid heat capacity, J/mol.K."""
        return self._cp_s

    @cp_s.setter
    def cp_s(self, value):
        if isinstance(value, Corel) or value is None:
            self._cp_s = value
        else:
            raise TypeError("cp_s must be an instance of Corel.")

    @property
    def cp_l(self):
        """float : Saturated liquid heat capacity, J/mol.K."""
        return self._cp_l

    @cp_l.setter
    def cp_l(self, value):
        if isinstance(value, Corel) or value is None:
            self._cp_l = value
        else:
            raise TypeError("cp_l must be an instance of Corel.")

    @property
    def cp_ig(self):
        """float : Ideal gas heat capacity, J/mol.K."""
        return self._cp_ig

    @cp_ig.setter
    def cp_ig(self, value):
        if isinstance(value, Corel) or value is None:
            self._cp_ig = value
        else:
            raise TypeError("cp_ig must be an instance of Corel.")

    @property
    def visc_l(self):
        """float : Saturated liquid viscosity, unit TBD."""
        return self._visc_l

    @visc_l.setter
    def visc_l(self, value):
        if isinstance(value, Corel) or value is None:
            self._visc_l = value
        else:
            raise TypeError("visc_l must be an instance of Corel.")

    @property
    def visc_ig(self):
        """float : Ideal gas viscosity, unit TBD."""
        return self._visc_ig

    @visc_ig.setter
    def visc_ig(self, value):
        if isinstance(value, Corel) or value is None:
            self._visc_ig = value
        else:
            raise TypeError("visc_ig must be an instance of Corel.")

    @property
    def tcond_s(self):
        """float : Solid thermal conductivity, unit TBD."""

    @tcond_s.setter
    def tcond_s(self, value):
        if isinstance(value, Corel) or value is None:
            self._tcond_s = value
        else:
            raise TypeError("tcond_s must be an instance of Corel.")

    @property
    def tcond_l(self):
        """float : Saturated liquid thermal conductivity, unit TBD."""
        return self._tcond_l

    @tcond_l.setter
    def tcond_l(self, value):
        if isinstance(value, Corel) or value is None:
            self._tcond_l = value
        else:
            raise TypeError("tcond_l must be an instance of Corel.")

    @property
    def tcond_ig(self):
        """float : Ideal gas thermal conductivity, unit TBD."""
        return self._tcond_ig

    @tcond_ig.setter
    def tcond_ig(self, value):
        if isinstance(value, Corel) or value is None:
            self._tcond_ig = value
        else:
            raise TypeError("tcond_ig must be an instance of Corel.")

    @property
    def sigma(self):
        """float : Surface tension, unit TBD."""
        return self._sigma

    @sigma.setter
    def sigma(self, value):
        if isinstance(value, Corel) or value is None:
            self._sigma = value
        else:
            raise TypeError("sigma must be an instance of Corel.")

    def k_wilson(self, p=None, t=None):
        """Wilson's equilibrium ratio (Ki = yi / xi).

        Parameters
        ----------
        p : float
            Pressure, Pa.
        t : float
            Temperature, K.

        Returns
        -------
        float
            Wilson's K-factor.
        """
        if isinstance(p, float) and isinstance(t, float) and p > 0.0 and t > 0.0:
            return (self._pc / p) * np.exp(5.37 * (1.0 + self._acentric) * (1.0 - self._tc / t))
        else:
            raise RuntimeError("p and t must both be positive floats.")

    def melting_curve(self, t=None):
        """Melting curve for a pure solid.

        Melting pressure can be estimated with the Clausius-Clapeyron equation. A very simple approach assumes that
        changes in the enthalpy of fusion, liquid molar volume, and solid molar volume are negligible over the
        temperature range of interest.  With these assumptions, the equation of the melting curve is as follows:

        dp/dt = delta_h / (t * delta_v)

        p - p0 = (delta_h / delta_v) * ln(t / t0)

        Variation of the enthalpy of fusion with temperature is related to differences in heat capacity between phases.

        ddelta_h/dt = dh_l/dt - dh_s/dt = cp_l - cp_s = detla_cp

        delta_h = integral_from_t0_to_t(delta_cp * dt) + delta_h0

        Temperature dependence of the heat capacity difference is usually weak and can be neglected.

        delta_h = delta_cp * (t - t0) + delta_h0

        Liquid and solid heat capacities can be evaluated at the melting temperature. These values are be derived from
        cp_l(t) and cp_s(t) correlations OR it can be entered as a constant (a useful adjustable parameter for
        correlating melting curves, sublimation curves, and solubility. The equation of the melting curve is as follows:

        p - p0 = (delta_cp / delta_v) * (t - t0) + ((delta_h - t0 * delta_cp) / delta_v) * ln(t / t0)

        Parameters
        ----------
        t : float
            Temperature specification, K.

        Returns
        -------
        float
            Melting pressure, Pa.

        References
        ----------
        [1] Tosun, I. The thermodynamics of phase and reaction equilibria, 1st ed., Elsevier: Amsterdam, 2013.
        [2] Poling, B. E.; Praunitz, J. M.; O'Connell, J. P. The properties of gases and liquids, 5th ed.,
        McGraw-Hill, 2000.
        [3] Pappa, G. D.; Voutsas, E. C.; Magoulas, K.; Tassios, D. P. Estimation of the differential molar heat
        capacities of organic compounds at their melting point. Ind. Eng. Chem. Res. 2005, 44, 3799–3806.
        """
        if isinstance(t, float) and t > 0.0:
            if self._hfus and self._mp and self._den_l and self._den_s:
                p0 = 101325.0
                t0 = self._mp
                tavg = (t + t0) / 2.0
                v_l = 1.0 / self._den_l(tavg)
                v_s = 1.0 / self._den_s(tavg)
                delta_v = v_l - v_s
                if self._cp_l and self._cp_s:
                    delta_cp = self._cp_l(tavg) - self._cp_s(tavg)
                    return p0 + (delta_cp/delta_v) * (t - t0) + ((self._hfus - t0 * delta_cp)/delta_v) * np.log(t/t0)
                else:
                    return p0 + (self._hfus/delta_v) * np.log(t/t0)
            else:
                raise RuntimeError("Enthalpy of fusion at melting point, liquid density, and solid density required.")
        else:
            raise RuntimeError("t must be a positive float.")

    def sublimation_curve(self):
        """Sublimation curve for a pure solid.

        Sublimation pressure can be estimated with the Clausius-Clapeyron equation. A very simple approach assumes that
        changes in the enthalpy of sublimation and solid molar volume are negligible over the temperature range of
        interest.  With these assumptions, the equation of the melting curve is as follows:

        dp/dt = delta_h / (t * delta_v)

        delta_h_sub = delta_h_vap + delta_h_fusion

        delta_v = v_v - v_s ~ v_v = R*t/p_sub

        p - p0 = (delta_h / delta_v) * ln(t / t0)

        Parameters
        ----------
        t : float
            Temperature specification, K.

        Returns
        -------
        float
            Sublimation pressure, Pa.

        References
        ----------
        [1] Tosun, I. The thermodynamics of phase and reaction equilibria, 1st ed., Elsevier: Amsterdam, 2013.
        [2] Poling, B. E.; Praunitz, J. M.; O'Connell, J. P. The properties of gases and liquids, 5th ed.,
        McGraw-Hill, 2000.
        [3] Pappa, G. D.; Voutsas, E. C.; Magoulas, K.; Tassios, D. P. Estimation of the differential molar heat
        capacities of organic compounds at their melting point. Ind. Eng. Chem. Res. 2005, 44, 3799–3806.
        """
        return

    def density(self, t=None, p=None, phase='l', spec='molar'):
        """Pure component liquid and/or solid density estimation.

        This function evaluates liquid or solid densities at pressures deviating from the vapor pressure curve or
        melting curve. The first step is calculating an initial density along the vapor pressure curve or melting curve.
        The next step is to correct for pressure with the isothermal compressibility:

        rho = rho_0 * exp(beta * (p - p_0))

        In this expression, rho_0 and p_0 are the saturation or melting density.  If pressure is not specified, then the
        saturated liquid or melting point solid density is returned. IF

        Parameters
        ----------
        p : float
            Pressure, Pa.
        t : float
            Temperature, K.
        spec : str
            Phase specification (either 'mass' or 'molar').

        Returns
        -------
        float or None
            Density of saturated liquid or melting point solid. None returned if 't' is outside correlation temp limits.
        float or None
            Density of solid phase at temperature 't' (None if no solid density correlation available).
        str
            Unit ('kg/m3' or 'mol/m3').
        """
        if not isinstance(p, (float, None)):
            raise TypeError("p must be None or a positive float.")
        elif not isinstance(t, float) and p > 0.0:
            raise TypeError("t must be a positive float.")
        elif phase not in ['l', 's']:
            raise ValueError("phase must be either 'l' or 's'.")
        elif spec not in ['mass', 'molar']:
            raise ValueError("spec must be either 'molar' or 'mass'.")
        else:
            if self._beta_l and p:
                p_corr_l = np.exp(self._beta_l(t) * ())
            else:
                beta_l = 0.0

            if self._beta_s:
                beta_s = self._beta_s(t)
            else:
                beta_s = 0.0

        if self._den_l and not self._den_s:
            # Liquid density correlation exists, solid density correlation does not.
            if spec == 'molar':
                if self._den_l.unit == 'kg/m3':
                    return self._den_l(t) * 1000.0 / self._mw
                elif self._den_l.unit == 'mol/m3':
                    return self._den_l(t)
                else:
                    raise RuntimeError("liquid density correlation does not have a valid unit.")
            elif spec == 'mass':
                return
            else:
                raise ValueError("spec is not valid.")
        elif not self._den_l and self._den_s:
            # Liquid density correlation does not exist, solid density correlation exists.
            if spec == 'molar':
                return
            elif spec == 'mass':
                return
            else:
                raise ValueError("spec is not valid.")
        elif self._den_l and self._den_s:
            # Liquid density and solid density correlations exist.
            if spec == 'molar':
                return
            elif spec == 'mass':
                return
            else:
                raise ValueError("spec is not valid.")
        else:
            raise RuntimeError("liquid and solid density correlations not loaded for Comp.")

    # TODO: Improve interface by making these checks part of getter/setter methods?
    def _check_assoc_sites(self):
        # Check to ensure there are no duplicate association sites.
        if self.assoc_sites is not None:
            if not isinstance(self.assoc_sites, (list, tuple)):
                raise TypeError("Comp objects must be a list of unique assoc_site objects.")
            elif len(self.assoc_sites) != len(set(self.assoc_sites)):
                raise ValueError("Comp objects must be a list of unique assoc_site objects.")
            else:
                pass

    def _check_assoc_parameters(self):
        # Check to ensure association parameter are lists of AssocSiteInter objects.
        if self.assoc_sites is not None:
            if self.cpa_assoc is not None:
                for asi in self.cpa_assoc:
                    if not isinstance(asi, AssocSiteInter):
                        raise TypeError("Association parameter lists must contain AssocSiteInter objects.")

    def __eq__(self, other):
        if isinstance(other, Comp):
            name_eq = self.name == other.name
            return name_eq
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.name)

    def __str__(self):
        return "Name: {}, CAS No.: {}".format(self.name, self.cas_no)

class PseudoComp(object):
    """A pseudo-component (polymer, distillation cut, asphaltene, etc.).

    Notes
    -----
    # TODO: Improve agreement bewteeen Comp and PseudoComp classes. Currently a skeleton implementation.
    # TODO: Add Riazi correlations to estimate Tc, Pc, Vc, Rhoc from mw, sg, NBP.
    """

    def __init__(self, name):
        """
        Parameters
        ----------
        name : str
        """
        # General Attributes
        self.name = name
        self.mw = None
        self.sg = None
        self.nbp = None
        self.acentric = None
        self.tc = None
        self.pc = None
        self.vc = None
        self.rhoc = None

        # EOS Physical Attributes
        self.srk_phys = None
        self.pr_phys = None
        self.gpr_phys = None
        self.cpa_phys = None
        self.spc_saft_phys = None

        # EOS Association Attributes
        self.assoc_sites = None
        self.cpa_assoc = None
        self.spc_saft_assoc = None

    @property
    def name(self):
        """str : Name of pseudo-component.

        Value can only be set whe creating a new Comp instance.
        """
        return self._name

    @name.setter
    def name(self, value):
        try:
            self._name
        except AttributeError:
            self._name = value

    def __eq__(self, other):
        if isinstance(other, PseudoComp):
            name_eq = self.name == other.name
            return name_eq
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self.name)

    def __str__(self):
        return "Name: {}, MW: {}, SG: {}, NBP: {}".format(self.name, self.mw, self.sg, self.nbp)

class CompSet(object):
    """Set of components or pseudo-components.

    Notes
    -----
    """
    def __init__(self, comps=None):
        """
        Parameters
        ----------
        comps : list or tuple of Comp objects
        """
        if comps is None:
            raise ValueError("comps must be provided to create an instance of CompSet.")
        else:
            self.comps = comps

    @property
    def comps(self):
        """list or tuple of Comp objects : A collection of Comp objects.

        Values can only be set whe creating a new CompSet instance.
        """
        return self._comps

    @comps.setter
    def comps(self, value):
        try:
            self._comps
        except AttributeError:
            if isinstance(value, (list, tuple)):
                if len(value) == 0:
                    raise ValueError("comps must contain at least one Comp object.")
                elif len(value) != len(set(value)):
                    raise ValueError("comps cannot contain duplicate Comp objects.")
                elif all(isinstance(item, (Comp, PseudoComp)) for item in value):
                    self._comps = value
                else:
                    raise TypeError("comps must contain only Comp or PseudoComp objects.")
            else:
                raise TypeError("comps must be a list or tuple.")

    @property
    def size(self):
        """int : The number of Comp or PseudoComp objects in 'comps'."""
        return len(self._comps)

    @property
    def can_associate(self):
        """list of bool : Boolean indicating if Comp or PseudoComp objects in 'comps' can associate."""
        result = []
        for comp in self._comps:
            if comp.assoc_sites is not None:
                result.append(True)
            else:
                result.append(False)
        return result

    @property
    def mw(self):
        """list of float or None : Molecular weight for each Comp or PseudoComp objects in 'comps'.

        Returns None if any Comp or PseudoComp object is missing molecular weight."""
        result = []
        for comp in self._comps:
            if comp.mw is None:
                return None
            else:
                result.append(comp.mw)
        return np.array(result)

    def __eq__(self, other):
        if isinstance(other, CompSet):
            return self.comps == other.comps
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def __hash__(self):
        return hash(self._comps)

    def __str__(self):
        result = []
        for comp in self._comps:
            result.append(comp.name)
        return ",".join(tuple(result))


##############################################################################
#
#  File format versions:
#
#  1: initial version
#
#  2: now contains reciprocal planck opacity and rosseland opacity
#     previous rosseland opacity was actually reciprocal planck opacity
#
##############################################################################

from __future__ import print_function, division

import os
import hashlib

import h5py
import numpy as np
from astropy import log as logger
import six

from ..version import __version__

from ..util.constants import c
from ..util.functions import FreezableClass
from ..util.interpolate import interp1d_fast_loglog
from ..util.integrate import integrate_loglog
from ..util.nans import check_for_nans

from .optical_properties import OpticalProperties
from .emissivities import Emissivities
from .mean_opacities import MeanOpacities

def henyey_greenstein(mu, g, p_lin_max):
    P1 = (1. - g * g) / (1. + g * g - 2. * g * mu) ** 1.5
    P2 = - p_lin_max * P1 * (1. - mu * mu) / (1. + mu * mu)
    P3 = P1 * 2. * mu / (1. + mu * mu)
    P4 = 0.
    return P1, P2, P3, P4

class SphericalDust(FreezableClass):
    r"""
    This class should be used in cases where fully arbitrary dust properties
    need to be specified, within the framework of randomly oriented grains,
    which means that the scattering phase function has the general form:

    .. math:: R(\theta) = \left[\begin{array}{cccc}P_1 & P_2 & 0 & 0 \\P_2 & P_1 & 0 & 0 \\0 & 0 & P_3 & -P_4 \\0 & 0 & P_4 & P_3\end{array}\right]

    This class is initialized with::

        d = SphericalDust()

    and the properties should then be set manually. See
    `here <http://docs.hyperion-rt.org/en/stable/setup/setup_dust.html#fully-customized-4-element-dust>`_
    for a description of the available properties and how to set them.
    """

    def __init__(self, *args):

        self._file = None
        self.md5 = None

        self.optical_properties = OpticalProperties()
        self.mean_opacities = MeanOpacities()
        self.emissivities = Emissivities()

        self.set_sublimation_specific_energy('no', 0.)

        self._freeze()

        if len(args) == 0:
            pass
        elif len(args) == 1:
            self.read(args[0])
        else:
            raise Exception("SphericalDust cannot take more than one argument")

    def hash(self):

        h = hashlib.md5()

        if self.optical_properties is None or not self.optical_properties.all_set():
            h.update('none'.encode('utf-8'))
        else:
            h.update(self.optical_properties.hash().encode('utf-8'))

        if self.emissivities is None or not self.emissivities.all_set():
            h.update('none'.encode('utf-8'))
        else:
            h.update(self.emissivities.hash().encode('utf-8'))

        if self.mean_opacities is None or not self.mean_opacities.all_set():
            h.update('none'.encode('utf-8'))
        else:
            h.update(self.mean_opacities.hash().encode('utf-8'))

        import struct
        h.update(self.sublimation_mode.encode('utf-8'))
        h.update(struct.pack('>d', self.sublimation_energy))

        return h.hexdigest()

    def set_lte_emissivities(self, n_temp=1200, temp_min=0.1, temp_max=100000.):
        '''
        Calculate the emissivities assuming LTE

        Parameters
        ----------
        n_temp : int, optional
            The number of temperatures to calculate the emissivities for
        temp_min : float, optional
            The minimum temperature to calculate the emissivities for
        temp_max : float, optional
            The maximum temperature to calculate the emissivities for
        '''
        self.mean_opacities.compute(self.optical_properties, n_temp=n_temp,
                                    temp_min=temp_min, temp_max=temp_max)
        self.emissivities.set_lte(self.optical_properties, self.mean_opacities)

    def plot(self, filename):

        # Check that the optical properties have been set
        self.optical_properties.ensure_all_set()

        import matplotlib.pyplot as plt

        # Save original rc parameters
        rc_orig = plt.rcParams

        # Reset to defaults
        plt.rcdefaults()
        plt.rc('legend', fontsize=7)
        plt.rc('axes', titlesize='x-small')
        plt.rc('axes', labelsize='x-small')
        plt.rc('xtick', labelsize='xx-small')
        plt.rc('ytick', labelsize='xx-small')
        plt.rc('axes', linewidth=0.5)
        plt.rc('patch', linewidth=0.5)

        # Compute mean opacities if not already existent
        self._compute_mean_opacities()

        # Check that emissivities are set (before computing mean opacities)
        if not self.emissivities.all_set():
            logger.info("Computing emissivities assuming LTE")
            self.emissivities.set_lte(self.optical_properties, self.mean_opacities)

        # Initialize figure
        fig = plt.figure(figsize=(10, 12))

        # Plot optical properties
        fig = self.optical_properties.plot(fig, [421, 423, 424, 425, 426])

        # Plot mean opacities
        fig = self.mean_opacities.plot(fig, 428)

        # Plot emissivities
        fig = self.emissivities.plot(fig, 427)

        # Adjust spacing between subplots
        fig.subplots_adjust(left=0.08, right=0.92, wspace=0.22, hspace=0.30)

        # Save figure
        fig.savefig(filename, bbox_inches='tight')

        # Close figure to save RAM
        plt.close(fig)

        # Restore rc parameters
        plt.rc(rc_orig)

    def set_sublimation_temperature(self, mode, temperature=0.):
        '''
        Set the dust sublimation mode and temperature.

        Parameters
        ----------
        mode : str
            The dust sublimation mode, which can be:
                * 'no'   - no sublimation
                * 'fast' - remove all dust in cells exceeding the
                           sublimation temperature
                * 'slow' - reduce the dust in cells exceeding the
                           sublimation temperature
                * 'cap'  - any temperature exceeding the sublimation
                           temperature is reset to the sublimation
                           temperature.

        temperature : float, optional
            The dust sublimation temperature, in K
        '''

        if mode not in ['no', 'fast', 'slow', 'cap']:
            raise Exception("mode should be one of no/fast/slow/cap")

        if mode != 'no' and temperature is None:
            raise Exception("Need to specify a sublimation temperature")

        self.sublimation_mode = mode
        self.sublimation_energy = self.temperature2specific_energy(temperature)

    def set_sublimation_specific_energy(self, mode, specific_energy=0.):
        '''
        Set the dust sublimation mode and specific energy.

        Parameters
        ----------
        mode : str
            The dust sublimation mode, which can be:
                * 'no'   - no sublimation
                * 'fast' - remove all dust in cells exceeding the
                           sublimation specific energy
                * 'slow' - reduce the dust in cells exceeding the
                           sublimation specific energy
                * 'cap'  - any specific energy exceeding the sublimation
                           specific energy is reset to the sublimation
                           specific energy.

        specific_energy : float, optional
            The dust sublimation specific energy, in cgs
        '''

        if mode not in ['no', 'fast', 'slow', 'cap']:
            raise Exception("mode should be one of no/fast/slow/cap")

        if mode != 'no' and specific_energy is None:
            raise Exception("Need to specify a sublimation specific_energy")

        self.sublimation_mode = mode
        self.sublimation_energy = specific_energy

    def _write_dust_sublimation(self, group):
        group.attrs['sublimation_mode'] = np.string_(self.sublimation_mode)
        if self.sublimation_mode in ['slow', 'fast', 'cap']:
            group.attrs['sublimation_specific_energy'] = self.sublimation_energy

    def _read_dust_sublimation(self, group):
        if 'sublimation_mode' in group.attrs:
            self.sublimation_mode = group.attrs['sublimation_mode'].decode('ascii')
            if self.sublimation_mode in ['slow', 'fast', 'cap']:
                self.sublimation_energy = group.attrs['sublimation_specific_energy']

    def _compute_mean_opacities(self):
        if not self.mean_opacities.all_set():
            self.mean_opacities.compute(self.optical_properties)

    def write(self, filename, compression=True):
        '''
        Write out to a standard dust file, including calculations of the mean
        opacities and optionally thermal emissivities.
        '''

        # Check that the optical properties have been set
        self.optical_properties.ensure_all_set()

        # Compute mean opacities if not already existent
        self._compute_mean_opacities()

        # Check that emissivities are set (before computing mean opacities)
        if not self.emissivities.all_set():
            logger.info("Computing emissivities assuming LTE")
            self.emissivities.set_lte(self.optical_properties, self.mean_opacities)

        # Create dust table set
        if isinstance(filename, six.string_types):
            dt = h5py.File(filename, 'w')
        else:
            dt = filename

        # Add standard keywords to header
        dt.attrs['version'] = 2
        dt.attrs['type'] = 1
        dt.attrs['python_version'] = np.string_(__version__)
        if self.md5:
            dt.attrs['asciimd5'] = np.string_(self.md5)

        # Add optical properties and scattering angle tables
        self.optical_properties.to_hdf5_group(dt)

        # Add mean opacities table
        self.mean_opacities.to_hdf5_group(dt)

        # Add emissivities and emissivity variable tables
        self.emissivities.to_hdf5_group(dt)

        # Dust sublimation parameters
        self._write_dust_sublimation(dt)

        # Check that there are no NaN values in the file - if there are, a
        # warning is emitted.
        check_for_nans(dt)

        # Close dust file
        if isinstance(dt, h5py.File):
            dt.close()

        self._file = (filename, self.hash())

    def read(self, filename):
        '''
        Read in from a standard dust file
        '''

        from ..util.functions import asstr

        if isinstance(filename, six.string_types):

            # Check file exists
            if not os.path.exists(filename):
                raise Exception("File not found: %s" % filename)

            # Read in dust table set
            dt = h5py.File(filename, 'r')
            close = True

        else:

            # Read in dust table set
            dt = filename
            close = False

        # Check version and type
        if dt.attrs['version'] not in [1, 2]:
            raise Exception("Version should be 1 or 2")
        if dt.attrs['type'] != 1:
            raise Exception("Type should be 1")
        if 'asciimd5' in dt.attrs:
            self.md5 = asstr(dt.attrs['asciimd5'])
        else:
            self.md5 = None

        # Read in the optical properties
        self.optical_properties.from_hdf5_group(dt)

        # Read in the planck and rosseland mean opacities
        if dt.attrs['version'] == 1:
            logger.warning("Version 1 dust file detected - discarding mean opacities and recomputing them")
            self.mean_opacities.compute(self.optical_properties)
        else:
            self.mean_opacities.from_hdf5_group(dt)

        # Read in emissivities
        self.emissivities.from_hdf5_group(dt)

        # Dust sublimation parameters
        self._read_dust_sublimation(dt)

        # Close file object if needed
        if close:
            dt.close()
            self._file = (filename, self.hash())

    def chi_nu_temperature(self, temperature):
        """
        Compute the mean opacity to extinction for a blackbody at a given temperature.

        Parameters
        ----------
        temperature : float
            The temperature of the blackbody to use

        Returns
        -------
        chi_nu_mean : float
            The mean opacity to extinction
        """
        self._compute_mean_opacities()
        return interp1d_fast_loglog(self.mean_opacities.temperature,
                                    self.mean_opacities.chi_planck,
                                    temperature,
                                    bounds_error=True)

    def kappa_nu_temperature(self, temperature):
        """
        Compute the mean opacity to absorption for a blackbody at a given temperature.

        Parameters
        ----------
        temperature : float
            The temperature of the blackbody to use

        Returns
        -------
        kappa_nu_mean : float
            The mean opacity to absorption
        """
        self._compute_mean_opacities()
        return interp1d_fast_loglog(self.mean_opacities.temperature,
                                    self.mean_opacities.kappa_planck,
                                    temperature,
                                    bounds_error=True)

    def chi_nu_spectrum(self, nu, fnu):
        """
        Compute the mean opacity to extinction for a given spectrum.

        Parameters
        ----------
        nu : array_like
            The frequencies, in Hz
        fnu : array_like
            The monochromatic fluxes per unit frequency. Units are unimportant
            since proportionality constants are cancelled out in the
            computation.

        Returns
        -------
        chi_nu_mean : float
            The mean opacity to extinction
        """
        if nu.max() > self.optical_properties.nu.max() or nu.min() < self.optical_properties.nu.min():
            raise Exception("Opacity to extinction is not defined at all "
                            "spectrum frequencies")
        chi_nu = self.optical_properties.interp_chi_nu(nu)
        return (integrate_loglog(nu, fnu * chi_nu) /
                integrate_loglog(nu, fnu))

    def kappa_nu_spectrum(self, nu, fnu):
        """
        Compute the mean opacity to absorption for a given spectrum.

        Parameters
        ----------
        nu : array_like
            The frequencies, in Hz
        fnu : array_like
            The monochromatic fluxes per unit frequency. Units are unimportant
            since proportionality constants are cancelled out in the
            computation.

        Returns
        -------
        kappa_nu_mean : float
            The mean opacity to absorption
        """
        if nu.max() > self.optical_properties.nu.max() or nu.min() < self.optical_properties.nu.min():
            raise Exception("Opacity to absorption is not defined at all "
                            "spectrum frequencies")
        kappa_nu = self.optical_properties.interp_kappa_nu(nu)
        return (integrate_loglog(nu, fnu * kappa_nu) /
                integrate_loglog(nu, fnu))

    def temperature2specific_energy(self, temperature):
        """
        Convert a temperature to its corresponding specific energy value.

        Parameters
        ----------
        temperature : float or array_like
            The temperature to convert

        Returns
        -------
        specific_energy : float or array_like
            The specific energy corresponding to the input temperature
        """

        self._compute_mean_opacities()

        specific_energy = interp1d_fast_loglog(self.mean_opacities.temperature,
                                               self.mean_opacities.specific_energy,
                                               temperature,
                                               bounds_error=False,
                                               fill_value=np.nan)

        if np.isscalar(temperature):
            if temperature < self.mean_opacities.temperature[0]:
                specific_energy = self.mean_opacities.specific_energy[0]
            elif temperature > self.mean_opacities.temperature[-1]:
                specific_energy = self.mean_opacities.specific_energy[-1]
        else:
            specific_energy[temperature < self.mean_opacities.temperature[0]] = self.mean_opacities.specific_energy[0]
            specific_energy[temperature > self.mean_opacities.temperature[-1]] = self.mean_opacities.specific_energy[-1]

        return specific_energy

    def specific_energy2temperature(self, specific_energy):
        """
        Convert a specific energy value to its corresponding temperature.

        Parameters
        ----------
        specific_energy : float or array_like
            The specific energy to convert

        Returns
        -------
        temperature : float or array_like
            The temperature corresponding to the input specific energy
        """

        self._compute_mean_opacities()

        temperature = interp1d_fast_loglog(self.mean_opacities.specific_energy,
                                           self.mean_opacities.temperature,
                                           specific_energy,
                                           bounds_error=False,
                                           fill_value=np.nan)

        if np.isscalar(specific_energy):
            if specific_energy < self.mean_opacities.specific_energy[0]:
                temperature = self.mean_opacities.temperature[0]
            elif specific_energy > self.mean_opacities.specific_energy[-1]:
                temperature = self.mean_opacities.temperature[-1]
        else:
            temperature[specific_energy < self.mean_opacities.specific_energy[0]] = self.mean_opacities.temperature[0]
            temperature[specific_energy > self.mean_opacities.specific_energy[-1]] = self.mean_opacities.temperature[-1]

        return temperature

class IsotropicDust(SphericalDust):
    """
    This class should be used for dust properties that include isotropic
    scattering. The dust properties should be instatiated as::

        d = IsotropicDust(nu, albedo, chi)

    where ``nu``, ``albedo``, and ``chi`` are 1-D Numpy arrays containing the
    frequencies, albedo, and opacity to extinction respectively.
    """

    def __init__(self, nu, albedo, chi):

        SphericalDust.__init__(self)

        # Set cos(theta) grid for computing the scattering matrix elements
        self.optical_properties.mu = np.linspace(-1., 1., 2)

        # Set optical properties
        self.optical_properties.nu = nu
        self.optical_properties.albedo = albedo
        self.optical_properties.chi = chi

        # Compute scattering matrix elements
        self.optical_properties.initialize_scattering_matrix()

        # Set scattering matrix to isotropic values
        self.optical_properties.P1[:, :] = 1.
        self.optical_properties.P2[:, :] = 0.
        self.optical_properties.P3[:, :] = 1.
        self.optical_properties.P4[:, :] = 0.

        # Sort optical properties
        self.optical_properties._sort()

class HenyeyGreensteinDust(SphericalDust):
    """
    This class should be used for dust properties that include
    scattering parameterized by the `Henyey-Greenstein, 1941
    <http://dx.doi.org/10.1086/144246>`_ function. The dust properties should
    be instatiated as::

        d = HenyeyGreensteinDust(nu, albedo, chi, g, p_lin_max)

    where ``nu``, ``albedo``, and ``chi`` are 1-D Numpy arrays containing the
    frequencies, albedo, and opacity to extinction respectively, and ``g`` and
    ``p_lin_max`` are also 1-D Numpy arrays containing the asymmetry parameter
    and the maximum linear polarization.
    """

    def __init__(self, nu, albedo, chi, g, p_lin_max):

        SphericalDust.__init__(self)

        # Set cos(theta) grid for computing the scattering matrix elements
        n_mu = 100
        self.optical_properties.mu = np.linspace(-1., 1., n_mu)

        # Set optical properties
        self.optical_properties.nu = nu
        self.optical_properties.albedo = albedo
        self.optical_properties.chi = chi

        # Compute scattering matrix elements
        self.optical_properties.initialize_scattering_matrix()

        for i in range(n_mu):
            self.optical_properties.P1[:, i], \
                self.optical_properties.P2[:, i], \
                self.optical_properties.P3[:, i], \
                self.optical_properties.P4[:, i] = henyey_greenstein(self.optical_properties.mu[i], g, p_lin_max)

class HOCHUNKDust(HenyeyGreensteinDust):
    """
    This class should be used for dust properties that include
    scattering parameterized by the `Henyey-Greenstein, 1941
    <http://dx.doi.org/10.1086/144246>`_ function, which are formatted for the
    `HOCHUNK code <http://gemelli.colorado.edu/~bwhitney/codes/>`_. The dust
    properties should be instatiated as::

        d = HOCHUNKDust(filename)

    where ``filename`` is the name of the file containing the dust properties
    in the HOCHUNK format.
    """

    def __init__(self, filename):

        # Read in dust file
        dustfile = np.loadtxt(filename, dtype=[('wav', float), ('c_ext', float),
                                               ('c_sca', float), ('chi', float), ('g', float),
                                               ('p_lin_max', float)], usecols=[0, 1, 2, 3, 4, 5])

        # Ensure file is ordered in increasing frequency
        if dustfile['wav'][-1] > dustfile['wav'][0]:
            dustfile = dustfile[::-1]

        # Compute frequency and albedo
        nu = c / dustfile['wav'] * 1.e4
        albedo = dustfile['c_sca'] / dustfile['c_ext']

        self.md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest()

        HenyeyGreensteinDust.__init__(self, nu, albedo, dustfile['chi'], dustfile['g'], dustfile['p_lin_max'])

TTsreDust = HOCHUNKDust

class CoatsphSingle(SphericalDust):

    def __init__(self, directory, size, density):
        '''
        Initialize single-component dust.

        Parameters
        ----------
        directory : str
            Directory containing all the files describing the dust
        size : float
            Grain size, in cm
        density : float
            Dust grain density, in g/cm^3
        '''

        SphericalDust.__init__(self)

        f = open('%s/coatsph_forw.dat' % directory, 'rb')
        version = f.readline()
        n_components = int(f.readline().strip().split()[5])

        # Read in main dust file

        dustfile = np.loadtxt(f, skiprows=3,
                              dtype=[('x', float), ('radius', float), ('wav', float),
                                     ('q_ext', float), ('q_sca', float), ('q_back', float),
                                     ('g', float)])

        n_wav = len(dustfile)

        self.optical_properties.nu = c / dustfile['wav'] * 1.e4
        self.optical_properties.albedo = dustfile['q_sca'] / dustfile['q_ext']
        self.optical_properties.chi = 0.75 * dustfile['q_ext'] / size / density

        # Read in scattering matrix elements

        for i in range(n_wav):

            filename = '%s/coatsph_scat_%04i_0001.dat' % (directory, i + 1)

            phasefile = np.loadtxt(filename, skiprows=9,
                                   dtype=[('theta', float), ('s11', float), ('polariz',
                                                                             float), ('s12', float), ('s33', float), ('s34',
                                                                                float)])

            if i == 0:
                self.optical_properties.mu = np.cos(np.radians(phasefile['theta']))
                self.optical_properties.initialize_scattering_matrix()

            self.optical_properties.P1[i, :] = phasefile['s11']
            self.optical_properties.P2[i, :] = phasefile['s12']
            self.optical_properties.P3[i, :] = phasefile['s33']
            self.optical_properties.P4[i, :] = phasefile['s34']

class CoatsphMultiple(SphericalDust):

    def __init__(self, directory):
        '''
        Initialize multi-component dust.

        Parameters
        ----------
        directory : str
            Directory containing all the files describing the dust
        '''

        SphericalDust.__init__(self)

        f = open('%s/coatsph_forw.dat' % directory, 'rb')
        version = f.readline()
        n_components = int(f.readline().strip().split()[5])

        # Read in main dust file

        dustfile = np.loadtxt(f, skiprows=7,
                              dtype=[('wav', float), ('c_ext', float), ('c_sca', float),
                                     ('chi', float), ('g', float), ('pmax', float),
                                     ('thetmax', float)])

        n_wav = len(dustfile)
        self.optical_properties.nu = c / dustfile['wav'] * 1.e4
        self.optical_properties.albedo = dustfile['c_sca'] / dustfile['c_ext']
        self.optical_properties.chi = dustfile['chi']

        # Read in scattering matrix elements

        for i in range(n_wav):

            filename = '%s/coatsph_scat.%04i.dat' % (directory, i + 1)

            phasefile = np.loadtxt(filename, skiprows=7,
                                   dtype=[('theta', float), ('s11', float), ('polariz',
                                                                             float), ('s12', float), ('s33', float), ('s34',
                                                                                float)])

            if i == 0:
                self.optical_properties.mu = np.cos(np.radians(phasefile['theta']))
                self.optical_properties.initialize_scattering_matrix()

            self.optical_properties.P1[i, :] = phasefile['s11']
            self.optical_properties.P2[i, :] = phasefile['s12']
            self.optical_properties.P3[i, :] = phasefile['s33']
            self.optical_properties.P4[i, :] = phasefile['s34']

class MieXDust(SphericalDust):

    def __init__(self, model):

        SphericalDust.__init__(self)

        wav = np.loadtxt('%s.alb' % model, usecols=[0])
        self.optical_properties.albedo = np.loadtxt('%s.alb' % model, usecols=[1])
        kappa = np.loadtxt('%s.k_abs' % model, usecols=[1])
        self.optical_properties.chi = kappa / (1 - self.optical_properties.albedo)

        # Check for NaN values
        for quantity in ['chi', 'albedo']:

            values = self.optical_properties.__dict__[quantity]

            if np.any(np.isnan(values)):
                logger.warning("NaN values found inside MieX %s file - interpolating" % quantity)
                invalid = np.isnan(values)
                values[invalid] = interp1d_fast_loglog(wav[~invalid], values[~invalid], wav[invalid])
                if np.any(np.isnan(values)):
                    raise Exception("Did not manage to fix NaN values in MieX %s" % quantity)

        self.optical_properties.nu = c / wav * 1.e4

        n_wav = len(wav)
        n_mu = (len(open('%s.f11' % model).readlines()) // n_wav) - 1

        mu = np.zeros(n_mu)

        # Read mu
        f11 = open('%s.f11' % model)
        f11.readline()
        f11.readline()
        for i in range(n_mu):
            mu[i] = np.cos(np.radians(float(f11.readline().split()[0])))
        f11.close()
        self.optical_properties.mu = mu[::-1]

        # Read in matrix elements

        self.optical_properties.initialize_scattering_matrix()

        f11 = open('%s.f11' % model)
        f12 = open('%s.f12' % model)
        f33 = open('%s.f33' % model)
        f34 = open('%s.f34' % model)

        f11.readline()
        f12.readline()
        f33.readline()
        f34.readline()

        for j in range(n_wav):

            if float(f11.readline()) != wav[j]:
                raise Exception("Incorrect wavelength in f11")
            if float(f12.readline()) != wav[j]:
                raise Exception("Incorrect wavelength in f12")
            if float(f33.readline()) != wav[j]:
                raise Exception("Incorrect wavelength in f33")
            if float(f34.readline()) != wav[j]:
                raise Exception("Incorrect wavelength in f34")

            for i in range(n_mu):

                self.optical_properties.P1[j, n_mu - i - 1] = float(f11.readline().split()[1])
                self.optical_properties.P2[j, n_mu - i - 1] = float(f12.readline().split()[1])
                self.optical_properties.P3[j, n_mu - i - 1] = float(f33.readline().split()[1])
                self.optical_properties.P4[j, n_mu - i - 1] = float(f34.readline().split()[1])

        for i in range(n_mu):

            for quantity in ['P1', 'P2', 'P3', 'P4']:

                values = self.optical_properties.__dict__[quantity]

                if np.any(np.isnan(values[:, i])):
                    logger.warning("NaN values found inside MieX %s file - interpolating" % quantity)
                    invalid = np.isnan(values[:, i])
                    values[:, i][invalid] = interp1d_fast_loglog(wav[~invalid], values[:, i][~invalid], wav[invalid])
                    if np.any(np.isnan(values[:, i])):
                        raise Exception("Did not manage to fix NaN values in MieX %s" % quantity)

class BHDust(SphericalDust):
    """
    This class should be used for dust properties that were computed using
    `this dust calculation code <https://github.com/hyperion-rt/bhmie>`_ which
    is a wrapper to the ``bhmie`` routine originally written by C.F. Bohren and
    D. Huffman and improved by B. Draine.

    When using the ``bhmie`` code, you should set the output format to ``2``,
    which will create a number of files ending in ``.wav``, ``.mu``, ``.alb``,
    etc. Then, instantiate this class with the name of the directory containing
    these output files along with the prefix used. For example, if you use
    ``directory/mydust`` as a prefix in ``bhmie``, you can import this dust
    with::

        >>> from hyperion.dust import BHDust
        >>> d = BHDust('directory/mydust')

    """

    def __init__(self, model):

        SphericalDust.__init__(self)

        mu = np.loadtxt('%s.mu' % model)

        nu = c / np.loadtxt('%s.wav' % model) * 1.e4
        albedo = np.loadtxt('%s.alb' % model)
        chi = np.loadtxt('%s.chi' % model)

        P1 = np.loadtxt('%s.f11' % model)
        P2 = np.loadtxt('%s.f12' % model)
        P3 = np.loadtxt('%s.f33' % model)
        P4 = np.loadtxt('%s.f34' % model)

        if nu[-1] < nu[0]:
            nu = nu[::-1]
            albedo = albedo[::-1]
            chi = chi[::-1]
            P1 = P1[::-1, :]
            P2 = P2[::-1, :]
            P3 = P3[::-1, :]
            P4 = P4[::-1, :]

        if mu[-1] < mu[0]:
            mu = mu[::-1]
            P1 = P1[:, ::-1]
            P2 = P2[:, ::-1]
            P3 = P3[:, ::-1]
            P4 = P4[:, ::-1]

        self.optical_properties.mu = mu

        self.optical_properties.nu = nu
        self.optical_properties.albedo = albedo
        self.optical_properties.chi = chi

        self.optical_properties.P1 = P1
        self.optical_properties.P2 = P2
        self.optical_properties.P3 = P3
        self.optical_properties.P4 = P4


#!/usr/bin/python

# Comments and/or additions are welcome (send e-mail to:
# robert.paton@chem.ox.ac.uk

#####################################
#        vib_scale_factors.py       #
#####################################
###  Written by:  Rob Paton #########
###  Last modified:  Mar 27, 2017 ###
#####################################

import numpy as np

import sys
if sys.version_info > (2, ):
    # Py3
    Str_char = "U%d"
else:
    # Py2
    Str_char = "S%d"

'''
Frequency scaling factors, taken from the beta 2 version of version 3 of The Trhular group database (https://t1.chem.umn.edu/freqscale/index.html
I. M. Alecu, J. Zheng, Y. Zhao, and D. G. Truhlar, J. Chem. Theory Comput. 6, 2872-2887 (2010).

The array is ordered as:
[level/basis set, zpe_fac, zpe_ref, zpe_meth, harm_fac, harm_ref, harm_meth, fund_fac, fund_ref, fund_meth]
where zpe_fac, harm_fac and fund_fac are the scaling factors for ZPEs, harmonic frequencies, and fundamentals, respectively.

The ref and meth elements refer to original references and method of determinantion by the Truhlar group. All information taken from
https://comp.chem.umn.edu/freqscale/version3b2.htm

Methods
D: The scale factor was directly obtained from the ZPVE15/10 or F38/10 databases given in Ref. 1.
C: The scale factor was obtained by applying a small systematic correction of -0.0025 to preexisting scale factor. The references for the preexisting (uncorrected) scale factors are given in Supporting Information of Ref. 1 and in Version 1 of this database
R: The scale factor was obtained via the Reduced Scale Factor Optimization Model described in Ref. 1. Briefly, this entails using the ZPE6 database for determining ZPE scale factors, and/or using the universal scale factor ratios of aF/ZPE = 0.974 and aH/ZPE = 1.014 to obtain the respective values for the scale factors for fundamental and harmonic frequencies.
'''
scaling_refs = np.array([("none"), ("I. M. Alecu, J. Zheng, Y. Zhao, and D. G. Truhlar, J. Chem. Theory Comput. 6, 2872-2887 (2010)."),("Y. Zhao and D. G. Truhlar, unpublished (2003), modified by systematic correction of -0.0025 by I. M. Alecu (2010)."), ("I. M. Alecu, unpublished (2011)."), ("J. Zheng, R. J. Rocha, M. Pelegrini, L. F. A. Ferrao, E. F. V. Carvalho, O. Roberto-Neto, F. B. C. Machado, and D. G. Truhlar, J. Chem. Phys. 136, 184310/1-10 (2012)."), ("J. Zheng and D. G. Truhlar, unpublished (2014)."), ("J. Bao and D. G. Truhlar, unpublished (2014)."), ("H. Yu, J. Zheng, and D. G. Truhlar, unpublished (2015)")])

scaling_data = np.array([("AM1" ,  0.948 ,  1 ,  "R" ,  0.961 ,  1 ,  "R" ,  0.923 ,  1 ,  "R" ),("B1B95/6-31+G(d,p)" ,  0.971 ,  1 ,  "C" ,  0.985 ,  1 ,  "R" ,  0.946 ,  1 ,  "R" ),("B1B95/MG3S" ,  0.973 ,  1 ,  "C" ,  0.987 ,  1 ,  "R" ,  0.948 ,  1 ,  "R" ),("B1LYP/MG3S" ,  0.978 ,  1 ,  "D" ,  0.994 ,  1 ,  "D" ,  0.955 ,  1 ,  "D" ),("B3LYP/6-31G(2df,2p)" ,  0.981 ,  1 ,  "C" ,  0.995 ,  1 ,  "R" ,  0.955 ,  1 ,  "R" ),("B3LYP/6-31G(d)" ,  0.977 ,  1 ,  "R" ,  0.991 ,  1 ,  "R" ,  0.952 ,  1 ,  "R" ),("B3LYP/aug-cc-pVTZ" ,  0.985 ,  3 ,  "R" ,  0.999 ,  3 ,  "R" ,  0.959 ,  3 ,  "R" ),("B3LYP/def2-TZVP" ,  0.985 ,  3 ,  "R" ,  0.999 ,  3 ,  "R" ,  0.959 ,  3 ,  "R" ),("B3LYP/ma-TZVP" ,  0.986 ,  1 ,  "R" ,  1.000 ,  1 ,  "R" ,  0.960 ,  1 ,  "R" ),("B3LYP/MG3S" ,  0.983 ,  1 ,  "D" ,  0.998 ,  1 ,  "D" ,  0.960 ,  1 ,  "D" ),("B3P86/6-31G(d)" ,  0.971 ,  1 ,  "R" ,  0.985 ,  1 ,  "R" ,  0.946 ,  1 ,  "R" ),("B3PW91/6-31G(d)" ,  0.972 ,  1 ,  "R" ,  0.986 ,  1 ,  "R" ,  0.947 ,  1 ,  "R" ),("B97-3/ma-TZVP" ,  0.975 ,  1 ,  "R" ,  0.989 ,  1 ,  "R" ,  0.950 ,  1 ,  "R" ),("B97-3/MG3S" ,  0.972 ,  1 ,  "D" ,  0.986 ,  1 ,  "D" ,  0.947 ,  1 ,  "D" ),("B98/def2-TZVP" ,  0.984 ,  1 ,  "R" ,  0.998 ,  1 ,  "R" ,  0.958 ,  1 ,  "R" ),("B98/ma-TZVP" ,  0.985 ,  1 ,  "R" ,  0.999 ,  1 ,  "R" ,  0.959 ,  1 ,  "R" ),("B98/MG3S" ,  0.982 ,  1 ,  "D" ,  0.995 ,  1 ,  "D" ,  0.956 ,  1 ,  "D" ),("BB1K/6-31+G(d,p)" ,  0.954 ,  1 ,  "C" ,  0.967 ,  1 ,  "R" ,  0.929 ,  1 ,  "R" ),("BB1K/MG3S" ,  0.957 ,  1 ,  "C" ,  0.970 ,  1 ,  "R" ,  0.932 ,  1 ,  "R" ),("BB95/6-31+G(d,p)" ,  1.011 ,  1 ,  "C" ,  1.025 ,  1 ,  "R" ,  0.985 ,  1 ,  "R" ),("BB95/MG3S" ,  1.012 ,  1 ,  "C" ,  1.026 ,  1 ,  "R" ,  0.986 ,  1 ,  "R" ),("BLYP/6-311G(df,p)" ,  1.013 ,  1 ,  "R" ,  1.027 ,  1 ,  "R" ,  0.987 ,  1 ,  "R" ),("BLYP/6-31G(d)" ,  1.009 ,  1 ,  "R" ,  1.023 ,  1 ,  "R" ,  0.983 ,  1 ,  "R" ),("BLYP/MG3S" ,  1.013 ,  1 ,  "D" ,  1.031 ,  1 ,  "D" ,  0.991 ,  1 ,  "D" ),("BMC-CCSD" ,  0.985 ,  1 ,  "D" ,  1.001 ,  1 ,  "D" ,  0.962 ,  1 ,  "D" ),("BMK/ma-TZVP" ,  0.972 ,  1 ,  "R" ,  0.986 ,  1 ,  "R" ,  0.947 ,  1 ,  "R" ),("BMK/MG3S" ,  0.971 ,  1 ,  "D" ,  0.984 ,  1 ,  "D" ,  0.945 ,  1 ,  "D" ),("BP86/6-31G(d)" ,  1.007 ,  1 ,  "R" ,  1.021 ,  1 ,  "R" ,  0.981 ,  1 ,  "R" ),("BP86/ma-TZVP" ,  1.014 ,  1 ,  "R" ,  1.028 ,  1 ,  "R" ,  0.988 ,  1 ,  "R" ),("BPW60/6-311+G(d,p)" ,  0.934 ,  2 ,  "C" ,  0.910 ,  2 ,  "R" ,  0.947 ,  2 ,  "R" ),("BPW63/MG3S" ,  0.923 ,  2 ,  "C" ,  0.899 ,  2 ,  "R" ,  0.936 ,  2 ,  "R" ),("CAM-B3LYP/ma-TZVP" ,  0.976 ,  1 ,  "R" ,  0.990 ,  1 ,  "R" ,  0.951 ,  1 ,  "R" ),("CCSD(T)/jul-cc-pVTZ" , 0.984 ,  1 ,  "R" ,  0.998 ,  1 ,  "R" ,  0.958 ,  1 ,  "R" ),("CCSD(T)/aug-cc-pVTZ" , 0.987 ,  1 ,  "R" ,  1.001 ,  1 ,  "R" ,  0.961 ,  1 ,  "R" ),("CCSD(T)-F12/jul-cc-pVTZ" , 0.981 ,  1 ,  "R" ,  0.995 ,  1 ,  "R" ,  0.955 ,  1 ,  "R" ),("CCSD(T)-F12a/cc-pVTZ-F12" , 0.984 ,  1 ,  "R" ,  0.998 ,  1 ,  "R" ,  0.958 ,  1 ,  "R" ),("CCSD/jul-cc-pVTZ" ,  0.973 ,  1 ,  "R" ,  0.987 ,  1 ,  "R" ,  0.948 ,  1 ,  "R" ),("CCSD-F12/jul-cc-pVTZ" ,  0.971 ,  1 ,  "R" ,  0.985 ,  1 ,  "R" ,  0.946 ,  1 ,  "R" ),("G96LYP80/6-311+G(d,p)" ,  0.911 ,  2 ,  "C" ,  0.887 ,  2 ,  "R" ,  0.924 ,  2 ,  "R" ),("G96LYP82/MG3S" ,  0.907 ,  2 ,  "C" ,  0.883 ,  2 ,  "R" ,  0.920 ,  2 ,  "R" ),("GAM/def2-TZVP" ,  0.980 ,  7 ,  "D" ,  0.994 ,  7 ,  "D" ,  0.955 ,  7 ,  "D" ),("GAM/ma-TZVP" ,  0.981 ,  7 ,  "D" ,  0.995 ,  7 ,  "D" ,  0.956 ,  7 ,  "D" ),("HF/3-21G" ,  0.919 ,  1 ,  "R" ,  0.932 ,  1 ,  "R" ,  0.895 ,  1 ,  "R" ),("HF/6-31+G(d)" ,  0.911 ,  1 ,  "R" ,  0.924 ,  1 ,  "R" ,  0.887 ,  1 ,  "R" ),("HF/6-31+G(d,p)" ,  0.915 ,  1 ,  "C" ,  0.928 ,  1 ,  "R" ,  0.891 ,  1 ,  "R" ),("HF/6-311G(d,p)" ,  0.920 ,  1 ,  "R" ,  0.933 ,  1 ,  "R" ,  0.896 ,  1 ,  "R" ),("HF/6-311G(df,p)" ,  0.920 ,  1 ,  "R" ,  0.933 ,  1 ,  "R" ,  0.896 ,  1 ,  "R" ),("HF/6-31G(d)" ,  0.909 ,  1 ,  "R" ,  0.922 ,  1 ,  "R" ,  0.885 ,  1 ,  "R" ),("HF/6-31G(d,p)" ,  0.913 ,  1 ,  "R" ,  0.926 ,  1 ,  "R" ,  0.889 ,  1 ,  "R" ),("HF/MG3S" ,  0.919 ,  1 ,  "D" ,  0.932 ,  1 ,  "D" ,  0.895 ,  1 ,  "D" ),("HFLYP/MG3S" ,  0.899 ,  1 ,  "D" ,  0.912 ,  1 ,  "D" ,  0.876 ,  1 ,  "D" ),("HSEh1PBE/ma-TZVP" ,  0.979 ,  1 ,  "R" ,  0.993 ,  1 ,  "R" ,  0.954 ,  1 ,  "R" ),("M05/aug-cc-pVTZ" ,  0.978 ,  1 ,  "R" ,  0.992 ,  1 ,  "R" ,  0.953 ,  1 ,  "R" ),("M05/def2-TZVP" ,  0.978 ,  3 ,  "R" ,  0.991 ,  3 ,  "R" ,  0.952 ,  3 ,  "R" ),("M05/ma-TZVP" ,  0.979 ,  1 ,  "R" ,  0.993 ,  1 ,  "R" ,  0.954 ,  1 ,  "R" ),("M05/maug-cc-pVTZ" ,  0.978 ,  1 ,  "R" ,  0.992 ,  1 ,  "R" ,  0.953 ,  1 ,  "R" ),("M05/MG3S" ,  0.977 ,  1 ,  "D" ,  0.989 ,  1 ,  "D" ,  0.951 ,  1 ,  "D" ),("M05-2X/6-31+G(d,p)" ,  0.961 ,  1 ,  "D" ,  0.974 ,  1 ,  "D" ,  0.936 ,  1 ,  "D" ),("M05-2X/aug-cc-pVTZ" ,  0.964 ,  1 ,  "R" ,  0.977 ,  1 ,  "R" ,  0.939 ,  1 ,  "R" ),("M05-2X/def2-TZVPP" ,  0.962 ,  1 ,  "D" ,  0.976 ,  1 ,  "D" ,  0.938 ,  1 ,  "D" ),("M05-2X/ma-TZVP" ,  0.965 ,  1 ,  "R" ,  0.979 ,  1 ,  "R" ,  0.940 ,  1 ,  "R" ),("M05-2X/maug-cc-pVTZ" ,  0.964 ,  1 ,  "R" ,  0.977 ,  1 ,  "R" ,  0.939 ,  1 ,  "R" ),("M05-2X/MG3S" ,  0.962 ,  1 ,  "D" ,  0.975 ,  1 ,  "D" ,  0.937 ,  1 ,  "D" ),("M06/6-31+G(d,p)" ,  0.980 ,  1 ,  "D" ,  0.989 ,  1 ,  "D" ,  0.950 ,  1 ,  "D" ),("M06/aug-cc-pVTZ" ,  0.984 ,  1 ,  "R" ,  0.998 ,  1 ,  "R" ,  0.958 ,  1 ,  "R" ),("M06/def2-TZVP" ,  0.982 ,  3 ,  "R" ,  0.996 ,  3 ,  "R" ,  0.956 ,  3 ,  "R" ),("M06/def2-TZVPP" ,  0.979 ,  1 ,  "D" ,  0.992 ,  1 ,  "D" ,  0.953 ,  1 ,  "D" ),("M06/ma-TZVP" ,  0.982 ,  1 ,  "R" ,  0.996 ,  1 ,  "R" ,  0.956 ,  1 ,  "R" ),("M06/maug-cc-pVTZ" ,  0.982 ,  1 ,  "R" ,  0.996 ,  1 ,  "R" ,  0.956 ,  1 ,  "R" ),("M06/MG3S" ,  0.981 ,  1 ,  "D" ,  0.994 ,  1 ,  "D" ,  0.955 ,  1 ,  "D" ),("M06-2X/6-31+G(d,p)" ,  0.967 ,  1 ,  "D" ,  0.979 ,  1 ,  "D" ,  0.940 ,  1 ,  "D" ),("M06-2X/6-311+G(d,p)" ,  0.970 ,  5 ,  "D" ,  0.983 ,  5 ,  "R" ,  0.944 ,  5 ,  "R" ),("M06-2X/6-311++G(d,p)" ,  0.970 ,  5 ,  "D" ,  0.983 ,  5 ,  "R" ,  0.944 ,  5 ,  "R" ),("M06-2X/aug-cc-pVTZ" ,  0.971 ,  1 ,  "D" ,  0.985 ,  1 ,  "D" ,  0.946 ,  1 ,  "D" ),("M06-2X/def2-TZVP" ,  0.971 ,  7 ,  "D" ,  0.984 ,  7 ,  "D" ,  0.946 ,  7 ,  "D" ),("M06-2X/def2-QZVP" ,  0.970 ,  7 ,  "D" ,  0.983 ,  7 ,  "D" ,  0.945 ,  7 ,  "D" ),("M06-2X/def2-TZVPP" ,  0.970 ,  1 ,  "D" ,  0.983 ,  1 ,  "D" ,  0.945 ,  1 ,  "D" ),("M06-2X/ma-TZVP" ,  0.972 ,  1 ,  "R" ,  0.986 ,  1 ,  "R" ,  0.947 ,  1 ,  "R" ),("M06/maug-cc-pVTZ" , 0.971 ,  1 ,  "D" ,  0.984 ,  1 ,  "D" ,  0.945 ,  1 ,  "D" ),("M06-2X/MG3S" ,  0.970 ,  1 ,  "D" ,  0.982 ,  1 ,  "D" ,  0.944 ,  1 ,  "D" ),("M06-HF/6-31+G(d,p)" ,  0.954 ,  1 ,  "D" ,  0.969 ,  1 ,  "D" ,  0.931 ,  1 ,  "D" ),("M06-HF/aug-cc-pVTZ" ,  0.961 ,  1 ,  "R" ,  0.974 ,  1 ,  "R" ,  0.936 ,  1 ,  "R" ),("M06-HF/def2-TZVPP" ,  0.958 ,  1 ,  "D" ,  0.970 ,  1 ,  "D" ,  0.932 ,  1 ,  "D" ),("M06-HF/ma-TZVP" ,  0.957 ,  1 ,  "R" ,  0.970 ,  1 ,  "R" ,  0.932 ,  1 ,  "R" ),("M06-HF/maug-cc-pVTZ" ,  0.959 ,  1 ,  "R" ,  0.972 ,  1 ,  "R" ,  0.934 ,  1 ,  "R" ),("M06-HF/MG3S" ,  0.955 ,  1 ,  "D" ,  0.967 ,  1 ,  "D" ,  0.930 ,  1 ,  "D" ),("M06-L/6-31+G(d,p)" ,  0.978 ,  1 ,  "D" ,  0.992 ,  1 ,  "D" ,  0.953 ,  1 ,  "D" ),("M06-L/aug-cc-pVTZ" ,  0.980 ,  1 ,  "R" ,  0.994 ,  1 ,  "R" ,  0.955 ,  1 ,  "R" ),("M06-L(DKH2)/aug-cc-pwcVTZ-DK" ,  0.985 ,  1 ,  "D" ,  0.999 ,  1 ,  "R" ,  0.959 ,  1 ,  "R" ),("M06-L/def2-TZVP" ,  0.976 ,  3 ,  "R" ,  0.990 ,  3 ,  "R" ,  0.951 ,  3 ,  "R" ),("M06-L/def2-TZVPP" ,  0.976 ,  1 ,  "D" ,  0.995 ,  1 ,  "D" ,  0.956 ,  1 ,  "D" ),("M06-L/ma-TZVP" ,  0.977 ,  1 ,  "R" ,  0.991 ,  1 ,  "R" ,  0.952 ,  1 ,  "R" ),("M06-L/maug-cc-pVTZ" ,  0.977 ,  1 ,  "R" ,  0.991 ,  1 ,  "R" ,  0.952 ,  1 ,  "R" ),("M06-L/MG3S" ,  0.978 ,  1 ,  "D" ,  0.996 ,  1 ,  "D" ,  0.958 ,  1 ,  "D" ),("M08-HX/6-31+G(d,p)" ,  0.972 ,  1 ,  "D" ,  0.983 ,  1 ,  "D" ,  0.944 ,  1 ,  "D" ),("M08-HX/aug-cc-pVTZ" ,  0.975 ,  1 ,  "R" ,  0.989 ,  1 ,  "R" ,  0.950 ,  1 ,  "R" ),("M08-HX/cc-pVTZ+" ,  0.974 ,  1 ,  "D" ,  0.985 ,  1 ,  "D" ,  0.946 ,  1 ,  "D" ),("M08-HX/def2-TZVPP" ,  0.973 ,  1 ,  "D" ,  0.984 ,  1 ,  "D" ,  0.945 ,  1 ,  "D" ),("M08-HX/jun-cc-pVTZ" ,  0.974 ,  6 ,  "D" ,  0.986 ,  6 ,  "D" ,  0.947 ,  6 ,  "D" ),("M08-HX/ma-TZVP" ,  0.976 ,  1 ,  "R" ,  0.990 ,  1 ,  "R" ,  0.951 ,  1 ,  "R" ),("M08-HX/maug-cc-pVTZ" ,  0.976 ,  1 ,  "R" ,  0.990 ,  1 ,  "R" ,  0.951 ,  1 ,  "R" ),("M08-HX/MG3S" ,  0.973 ,  1 ,  "D" ,  0.984 ,  1 ,  "D" ,  0.946 ,  1 ,  "D" ),("M08-SO/6-31+G(d,p)" ,  0.979 ,  1 ,  "D" ,  0.989 ,  1 ,  "D" ,  0.951 ,  1 ,  "D" ),("M08-SO/aug-cc-pVTZ" ,  0.985 ,  1 ,  "R" ,  0.999 ,  1 ,  "R" ,  0.959 ,  1 ,  "R" ),("M08-SO/cc-pVTZ+" ,  0.982 ,  1 ,  "D" ,  0.995 ,  1 ,  "D" ,  0.956 ,  1 ,  "D" ),("M08-SO/def2-TZVPP" ,  0.980 ,  1 ,  "D" ,  0.993 ,  1 ,  "D" ,  0.954 ,  1 ,  "D" ),("M08-SO/ma-TZVP" ,  0.984 ,  1 ,  "R" ,  0.998 ,  1 ,  "R" ,  0.958 ,  1 ,  "R" ),("M08-SO/maug-cc-pVTZ" ,  0.983 ,  1 ,  "R" ,  0.997 ,  1 ,  "R" ,  0.957 ,  1 ,  "R" ),("M08-SO/MG3" ,  0.984 ,  4 ,  "D" ,  0.998 ,  4 ,  "R" ,  0.959 ,  4 ,  "R" ),("M08-SO/MG3S" ,  0.983 ,  1 ,  "D" ,  0.995 ,  1 ,  "D" ,  0.956 ,  1 ,  "D" ),("M08-SO/MG3SXP" ,  0.984 ,  1 ,  "D" ,  0.996 ,  1 ,  "D" ,  0.957 ,  1 ,  "D" ),("MN12-L/MG3S" ,  0.968 ,  6 ,  "D" ,  0.981 ,  6 ,  "D" ,  0.943 ,  6 ,  "D" ),("MN12-SX/6-311++G(d,p)" ,  0.976 ,  6 ,  "D" ,  0.986 ,  6 ,  "D" ,  0.947 ,  6 ,  "D" ),("MN15-L/MG3S" ,  0.977 ,  1 ,  "D" ,  0.991 ,  1 ,  "R" ,  0.952 ,  1 ,  "R" ),("MN15-L/maug-cc-pVTZ" ,  0.979 ,  1 ,  "D" ,  0.993 ,  1 ,  "R" ,  0.954 ,  1 ,  "R" ),("MC3BB" ,  0.965 ,  1 ,  "C" ,  0.979 ,  1 ,  "R" ,  0.940 ,  1 ,  "R" ),("MC3MPW" ,  0.964 ,  1 ,  "C" ,  0.977 ,  1 ,  "R" ,  0.939 ,  1 ,  "R" ),("MC-QCISD/3" ,  0.992 ,  1 ,  "C" ,  1.006 ,  1 ,  "R" ,  0.966 ,  1 ,  "R" ),("MOHLYP/ma-TZVP" ,  1.027 ,  1 ,  "R" ,  1.041 ,  1 ,  "R" ,  1.000 ,  1 ,  "R" ),("MOHLYP/MG3S" ,  1.022 ,  1 ,  "R" ,  1.036 ,  1 ,  "R" ,  0.995 ,  1 ,  "R" ),("MP2(FC)/6-31+G(d,p)" , 0.968 ,  1 ,  "C" ,  0.982 ,  1 ,  "R" ,  0.943 ,  1 ,  "R" ),("MP2(FC)/6-311G(d,p)" , 0.970 ,  1 ,  "R" ,  0.984 ,  1 ,  "R" ,  0.945 ,  1 ,  "R" ),("MP2(FC)/6-31G(d)" ,  0.964 ,  1 ,  "R" ,  0.977 ,  1 ,  "R" ,  0.939 ,  1 ,  "R" ),("MP2(FC)/6-31G(d,p)" , 0.958 ,  1 ,  "R" ,  0.971 ,  1 ,  "R" ,  0.933 ,  1 ,  "R" ),("MP2(FC)/cc-pVDZ" ,  0.977 ,  1 ,  "C" ,  0.991 ,  1 ,  "R" ,  0.952 ,  1 ,  "R" ),("MP2(FULL)/6-31G(d)" ,  0.963 ,  1 ,  "R" ,  0.976 ,  1 ,  "R" ,  0.938 ,  1 ,  "R" ),("MP4(SDQ)/jul-cc-pVTZ", 0.973 ,  1 ,  "R" ,  0.987 ,  1 ,  "R" ,  0.948 ,  1 ,  "R" ),("MPW1B95/6-31+G(d,p)" ,  0.970 ,  1 ,  "C" ,  0.984 ,  1 ,  "R" ,  0.945 ,  1 ,  "R" ),("MPW1B95/MG3" ,  0.970 ,  1 ,  "C" ,  0.984 ,  1 ,  "R" ,  0.945 ,  1 ,  "R" ),("MPW1B95/MG3S" ,  0.972 ,  1 ,  "C" ,  0.986 ,  1 ,  "R" ,  0.947 ,  1 ,  "R" ),("MPW1K/6-31+G(d,p)" ,  0.949 ,  1 ,  "C" ,  0.962 ,  1 ,  "R" ,  0.924 ,  1 ,  "R" ),("MPW1K/ma-TZVP" ,  0.956 ,  1 ,  "R" ,  0.969 ,  1 ,  "R" ,  0.931 ,  1 ,  "R" ),("MPW1K/MG3" ,  0.953 ,  1 ,  "C" ,  0.966 ,  1 ,  "R" ,  0.928 ,  1 ,  "R" ),("MPW1K/MG3S" ,  0.956 ,  1 ,  "C" ,  0.969 ,  1 ,  "R" ,  0.931 ,  1 ,  "R" ),("MPW1K/MIDI!" ,  0.953 ,  1 ,  "R" ,  0.966 ,  1 ,  "R" ,  0.928 ,  1 ,  "R" ),("MPW1K/MIDIY" ,  0.947 ,  1 ,  "R" ,  0.960 ,  1 ,  "R" ,  0.922 ,  1 ,  "R" ),("MPW3LYP/6-31+G(d,p)" ,  0.980 ,  1 ,  "C" ,  0.994 ,  1 ,  "R" ,  0.955 ,  1 ,  "R" ),("MPW3LYP/6-311+G(2d,p)" ,  0.986 ,  1 ,  "R" ,  1.000 ,  1 ,  "R" ,  0.960 ,  1 ,  "R" ),("MPW3LYP/6-31G(d)" ,  0.976 ,  1 ,  "R" ,  0.990 ,  1 ,  "R" ,  0.951 ,  1 ,  "R" ),("MPW3LYP/ma-TZVP" ,  0.986 ,  1 ,  "R" ,  1.000 ,  1 ,  "R" ,  0.960 ,  1 ,  "R" ),("MPW3LYP/MG3S" ,  0.982 ,  1 ,  "C" ,  0.996 ,  1 ,  "R" ,  0.956 ,  1 ,  "R" ),("MPW74/6-311+G(d,p)" ,  0.912 ,  2 ,  "C" ,  0.888 ,  2 ,  "R" ,  0.925 ,  2 ,  "R" ),("MPW76/MG3S" ,  0.909 ,  2 ,  "C" ,  0.885 ,  2 ,  "R" ,  0.922 ,  2 ,  "R" ),("MPWB1K/6-31+G(d,p)" ,  0.951 ,  1 ,  "C" ,  0.964 ,  1 ,  "R" ,  0.926 ,  1 ,  "R" ),("MPWB1K/MG3S" ,  0.954 ,  1 ,  "C" ,  0.967 ,  1 ,  "R" ,  0.929 ,  1 ,  "R" ),("MPWLYP1M/ma-TZVP" ,  1.009 ,  1 ,  "R" ,  1.023 ,  1 ,  "R" ,  0.983 ,  1 ,  "R" ),("OreLYP/ma-TZVP" ,  1.010 ,  7 ,  "D" ,  1.024 ,  7 ,  "D" ,  0.984 ,  7 ,  "D" ),("OreLYP/def2-TZVP" ,  1.008 ,  7 ,  "D" ,  1.023 ,  7 ,  "D" ,  0.982 ,  7 ,  "D" ),("PBE/def2-TZVP" ,  1.011 ,  3 ,  "R" ,  1.026 ,  3 ,  "R" ,  0.985 ,  3 ,  "R" ),("PBE/MG3S" ,  1.010 ,  1 ,  "D" ,  1.025 ,  1 ,  "D" ,  0.985 ,  1 ,  "D" ),("PBE/ma-TZVP" ,  1.014 ,  1 ,  "D" ,  1.028 ,  1 ,  "D" ,  0.987 ,  1 ,  "D" ),("PBE0/MG3S" ,  0.975 ,  1 ,  "D" ,  0.989 ,  1 ,  "D" ,  0.950 ,  1 ,  "D" ),("PBE1KCIS/MG3" ,  0.981 ,  1 ,  "C" ,  0.995 ,  1 ,  "R" ,  0.955 ,  1 ,  "R" ),("PBE1KCIS/MG3S" ,  0.981 ,  1 ,  "C" ,  0.995 ,  1 ,  "R" ,  0.955 ,  1 ,  "R" ),("PM3" ,  0.940 ,  1 ,  "R" ,  0.953 ,  1 ,  "R" ,  0.916 ,  1 ,  "R" ),("PM6" ,  1.078 ,  1 ,  "R" ,  1.093 ,  1 ,  "R" ,  1.050 ,  1 ,  "R" ),("PW6B95/6-31+G(d,p)" ,  0.970 ,  1 ,  "C" ,  0.984 ,  1 ,  "R" ,  0.945 ,  1 ,  "R" ),("QCISD(FC)/6-31G(d)" ,  0.973 ,  1 ,  "R" ,  0.987 ,  1 ,  "R" ,  0.948 ,  1 ,  "R" ),("revTPSS/def2-TZVP" ,  0.998 ,  7 ,  "D" ,  1.012 ,  7 ,  "D" ,  0.972 ,  7 ,  "D" ),("revTPSS/ma-TZVP" ,  0.999 ,  7 ,  "D" ,  1.013 ,  7 ,  "D" ,  0.973 ,  7 ,  "D" ),("SOGGA/ma-TZVP" ,  1.017 ,  1 ,  "R" ,  1.031 ,  1 ,  "R" ,  0.991 ,  1 ,  "R" ),("tHCTHhyb/ma-TZVP" ,  0.989 ,  1 ,  "R" ,  1.003 ,  1 ,  "R" ,  0.963 ,  1 ,  "R" ),("TPSS1KCIS/def2-TZVP" ,  0.982 ,  1 ,  "R" ,  0.996 ,  1 ,  "R" ,  0.956 ,  1 ,  "R" ),("TPSS1KCIS/ma-TZVP" ,  0.983 ,  1 ,  "R" ,  0.997 ,  1 ,  "R" ,  0.957 ,  1 ,  "R" ),("TPSSh/MG3S" ,  0.984 ,  1 ,  "D" ,  1.002 ,  1 ,  "D" ,  0.963 ,  1 ,  "D" ),("VSXC/MG3S" ,  0.986 ,  1 ,  "D" ,  1.001 ,  1 ,  "D" ,  0.962 ,  1 ,  "D" ),("wB97/def2-TZVP", 0.969 ,  1 ,  "R" ,  0.983 ,  1 ,  "R" ,  0.944 ,  1 ,  "R" ),("wB97/ma-TZVP", 0.970 ,  1 ,  "R" ,  0.984 ,  1 ,  "R" ,  0.945 ,  1 ,  "R" ),("wB97X/def2-TZVP", 0.970 ,  1 ,  "R" ,  0.984 ,  1 ,  "R" ,  0.945 ,  1 ,  "R" ),("wB97X/ma-TZVP" , 0.971 ,  1 ,  "R" ,  0.985 ,  1 ,  "R" ,  0.946 ,  1 ,  "R" ),("wB97X-D/def2-TZVP", 0.975 ,  1 ,  "R" ,  0.989 ,  1 ,  "R" ,  0.950 ,  1 ,  "R" ),("wB97X-D/ma-TZVP", 0.975 ,  1 ,  "R" ,  0.989 ,  1 ,  "R" ,  0.950 ,  1 ,  "R" ),("wB97X-D/maug-cc-pVTZ", 0.974 ,  1 ,  "R" ,  0.988 ,  1 ,  "R" ,  0.949 ,  1 ,  "R" ),("X1B95/6-31+G(d,p)" ,  0.968 ,  1 ,  "C" ,  0.982 ,  1 ,  "R" ,  0.943 ,  1 ,  "R" ),("X1B95/MG3S" ,  0.971 ,  1 ,  "C" ,  0.985 ,  1 ,  "R" ,  0.946 ,  1 ,  "R" ),("XB1K/6-31+G(d,p)" ,  0.952 ,  1 ,  "C" ,  0.965 ,  1 ,  "R" ,  0.927 ,  1 ,  "R" ),("XB1K/MG3S" ,  0.955 ,  1 ,  "C" ,  0.968 ,  1 ,  "R" ,  0.930 ,  1 ,  "R" )],dtype=[('level', Str_char%30),('zpe_fac', 'f4'), ('zpe_ref', 'i4'), ('zpe_meth', Str_char%1), ('harm_fac', 'f4'), ('harm_ref', 'i4'), ('harm_meth', Str_char%1), ('fund_fac', 'f4'), ('fund_ref', 'i4'), ('fund_meth', Str_char%1)])


from itertools import product as product
from math import ceil

import matplotlib.pyplot as plt
import numpy as np
import torch

from utils.config import cfg_mnet

#-----------------------------#
#   中心解码，宽高解码
#-----------------------------#
def decode(loc, priors, variances):
    boxes = torch.cat((priors[:, :2] + loc[:, :2] * variances[0] * priors[:, 2:],
                    priors[:, 2:] * torch.exp(loc[:, 2:] * variances[1])), 1)
    boxes[:, :2] -= boxes[:, 2:] / 2
    boxes[:, 2:] += boxes[:, :2]
    return boxes

#-----------------------------#
#   关键点解码
#-----------------------------#
def decode_landm(pre, priors, variances):
    landms = torch.cat((priors[:, :2] + pre[:, :2] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 2:4] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 4:6] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 6:8] * variances[0] * priors[:, 2:],
                        priors[:, :2] + pre[:, 8:10] * variances[0] * priors[:, 2:],
                        ), dim=1)
    return landms

class Anchors(object):
    def __init__(self, cfg, image_size=None):
        super(Anchors, self).__init__()
        self.min_sizes  = cfg['min_sizes']
        self.steps      = cfg['steps']
        self.clip       = cfg['clip']
        #---------------------------#
        #   图片的尺寸
        #---------------------------#
        self.image_size = image_size
        #---------------------------#
        #   三个有效特征层高和宽
        #---------------------------#
        self.feature_maps = [[ceil(self.image_size[0]/step), ceil(self.image_size[1]/step)] for step in self.steps]

    def get_anchors(self):
        anchors = []
        for k, f in enumerate(self.feature_maps):
            min_sizes = self.min_sizes[k]
            #-----------------------------------------#
            #   对特征层的高和宽进行循环迭代
            #-----------------------------------------#
            for i, j in product(range(f[0]), range(f[1])):
                for min_size in min_sizes:
                    s_kx = min_size / self.image_size[1]
                    s_ky = min_size / self.image_size[0]
                    dense_cx = [x * self.steps[k] / self.image_size[1] for x in [j + 0.5]]
                    dense_cy = [y * self.steps[k] / self.image_size[0] for y in [i + 0.5]]
                    for cy, cx in product(dense_cy, dense_cx):
                        anchors += [cx, cy, s_kx, s_ky]

        output = torch.Tensor(anchors).view(-1, 4)
        output  = np.zeros_like(anchors[:, :4])
        output[:,0] = anchors[:,0] - anchors[:,2] / 2
        output[:,1] = anchors[:,1] - anchors[:,3] / 2
        output[:,2] = anchors[:,0] + anchors[:,2] / 2
        output[:,3] = anchors[:,1] + anchors[:,3] / 2

        if self.clip:
            output = np.clip(output, 0, 1)
        return output

if __name__ == "__main__":
    cfg_mnet['image_size'] = 640
    #--------------------------------#
    #   先验框的生成
    #--------------------------------#
    cfg     = cfg_mnet
    anchors = Anchors(cfg, image_size = (cfg_mnet['image_size'], cfg_mnet['image_size'])).get_anchors()
    anchors = anchors[-800:] * cfg_mnet['image_size']

    #--------------------------------#
    #   先验框中心绘制
    #--------------------------------#
    center_x = (anchors[:, 0] + anchors[:, 2]) / 2
    center_y = (anchors[:, 1] + anchors[:, 3]) / 2

    fig = plt.figure()
    ax  = fig.add_subplot(121)
    plt.ylim(-300,900)
    plt.xlim(-300,900)
    ax.invert_yaxis()
    plt.scatter(center_x,center_y)

    #--------------------------------#
    #   先验框宽高绘制
    #--------------------------------#
    box_widths  = anchors[0:2,2] - anchors[0:2,0]
    box_heights = anchors[0:2,3] - anchors[0:2,1]
    for i in [0,1]:
        rect = plt.Rectangle([anchors[i, 0], anchors[i, 1]], box_widths[i], box_heights[i], color="r", fill=False)
        ax.add_patch(rect)

    #--------------------------------#
    #   先验框中心绘制
    #--------------------------------#
    ax = fig.add_subplot(122)
    plt.ylim(-300,900)
    plt.xlim(-300,900)
    ax.invert_yaxis()  #y轴反向
    plt.scatter(center_x,center_y)

    #--------------------------------#
    #   对先验框调整获得预测框
    #--------------------------------#
    mbox_loc = np.random.randn(800, 4)
    mbox_ldm = np.random.randn(800, 10)

    anchors[:, :2] = (anchors[:, :2] + anchors[:, 2:]) / 2
    anchors[:, 2:] = (anchors[:, 2:] - anchors[:, :2]) * 2

    mbox_loc                = torch.Tensor(mbox_loc)
    anchors                 = torch.Tensor(anchors)
    cfg_mnet['variance']    = torch.Tensor(cfg_mnet['variance'])
    decode_bbox             = decode(mbox_loc, anchors, cfg_mnet['variance'])

    box_widths  = decode_bbox[0: 2, 2] - decode_bbox[0: 2, 0]
    box_heights = decode_bbox[0: 2, 3] - decode_bbox[0: 2, 1]

    for i in [0,1]:
        rect = plt.Rectangle([decode_bbox[i, 0], decode_bbox[i, 1]], box_widths[i], box_heights[i], color="r", fill=False)
        plt.scatter((decode_bbox[i, 2] + decode_bbox[i, 0]) / 2, (decode_bbox[i,3] + decode_bbox[i,1]) / 2, color="b")
        ax.add_patch(rect)

    plt.show()


# Licensed under a 3-clause BSD style license - see LICENSE.rst
import numpy as np
import astropy.units as u
from gammapy.maps import Map, MapCoord, WcsGeom, MapAxes
from gammapy.modeling.models import PowerLawSpectralModel
from gammapy.utils.random import InverseCDFSampler, get_random_state
from gammapy.utils.gauss import Gauss2DPDF
from .kernel import PSFKernel
from .table import EnergyDependentTablePSF
from ..core import IRFMap

__all__ = ["PSFMap"]

class PSFMap(IRFMap):
    """Class containing the Map of PSFs and allowing to interact with it.

    Parameters
    ----------
    psf_map : `~gammapy.maps.Map`
        the input PSF Map. Should be a Map with 2 non spatial axes.
        rad and true energy axes should be given in this specific order.
    exposure_map : `~gammapy.maps.Map`
        Associated exposure map. Needs to have a consistent map geometry.

    Examples
    --------
    ::

        import numpy as np
        from astropy import units as u
        from astropy.coordinates import SkyCoord
        from gammapy.maps import Map, WcsGeom, MapAxis
        from gammapy.irf import EnergyDependentMultiGaussPSF, EffectiveAreaTable2D
        from gammapy.makers.utils import make_psf_map, PSFMap, make_map_exposure_true_energy

        # Define energy axis. Note that the name is fixed.
        energy_axis = MapAxis.from_edges(np.logspace(-1., 1., 4), unit='TeV', name='energy')
        # Define rad axis. Again note the axis name
        rads = np.linspace(0., 0.5, 100) * u.deg
        rad_axis = MapAxis.from_edges(rads, unit='deg', name='theta')

        # Define parameters
        pointing = SkyCoord(0., 0., unit='deg')
        max_offset = 4 * u.deg

        # Create WcsGeom
        geom = WcsGeom.create(binsz=0.25*u.deg, width=10*u.deg, skydir=pointing, axes=[rad_axis, energy_axis])

        # Extract EnergyDependentTablePSF from CTA 1DC IRF
        filename = '$GAMMAPY_DATA/cta-1dc/caldb/data/cta/1dc/bcf/South_z20_50h/irf_file.fits'
        psf = EnergyDependentMultiGaussPSF.read(filename, hdu='POINT SPREAD FUNCTION')
        psf3d = psf.to_psf3d(rads)
        aeff2d = EffectiveAreaTable2D.read(filename, hdu='EFFECTIVE AREA')

        # Create the exposure map
        exposure_geom = geom.to_image().to_cube([energy_axis])
        exposure_map = make_map_exposure_true_energy(pointing, "1 h", aeff2d, exposure_geom)

        # create the PSFMap for the specified pointing
        psf_map = make_psf_map(psf3d, pointing, geom, max_offset, exposure_map)

        # Get an EnergyDependentTablePSF at any position in the image
        psf_table = psf_map.get_energy_dependent_table_psf(SkyCoord(2., 2.5, unit='deg'))

        # Write map to disk
        psf_map.write('psf_map.fits')
    """
    tag = "psf_map"
    required_axes = ["rad", "energy_true"]

    def __init__(self, psf_map, exposure_map=None):
        super().__init__(irf_map=psf_map, exposure_map=exposure_map)

    @property
    def psf_map(self):
        return self._irf_map

    @psf_map.setter
    def psf_map(self, value):
        self._irf_map = value

    def get_energy_dependent_table_psf(self, position=None):
        """Get energy-dependent PSF at a given position.

        By default the PSF at the center of the map is returned.

        Parameters
        ----------
        position : `~astropy.coordinates.SkyCoord`
            the target position. Should be a single coordinates

        Returns
        -------
        psf_table : `~gammapy.irf.EnergyDependentTablePSF`
            the table PSF
        """
        if position is None:
            position = self.psf_map.geom.center_skydir

        if position.size != 1:
            raise ValueError(
                "EnergyDependentTablePSF can be extracted at one single position only."
            )

        energy = self.psf_map.geom.axes["energy_true"].center
        rad = self.psf_map.geom.axes["rad"].center

        coords = {
            "skycoord": position,
            "energy_true": energy.reshape((-1, 1, 1, 1)),
            "rad": rad.reshape((1, -1, 1, 1)),
        }

        data = self.psf_map.interp_by_coord(coords)
        psf_values = u.Quantity(data[:, :, 0, 0], unit=self.psf_map.unit, copy=False)

        if self.exposure_map is not None:
            coords = {
                "skycoord": position,
                "energy_true": energy.reshape((-1, 1, 1)),
                "rad": 0 * u.deg,
            }
            data = self.exposure_map.interp_by_coord(coords).squeeze()
            exposure = data * self.exposure_map.unit
        else:
            exposure = None

        # Beware. Need to revert rad and energies to follow the TablePSF scheme.
        return EnergyDependentTablePSF(
            axes=self.psf_map.geom.axes[["energy_true", "rad"]],
            data=psf_values.value,
            unit=psf_values.unit,
            exposure=exposure,
        )

    def get_psf_kernel(self, position, geom, max_radius=None, factor=4):
        """Returns a PSF kernel at the given position.

        The PSF is returned in the form a WcsNDMap defined by the input Geom.

        Parameters
        ----------
        position : `~astropy.coordinates.SkyCoord`
            the target position. Should be a single coordinate
        geom : `~gammapy.maps.Geom`
            the target geometry to use
        max_radius : `~astropy.coordinates.Angle`
            maximum angular size of the kernel map
        factor : int
            oversampling factor to compute the PSF

        Returns
        -------
        kernel : `~gammapy.irf.PSFKernel`
            the resulting kernel
        """
        if position is None:
            position = self.psf_map.geom.center_skydir

        table_psf = self.get_energy_dependent_table_psf(position)

        if max_radius is None:
            max_radius = np.max(table_psf.axes["rad"].center)
            min_radius_geom = np.min(geom.width) / 2.0
            max_radius = min(max_radius, min_radius_geom)

        return PSFKernel.from_table_psf(table_psf, geom, max_radius, factor)

    def containment_radius_map(self, energy, fraction=0.68):
        """Containment radius map.

        Parameters
        ----------
        energy : `~astropy.units.Quantity`
            Scalar energy at which to compute the containment radius
        fraction : float
            the containment fraction (range: 0 to 1)

        Returns
        -------
        containment_radius_map : `~gammapy.maps.Map`
            Containment radius map
        """
        coords = self.psf_map.geom.to_image().get_coord().skycoord.flatten()
        m = Map.from_geom(self.psf_map.geom.to_image(), unit="deg")

        for coord in coords:
            psf_table = self.get_energy_dependent_table_psf(coord)
            containment_radius = psf_table.containment_radius(
                energy_true=energy, fraction=fraction
            )
            m.fill_by_coord(coord, np.atleast_1d(containment_radius))

        return m

    @classmethod
    def from_geom(cls, geom):
        """Create psf map from geom.

        Parameters
        ----------
        geom : `Geom`
            PSF map geometry.

        Returns
        -------
        psf_map : `PSFMap`
            Point spread function map.
        """
        geom_exposure_psf = geom.squash(axis_name="rad")
        exposure_psf = Map.from_geom(geom_exposure_psf, unit="m2 s")
        psf_map = Map.from_geom(geom, unit="sr-1")
        return cls(psf_map, exposure_psf)

    def sample_coord(self, map_coord, random_state=0):
        """Apply PSF corrections on the coordinates of a set of simulated events.

        Parameters
        ----------
        map_coord : `~gammapy.maps.MapCoord` object.
            Sequence of coordinates and energies of sampled events.
        random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
            Defines random number generator initialisation.
            Passed to `~gammapy.utils.random.get_random_state`.

        Returns
        -------
        corr_coord : `~gammapy.maps.MapCoord` object.
            Sequence of PSF-corrected coordinates of the input map_coord map.
        """

        random_state = get_random_state(random_state)
        rad_axis = self.psf_map.geom.axes["rad"]

        coord = {
            "skycoord": map_coord.skycoord.reshape(-1, 1),
            "energy_true": map_coord["energy_true"].reshape(-1, 1),
            "rad": rad_axis.center,
        }

        pdf = (
            self.psf_map.interp_by_coord(coord)
            * rad_axis.center.value
            * rad_axis.bin_width.value
        )

        sample_pdf = InverseCDFSampler(pdf, axis=1, random_state=random_state)
        pix_coord = sample_pdf.sample_axis()
        separation = rad_axis.pix_to_coord(pix_coord)

        position_angle = random_state.uniform(360, size=len(map_coord.lon)) * u.deg

        event_positions = map_coord.skycoord.directional_offset_by(
            position_angle=position_angle, separation=separation
        )
        return MapCoord.create(
            {"skycoord": event_positions, "energy_true": map_coord["energy_true"]}
        )

    @classmethod
    def from_energy_dependent_table_psf(cls, table_psf, geom=None):
        """Create PSF map from table PSF object.

        Helper function to create an allsky PSF map from
        table PSF, which does not depend on position.

        Parameters
        ----------
        table_psf : `EnergyDependentTablePSF`
            Table PSF

        Returns
        -------
        psf_map : `PSFMap`
            Point spread function map.
        """
        if geom is None:
            geom = WcsGeom.create(
                npix=(2, 1),
                proj="CAR",
                binsz=180,
                axes=table_psf.axes.reverse,
            )
        coords = geom.get_coord()

        data = table_psf.evaluate(
            energy_true=coords["energy_true"], rad=coords["rad"]
        )
        psf_map = Map.from_geom(geom, data=data.to_value("sr-1"), unit="sr-1")

        geom_exposure = geom.squash(axis_name="rad")

        exposure_map = Map.from_geom(geom_exposure, unit="cm2 s", data=1)
        data = table_psf.exposure
        exposure_map.quantity = exposure_map.data * data.reshape((-1, 1, 1, 1))
        return cls(psf_map=psf_map, exposure_map=exposure_map)

    @classmethod
    def from_gauss(cls, energy_axis_true, rad_axis=None, sigma=0.1 * u.deg):
        """Create all -sky PSF map from Gaussian width.

        This is used for testing and examples.

        The width can be the same for all energies
        or be an array with one value per energy node.
        It does not depend on position.

        Parameters
        ----------
        energy_axis_true : `~gammapy.maps.MapAxis`
            True energy axis.
        rad_axis : `~gammapy.maps.MapAxis`
            Offset angle wrt source position axis.
        sigma : `~astropy.coordinates.Angle`
            Gaussian width.

        Returns
        -------
        psf_map : `PSFMap`
            Point spread function map.
        """
        from gammapy.datasets.map import RAD_AXIS_DEFAULT

        if rad_axis is None:
            rad_axis = RAD_AXIS_DEFAULT.copy()

        axes = MapAxes([energy_axis_true, rad_axis])
        coords = axes.get_coord()

        sigma = np.broadcast_to(u.Quantity(sigma), energy_axis_true.nbin, subok=True)
        gauss = Gauss2DPDF(sigma=sigma.reshape((-1, 1)))
        data = gauss(coords["rad"])

        table_psf = EnergyDependentTablePSF(
            axes=axes, unit=data.unit, data=data.value
        )
        return cls.from_energy_dependent_table_psf(table_psf)

    def to_image(self, spectrum=None, keepdims=True):
        """Reduce to a 2-D map after weighing
        with the associated exposure and a spectrum

        Parameters
        ----------
        spectrum : `~gammapy.modeling.models.SpectralModel`, optional
            Spectral model to compute the weights.
            Default is power-law with spectral index of 2.
        keepdims : bool, optional
            If True, the energy axis is kept with one bin.
            If False, the axis is removed

        Returns
        -------
        psf_out : `PSFMap`
            `PSFMap` with the energy axis summed over
        """
        from gammapy.makers.utils import _map_spectrum_weight

        if spectrum is None:
            spectrum = PowerLawSpectralModel(index=2.0)

        exp_weighed = _map_spectrum_weight(self.exposure_map, spectrum)
        exposure = exp_weighed.sum_over_axes(
            axes_names=["energy_true"], keepdims=keepdims
        )

        psf_data = exp_weighed.data * self.psf_map.data / exposure.data
        psf_map = Map.from_geom(geom=self.psf_map.geom, data=psf_data, unit="sr-1")

        psf = psf_map.sum_over_axes(axes_names=["energy_true"], keepdims=keepdims)
        return self.__class__(psf_map=psf, exposure_map=exposure)

import numpy,import astropy,from astropy