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