# -*- coding: utf-8 -*-
"""
Created on Sun May 21 08:51:33 2023

@author: atakan
"""


import copy

# import src.models.fluids.fluid_props as fprop
import numpy as np
from scipy.optimize import minimize  # root, root_scalar
import matplotlib.pyplot as plt
import carbatpy as cb


class StaticHeatExchanger:
    """
    Class for static counter-flow heat exchanger

    means: no time dependence and no heat transfer coefficients * areas
    are used (UA)! Instead a minimum approach temperature is tried to be met.
    At the moment, this is mainly done by varying one of the mass flow rates.
    But this is sometimes not enough and a variation of the working fluid
    pressure will soon be included.
    Only the first law and second law will be checked (the latter must be
    improved).

    """

    def __init__(self, fluids, h_dot_min, h_out_w, h_limit_s=np.nan,
                 points=50, d_temp_separation_min=0.5, calc_type="const",
                 name="evaporator"):
        """
        class to calculate (static/steady state) heat-exchangers

        includes pinch-point analysis and plotting,
        only implemented for simple thermodynamic calculations
        (no convection coefficients and heat exchanger areas regarded yet)

        Parameters
        ----------
        fluids : list of 2 fprop.Fluid
            The definition of the two fluids, as they enter the heat exchangers.
            Typically at room temperature.
        h_dot_min : float
            enthalpy flow rate (W) which has to be transfered.
        h_out_w : float
            exit enthalpy of the working fluid.
        h_limit_s : float or nan, optional
            if there is a limit in enthalpy for the secondary fluid, it can be
            given here. The default is nan.
        points : integer, optional
            for how many points (array) shall the minimum approach temperature
            be checked and properties be returned (for plotting etc.)
            The default is 30.
        d_temp_separation_min : float, optional
            Minimium approach temperature (pinch point) between the two fluids.
            The default is 0.5.
        calc_type : string, optional
            which calculation type shall be performed; only one implemented so
            far. The default is "const".
        name : string, optional
            name of the heat exchanger. The default is "evaporator".

        Returns
        -------
        None.

        """
        self.fluids = fluids
        state_in_w = fluids[0].properties.state
        self.m_dot_s = 0
        self.h_limit_s = h_limit_s
        self.q_dot = h_dot_min
        self.m_dot_w = np.abs(h_dot_min/(h_out_w-state_in_w[2]))
        self.h_out_w = h_out_w
        self.h_out_s = float(copy.copy(h_limit_s))  # this may be varied
        self.points = points
        self. d_temp_separation_min = d_temp_separation_min
        self.heating = -1
        if h_out_w < state_in_w[2]:
            self.heating = 1  # condenser (heating of the secondary fluid)
        self.calc_type = calc_type
        self.name = name
        self.all_states = np.zeros(
            (points, len(cb.fprop._THERMO_STRING.split(";"))))
        self.h_in_out = np.zeros((2, 4))
        self.dt_mean = None
        self.dt_min = None
        self.dt_max = None
        self.warning = 0
        self.warning_message = "All o.k."

    def pinch_calc(self,  verbose=False, plot_info={}):
        """
        Calculate the changes in enthalpy and temperature in the heat exchanger

        counter-flow hex assumed! Both flows are isobaric.
        Is used to check, whether the second law is violated. The factor can
        be used to vary the mass flow rate of the working fluid, until no
        violation is found (done in root finding).

        Parameters
        ----------
        verbose : boolean, optional
            if True, several variables will be printed out. Default is False.
        plot_info : dictionary, optional
            if not empty a Figure, an Axes, a list of What shall be plotted,
            a list with the colour/styles and a list with the labels must be 
            passed. in "what", the two numbers coincide with the fluid THERMO
            order. The x-shift can be used in cycle calculations, to shift the
            curves, by the value (it will be added).
            The names in the dictionary are: "fig", "ax", "what","col",
            "label", "x-shift". Default is empty.


        Raises
        ------
        Exception
            if temperatures are not consistent.

        Returns
        -------
        m_dot_s : float
            secondary fluid mass flow rate (kg/s.
        d_tempall : numpy-array
            The temperature differences along the counter-flow heat exchanger.
        w_array : array
            properties of the working fluid along the heat exchanger
            (T,p,h, etc. see fluid class).
        s_array : array
            properties of the secondary fluid along the heat exchanger
            (T,p,h, etc. see fluid class).

        """
        self.warning = 0
        self.warning_message = "All o.k."
        w_in = copy.copy(self.fluids[0])
        s_in = copy.copy(self.fluids[1])

        w_out = copy.copy(self.fluids[0])  # not yet the correct state!
        s_out = copy.copy(self.fluids[1])

        #  fixed values
        self. h_in_out[1, 0] = s_in.properties.enthalpy
        self. h_in_out[0, 0] = w_in.properties.enthalpy
        self. h_in_out[0, 1] = self.h_out_w

        # fixed limiting state, secondary fluid
        state_out_s = s_out.set_state([self.h_out_s,
                                       s_in.properties.pressure], "HP")

        self. h_in_out[1, 1] = state_out_s[2]

        h_delta_s = np.abs(
            state_out_s[2] - s_in.properties.enthalpy)

        # fixed heat flow,  determines mass flow rate
        self.m_dot_s = self.q_dot / h_delta_s

        h_array = np.linspace(
            self.h_in_out[1, 0], self.h_in_out[1, 1], self.points)
        values = np.zeros((self.points, 2))
        values[:, 0] = h_array
        values[:, 1] = s_out.properties.pressure

        s_array = s_out.set_state_v(values, given="HP")
        # if not(self.heating): s_array =np.flip(s_array, axis =0)

        # now working fluid:

        h_array = np.linspace(self.h_in_out[0, 1],
                              self.h_in_out[0, 0],
                              self.points)

        values = np.zeros((self.points, 2))
        values[:, 0] = h_array.T
        values[:, 1] = w_out.properties.pressure

        w_array = w_out.set_state_v(values, "HP")
        # temperature difference, ok?
        d_tempall = w_array[:, 0]-s_array[:, 0]
        self.dt_mean = d_tempall.mean()
        self.dt_min = np.abs(d_tempall).min()
        self.dt_max = np.abs(d_tempall).max()

        if abs(self.dt_min) - self.d_temp_separation_min < -1e-3:
            self.warning = 900
            self.warning_message = "Below minumum approch temperature!"
        positive = np.any(d_tempall > 0)
        negative = np.any(d_tempall < 0)
        below = True
        if self.heating < 0:
            below = False

        crossing = (positive > 0 and negative > 0)
        wrong_side = (positive > 0 and not below) or (negative > 0 and below)
        if crossing or wrong_side:
            self.warning = 999
            self.dt_mean = 1e6
            self.warning_message = "Temperatures crossing or wrong side!"

        # if self.heating:
        #     d_tempall = w_array[:, 0]-s_array[:, 0]
        # else:
        #     d_tempall = w_array[:, 0] - np.flip(s_array[:, 0],axis=0)
        
        if len(plot_info)>0:
            if plot_info["what"][0] ==2:
                data = (w_array[:,plot_info["what"][0]] -
                        w_array[:,plot_info["what"][0]].min()) \
                    *self.m_dot_w + plot_info["x-shift"][0]
                plot_info["ax"].plot(data,
                                     w_array[:,plot_info["what"][1]],
                                     plot_info["col"][0],
                                     label=plot_info["label"][0])
                data = (s_array[:,plot_info["what"][0]] -
                        s_array[:,plot_info["what"][0]].min()) \
                    *self.m_dot_s + plot_info["x-shift"][1]
                plot_info["ax"].plot(data,
                                     s_array[:,plot_info["what"][1]],
                                     plot_info["col"][1],
                                     label=plot_info["label"][1])
            else:
                print(f"H-Ex: plotting only implemented fot T-H_dot [2,0]. You requested{plot_info['what']}")
        if verbose:
            print(
                f"Min T-distance {self.dt_min}, Mean T-distance {self.dt_mean}")
            if self.heating > 0:
                print("cond", d_tempall[0], d_tempall[-1],
                      d_tempall.min(), d_tempall.max())
            else:
                print("evap", d_tempall[0], d_tempall[-1],
                      d_tempall.max(), d_tempall.min())

        return self.m_dot_s, d_tempall*self.heating, w_array, s_array

    def pinch_root(self, h_out_s,  secondary, verbose=False):
        """
        Function for finding the minimum mean temperature difference in a heat
        exchanger, while not going below the minimum approach temperature.

        The output enthalpy of the secondary fluid is set (as default) and the
        heat exchanger is evaluated.


        Parameters
        ----------
        h_out_s : float
            the output enthalpy, default would be of the secondary fluid.
        secondary : boolean
            shall the secondary fluid output be varied (or the working fluid)?

        Returns
        -------
        mean T-difference, float
            root tries to reach a value of 0.

        """
        
        if isinstance(h_out_s, float): # numpy expects a float (later)
            value = h_out_s
        else:
           value = h_out_s[0]
           
        if secondary:
            self.h_out_s = value
        else:
            self.h_out_w = value
            
        mdot_s, d_temps, wf_states, sf_states = self.pinch_calc()

        if self.warning < 100:
            return abs(self.dt_mean)
        else:
            return 500.0

    def find_pinch(self, secondary=True):
        """
        Function tries to vary the secondary fluid enthalpy  until a
        minimum approach temperature is reached. This also changes the
        mass flow rate.  This is then also the new
        exit state
        within the heat exchanger. If this is also not succesful,
        self.warning is set to 1. This should be checked.
        Parameters
        ----------
        secondary : Boolean, optional
            shall the output state of the secondary (True) or the working
            (False) fluid be varied? default = True

        Returns
        -------
        float
            the optimized enthalpy of the secondary fluid.

        """
        verbose = False
        if secondary:
            x0 = copy.copy(self.h_out_s)
        else:
            x0 = copy.copy(self.h_out_w)
        
        tolerance = 1e-3

        try:
            result = minimize(self.pinch_root, x0, args =(secondary,),
                              method='Nelder-Mead', tol=tolerance)

            if verbose:
                print(
                    f"result {result}, heating {self.heating}")

            if result.success or result.status == 2:
                if result.status == 2:
                    self.warning = 2  # T-difference probably smaller
                    self.warning_message = "Minimization problem: "+result.message
                if secondary:
                    self.h_out_s = result.x[0]
                else:
                    self.h_out_w = result.x[0]
                print(
                    f"Min T-distance {self.dt_min:.3f}, Mean T-distance {self.dt_mean:.3f}")

                return result.x[0]

        # except:
        except Exception as inst:
            print("find pinch:", type(inst))    # the exception type
            print(inst.args)     # arguments stored in .args
            print(inst)          # __str__ allows args to be printed directly,
            # but may be overridden in exception subclasses

            
            print("root-exception", self.heating)
            return 10000
        print("root-finding problem! (in heat_exchanger_thermo_v2.find_pinch)",
              result)
        print(f"Heating: {self.heating}")
        self.warning = 1
        return self.warning

    def pinch_plot(self, plot_fname="", plotting=True):
        """
        calculates the secondary fluid output state and mass flow, for the
        minimum approach temperature of the HeatExchanger instance. When wanted,
        this is also plotted

        Parameters
        ----------
        plot_fname : string, optional
            file-name to store the plot. The default is "".
        plotting : Boolean, optional
            should it be plotted? The default is True.

        Returns
        -------
        m_dot_s : float
            mass flow raete of the secondary fluid in SI units (kg/s).
        d_tempall : np.array
            the temperature differences between the two fluids along the heat
            exchanger.
        w_array : np.array [self.points, 7]
            the states of the working fluid along the heat exchanger.
        s_array : np.array [self.points, 7]
            the states of the secondary fluid along the heat exchanger.

        """
        print(f"------pinch-plot running -----plot:{plotting}")
        m_dot_s, d_tempall, w_array, s_array = self.pinch_calc()

        if plotting:
            h_w_plot_array = (
                w_array[:, 2] - w_array[:, 2].min()) * self.m_dot_w
            fig, ax_one = plt.subplots(1, 1)
            ax_one.plot((s_array[:, 2] - s_array[:, 2].min()) * self.m_dot_s,
                        s_array[:, 0], "v")
            ax_one.plot(h_w_plot_array, w_array[:, 0], "o")
            ax_one.set_xlabel(
                "specific enthalpy flow per mass of secondary fluid / (J / kg)")
            ax_one.set_ylabel("temperature / (K)")
            ax_one.set_title("heat exchanger, simple")
        if plot_fname != "":
            fig.savefig(plot_fname)

        return m_dot_s, d_tempall, w_array, s_array


if __name__ == "__main__":
    # two test cases condenser and evaporator:

    FLUID = "Propane * Pentane"  # working fluid
    FLS = "Methanol"  # "Water"  # secondary fluid
    comp = [.50, 0.5]
    flm = cb.fprop.FluidModel(FLUID)
    myFluid = cb.fprop.Fluid(flm, comp)

    secFlm = cb.fprop.FluidModel(FLS)
    secFluid = cb.fprop.Fluid(secFlm, [1.])
    D_TEMP_MIN = 6.0

    # Condenser, working fluid fixes all, secondary output enthalpy can be varied:
    SEC_TEMP_IN = 300.0
    SEC_TEMP_OUT_MAX = 370.0
    SEC_PRES_IN = 5e5
    H_DOT = 1e3
    state_sec_out = secFluid.set_state([SEC_TEMP_OUT_MAX, SEC_PRES_IN], "TP")

    state_sec_in = secFluid.set_state(
        [SEC_TEMP_IN, SEC_PRES_IN], "TP")  # this is the entering state

    # working fluid

    TEMP_SAT_VAP = SEC_TEMP_OUT_MAX + D_TEMP_MIN
    state_in = myFluid.set_state(
        [TEMP_SAT_VAP, 1.], "TQ")  # find minimum pressure

    WF_TEMP_IN = TEMP_SAT_VAP + D_TEMP_MIN
    WF_TEMP_OUT = SEC_TEMP_IN + D_TEMP_MIN
    state_out = myFluid.set_state([WF_TEMP_OUT, state_in[1]], "TP")

    state_in = myFluid.set_state([myFluid.properties.pressure,
                                  WF_TEMP_IN],
                                 "PT")

    hex0 = StaticHeatExchanger([myFluid, secFluid], H_DOT, state_out[2],
                               state_sec_out[2],
                               d_temp_separation_min=D_TEMP_MIN)
   
    factor0 = hex0.find_pinch()
    if hex0.warning > 0:
        print(hex0.warning_message)
        
    # now plotting can directly be done in pinch_calc 2024-05-24
    fig_act, ax_act = plt.subplots(1)
    PLOT_INFO ={"fig":fig_act, "ax":ax_act, "what":[2,0],"col":["r:","ko"],
    "label":["work,c","sec,c"], "x-shift":[0,0]}
    hex0.pinch_calc(plot_info=PLOT_INFO)
    ax_act.legend()
    #-----------------------------------------------
    #       Previous way
    # ms0, d_tempall0, w0, s0 = hex0.pinch_plot("hex-plot.png")
    

    #  Evaporator: ----------------------------

    SEC_TEMP_IN = 300.0
    SEC_TEMP_OUT = 285
    SEC_PRES_IN = 15e5
    H_DOT = 1e3
    extra = 2
    # D_TEMP_SUPER = 5.
    D_TEMP_MIN = 6.0
    state_sec_out = secFluid.set_state([SEC_TEMP_OUT, SEC_PRES_IN], "TP")
    # this mus be the last set_state before the hex is constructed:
    state_sec_in = secFluid.set_state([SEC_TEMP_IN, SEC_PRES_IN], "TP")

    # WF_TEMP_IN = SEC_TEMP_OUT  # - D_TEMP_MIN
    state_out = myFluid.set_state([SEC_TEMP_IN-D_TEMP_MIN - extra, 1.0], "TQ")
    state_in = myFluid.set_state(
        [SEC_TEMP_OUT-D_TEMP_MIN - extra, state_out[1]], "TP")

    # print("state in", state_in)

    hex1 = StaticHeatExchanger([myFluid, secFluid], H_DOT, state_out[2],
                               state_sec_out[2],
                               d_temp_separation_min=D_TEMP_MIN)
    # ms1, d_tempall1, w1, s1 = hex1.pinch_calc()
    

    factor_out = hex1.find_pinch()
    if hex1.warning > 2:
        print("Second heat exchanger:", hex1.warning_message)
    else:

        # plotting in the same figure
        PLOT_INFO ={"fig":fig_act, "ax":ax_act, "what":[2,0],"col":["k:","bo"],
        "label":["work,e","sec,e"], "x-shift":[0,0]}
        hex1.pinch_calc(plot_info=PLOT_INFO)
        ax_act.legend()
