"""
:mod:`mirgecom.eos` provides implementations of gas equations of state.

Equations of State
^^^^^^^^^^^^^^^^^^
This module is designed provide Equation of State objects used to compute and
manage the relationships between and among state and thermodynamic variables.

.. autoclass:: EOSDependentVars
.. autoclass:: GasEOS
.. autoclass:: MixtureEOS
.. autoclass:: IdealSingleGas
.. autoclass:: PyrometheusMixture
"""

__copyright__ = """
Copyright (C) 2021 University of Illinois Board of Trustees
"""

__license__ = """
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
"""

from dataclasses import dataclass
import numpy as np
from pytools import memoize_in
from meshmode.mesh import BTAG_ALL, BTAG_NONE  # noqa
from mirgecom.fluid import ConservedVars, make_conserved
from abc import ABCMeta, abstractmethod

@dataclass
class EOSDependentVars:
    """State-dependent quantities for :class:`GasEOS`.

    Prefer individual methods for model use, use this
    structure for visualization or probing.

    .. attribute:: temperature
    .. attribute:: pressure
    """

    temperature: np.ndarray
    pressure: np.ndarray

class GasEOS(metaclass=ABCMeta):
    r"""Abstract interface to equation of state class.

    Equation of state (EOS) classes are responsible for
    computing relations between fluid or gas state variables.

    Each interface call takes an :class:`~mirgecom.fluid.ConservedVars` object
    array representing the simulation state quantities. Each EOS class
    implementation should document its own state data requirements.

    .. automethod:: pressure
    .. automethod:: temperature
    .. automethod:: sound_speed
    .. automethod:: internal_energy
    .. automethod:: gas_const
    .. automethod:: dependent_vars
    .. automethod:: total_energy
    .. automethod:: kinetic_energy
    .. automethod:: gamma
    .. automethod:: transport_model
    .. automethod:: get_internal_energy
    """

    @abstractmethod
    def pressure(self, cv: ConservedVars):
        """Get the gas pressure."""

    @abstractmethod
    def temperature(self, cv: ConservedVars):
        """Get the gas temperature."""

    @abstractmethod
    def sound_speed(self, cv: ConservedVars):
        """Get the gas sound speed."""

    @abstractmethod
    def gas_const(self, cv: ConservedVars):
        r"""Get the specific gas constant ($R_s$)."""

    @abstractmethod
    def heat_capacity_cp(self, cv: ConservedVars):
        r"""Get the specific heat capacity at constant pressure ($C_p$)."""

    @abstractmethod
    def heat_capacity_cv(self, cv: ConservedVars):
        r"""Get the specific heat capacity at constant volume ($C_v$)."""

    @abstractmethod
    def internal_energy(self, cv: ConservedVars):
        """Get the thermal energy of the gas."""

    @abstractmethod
    def total_energy(self, cv: ConservedVars, pressure: np.ndarray):
        """Get the total (thermal + kinetic) energy for the gas."""

    @abstractmethod
    def kinetic_energy(self, cv: ConservedVars):
        """Get the kinetic energy for the gas."""

    @abstractmethod
    def gamma(self, cv: ConservedVars):
        """Get the ratio of gas specific heats Cp/Cv."""

    @abstractmethod
    def transport_model(self):
        """Get the transport model if it exists."""

    @abstractmethod
    def get_internal_energy(self, temperature, *, mass, species_mass_fractions):
        """Get the fluid internal energy from temperature and mass."""

    def dependent_vars(self, cv: ConservedVars) -> EOSDependentVars:
        """Get an agglomerated array of the dependent variables."""
        return EOSDependentVars(
            pressure=self.pressure(cv),
            temperature=self.temperature(cv),
        )

class MixtureEOS(GasEOS):
    r"""Abstract interface to gas mixture equation of state class.

    This EOS base class extends the GasEOS base class to include the
    necessary interface for dealing with gas mixtures.

    .. automethod:: get_density
    .. automethod:: get_species_molecular_weights
    .. automethod:: get_production_rates
    .. automethod:: species_enthalpies
    .. automethod:: get_species_source_terms
    """

    @abstractmethod
    def get_density(self, pressure, temperature, species_mass_fractions):
        """Get the density from pressure, temperature, and species fractions (Y)."""

    @abstractmethod
    def get_species_molecular_weights(self):
        """Get the species molecular weights."""

    @abstractmethod
    def species_enthalpies(self, cv: ConservedVars):
        """Get the species specific enthalpies."""

    @abstractmethod
    def get_production_rates(self, cv: ConservedVars):
        """Get the production rate for each species."""

    @abstractmethod
    def get_species_source_terms(self, cv: ConservedVars):
        r"""Get the species mass source terms to be used on the RHS for chemistry."""

class IdealSingleGas(GasEOS):
    r"""Ideal gas law single-component gas ($p = \rho{R}{T}$).

    The specific gas constant, R, defaults to the air-like 287.1 J/(kg*K),
    but can be set according to simulation units and materials.

    Each interface call expects that the :class:`~mirgecom.fluid.ConservedVars`
    object representing the simulation conserved quantities contains at least
    the canonical conserved quantities mass ($\rho$), energy ($\rho{E}$), and
    momentum ($\rho\vec{V}$).

    .. automethod:: __init__
    .. automethod:: pressure
    .. automethod:: temperature
    .. automethod:: sound_speed
    .. automethod:: internal_energy
    .. automethod:: gas_const
    .. automethod:: dependent_vars
    .. automethod:: total_energy
    .. automethod:: kinetic_energy
    .. automethod:: gamma
    .. automethod:: transport_model
    .. automethod:: get_internal_energy
    """

    def __init__(self, gamma=1.4, gas_const=287.1, transport_model=None):
        """Initialize Ideal Gas EOS parameters."""
        self._gamma = gamma
        self._gas_const = gas_const
        self._transport_model = transport_model

    def transport_model(self):
        """Get the transport model object for this EOS."""
        return self._transport_model

    def gamma(self, cv: ConservedVars = None):
        """Get specific heat ratio Cp/Cv."""
        return self._gamma

    def heat_capacity_cp(self, cv: ConservedVars = None):
        r"""Get specific heat capacity at constant pressure.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector of
            species masses, ($\rho{Y}_\alpha$).
        """
        return self._gas_const * self._gamma / (self._gamma - 1)

    def heat_capacity_cv(self, cv: ConservedVars = None):
        r"""Get specific heat capacity at constant volume.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector of
            species masses, ($\rho{Y}_\alpha$).
        """
        return self._gas_const / (self._gamma - 1)

    def gas_const(self, cv: ConservedVars = None):
        """Get specific gas constant R."""
        return self._gas_const

    def kinetic_energy(self, cv: ConservedVars):
        r"""Get kinetic (i.e. not internal) energy of gas.

        The kinetic energy is calculated as:

        .. math::

            k = \frac{1}{2\rho}(\rho\vec{V} \cdot \rho\vec{V})

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The kinetic energy of the fluid flow
        """
        mom = cv.momentum
        return (0.5 * np.dot(mom, mom) / cv.mass)

    def internal_energy(self, cv: ConservedVars):
        r"""Get internal thermal energy of gas.

        The internal energy (e) is calculated as:

        .. math::

            e = \rho{E} - \frac{1}{2\rho}(\rho\vec{V} \cdot \rho\vec{V})

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The internal energy of the fluid material
        """
        return (cv.energy - self.kinetic_energy(cv))

    def pressure(self, cv: ConservedVars):
        r"""Get thermodynamic pressure of the gas.

        Gas pressure (p) is calculated from the internal energy (e) as:

        .. math::

            p = (\gamma - 1)e

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The fluid pressure
        """
        return self.internal_energy(cv) * (self._gamma - 1.0)

    def sound_speed(self, cv: ConservedVars):
        r"""Get the speed of sound in the gas.

        The speed of sound (c) is calculated as:

        .. math::

            c = \sqrt{\frac{\gamma{p}}{\rho}}

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The speed of sound in the fluid
        """
        actx = cv.array_context
        return actx.np.sqrt(self._gamma / cv.mass * self.pressure(cv))

    def temperature(self, cv: ConservedVars):
        r"""Get the thermodynamic temperature of the gas.

        The thermodynamic temperature (T) is calculated from
        the internal energy (e) and specific gas constant (R)
        as:

        .. math::

            T = \frac{(\gamma - 1)e}{R\rho}

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The fluid temperature
        """
        return (
            (((self._gamma - 1.0) / self._gas_const)
             * self.internal_energy(cv) / cv.mass)
        )

    def total_energy(self, cv, pressure):
        r"""
        Get gas total energy from mass, pressure, and momentum.

        The total energy density (rhoE) is calculated from
        the mass density (rho) , pressure (p) , and
        momentum (rhoV) as:

        .. math::

            \rho{E} = \frac{p}{(\gamma - 1)} +
            \frac{1}{2}\rho(\vec{v} \cdot \vec{v})

        .. note::

            The total_energy function computes cv.energy from pressure,
            mass, and momentum in this case. In general in the EOS we need
            DV = EOS(CV), and inversions CV = EOS(DV). This is one of those
            inversion interfaces.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$).

        pressure: :class:`~meshmode.dof_array.DOFArray`
            The fluid pressure

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The total energy of the fluid (i.e. internal + kinetic)
        """
        return (pressure / (self._gamma - 1.0)
                + self.kinetic_energy(cv))

    def get_internal_energy(self, temperature, mass, species_mass_fractions=None):
        r"""Get the gas thermal energy from temperature, and fluid density.

        The gas internal energy $e$ is calculated from:

        .. math::

            e = R_s T \frac{\rho}{\left(\gamma - 1\right)}

        Parameters
        ----------
        temperature: :class:`~meshmode.dof_array.DOFArray`
            The fluid temperature
        mass: float or :class:`~meshmode.dof_array.DOFArray`
            The fluid mass density
        species_mass_fractions:
            Unused
        """
        return self._gas_const * mass * temperature / (self._gamma - 1)

class PyrometheusMixture(MixtureEOS):
    r"""Ideal gas mixture ($p = \rho{R}_\mathtt{mix}{T}$).

    This is the :mod:`pyrometheus`-based EOS. Please refer to the :any:`documentation
    of Pyrometheus <pyrometheus>` for underlying implementation details.

    Each interface call expects that the :class:`mirgecom.fluid.ConservedVars` object
    representing the simulation conserved quantities contains at least the
    canonical conserved quantities mass ($\rho$), energy ($\rho{E}$), and
    momentum ($\rho\vec{V}$), and the vector of species masses, ($\rho{Y}_\alpha$).

    .. important::
        When using this EOS, users should take care to match solution initialization
        with the appropriate units that are used in the user-provided `Cantera`
        mechanism input files.

    .. automethod:: __init__
    .. automethod:: pressure
    .. automethod:: temperature
    .. automethod:: sound_speed
    .. automethod:: internal_energy
    .. automethod:: gas_const
    .. automethod:: dependent_vars
    .. automethod:: total_energy
    .. automethod:: kinetic_energy
    .. automethod:: gamma
    .. automethod:: transport_model
    .. automethod:: get_internal_energy
    .. automethod:: get_density
    .. automethod:: get_species_molecular_weights
    .. automethod:: get_production_rates
    .. automethod:: species_enthalpies
    .. automethod:: get_species_source_terms
    """

    def __init__(self, pyrometheus_mech, temperature_guess=300.0,
                 transport_model=None):
        """Initialize Pyrometheus-based EOS with mechanism class.

        Parameters
        ----------
        pyrometheus_mech: :class:`pyrometheus.Thermochemistry`
            The :mod:`pyrometheus`  mechanism :class:`~pyrometheus.Thermochemistry`
            object that is generated by the user with a call to
            *pyrometheus.get_thermochem_class*. To create the mechanism
            object, users need to provide a mechanism input file. Several built-in
            mechanisms are provided in `mirgecom/mechanisms/` and can be used through
            the :meth:`mirgecom.mechanisms.get_mechanism_cti`.

        tguess: float
            This provides a constant starting temperature for the Newton iterations
            used to find the mixture temperature. It defaults to 300.0 Kelvin. This
            parameter is important for the performance and proper function of the
            code. Users should set a tguess that is close to the average temperature
            of the simulated domain.  Ideally, we would use the last computed
            temperature for the guess, but doing so requires restart infrastructure
            that is TBD.
        """
        self._pyrometheus_mech = pyrometheus_mech
        self._tguess = temperature_guess
        self._transport_model = transport_model

    def transport_model(self):
        """Get the transport model object for this EOS."""
        return self._transport_model

    def heat_capacity_cp(self, cv: ConservedVars):
        r"""Get mixture-averaged specific heat capacity at constant pressure.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector of
            species masses, ($\rho{Y}_\alpha$).
        """
        temp = self.temperature(cv)
        y = cv.species_mass_fractions
        return self._pyrometheus_mech.get_mixture_specific_heat_cp_mass(temp, y)

    def heat_capacity_cv(self, cv: ConservedVars):
        r"""Get mixture-averaged specific heat capacity at constant volume.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector of
            species masses, ($\rho{Y}_\alpha$).
        """
        temp = self.temperature(cv)
        y = cv.species_mass_fractions
        return (
            self._pyrometheus_mech.get_mixture_specific_heat_cp_mass(temp, y)
            / self.gamma(cv)
        )

    def gamma(self, cv: ConservedVars):
        r"""Get mixture-averaged specific heat ratio for mixture $\frac{C_p}{C_p - R_s}$.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector of
            species masses, ($\rho{Y}_\alpha$).
        """
        temperature = self.temperature(cv)
        y = cv.species_mass_fractions
        cp = self._pyrometheus_mech.get_mixture_specific_heat_cp_mass(temperature, y)
        rspec = self.gas_const(cv)
        return cp / (cp - rspec)

    def gas_const(self, cv: ConservedVars):
        r"""Get specific gas constant $R_s$.

        The mixture specific gas constant is calculated
        as $R_s = \frac{R}{\sum{\frac{{Y}_\alpha}{{M}_\alpha}}}$ by the
        :mod:`pyrometheus` mechanism provided by the user. ${M}_\alpha$ are the
        species molar masses.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).
        """
        y = cv.species_mass_fractions
        return self._pyrometheus_mech.get_specific_gas_constant(y)

    def kinetic_energy(self, cv: ConservedVars):
        r"""Get kinetic (i.e. not internal) energy of gas.

        The kinetic energy is calculated as:

        .. math::

            k = \frac{1}{2\rho}(\rho\vec{V} \cdot \rho\vec{V})

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).
        """
        mom = cv.momentum
        return (0.5 * np.dot(mom, mom) / cv.mass)

    def internal_energy(self, cv: ConservedVars):
        r"""Get internal thermal energy of gas.

        The internal energy ($e$) is calculated as:

        .. math::

            e = \rho{E} - \frac{1}{2\rho}(\rho\vec{V} \cdot \rho\vec{V})

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            Internal energy of the fluid
        """
        return (cv.energy - self.kinetic_energy(cv))

    def get_density(self, pressure, temperature, species_mass_fractions):
        r"""Get the density from pressure, temperature, and species fractions (Y).

        The gas density $\rho$ is calculated from pressure, temperature and $R$ as:

        .. math::

            \rho = \frac{p}{R_s T}

        Parameters
        ----------
        pressure: :class:`~meshmode.dof_array.DOFArray`
            The fluid pressure
        temperature: :class:`~meshmode.dof_array.DOFArray`
            The fluid temperature
        species_mass_fractions: numpy.ndarray
            Object array of species mass fractions

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The total fluid mass density
        """
        return self._pyrometheus_mech.get_density(pressure, temperature,
                                                  species_mass_fractions)

    def get_internal_energy(self, temperature, species_mass_fractions,
                            mass=None):
        r"""Get the gas thermal energy from temperature, and species fractions (Y).

        The gas internal energy $e$ is calculated from:

        .. math::

            e = R_s T \sum{Y_\alpha h_\alpha}

        Parameters
        ----------
        temperature: :class:`~meshmode.dof_array.DOFArray`
            The fluid temperature
        species_mass_fractions: numpy.ndarray
            Object array of species mass fractions
        mass:
            Unused
        """
        return self._pyrometheus_mech.get_mixture_internal_energy_mass(
            temperature, species_mass_fractions)

    def get_species_molecular_weights(self):
        """Get the species molecular weights."""
        return self._pyrometheus_mech.wts

    def species_enthalpies(self, cv: ConservedVars):
        """Get the species specific enthalpies."""
        return self._pyrometheus_mech.get_species_enthalpies_rt(self.temperature(cv))

    def get_production_rates(self, cv: ConservedVars):
        r"""Get the production rate for each species.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).

        Returns
        -------
        numpy.ndarray
            The chemical production rates for each species
        """
        temperature = self.temperature(cv)
        y = cv.species_mass_fractions
        return self._pyrometheus_mech.get_net_production_rates(
            cv.mass, temperature, y)

    def pressure(self, cv: ConservedVars):
        r"""Get thermodynamic pressure of the gas.

        Gas pressure ($p$) is calculated from the internal energy ($e$) as:

        .. math::

            p = (\gamma_{\mathtt{mix}} - 1)e

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The pressure of the fluid.
        """
        @memoize_in(cv, (PyrometheusMixture.pressure,
                         type(self._pyrometheus_mech)))
        def get_pressure():
            temperature = self.temperature(cv)
            y = cv.species_mass_fractions
            return self._pyrometheus_mech.get_pressure(cv.mass, temperature, y)
        return get_pressure()

    def sound_speed(self, cv: ConservedVars):
        r"""Get the speed of sound in the gas.

        The speed of sound ($c$) is calculated as:

        .. math::

            c = \sqrt{\frac{\gamma_{\mathtt{mix}}{p}}{\rho}}

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The speed of sound in the fluid.
        """
        @memoize_in(cv, (PyrometheusMixture.sound_speed,
                         type(self._pyrometheus_mech)))
        def get_sos():
            actx = cv.array_context
            return actx.np.sqrt((self.gamma(cv) * self.pressure(cv)) / cv.mass)
        return get_sos()

    def temperature(self, cv: ConservedVars):
        r"""Get the thermodynamic temperature of the gas.

        The thermodynamic temperature ($T$) is calculated from
        the internal energy ($e$) and specific gas constant ($R_s$)
        as:

        .. math::

            T = \frac{(\gamma_{\mathtt{mix}} - 1)e}{R_s \rho}

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The temperature of the fluid.
        """
        @memoize_in(cv, (PyrometheusMixture.temperature,
                         type(self._pyrometheus_mech)))
        def get_temp():
            y = cv.species_mass_fractions
            e = self.internal_energy(cv) / cv.mass
            return self._pyrometheus_mech.get_temperature(e, self._tguess,
                                                          y, True)
        return get_temp()

    def total_energy(self, cv, pressure):
        r"""
        Get gas total energy from mass, pressure, and momentum.

        The total energy density ($\rho E$) is calculated from
        the mass density ($\rho$) , pressure ($p$) , and
        momentum ($\rho\vec{V}$) as:

        .. math::

            \rho E = \frac{p}{(\gamma_{\mathtt{mix}} - 1)} +
            \frac{1}{2}\rho(\vec{v} \cdot \vec{v})

        .. note::

            The total_energy function computes cv.energy from pressure,
            mass, and momentum in this case. In general in the EOS we need
            DV = EOS(CV), and inversions CV = EOS(DV). This is one of those
            inversion interfaces.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).
        pressure: :class:`~meshmode.dof_array.DOFArray`
            The fluid pressure

        Returns
        -------
        :class:`~meshmode.dof_array.DOFArray`
            The total energy fo the fluid (i.e. internal + kinetic)
        """
        return (pressure / (self.gamma(cv) - 1.0)
                + self.kinetic_energy(cv))

    def get_species_source_terms(self, cv: ConservedVars):
        r"""Get the species mass source terms to be used on the RHS for chemistry.

        Parameters
        ----------
        cv: :class:`~mirgecom.fluid.ConservedVars`
            :class:`~mirgecom.fluid.ConservedVars` containing at least the mass
            ($\rho$), energy ($\rho{E}$), momentum ($\rho\vec{V}$), and the vector
            of species masses, ($\rho{Y}_\alpha$).

        Returns
        -------
        :class:`~mirgecom.fluid.ConservedVars`
            Chemistry source terms
        """
        omega = self.get_production_rates(cv)
        w = self.get_species_molecular_weights()
        dim = cv.dim
        species_sources = w * omega
        rho_source = 0 * cv.mass
        mom_source = 0 * cv.momentum
        energy_source = 0 * cv.energy

        return make_conserved(dim, rho_source, energy_source, mom_source,
                              species_sources)


# -*- coding: utf-8 -*-
# ---
# jupyter:
#   jupytext:
#     formats: ipynb,py
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.6.0
#   kernelspec:
#     display_name: deep_ml_curriculum
#     language: python
#     name: deep_ml_curriculum
# ---

# # Time Series Anaysis
# Time Series are a sequence of data points ordered in time. Each data point contains one or more values and also has a time stamp. For instance, the average daily temperature is a time series. Time Series Analysis (TSA) is studying the behaviour of time series.
#
# An important part of time series analysis (TSA) is time series forcasting (TSF) which is using a sequence of data to predict future behaviour. In this session we will discuss TSA, and tool and technques in TSA. The next session will be focused on forcasting.
#
#
# TSA has its own unique vocabulary and technical terms. Throughout this notebook we will explain some of these terms and what they mean in practice.
#
#
# ## Tools
#
# One of the main tools for TSA is __Pandas__. In previous notebooks we learned about Pandas and it's capabilities in reading, writing, and manipulating tabular data. Pandas also has a wide range of functionalities for TSA.
#
# Another common tool for TSA in python is __Statsmodels__. Statsmodels is library with wide range of statistical analysis tools and a sub-module dedicated to TSA.

# +
# Our main packages for TSA
import pandas as pd

# For math and array operations
import numpy as np

# For plotting
import matplotlib.pyplot as plt
# -

# Later, we will import statsmodels sub-modules as we need them.

# ## Pandas for time series
#
# Pandas has functionalities for reading time and date.

data_normal = pd.read_csv("../../data/processed/Generated/SampleSales.csv")
data_normal["Date"]

# When reading data that contains date/time we can ask pandas to parse it as datetimes with `parse_dates = ['Column_name']`.

data_date_parsed = pd.read_csv(
    "../../data/processed/Generated/SampleSales.csv", parse_dates=["Date"]
)
data_date_parsed["Date"]

# In the first scenario when we print 'Date' column at the bottom it prints `dtype = object` which in this case it means pandas understands the dates as a string (or text). It has no understanding of what the dates actually mean. But in the second scenario, the type is `datetime64[ns]` which is a date/time object with accuracy of nanoseconds. Now pandas knows these are dates and it knows the year, month, day, etc. Also, despite showing only year, month, and day it is saving it to nanoseconds accuracy, which means we could also have time of the day included as well.
#

# Also, by passing `index_col = 'Date'` we can let pandas know that in the index column, instead of using numbers (0, 1, 2, ...) use the _'Date'_ column. This will make it easier to access data based on date.

data_index_parsed = pd.read_csv(
    "../../data/processed/Generated/SampleSales.csv",
    parse_dates=["Date"],
    index_col=["Date"],
)

# Notice freq is none, that makes sense since we don't have values for every day
data_index_parsed.index

data_index_parsed['Total'].plot()

data_index_parsed

# Now we can get the data for a certain date or time period. Let stick to the ISO format which is year-month-day, this sidesteps the confusion over US vs UK date formats.

data_index_parsed.loc["2014-12-19"]

data_index_parsed.loc["2014-11-01":"2014-12-01"]

# Also, if you have an array of dates in string format you can use `.to_datetime()` to convert it to datetime object. The date string can be in many formats and pandas can usually figure it out.

pd.to_datetime(["1/3/2019", "July 23rd, 1964", "2006-01-09"])

# Or you can specify which format pandas should use.

pd.to_datetime(["1/3/2019"], format="%d/%m/%Y")

pd.to_datetime(["1/3/2019"], format="%m/%d/%Y")

# ## Resampling
# There are times you might need weekly data but you have it on daily basis. You can use resampling to transform the data into the new time intervals. This is also useful when you are dealing with datapoints which are not equally spaced. Let's have another look at the data we loaded earlier.

data_index_parsed.head(10)

# This data shows the time and amount of purchases from a shop. The purchases are not taking place on a fixed interval. They can happen anytime. We can resample this data into weekly sales which makes it easier to analyse. To do this, we need to use the `.resample()` method and pass in the new interval. After that we need to specify how we want the data in each interval to be aggregated. In this case, we choose the sum function because we want the total sales in each week.

df = data_index_parsed.resample("W").sum()
df.head(10)

# Now we have weekly sales. Note that we passed in "W" to convert the data into weekly sales. There are a range of options available in pandas for various intervals. The table below shows a range of options for resampling.

# Note that not all the options on the table below are applicable for resampling. The options for resampling are "M", "A", "Q", "BM", "BA", "BQ", and "W". The other options in the table have other application which we will discuss later.

# DateOffsets and their frequency strings. ([Source](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html#dateoffset-objects))

#
#

#

#

#       Date Offset
#       Frequency String
#       Description
#

#

#

#

#       DateOffset
#       None
#       Generic offset class, defaults to 1 calendar day
#

#

#       BDay or BusinessDay
#       'B'
#       business day (weekday)
#

#

#       CDay or CustomBusinessDay
#       'C'
#       custom business day
#

#

#       Week
#       'W'
#       one week, optionally anchored on a day of the week
#

#

#       WeekOfMonth
#       'WOM'
#       the x-th day of the y-th week of each month
#

#

#       LastWeekOfMonth
#       'LWOM'
#       the x-th day of the last week of each month
#

#

#       MonthEnd
#       'M'
#       calendar month end
#

#

#       MonthBegin
#       'MS'
#       calendar month begin
#

#

#       BMonthEnd or BusinessMonthEnd
#       'BM'
#       business month end
#

#

#       BMonthBegin or BusinessMonthBegin
#       'BMS'
#       business month begin
#

#

#       CBMonthEnd or CustomBusinessMonthEnd
#       'CBM'
#       custom business month end
#

#

#       CBMonthBegin or CustomBusinessMonthBegin
#       'CBMS'
#       custom business month begin
#

#

#       SemiMonthEnd
#       'SM'
#       15th (or other day_of_month) and calendar month end
#

#

#       SemiMonthBegin
#       'SMS'
#       15th (or other day_of_month) and calendar month begin
#

#

#       QuarterEnd
#       'Q'
#       calendar quarter end
#

#

#       QuarterBegin
#       'QS'
#       calendar quarter begin
#

#

#       BQuarterEnd
#       'BQ
#       business quarter end
#

#

#       BQuarterBegin
#       'BQS'
#       business quarter begin
#

#

#       FY5253Quarter
#       'REQ'
#       retail (aka 52-53 week) quarter
#

#

#       YearEnd
#       'A'
#       calendar year end
#

#

#       YearBegin
#       'AS' or 'BYS'
#       calendar year begin
#

#

#       BYearEnd
#       'BA'
#       business year end
#

#

#       BYearBegin
#       'BAS'
#       business year begin
#

#

#       FY5253
#       'RE'
#       retail (aka 52-53 week) year
#

#

#       Easter
#       None
#       Easter holiday
#

#

#       BusinessHour
#       'BH'
#       business hour
#

#

#       CustomBusinessHour
#       'CBH'
#       custom business hour
#

#

#       Day
#       'D'
#       one absolute day
#

#

#       Hour
#       'H'
#       one hour
#

#

#       Minute
#       'T' or 'min'
#       one minute
#

#

#       Second
#       'S'
#       one second
#

#

#       Milli
#       'L' or 'ms'
#       one millisecond
#

#

#       Micro
#       'U' or 'us'
#       one microsecond
#

#

#       Nano
#       'N'
#       one nanosecond
#

#

#

#

# Lets compare differen't resamplings
ax=plt.gca()
data_index_parsed['Total'].resample("D").sum().plot(label="Day", ax=ax)
data_index_parsed['Total'].resample("W").sum().plot(label="Week", ax=ax)
data_index_parsed['Total'].resample("M").sum().plot(label="Month", ax=ax)
plt.ylabel('Total')
plt.legend()

# ## Shifting
# Shifiting is moving the data forward or backward in time. For instance, the code below shifts the data one week to the future.

# Pandas knows the interval is a week
df.info()

df.head()

df.shift(periods=1)

# Notice the value that belonged to "2014-11-09" now belongs to "2014-11-09". Also, you can see in the first row since there was no data point to be moved there, its new value is `NaN`.

# Now the question is how does pandas know how much it should shift the data? In other words, why did it decide to move the data one week and not one day? The answer is because pandas knows our data is weekly, so when we asked it to shift the data by one period it assumed one week. This is called the frequency of the data and we can check it using `.info()` as we did above.

# The frequency is "W-SUN" which means weekly data and each week starts at sunday.

# __What if we want Monday to be start of the week?__

# When we were resampling the data we asked pandas to create weekly data. We can specify which day should start of the week.

data_index_parsed.resample("W-MON").sum()

# __What if we only wanted to shift the data one day and not one week?__

# We can specify a frequency when shifting.

df.shift(periods=1, freq="D")

# Note in the above example only the dates changed and `NaN` didn't appear in the first row.

# We can also shift backward. If we do this, `NaN` will appear at the end of the data.

df.shift(-2)

# ## Rolling
# Rolling is applying a function to a number of consecutive data points. For instance, at each point in time we take the average of the past 4 weeks. Taking average of past few periods is also know as __Moving Average Smoothing__.

# We first need to select the window (how many values we should consider), and then apply an aggregation function. (e.g. mean, sum, etc.)
#
#
#
#

#

df.rolling(window=4).mean()

# __Note:__ Since the first three points didn't have three instances before them, the moving average was not calculated for them.

# Let's save the moving average in the same data frame and plot the result to better understand why we would do this.

df["MA-4"] = df.rolling(window=4).mean()  # MA-4 for moving average of size 4 window
df.plot(y=["Total", "MA-4"], figsize=(16, 8))

# Let's try a larger window size. Because now we have two columns in the data, we need to specify which column we are taking a moving average of.

df["MA-13"] = (
    df["Total"].rolling(window=13).mean()
)  # MA-4 for moving average of size 4 window
df.plot(y=["Total", "MA-4", "MA-13"], figsize=(16, 8))

# As the name suggests __Moving Average Smoothing__ creates a smoothed out version of the data. The larger the window size, smoother the data will be. This is helpful when we want to get rid of noise in the data so we can have a better look at the trends. However, noise reduction comes at a cost. The downside of using moving average is the it creates lag in the data. For instance if there is a sudden increase in the data moving average responds to this change slower. It also doesn't reach the peaks of the original data. In the plot above you can see that "MA-13" does not show the high and low values of the data but only follows th main trend.

# Another useful function pandas provide is **expanding**. While at each point rolling applied a function over a fixed window, expanding applies a function over all the points that came before that specific point. For instance, if we apply mean function at 10th point we will have the average of first 10 points and at 20th point we will have the average of first 20 points.
#
# exanding().sum() is the same as .cumsum()
#

df["Total"].expanding().sum().plot()

# exanding().sum() is the same as .cumsum(), but you can use operations other than sum
df["Total"].cumsum().plot()

#

#
Exercise

#
# Here's an exercise to get you familiar with the Pandas docs on timeseries.
#
# You need to change the timezone on a dataframe but your not sure how.
#
# - Look through [the dataframe docs](https://pandas.pydata.org/docs/reference/frame.html) and find how to set a timezone on a series that doesn't have one (but you know it's 'Australia/Perth')
# - Set it to 'Australia/Perth'
# - plot it
#
#
#
#   → Hints
#
#   * Start in the Time-Series related section
#   * Scan it, then Ctrl-F for `timezone`
#   * Jargon is `localize` a `tz naive` series
#
#
#
#

#

#
#
#     → Solution
#
#
#   ```python
#   # DataFrame.tz_localize
#   df.tz_localize('Australia/Perth').plot(
#   ```
#
#
#
#

# Other operations:
#
# - https://pandas.pydata.org/pandas-docs/stable/reference/general_functions.html#top-level-dealing-with-datetimelike
# - https://pandas.pydata.org/pandas-docs/stable/reference/frame.html#time-series-related
# - https://pandas.pydata.org/pandas-docs/stable/reference/series.html#time-series-related

# ## Lowess Smoothing
# Now that we learned about moving average smoothing, let's discuss another technique. Locally weighted scatterplot smoothing (LOWESS) is another technique for smoothing.
#
# Lowess creates local models for small subsets of data and uses a weighted average of them for smoothing.
#

# The implementation of Lowess in statsmodels can be found in nonparametric sub-module.
from statsmodels.nonparametric.smoothers_lowess import lowess

# To create a smoothed output it requires x and y values. For time series x would be the time dimension and y would be the values.

x = df.index
y = df["Total"]
result = lowess(y, x)
df["Lowess"] = result[
    :, 1
]  # Lowess returns x and y. The second column (column index 1) are the values we need.
df.plot(y=["Total", "Lowess"])

# ## Time Series Decomposition

# The goal of decomposition is to break down time series into different elements each describing a specific behaviour of data.

# ### STL Decomposition

# Seasonal-Trend decomposition with LOESS (STL) is another method for decomposition of time series. This method breaks down the data into three elements:
# 1. Trend
# 2. Seasonality
# 3. Residual (Error or Noise)
#
# We learned about LOWESS for smoothing (LOESS is a similar method with only minor differences). LOESS showed the capability to extract the trend of time series. By removing the trend from the data STL assumes the remainder is sum of a seasonal component and some residual. Seasonal component is the repeating pattern of data. The sort of behaviour that repeats weekly, monthly, etc.
STL can decompose the time serie in two ways. First is additive method, where the original data is sum of its components:
# $$y_t = \tau_t + s_t + r_t $$
# Second, the multiplicative approach where the data is product of its components:
# $$y_t = \tau_t \times s_t \times r_t $$
# Statsmodel has a function for STL called `seasonal_decompose`.

# This data shows the number of customers a retail shop had per months
cust = pd.read_csv(
    "../../data/processed/Generated/Customers.csv", parse_dates=True, index_col="Month"
)
cust.head()

# Let's have quick look at the data.

cust.plot(y="Customers")

# We can clearly see a repeating pattern. That's called seasonality and we expect STL to be able to capture this behaviour.

from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(cust["Customers"], model='additive')

# Now the time serie is decomposed and we can plot the result.

ax = result.plot()
ax.set_figwidth(16)
ax.set_figheight(8)

# By default the model used additive approach for the data. If we want to use multiplicative approach we need to specify it in the function. But what is the difference between additive and multiplicative? Additive approach is used when the height of the repeating patterns are constant. This means the amplitude of seasonal component is not changing. But in this data we can clearly see the seasonal component is getting larger and larger. This is also visible in the residual component. The residual component is meant to look like random noise but we can clearly see some regular oscillation in there. So let's try multiplicative approach.

result = seasonal_decompose(cust["Customers"], model="multiplicative")
print('sum of residuals {:2.2f}'.format(result.resid.sum()))
ax = result.plot()
ax.set_figwidth(16)
ax.set_figheight(8)

# __Note:__ While residuals in additive model is around zero, in multiplicative model they are around one.

# As we can see, multiplicative approach looks much better. There is still a bit of pattern in residuals but it has improved significantly compared to additive approach.

# ## Exercise
#

#
# let's try these on real data, which will be much harder. Use the data below and perform the following analyses:
#
# - STL decomposition using additive method.
#
#
# Hints
#
# * Find the relevent code, copy it, and change it to use `df['target']`
#
#

#

df = block0 = pd.read_csv("../../data/processed/smartmeter/block_0.csv", parse_dates=['day'], index_col=['day'])[['energy_sum']]
df = df.groupby('day').mean().iloc[:100]
df = df.rename(columns={'energy_sum':'target'})
df.plot()
df

# Exercise 1
from statsmodels.tsa.seasonal import seasonal_decompose
result = seasonal_decompose(df['target'],model = 'add')
result.plot();
print('sum of residuals {:2.2f}'.format(result.resid.sum()))

#
#

#
# ### Solution
# See solution
#
#
#
# ```Python
# # Exercise 1
# from statsmodels.tsa.seasonal import seasonal_decompose
# result = seasonal_decompose(df['target'],model = 'add')
# result.plot();
# print(f'sum of residuals {result.resid.sum():2.2f}')
# ```
#
#
#

# In the next notebook we will look at how to do predictions, including seasonality

# ## Further Reading:
# - [Pandas time-series documentation](https://pandas.pydata.org/pandas-docs/stable/user_guide/timeseries.html)
# - [Statsmodel documentation](https://www.statsmodels.org/)
# - [Forecasting: Principles and Practice](https://otexts.com/fpp2/)
# - [Exponential Smoothing](https://en.wikipedia.org/wiki/Exponential_smoothing)
# - [Gentle Introduction to Exponential smoothing](https://machinelearningmastery.com/exponential-smoothing-for-time-series-forecasting-in-python/)
#
#

import numpy,import statsmodels,from statsmodels


#!/usr/bin/env python3

import os
import glob
import numpy as np
import scipy
from ffindex import *
from parsers import parse_hhr, parse_pdb_lines
from pyrosetta import *

init("-mute all")

MAXSEQ=20000

#============================================
# Global variables such as data directory path
#============================================
LABEL_dir = "/net/scratch/minkbaek/SStor/labels"
TEMPL_dir = "/net/scratch/minkbaek/trRefine/added/templ/sub_0/templates"
#TEMPL_dir = "/net/scratch/minkbaek/SStor/templs"
DECOY_DIR = "/projects/casp/dldata"

blosum_mtx = np.array([[  4, -1, -2, -2,  0, -1, -1,  0, -2, -1, -1, -1, -1, -2, -1,  1,  0, -3, -2,  0],
                       [ -1,  5,  0, -2, -3,  1,  0, -2,  0, -3, -2,  2, -1, -3, -2, -1, -1, -3, -2, -3],
                       [ -2,  0,  6,  1, -3,  0,  0,  0,  1, -3, -3,  0, -2, -3, -2,  1,  0, -4, -2, -3],
                       [ -2, -2,  1,  6, -3,  0,  2, -1, -1, -3, -4, -1, -3, -3, -1,  0, -1, -4, -3, -3],
                       [  0, -3, -3, -3,  9, -3, -4, -3, -3, -1, -1, -3, -1, -2, -3, -1, -1, -2, -2, -1],
                       [ -1,  1,  0,  0, -3,  5,  2, -2,  0, -3, -2,  1,  0, -3, -1,  0, -1, -2, -1, -2],
                       [ -1,  0,  0,  2, -4,  2,  5, -2,  0, -3, -3,  1, -2, -3, -1,  0, -1, -3, -2, -2],
                       [  0, -2,  0, -1, -3, -2, -2,  6, -2, -4, -4, -2, -3, -3, -2,  0, -2, -2, -3, -3],
                       [ -2,  0,  1, -1, -3,  0,  0, -2,  8, -3, -3, -1, -2, -1, -2, -1, -2, -2,  2, -3],
                       [ -1, -3, -3, -3, -1, -3, -3, -4, -3,  4,  2, -3,  1,  0, -3, -2, -1, -3, -1,  3],
                       [ -1, -2, -3, -4, -1, -2, -3, -4, -3,  2,  4, -2,  2,  0, -3, -2, -1, -2, -1,  1],
                       [ -1,  2,  0, -1, -3,  1,  1, -2, -1, -3, -2,  5, -1, -3, -1,  0, -1, -3, -2, -2],
                       [ -1, -1, -2, -3, -1,  0, -2, -3, -2,  1,  2, -1,  5,  0, -2, -1, -1, -1, -1,  1],
                       [ -2, -3, -3, -3, -2, -3, -3, -3, -1,  0,  0, -3,  0,  6, -4, -2, -2,  1,  3, -1],
                       [ -1, -2, -2, -1, -3, -1, -1, -2, -2, -3, -3, -1, -2, -4,  7, -1, -1, -4, -3, -2],
                       [  1, -1,  1,  0, -1,  0,  0,  0, -1, -2, -2,  0, -1, -2, -1,  4,  1, -3, -2, -2],
                       [  0, -1,  0, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, -2, -1,  1,  5, -2, -2,  0],
                       [ -3, -3, -4, -4, -2, -2, -3, -2, -2, -3, -2, -3, -1,  1, -4, -3, -2, 11,  2, -3],
                       [ -2, -2, -2, -3, -2, -1, -2, -3,  2, -1, -1, -2, -1,  3, -3, -2, -2,  2,  7, -1],
                       [  0, -3, -3, -3, -1, -2, -2, -3, -3,  3,  1, -2,  1, -1, -2, -2,  0, -3, -1,  4]]).astype(np.float32)
blosum_mtx /= 10.0 # same as Nao's

tip_atoms = {"ALA": "CB", "CYS": "SG", "ASP": "CG", "GLU": "CD",
             "PHE": "CZ", "GLY": "CA", "HIS":"NE2", "ILE":"CD1",
             "LYS": "NZ", "LEU": "CG", "MET": "SD", "ASN": "CG",
             "PRO": "CG", "GLN": "CD", "ARG": "CZ", "SER": "OG",
             "THR":"OG1", "VAL": "CB", "TRP":"CH2", "TYR": "OH"}

def f(X, cutoff=3, scaling=3.0):
    X_prime = np.maximum(X, np.zeros_like(X) + cutoff) - cutoff
    return np.arcsinh(X_prime)/scaling

def blosum(seq):
    return blosum_mtx[seq]

def t2q(tmplt,nres):

    dmin = 2.0
    dmax = 20.0

    qmap = tmplt['qmap']
    L = qmap.shape[0]

    # bin distance matrix
    nbins = 18
    bins = np.linspace(dmin, dmax, nbins+1)
    dist = tmplt['dist6d']
    dist[dist<0.001] = 999.9
    dbin = np.digitize(dist, bins).astype(np.float32)
    dbin[dbin > nbins] = 0

    f1d = np.zeros((nres,4), dtype=np.float32)
    f1d[qmap] = np.concatenate([
        tmplt['phi'][:,None],
        tmplt['psi'][:,None],
        tmplt['scores'][:,None],
        tmplt['conf'][:,None]
    ], axis=-1)

    f2d = np.zeros((nres,nres,6), dtype=np.float32)
    f2d[np.ix_(qmap,qmap)] = np.concatenate([
        dbin[:,:,None],
        tmplt['omega6d'][:,:,None],
        tmplt['theta6d'][:,:,None],
        tmplt['phi6d'][:,:,None],
        np.tile(tmplt['stats'][None,None,:],[L,L,1])
    ], axis=-1)

    return [f1d,f2d]

def subsample_msa(msa):
    n_sub = int(msa.shape[0]/2)
    if n_sub < 5:
        return msa
    seq = msa[0]
    tmp = msa[1:]
    np.random.shuffle(tmp)
    return np.concatenate([seq[np.newaxis,:], tmp[:n_sub-1,:]], axis=0)

def get_seqsep(n_res):
    pos = np.arange(n_res)
    pos = pos.astype(np.float32)
    tiled_pos = np.tile(pos, (n_res, 1))
    seqsep = np.abs(tiled_pos - tiled_pos.T)/100.0 - 1.0
    return seqsep

def get_labels(pdb, noise_level=0.0):
    label_fn = os.path.join(LABEL_dir, pdb + ".npz")
    data = np.load(label_fn)
    #
    msa = subsample_msa(data['msa'])
    seq = blosum(msa[0])
    #
    dssp = data['dssp'].astype(np.float32)
    phi = data['phi'].astype(np.float32)
    psi = data['psi'].astype(np.float32)
    omg = data['omg'].astype(np.float32)
    omg = np.stack((1-omg, omg), axis=-1)

    return dssp, phi, psi, omg, msa, seq

def get_input_features(filename, is_train=True, mask_diag=8):
    fn_s = glob.glob("%s/%s/*.npz"%(DECOY_DIR, filename))
    np.random.shuffle(fn_s)
    fn = fn_s[0]
    #
    data = np.load(fn)
    #
    seqsep = get_seqsep(data['maps'].shape[0])
    #
    # structure-based features
    d_CB = f(data['maps'][:,:,0].astype(np.float32))
    o_map = np.stack([data["omega6d"], data["theta6d"], data["phi6d"]], axis=-1).astype(np.float32)
    o_map = np.concatenate([np.sin(o_map), np.cos(o_map)], axis=-1)
    feat_2d = np.concatenate((d_CB[:,:,None], o_map, seqsep[:,:,None]), axis=-1)
    if is_train: # add gaussian noise in training stage to avoid overfitting
        scale = np.random.uniform(low=0.05, high=0.15)
        noise = np.random.normal(scale=scale, size=feat_2d.shape).astype(np.float32)
        feat_2d += noise
    feat_2d = np.nan_to_num(feat_2d)
    #
    if mask_diag > -1:
        for i in range(feat_2d.shape[-1]):
            for k in range(mask_diag):
                np.fill_diagonal(feat_2d[k:,:,i], 0.0)
                np.fill_diagonal(feat_2d[:,k:,i], 0.0)
    #
    # Template information
    nres = data['maps'].shape[0]
    tlist = glob.glob("%s/%s/*.npz"%(TEMPL_dir, filename))
    np.random.shuffle(tlist)
    n_templ = np.random.randint(10, 26)
    tlist = tlist[:n_templ]
    pthreads = [t2q(np.load(f), nres) for f in tlist]
    pth_1d = np.stack([p[0] for p in pthreads], axis=0)
    pth_2d = np.stack([p[1] for p in pthreads], axis=0)
    #
    return feat_2d, pth_1d, pth_2d

def load_train_data(pdb, noise_level=0.0, tag=None, is_train=True, mask_diag=8):
    #
    SS_label, phi_label, psi_label, omg_label, msa, seq = get_labels(pdb, noise_level=noise_level)
    feat_2d, pth_1d, pth_2d = get_input_features(pdb, is_train=is_train, mask_diag=mask_diag)
    #

    return seq, msa, feat_2d, pth_1d, pth_2d, SS_label, phi_label, psi_label, omg_label

# TODO
# input feature generation for prediction
def read_msa(a3m_fn):
    import string

    seqs = []
    table = str.maketrans(dict.fromkeys(string.ascii_lowercase))

    # read file
    with open(a3m_fn) as fp:
        for line in fp:
            if line[0] == '>': continue
            seqs.append(line.rstrip().translate(table)) # remove lowercase letters
    # convert letters into numbers
    alphabet = np.array(list("ARNDCQEGHILKMFPSTWYV-"), dtype='|S1').view(np.uint8)
    msa = np.array([list(s) for s in seqs], dtype='|S1').view(np.uint8)
    for i in range(alphabet.shape[0]):
        msa[msa == alphabet[i]] = i

    msa[msa > 20] = 20
    return msa

def get_dihedrals(a, b, c, d):
    b0 = -1.0*(b - a)
    b1 = c - b
    b2 = d - c

    b1 /= np.linalg.norm(b1, axis=-1)[:,None]

    v = b0 - np.sum(b0*b1, axis=-1)[:,None]*b1
    w = b2 - np.sum(b2*b1, axis=-1)[:,None]*b1

    x = np.sum(v*w, axis=-1)
    y = np.sum(np.cross(b1, v)*w, axis=-1)

    return np.arctan2(y, x)

def get_angles(a, b, c):
    v = a - b
    v /= np.linalg.norm(v, axis=-1)[:,None]

    w = c - b
    w /= np.linalg.norm(w, axis=-1)[:,None]

    x = np.sum(v*w, axis=1)

    return np.arccos(np.clip(x, -1.0, 1.0))

def get_dist_ori_maps(model, resNo_s, n_res, dmax=20.0, return_sep=False):
    atm_names = list()
    for i in range(1, model.size()+1):
        if model.residue(i).name()[:3] == "GLY":
            atm_names.append('CA')
        else:
            atm_names.append('CB')
    #
    CA  = np.zeros((n_res, 3), dtype=np.float32)
    N   = np.zeros((n_res, 3), dtype=np.float32)
    O   = np.zeros((n_res, 3), dtype=np.float32)
    C   = np.zeros((n_res, 3), dtype=np.float32)
    CB  = np.zeros((n_res, 3), dtype=np.float32)
    #
    for idx, resNo in enumerate(resNo_s):
        i = idx + 1 # resNo in pose
        CA[resNo, :] = np.array(model.residue(i).atom("CA").xyz())
        N[resNo, :] = np.array(model.residue(i).atom("N").xyz())
        O[resNo, :] = np.array(model.residue(i).atom("O").xyz())
        C[resNo, :] = np.array(model.residue(i).atom("C").xyz())
        CB[resNo, :] = np.array(model.residue(i).atom(atm_names[idx]).xyz())
    #
    mask = np.zeros((n_res, n_res), dtype=np.float32)
    for i in resNo_s:
        for j in resNo_s:
            mask[i,j] = 1.0
    #
    dist_CB_CB = scipy.spatial.distance.cdist(CB, CB, metric='euclidean')
    #
    dist_maps = f(dist_CB_CB[:,:,None])
    #
    # recreate Cb from given N, Ca, C
    b = CA - N
    c = C - CA
    a = np.cross(b, c)
    Cb = -0.58273431*a + 0.56802827*b - 0.54067466*c + CA
    #
    # neighbor search
    kdCb = scipy.spatial.cKDTree(Cb)
    indices = kdCb.query_ball_tree(kdCb, dmax)
    #
    # indices of contacting residues
    idx = np.array([[i,j] for i in range(len(indices)) for j in indices[i] if (i != j) and (i in resNo_s) and (j in resNo_s)]).T
    idx0 = idx[0]
    idx1 = idx[1]
    #
    # matrix of Ca-Cb-Cb-Ca dihedrals
    omega6d = np.zeros((n_res, n_res))
    ang = get_dihedrals(CA[idx0], CB[idx0], CB[idx1], CA[idx1])
    omega6d[idx0, idx1] = ang
    #
    # matrix of polar coord theta
    theta6d = np.zeros((n_res, n_res))
    ang = get_dihedrals(N[idx0], CA[idx0], Cb[idx0], Cb[idx1])
    theta6d[idx0, idx1] = ang
    #
    # matrix of polar coord phi
    phi6d = np.zeros((n_res, n_res))
    ang = get_angles(CA[idx0], Cb[idx0], Cb[idx1])
    phi6d[idx0, idx1] = ang
    #
    if return_sep:
        dist6d = np.zeros((nres, nres), dtype=np.float32)
        dist6d[idx0, idx1] = np.linalg.norm(Cb[idx1]-Cb[idx0], axis=-1)
        return dist6d, omega6d, theta6d, phi6d
    #
    ori_map = np.stack([omega6d, theta6d, phi6d], axis=-1).astype(np.float32)
    ori_map = np.concatenate([np.sin(ori_map), np.cos(ori_map)], axis=-1)
    #
    ori_map = ori_map * mask[:,:,None]
    dist_maps = dist_maps * mask[:,:,None]
    #
    return dist_maps, ori_map

def get_phipsi(xyz, tmap):
    nres = xyz.shape[1]

    N = xyz[0]
    Ca = xyz[1]
    C = xyz[2]

    C_prev = np.insert(C, 0, np.nan, axis=0)
    N_next = np.insert(N, nres, np.nan, axis=0)

    phi = get_dihedrals(C_prev[:-1,:], N, Ca, C)
    psi = get_dihedrals(N, Ca, C, N_next[1:,:])
    phi[0] = 0.0
    psi[-1] = 0.0
    tor = np.stack((phi, psi), axis=-1)[tmap]
    return tor

# get 6d coordinates from x,y,z coords of N,Ca,C atoms
def get_coords6d(xyz, dmax):

    nres = xyz.shape[1]

    # three anchor atoms
    N  = xyz[0]
    Ca = xyz[1]
    C  = xyz[2]

    # recreate Cb given N,Ca,C
    b = Ca - N
    c = C - Ca
    a = np.cross(b, c)
    Cb = -0.58273431*a + 0.56802827*b - 0.54067466*c + Ca

    # fast neighbors search to collect all
    # Cb-Cb pairs within dmax
    kdCb = scipy.spatial.cKDTree(Cb)
    indices = kdCb.query_ball_tree(kdCb, dmax)

    # indices of contacting residues
    idx = np.array([[i,j] for i in range(len(indices)) for j in indices[i] if i != j]).T
    idx0 = idx[0]
    idx1 = idx[1]

    # Cb-Cb distance matrix
    dist6d = np.zeros((nres, nres))
    dist6d[idx0,idx1] = np.linalg.norm(Cb[idx1]-Cb[idx0], axis=-1)

    # matrix of Ca-Cb-Cb-Ca dihedrals
    omega6d = np.zeros((nres, nres))
    omega6d[idx0,idx1] = get_dihedrals(Ca[idx0], Cb[idx0], Cb[idx1], Ca[idx1])

    # matrix of polar coord theta
    theta6d = np.zeros((nres, nres))
    theta6d[idx0,idx1] = get_dihedrals(N[idx0], Ca[idx0], Cb[idx0], Cb[idx1])

    # matrix of polar coord phi
    phi6d = np.zeros((nres, nres))
    phi6d[idx0,idx1] = get_angles(Ca[idx0], Cb[idx0], Cb[idx1])

    return dist6d, omega6d, theta6d, phi6d

def get_2D_str_feat(model, n_res):
    resNo_s = list()
    for i in range(1, model.size()+1):
        resNo = model.pdb_info().number(i) # resNo in PDB file
        resNo_s.append(resNo-1) # -1 to make it as python array index
    resNo_s = np.array(resNo_s)
    d_map, o_map = get_dist_ori_maps(model, resNo_s, n_res)
    seqsep = get_seqsep(n_res)
    return np.concatenate((d_map, o_map, seqsep[:,:,None]), axis=-1)

def make_input_features(a3m_fn, pdb_fn=None, hhr_fn=None, ffdb=None, mask_diag=8, seqID=999.0):
    if isinstance(a3m_fn, str):
        msa = read_msa(a3m_fn)
    else:
        msa = a3m_fn
    seq = blosum(msa[0])
    if len(msa) > MAXSEQ:
        msa = msa[:MAXSEQ]
    #
    if pdb_fn == None: return seq, msa
    model = pose_from_file(pdb_fn)
    feat_2d = get_2D_str_feat(model, len(msa[0]))
    for i in range(feat_2d.shape[-1]):
        for k in range(mask_diag):
            np.fill_diagonal(feat_2d[k:,:,i], 0.0)
            np.fill_diagonal(feat_2d[:,k:,i], 0.0)

    pth_1d = list()
    pth_2d = list()
    if hhr_fn != None:
        L0 = len(msa[0])
        dmin = 2.0
        dmax = 20.0
        nbins = 18
        dbins = np.linspace(dmin, dmax, nbins+1)
        hhr = parse_hhr(hhr_fn, ffdb.index, seqID)
        for i, hit in enumerate(hhr[:10]):
            # extract template from FFindexDB
            entry = get_entry_by_name(hit[0], ffdb.index)
            data = read_entry_lines(entry, ffdb.data)
            xyz_tmp, idx_tmp = parse_pdb_lines(data)
            idx_tmp = idx_tmp-1 # change residue indices to start from 0

            # 6d coordinates
            d, o, t, p = get_coords6d(xyz_tmp, 20.0)
            db = np.digitize(d,dbins)

            # residue indices in the query and the template
            sel = np.intersect1d(hit[1][:,1], idx_tmp, return_indices=True)[1]
            qmap = hit[1][sel,0]
            tmap = hit[1][sel,1]

            # format 1d features
            f1d_a = np.zeros((L0,2), dtype=np.float32)
            f1d_a[qmap] = get_phipsi(xyz_tmp, tmap)
            f1d_k = np.zeros((L0,2), dtype=np.float32)
            f1d_k[qmap] = hit[1][sel,2:]
            pth_1d.append(np.concatenate((f1d_a, f1d_k), axis=-1))

            # format 2d features
            f2d_k = np.zeros((L0,L0,6), dtype=np.float32)
            ij = np.ix_(qmap,qmap.T)
            f2d_k[:,:,0][ij] = db[tmap][:,tmap]
            f2d_k[:,:,1][ij] = o[tmap][:,tmap]
            f2d_k[:,:,2][ij] = t[tmap][:,tmap]
            f2d_k[:,:,3][ij] = p[tmap][:,tmap]
            f2d_k[:,:,4:][ij] = np.array(hit[2:])#s0d[i][None,None,:]
            pth_2d.append(f2d_k)

        pth_1d = np.array(pth_1d)
        pth_2d = np.array(pth_2d)
    #
    return seq, msa, feat_2d, pth_1d, pth_2d


#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright (C) 2018 Brno University of Technology FIT
# Author: Jan Profant <jan.profant@phonexia.com>
# All Rights Reserved

import h5py
import numpy as np
from scipy.io.idl import AttrDict
from scipy.sparse import coo_matrix

from vbdiar.utils.utils import Utils

class PLDA(object):

    V = None
    U = None
    D = None
    mc = None
    mu = None
    _R = None
    _T = None
    _y = None
    _x = None
    Syy = None
    vdim = None
    srank = None
    crank = None
    noise_type = None

    def init(self, vdim, srank, crank, diag="isotropic"):
        """

        Args:
            vdim:
            srank:
            crank:
            diag:
        """
        self.vdim = vdim
        self.srank = srank
        self.crank = crank
        self.noise_type = diag

        np.random.seed(7)
        self.V = np.array(np.random.randn(vdim, srank), np.float) * 1e0
        self.U = np.array(np.random.randn(vdim, crank), np.float) * 1e0
        if diag == "zero":
            self.D = np.zeros((vdim, 1), np.float)
        else:
            self.D = np.ones((vdim, 1), np.float)

        self.mu = np.zeros((vdim, 1), np.float)

        # expectations
        self._R = np.eye(self.srank + self.crank, self.srank + self.crank)
        self._T = np.zeros((self.srank + self.crank, self.vdim))
        self.Syy = np.zeros((self.srank, self.srank))
        # point estimates
        self._y = None
        self._x = None

    def __str__(self):
        return "= PLDA model d=%d,s=%d,c=%d,type=%s" % (self.vdim, self.srank, self.crank, self.noise_type)

    def __init__(self, model_file):
        """

        Args:
            model_file:

        Returns:

        """
        model = h5py.File(model_file, 'r')
        v, u, d, noise_type, mu = model['V'][:], model['U'][:], model['D'][:], model['noise_type'], model['mu'][:]
        vdim, srank = v.shape
        crank = u.shape[1]
        self.init(vdim, srank, crank, noise_type)
        self.V = v
        self.U = u
        self.D = d
        self.mu = mu
        self.cache_statistics()

    @staticmethod
    def symmetrize(a):
        a = a + a.T
        a *= 0.5
        return a

    @staticmethod
    def logdet_chol(chol_m):
        """Return the log determinant of a matrix given the cholesky decomposition"""
        return 2 * np.sum(np.log(np.diagonal(chol_m)))

    @staticmethod
    def invhandle(m, func_only=True):
        """Return a function handle on multiplying a matrix
           by the inverse of the marix provided in this function
           if func_only=False then return: [logdet,chol,handle]
           (Niko's style)
           This is used when scoring the PLDA system
           """
        cm = np.linalg.cholesky(m)

        def h(a):
            return np.linalg.solve(cm.T, np.linalg.solve(cm, a.T))

        if func_only:
            return h
        else:
            logdet = PLDA.logdet_chol(cm)
            return [logdet, cm, h]

    def cache_statistics(self):
        """
            various values independant of the data in the cache object
        """
        # values independant of the data
        mc = AttrDict()
        mc.UD = self.U*self.D
        mc.VD = self.V*self.D
        mc.J = np.dot(self.U.T, mc.VD)

        mc.K = np.sqrt(self.D) * self.U
        mc.K = PLDA.symmetrize(np.dot(mc.K.T, mc.K)+np.eye(self.crank))

        mc.iK = np.linalg.inv(mc.K)
        mc.P0 = mc.VD.T-np.dot(np.dot(mc.J.T, mc.iK), mc.UD.T)
        self.mc = mc
        return mc

    def score_with_constant_n(self, nt, ft, nt2, ft2):
        """  Compute verification score
             This changes for every pair nT nt and can be done only once for 1 session training
             fT = P*y_head = (V'D-J'iK*U'D)*f, where f is the first order stats
        """
        # Covariance and first order stats preparation
        Py = PLDA.symmetrize(np.dot(self.mc.P0, self.V))  # (DV-iKJDU)V=VDV-JiKJ

        # P in eq. 31, EM for PLDA by Niko
        Py_Train = nt * Py + np.eye(self.srank)
        Py_Test = nt2 * Py + np.eye(self.srank)
        Py_SameSpk = (nt2 + nt) * Py + np.eye(self.srank)

        [logdetQ1, cholQ1, hQ1] = PLDA.invhandle(Py_Train, func_only=False)
        [logdetQ2, cholQ2, hQ2] = PLDA.invhandle(Py_Test, func_only=False)
        [logdetQ12, cholQ12, hQ12] = PLDA.invhandle(Py_SameSpk, func_only=False)
        # fT*hQ1(fT.T) is terms y_head*P*y_head in eq 30, EM for PLDA by Niko
        Q1 = 0.5 * (logdetQ1 + sum(ft * hQ12(ft.T)) - sum(ft * hQ1(ft.T)))
        Q2 = 0.5 * (logdetQ2 + sum(ft2 * hQ12(ft2.T)) - sum(ft2 * hQ2(ft2.T)))
        # Q2=Q2[np.newaxis,:] (can be dangerous)
        Q1 = Q1[:, np.newaxis]
        scores = np.dot(ft.T, hQ12(ft2.T))
        Q2 -= 0.5 * logdetQ12
        scores += Q1
        scores += Q2
        return scores

    def prepare_stats(self, data):
        """ Convert data (e.g. i-vectors) to statistics useful for scoring
            Input:
                data:      ivect-dim x nb_ex matrix
                seg2model: defines of multisession enrollment (or test). It is
                           2 column array of integer indices mapping rows of data
                           (1st column) to rows of output statistics (2nd column).
                           By default (seg2model=None) each vector in data has its
                           own output vector of statistics (i.e. single session
                           enrollment or test). seg2model can be also represented
                           by coo_matrix or by 1D array of labels if each input
                           vector belongs to exactly one enrollment.
            Output: a AttrDict object with
                    N: vector of counts for each speaker
                    F: array of sum of ivector for each speaker transformed by P0
                       to srank dimensionality
        """
        seg2model = np.arange(len(data))
        if seg2model.ndim == 1:
            seg2model = np.c_[np.arange(len(data)), seg2model]
        seg2model = coo_matrix((np.ones(len(seg2model)), (seg2model[:, 0], seg2model[:, 1])))

        stats = AttrDict()
        stats.N = np.array(seg2model.T.sum(1))
        stats.F = np.dot(self.mc.P0, seg2model.T.dot(data - self.mu).T).T
        return stats

    def score(self, test, enroll):
        """
            Score ivectors based on the PLDA model
            Input:
                PLDA object. PLDA.V and PLDA.U gives the subspaces
                stats objects: enroll and test
            Output:
                2D array of scores of all possibilities
        """
        test = test - self.mu
        enroll = enroll - self.mu
        test = Utils.l2_norm(test)
        enroll = Utils.l2_norm(enroll)
        Tstats = self.prepare_stats(test)
        tstats = self.prepare_stats(enroll)
        # Create scores
        scores = np.zeros((len(Tstats.N), len(tstats.N)), 'f')
        (a, b) = scores.shape

        # Score for each uniq combination of N enroll and M test sessions (only enroll for now)
        for n_enroll_sessions in np.unique(Tstats.N):
            idxs_T = np.where(Tstats.N == n_enroll_sessions)[0]
            for n_test_sessions in np.unique(tstats.N):
                idxs_t = np.where(tstats.N == n_test_sessions)[0]
                scores[np.ix_(idxs_T, idxs_t)] = self.score_with_constant_n(n_enroll_sessions, Tstats.F[idxs_T, :].T,
                                                                            n_test_sessions, tstats.F[idxs_t, :].T)
        return scores.T


# ============================================================================
# 付録 G 電気ヒートポンプ・ガス瞬間式併用型給湯温水暖房機
#       （給湯熱源：電気ヒートポンプ・ガス瞬間式併用、暖房熱源：ガス瞬間式）
# ============================================================================

import numpy as np
import pandas as pd

from pyhees.section7_1_c import \
    get_e_k_d, \
    get_e_s_d, \
    get_e_w_d, \
    get_e_b1_d, \
    get_e_b2_d, \
    get_e_ba1_d, \
    get_e_ba2_d, \
    get_f_hs

# ============================================================================
# G.3 試験された値を用いる方法
# ============================================================================

# ============================================================================
# G.3.3 ハイブリッド給湯機の仕様
# ============================================================================

# ハイブリッド給湯機の仕様一覧を読み込む
def load_specification(filename):
    """

    Args:
      filename: ファイル名

    Returns:

    """
    global csv
    csv = pd.read_csv(filename, encoding='Shift_JIS')

# ハイブリッド給湯機の仕様を取得する
def get_specification(package_id):
    """

    Args:
      package_id: パッケージID

    Returns:
      ハイブリッド給湯機の仕様

    """
    list = csv[csv['NO'] == int(package_id)]
    data = list.values[0, 12:25]
    return {
        'a_HP': data[0],
        'b_HP': data[1],
        'a_TU': data[2],
        'b_TU': data[3],
        'e_HP_std_m7': data[4],
        'e_HP_std_2': data[5],
        'e_HP_std_7': data[6],
        'e_HP_std_25': data[7],
        'Q_HP_max': data[8],
        'etr_loss_TU': data[9],
        'Theta_ex_min_HP': data[10],
        'e_BB_jis': data[11],
        # 'R_day': data[12]
        #TODO: 読込方法は要検討
        'R_day': 0.5
    }

# 直接指定されたハイブリッド給湯機のパラメーターを取得する
def get_input_specification(hybrid_param):
    """

    Args:
      hybrid_param: ハイブリッド給湯機のパラメーター

    Returns:
      ハイブリッド給湯機のパラメーター

    """
    return {
        'a_HP': hybrid_param['a_HP'],
        'a_TU': hybrid_param['a_TU'],
        'b_HP': hybrid_param['b_HP'],
        'b_TU': hybrid_param['b_TU'],
        'e_HP_std_m7': hybrid_param['e_HP_std_m7'],
        'e_HP_std_2': hybrid_param['e_HP_std_2'],
        'e_HP_std_7': hybrid_param['e_HP_std_7'],
        'e_HP_std_25': hybrid_param['e_HP_std_25'],
        'Q_HP_max': hybrid_param['Q_HP_max'],
        'etr_loss_TU': hybrid_param['etr_loss_TU'],
        'Theta_ex_min_HP': hybrid_param['Theta_ex_min_HP'],
        'e_BB_jis': hybrid_param['e_BB_jis'],
        'R_day': hybrid_param['R_day']
    }

# ============================================================================
# G.3.4 消費電力量
# ============================================================================

# 1日当たりの給湯機の消費電量量 (8)
def calc_E_E_hs_d_t(bath_function, package_id, hybrid_param, theta_ex_d_Ave_d, L_dashdash_k_d_t, L_dashdash_s_d_t, L_dashdash_w_d_t,
                    L_dashdash_b1_d_t, L_dashdash_b2_d_t, L_dashdash_ba1_d_t, L_dashdash_ba2_d_t, W_dash_ba1_d_t):
    """

    Args:
      bath_function: param package_id:
      hybrid_param: param theta_ex_d_Ave_d:
      L_dashdash_k_d_t: param L_dashdash_s_d_t:
      L_dashdash_w_d_t: param L_dashdash_b1_d_t:
      L_dashdash_b2_d_t: param L_dashdash_ba1_d_t:
      L_dashdash_ba2_d_t: param W_dash_ba1_d_t:
      package_id:
      theta_ex_d_Ave_d:
      L_dashdash_s_d_t:
      L_dashdash_b1_d_t:
      L_dashdash_ba1_d_t:
      W_dash_ba1_d_t:

    Returns:

    """
    # ハイブリッド給湯機の仕様
    if package_id != None:
        # 試験された値を用いる場合
        spec = get_specification(package_id)
    else:
        # パラメーターが直接指定された場合
        spec = get_input_specification(hybrid_param)

    # 1時間当たりの太陽熱補正給湯熱負荷 (MJ/h)
    L_dashdash_d_t = get_L_dashdash_d_t(L_dashdash_k_d_t, L_dashdash_s_d_t, L_dashdash_w_d_t, L_dashdash_b1_d_t,
                                        L_dashdash_b2_d_t, L_dashdash_ba1_d_t)

    # 1日当たりの太陽熱補正給湯熱負荷 (MJ/d)
    L_dashdash_d = get_L_dashdash_d(L_dashdash_d_t)

    # 1日当たりの電気ヒートポンプが分担する給湯熱負荷 (MJ/d)
    L_HP_d = get_L_HP_d(spec['Theta_ex_min_HP'], spec['a_HP'], spec['b_HP'], spec['etr_loss_TU'], spec['Q_HP_max'],
                        L_dashdash_d, theta_ex_d_Ave_d)

    # 1日当たりの電気ヒートポンプの加熱量 (MJ/d)
    Q_HP_d = get_Q_HP_d(L_HP_d, spec['etr_loss_TU'])

    # 電気ヒートポンプの日平均熱効率 (-)
    e_HP_d = get_e_HP_d(spec['e_HP_std_m7'], spec['e_HP_std_2'], spec['e_HP_std_7'], spec['e_HP_std_25'], theta_ex_d_Ave_d)

    # 1時間当たりの電気ヒートポンプの消費電力量 (kWh/h)
    E_E_hs_HP_d_t = get_E_E_hs_HP_d_t(Q_HP_d, e_HP_d, spec['R_day'], L_dashdash_d, L_dashdash_d_t)

    # 1時間当たりのタンクユニットの消費電力量 (kWh/h)
    E_E_hs_TU_d_t = get_E_E_hs_TU_d_t(spec['a_TU'], spec['b_TU'], L_dashdash_d, L_dashdash_d_t)

    # 1時間当たりの保温時における消費電力量 (kWh/h)
    L_dashdash_ba2_d = get_L_dashdash_ba2_d(L_dashdash_ba2_d_t)
    L_BB_ba2_d = get_L_BB_ba2_d(L_dashdash_ba2_d)
    L_BB_ba2_d_t = get_L_BB_ba2_d_t(L_dashdash_ba2_d_t)
    E_E_hs_BB_d_t = get_E_E_hs_BB_d_t(W_dash_ba1_d_t, L_BB_ba2_d, L_BB_ba2_d_t, bath_function)

    print('E_E_hs_HP = {}'.format(np.sum(E_E_hs_HP_d_t)))
    print('E_E_hs_TU = {}'.format(np.sum(E_E_hs_TU_d_t)))
    print('E_E_hs_BB = {}'.format(np.sum(E_E_hs_BB_d_t)))

    return E_E_hs_HP_d_t + E_E_hs_TU_d_t + E_E_hs_BB_d_t

# 1時間当たりの電気ヒートポンプ消費電力量 (kWh/h)  (9-1) (9-2)
def get_E_E_hs_HP_d_t(Q_HP_d, e_HP_d, R_day, L_dashdash_d, L_dashdash_d_t):
    """

    Args:
      Q_HP_d: 1日当たりの電気ヒートポンプの過熱量 (MJ/d)
      e_HP_d: 電気ヒートポンプの日平均熱効率 (-)
      R_day: ヒートポンプ昼間沸上率（-）
      L_dashdash_d: 1日当たりの太陽熱補正給湯熱負荷 (MJ/d)
      L_dashdash_d_t: 1時間当たりの太陽熱補正給湯熱負荷 (MJ/h)

    Returns:
      1時間当たりの電気ヒートポンプ消費電力量 (kWh/h)

    """
    E_E_hs_HP_d_t = np.zeros(24 * 365)

    # 24時間化
    L_dashdash_d = np.repeat(L_dashdash_d, 24)
    e_HP_d = np.repeat(e_HP_d, 24)
    Q_HP_d = np.repeat(Q_HP_d, 24)

    # ヒートポンプ昼間沸上運転開始時刻
    t_bw_start = get_t_bw_start()

    # ヒートポンプ昼間沸上運転終了時刻
    t_bw_end = get_t_bw_end()

    # t_bw_start <= t < t_bw_end の場合 (9-1)
    t1 = np.tile(np.logical_and(t_bw_start <= np.arange(24), np.arange(24) < t_bw_end), 365)

    E_E_hs_HP_d_t[t1] = Q_HP_d[t1] / (3.6 * e_HP_d[t1]) * R_day * (1 / (t_bw_end - t_bw_start))

    # 0 <= t < t_bw_start または　t_bw_end <= t < 24 の場合 (9-2)
    t2 = np.tile(np.logical_or(np.logical_and(0 <= np.arange(24), np.arange(24) < t_bw_start),
                               np.logical_and(t_bw_end <= np.arange(24), np.arange(24) < 24)), 365)

    t3 = np.tile(np.logical_and(t_bw_start <= np.arange(24), np.arange(24) < t_bw_end - 1), 365)

    temp = L_dashdash_d_t.copy()
    temp[t2] = 0.0
    L_dashdash_d_t_start_end = np.repeat(np.sum(temp.reshape((365, 24)), axis=1), 24)

    f1 = L_dashdash_d != L_dashdash_d_t_start_end

    f2 = np.logical_and(t2, f1)

    E_E_hs_HP_d_t[f2] = Q_HP_d[f2] / (3.6 * e_HP_d[f2]) * (1 - R_day) * \
                        (L_dashdash_d_t[f2] / (L_dashdash_d[f2] - L_dashdash_d_t_start_end[f2]))

    return E_E_hs_HP_d_t

def get_t_bw_start():
    """ヒートポンプ昼間沸上運転開始時刻

    Args:

    Returns:
      int: ヒートポンプ昼間沸上運転開始時刻

    """
    return 9

def get_t_bw_end():
    """ヒートポンプ昼間沸上運転終了時刻

    Args:

    Returns:
      int: ヒートポンプ昼間沸上運転終了時刻

    """
    return 16

# 1時間当たりのタンクユニットの消費電力量 (10)
def get_E_E_hs_TU_d_t(a_TU, b_TU, L_dashdash_d, L_dashdash_d_t):
    """

    Args:
      a_TU: param b_TU:
      L_dashdash_d: param L_dashdash_d_t:
      b_TU:
      L_dashdash_d_t:

    Returns:

    """
    E_E_hs_TU_d_t = np.zeros(24 * 365)

    # L_dashdash_d_t > 0
    f1 = L_dashdash_d_t > 0
    E_E_hs_TU_d_t[f1] = a_TU * L_dashdash_d_t[f1] + b_TU / 24

    # L_dashdash_d = 0
    f2 = L_dashdash_d_t == 0
    E_E_hs_TU_d_t[f2] = b_TU / 24

    return E_E_hs_TU_d_t

# 1日当たりの保温時における消費電力量 (11)
def get_E_E_hs_BB_d_t(W_dash_ba1_d_t, L_BB_ba2_d, L_BB_ba2_d_t, bath_function):
    """

    Args:
      W_dash_ba1_d_t: param L_BB_ba2_d:
      L_BB_ba2_d_t: param bath_function:
      L_BB_ba2_d:
      bath_function:

    Returns:

    """
    if bath_function == '給湯単機能' or bath_function == 'ふろ給湯機(追焚なし)':
        # (11a)
        return (0.000393 * W_dash_ba1_d_t * 10 ** 3 / 3600)
    elif bath_function == 'ふろ給湯機(追焚あり)':
        # (11b)
        L_BB_ba2_d = np.repeat(L_BB_ba2_d, 24)
        E_E_hs_BB_d_t = np.zeros(24 * 365)

        fb1 = L_BB_ba2_d > 0
        E_E_hs_BB_d_t[fb1] = ((0.01723 * L_BB_ba2_d[fb1] + 0.06099) * 10 ** 3 / 3600) \
                             * L_BB_ba2_d_t[fb1] / L_BB_ba2_d[fb1]

        fb2 = L_BB_ba2_d == 0
        E_E_hs_BB_d_t[fb2] = 0

        return E_E_hs_BB_d_t
    else:
        raise ValueError(bath_function)

# ============================================================================
# G.3.5 ガス消費量
# ============================================================================

# 1日当たりの給湯機のガス消費量 (12)
def get_E_G_hs_d_t(bath_function, package_id, theta_ex_d_Ave_d, L_dashdash_k_d_t, L_dashdash_s_d_t, L_dashdash_w_d_t,
                   L_dashdash_b1_d_t, L_dashdash_b2_d_t, L_dashdash_ba1_d_t, L_dashdash_ba2_d_t, W_dash_ba1_d_t, hybrid_param):
    """

    Args:
      bath_function: param package_id:
      theta_ex_d_Ave_d: param L_dashdash_k_d_t:
      L_dashdash_s_d_t: param L_dashdash_w_d_t:
      L_dashdash_b1_d_t: param L_dashdash_b2_d_t:
      L_dashdash_ba1_d_t: param L_dashdash_ba2_d_t:
      W_dash_ba1_d_t: param hybrid_param:
      package_id:
      L_dashdash_k_d_t:
      L_dashdash_w_d_t:
      L_dashdash_b2_d_t:
      L_dashdash_ba2_d_t:
      hybrid_param:

    Returns:

    """
    # ハイブリッド給湯機の仕様
    if package_id != None:
        # 試験された値を用いる場合
        spec = get_specification(package_id)
    else:
        # パラメーターが直接指定された場合
        spec = get_input_specification(hybrid_param)

    # 当該給湯機に対する効率の補正係数
    f_hs = get_f_hs(spec['e_BB_jis'])

    L_dashdash_d_t = get_L_dashdash_d_t(
        L_dashdash_k_d_t=L_dashdash_k_d_t,
        L_dashdash_s_d_t=L_dashdash_s_d_t,
        L_dashdash_w_d_t=L_dashdash_w_d_t,
        L_dashdash_b1_d_t=L_dashdash_b1_d_t,
        L_dashdash_b2_d_t=L_dashdash_b2_d_t,
        L_dashdash_ba1_d_t=L_dashdash_ba1_d_t,
    )
    L_dashdash_k_d = get_L_dashdash_k_d(L_dashdash_k_d_t)
    L_dashdash_s_d = get_L_dashdash_s_d(L_dashdash_s_d_t)
    L_dashdash_w_d = get_L_dashdash_w_d(L_dashdash_w_d_t)
    L_dashdash_b1_d = get_L_dashdash_b1_d(L_dashdash_b1_d_t)
    L_dashdash_b2_d = get_L_dashdash_b2_d(L_dashdash_b2_d_t)
    L_dashdash_ba1_d = get_L_dashdash_ba1_d(L_dashdash_ba1_d_t)
    L_dashdash_ba2_d = get_L_dashdash_ba1_d(L_dashdash_ba2_d_t)
    L_dashdash_d = get_L_dashdash_d(L_dashdash_d_t)

    L_HP = get_L_HP_d(
        Theta_ex_min_HP=spec['Theta_ex_min_HP'],
        a_HP=spec['a_HP'],
        b_HP=spec['b_HP'],
        etr_loss_TU=spec['etr_loss_TU'],
        Q_HP_max=spec['Q_HP_max'],
        L_dashdash_d=L_dashdash_d,
        theta_ex_d_Ave_d=theta_ex_d_Ave_d
    )
    L_BB_k_d = get_L_BB_k_d(L_HP, L_dashdash_d, L_dashdash_k_d)
    L_BB_s_d = get_L_BB_s_d(L_HP, L_dashdash_d, L_dashdash_s_d)
    L_BB_w_d = get_L_BB_w_d(L_HP, L_dashdash_d, L_dashdash_w_d)
    L_BB_b1_d = get_L_BB_b1_d(L_HP, L_dashdash_d, L_dashdash_b1_d)
    L_BB_b2_d = get_L_BB_b2_d(L_HP, L_dashdash_d, L_dashdash_b2_d)
    L_BB_ba1_d = get_L_BB_ba1_d(L_HP, L_dashdash_d, L_dashdash_ba1_d)
    L_BB_ba2_d = get_L_BB_ba2_d(L_dashdash_ba2_d)

    L_BB_k_d_t = get_L_BB_k_d_t(L_BB_k_d, L_dashdash_k_d_t)
    L_BB_s_d_t = get_L_BB_s_d_t(L_BB_s_d, L_dashdash_s_d_t)
    L_BB_w_d_t = get_L_BB_w_d_t(L_BB_w_d, L_dashdash_w_d_t)
    L_BB_b1_d_t = get_L_BB_b1_d_t(L_BB_b1_d, L_dashdash_b1_d_t)
    L_BB_b2_d_t = get_L_BB_b2_d_t(L_BB_b2_d, L_dashdash_b2_d_t)
    L_BB_ba1_d_t = get_L_BB_ba1_d_t(L_BB_ba1_d, L_dashdash_ba1_d_t)
    L_BB_ba2_d_t = get_L_BB_ba2_d_t(L_dashdash_ba2_d_t)

    # 日平均給湯機効率
    e_BB_k_d = get_e_k_d(theta_ex_d_Ave_d, L_BB_k_d, L_BB_w_d, f_hs)
    e_BB_s_d = get_e_s_d(theta_ex_d_Ave_d, L_BB_s_d, f_hs)
    e_BB_w_d = get_e_w_d(theta_ex_d_Ave_d, L_BB_k_d, L_BB_w_d, f_hs)

    if bath_function == '給湯単機能':

        # 日平均給湯機効率
        e_BB_b1_d = get_e_b1_d(theta_ex_d_Ave_d, L_BB_b1_d, f_hs)
        e_BB_ba1_d = get_e_ba1_d(theta_ex_d_Ave_d, L_BB_ba1_d, f_hs)

        # (12a)
        E_G_hs_d_t = L_BB_k_d_t / np.repeat(e_BB_k_d, 24) \
                     + L_BB_s_d_t / np.repeat(e_BB_s_d, 24) \
                     + L_BB_w_d_t / np.repeat(e_BB_w_d, 24) \
                     + L_BB_b1_d_t / np.repeat(e_BB_b1_d, 24) \
                     + L_BB_ba1_d_t / np.repeat(e_BB_ba1_d, 24)
    elif bath_function == 'ふろ給湯機(追焚なし)':

        # 日平均給湯機効率
        e_BB_b2_d = get_e_b2_d(theta_ex_d_Ave_d, L_BB_b2_d, f_hs)
        e_BB_ba1_d = get_e_ba1_d(theta_ex_d_Ave_d, L_BB_ba1_d, f_hs)

        # (12b)
        E_G_hs_d_t = L_BB_k_d_t / np.repeat(e_BB_k_d, 24) \
                     + L_BB_s_d_t / np.repeat(e_BB_s_d, 24) \
                     + L_BB_w_d_t / np.repeat(e_BB_w_d, 24) \
                     + L_BB_b2_d_t / np.repeat(e_BB_b2_d, 24) \
                     + L_BB_ba1_d_t / np.repeat(e_BB_ba1_d, 24)
    elif bath_function == 'ふろ給湯機(追焚あり)':

        # 日平均給湯機効率
        e_BB_b2_d = get_e_b2_d(theta_ex_d_Ave_d, L_BB_b2_d, f_hs)
        e_BB_ba2_d = get_e_ba2_d(theta_ex_d_Ave_d, L_BB_ba2_d, f_hs)

        # (12c)
        E_G_hs_d_t = L_BB_k_d_t / np.repeat(e_BB_k_d, 24) \
                     + L_BB_s_d_t / np.repeat(e_BB_s_d, 24) \
                     + L_BB_w_d_t / np.repeat(e_BB_w_d, 24) \
                     + L_BB_b2_d_t / np.repeat(e_BB_b2_d, 24) \
                     + L_BB_ba2_d_t / np.repeat(e_BB_ba2_d, 24)
    else:
        raise ValueError(bath_function)

    return E_G_hs_d_t

# ============================================================================
# G.3.6 電気ヒートポンプの加熱量
# ============================================================================

# 1日当たりの電気ヒートポンプの加熱量 (13)
def get_Q_HP_d(L_HP_d, etr_loss_TU):
    """

    Args:
      L_HP_d: param etr_loss_TU:
      etr_loss_TU:

    Returns:

    """
    return L_HP_d / (1 - etr_loss_TU)

# ============================================================================
# G.3.7 電気ヒートポンプの日平均熱効率
# ============================================================================

# 電気ヒートポンプの日平均熱効率 (14)
def get_e_HP_d(e_HP_std_m7, e_HP_std_2, e_HP_std_7, e_HP_std_25, theta_ex_d_Ave_d):
    """

    Args:
      e_HP_std_m7: param e_HP_std_2:
      e_HP_std_7: param e_HP_std_25:
      theta_ex_d_Ave_d:
      e_HP_std_2:
      e_HP_std_25:

    Returns:

    """
    e_HP_d = np.zeros(365)

    # 1) theta_ex_d_Ave_d < 2
    f1 = (theta_ex_d_Ave_d < 2)
    e_HP_d[f1] = e_HP_std_2 - (2 - theta_ex_d_Ave_d[f1]) / 9 * (e_HP_std_2 - e_HP_std_m7)

    # 2) 2 <= theta_ex_d_Ave_d < 7
    f2 = np.logical_and(2 <= theta_ex_d_Ave_d, theta_ex_d_Ave_d < 7)
    e_HP_d[f2] = e_HP_std_7 - (7 - theta_ex_d_Ave_d[f2]) / 5 * (e_HP_std_7 - e_HP_std_2)

    # 3) 7 <= theta_ex_d_Ave_d < 25
    f3 = np.logical_and(7 <= theta_ex_d_Ave_d, theta_ex_d_Ave_d < 25)
    e_HP_d[f3] = e_HP_std_25 - (25 - theta_ex_d_Ave_d[f3]) / 18 * (e_HP_std_25 - e_HP_std_7)

    # 4) 25 <= theta_ex_d_Ave_d
    f4 = 25 <= theta_ex_d_Ave_d
    e_HP_d[f4] = e_HP_std_25

    return e_HP_d

# ============================================================================
# G.3.8 給湯熱負荷
# ============================================================================

# バックアップボイラーが分担する給湯熱負荷

# (15a)
def get_L_BB_k_d_t(L_BB_k_d, L_dashdash_k_d_t):
    """

    Args:
      L_BB_k_d: param L_dashdash_k_d_t:
      L_dashdash_k_d_t:

    Returns:

    """
    L_BB_k_d_t = np.zeros(24 * 365)

    L_BB_k_d = np.repeat(L_BB_k_d, 24)
    L_dashdash_k_d = np.repeat(get_L_dashdash_k_d(L_dashdash_k_d_t), 24)

    f = L_dashdash_k_d > 0
    L_BB_k_d_t[f] = L_BB_k_d[f] * L_dashdash_k_d_t[f] / L_dashdash_k_d[f]

    return L_BB_k_d_t

# (15b)
def get_L_BB_s_d_t(L_BB_s_d, L_dashdash_s_d_t):
    """

    Args:
      L_BB_s_d: param L_dashdash_s_d_t:
      L_dashdash_s_d_t:

    Returns:

    """
    L_BB_s_d_t = np.zeros(24 * 365)

    L_BB_s_d = np.repeat(L_BB_s_d, 24)
    L_dashdash_s_d = np.repeat(get_L_dashdash_s_d(L_dashdash_s_d_t), 24)

    f = L_dashdash_s_d > 0
    L_BB_s_d_t[f] = L_BB_s_d[f] * L_dashdash_s_d_t[f] / L_dashdash_s_d[f]

    return L_BB_s_d_t

# (15c)
def get_L_BB_w_d_t(L_BB_w_d, L_dashdash_w_d_t):
    """

    Args:
      L_BB_w_d: param L_dashdash_w_d_t:
      L_dashdash_w_d_t:

    Returns:

    """
    L_BB_w_d_t = np.zeros(24 * 365)

    L_BB_w_d = np.repeat(L_BB_w_d, 24)
    L_dashdash_w_d = np.repeat(get_L_dashdash_w_d(L_dashdash_w_d_t), 24)

    f = L_dashdash_w_d > 0
    L_BB_w_d_t[f] = L_BB_w_d[f] * L_dashdash_w_d_t[f] / L_dashdash_w_d[f]

    return L_BB_w_d_t

# (15d)
def get_L_BB_b1_d_t(L_BB_b1_d, L_dashdash_b1_d_t):
    """

    Args:
      L_BB_b1_d: param L_dashdash_b1_d_t:
      L_dashdash_b1_d_t:

    Returns:

    """
    L_BB_b1_d_t = np.zeros(24 * 365)

    L_BB_b1_d = np.repeat(L_BB_b1_d, 24)
    L_dashdash_b1_d = np.repeat(get_L_dashdash_b1_d(L_dashdash_b1_d_t), 24)

    f = L_dashdash_b1_d > 0
    L_BB_b1_d_t[f] = L_BB_b1_d[f] * L_dashdash_b1_d_t[f] / L_dashdash_b1_d[f]

    return L_BB_b1_d_t

# (15e)
def get_L_BB_b2_d_t(L_BB_b2_d, L_dashdash_b2_d_t):
    """

    Args:
      L_BB_b2_d: param L_dashdash_b2_d_t:
      L_dashdash_b2_d_t:

    Returns:

    """
    L_BB_b2_d_t = np.zeros(24 * 365)

    L_BB_b2_d = np.repeat(L_BB_b2_d, 24)
    L_dashdash_b2_d = np.repeat(get_L_dashdash_b2_d(L_dashdash_b2_d_t), 24)

    f = L_dashdash_b2_d > 0
    L_BB_b2_d_t[f] = L_BB_b2_d[f] * L_dashdash_b2_d_t[f] / L_dashdash_b2_d[f]

    return L_BB_b2_d_t

# (15f)
def get_L_BB_ba1_d_t(L_BB_ba1_d, L_dashdash_ba1_d_t):
    """

    Args:
      L_BB_ba1_d: param L_dashdash_ba1_d_t:
      L_dashdash_ba1_d_t:

    Returns:

    """
    L_BB_ba1_d_t = np.zeros(24 * 365)

    L_BB_ba1_d = np.repeat(L_BB_ba1_d, 24)
    L_dashdash_ba1_d = np.repeat(get_L_dashdash_ba1_d(L_dashdash_ba1_d_t), 24)

    f = L_dashdash_ba1_d > 0
    L_BB_ba1_d_t[f] = L_BB_ba1_d[f] * L_dashdash_ba1_d_t[f] / L_dashdash_ba1_d[f]

    return L_BB_ba1_d_t

# (15g)
def get_L_BB_ba2_d_t(L_dashdash_ba2_d_t):
    """

    Args:
      L_dashdash_ba2_d_t:

    Returns:

    """
    return L_dashdash_ba2_d_t

# (16a)
def get_L_BB_k_d(L_HP_d, L_dashdash_d, L_dashdash_k_d):
    """

    Args:
      L_HP_d: param L_dashdash_d:
      L_dashdash_k_d:
      L_dashdash_d:

    Returns:

    """
    return L_dashdash_k_d - L_HP_d * (L_dashdash_k_d / L_dashdash_d)

# (16b)
def get_L_BB_s_d(L_HP_d, L_dashdash_d, L_dashdash_s_d):
    """

    Args:
      L_HP_d: param L_dashdash_d:
      L_dashdash_s_d:
      L_dashdash_d:

    Returns:

    """
    return L_dashdash_s_d - L_HP_d * (L_dashdash_s_d / L_dashdash_d)

# (16c)
def get_L_BB_w_d(L_HP_d, L_dashdash_d, L_dashdash_w_d):
    """

    Args:
      L_HP_d: param L_dashdash_d:
      L_dashdash_w_d:
      L_dashdash_d:

    Returns:

    """
    return L_dashdash_w_d - L_HP_d * (L_dashdash_w_d / L_dashdash_d)

# (16d)
def get_L_BB_b1_d(L_HP_d, L_dashdash_d, L_dashdash_b1_d):
    """

    Args:
      L_HP_d: param L_dashdash_d:
      L_dashdash_b1_d:
      L_dashdash_d:

    Returns:

    """
    return L_dashdash_b1_d - L_HP_d * (L_dashdash_b1_d / L_dashdash_d)

# (16e)
def get_L_BB_b2_d(L_HP_d, L_dashdash_d, L_dashdash_b2_d):
    """

    Args:
      L_HP_d: param L_dashdash_d:
      L_dashdash_b2_d:
      L_dashdash_d:

    Returns:

    """
    return L_dashdash_b2_d - L_HP_d * (L_dashdash_b2_d / L_dashdash_d)

# (16f)
def get_L_BB_ba1_d(L_HP_d, L_dashdash_d, L_dashdash_ba1_d):
    """

    Args:
      L_HP_d: param L_dashdash_d:
      L_dashdash_ba1_d:
      L_dashdash_d:

    Returns:

    """
    return L_dashdash_ba1_d - L_HP_d * (L_dashdash_ba1_d / L_dashdash_d)

# (16g)
def get_L_BB_ba2_d(L_dashdash_ba2_d):
    """

    Args:
      L_dashdash_ba2_d:

    Returns:

    """
    return L_dashdash_ba2_d

# ============================================================================
# G.4 1日当たり／1時間当たりの太陽熱補正給湯熱負荷
# ============================================================================

# 1日当たりの電気ヒートポンプが分担する給湯熱負荷 (17)
def get_L_HP_d(Theta_ex_min_HP, a_HP, b_HP, etr_loss_TU, Q_HP_max, L_dashdash_d, theta_ex_d_Ave_d):
    """

    Args:
      Theta_ex_min_HP: param a_HP:
      b_HP: param etr_loss_TU:
      Q_HP_max: param L_dashdash_d:
      theta_ex_d_Ave_d:
      a_HP:
      etr_loss_TU:
      L_dashdash_d:

    Returns:

    """
    L_HP_d = np.zeros(365)

    # 1. theta_ex_d_Ave_d >= Theta_ex_min_HP (17a)
    f1 = (theta_ex_d_Ave_d >= Theta_ex_min_HP)
    L_HP_d[f1] = np.clip(
        (a_HP * L_dashdash_d[f1] + b_HP) * (1 - etr_loss_TU),
        None,
        np.clip(L_dashdash_d[f1], None, Q_HP_max * (1 - etr_loss_TU))
    )

    # 2. theta_ex_d_Ave_d < Theta_ex_min_HP (17b)
    f2 = (theta_ex_d_Ave_d < Theta_ex_min_HP)
    L_HP_d[f2] = 0

    return L_HP_d

# 1日当たりの太陽熱補正給湯熱負荷 (MJ/d) (18)
def get_L_dashdash_d(L_dashdash_d_t):
    """

    Args:
      L_dashdash_d_t: 1時間当たりの太陽熱補正給湯熱負荷 (MJ/h)

    Returns:
      1日当たりの太陽熱補正給湯熱負荷 (MJ/d)

    """
    return np.sum(L_dashdash_d_t.reshape(365, 24), axis=1)

# 1時間当たりの太陽熱補正給湯熱負荷 (MJ/h) (19)
def get_L_dashdash_d_t(L_dashdash_k_d_t, L_dashdash_s_d_t, L_dashdash_w_d_t, L_dashdash_b1_d_t, L_dashdash_b2_d_t,
                       L_dashdash_ba1_d_t):
    """

    Args:
      L_dashdash_k_d_t: 1時間当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/h)
      L_dashdash_s_d_t: 1時間当たりの浴室シャワー水栓における太陽熱補正給湯負荷 (MJ/h)
      L_dashdash_w_d_t: 1時間当たりの洗面水栓における太陽熱補正給湯負荷 (MJ/h)
      L_dashdash_b1_d_t: 1時間当たりの浴槽水栓湯はり時における太陽熱補正給湯負荷 (MJ/h)
      L_dashdash_b2_d_t: 1時間当たりの浴槽自動湯はり時における太陽熱補正給湯負荷 (MJ/h)
      L_dashdash_ba1_d_t: 1時間当たりの浴槽水栓さし湯時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      1時間当たりの太陽熱補正給湯熱負荷 (MJ/h)

    """
    return L_dashdash_k_d_t \
           + L_dashdash_s_d_t \
           + L_dashdash_w_d_t \
           + L_dashdash_b1_d_t \
           + L_dashdash_b2_d_t \
           + L_dashdash_ba1_d_t

# 1日当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/d)
def get_L_dashdash_k_d(L_dashdash_k_d_t):
    """

    Args:
      L_dashdash_k_d_t: 1時間当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/h)

    Returns:
      1日当たりの台所水栓における太陽熱補正給湯熱負荷 (MJ/d)

    """
    return np.sum(L_dashdash_k_d_t.reshape((365, 24)), axis=1)

# 1日当たりの浴室シャワー水栓における太陽熱補正給湯負荷 (MJ/d)
def get_L_dashdash_s_d(L_dashdash_s_d_t):
    """

    Args:
      L_dashdash_s_d_t: 1時間当たりの浴室シャワー水栓における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      1日当たりの浴室シャワー水栓における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_s_d_t.reshape((365, 24)), axis=1)

# 1日当たりの洗面水栓における太陽熱補正給湯負荷 (MJ/d)
def get_L_dashdash_w_d(L_dashdash_w_d_t):
    """

    Args:
      L_dashdash_w_d_t: 1時間当たりの洗面水栓における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      1日当たりの洗面水栓における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_w_d_t.reshape((365, 24)), axis=1)

# 1日当たりの浴槽水栓湯はり時における太陽熱補正給湯負荷 (MJ/d)
def get_L_dashdash_b1_d(L_dashdash_b1_d_t):
    """

    Args:
      L_dashdash_b1_d_t: 1時間当たりの浴槽水栓湯はり時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      1日当たりの浴槽水栓湯はり時における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_b1_d_t.reshape((365, 24)), axis=1)

# 1日当たりの浴槽自動湯はり時における太陽熱補正給湯負荷 (MJ/d)
def get_L_dashdash_b2_d(L_dashdash_b2_d_t):
    """

    Args:
      L_dashdash_b2_d_t: 1時間当たりの浴槽自動湯はり時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      1日当たりの浴槽自動湯はり時における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_b2_d_t.reshape((365, 24)), axis=1)

# 1日当たりの浴槽水栓さし湯時における太陽熱補正給湯負荷 (MJ/d)
def get_L_dashdash_ba1_d(L_dashdash_ba1_d_t):
    """

    Args:
      L_dashdash_ba1_d_t: 1時間当たりの浴槽水栓さし湯時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      1日当たりの浴槽水栓さし湯時における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_ba1_d_t.reshape((365, 24)), axis=1)

# 1日当たりの浴槽追焚時における太陽熱補正給湯負荷 (MJ/d)
def get_L_dashdash_ba2_d(L_dashdash_ba2_d_t):
    """

    Args:
      L_dashdash_ba2_d_t: 1時間当たりの浴槽追焚時における太陽熱補正給湯負荷 (MJ/h)

    Returns:
      1日当たりの浴槽追焚時における太陽熱補正給湯負荷 (MJ/d)

    """
    return np.sum(L_dashdash_ba2_d_t.reshape((365, 24)), axis=1)