diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index d459df9a..c8c2f67f 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -10,6 +10,7 @@ from .heat_exchangers.base import HeatExchanger # noqa: F401 from .heat_exchangers.condenser import Condenser # noqa: F401 from .heat_exchangers.desuperheater import Desuperheater # noqa: F401 +from .heat_exchangers.movingboundary import MovingBoundaryCondenser # noqa: F401 from .heat_exchangers.parabolic_trough import ParabolicTrough # noqa: F401 from .heat_exchangers.simple import HeatExchangerSimple # noqa: F401 from .heat_exchangers.simple import SimpleHeatExchanger # noqa: F401 diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py new file mode 100644 index 00000000..ae32ddcd --- /dev/null +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -0,0 +1,346 @@ +# -*- coding: utf-8 + +"""Module of class MovingBoundaryCondenser. + + +This file is part of project TESPy (github.com/oemof/tespy). It's copyrighted +by the contributors recorded in the version control history of the file, +available from its original location +tespy/components/heat_exchangers/movingboundary.py + +SPDX-License-Identifier: MIT +""" +import math + +from tespy.components.heat_exchangers.base import HeatExchanger +from tespy.tools.data_containers import ComponentProperties as dc_cp +from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp +from tespy.tools.fluid_properties import T_mix_ph +from tespy.tools.global_vars import ERR +from tespy.tools.fluid_properties import h_mix_pQ + + +class MovingBoundaryCondenser(HeatExchanger): + + @staticmethod + def component(): + return 'moving boundary condenser' + + def get_parameters(self): + params = super().get_parameters() + params.update({ + 'U_desup': dc_cp(min_val=0), + 'U_cond': dc_cp(min_val=0), + 'U_subcool': dc_cp(min_val=0), + 'A': dc_cp(min_val=0), + 'U_sections_group': dc_gcp( + elements=['U_desup', 'U_cond', 'U_subcool', 'A'], + func=self.U_sections_func, deriv=self.U_sections_deriv, latex=None, + num_eq=1 + ), + 'UA': dc_cp( + min_val=0, num_eq=1, func=self.UA_func, deriv=self.UA_deriv + ), + 'td_pinch': dc_cp( + min_val=0, num_eq=1, func=self.td_pinch_func, + deriv=self.td_pinch_deriv, latex=None + ) + }) + return params + + def get_U_sections_and_h_steps(self, get_U_values=False): + """Get the U values of the sections and the boundary hot side enthalpies + + Parameters + ---------- + get_U_values : boolean + Also return the U values for the sections of the heat exchanger. + + Returns + ------- + tuple + U values in the heat exchange sections and boundary hot side + enthalpies + """ + i1, _ = self.inl + o1, _ = self.outl + U_in_sections = [] + + h_sat_gas = h_mix_pQ(o1.p.val_SI, 1, o1.fluid_data) + h_sat_liquid = h_mix_pQ(o1.p.val_SI, 0, o1.fluid_data) + + if i1.h.val_SI > h_sat_gas + ERR and o1.h.val_SI < h_sat_liquid - ERR: + h_at_steps_1 = [i1.h.val_SI, h_sat_gas, h_sat_liquid, o1.h.val_SI] + if get_U_values: + U_in_sections = [self.U_desup.val, self.U_cond.val, self.U_subcool.val] + + elif ((i1.h.val_SI > h_sat_gas + ERR) ^ (o1.h.val_SI < h_sat_liquid - ERR)): + if i1.h.val_SI > h_sat_gas + ERR: + h_at_steps_1 = [i1.h.val_SI, h_sat_gas, o1.h.val_SI] + if get_U_values: + U_in_sections = [self.U_desup.val, self.U_cond.val] + else: + h_at_steps_1 = [i1.h.val_SI, h_sat_liquid, o1.h.val_SI] + if get_U_values: + U_in_sections = [self.U_cond.val, self.U_subcool.val] + + else: + h_at_steps_1 = [i1.h.val_SI, o1.h.val_SI] + + if get_U_values: + if i1.h.val_SI > h_sat_gas + ERR: + U_in_sections = [self.U_desup.val] + elif i1.h.val_SI > h_sat_liquid - ERR: + U_in_sections = [self.U_cond.val] + else: + U_in_sections = [self.U_subcool.val] + + return U_in_sections, h_at_steps_1 + + def calc_td_log_and_Q_in_sections(self, h_at_steps_1): + """Calculate logarithmic temperature difference and heat exchange in + heat exchanger sections. + + Parameters + ---------- + h_at_steps_1 : list + Enthalpy values at boundaries of sections. + + Returns + ------- + tuple + Lists of logarithmic temperature difference and heat exchange in + the heat exchanger sections starting from hot side inlet. + """ + i1, i2 = self.inl + steps = len(h_at_steps_1) + sections = steps - 1 + + p_at_steps_1 = [i1.p.val_SI for _ in range(steps)] + T_at_steps_1 = [ + T_mix_ph(p, h, i1.fluid_data, i1.mixing_rule) + for p, h in zip(p_at_steps_1, h_at_steps_1) + ] + + Q_in_sections = [ + i1.m.val_SI * (h_at_steps_1[i + 1] - h_at_steps_1[i]) + for i in range(sections) + ] + + h_at_steps_2 = [i2.h.val_SI] + for Q in Q_in_sections[::-1]: + h_at_steps_2.append(h_at_steps_2[-1] + abs(Q) / i2.m.val_SI) + + p_at_steps_2 = [i2.p.val_SI for _ in range(sections + 1)] + T_at_steps_2 = [ + T_mix_ph(p, h, i2.fluid_data, i2.mixing_rule) + for p, h in zip(p_at_steps_2, h_at_steps_2) + ] + + # counter flow version + td_at_steps = [ + T1 - T2 for T1, T2 in zip(T_at_steps_1, T_at_steps_2[::-1]) + ] + + td_at_steps = [abs(td) for td in td_at_steps] + td_log_in_sections = [ + (td_at_steps[i + 1] - td_at_steps[i]) + / math.log(td_at_steps[i + 1] / td_at_steps[i]) + for i in range(sections) + ] + return td_log_in_sections, Q_in_sections + + def calc_UA_in_sections(self): + """Calc UA values for all sections. + + Returns + ------- + list + List of UA values starting from hot side inlet. + """ + _, h_at_steps_1 = self.get_U_sections_and_h_steps() + td_log_in_sections, Q_in_sections = self.calc_td_log_and_Q_in_sections(h_at_steps_1) + UA_in_sections = [ + abs(Q) / td_log + for Q, td_log in zip(Q_in_sections, td_log_in_sections) + ] + + return UA_in_sections + + def UA_func(self, **kwargs): + r""" + Calculate heat transfer from heat transfer coefficients for + desuperheating and condensation as well as total heat exchange area. + + Returns + ------- + residual : float + Residual value of equation. + """ + UA_in_sections = self.calc_UA_in_sections() + return self.UA.val - sum(UA_in_sections) + + def UA_deriv(self, increment_filter, k): + r""" + Partial derivatives of heat transfer coefficient function. + + Parameters + ---------- + increment_filter : ndarray + Matrix for filtering non-changing variables. + + k : int + Position of derivatives in Jacobian matrix (k-th equation). + """ + f = self.UA_func + for c in self.inl + self.outl: + if self.is_variable(c.m): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, "m", c) + if self.is_variable(c.p): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + if self.is_variable(c.h): + self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) + + def U_sections_func(self, **kwargs): + r""" + Calculate heat transfer from heat transfer coefficients for + desuperheating and condensation as well as total heat exchange area. + + Returns + ------- + residual : float + Residual value of equation. + """ + U_in_sections, h_at_steps_1 = self.get_U_sections_and_h_steps(get_U_values=True) + td_log_in_sections, Q_in_sections = self.calc_td_log_and_Q_in_sections(h_at_steps_1) + + Q_total = sum(Q_in_sections) + + return ( + Q_total + + self.A.val / Q_total + * sum([ + Q * td_log * U + for Q, td_log, U + in zip(Q_in_sections, td_log_in_sections, U_in_sections) + ]) + ) + + def U_sections_deriv(self, increment_filter, k): + r""" + Partial derivatives of heat transfer coefficient function. + + Parameters + ---------- + increment_filter : ndarray + Matrix for filtering non-changing variables. + + k : int + Position of derivatives in Jacobian matrix (k-th equation). + """ + f = self.U_sections_func + for c in self.inl + self.outl: + if self.is_variable(c.m): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, "m", c) + if self.is_variable(c.p): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + if self.is_variable(c.h): + self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) + + def calc_td_pinch(self): + """Calculate the pinch point temperature difference + + Returns + ------- + float + Value of the pinch point temperature difference + """ + o1 = self.outl[0] + i2 = self.inl[1] + + h_sat = h_mix_pQ(o1.p.val_SI, 1, o1.fluid_data) + + if o1.h.val_SI < h_sat: + # we have two sections in this case + Q_cond = o1.m.val_SI * (o1.h.val_SI - h_sat) + + # calculate the intermediate temperatures + T_cond_i1 = o1.calc_T_sat() + h_cond_o2 = i2.h.val_SI + abs(Q_cond) / i2.m.val_SI + T_cond_o2 = T_mix_ph(i2.p.val_SI, h_cond_o2, i2.fluid_data) + + return T_cond_i1 - T_cond_o2 + + else: + o2 = self.outl[1] + return o1.calc_T_sat() - o2.calc_T() + + def td_pinch_func(self): + r""" + Equation for pinch point temperature difference of a condenser. + + Returns + ------- + residual : float + Residual value of equation. + + .. math:: + + 0 = td_\text{pinch} - T_\text{sat,in,1} + + T_\left( + p_\text{in,2},\left[ + h_\text{in,2} + + \frac{\dot Q_\text{cond}}{\dot m_\text{in,2}} + \right] + \right) + """ + return self.td_pinch.val - self.calc_td_pinch() + + def td_pinch_deriv(self, increment_filter, k): + """ + Calculate partial derivates of upper terminal temperature function. + + Parameters + ---------- + increment_filter : ndarray + Matrix for filtering non-changing variables. + + k : int + Position of derivatives in Jacobian matrix (k-th equation). + """ + f = self.td_pinch_func + for c in [self.outl[0], self.inl[1]]: + if self.is_variable(c.m, increment_filter): + self.jacobian[k, c.m.J_col] = self.numeric_deriv(f, 'm', c) + if self.is_variable(c.p, increment_filter): + self.jacobian[k, c.p.J_col] = self.numeric_deriv(f, 'p', c) + if self.is_variable(c.h, increment_filter): + self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) + + def calc_parameters(self): + super().calc_parameters() + + U_sections_specified = all([ + self.get_attr(f"U_{key}").is_set + for key in ["desup", "cond", "subcool"] + ]) + + if U_sections_specified: + U_in_sections, h_at_steps_1 = self.get_U_sections_and_h_steps(get_U_values=True) + td_log_in_sections, Q_in_sections = self.calc_td_log_and_Q_in_sections(h_at_steps_1) + self.A.val = self.Q.val ** 2 / ( + sum([ + abs(Q) * td_log * U + for Q, td_log, U + in zip(Q_in_sections, td_log_in_sections, U_in_sections) + ]) + ) + assert abs(abs(self.Q.val) / sum([ + ((Q / self.Q.val) * td_log * U) + for Q, td_log, U + in zip(Q_in_sections, td_log_in_sections, U_in_sections) + ]) - self.A.val) < 1e-6 + assert round(sum([Q for Q in Q_in_sections]), 3) == round(self.Q.val, 3) + + self.UA.val = sum(self.calc_UA_in_sections()) + self.td_pinch.val = self.calc_td_pinch() diff --git a/tests/test_components/test_partload_model_movingboundarycondenser.py b/tests/test_components/test_partload_model_movingboundarycondenser.py new file mode 100644 index 00000000..a200701d --- /dev/null +++ b/tests/test_components/test_partload_model_movingboundarycondenser.py @@ -0,0 +1,160 @@ +from tespy.components import HeatExchanger, Source, Sink, Compressor, MovingBoundaryCondenser +from tespy.connections import Connection +from tespy.networks import Network +import numpy as np + + +nw = Network(T_unit="C", p_unit="bar") + + +so1 = Source("cw source") +so2 = Source("wf source") + +# multiple-boundary heat exchanger +# allow for U value change as function of volumetric flow/mass flow/... +cd = MovingBoundaryCondenser("Condenser") +cp = Compressor("compressor") + +si1 = Sink("cw sink") +si2 = Sink("wf sink") + + +c1 = Connection(so1, "out1", cd, "in2", label="1") +c2 = Connection(cd, "out2", si1, "in1", label="2") + +c10 = Connection(so2, "out1", cp, "in1", label="10") +c11 = Connection(cp, "out1", cd, "in1", label="11") +c12 = Connection(cd, "out1", si2, "in1", label="12") + +nw.add_conns(c1, c2, c10, c11, c12) + +cd.set_attr(pr1=1, pr2=1) +cp.set_attr(eta_s=0.8) + +c10.set_attr(T=20, x=1) +c11.set_attr(fluid={"NH3": 1}) +c12.set_attr(x=0, T=80) + +c1.set_attr(fluid={"INCOMP::Water": 1}, m=10, T=70, p=1, h0=1e5) +c2.set_attr(h0=1e5, T=80) + +cd.set_attr(U_desup=4000, U_cond=16000, U_subcool=5000) + +nw.solve("design") +nw.save("design") +nw.print_results() + + +# test pinch specification +c12.set_attr(T=None) +cd.set_attr(td_pinch=3) +nw.solve("design") +nw.print_results() +# exit() +# print(de.A_desup, de.A_cond) + +# c12.set_attr(T=None) +cd.set_attr(A=cd.A.val, td_pinch=None) + +# Alternative: fix the input temperatures and mass flows +# outlet conditions (condensing temperature and water outlet are unknows) +# c2.set_attr(T=None) +# c10.set_attr(m=c10.m.val) + +# + +# get rid of warnings +cd.zeta1.set_attr(min_val=-2) + +nw.solve("design") +nw.print_results() +# exit() +# print(c2.T.val) + + +Q = [] +T_cond = [] +m_refrig = [] +dT_pinch = [] +Q_in_sections = [] +for m in np.linspace(12, 5, 20): + c1.set_attr(m=m) + nw.solve("design") + m_refrig += [c12.m.val] + T_cond += [c12.T.val] + Q += [abs(cd.Q.val)] + dT_pinch += [cd.td_pinch.val] + _, h_at_steps = cd.get_U_sections_and_h_steps() + _, Q_in_section = cd.calc_td_log_and_Q_in_sections(h_at_steps) + Q_in_sections += [Q_in_section] + + +from matplotlib import pyplot as plt + + +fig, ax = plt.subplots(4, sharex=True, figsize=(12, 8)) + +ax[0].scatter(Q, m_refrig) +ax[0].set_ylabel("refrigerant mass flow") +ax[1].scatter(Q, T_cond) +ax[1].set_ylabel("condensation temperature") +ax[2].scatter(Q, dT_pinch) +ax[2].set_ylabel("pinch temperature difference") +ax[3].scatter(Q, [abs(q[0] / Q) for q, Q in zip(Q_in_sections, Q)], label="desup") +ax[3].scatter(Q, [abs(q[1] / Q) for q, Q in zip(Q_in_sections, Q)], label="cond") +ax[3].legend() +ax[3].set_ylabel("heat transfer shares of total heat transfer") + +ax[3].set_xlabel("total heat transfer") + +[_.grid() for _ in ax] + +plt.tight_layout() +fig.savefig("mb_U_sections_diff_m.png") + +plt.close() + +c1.set_attr(m=10) +nw.solve("design") +nw.print_results() + +cd.set_attr(A=None, UA=cd.UA.val) + +Q = [] +T_cond = [] +m_refrig = [] +dT_pinch = [] +Q_in_sections = [] +for m in np.linspace(12, 5, 20): + c1.set_attr(m=m) + nw.solve("design") + m_refrig += [c12.m.val] + T_cond += [c12.T.val] + Q += [abs(cd.Q.val)] + # Q_cond += [cd.Q_cond] + dT_pinch += [cd.td_pinch.val] + _, h_at_steps = cd.get_U_sections_and_h_steps() + _, Q_in_section = cd.calc_td_log_and_Q_in_sections(h_at_steps) + Q_in_sections += [Q_in_section] + +print(Q_in_sections) + +fig, ax = plt.subplots(4, sharex=True, figsize=(12, 8)) + +ax[0].scatter(Q, m_refrig) +ax[0].set_ylabel("refrigerant mass flow") +ax[1].scatter(Q, T_cond) +ax[1].set_ylabel("condensation temperature") +ax[2].scatter(Q, dT_pinch) +ax[2].set_ylabel("pinch temperature difference") +ax[3].scatter(Q, [abs(q[0] / Q) for q, Q in zip(Q_in_sections, Q)], label="desup") +ax[3].scatter(Q, [abs(q[1] / Q) for q, Q in zip(Q_in_sections, Q)], label="cond") +ax[3].legend() +ax[3].set_ylabel("heat transfer shares of total heat transfer") + +ax[3].set_xlabel("total heat transfer") + +[_.grid() for _ in ax] + +plt.tight_layout() +fig.savefig("mb_UA_diff_m.png") \ No newline at end of file