# Licensed under a 3-clause BSD style license - see LICENSE.rst
import logging
from pathlib import Path
import numpy as np
from astropy import units as u
from astropy.io import fits
from astropy.table import Table
from gammapy.data import GTI
from gammapy.irf import EDispKernel, EDispKernelMap
from gammapy.maps import RegionNDMap
from gammapy.stats import WStatCountsStatistic, cash, get_wstat_mu_bkg, wstat
from gammapy.utils.random import get_random_state
from gammapy.utils.scripts import make_name, make_path
from .map import MapDataset
from .utils import get_axes, get_figure

__all__ = ["SpectrumDatasetOnOff", "SpectrumDataset"]

log = logging.getLogger(__name__)

class SpectrumDataset(MapDataset):
    """Spectrum dataset for likelihood fitting.

    The spectrum dataset bundles reduced counts data, with a spectral model,
    background model and instrument response function to compute the fit-statistic
    given the current model and data.

    Parameters
    ----------
    models : `~gammapy.modeling.models.Models`
        Fit model
    counts : `~gammapy.maps.RegionNDMap`
        Counts spectrum
    exposure : `~gammapy.maps.RegionNDMap`
        Effective area
    edisp : `~gammapy.irf.EDispKernelMap`
        Energy dispersion kernel.
    mask_safe : `~gammapy.maps.RegionNDMap`
        Mask defining the safe data range.
    mask_fit : `~gammapy.maps.RegionNDMap`
        Mask to apply to the likelihood for fitting.
    name : str
        Dataset name.
    gti : `~gammapy.data.GTI`
        GTI of the observation or union of GTI if it is a stacked observation
    meta_table : `~astropy.table.Table`
        Table listing informations on observations used to create the dataset.
        One line per observation for stacked datasets.

    See Also
    --------
    SpectrumDatasetOnOff, FluxPointsDataset, MapDataset
    """

    stat_type = "cash"
    tag = "SpectrumDataset"

    def __init__(
        self,
        models=None,
        counts=None,
        exposure=None,
        background=None,
        edisp=None,
        mask_safe=None,
        mask_fit=None,
        name=None,
        gti=None,
        meta_table=None,
    ):

        self._name = make_name(name)
        self._evaluators = {}

        if mask_fit is not None and mask_fit.dtype != np.dtype("bool"):
            raise ValueError("mask data must have dtype bool")

        self.counts = counts
        self.mask_fit = mask_fit
        self.exposure = exposure
        self.edisp = edisp
        self.background = background
        self.mask_safe = mask_safe
        self.gti = gti
        self.meta_table = meta_table
        self.models = models

    @property
    def psf(self):
        return None

    @property
    def mask_safe(self):
        if self._mask_safe is None:
            data = np.ones(self._geom.data_shape, dtype=bool)
            self._mask_safe = RegionNDMap.from_geom(self._geom, data=data)

        return self._mask_safe

    @mask_safe.setter
    def mask_safe(self, mask):
        if mask is None or isinstance(mask, RegionNDMap):
            self._mask_safe = mask
        else:
            raise ValueError(f"Must be `RegionNDMap` and not {type(mask)}")

    def stat_array(self):
        """Likelihood per bin given the current model parameters"""
        return cash(n_on=self.counts.data, mu_on=self.npred().data)

    def stat_sum(self):
        """Total statistic given the current model parameters."""
        stat = self.stat_array()

        if self.mask is not None:
            stat = stat[self.mask.data]

        return np.sum(stat, dtype=np.float64)

    def write(self):
        raise NotImplementedError

    def read(self):
        raise NotImplementedError

    def to_hdulist(self):
        raise NotImplementedError

    def from_hdulist(self):
        raise NotImplementedError

    def from_dict(self):
        raise NotImplementedError

    # TODO: decide what to about these "useless" methods
    def to_spectrum_dataset(self, *args, **kwargs):
        """Returns self"""
        return self

    def cutout(self, *args, **kwargs):
        """Returns self"""
        return self

    def pad(self, *args, **kwargs):
        """Returns self"""
        return self

    @property
    # TODO: make this a method to support different methods?
    def energy_range(self):
        """Energy range defined by the safe mask"""
        energy = self._geom.axes["energy"].edges
        energy_min, energy_max = energy[:-1], energy[1:]

        if self.mask_safe is not None:
            if self.mask_safe.data.any():
                energy_min = energy_min[self.mask_safe.data[:, 0, 0]]
                energy_max = energy_max[self.mask_safe.data[:, 0, 0]]
            else:
                return None, None

        return u.Quantity([energy_min.min(), energy_max.max()])

    def plot_fit(
        self,
        ax_spectrum=None,
        ax_residuals=None,
        kwargs_spectrum=None,
        kwargs_residuals=None,
    ):
        """Plot spectrum and residuals in two panels.

        Calls `~SpectrumDataset.plot_excess` and `~SpectrumDataset.plot_residuals`.

        Parameters
        ----------
        ax_spectrum : `~matplotlib.axes.Axes`
            Axes to plot spectrum on.
        ax_residuals : `~matplotlib.axes.Axes`
            Axes to plot residuals on.
        kwargs_spectrum : dict
            Keyword arguments passed to `~SpectrumDataset.plot_excess`.
        kwargs_residuals : dict
            Keyword arguments passed to `~SpectrumDataset.plot_residuals`.

        Returns
        -------
        ax_spectrum, ax_residuals : `~matplotlib.axes.Axes`
            Spectrum and residuals plots.
        """
        from matplotlib.gridspec import GridSpec

        gs = GridSpec(7, 1)
        ax_spectrum, ax_residuals = get_axes(
            ax_spectrum,
            ax_residuals,
            8,
            7,
            [gs[:5, :]],
            [gs[5:, :]],
            kwargs2={"sharex": ax_spectrum},
        )
        kwargs_spectrum = kwargs_spectrum or {}
        kwargs_residuals = kwargs_residuals or {}

        self.plot_excess(ax_spectrum, **kwargs_spectrum)
        ax_spectrum.label_outer()

        self.plot_residuals(ax_residuals, **kwargs_residuals)
        method = kwargs_residuals.get("method", "diff")
        label = self._residuals_labels[method]
        ax_residuals.set_ylabel(f"Residuals\n{label}")

        return ax_spectrum, ax_residuals

    @property
    def _energy_unit(self):
        return self._geom.axes[0].unit

    def _plot_energy_range(self, ax):
        energy_min, energy_max = self.energy_range
        kwargs = {"color": "black", "linestyle": "dashed"}
        ax.axvline(energy_min.to_value(self._energy_unit), label="fit range", **kwargs)
        ax.axvline(energy_max.to_value(self._energy_unit), **kwargs)

    def plot_counts(
        self, ax=None, kwargs_counts=None, kwargs_background=None, **kwargs
    ):
        """Plot counts and background.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`
            Axes to plot on.
        kwargs_counts: dict
            Keyword arguments passed to `~matplotlib.axes.Axes.hist` for the counts.
        kwargs_background: dict
            Keyword arguments passed to `~matplotlib.axes.Axes.hist` for the background.
        **kwargs: dict
            Keyword arguments passed to both `~matplotlib.axes.Axes.hist`.

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axes object.
        """
        kwargs_counts = kwargs_counts or {}
        kwargs_background = kwargs_background or {}

        plot_kwargs = kwargs.copy()
        plot_kwargs.update(kwargs_counts)
        plot_kwargs.setdefault("label", "Counts")
        ax = self.counts.plot_hist(ax, **plot_kwargs)

        plot_kwargs = kwargs.copy()
        plot_kwargs.update(kwargs_background)

        plot_kwargs.setdefault("label", "Background")
        self.background.plot_hist(ax, **plot_kwargs)

        self._plot_energy_range(ax)
        energy_min, energy_max = self.energy_range
        ax.set_xlim(0.7 * energy_min.value, 1.3 * energy_max.value)

        ax.legend(numpoints=1)
        return ax

    def plot_excess(
        self, ax=None, kwargs_excess=None, kwargs_npred_signal=None, **kwargs
    ):
        """Plot excess and predicted signal.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`
            Axes to plot on.
        kwargs_excess: dict
            Keyword arguments passed to `~matplotlib.axes.Axes.errorbar` for
            the excess.
        kwargs_npred_signal : dict
            Keyword arguments passed to `~matplotlib.axes.Axes.hist` for the
            predicted signal.
        **kwargs: dict
            Keyword arguments passed to both plot methods.

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axes object.
        """
        kwargs_excess = kwargs_excess or {}
        kwargs_npred_signal = kwargs_npred_signal or {}

        plot_kwargs = kwargs.copy()
        plot_kwargs.update(kwargs_excess)
        plot_kwargs.setdefault("label", "Excess counts")
        ax = self.excess.plot(
            ax, yerr=np.sqrt(np.abs(self.excess.data.flatten())), **plot_kwargs
        )

        plot_kwargs = kwargs.copy()
        plot_kwargs.update(kwargs_npred_signal)
        plot_kwargs.setdefault("label", "Predicted signal counts")
        self.npred_signal().plot_hist(ax, **plot_kwargs)

        self._plot_energy_range(ax)
        ax.legend(numpoints=1)
        return ax

    def residuals(self, method="diff"):
        """Compute the spectral residuals.

        Parameters
        ----------
        method : {"diff", "diff/model", "diff/sqrt(model)"}
            Method used to compute the residuals. Available options are:
                - ``diff`` (default): data - model
                - ``diff/model``: (data - model) / model
                - ``diff/sqrt(model)``: (data - model) / sqrt(model)

        Returns
        -------
        residuals : `RegionNDMap`
            Residual spectrum
        """
        residuals = self._compute_residuals(self.counts, self.npred(), method)
        return residuals

    def plot_residuals(self, ax=None, method="diff", **kwargs):
        """Plot spectrum residuals.

        Parameters
        ----------
        ax : `~matplotlib.axes.Axes`
            Axes to plot on.
        method : {"diff", "diff/model", "diff/sqrt(model)"}
            Normalization used to compute the residuals, see `SpectrumDataset.residuals`.
        **kwargs : dict
            Keyword arguments passed to `~matplotlib.axes.Axes.errorbar`.

        Returns
        -------
        ax : `~matplotlib.axes.Axes`
            Axes object.
        """
        # TODO: remove code duplication with `MapDataset.plot_residuals_spectral()`
        residuals = self.residuals(method)
        if method == "diff":
            yerr = np.sqrt((self.counts.data + self.npred().data).flatten())
        else:
            yerr = np.ones_like(residuals.data.flatten())

        kwargs.setdefault("color", kwargs.pop("c", "black"))
        ax = residuals.plot(ax, yerr=yerr, **kwargs)
        ax.axhline(0, color=kwargs["color"], lw=0.5)

        label = self._residuals_labels[method]
        ax.set_ylabel(f"Residuals ({label})")
        ax.set_yscale("linear")
        ymin = 1.05 * np.nanmin(residuals.data - yerr)
        ymax = 1.05 * np.nanmax(residuals.data + yerr)
        ax.set_ylim(ymin, ymax)
        return ax

    @classmethod
    def create(
        cls,
        e_reco,
        e_true=None,
        region=None,
        reference_time="2000-01-01",
        name=None,
        meta_table=None,
    ):
        """Creates empty spectrum dataset.

        Empty containers are created with the correct geometry.
        counts, background and aeff are zero and edisp is diagonal.

        The safe_mask is set to False in every bin.

        Parameters
        ----------
        e_reco : `~gammapy.maps.MapAxis`
            counts energy axis. Its name must be "energy".
        e_true : `~gammapy.maps.MapAxis`
            effective area table energy axis. Its name must be "energy-true".
            If not set use reco energy values. Default : None
        region : `~regions.SkyRegion`
            Region to define the dataset for.
        reference_time : `~astropy.time.Time`
            reference time of the dataset, Default is "2000-01-01"
        meta_table : `~astropy.table.Table`
            Table listing informations on observations used to create the dataset.
            One line per observation for stacked datasets.
        """
        if e_true is None:
            e_true = e_reco.copy(name="energy_true")

        if region is None:
            region = "icrs;circle(0, 0, 1)"

        name = make_name(name)
        counts = RegionNDMap.create(region=region, axes=[e_reco])
        background = RegionNDMap.create(region=region, axes=[e_reco])
        exposure = RegionNDMap.create(
            region=region, axes=[e_true], unit="cm2 s", meta={"livetime": 0 * u.s}
        )
        edisp = EDispKernelMap.from_diagonal_response(e_reco, e_true, geom=counts.geom)
        mask_safe = RegionNDMap.from_geom(counts.geom, dtype="bool")
        gti = GTI.create(u.Quantity([], "s"), u.Quantity([], "s"), reference_time)

        return SpectrumDataset(
            counts=counts,
            exposure=exposure,
            background=background,
            edisp=edisp,
            mask_safe=mask_safe,
            gti=gti,
            name=name,
        )

    def peek(self, fig=None):
        """Quick-look summary plots.

        Parameters
        ----------
        fig : `~matplotlib.figure.Figure`
            Figure to add AxesSubplot on.

        Returns
        -------
        ax1, ax2, ax3 : `~matplotlib.axes.AxesSubplot`
            Counts, effective area and energy dispersion subplots.
        """
        fig = get_figure(fig, 16, 4)
        ax1, ax2, ax3 = fig.subplots(1, 3)

        ax1.set_title("Counts")
        self.plot_counts(ax1)

        ax2.set_title("Exposure")
        self.exposure.plot(ax2)
        self._plot_energy_range(ax2)
        energy_min, energy_max = self.energy_range
        ax2.set_xlim(0.7 * energy_min.value, 1.3 * energy_max.value)

        ax3.set_title("Energy Dispersion")
        if self.edisp is not None:
            kernel = self.edisp.get_edisp_kernel()
            kernel.plot_matrix(ax3, vmin=0, vmax=1)

        # TODO: optimize layout
        fig.subplots_adjust(wspace=0.3)
        return ax1, ax2, ax3

class SpectrumDatasetOnOff(SpectrumDataset):
    """Spectrum dataset for on-off likelihood fitting.

    The on-off spectrum dataset bundles reduced counts data, off counts data,
    with a spectral model, relative background efficiency and instrument
    response functions to compute the fit-statistic given the current model
    and data.

    Parameters
    ----------
    models : `~gammapy.modeling.models.Models`
        Fit model
    counts : `~gammapy.maps.RegionNDMap`
        ON Counts spectrum
    counts_off : `~gammapy.maps.RegionNDMap`
        OFF Counts spectrum
    exposure : `~gammapy.maps.RegionNDMap`
        Exposure
    edisp : `~gammapy.irf.EDispKernelMap`
        Energy dispersion kernel
    mask_safe : `~gammapy.maps.RegionNDMap`
        Mask defining the safe data range.
    mask_fit : `~gammapy.maps.RegionNDMap`
        Mask to apply to the likelihood for fitting.
    acceptance : `~gammapy.maps.RegionNDMap` or float
        Relative background efficiency in the on region.
    acceptance_off : `~gammapy.maps.RegionNDMap` or float
        Relative background efficiency in the off region.
    name : str
        Name of the dataset.
    gti : `~gammapy.data.GTI`
        GTI of the observation or union of GTI if it is a stacked observation
    meta_table : `~astropy.table.Table`
        Table listing informations on observations used to create the dataset.
        One line per observation for stacked datasets.

    See Also
    --------
    SpectrumDataset, FluxPointsDataset, MapDataset
    """

    stat_type = "wstat"
    tag = "SpectrumDatasetOnOff"

    def __init__(
        self,
        models=None,
        counts=None,
        counts_off=None,
        exposure=None,
        edisp=None,
        mask_safe=None,
        mask_fit=None,
        acceptance=None,
        acceptance_off=None,
        name=None,
        gti=None,
        meta_table=None,
    ):

        self._name = make_name(name)
        self._evaluators = {}

        self.counts = counts
        self.counts_off = counts_off

        self.mask_fit = mask_fit
        self.exposure = exposure
        self.edisp = edisp
        self.mask_safe = mask_safe
        self.meta_table = meta_table

        if np.isscalar(acceptance):
            data = np.ones(self._geom.data_shape) * acceptance
            acceptance = RegionNDMap.from_geom(self._geom, data=data)

        self.acceptance = acceptance

        if np.isscalar(acceptance_off):
            data = np.ones(self._geom.data_shape) * acceptance_off
            acceptance_off = RegionNDMap.from_geom(self._geom, data=data)

        self.acceptance_off = acceptance_off

        self.gti = gti
        self.models = models

    def __str__(self):
        str_ = super().__str__()

        str_list = str_.split("\n")

        if getattr(self, "counts_off", None) is not None:
            counts_off = np.sum(self.counts_off.data)
            str_cts = "\t{:32}: {:.2f}".format("Total off counts", counts_off)

        str_list.insert(6, str_cts)

        acceptance = np.nan
        if self.acceptance is not None:
            acceptance = np.mean(self.acceptance.data)

        str_acc = "\n\t{:32}: {:.3f}\n".format("Acceptance mean", acceptance)

        acceptance_off = np.nan
        if self.acceptance_off is not None:
            acceptance_off = np.sum(self.acceptance_off.data)
        str_acc += "\t{:32}: {:.3f}".format("Acceptance off", acceptance_off)

        str_list.insert(16, str_acc)
        str_ = "\n".join(str_list)

        return str_.expandtabs(tabsize=2)

    def npred_background(self):
        """Background counts estimated from the marginalized likelihood estimate.
         See :ref:`wstat`
         """
        mu_bkg = self.alpha.data * get_wstat_mu_bkg(
            n_on=self.counts.data,
            n_off=self.counts_off.data,
            alpha=self.alpha.data,
            mu_sig=self.npred_signal().data,
        )
        return RegionNDMap.from_geom(geom=self._geom, data=mu_bkg)

    def npred_off(self):
        """Predicted counts in the off region

        Returns
        -------
        npred_off : `Map`
            Predicted off counts
        """
        return self.npred_background() / self.alpha

    @property
    def background(self):
        """ alpha * noff"""
        return self.alpha * self.counts_off

    @property
    def alpha(self):
        """Exposure ratio between signal and background regions"""
        alpha = self.acceptance / self.acceptance_off
        np.nan_to_num(alpha.data, copy=False)
        return alpha

    @property
    def _geom(self):
        """Main analysis geometry"""
        if self.counts is not None:
            return self.counts.geom
        elif self.counts_off is not None:
            return self.counts_off.geom
        elif self.acceptance is not None:
            return self.acceptance.geom
        elif self.acceptance_off is not None:
            return self.acceptance_off.geom
        else:
            raise ValueError(
                "Either 'counts', 'counts_off', 'acceptance' or 'acceptance_of' must be defined."
            )

    def stat_array(self):
        """Likelihood per bin given the current model parameters"""
        mu_sig = self.npred_signal().data
        on_stat_ = wstat(
            n_on=self.counts.data,
            n_off=self.counts_off.data,
            alpha=self.alpha.data,
            mu_sig=mu_sig,
        )
        return np.nan_to_num(on_stat_)

    def fake(self, npred_background, random_state="random-seed"):
        """Simulate fake counts for the current model and reduced irfs.

        This method overwrites the counts and off counts defined on the dataset object.

        Parameters
        ----------
        npred_background : `~gammapy.maps.RegionNDMap`
            Predicted background to be used in the on region.
        random_state : {int, 'random-seed', 'global-rng', `~numpy.random.RandomState`}
            Defines random number generator initialisation.
            Passed to `~gammapy.utils.random.get_random_state`.
        """
        random_state = get_random_state(random_state)

        npred = self.npred_signal()
        npred.data = random_state.poisson(npred.data)
        npred_bkg = random_state.poisson(npred_background.data)
        self.counts = npred + npred_bkg

        npred_off = npred_background / self.alpha
        npred_off.data = random_state.poisson(npred_off.data)
        self.counts_off = npred_off

    @classmethod
    def create(
        cls,
        e_reco,
        e_true=None,
        region=None,
        reference_time="2000-01-01",
        name=None,
        meta_table=None,
    ):
        """Create empty SpectrumDatasetOnOff.

        Empty containers are created with the correct geometry.
        counts, counts_off and aeff are zero and edisp is diagonal.

        The safe_mask is set to False in every bin.

        Parameters
        ----------
        e_reco : `~gammapy.maps.MapAxis`
            counts energy axis. Its name must be "energy".
        e_true : `~gammapy.maps.MapAxis`
            effective area table energy axis. Its name must be "energy-true".
            If not set use reco energy values. Default : None
        region : `~regions.SkyRegion`
            Region to define the dataset for.
        reference_time : `~astropy.time.Time`
            reference time of the dataset, Default is "2000-01-01"
        meta_table : `~astropy.table.Table`
            Table listing informations on observations used to create the dataset.
            One line per observation for stacked datasets.
        """
        dataset = super().create(
            e_reco=e_reco,
            e_true=e_true,
            region=region,
            reference_time=reference_time,
            name=name,
        )

        counts_off = dataset.counts.copy()
        acceptance = RegionNDMap.from_geom(counts_off.geom, dtype=int)
        acceptance.data += 1

        acceptance_off = RegionNDMap.from_geom(counts_off.geom, dtype=int)
        acceptance_off.data += 1

        return cls.from_spectrum_dataset(
            dataset=dataset,
            acceptance=acceptance,
            acceptance_off=acceptance_off,
            counts_off=counts_off,
        )

    @classmethod
    def read(cls, filename):
        """Read from file

        For now, filename is assumed to the name of a PHA file where BKG file, ARF, and RMF names
        must be set in the PHA header and be present in the same folder

        Parameters
        ----------
        filename : str
            OGIP PHA file to read
        """
        raise NotImplementedError(
            "To read from an OGIP fits file use SpectrumDatasetOnOff.from_ogip_files."
        )

    def _is_stackable(self):
        """Check if the Dataset contains enough information to be stacked"""
        if (
            self.acceptance_off is None
            or self.acceptance is None
            or self.counts_off is None
        ):
            return False
        else:
            return True

    def stack(self, other):
        r"""Stack this dataset with another one.

        Safe mask is applied to compute the stacked counts vector.
        Counts outside each dataset safe mask are lost.

        Stacking is performed in-place.

        The stacking of 2 datasets is implemented as follows.
        Here, :math:`k`  denotes a bin in reconstructed energy and :math:`j = {1,2}` is the dataset number

        The ``mask_safe`` of each dataset is defined as:

        .. math::

            \epsilon_{jk} =\left\{\begin{array}{cl} 1, &
            \mbox{if k is inside the energy thresholds}\\ 0, &
            \mbox{otherwise} \end{array}\right.

        Then the total ``counts`` and ``counts_off`` are computed according to:

        .. math::

            \overline{\mathrm{n_{on}}}_k =  \mathrm{n_{on}}_{1k} \cdot \epsilon_{1k} +
            \mathrm{n_{on}}_{2k} \cdot \epsilon_{2k}

            \overline{\mathrm{n_{off}}}_k = \mathrm{n_{off}}_{1k} \cdot \epsilon_{1k} +
            \mathrm{n_{off}}_{2k} \cdot \epsilon_{2k}

        The stacked ``safe_mask`` is then:

        .. math::

            \overline{\epsilon_k} = \epsilon_{1k} OR \epsilon_{2k}

        In each energy bin :math:`k`, the count excess is computed taking into account the ON ``acceptance``,
        :math:`a_{on}_k` and the OFF one: ``acceptance_off``, :math:`a_{off}_k`. They define
        the :math:`\alpha_k=a_{on}_k/a_{off}_k` factors such that :math:`n_{ex}_k = n_{on}_k - \alpha_k n_{off}_k`.
        We define the stacked value of :math:`\overline{{a}_{on}}_k = 1` so that:

        .. math::

            \overline{{a}_{off}}_k = \frac{\overline{\mathrm {n_{off}}}}{\alpha_{1k} \cdot
            \mathrm{n_{off}}_{1k} \cdot \epsilon_{1k} + \alpha_{2k} \cdot \mathrm{n_{off}}_{2k} \cdot \epsilon_{2k}}

        The stacking of :math:`j` elements is implemented as follows.  :math:`k`
        and :math:`l` denote a bin in reconstructed and true energy, respectively.

        .. math::

            \epsilon_{jk} =\left\{\begin{array}{cl} 1, & \mbox{if
                bin k is inside the energy thresholds}\\ 0, & \mbox{otherwise} \end{array}\right.

            \overline{t} = \sum_{j} t_i

            \overline{\mathrm{aeff}}_l = \frac{\sum_{j}\mathrm{aeff}_{jl}
                \cdot t_j}{\overline{t}}

            \overline{\mathrm{edisp}}_{kl} = \frac{\sum_{j} \mathrm{edisp}_{jkl}
                \cdot \mathrm{aeff}_{jl} \cdot t_j \cdot \epsilon_{jk}}{\sum_{j} \mathrm{aeff}_{jl}
                \cdot t_j}

        Parameters
        ----------
        other : `~gammapy.datasets.SpectrumDatasetOnOff`
            the dataset to stack to the current one

        Examples
        --------
        >>> from gammapy.datasets import SpectrumDatasetOnOff
        >>> obs_ids = [23523, 23526, 23559, 23592]
        >>> datasets = []
        >>> for obs in obs_ids:
        >>>     filename = "$GAMMAPY_DATA/joint-crab/spectra/hess/pha_obs{}.fits"
        >>>     ds = SpectrumDatasetOnOff.from_ogip_files(filename.format(obs))
        >>>     datasets.append(ds)
        >>> stacked = datasets[0]
        >>> for ds in datasets[1:]:
        >>>     stacked.stack(ds)
        >>> print(stacked)
        """
        if not isinstance(other, SpectrumDatasetOnOff):
            raise TypeError("Incompatible types for SpectrumDatasetOnOff stacking")

        # We assume here that counts_off, acceptance and acceptance_off are well defined.
        if not self._is_stackable() or not other._is_stackable():
            raise ValueError("Cannot stack incomplete SpectrumDatsetOnOff.")

        geom = self.counts.geom
        total_off = RegionNDMap.from_geom(geom)
        total_alpha = RegionNDMap.from_geom(geom)

        total_off.stack(self.counts_off, weights=self.mask_safe)
        total_off.stack(other.counts_off, weights=other.mask_safe)

        total_alpha.stack(self.alpha * self.counts_off, weights=self.mask_safe)
        total_alpha.stack(other.alpha * other.counts_off, weights=other.mask_safe)

        with np.errstate(divide="ignore", invalid="ignore"):
            acceptance_off = total_off / total_alpha
            average_alpha = total_alpha.data.sum() / total_off.data.sum()

        # For the bins where the stacked OFF counts equal 0, the alpha value is performed by weighting on the total
        # OFF counts of each run
        is_zero = total_off.data == 0
        acceptance_off.data[is_zero] = 1 / average_alpha

        self.acceptance = RegionNDMap.from_geom(geom)
        self.acceptance.data += 1
        self.acceptance_off = acceptance_off

        if self.counts_off is not None:
            self.counts_off *= self.mask_safe
            self.counts_off.stack(other.counts_off, weights=other.mask_safe)

        super().stack(other)

    def to_ogip_files(self, outdir=None, use_sherpa=False, overwrite=False):
        """Write OGIP files.

        If you want to use the written files with Sherpa you have to set the
        ``use_sherpa`` flag. Then all files will be written in units 'keV' and
        'cm2'.

        The naming scheme is fixed, with {name} the dataset name:

        * PHA file is named pha_obs{name}.fits
        * BKG file is named bkg_obs{name}.fits
        * ARF file is named arf_obs{name}.fits
        * RMF file is named rmf_obs{name}.fits

        Parameters
        ----------
        outdir : `pathlib.Path`
            output directory, default: pwd
        use_sherpa : bool, optional
            Write Sherpa compliant files, default: False
        overwrite : bool
            Overwrite existing files?
        """
        # TODO: refactor and reduce amount of code duplication
        outdir = Path.cwd() if outdir is None else make_path(outdir)
        outdir.mkdir(exist_ok=True, parents=True)

        phafile = f"pha_obs{self.name}.fits"

        bkgfile = phafile.replace("pha", "bkg")
        arffile = phafile.replace("pha", "arf")
        rmffile = phafile.replace("pha", "rmf")

        counts_table = self.counts.to_table()
        counts_table["QUALITY"] = np.logical_not(self.mask_safe.data[:, 0, 0])
        counts_table["BACKSCAL"] = self.acceptance.data[:, 0, 0]
        counts_table["AREASCAL"] = np.ones(self.acceptance.data.size)
        meta = self._ogip_meta()

        meta["respfile"] = rmffile
        meta["backfile"] = bkgfile
        meta["ancrfile"] = arffile
        meta["hduclas2"] = "TOTAL"
        counts_table.meta = meta

        name = counts_table.meta["name"]
        hdu = fits.BinTableHDU(counts_table, name=name)

        energy_axis = self.counts.geom.axes[0]

        hdu_format = "ogip-sherpa" if use_sherpa else "ogip"

        hdulist = fits.HDUList(
            [fits.PrimaryHDU(), hdu, energy_axis.to_table_hdu(format=hdu_format)]
        )

        if self.gti is not None:
            hdu = fits.BinTableHDU(self.gti.table, name="GTI")
            hdulist.append(hdu)

        if self.counts.geom._region is not None and self.counts.geom.wcs is not None:
            region_table = self.counts.geom._to_region_table()
            region_hdu = fits.BinTableHDU(region_table, name="REGION")
            hdulist.append(region_hdu)

        hdulist.writeto(str(outdir / phafile), overwrite=overwrite)

        aeff = self.exposure / self.exposure.meta["livetime"]

        aeff.write(
            outdir / arffile,
            overwrite=overwrite,
            format=hdu_format,
            ogip_column="SPECRESP",
        )

        if self.counts_off is not None:
            counts_off_table = self.counts_off.to_table()
            counts_off_table["QUALITY"] = np.logical_not(self.mask_safe.data[:, 0, 0])
            counts_off_table["BACKSCAL"] = self.acceptance_off.data[:, 0, 0]
            counts_off_table["AREASCAL"] = np.ones(self.acceptance.data.size)
            meta = self._ogip_meta()
            meta["hduclas2"] = "BKG"

            counts_off_table.meta = meta
            name = counts_off_table.meta["name"]
            hdu = fits.BinTableHDU(counts_off_table, name=name)
            hdulist = fits.HDUList(
                [fits.PrimaryHDU(), hdu, energy_axis.to_table_hdu(format=hdu_format)]
            )
            if (
                self.counts_off.geom._region is not None
                and self.counts_off.geom.wcs is not None
            ):
                region_table = self.counts_off.geom._to_region_table()
                region_hdu = fits.BinTableHDU(region_table, name="REGION")
                hdulist.append(region_hdu)

            hdulist.writeto(str(outdir / bkgfile), overwrite=overwrite)

        if self.edisp is not None:
            kernel = self.edisp.get_edisp_kernel()
            kernel.write(outdir / rmffile, overwrite=overwrite, use_sherpa=use_sherpa)

    def _ogip_meta(self):
        """Meta info for the OGIP data format"""
        try:
            livetime = self.exposure.meta["livetime"]
        except KeyError:
            raise ValueError(
                "Storing in ogip format require the livetime "
                "to be defined in the exposure meta data"
            )
        return {
            "name": "SPECTRUM",
            "hduclass": "OGIP",
            "hduclas1": "SPECTRUM",
            "corrscal": "",
            "chantype": "PHA",
            "detchans": self.counts.geom.axes[0].nbin,
            "filter": "None",
            "corrfile": "",
            "poisserr": True,
            "hduclas3": "COUNT",
            "hduclas4": "TYPE:1",
            "lo_thres": self.energy_range[0].to_value("TeV"),
            "hi_thres": self.energy_range[1].to_value("TeV"),
            "exposure": livetime.to_value("s"),
            "obs_id": self.name,
        }

    @classmethod
    def from_ogip_files(cls, filename):
        """Read `~gammapy.datasets.SpectrumDatasetOnOff` from OGIP files.

        BKG file, ARF, and RMF must be set in the PHA header and be present in
        the same folder.

        The naming scheme is fixed to the following scheme:

        * PHA file is named ``pha_obs{name}.fits``
        * BKG file is named ``bkg_obs{name}.fits``
        * ARF file is named ``arf_obs{name}.fits``
        * RMF file is named ``rmf_obs{name}.fits``
          with ``{name}`` the dataset name.

        Parameters
        ----------
        filename : str
            OGIP PHA file to read
        """
        filename = make_path(filename)
        dirname = filename.parent

        with fits.open(str(filename), memmap=False) as hdulist:
            counts = RegionNDMap.from_hdulist(hdulist, format="ogip")
            acceptance = RegionNDMap.from_hdulist(
                hdulist, format="ogip", ogip_column="BACKSCAL"
            )
            livetime = counts.meta["EXPOSURE"] * u.s

            if "GTI" in hdulist:
                gti = GTI(Table.read(hdulist["GTI"]))
            else:
                gti = None

            mask_safe = RegionNDMap.from_hdulist(
                hdulist, format="ogip", ogip_column="QUALITY"
            )
            mask_safe.data = np.logical_not(mask_safe.data)

        phafile = filename.name

        try:
            rmffile = phafile.replace("pha", "rmf")
            kernel = EDispKernel.read(dirname / rmffile)
            edisp = EDispKernelMap.from_edisp_kernel(kernel, geom=counts.geom)

        except OSError:
            # TODO : Add logger and echo warning
            edisp = None

        try:
            bkgfile = phafile.replace("pha", "bkg")
            with fits.open(str(dirname / bkgfile), memmap=False) as hdulist:
                counts_off = RegionNDMap.from_hdulist(hdulist, format="ogip")
                acceptance_off = RegionNDMap.from_hdulist(
                    hdulist, ogip_column="BACKSCAL"
                )
        except OSError:
            # TODO : Add logger and echo warning
            counts_off, acceptance_off = None, None

        arffile = phafile.replace("pha", "arf")
        aeff = RegionNDMap.read(dirname / arffile, format="ogip-arf")
        exposure = aeff * livetime
        exposure.meta["livetime"] = livetime

        if edisp is not None:
            edisp.exposure_map.data = exposure.data[:, :, np.newaxis, :]

        return cls(
            counts=counts,
            exposure=exposure,
            counts_off=counts_off,
            edisp=edisp,
            mask_safe=mask_safe,
            acceptance=acceptance,
            acceptance_off=acceptance_off,
            name=str(counts.meta["OBS_ID"]),
            gti=gti,
        )

    def info_dict(self, in_safe_data_range=True):
        """Info dict with summary statistics, summed over energy

        Parameters
        ----------
        in_safe_data_range : bool
            Whether to sum only in the safe energy range

        Returns
        -------
        info_dict : dict
            Dictionary with summary info.
        """
        info = super().info_dict(in_safe_data_range)

        if self.mask_safe and in_safe_data_range:
            mask = self.mask_safe.data.astype(bool)
        else:
            mask = slice(None)

        counts_off = np.nan
        if self.counts_off is not None:
            counts_off = self.counts_off.data[mask].sum()

        info["counts_off"] = counts_off

        acceptance = 1
        if self.acceptance:
            # TODO: handle energy dependent a_on / a_off
            acceptance = self.acceptance.data[mask].sum()

        info["acceptance"] = acceptance

        acceptance_off = np.nan
        if self.acceptance_off:
            acceptance_off = acceptance * counts_off / info["background"]

        info["acceptance_off"] = acceptance_off

        alpha = np.nan
        if self.acceptance_off and self.acceptance:
            alpha = np.mean(self.alpha.data[mask])

        info["alpha"] = alpha

        info["sqrt_ts"] = WStatCountsStatistic(
            info["counts"], info["counts_off"], acceptance / acceptance_off,
        ).sqrt_ts
        info["stat_sum"] = self.stat_sum()
        return info

    def to_dict(self, filename, *args, **kwargs):
        """Convert to dict for YAML serialization."""
        outdir = Path(filename).parent
        filename = str(outdir / f"pha_obs{self.name}.fits")

        return {"name": self.name, "type": self.tag, "filename": filename}

    def write(self, filename, overwrite):
        """Write spectrum dataset on off to file.

        Currently only the OGIP format is supported

        Parameters
        ----------
        filename : str
            Filename to write to.
        overwrite : bool
            Overwrite existing file.
        """
        outdir = Path(filename).parent
        self.to_ogip_files(outdir=outdir, overwrite=overwrite)

    @classmethod
    def from_dict(cls, data, **kwargs):
        """Create flux point dataset from dict.

        Parameters
        ----------
        data : dict
            Dict containing data to create dataset from.

        Returns
        -------
        dataset : `SpectrumDatasetOnOff`
            Spectrum dataset on off.

        """

        filename = make_path(data["filename"])
        dataset = cls.from_ogip_files(filename=filename)
        dataset.mask_fit = None
        return dataset

    @classmethod
    def from_spectrum_dataset(
        cls, dataset, acceptance, acceptance_off, counts_off=None
    ):
        """Create spectrum dataseton off from another dataset.

        Parameters
        ----------
        dataset : `SpectrumDataset`
            Spectrum dataset defining counts, edisp, exposure etc.
        acceptance : `~numpy.array` or float
            Relative background efficiency in the on region.
        acceptance_off : `~numpy.array` or float
            Relative background efficiency in the off region.
        counts_off : `~gammapy.maps.RegionNDMap`
            Off counts spectrum . If the dataset provides a background model,
            and no off counts are defined. The off counts are deferred from
            counts_off / alpha.

        Returns
        -------
        dataset : `SpectrumDatasetOnOff`
            Spectrum dataset on off.

        """
        if counts_off is None and dataset.background is not None:
            alpha = acceptance / acceptance_off
            counts_off = dataset.npred_background() / alpha

        return cls(
            models=dataset.models,
            counts=dataset.counts,
            exposure=dataset.exposure,
            counts_off=counts_off,
            edisp=dataset.edisp,
            mask_safe=dataset.mask_safe,
            mask_fit=dataset.mask_fit,
            acceptance=acceptance,
            acceptance_off=acceptance_off,
            gti=dataset.gti,
            name=dataset.name,
            meta_table=dataset.meta_table,
        )

    def to_spectrum_dataset(self, name=None):
        """ Convert a SpectrumDatasetOnOff to a SpectrumDataset
        The background model template is taken as alpha*counts_off

        Parameters
        ----------
        name: str
            Name of the new dataset

        Returns
        -------
        dataset: `SpectrumDataset`
            SpectrumDatset with cash statistics
        """

        name = make_name(name)
        return SpectrumDataset(
            counts=self.counts,
            exposure=self.exposure,
            edisp=self.edisp,
            name=name,
            gti=self.gti,
            mask_fit=self.mask_fit,
            mask_safe=self.mask_safe,
            meta_table=self.meta_table,
            background=self.background,
        )

    def slice_by_idx(self, slices, name=None):
        """Slice sub dataset.

        The slicing only applies to the maps that define the corresponding axes.

        Parameters
        ----------
        slices : dict
            Dict of axes names and integers or `slice` object pairs. Contains one
            element for each non-spatial dimension. For integer indexing the
            corresponding axes is dropped from the map. Axes not specified in the
            dict are kept unchanged.
        name : str
            Name of the sliced dataset.

        Returns
        -------
        map_out : `Map`
            Sliced map object.
        """
        name = make_name(name)
        kwargs = {"gti": self.gti, "name": name}

        if self.counts is not None:
            kwargs["counts"] = self.counts.slice_by_idx(slices=slices)

        if self.exposure is not None:
            kwargs["exposure"] = self.exposure.slice_by_idx(slices=slices)

        if self.edisp is not None:
            kwargs["edisp"] = self.edisp.slice_by_idx(slices=slices)

        if self.mask_safe is not None:
            kwargs["mask_safe"] = self.mask_safe.slice_by_idx(slices=slices)

        if self.mask_fit is not None:
            kwargs["mask_fit"] = self.mask_fit.slice_by_idx(slices=slices)

        kwargs["acceptance"] = self.acceptance.slice_by_idx(slices=slices)
        kwargs["acceptance_off"] = self.acceptance_off.slice_by_idx(slices=slices)
        kwargs["counts_off"] = self.counts_off.slice_by_idx(slices=slices)
        return self.__class__(**kwargs)

    def resample_energy_axis(self, energy_axis, name=None):
        """Resample SpectrumDatasetOnOff over new reconstructed energy axis.

        Counts are summed taking into account safe mask.

        Parameters
        ----------
        energy_axis : `~gammapy.maps.MapAxis`
            New reconstructed energy axis
        name: str
            Name of the new dataset.

        Returns
        -------
        dataset: `SpectrumDataset`
            Resampled spectrum dataset .
        """
        dataset = super().resample_energy_axis(energy_axis=energy_axis, name=name)

        axis = dataset.counts.geom.axes["energy"]

        counts_off = None
        if self.counts_off is not None:
            counts_off = self.counts_off
            counts_off = counts_off.resample_axis(axis=axis, weights=self.mask_safe)

        acceptance = 1
        acceptance_off = None
        if self.acceptance is not None:
            acceptance = self.acceptance
            acceptance = acceptance.resample_axis(axis=axis, weights=self.mask_safe)

            background = self.alpha * self.counts_off
            background = background.resample_axis(axis=axis, weights=self.mask_safe)

            acceptance_off = acceptance * counts_off / background

        return self.__class__.from_spectrum_dataset(
            dataset,
            acceptance=acceptance,
            acceptance_off=acceptance_off,
            counts_off=counts_off,
        )


#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""
.. py:currentmodule:: xray.mac.models.casino
.. moduleauthor:: Hendrix Demers <hendrix.demers@mail.mcgill.ca>

Compute mass absorption coefficient like in CASINO v2.
"""

###############################################################################
# Copyright 2021 Hendrix Demers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
###############################################################################

# Standard library modules.
import math
import logging

# Third party modules.
import numpy as np

# Local modules.

# Project modules.
from xray.mac import get_current_module_path

# Globals and constants variables.
# Define used to related element name and atomic number.
HYDROGEN = 1
BERYLLIUM = 4
CARBON = 6
OXYGEN = 8
ALUMINUM = 13
SILICON = 14
GOLD = 79

# Define used to related element symbol and mass density.
MASS_DENSITY_H_g_cm3 = 1.0
MASS_DENSITY_BE_g_cm3 = 1.848
MASS_DENSITY_C_g_cm3 = 2.34
MASS_DENSITY_O_g_cm3 = 1.0
MASS_DENSITY_AL_g_cm3 = 2.7
MASS_DENSITY_SI_g_cm3 = 2.34
MASS_DENSITY_AU_g_cm3 = 19.3
MASS_DENSITY_H2O_g_cm3 = 1.0

# Define used to related element symbol and mass fraction.
# Mass fraction of H in parylene C6H6.
CH_C6H6 = 0.08
# Mass fraction of C in parylene C6H6.
CC = 0.92
CH_H2O = 0.1111
CO = 0.8889

NOISE_FWHM = 53.0
DETECTOR_FWHM = 1.61
HDV = 0.01

noz = 0

# Inner-shell ionisation energy (critical excitation energy) in keV.
transitions = [
    [0.013598, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.024586, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.054748, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.110996, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.187994, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.283790, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.401586, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.531982, 0.023699, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.685377, 0.030999, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [0.866871, 0.044998, 0.018299, 0.018299, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [1.072064, 0.063298, 0.031099, 0.031099, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [1.304956, 0.089397, 0.051398, 0.051398, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [1.559547, 0.117696, 0.073098, 0.073098, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [1.838838, 0.148695, 0.099197, 0.099197, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [2.145427, 0.189294, 0.132196, 0.132196, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [2.471916, 0.229192, 0.164794, 0.132196, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [2.822304, 0.270191, 0.201593, 0.199993, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [3.202791, 0.319989, 0.247292, 0.245192, 0.025299, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [3.607278, 0.377087, 0.296290, 0.293590, 0.033899, 0.000000, 0.000000, 0.000000, 0.000000, 0.000000],
    [4.037963, 0.437785, 0.349988, 0.346388, 0.043699, 0.025399, 0.000000, 0.000000, 0.000000, 0.000000],
    [4.492648, 0.500383, 0.406686, 0.402186, 0.053798, 0.032299, 0.032299, 0.000000, 0.000000, 0.000000],
    [4.966231, 0.563681, 0.461484, 0.455485, 0.060298, 0.034599, 0.034599, 0.000000, 0.000000, 0.000000],
    [5.464915, 0.628179, 0.520482, 0.512883, 0.066498, 0.037799, 0.037799, 0.000000, 0.000000, 0.000000],
    [5.988997, 0.694576, 0.583680, 0.574481, 0.074097, 0.042499, 0.042499, 0.000000, 0.000000, 0.000000],
    [6.538778, 0.768974, 0.651378, 0.640278, 0.083897, 0.048598, 0.048598, 0.000000, 0.000000, 0.000000],
    [7.111759, 0.846071, 0.721076, 0.708076, 0.092897, 0.053998, 0.053998, 0.000000, 0.000000, 0.000000],
    [7.708639, 0.925569, 0.793573, 0.778574, 0.100697, 0.059498, 0.059498, 0.000000, 0.000000, 0.000000],
    [8.332519, 1.008066, 0.871870, 0.854671, 0.111796, 0.068098, 0.068098, 0.000000, 0.000000, 0.000000],
    [8.978596, 1.096063, 0.950968, 0.931068, 0.119796, 0.073598, 0.073598, 0.000000, 0.000000, 0.000000],
    [9.658273, 1.193560, 1.042765, 1.019665, 0.135895, 0.086597, 0.086597, 0.000000, 0.000000, 0.000000],
    [10.366749, 1.297656, 1.142261, 1.115362, 0.158095, 0.106796, 0.102897, 0.000000, 0.000000, 0.000000],
    [11.102724, 1.414252, 1.247758, 1.216659, 0.179994, 0.127896, 0.120796, 0.000000, 0.000000, 0.000000],
    [11.866297, 1.526448, 1.358554, 1.323055, 0.203493, 0.146395, 0.140495, 0.041199, 0.041199, 0.000000],
    [12.657371, 1.653844, 1.476150, 1.435751, 0.231492, 0.168194, 0.161895, 0.056698, 0.056698, 0.000000],
    [13.473244, 1.781940, 1.595946, 1.549847, 0.256491, 0.189294, 0.181494, 0.070098, 0.068998, 0.027299],
    [14.325114, 1.920935, 1.727141, 1.674843, 0.256491, 0.222692, 0.213793, 0.088897, 0.088897, 0.023999],
    [15.199184, 2.065030, 1.863837, 1.804339, 0.322089, 0.247392, 0.238492, 0.111796, 0.110296, 0.029299],
    [16.104053, 2.216225, 2.006732, 1.939534, 0.357488, 0.279791, 0.269091, 0.134995, 0.133095, 0.037699],
    [17.037823, 2.372420, 2.155427, 2.079930, 0.393587, 0.312389, 0.300290, 0.159595, 0.157395, 0.045498],
    [17.996990, 2.531514, 2.306622, 2.222225, 0.430285, 0.344188, 0.330489, 0.182394, 0.179994, 0.051298],
    [18.984957, 2.697608, 2.464617, 2.370420, 0.468384, 0.378387, 0.362988, 0.207393, 0.204593, 0.058098],
    [19.998823, 2.865403, 2.625011, 2.520115, 0.504583, 0.409686, 0.392287, 0.230292, 0.226992, 0.061798],
    [21.043287, 3.042397, 2.793105, 2.676809, 0.504583, 0.444885, 0.424986, 0.256391, 0.252891, 0.061798],
    [22.116451, 3.223891, 2.966799, 2.837804, 0.584980, 0.482784, 0.460584, 0.283590, 0.279391, 0.074897],
    [23.219114, 3.411784, 3.145993, 3.003698, 0.627079, 0.520982, 0.496183, 0.311689, 0.306990, 0.080997],
    [24.349474, 3.604178, 3.330187, 3.173193, 0.669877, 0.559081, 0.531482, 0.339988, 0.334689, 0.086397],
    [25.513136, 3.805671, 3.523581, 3.350986, 0.717476, 0.602380, 0.571381, 0.372787, 0.366688, 0.095197],
    [26.710295, 4.017864, 3.726874, 3.537380, 0.770174, 0.650678, 0.616479, 0.410486, 0.403686, 0.107596],
    [27.938953, 4.237356, 3.937867, 3.729974, 0.825572, 0.702176, 0.664277, 0.450785, 0.443085, 0.121896],
    [29.199110, 4.464549, 4.155960, 3.928667, 0.883770, 0.756374, 0.714376, 0.493283, 0.484784, 0.136495],
    [30.490168, 4.698141, 4.380251, 4.132060, 0.943668, 0.811872, 0.765574, 0.536882, 0.527482, 0.151995],
    [31.812721, 4.939033, 4.611844, 4.341253, 1.005966, 0.869671, 0.818672, 0.582480, 0.572081, 0.168294],
    [33.168274, 5.187924, 4.851935, 4.556945, 1.072064, 0.930468, 0.874570, 0.631279, 0.619379, 0.186394],
    [34.560230, 5.452615, 5.103527, 4.782038, 1.072064, 0.998966, 0.936968, 0.631279, 0.672277, 0.186394],
    [35.983379, 5.714106, 5.359218, 5.011730, 1.217059, 1.064964, 0.997566, 0.739475, 0.725475, 0.230792],
    [37.439331, 5.988597, 5.623410, 5.246822, 1.292756, 1.136662, 1.062164, 0.796073, 0.780674, 0.252991],
    [38.923279, 6.266088, 5.890400, 5.482514, 1.361254, 1.204359, 1.123362, 0.848471, 0.831672, 0.270391],
    [40.441631, 6.548578, 6.163991, 5.723206, 1.434551, 1.272757, 1.185360, 0.901269, 0.883270, 0.289590],
    [41.989178, 6.834569, 6.440182, 5.964098, 1.510949, 1.337355, 1.242158, 0.951068, 0.930968, 0.304490],
    [43.567421, 7.125758, 6.721272, 6.207690, 1.575247, 1.402753, 1.297356, 0.999866, 0.977667, 0.315189],
    [45.182468, 7.427649, 7.012562, 6.459081, 1.575247, 1.471350, 1.356854, 1.051464, 1.026865, 0.315189],
    [46.832615, 7.736538, 7.311552, 6.715972, 1.722742, 1.540648, 1.419752, 1.105963, 1.080163, 0.345688],
    [48.517357, 8.051727, 7.616842, 6.976664, 1.799939, 1.613845, 1.480550, 1.160561, 1.130862, 0.360188],
    [50.237400, 8.375317, 7.930032, 7.242555, 1.880736, 1.688243, 1.543948, 1.217159, 1.185160, 0.375787],
    [51.993935, 8.707705, 8.251320, 7.513745, 1.967433, 1.767640, 1.611245, 1.274957, 1.241158, 0.397887],
    [53.786678, 9.045494, 8.580309, 7.789836, 2.046731, 1.841738, 1.675543, 1.332455, 1.294856, 0.416286],
    [55.615818, 9.393883, 8.917499, 8.070827, 2.128228, 1.922735, 1.741141, 1.391453, 1.351354, 0.435685],
    [57.483555, 9.750969, 9.263987, 8.357616, 2.206425, 2.005732, 1.811739, 1.453251, 1.409252, 0.449085],
    [59.387592, 10.115356, 9.616574, 8.647707, 2.306722, 2.089729, 1.884436, 1.514549, 1.467650, 0.471684],
    [61.330223, 10.486046, 9.977862, 8.943297, 2.398019, 2.172926, 1.949734, 1.576247, 1.527748, 0.487183],
    [63.311657, 10.870031, 10.348249, 9.243787, 2.491116, 2.263423, 2.023531, 1.639344, 1.588446, 0.506183],
    [65.348587, 11.270318, 10.739036, 9.560376, 2.600812, 2.365320, 2.107529, 1.716342, 1.661644, 0.538082],
    [67.414116, 11.681105, 11.135722, 9.880765, 2.707908, 2.468616, 2.193926, 1.793139, 1.735041, 0.565481],
    [69.522644, 12.099390, 11.543609, 10.206453, 2.819504, 2.574813, 2.280923, 1.871537, 1.809139, 0.594980],
    [71.673973, 12.526276, 11.958294, 10.534943, 2.931601, 2.681509, 2.367220, 1.948834, 1.882836, 0.624979],
    [73.868294, 12.967561, 12.384581, 10.870532, 3.048397, 2.792105, 2.457117, 2.030731, 1.960034, 0.654278],
    [76.108421, 13.418045, 12.823666, 11.214820, 3.173592, 2.908601, 2.550614, 2.116028, 2.040331, 0.690077],
    [78.392143, 13.879430, 13.272150, 11.563309, 3.295888, 3.026397, 2.645310, 2.201825, 2.121528, 0.721976],
    [80.722168, 14.352314, 13.733134, 11.918296, 3.424784, 3.147693, 2.742907, 2.291022, 2.205625, 0.758774],
    [83.099487, 14.838798, 14.208219, 12.283484, 3.561479, 3.278389, 2.847003, 2.384819, 2.294822, 0.800273],
    [85.527504, 15.346180, 14.697402, 12.657071, 3.703974, 3.415584, 2.956500, 2.485016, 2.389219, 0.845471],
    [88.001518, 15.860263, 15.199485, 13.034759, 3.850570, 3.554080, 3.066296, 2.585512, 2.483916, 0.893570],
    [90.522842, 16.386944, 15.710567, 13.418145, 3.998965, 3.696175, 3.176792, 2.687509, 2.579513, 0.938168],
    [93.101845, 16.938726, 16.243750, 13.813332, 4.149260, 3.853969, 3.301788, 2.797905, 2.682909, 0.995266],
    [95.726654, 17.492407, 16.784130, 14.213018, 4.316854, 4.007864, 3.425884, 2.908601, 2.786606, 1.041965],
    [98.400665, 18.048389, 17.336514, 14.618905, 4.481848, 4.158859, 3.537880, 3.021398, 2.892302, 1.096963],
    [101.133575, 18.638369, 17.905893, 15.030691, 4.651842, 4.326853, 3.662876, 3.136094, 2.999798, 1.152961],
    [103.918381, 19.236048, 18.483675, 15.443876, 4.821836, 4.489348, 3.791671, 3.248290, 3.104795, 1.208359],
    [106.751686, 19.839329, 19.082554, 15.870462, 5.001831, 4.655842, 3.908868, 3.370086, 3.218891, 1.268957],
    [109.647186, 20.471407, 19.692532, 16.299747, 5.182125, 4.830236, 4.045963, 3.490682, 3.331887, 1.329455],
    [112.597588, 21.103884, 20.313011, 16.732533, 5.366718, 5.000731, 4.173659, 3.611078, 3.441684, 1.387053],
    [115.602188, 21.756662, 20.946892, 17.165720, 5.547812, 5.182024, 4.303254, 3.727474, 3.551579, 1.440751],
    [118.673981, 22.426041, 21.599770, 17.609404, 5.723006, 5.366018, 4.434550, 3.850170, 3.665676, 1.500649],
    [121.813866, 23.096418, 22.265446, 18.056187, 5.932699, 5.541013, 4.556446, 3.972466, 3.777972, 1.558547],
    [125.022758, 23.772097, 22.943224, 18.503473, 6.120292, 5.710007, 4.666842, 4.091961, 3.886768, 1.617045],
    [128.215652, 24.459169, 23.778194, 18.929359, 6.287787, 5.894800, 4.796837, 4.226857, 3.970866, 1.642944],
    [128.215652, 24.459169, 23.778194, 18.929359, 6.287787, 5.894800, 4.796837, 4.226857, 3.970866, 1.642944],
    [128.215652, 24.459169, 23.778194, 18.929359, 6.287787, 5.894800, 4.796837, 4.226857, 3.970866, 1.642944],
    [128.215652, 24.459169, 23.778194, 18.929359, 6.287787, 5.894800, 4.796837, 4.226857, 3.970866, 1.642944]]

# Atomic weight in g/mol.
A = [0, 1.008, 4.003, 6.941, 9.012, 10.81, 12.01, 14.01, 16.00, 19.00, 20.18,
     22.99, 24.31, 26.98, 28.09, 30.97, 32.06, 35.45, 39.95, 39.10, 40.08, 44.96, 47.90, 50.94,
     52.00, 54.94, 55.85, 58.93, 58.71, 63.55, 65.37, 69.72, 72.59, 74.92, 78.96, 79.90, 83.80,
     85.47, 87.62, 88.91, 91.22, 92.91, 95.94, 98.91, 101.1, 102.9, 106.4, 107.9, 112.4, 114.8,
     118.7, 121.8, 127.6, 126.9, 131.3, 132.9, 137.3, 138.9, 140.1, 140.9, 144.2, 145, 150.4,
     152.0, 157.3, 158.9, 160.5, 164.9, 167.3, 168.9, 173.0, 175.5, 180.9, 183.9, 186.2, 190.2,
     192.2, 195.1, 197.0, 200.6, 204.4, 209.0, 210, 210, 222, 223, 226.0, 227, 232, 231, 238, 237,
     244, 243, 247, 247, 251, 254, 257, 257, 254, 257]

def square(a):
    return a * a

def mac_zaluzec_cm2_g(wavelength_A, atomic_number_absorber):  # noqa
    """
    Compute mass absorption coefficient from Zaluzec model.

    Le domaine de validite a ete etendue pour 0<e<185 (meme parametre que e>=185).

    @todo Find the unit of the returned mass absorption coefficient.

    @param wavelength_A electron wavelength in Angstrom.
    @param atomic_number_absorber atomic number of the absorber element.
    @return mass absorption coefficient in ??.
    @retval -1.0 if the inputs are out of range of the model.
    """
    l_A = wavelength_A  # noqa
    absorber = atomic_number_absorber

    energy_eV = 12398.1 / l_A  # noqa
    c = 0.
    n = 0.
    c_abs = 0.0

    # Ne fonct. pas puisque pas de raie de plus faible energie que .183
    if energy_eV < 185 and absorber != 1:
        # .. todo:: Bug in CASINO: wavelength passed to macs_total.
        c_abs = macs_total(energy_eV * 1.0e-3, absorber)
        # c_abs = macs_total(l, absorber)
        return c_abs

    if energy_eV > 1487 and absorber != 1:
        # .. todo:: Bug in CASINO: wavelngth passed to macs_total.
        c_abs = macs_total(energy_eV * 1.0e-3, absorber)
        # c_abs = macs_total(l, absorber)
        return c_abs
    else:
        if absorber == HYDROGEN:
            if energy_eV > 1487:
                c_abs = special_equations(energy_eV / 1000.0, absorber)
                return c_abs

            if 679 < energy_eV <= 1487:
                c = 0.001472
                n = 3.359
            if 395 < energy_eV <= 679:
                c = 0.001816
                n = 3.285
            if energy_eV <= 395:
                c = 0.002138
                n = 3.231

            if c == 0.0:
                logging.error("Erreur dans la fonction COEFF_ABS H")
                raise ValueError
            else:
                c_abs = c * math.pow((12398.1 / energy_eV), n)

            if c_abs < 0.0:
                c_abs = 0.0

            return c_abs

        if absorber == BERYLLIUM:
            if 679 < energy_eV <= 1487:
                c = 0.3102
                n = 3.001
            if 285 < energy_eV <= 679:
                c = 0.5060
                n = 2.831
            if energy_eV <= 285:
                c = 2.2480
                n = 2.419

            if c == 0.0:
                logging.error("\n\nerreur dans la fonction COEFF_ABS Be")
                raise ValueError
            else:
                c_abs = c * math.pow((12398.1 / energy_eV), n)

            return c_abs

        if absorber == CARBON:
            if 705 < energy_eV <= 1487:
                c = 1.966
                n = 2.788
            if 285 < energy_eV <= 705:
                c = 4.1290
                n = 2.529
            if energy_eV <= 285:
                c = 0.2572
                n = 2.404

            if c == 0.0:
                logging.error("\n\nerreur dans la fonction COEFF_ABS C")
                raise ValueError
            else:
                c_abs = c * math.pow((12398.1 / energy_eV), n)

            return c_abs

        if absorber == OXYGEN:
            if 532 < energy_eV <= 1487:
                c = 6.9980
                n = 2.573
            if energy_eV <= 532:
                c = .4810
                n = 2.479

            if c == 0.0:
                logging.error("\n\nerreur dans la fonction COEFF_ABS O")
                raise ValueError
            else:
                c_abs = c * math.pow((12398.1 / energy_eV), n)

            return c_abs

        if absorber == ALUMINUM:
            if 556 < energy_eV <= 1487:
                c = 1.2860
                n = 2.712
                c_abs = c * math.pow((12398.1 / energy_eV), n)

                return c_abs
            if energy_eV <= 556:
                c_abs = -7.059e-4 * math.pow(l_A, 4) - \
                        4.815e-2 * math.pow(l_A, 3) + \
                        25.79 * square(l_A) - 3.644e2 * l_A + 1801.

                if c_abs < 0.0:
                    c_abs = 0.0

                return c_abs

            if c_abs == 0.0:
                logging.error("\n\nerreur dans la fonction COEFF_ABS Al")
                raise ValueError

        if absorber == SILICON:
            if 637 < energy_eV <= 1487:
                c = 1.759
                n = 2.706
                c_abs = c * math.pow((12398.1 / energy_eV), n)

                return c_abs

            if energy_eV <= 637:
                c_abs = -0.2407 * math.pow(l_A, 3) + 39.83 * square(l_A) - 527. * l_A + 2278.
                if c_abs < 0.0:
                    c_abs = 0.0

                return c_abs

            if c_abs == 0.0:
                logging.error("\n\nerreur dans la fonction COEFF_ABS Si")
                raise ValueError

        if absorber == GOLD:
            if 776 < energy_eV <= 1487:
                c = 41.17
                n = 1.906
                c_abs = c * math.pow((12398.1 / energy_eV), n)

                return c_abs

            if energy_eV <= 776:
                c_abs = -4.411e-4 * math.pow(l_A, 5) + \
                        9.878e-2 * math.pow(l_A, 4) - \
                        8.218 * math.pow(l_A, 3) + \
                        302. * square(l_A) - 4.466e3 * l_A + 29660.
                if c_abs < 0.0:
                    c_abs = 0.0

                return c_abs

            if c_abs == 0.0:
                logging.error("\n\nerreur dans la fonction COEFF_ABS Au")
                raise ValueError

    return -1.0

def special_equations(energy_keV, atomic_number):  # noqa
    """
    Equations used in mac_zaluzec_cm2_g.

    @param energy_keV
    @param atomic_number
    @return macs
    """
    z = atomic_number

    l_A = 12.3981 / energy_keV  # noqa
    if z == 1:
        if 2.0 >= energy_keV >= 1:
            macs = 3.0353 * math.pow(l_A, 0.01460)
            return macs

        if 3.5 >= energy_keV >= 1:
            macs = 3.5 * math.pow(l_A, 0.05890)
            return macs

        if 4.0 >= energy_keV >= 1:
            macs = 3.5 * math.pow(l_A, 0.07434)
            return macs

        if 6.0 >= energy_keV >= 1:
            macs = 2.937 * math.pow(l_A, 0.27795)
            return macs

        if 9.2 >= energy_keV >= 1:
            macs = 0.627 * math.pow(l_A, .4231)
            return macs

        if 40.0 >= energy_keV >= 1:
            macs = 0.089 * math.pow(l_A, .44767)
            return macs

    if z == 2:
        if 3.7 <= energy_keV <= 6.0:
            macs = 3.7 * math.pow(l_A, .10107)
            return macs

        if energy_keV <= 9.5:
            macs = 2.44950 * math.pow(l_A, .19040)
            return macs

        if energy_keV <= 18.:
            macs = 1.1265 * math.pow(l_A, .21026)
            return macs

        if energy_keV <= 40.:
            macs = 0.08672 * math.pow(l_A, .18714)
            return macs

    if z == 3:
        if 5.90 <= energy_keV <= 8.80:
            macs = 5.9 * math.pow(l_A, .18233)
            return macs

        if energy_keV <= 12.50:
            macs = 4.17123 * math.pow(l_A, 0.26263)
            return macs

        if energy_keV <= 19.20:
            macs = 1.97884 * math.pow(l_A, .26156)
            return macs

        if energy_keV <= 40.:
            macs = .2920 * math.pow(l_A, .22601)
            return macs

    if z == 4:
        if 8.0 <= energy_keV <= 12.50:
            macs = 8. * math.pow(l_A, .38680)
            return macs

        if energy_keV <= 19.80:
            macs = 5.53694 * math.pow(l_A, .38371)
            return macs

        if energy_keV <= 40.:
            macs = 0.63152 * math.pow(l_A, .29211)
            return macs

    if z == 5:
        if 10.2 <= energy_keV <= 15.:
            macs = 10.2 * math.pow(l_A, .62897)
            return macs

        if energy_keV <= 21.:
            macs = 6.84584 * math.pow(l_A, .49970)
            return macs

        if energy_keV <= 31.:
            macs = 3.83941 * math.pow(l_A, .40524)
            return macs

        if energy_keV >= 40.:
            macs = .63152 * math.pow(l_A, .29211)
            return macs

    if z == 6:
        if 13. <= energy_keV <= 21.:
            macs = 13. * math.pow(l_A, 1.10987)
            return macs

        if energy_keV <= 27.5:
            macs = 9.94906 * math.pow(l_A, .74709)
            return macs

        if energy_keV <= 40.:
            macs = 2.588 * math.pow(l_A, .40943)
            return macs

    if z == 7:
        if 15. <= energy_keV < 21.:
            macs = 15. * math.pow(l_A, 1.4640)
            return macs

        if energy_keV <= 30.:
            macs = 12.96310 * math.pow(l_A, 1.06418)
            return macs

        if energy_keV <= 40.:
            macs = 6.26451 * math.pow(l_A, .60023)
            return macs

    if z == 8:
        if 17.2 <= energy_keV <= 25.:
            macs = 17.2 * math.pow(l_A, 1.83450)
            return macs

        if energy_keV <= 40.:
            macs = 13.21194 * math.pow(l_A, 1.07154)
            return macs

    if z == 9:
        if 20. <= energy_keV <= 28.5:
            macs = 20.0 * math.pow(l_A, 2.41791)
            return macs

        if energy_keV <= 40.:
            macs = 14.85851 * math.pow(l_A, 1.19931)
            return macs

    if z == 10:
        if 23. <= energy_keV <= 30.:
            macs = 23. * math.pow(l_A, 3.28063)
            return macs

        if energy_keV <= 40.:
            macs = 19.86943 * math.pow(l_A, 1.79454)
            return macs

    if z == 11:
        if energy_keV <= 25. and energy_keV <= 30.:
            macs = 25. * math.pow(l_A, 3.51621)
            return macs

        if energy_keV <= 40.:
            macs = 22.51741 * math.pow(l_A, 2.05417)
            return macs

    if z == 12:
        if 27.3 <= energy_keV <= 30.:
            macs = 27.3 * math.pow(l_A, 3.19488)
            return macs

    if z == 13:
        if 29.5 <= energy_keV < 40.:
            macs = 29.5 * math.pow(l_A, 3.40890)
            return macs

    if z == 14:
        if 32.5 <= energy_keV <= 40.:
            macs = 32.5 * math.pow(l_A, 4.90164)
            return macs

    if z == 15:
        if energy_keV <= 35. and energy_keV <= 40.:
            macs = 35. * math.pow(l_A, 5.66476)
            return macs

    if z == 16:
        if 37.5 <= energy_keV <= 40.:
            macs = 37.5 * math.pow(l_A, 6.09135)
            return macs

    return 0.0

def macs_henke_ebisu(energy_keV, atomic_number):  # noqa
    """
    Compute mass absorption coefficient from Henke and Ebisu (1974) model.

    Parameterization from tables in files KCOEFF.PRN and LCOEFF.PRN.

    @todo Find the unit of the returned mass absorption coefficient.

    @note Use Zaluzec model for hydrogen absorber.

    @param energy_keV energy in keV
    @param atomic_number atomic number of the absorber.
    @return mass absorption coefficient in ??.
    @retval -1.0 if energy is greater than 1.6 keV.
    @retval -1.0 if absorber is less than 3 or greater than 94.
    """
    if energy_keV <= 0.0:
        raise ValueError

    z = atomic_number

    if z == 1:
        absp = mac_zaluzec_cm2_g(12.3981 / energy_keV, z)
        return absp

    if energy_keV > 1.6:
        return -1

    zmin = 3
    zmax = 94
    if (z < zmin) or (z > zmax):
        return -1

    i = 0
    emin = i
    emax = i + 1
    # ffitfener = 0
    nener = 14

    # Energy and there corresponding columns value in the kcoeff.prn (0) and lcoeff.prn (1) files.
    # position is a pair of (fileIndex, columnIndex).
    # Updated value to have the same energy for x-ray line and in the files.
    energies_keV = [0.183, 0.277, 0.392, 0.452, 0.525, 0.573, 0.637, 0.677, 0.705, 0.776, 0.848, 0.852, 0.930, 1.012]  # noqa
    # Original values
    # energie[14] ={0.185, 0.281, 0.392, 0.452, 0.525, 0.573, 0.637, 0.677, 0.705, 0.776, 0.849, 0.852, 0.930, 1.012}
    position = [[0, 1], [0, 2], [0, 3], [1, 1], [0, 4], [1, 3], [1, 4], [0, 5], [1, 5], [1, 6], [0, 6], [1, 7], [1, 8],
                [1, 9]]

    if 0.183 <= energy_keV <= 1.012:
        # Recherche de position dans les fichiers.
        condition = True
        while condition:
            emin = i
            emax = i + 1
            i += 1

            condition = ((energies_keV[emax] < energy_keV) and (i < nener))

        ffitfener = 0
    else:
        if energy_keV < 0.183:
            emin = 1
            emax = 2
            ffitfener = 1
        else:
            # Position par defaut si extrapolation au-dela de 1.01.
            emin = 12
            emax = 13
            ffitfener = 0

    posfmin = position[emin][0]
    posfmax = position[emax][0]
    poscmin = position[emin][1]
    poscmax = position[emax][1]
    posl = z - 2

    # kcol = 6
    # lcol = 9
    #
    # absmin = 0.0
    # absmax = 0.0

    # for INI File
    file_path_K = get_current_module_path(__file__, "../../../data/casino/KCOEFF.PRN")  # noqa
    file_path_L = get_current_module_path(__file__, "../../../data/casino/LCOEFF.PRN")  # noqa

    if posfmin == 0:
        with open(file_path_K, 'r') as kcoeff:
            lines = kcoeff.readlines()

            items = lines[posl - 1].split()
            absmin = float(items[poscmin - 1])
    else:
        with open(file_path_L, 'r') as lcoeff:
            lines = lcoeff.readlines()

            items = lines[posl - 1].split()
            absmin = float(items[poscmin - 1])

    if posfmax == 0:
        with open(file_path_K, 'r') as kcoeff:
            lines = kcoeff.readlines()

            items = lines[posl - 1].split()
            absmax = float(items[poscmax - 1])
    else:
        with open(file_path_L, 'r') as lcoeff:
            lines = lcoeff.readlines()

            items = lines[posl - 1].split()
            absmax = float(items[poscmax - 1])

    # absp = 0.0
    if ffitfener == 0:
        absp = ((absmax - absmin) / (energies_keV[emax] - energies_keV[emin])) * (energy_keV - energies_keV[emin]) + \
               absmin
    else:
        # Fit exponentiel a faible energie.
        absp = math.exp(
            ((math.log(absmax) - math.log(absmin)) / (math.log(energies_keV[emax]) - math.log(energies_keV[emin]))) * (
                        math.log(energy_keV) - math.log(energies_keV[emin])) + math.log(absmin))

    return absp

def macs_total(energy_keV, atomic_number):  # noqa
    """
    Compute mass absorption coefficient by combining Leroux and Henke-Ebisu models.

    @note For energy less or equal than 1.01 keV it use Henke-Ebisu model.
    @note For energy greater than 1.01 keV it use Heinrich model.

    @todo Find the unit of the returned mass absorption coefficient.

    @param energy_keV electron energy in keV.
    @param atomic_number atomic number of the absorber
    @return mass absorption coefficient in ??.
    """

    if energy_keV <= 1.01:
        abst = macs_henke_ebisu(energy_keV, atomic_number)
    else:
        abst = macs_heinrich(12.3981 / energy_keV, atomic_number)

    return abst

def macs_heinrich(wavelength_A, atomic_number_absorber):  # noqa
    """
    Compute mass absorption coefficient from Heinrich model.

    @todo Find the unit of the returned mass absorption coefficient.

    @param wavelength_A electron wavelength in Angstrom.
    @param atomic_number_absorber
    @return mass absorption coefficient in ??.
    """
    l_A = wavelength_A  # noqa

    z = atomic_number_absorber
    atomic_number = atomic_number_absorber

    # c_total = 0

    ntot = 0

    atot = 0

    btot = 0
    bmult = 1
    cmult = 1

    energy_c_keV = np.zeros(10)  # noqa
    n = np.zeros(4)
    a = np.zeros(5)
    b = np.zeros(5)
    c_tot = np.zeros(6)
    C = np.zeros((6, 7))  # noqa

    for i in range(10):
        energy_c_keV[i] = transitions[atomic_number - 1][i]

    energy_keV = 12.3981 / l_A  # noqa

    if energy_keV > energy_c_keV[0]:
        if atomic_number < 6:
            C[0][0] = -2.87536e-4
            C[0][1] = 1.808599e-3
            C[0][2] = 0
            C[0][3] = 0
            C[0][4] = 0
            C[0][5] = 0

            n[0] = 3.34745
            n[1] = 0.02652873
            n[2] = -0.01273815
            n[3] = 0

            a[0] = 24.4545
            a[1] = 155.6055
            a[2] = -14.15422
            a[3] = 0
            a[4] = 0

            b[0] = -103.0
            b[1] = -18.2
            b[2] = 0
            b[3] = 0
            b[4] = 0

        if atomic_number > 5:
            C[0][0] = 5.253e-3
            C[0][1] = 1.33257e-3
            C[0][2] = -7.5937e-5
            C[0][3] = 1.69357e-6
            C[0][4] = -1.3975e-8
            C[0][5] = 0

            n[0] = 3.112
            n[1] = -0.0121
            n[2] = 0
            n[3] = 0

            a[0] = 0
            a[1] = 47.0
            a[2] = 6.52
            a[3] = -0.152624
            a[4] = 0

            b[0] = 0
            b[1] = 0
            b[2] = 0
            b[3] = 0
            b[4] = 0

    if energy_c_keV[0] > energy_keV > energy_c_keV[3]:
        C[0][0] = -9.24e-5
        C[0][1] = 1.41478e-4
        C[0][2] = -5.24999e-6
        C[0][3] = 9.85296e-8
        C[0][4] = -9.07306e-10
        C[0][5] = 3.19254e-12

        n[0] = 2.7575
        n[1] = 1.889e-3
        n[2] = -4.982e-5
        n[3] = 0

        a[0] = 0
        a[1] = 17.8096
        a[2] = 0.067429
        a[3] = 0.01253775
        a[4] = -1.16286e-4

        b[0] = 0
        b[1] = 0
        b[2] = 0
        b[3] = 0
        b[4] = 0

        if energy_c_keV[0] > energy_keV > energy_c_keV[1]:
            cmult = 1
        if energy_c_keV[1] > energy_keV > energy_c_keV[2]:
            cmult = 0.858
        if energy_c_keV[2] > energy_keV > energy_c_keV[3]:
            cmult = (0.8933 - z * 8.29e-3 + math.pow(z, 2) * 6.38e-5)

    if energy_c_keV[3] > energy_keV > energy_c_keV[4]:
        if atomic_number < 30:
            C[0][0] = 1.889757e-2
            C[0][1] = -1.8517159e-3
            C[0][2] = 6.9602789e-5
            C[0][3] = -1.1641145e-6
            C[0][4] = 7.2773258e-9
            C[0][5] = 0

        if atomic_number > 29:
            C[0][0] = 3.0039e-3
            C[0][1] = -1.73663566e-4
            C[0][2] = 4.0424792e-6
            C[0][3] = -4.0585911e-8
            C[0][4] = 1.497763e-10
            C[0][5] = 0

        n[0] = 0.5385
        n[1] = 0.084597
        n[2] = -1.08246e-3
        n[3] = 4.4509e-6

        a[0] = 0
        a[1] = 10.2575657
        a[2] = -0.822863477
        a[3] = 2.63199611e-2
        a[4] = -1.8641019e-4

        if atomic_number < 61:
            b[0] = 0
            b[1] = 5.654
            b[2] = -0.536839169
            b[3] = 0.018972278
            b[4] = -1.683474e-4

        if atomic_number > 60:
            b[0] = 0
            b[1] = -1232.4022
            b[2] = 51.114164
            b[3] = -0.699473097
            b[4] = 3.1779619e-3

    if energy_c_keV[4] > energy_keV > energy_c_keV[8]:
        C[0][0] = 7.7708e-5
        C[0][1] = -7.83544e-6
        C[0][2] = 2.209365e-7
        C[0][3] = -1.29086e-9
        C[0][4] = 0
        C[0][5] = 0

        C[1][0] = 1.406
        C[1][1] = 0.0162
        C[1][2] = -6.561e-4
        C[1][3] = 4.865e-6
        C[1][4] = 0
        C[1][5] = 0

        C[2][0] = 0.584
        C[2][1] = 0.01955
        C[2][2] = -1.285e-4
        C[2][3] = 0
        C[2][4] = 0
        C[2][5] = 0

        C[3][0] = 1.082
        C[3][1] = 1.366e-3
        C[3][2] = 0
        C[3][3] = 0
        C[3][4] = 0
        C[3][5] = 0

        C[4][0] = 1.6442
        C[4][1] = -0.0480
        C[4][2] = 4.0664e-4
        C[4][3] = 0
        C[4][4] = 0
        C[4][5] = 0

        n[0] = 3.0
        n[1] = -0.004
        n[2] = 0
        n[3] = 0

        a[0] = 0
        a[1] = 4.62
        a[2] = -0.04
        a[3] = 0
        a[4] = 0

        b[0] = 2.51
        b[1] = -0.052
        b[2] = 3.78e-4
        b[3] = 0
        b[4] = 0

        bmult = energy_c_keV[7]

    if energy_c_keV[8] > energy_keV > energy_c_keV[9]:
        C[0][0] = 4.3156e-3
        C[0][1] = -1.4653e-4
        C[0][2] = 1.707073e-6
        C[0][3] = -6.69827e-9
        C[0][4] = 0
        C[0][5] = 0

        cmult = 1.08

        n[0] = 0.3736
        n[1] = 0.02401
        n[2] = 0
        n[3] = 0

        a[0] = 0
        a[1] = 19.64
        a[2] = -0.61239
        a[3] = 5.39309e-3
        a[4] = 0

        b[0] = -113.0
        b[1] = 4.5
        b[2] = 0
        b[3] = 0
        b[4] = 0

    if energy_keV < energy_c_keV[9]:
        cutoff = (0.252 * z - 31.1812) * z + 1042

        C[0][0] = 4.3156e-3
        C[0][1] = -1.4653e-4
        C[0][2] = 1.707073e-6
        C[0][3] = -6.69827e-9
        C[0][4] = 0
        C[0][5] = 0

        cmult = 1.08

        a[0] = 0
        a[1] = 19.64
        a[2] = -0.61239
        a[3] = 5.39309e-3
        a[4] = 0

    for i in range(5):
        for j in range(6):
            c_tot[i] += cmult * C[i][j] * math.pow(z, j)
    for i in range(4):
        ntot += n[i] * math.pow(z, i)
    for i in range(5):
        atot += a[i] * math.pow(z, i)
    for i in range(5):
        btot += bmult * b[i] * math.pow(z, i)

    if energy_c_keV[4] > energy_keV > energy_c_keV[5]:
        c_total = c_tot[0] * c_tot[1] * c_tot[2]
    elif energy_c_keV[5] > energy_keV > energy_c_keV[6]:
        c_total = c_tot[0] * c_tot[1] * c_tot[3]
    elif energy_c_keV[6] > energy_keV > energy_c_keV[7]:
        c_total = 0.95 * c_tot[0] * c_tot[1]
    elif energy_c_keV[7] > energy_keV > energy_c_keV[8]:
        c_total = c_tot[0] * c_tot[1] * c_tot[4]
    else:
        c_total = c_tot[0]

    macsh = c_total * math.pow(z, 4) / A[atomic_number] * math.pow((12.397 / energy_keV), ntot) * (
                1 - math.exp((-energy_keV * 1000 + btot) / atot))

    if energy_keV < energy_c_keV[9]:
        macsh = 1.02 * c_total * math.pow((12.397 / energy_keV), ntot) * c_total * math.pow(z, 4) / A[atomic_number] * (
                    (energy_keV * 1000 - cutoff) / (energy_c_keV[9] - cutoff))

    return macsh

def efficiency(energy_keV):  # noqa
    """
    Compute x-ray detector collection efficiency.

    If energy is less than 7 keV, include absorption from:
    - Al layer
    - Formvar layer
    - Au layer
    - Si dead layer
    - Be window
    - Carbon contamination layer
    - Ice contamination layer

    If energy is greater than 15 keV, include the effect of Si thickness.

    If energy is between 7 and 15 keV, efficiency is one.

    If energy is less than 0.03 keV, the efficiency is zero.

    @param energy_keV x-ray energy in keV.
    @return fraction of detected x-ray.
    """

    # detector parameters.
    parameters = {}
    # Aluminium.
    parameters[1] = 120e-7
    # Parylene.
    parameters[2] = 130e-7
    # Au.
    parameters[3] = 0.0
    # Couche morte de Si.
    parameters[4] = 50e-7
    # Berrylium.
    parameters[5] = 0.0
    # Carbone.
    parameters[6] = 0.0
    # Glace H2O.
    parameters[7] = 0.0
    # Cristal.
    parameters[8] = 0.3

    l_A = 12.3981 / energy_keV  # noqa

    if energy_keV < 7.0:
        if energy_keV > 0.03:
            if parameters[1] != 0.0:
                a_al = mac_zaluzec_cm2_g(l_A, 13) * MASS_DENSITY_AL_g_cm3 * parameters[1]
            else:
                a_al = 0

            # APA=(CH_C6H6*mac_zaluzec_cm2_g(l,1)+CC*mac_zaluzec_cm2_g(l,6))*D_C*a[2]

            if parameters[2] != 0.0:
                a_formvar = (0.0707 * mac_zaluzec_cm2_g(l_A, 1) +
                             0.6063 * mac_zaluzec_cm2_g(l_A, 6) +
                             0.3231 * mac_zaluzec_cm2_g(l_A, 8)) * MASS_DENSITY_C_g_cm3 * parameters[2]
            else:
                a_formvar = 0

            if parameters[3] != 0.0:
                a_au = mac_zaluzec_cm2_g(l_A, 79) * MASS_DENSITY_AU_g_cm3 * parameters[3]
            else:
                a_au = 0

            if parameters[4] != 0.0:
                a_si_dl = mac_zaluzec_cm2_g(l_A, 14) * MASS_DENSITY_SI_g_cm3 * parameters[4]
            else:
                a_si_dl = 0

            if parameters[5] != 0.0:
                a_be = mac_zaluzec_cm2_g(l_A, 4) * MASS_DENSITY_BE_g_cm3 * parameters[5]
            else:
                a_be = 0

            if parameters[6] != 0.0:
                a_c = mac_zaluzec_cm2_g(l_A, 6) * MASS_DENSITY_C_g_cm3 * parameters[6]
            else:
                a_c = 0

            if parameters[7] != 0.0:
                a_h2o = (CH_H2O * mac_zaluzec_cm2_g(l_A, 1) + CO * mac_zaluzec_cm2_g(l_A, 8)) * \
                        MASS_DENSITY_H2O_g_cm3 * parameters[7]
            else:
                a_h2o = 0

            eff = math.exp(-a_h2o - a_al - a_formvar - a_au - a_si_dl - a_be - a_c)
            return eff
        else:
            return 0.0

    if energy_keV > 15.0:
        tsi = parameters[8]
        asi = macs_total(12.3981 / l_A, 14) * MASS_DENSITY_SI_g_cm3 * tsi
        eff = (1.0 - math.exp(-asi))

        return eff

    if 7.0 < energy_keV < 10.0:
        return 1.0

    return 1.0


# -*- coding: utf-8 -*-

# Cage deformer for Maya based on Mean value coordinates
#
#  @author      Shizuo KAJI
#  @date        2013/5/13

# usage:
#   Load the plugin from "Plugin Manager"
#   Select a target mesh and type the following from the script editor:
#        import maya.cmds as cmds
#        deformer = cmds.deformer(type='cageDeformerMVC')[0]
#   Then, select a cage mesh and type the following:
#        shape=pm.selected( type="transform" )[0].getShapes()[0]
#        cmds.connectAttr(shape+".outMesh", deformer+".cageMesh")
#   Wait for a moment, and edit the cage mesh

import maya.OpenMayaMPx as OpenMayaMPx
import maya.OpenMaya as OpenMaya
import numpy as np

OFF = 10
ON = 0

class CageDeformerNode(OpenMayaMPx.MPxDeformerNode):
    kPluginNodeId = OpenMaya.MTypeId(0x000000020)
    kPluginNodeName = 'cageDeformerMVC'

    aCageMesh = OpenMaya.MObject()

    w=[]   # weights
    p=[]   # vertices of the cage

    def __init__(self):
        OpenMayaMPx.MPxDeformerNode.__init__(self)

    def deform(self, data, itGeo, localToWorldMatrix, mIndex):
        blendMode = data.inputValue( CageDeformerNode.aBlendMode ).asShort()
        if blendMode == OFF:
            self._resetPosition(itGeo)
            return
        CageMesh = data.inputValue(CageDeformerNode.aCageMesh).asMesh()
        if CageMesh.isNull():
            return
        cageMesh = OpenMaya.MFnMesh(CageMesh)
        q=self._getPoints(cageMesh)
        if len(self.p)!= len(q):
            # initialisation
            self.p=q
            tri=self._getTri(cageMesh)
            self.w=[]
            while not itGeo.isDone():
                mu=np.zeros(len(q))
                pt = itGeo.position()
                v = np.array([pt.x, pt.y, pt.z])
                for tr in tri:
                    e=[v-q[tr[i]] for i in range(3)]
                    en=[e[i]/np.linalg.norm(e[i]) for i in range(3)]
                    b=[np.arccos(np.dot(en[(i+1)%3],en[(i+2)%3])) for i in range(3)]
                    n=[np.cross(en[(i+1)%3],en[(i+2)%3]) for i in range(3)]
                    nu=[n[i]/np.linalg.norm(n[i]) for i in range(3)]
                    a=[np.dot(nu[(i+1)%3],nu[(i+2)%3]) for i in range(3)]
                    for i in range(3):
                        mu[tr[i]]-=(b[i]+b[(i+2)%3]*a[(i+1)%3]+b[(i+1)%3]*a[(i+2)%3])/(2.0*np.dot(en[i],nu[i]))
                mu /= np.array([np.linalg.norm(v-q[j]) for j in range(len(q))])
                smu=sum(mu)
                self.w.append(np.array([mu[j]/smu for j in range(len(q))]))
                itGeo.next()
            return

        # run-time
        pts = OpenMaya.MPointArray()
        itGeo.allPositions(pts)
        for i in range(pts.length()):
            pos = sum([self.w[i][j]*q[j] for j in range(len(q))])
            pts[i].x, pts[i].y, pts[i].z=[pos[0],pos[1],pos[2]]
        itGeo.setAllPositions(pts)
        return

    def _resetPosition(self,itGeo):
        while not itGeo.isDone():
            pt = itGeo.position()
            itGeo.setPosition(pt)
            itGeo.next()
        return

    def _getPoints(self, mesh):
        qs = OpenMaya.MPointArray()
        mesh.getPoints(qs)
        q=[np.array( [ qs[i].x, qs[i].y, qs[i].z ]) for i in range(qs.length())]
        return q

    def _getTri(self, mesh):
        count = OpenMaya.MIntArray()
        tl = OpenMaya.MIntArray( )
        mesh.getTriangles(count, tl)
        num=len(tl)/3
        tri = [ (tl[3*i], tl[3*i+1], tl[3*i+2]) for i in range(num)]
        return tri

def creator():
    return OpenMayaMPx.asMPxPtr(CageDeformerNode())

def initialize():
    outputGeom = OpenMayaMPx.cvar.MPxDeformerNode_outputGeom

    tAttr = OpenMaya.MFnTypedAttribute()
    CageDeformerNode.aCageMesh = tAttr.create('cageMesh', 'cm', OpenMaya.MFnData.kMesh)
    tAttr.setStorable(False)
    CageDeformerNode.addAttribute( CageDeformerNode.aCageMesh )
    CageDeformerNode.attributeAffects(CageDeformerNode.aCageMesh, outputGeom)

    # interpolation mode
    eAttr = OpenMaya.MFnEnumAttribute()
    CageDeformerNode.aBlendMode = eAttr.create( "blendMode", "bm", 0 )
    eAttr.addField( "MVC", ON )
    eAttr.addField( "off", OFF )
    eAttr.setStorable( True )
    CageDeformerNode.addAttribute( CageDeformerNode.aBlendMode)
    CageDeformerNode.attributeAffects(CageDeformerNode.aBlendMode, outputGeom)

def initializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj, 'Shizuo KAJI', '1.0', 'Any')
    try:
        plugin.registerNode(CageDeformerNode.kPluginNodeName, CageDeformerNode.kPluginNodeId, creator, initialize, OpenMayaMPx.MPxNode.kDeformerNode)
    except:
        raise RuntimeError, 'Failed to register node'

def uninitializePlugin(obj):
    plugin = OpenMayaMPx.MFnPlugin(obj)
    try:
        plugin.deregisterNode(CageDeformerNode.kPluginNodeId)
    except:
        raise RuntimeError, 'Failed to deregister node'