Source code for carbatpy.models.components.heat_exchanger_thermo_v2

# -*- 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


[docs]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). """
[docs] def __init__(self, fluids, h_dot_min, h_out_w, h_limit_s, **kwargs): #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, if there is a limit in enthalpy for the secondary fluid, it can be given here. It is also a starting value for possible iterations. kwargs : dict, optional Optional parameters for the heat exchanger configuration: - points : int, for how many points (array) shall the minimum approach temperature be checked and properties be returned (for plotting etc.). default 50 - d_temp_separation_min : float, Minimium approach temperature ( pinch point) between the two fluids. default 0.5 - calc_type : str, which calculation type shall be performed; only one implemented so far, default "const" - name : str, name of the heat exchanger .default "evaporator" - plot_info : dict, if not None, 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.default {} Returns ------- None. """ self.fluids = fluids state_in_w = fluids[0].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 = kwargs.get('points', 50) self.d_temp_separation_min = kwargs.get('d_temp_separation_min', 0.5) self.calc_type = kwargs.get('calc_type', "const") self.name = kwargs.get('name', "evaporator") self.plot_info = kwargs.get('plot_info', None) # 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( (self.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."
@property
[docs] def plot_info(self): return self._plot_info
@plot_info.setter def plot_info(self, value): if value is None: value = {} elif not isinstance(value, dict): raise ValueError("plot_info must be a dictionary") self._plot_info = value
[docs] def pinch_calc(self, verbose=False): """ 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. 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.enthalpy self. h_in_out[0, 0] = w_in.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.pressure], "HP") self. h_in_out[1, 1] = state_out_s[2] h_delta_s = np.abs( state_out_s[2] - s_in.enthalpy) # fixed heat flow, determines mass flow rate self.m_dot_s = self.q_dot / h_delta_s s_array = self._calculate_state_array(s_out, self.h_in_out[1, :2]) w_array = self._calculate_state_array(w_out, self.h_in_out[0, 1::-1]) # temperature difference, ok? d_tempall = w_array[:, 0]-s_array[:, 0] self.dt_mean, self.dt_min, self.dt_max = d_tempall.mean(), np.abs( d_tempall).min(), np.abs(d_tempall).max() self._check_temperature_consistency(d_tempall) if self.plot_info: self._plot_heat_exchanger(w_array, s_array) if verbose: self._print_verbose(d_tempall) return self.m_dot_s, d_tempall*self.heating, w_array, s_array
[docs] 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
[docs] 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 x0 = copy.copy(self.h_out_s if secondary else self.h_out_w) # 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 e: self._handle_exception(e) 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
[docs] 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
[docs] def _calculate_state_array(self, fluid, h_range): h_array = np.linspace(h_range[0], h_range[1], self.points) values = np.zeros((self.points, 2)) values[:, 0] = h_array values[:, 1] = fluid.pressure return fluid.set_state_v(values, "HP")
[docs] def _check_temperature_consistency(self, d_tempall): eps_min = -1e-3 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) abs_dt_min = np.abs(self.dt_min) difference = abs_dt_min - self.d_temp_separation_min # print(f"Debug: abs_dt_min = {abs_dt_min}, d_temp_separation_min = {self.d_temp_separation_min}, difference = {difference}") if difference < eps_min: self.warning = 907 self.warning_message = "Below minimum approach temperature!" # print(f"907: {difference}, {abs_dt_min}, {self.d_temp_separation_min}") elif crossing or wrong_side: self.warning = 999 self.dt_mean = 1e6 self.warning_message = "Temperatures crossing or wrong side!" else: self.warning = 0 self.warning_message = "All o.k."
[docs] def _plot_heat_exchanger(self, w_array, s_array): plot_info = self.plot_info # print("Plot info:", plot_info) # Debugging-Ausgabe if plot_info["what"][0] == 2: data_w = (w_array[:, plot_info["what"][0]] - w_array[:, plot_info["what"][0]].min()) * self.m_dot_w + plot_info["x-shift"][0] data_s = (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_w, w_array[:, plot_info["what"][1]], plot_info["col"][0], label=plot_info["label"][0]) plot_info["ax"].plot(data_s, s_array[:, plot_info["what"][1]], plot_info["col"][1], label=plot_info["label"][1]) else: print(f"H-Ex: plotting only implemented for T-H_dot [2,0]. You requested {plot_info['what']}")
[docs] def _print_verbose(self, d_tempall): 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())
[docs] def _handle_exception(self, e): print("find pinch:", type(e)) print(e.args) print(e) print("root-exception", self.heating)
if __name__ == "__main__": # two test cases condenser and evaporator:
[docs] 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 = 5.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.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.plot_info=PLOT_INFO hex0.pinch_calc(verbose=True) 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, hex1.dt_min) 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.plot_info=PLOT_INFO hex1.pinch_calc(verbose=True) ax_act.legend()