From ecb2fc76498e56e6d82c86ed97d82a3c5de0e545 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 May 2024 22:03:32 +0200 Subject: [PATCH 01/55] Draft for a moving boundary condensation component --- src/tespy/components/__init__.py | 1 + .../heat_exchangers/movingboundary.py | 227 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 src/tespy/components/heat_exchangers/movingboundary.py diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index d459df9af..c8c2f67fb 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 000000000..1af175281 --- /dev/null +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -0,0 +1,227 @@ +# -*- 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 numpy as np + +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.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), + 'A': dc_cp(min_val=0), + 'UA_group': dc_gcp( + elements=['U_desup', 'U_cond', 'A'], + func=self.UA_func, deriv=self.UA_deriv, latex=None, + num_eq=1 + ), + '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 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. + """ + Q_total = self.inl[0].m.val_SI * (self.outl[0].h.val_SI - self.inl[0].h.val_SI) + h_sat = h_mix_pQ(self.outl[0].p.val_SI, 1, self.outl[0].fluid_data) + + if self.outl[0].h.val_SI < h_sat: + # we have two sections in this case + Q_desup = self.inl[0].m.val_SI * (h_sat - self.inl[0].h.val_SI) + Q_cond = self.inl[0].m.val_SI * (self.outl[0].h.val_SI - h_sat) + + # calculate the intermediate temperatures + T_desup_i1 = self.inl[0].calc_T() + T_desup_o1 = self.outl[0].calc_T_sat() + T_cond_i1 = T_desup_o1 + # not considering any pressure loss yet + T_cond_o1 = T_desup_o1 + + T_cond_i2 = self.inl[1].calc_T() + h_cond_o2 = self.inl[1].h.val_SI + abs(Q_cond) / self.inl[1].m.val_SI + T_cond_o2 = T_mix_ph(self.inl[1].p.val_SI, h_cond_o2, self.inl[1].fluid_data) + T_desup_i2 = T_cond_o2 + T_desup_o2 = self.outl[1].calc_T() + + ttd_desup_u = T_desup_i1 - T_desup_o2 + ttd_desup_l = T_desup_o1 - T_desup_i2 + + ttd_cond_u = T_cond_i1 - T_cond_o2 + ttd_cond_l = T_cond_o1 - T_cond_i2 + + if ttd_desup_u < 0: + ttd_desup_u = abs(ttd_desup_u) + if ttd_desup_l < 0: + ttd_desup_l = abs(ttd_desup_l) + + if ttd_cond_u < 0: + ttd_cond_u = abs(ttd_cond_u) + if ttd_cond_l < 0: + ttd_cond_l = abs(ttd_cond_l) + + td_log_desup = (ttd_desup_l - ttd_desup_u) / np.log(ttd_desup_l / ttd_desup_u) + td_log_cond = (ttd_cond_l - ttd_cond_u) / np.log(ttd_cond_l / ttd_cond_u) + + residual = ( + Q_total + + self.A.val * (Q_desup / Q_total) * self.U_desup.val * td_log_desup + + self.A.val * (Q_cond / Q_total) * self.U_cond.val * td_log_cond + ) + + else: + # only condensation is happening + residual = Q_total + self.A.val * self.U_cond.val * self.calculate_td_log() + + return residual + + 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, d=1e-5) + + 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) + """ + 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 self.td_pinch.val - T_cond_i1 + T_cond_o2 + + else: + o2 = self.outl[1] + return self.td_pinch.val - o1.calc_T_sat() + o2.calc_T() + + 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() + + # this should be exported to a function, which can be called from here + # and from the UA_func. + h_sat = h_mix_pQ(self.outl[0].p.val_SI, 1, self.outl[0].fluid_data) + self.Q_desup = self.inl[0].m.val_SI * (h_sat - self.inl[0].h.val_SI) + Q_desup = self.Q_desup + self.Q_cond = self.inl[0].m.val_SI * (self.outl[0].h.val_SI - h_sat) + Q_cond = self.Q_cond + T_desup_i1 = self.inl[0].calc_T() + self.T_desup_o1 = self.outl[0].calc_T_sat() + T_desup_o1 = self.T_desup_o1 + T_cond_i1 = T_desup_o1 + T_cond_o1 = T_desup_o1 + + T_cond_i2 = self.inl[1].calc_T() + h_cond_o2 = self.inl[1].h.val_SI + abs(Q_cond) / self.inl[1].m.val_SI + T_cond_o2 = T_mix_ph(self.inl[1].p.val_SI, h_cond_o2, self.inl[1].fluid_data) + T_desup_i2 = T_cond_o2 + self.T_desup_i2 = T_desup_i2 + T_desup_o2 = self.outl[1].calc_T() + + ttd_desup_u = T_desup_i1 - T_desup_o2 + ttd_desup_l = T_desup_o1 - T_desup_i2 + + ttd_cond_u = T_cond_i1 - T_cond_o2 + ttd_cond_l = T_cond_o1 - T_cond_i2 + + td_log_desup = (ttd_desup_l - ttd_desup_u) / np.log(ttd_desup_l / ttd_desup_u) + td_log_cond = (ttd_cond_l - ttd_cond_u) / np.log(ttd_cond_l / ttd_cond_u) + + self.td_pinch.val = T_desup_o1 - T_desup_i2 + + self.A.val = abs(self.Q.val) / ((Q_desup / self.Q.val) * self.U_desup.val * td_log_desup + (Q_cond / self.Q.val) * self.U_cond.val * td_log_cond) + + # some intermediate tests for consistency + assert abs(abs(self.Q.val) / ((Q_desup / self.Q.val) * self.U_desup.val * td_log_desup + (Q_cond / self.Q.val) * self.U_cond.val * td_log_cond) - self.A.val) < 1e-6 + assert round(Q_cond + Q_desup, 3) == round(self.Q.val, 3) From c90c959b2ed6bcf8e7ae9893af2b9ccc6359a322 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 May 2024 22:04:11 +0200 Subject: [PATCH 02/55] Add a testing draft for the moving boundary component --- .../test_partload_model_movingboundary.py | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 tests/test_components/test_partload_model_movingboundary.py diff --git a/tests/test_components/test_partload_model_movingboundary.py b/tests/test_components/test_partload_model_movingboundary.py new file mode 100644 index 000000000..3f3c853d5 --- /dev/null +++ b/tests/test_components/test_partload_model_movingboundary.py @@ -0,0 +1,134 @@ +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) + +nw.solve("design") +nw.save("design") +nw.print_results() + +# print(de.A_desup, de.A_cond) + +c12.set_attr(T=None) +cd.set_attr(A=cd.A.val) + +# 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() + +# print(c2.T.val) + +Q = [] +T_cond = [] +m_refrig = [] +dT_pinch = [] +Q_cond = [] + +for m in np.linspace(12, 4.55, 40): + print(m) + 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.T_desup_o1 - cd.T_desup_i2] + + +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(qc / q) for qc, q in zip(Q_cond, Q)], label="cond") +ax[3].scatter(Q, [abs((q + qc) / q) for qc, q in zip(Q_cond, Q)], label="desup") +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_partload_m_changing.png") + + +# fig, ax = plt.subplots(1) + +# for i, m in enumerate(np.linspace(12, 5, 25)): +# c1.set_attr(m=m) +# nw.solve("design") + +# if i % 5 ==0: + +# if de.T_desup_i2 > de.T_desup_o1: +# print("PINCH VIOLATION") + +# print(de.Q_cond / de.Q.val) +# de.Q.val = abs(de.Q.val) +# de.Q_cond = abs(de.Q_cond) + +# _ = ax.plot([0, de.Q_cond, de.Q.val], [c12.T.val, de.T_desup_o1 - 273.15, c11.T.val], label=f"Q={round(de.Q.val)}") +# ax.plot([0, de.Q_cond, de.Q.val], [c1.T.val, de.T_desup_i2 - 273.15, c2.T.val], c=_[0].get_color()) + + +# ax.set_ylabel("temperature") +# ax.set_xlabel("heat transfer") +# ax.grid() +# ax.legend() + +# plt.tight_layout() + +# fig.savefig("mb_QT.png") From d7cdac45617e7a218a12f41bb7d425957544d120 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 May 2024 22:18:16 +0200 Subject: [PATCH 03/55] Add a test for the pinch point temperature difference function --- .../test_partload_model_movingboundary.py | 57 ++++++++++--------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/tests/test_components/test_partload_model_movingboundary.py b/tests/test_components/test_partload_model_movingboundary.py index 3f3c853d5..885eb7ffa 100644 --- a/tests/test_components/test_partload_model_movingboundary.py +++ b/tests/test_components/test_partload_model_movingboundary.py @@ -44,15 +44,22 @@ nw.save("design") nw.print_results() -# print(de.A_desup, de.A_cond) +# test pinch specification c12.set_attr(T=None) -cd.set_attr(A=cd.A.val) +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) +# c2.set_attr(T=None) +# c10.set_attr(m=c10.m.val) # @@ -60,8 +67,8 @@ cd.zeta1.set_attr(min_val=-2) nw.solve("design") -# nw.print_results() - +nw.print_results() +# exit() # print(c2.T.val) Q = [] @@ -71,14 +78,13 @@ Q_cond = [] for m in np.linspace(12, 4.55, 40): - print(m) 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.T_desup_o1 - cd.T_desup_i2] + dT_pinch += [cd.td_pinch.val] from matplotlib import pyplot as plt @@ -105,30 +111,29 @@ fig.savefig("mb_partload_m_changing.png") -# fig, ax = plt.subplots(1) +fig, ax = plt.subplots(1) -# for i, m in enumerate(np.linspace(12, 5, 25)): -# c1.set_attr(m=m) -# nw.solve("design") +for i, m in enumerate(np.linspace(12, 5, 25)): + c1.set_attr(m=m) + nw.solve("design") -# if i % 5 ==0: + if i % 5 ==0: -# if de.T_desup_i2 > de.T_desup_o1: -# print("PINCH VIOLATION") + if cd.T_desup_i2 > cd.T_desup_o1: + print("PINCH VIOLATION") -# print(de.Q_cond / de.Q.val) -# de.Q.val = abs(de.Q.val) -# de.Q_cond = abs(de.Q_cond) + cd.Q.val = abs(cd.Q.val) + cd.Q_cond = abs(cd.Q_cond) -# _ = ax.plot([0, de.Q_cond, de.Q.val], [c12.T.val, de.T_desup_o1 - 273.15, c11.T.val], label=f"Q={round(de.Q.val)}") -# ax.plot([0, de.Q_cond, de.Q.val], [c1.T.val, de.T_desup_i2 - 273.15, c2.T.val], c=_[0].get_color()) + _ = ax.plot([0, cd.Q_cond, cd.Q.val], [c12.T.val, cd.T_desup_o1 - 273.15, c11.T.val], label=f"Q={round(cd.Q.val)}") + ax.plot([0, cd.Q_cond, cd.Q.val], [c1.T.val, cd.T_desup_i2 - 273.15, c2.T.val], c=_[0].get_color()) -# ax.set_ylabel("temperature") -# ax.set_xlabel("heat transfer") -# ax.grid() -# ax.legend() +ax.set_ylabel("temperature") +ax.set_xlabel("heat transfer") +ax.grid() +ax.legend() -# plt.tight_layout() +plt.tight_layout() -# fig.savefig("mb_QT.png") +fig.savefig("mb_QT.png") From 46db812361587d404298b145a24a8ef99a649bec Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Thu, 30 May 2024 05:30:48 +0200 Subject: [PATCH 04/55] Add a second draft component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `MovingBoundaryHeatExchanger` is the more generic version of the first implementation. It allows for any number of sections and eventually will also be implemented for evaporation. The main difference to the first draft of the `MovingBoundaryCondenser` is, that it is nondimensional to U or A. The sum of all UA values in the sections has to be equal to the UA value originally calculated in the design phase. An adoption can be made to this later, modifying the total UA value by lookup tables similar to the ´kA_char´ approaches in the already existing heat exchanger components. --- src/tespy/components/__init__.py | 1 + .../heat_exchangers/movingboundary.py | 166 ++++++++++++++++++ 2 files changed, 167 insertions(+) diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index c8c2f67fb..2724bffc1 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -11,6 +11,7 @@ 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.movingboundary import MovingBoundaryHeatExchanger # 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 index 1af175281..6dc9f04b6 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -19,6 +19,172 @@ from tespy.tools.fluid_properties import h_mix_pQ +class MovingBoundaryHeatExchanger(HeatExchanger): + + @staticmethod + def component(): + return 'moving boundary heat exchanger' + + def get_parameters(self): + params = super().get_parameters() + params.update({ + '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 calc_UA_in_sections(self): + i1, i2 = self.inl + o1, o2 = self.outl + 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 and o1.h.val_SI < h_sat_liquid: + h_at_steps_1 = [i1.h.val_SI, h_sat_gas, h_sat_liquid, o1.h.val_SI] + sections = 3 + + elif ((i1.h.val_SI > h_sat_gas) ^ (o1.h.val_SI < h_sat_liquid)): + sections = 2 + if i1.h.val_SI > h_sat_gas: + h_at_steps_1 = [i1.h.val_SI, h_sat_gas, o1.h.val_SI] + else: + h_at_steps_1 = [i1.h.val_SI, h_sat_liquid, o1.h.val_SI] + + else: + sections = 1 + h_at_steps_1 = [i1.h.val_SI, o1.h.val_SI] + + p_at_steps_1 = [i1.p.val_SI for _ in range(sections + 1)] + T_at_steps_1 = [T_mix_ph(p, h, i1.fluid_data) 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: + 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])] + # parallel flow version + # td_at_steps = [T1 - T2 for T1, T2 in zip(T_at_steps_1, T_at_steps_2)] + + td_log_in_sections = [ + (td_at_steps[i + 1] - td_at_steps[i]) + / np.log(td_at_steps[i + 1] / td_at_steps[i]) + for i in range(sections) + ] + print(Q_in_sections) + print(td_log_in_sections) + UA_in_sections = [abs(Q) / td_log for Q, td_log in zip(Q_in_sections, td_log_in_sections)] + print(UA_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. + """ + return self.UA.val - sum(self.calc_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 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) + """ + 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 self.td_pinch.val - T_cond_i1 + T_cond_o2 + + else: + o2 = self.outl[1] + return self.td_pinch.val - o1.calc_T_sat() + o2.calc_T() + + 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() + self.UA.val = sum(self.calc_UA_in_sections()) + + class MovingBoundaryCondenser(HeatExchanger): @staticmethod From c57d184347c04230d3284110277aa9dd459a288e Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 17 Jul 2024 16:56:21 +0200 Subject: [PATCH 05/55] Fix a bug that applied the wrong order of heat exchange rates for the different sections for cold side temperature calculation --- .../heat_exchangers/movingboundary.py | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index 6dc9f04b6..59717af9c 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -10,12 +10,13 @@ SPDX-License-Identifier: MIT """ -import numpy as np +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 @@ -44,13 +45,13 @@ def calc_UA_in_sections(self): 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 and o1.h.val_SI < h_sat_liquid: + 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] sections = 3 - elif ((i1.h.val_SI > h_sat_gas) ^ (o1.h.val_SI < h_sat_liquid)): + elif ((i1.h.val_SI > h_sat_gas + ERR) ^ (o1.h.val_SI < h_sat_liquid - ERR)): sections = 2 - if i1.h.val_SI > h_sat_gas: + if i1.h.val_SI > h_sat_gas + ERR: h_at_steps_1 = [i1.h.val_SI, h_sat_gas, o1.h.val_SI] else: h_at_steps_1 = [i1.h.val_SI, h_sat_liquid, o1.h.val_SI] @@ -65,7 +66,7 @@ def calc_UA_in_sections(self): 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: + 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)] @@ -78,13 +79,11 @@ def calc_UA_in_sections(self): td_log_in_sections = [ (td_at_steps[i + 1] - td_at_steps[i]) - / np.log(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) ] - print(Q_in_sections) - print(td_log_in_sections) UA_in_sections = [abs(Q) / td_log for Q, td_log in zip(Q_in_sections, td_log_in_sections)] - print(UA_in_sections) + return UA_in_sections def UA_func(self, **kwargs): @@ -120,6 +119,27 @@ def UA_deriv(self, increment_filter, k): if self.is_variable(c.h): self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) + def calc_td_pinch(self): + 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. @@ -139,25 +159,7 @@ def td_pinch_func(self): \right] \right) """ - 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 self.td_pinch.val - T_cond_i1 + T_cond_o2 - - else: - o2 = self.outl[1] - return self.td_pinch.val - o1.calc_T_sat() + o2.calc_T() + return self.td_pinch.val - self.calc_td_pinch() def td_pinch_deriv(self, increment_filter, k): """ @@ -183,7 +185,7 @@ def td_pinch_deriv(self, increment_filter, k): def calc_parameters(self): super().calc_parameters() self.UA.val = sum(self.calc_UA_in_sections()) - + self.td_pinch.val = self.calc_td_pinch() class MovingBoundaryCondenser(HeatExchanger): @@ -256,8 +258,8 @@ def UA_func(self, **kwargs): if ttd_cond_l < 0: ttd_cond_l = abs(ttd_cond_l) - td_log_desup = (ttd_desup_l - ttd_desup_u) / np.log(ttd_desup_l / ttd_desup_u) - td_log_cond = (ttd_cond_l - ttd_cond_u) / np.log(ttd_cond_l / ttd_cond_u) + td_log_desup = (ttd_desup_l - ttd_desup_u) / math.log(ttd_desup_l / ttd_desup_u) + td_log_cond = (ttd_cond_l - ttd_cond_u) / math.log(ttd_cond_l / ttd_cond_u) residual = ( Q_total @@ -381,8 +383,8 @@ def calc_parameters(self): ttd_cond_u = T_cond_i1 - T_cond_o2 ttd_cond_l = T_cond_o1 - T_cond_i2 - td_log_desup = (ttd_desup_l - ttd_desup_u) / np.log(ttd_desup_l / ttd_desup_u) - td_log_cond = (ttd_cond_l - ttd_cond_u) / np.log(ttd_cond_l / ttd_cond_u) + td_log_desup = (ttd_desup_l - ttd_desup_u) / math.log(ttd_desup_l / ttd_desup_u) + td_log_cond = (ttd_cond_l - ttd_cond_u) / math.log(ttd_cond_l / ttd_cond_u) self.td_pinch.val = T_desup_o1 - T_desup_i2 From f33b15a0efbce878b93f9e7d46b60873fb928fab Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 17 Jul 2024 16:57:55 +0200 Subject: [PATCH 06/55] Check in a test for the generalized moving boundary heat exchanger --- ...partload_model_movingboundarycondenser.py} | 0 ...st_partload_model_movingboundarygeneral.py | 164 ++++++++++++++++++ 2 files changed, 164 insertions(+) rename tests/test_components/{test_partload_model_movingboundary.py => test_partload_model_movingboundarycondenser.py} (100%) create mode 100644 tests/test_components/test_partload_model_movingboundarygeneral.py diff --git a/tests/test_components/test_partload_model_movingboundary.py b/tests/test_components/test_partload_model_movingboundarycondenser.py similarity index 100% rename from tests/test_components/test_partload_model_movingboundary.py rename to tests/test_components/test_partload_model_movingboundarycondenser.py diff --git a/tests/test_components/test_partload_model_movingboundarygeneral.py b/tests/test_components/test_partload_model_movingboundarygeneral.py new file mode 100644 index 000000000..4184bb8de --- /dev/null +++ b/tests/test_components/test_partload_model_movingboundarygeneral.py @@ -0,0 +1,164 @@ + +from tespy.components import HeatExchanger, Source, Sink, Compressor, MovingBoundaryHeatExchanger +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 = MovingBoundaryHeatExchanger("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() + +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) +# c1.set_attr(m=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() + +c12.set_attr(x=None) +Q = [] +UA = [] +for Td_bp in np.linspace(-10, -1, 10): + c12.set_attr(Td_bp=Td_bp) + nw.solve("design") + UA += [cd.UA.val] + Q += [c12.p.val] + + +c12.set_attr(Td_bp=None) +for x in np.linspace(0, 0.3, 5): + c12.set_attr(x=x) + nw.solve("design") + UA += [cd.UA.val] + Q += [c12.p.val] + +cd.set_attr(UA=cd.UA.val, td_pinch=None) + +from matplotlib import pyplot as plt +plt.plot(UA, Q) + +plt.show() + +# exit() +# print(c2.T.val) + +Q = [] +T_cond = [] +m_refrig = [] +dT_pinch = [] +# Q_cond = [] + +for m in np.linspace(12, 4.55, 40): + 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] + + + + +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(qc / q) for qc, q in zip(Q_cond, Q)], label="cond") +# ax[3].scatter(Q, [abs((q + qc) / q) for qc, q in zip(Q_cond, Q)], label="desup") +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_partload_m_changing.png") + + +fig, ax = plt.subplots(1) + +for i, m in enumerate(np.linspace(12, 5, 25)): + c1.set_attr(m=m) + nw.solve("design") + + if i % 5 ==0: + + if cd.T_desup_i2 > cd.T_desup_o1: + print("PINCH VIOLATION") + + cd.Q.val = abs(cd.Q.val) + cd.Q_cond = abs(cd.Q_cond) + + _ = ax.plot([0, cd.Q_cond, cd.Q.val], [c12.T.val, cd.T_desup_o1 - 273.15, c11.T.val], label=f"Q={round(cd.Q.val)}") + ax.plot([0, cd.Q_cond, cd.Q.val], [c1.T.val, cd.T_desup_i2 - 273.15, c2.T.val], c=_[0].get_color()) + + +ax.set_ylabel("temperature") +ax.set_xlabel("heat transfer") +ax.grid() +ax.legend() + +plt.tight_layout() + +fig.savefig("mb_QT.png") \ No newline at end of file From 83dc271a13b47c1974811efbf3b21e7579dafb9a Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Mon, 29 Jul 2024 22:35:41 +0200 Subject: [PATCH 07/55] Trial for part load variable U values of individual sections --- .../heat_exchangers/movingboundary.py | 32 +++++++++++- ...st_partload_model_movingboundarygeneral.py | 51 +++++++++++++------ 2 files changed, 65 insertions(+), 18 deletions(-) diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index 59717af9c..f01092f94 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -76,7 +76,7 @@ def calc_UA_in_sections(self): td_at_steps = [T1 - T2 for T1, T2 in zip(T_at_steps_1, T_at_steps_2[::-1])] # parallel flow version # td_at_steps = [T1 - T2 for T1, T2 in zip(T_at_steps_1, T_at_steps_2)] - + 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]) @@ -96,7 +96,35 @@ def UA_func(self, **kwargs): residual : float Residual value of equation. """ - return self.UA.val - sum(self.calc_UA_in_sections()) + UA_in_sections = self.calc_UA_in_sections() + sum_UA = sum(UA_in_sections) + + # UA_sum = 0 + m_1 = self.inl[0].m + m_2 = self.inl[1].m + # sum_UA = 0 + if len(UA_in_sections) == 3: + for i, UA in enumerate(UA_in_sections): + if i == 1: + f_UA = 2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.4)) + else: + f_UA = 2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.8)) + + # sum_UA += f_UA * UA + + + elif len(UA_in_sections) == 2: + for i, UA in enumerate(UA_in_sections): + if i == 1: + f_UA = 2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.4)) + else: + f_UA = 2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.8)) + + # sum_UA += f_UA * UA + else: + pass + + return self.UA.design - sum_UA# * (2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.4))) def UA_deriv(self, increment_filter, k): r""" diff --git a/tests/test_components/test_partload_model_movingboundarygeneral.py b/tests/test_components/test_partload_model_movingboundarygeneral.py index 4184bb8de..464b3e9e9 100644 --- a/tests/test_components/test_partload_model_movingboundarygeneral.py +++ b/tests/test_components/test_partload_model_movingboundarygeneral.py @@ -47,7 +47,7 @@ # test pinch specification -c12.set_attr(T=None) +c12.set_attr(T=None, x=None, Td_bp=-2) cd.set_attr(td_pinch=3) nw.solve("design") nw.print_results() @@ -68,16 +68,19 @@ cd.zeta1.set_attr(min_val=-2) nw.solve("design") +nw.save("design") nw.print_results() -c12.set_attr(x=None) -Q = [] +# c12.set_attr(x=None) +pressure = [] +massflow = [] UA = [] for Td_bp in np.linspace(-10, -1, 10): c12.set_attr(Td_bp=Td_bp) nw.solve("design") UA += [cd.UA.val] - Q += [c12.p.val] + pressure += [c12.p.val] + massflow += [c12.m.val] c12.set_attr(Td_bp=None) @@ -85,14 +88,30 @@ c12.set_attr(x=x) nw.solve("design") UA += [cd.UA.val] - Q += [c12.p.val] + pressure += [c12.p.val] + massflow += [c12.m.val] + +c12.set_attr(x=None, Td_bp=-2) cd.set_attr(UA=cd.UA.val, td_pinch=None) from matplotlib import pyplot as plt -plt.plot(UA, Q) -plt.show() +fig, ax = plt.subplots(2, sharex=True) + +ax[0].plot(massflow, pressure) +ax[1].plot(massflow, UA) + +ax[0].set_title("Resulting condensation pressure and UA in design mode") + +ax[1].set_xlabel("refrigerant mass flow") +ax[0].set_ylabel("Condensation pressure") +ax[1].set_ylabel("UA") + +plt.tight_layout() + +fig.savefig("mb_pressure_and_UA_vs_massflow_design.png") +# plt.show() # exit() # print(c2.T.val) @@ -103,9 +122,11 @@ dT_pinch = [] # Q_cond = [] -for m in np.linspace(12, 4.55, 40): +for m in np.linspace(10, 5, 20): c1.set_attr(m=m) - nw.solve("design") + print(m) + nw.solve("offdesign", design_path="design") + # nw.solve("design") m_refrig += [c12.m.val] T_cond += [c12.T.val] Q += [abs(cd.Q.val)] @@ -113,9 +134,7 @@ dT_pinch += [cd.td_pinch.val] - - -fig, ax = plt.subplots(4, sharex=True, figsize=(12, 8)) +fig, ax = plt.subplots(3, sharex=True, figsize=(12, 8)) ax[0].scatter(Q, m_refrig) ax[0].set_ylabel("refrigerant mass flow") @@ -125,15 +144,15 @@ ax[2].set_ylabel("pinch temperature difference") # ax[3].scatter(Q, [abs(qc / q) for qc, q in zip(Q_cond, Q)], label="cond") # ax[3].scatter(Q, [abs((q + qc) / q) for qc, q in zip(Q_cond, Q)], label="desup") -ax[3].legend() -ax[3].set_ylabel("heat transfer shares of total heat transfer") +# ax[3].legend() +# ax[3].set_ylabel("heat transfer shares of total heat transfer") -ax[3].set_xlabel("total heat transfer") +ax[2].set_xlabel("total heat transfer") [_.grid() for _ in ax] plt.tight_layout() -fig.savefig("mb_partload_m_changing.png") +fig.savefig("mb_partload_without_UA_correction.png") fig, ax = plt.subplots(1) From 463064f2185f186ba7d3fc07a30fe54a6292a772 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 4 Aug 2024 23:43:14 +0200 Subject: [PATCH 08/55] Clean up the MovingboundaryCondenser design mode calculation options --- .../heat_exchangers/movingboundary.py | 411 +++++++----------- 1 file changed, 167 insertions(+), 244 deletions(-) diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index f01092f94..ae32ddcd5 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -20,15 +20,24 @@ from tespy.tools.fluid_properties import h_mix_pQ -class MovingBoundaryHeatExchanger(HeatExchanger): +class MovingBoundaryCondenser(HeatExchanger): @staticmethod def component(): - return 'moving boundary heat exchanger' + 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 ), @@ -39,50 +48,122 @@ def get_parameters(self): }) return params - def calc_UA_in_sections(self): - i1, i2 = self.inl - o1, o2 = self.outl + 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] - sections = 3 + 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)): - sections = 2 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: - sections = 1 h_at_steps_1 = [i1.h.val_SI, o1.h.val_SI] - p_at_steps_1 = [i1.p.val_SI for _ in range(sections + 1)] - T_at_steps_1 = [T_mix_ph(p, h, i1.fluid_data) for p, h in zip(p_at_steps_1, h_at_steps_1)] + 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 - Q_in_sections = [i1.m.val_SI * (h_at_steps_1[i + 1] - h_at_steps_1[i]) for i in range(sections)] + 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)] + 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])] - # parallel flow version - # td_at_steps = [T1 - T2 for T1, T2 in zip(T_at_steps_1, T_at_steps_2)] + 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) ] - UA_in_sections = [abs(Q) / td_log for Q, td_log in zip(Q_in_sections, td_log_in_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 @@ -97,34 +178,7 @@ def UA_func(self, **kwargs): Residual value of equation. """ UA_in_sections = self.calc_UA_in_sections() - sum_UA = sum(UA_in_sections) - - # UA_sum = 0 - m_1 = self.inl[0].m - m_2 = self.inl[1].m - # sum_UA = 0 - if len(UA_in_sections) == 3: - for i, UA in enumerate(UA_in_sections): - if i == 1: - f_UA = 2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.4)) - else: - f_UA = 2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.8)) - - # sum_UA += f_UA * UA - - - elif len(UA_in_sections) == 2: - for i, UA in enumerate(UA_in_sections): - if i == 1: - f_UA = 2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.4)) - else: - f_UA = 2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.8)) - - # sum_UA += f_UA * UA - else: - pass - - return self.UA.design - sum_UA# * (2 / ((1 / (m_2.val_SI / m_2.design) ** 0.8) + (1 / (m_1.val_SI / m_1.design) ** 0.4))) + return self.UA.val - sum(UA_in_sections) def UA_deriv(self, increment_filter, k): r""" @@ -147,51 +201,34 @@ def UA_deriv(self, increment_filter, k): if self.is_variable(c.h): self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) - def calc_td_pinch(self): - 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): + def U_sections_func(self, **kwargs): r""" - Equation for pinch point temperature difference of a condenser. + 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. - - .. 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. + 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 ---------- @@ -201,126 +238,42 @@ def td_pinch_deriv(self, increment_filter, k): 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): + 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, increment_filter): + if self.is_variable(c.h): self.jacobian[k, c.h.J_col] = self.numeric_deriv(f, 'h', c) - def calc_parameters(self): - super().calc_parameters() - self.UA.val = sum(self.calc_UA_in_sections()) - self.td_pinch.val = self.calc_td_pinch() - -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), - 'A': dc_cp(min_val=0), - 'UA_group': dc_gcp( - elements=['U_desup', 'U_cond', 'A'], - func=self.UA_func, deriv=self.UA_deriv, latex=None, - num_eq=1 - ), - '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 UA_func(self, **kwargs): - r""" - Calculate heat transfer from heat transfer coefficients for - desuperheating and condensation as well as total heat exchange area. + def calc_td_pinch(self): + """Calculate the pinch point temperature difference Returns ------- - residual : float - Residual value of equation. + float + Value of the pinch point temperature difference """ - Q_total = self.inl[0].m.val_SI * (self.outl[0].h.val_SI - self.inl[0].h.val_SI) - h_sat = h_mix_pQ(self.outl[0].p.val_SI, 1, self.outl[0].fluid_data) + o1 = self.outl[0] + i2 = self.inl[1] + + h_sat = h_mix_pQ(o1.p.val_SI, 1, o1.fluid_data) - if self.outl[0].h.val_SI < h_sat: + if o1.h.val_SI < h_sat: # we have two sections in this case - Q_desup = self.inl[0].m.val_SI * (h_sat - self.inl[0].h.val_SI) - Q_cond = self.inl[0].m.val_SI * (self.outl[0].h.val_SI - h_sat) + Q_cond = o1.m.val_SI * (o1.h.val_SI - h_sat) # calculate the intermediate temperatures - T_desup_i1 = self.inl[0].calc_T() - T_desup_o1 = self.outl[0].calc_T_sat() - T_cond_i1 = T_desup_o1 - # not considering any pressure loss yet - T_cond_o1 = T_desup_o1 - - T_cond_i2 = self.inl[1].calc_T() - h_cond_o2 = self.inl[1].h.val_SI + abs(Q_cond) / self.inl[1].m.val_SI - T_cond_o2 = T_mix_ph(self.inl[1].p.val_SI, h_cond_o2, self.inl[1].fluid_data) - T_desup_i2 = T_cond_o2 - T_desup_o2 = self.outl[1].calc_T() - - ttd_desup_u = T_desup_i1 - T_desup_o2 - ttd_desup_l = T_desup_o1 - T_desup_i2 - - ttd_cond_u = T_cond_i1 - T_cond_o2 - ttd_cond_l = T_cond_o1 - T_cond_i2 - - if ttd_desup_u < 0: - ttd_desup_u = abs(ttd_desup_u) - if ttd_desup_l < 0: - ttd_desup_l = abs(ttd_desup_l) - - if ttd_cond_u < 0: - ttd_cond_u = abs(ttd_cond_u) - if ttd_cond_l < 0: - ttd_cond_l = abs(ttd_cond_l) - - td_log_desup = (ttd_desup_l - ttd_desup_u) / math.log(ttd_desup_l / ttd_desup_u) - td_log_cond = (ttd_cond_l - ttd_cond_u) / math.log(ttd_cond_l / ttd_cond_u) - - residual = ( - Q_total - + self.A.val * (Q_desup / Q_total) * self.U_desup.val * td_log_desup - + self.A.val * (Q_cond / Q_total) * self.U_cond.val * td_log_cond - ) - - else: - # only condensation is happening - residual = Q_total + self.A.val * self.U_cond.val * self.calculate_td_log() - - return residual - - def UA_deriv(self, increment_filter, k): - r""" - Partial derivatives of heat transfer coefficient function. + 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) - Parameters - ---------- - increment_filter : ndarray - Matrix for filtering non-changing variables. + return T_cond_i1 - T_cond_o2 - 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, d=1e-5) + else: + o2 = self.outl[1] + return o1.calc_T_sat() - o2.calc_T() def td_pinch_func(self): r""" @@ -341,25 +294,7 @@ def td_pinch_func(self): \right] \right) """ - 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 self.td_pinch.val - T_cond_i1 + T_cond_o2 - - else: - o2 = self.outl[1] - return self.td_pinch.val - o1.calc_T_sat() + o2.calc_T() + return self.td_pinch.val - self.calc_td_pinch() def td_pinch_deriv(self, increment_filter, k): """ @@ -385,39 +320,27 @@ def td_pinch_deriv(self, increment_filter, k): def calc_parameters(self): super().calc_parameters() - # this should be exported to a function, which can be called from here - # and from the UA_func. - h_sat = h_mix_pQ(self.outl[0].p.val_SI, 1, self.outl[0].fluid_data) - self.Q_desup = self.inl[0].m.val_SI * (h_sat - self.inl[0].h.val_SI) - Q_desup = self.Q_desup - self.Q_cond = self.inl[0].m.val_SI * (self.outl[0].h.val_SI - h_sat) - Q_cond = self.Q_cond - T_desup_i1 = self.inl[0].calc_T() - self.T_desup_o1 = self.outl[0].calc_T_sat() - T_desup_o1 = self.T_desup_o1 - T_cond_i1 = T_desup_o1 - T_cond_o1 = T_desup_o1 - - T_cond_i2 = self.inl[1].calc_T() - h_cond_o2 = self.inl[1].h.val_SI + abs(Q_cond) / self.inl[1].m.val_SI - T_cond_o2 = T_mix_ph(self.inl[1].p.val_SI, h_cond_o2, self.inl[1].fluid_data) - T_desup_i2 = T_cond_o2 - self.T_desup_i2 = T_desup_i2 - T_desup_o2 = self.outl[1].calc_T() - - ttd_desup_u = T_desup_i1 - T_desup_o2 - ttd_desup_l = T_desup_o1 - T_desup_i2 - - ttd_cond_u = T_cond_i1 - T_cond_o2 - ttd_cond_l = T_cond_o1 - T_cond_i2 - - td_log_desup = (ttd_desup_l - ttd_desup_u) / math.log(ttd_desup_l / ttd_desup_u) - td_log_cond = (ttd_cond_l - ttd_cond_u) / math.log(ttd_cond_l / ttd_cond_u) - - self.td_pinch.val = T_desup_o1 - T_desup_i2 - - self.A.val = abs(self.Q.val) / ((Q_desup / self.Q.val) * self.U_desup.val * td_log_desup + (Q_cond / self.Q.val) * self.U_cond.val * td_log_cond) - - # some intermediate tests for consistency - assert abs(abs(self.Q.val) / ((Q_desup / self.Q.val) * self.U_desup.val * td_log_desup + (Q_cond / self.Q.val) * self.U_cond.val * td_log_cond) - self.A.val) < 1e-6 - assert round(Q_cond + Q_desup, 3) == round(self.Q.val, 3) + 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() From 773f125ccc60ce0d3ad4d19311d28b52b6058026 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 4 Aug 2024 23:43:33 +0200 Subject: [PATCH 09/55] Test all features of the condenser --- ..._partload_model_movingboundarycondenser.py | 69 ++++++++++++------- 1 file changed, 45 insertions(+), 24 deletions(-) diff --git a/tests/test_components/test_partload_model_movingboundarycondenser.py b/tests/test_components/test_partload_model_movingboundarycondenser.py index 885eb7ffa..a200701d6 100644 --- a/tests/test_components/test_partload_model_movingboundarycondenser.py +++ b/tests/test_components/test_partload_model_movingboundarycondenser.py @@ -38,7 +38,7 @@ 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) +cd.set_attr(U_desup=4000, U_cond=16000, U_subcool=5000) nw.solve("design") nw.save("design") @@ -71,20 +71,22 @@ # exit() # print(c2.T.val) + Q = [] T_cond = [] m_refrig = [] dT_pinch = [] -Q_cond = [] - -for m in np.linspace(12, 4.55, 40): +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] from matplotlib import pyplot as plt @@ -98,8 +100,8 @@ ax[1].set_ylabel("condensation temperature") ax[2].scatter(Q, dT_pinch) ax[2].set_ylabel("pinch temperature difference") -ax[3].scatter(Q, [abs(qc / q) for qc, q in zip(Q_cond, Q)], label="cond") -ax[3].scatter(Q, [abs((q + qc) / q) for qc, q in zip(Q_cond, Q)], label="desup") +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") @@ -108,32 +110,51 @@ [_.grid() for _ in ax] plt.tight_layout() -fig.savefig("mb_partload_m_changing.png") +fig.savefig("mb_U_sections_diff_m.png") +plt.close() -fig, ax = plt.subplots(1) +c1.set_attr(m=10) +nw.solve("design") +nw.print_results() -for i, m in enumerate(np.linspace(12, 5, 25)): +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] - if i % 5 ==0: - - if cd.T_desup_i2 > cd.T_desup_o1: - print("PINCH VIOLATION") +print(Q_in_sections) - cd.Q.val = abs(cd.Q.val) - cd.Q_cond = abs(cd.Q_cond) +fig, ax = plt.subplots(4, sharex=True, figsize=(12, 8)) - _ = ax.plot([0, cd.Q_cond, cd.Q.val], [c12.T.val, cd.T_desup_o1 - 273.15, c11.T.val], label=f"Q={round(cd.Q.val)}") - ax.plot([0, cd.Q_cond, cd.Q.val], [c1.T.val, cd.T_desup_i2 - 273.15, c2.T.val], c=_[0].get_color()) +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") -ax.set_ylabel("temperature") -ax.set_xlabel("heat transfer") -ax.grid() -ax.legend() +[_.grid() for _ in ax] plt.tight_layout() - -fig.savefig("mb_QT.png") +fig.savefig("mb_UA_diff_m.png") \ No newline at end of file From c0e4efcbd7744a6eb16fdbd7230950fb129d73ab Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Tue, 6 Aug 2024 17:24:03 +0200 Subject: [PATCH 10/55] Remove deprecated import --- src/tespy/components/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index 2724bffc1..c8c2f67fb 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -11,7 +11,6 @@ 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.movingboundary import MovingBoundaryHeatExchanger # 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 From 6746b01d7b0fe2bf07b6a73843a55c559a122278 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Tue, 6 Aug 2024 17:25:54 +0200 Subject: [PATCH 11/55] Remove deprecated test --- ...st_partload_model_movingboundarygeneral.py | 183 ------------------ 1 file changed, 183 deletions(-) delete mode 100644 tests/test_components/test_partload_model_movingboundarygeneral.py diff --git a/tests/test_components/test_partload_model_movingboundarygeneral.py b/tests/test_components/test_partload_model_movingboundarygeneral.py deleted file mode 100644 index 464b3e9e9..000000000 --- a/tests/test_components/test_partload_model_movingboundarygeneral.py +++ /dev/null @@ -1,183 +0,0 @@ - -from tespy.components import HeatExchanger, Source, Sink, Compressor, MovingBoundaryHeatExchanger -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 = MovingBoundaryHeatExchanger("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() - -nw.solve("design") -nw.save("design") -nw.print_results() - - -# test pinch specification -c12.set_attr(T=None, x=None, Td_bp=-2) -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) -# c1.set_attr(m=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.save("design") -nw.print_results() - -# c12.set_attr(x=None) -pressure = [] -massflow = [] -UA = [] -for Td_bp in np.linspace(-10, -1, 10): - c12.set_attr(Td_bp=Td_bp) - nw.solve("design") - UA += [cd.UA.val] - pressure += [c12.p.val] - massflow += [c12.m.val] - - -c12.set_attr(Td_bp=None) -for x in np.linspace(0, 0.3, 5): - c12.set_attr(x=x) - nw.solve("design") - UA += [cd.UA.val] - pressure += [c12.p.val] - massflow += [c12.m.val] - -c12.set_attr(x=None, Td_bp=-2) - -cd.set_attr(UA=cd.UA.val, td_pinch=None) - -from matplotlib import pyplot as plt - -fig, ax = plt.subplots(2, sharex=True) - -ax[0].plot(massflow, pressure) -ax[1].plot(massflow, UA) - -ax[0].set_title("Resulting condensation pressure and UA in design mode") - -ax[1].set_xlabel("refrigerant mass flow") -ax[0].set_ylabel("Condensation pressure") -ax[1].set_ylabel("UA") - -plt.tight_layout() - -fig.savefig("mb_pressure_and_UA_vs_massflow_design.png") -# plt.show() - -# exit() -# print(c2.T.val) - -Q = [] -T_cond = [] -m_refrig = [] -dT_pinch = [] -# Q_cond = [] - -for m in np.linspace(10, 5, 20): - c1.set_attr(m=m) - print(m) - nw.solve("offdesign", design_path="design") - # 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] - - -fig, ax = plt.subplots(3, 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(qc / q) for qc, q in zip(Q_cond, Q)], label="cond") -# ax[3].scatter(Q, [abs((q + qc) / q) for qc, q in zip(Q_cond, Q)], label="desup") -# ax[3].legend() -# ax[3].set_ylabel("heat transfer shares of total heat transfer") - -ax[2].set_xlabel("total heat transfer") - -[_.grid() for _ in ax] - -plt.tight_layout() -fig.savefig("mb_partload_without_UA_correction.png") - - -fig, ax = plt.subplots(1) - -for i, m in enumerate(np.linspace(12, 5, 25)): - c1.set_attr(m=m) - nw.solve("design") - - if i % 5 ==0: - - if cd.T_desup_i2 > cd.T_desup_o1: - print("PINCH VIOLATION") - - cd.Q.val = abs(cd.Q.val) - cd.Q_cond = abs(cd.Q_cond) - - _ = ax.plot([0, cd.Q_cond, cd.Q.val], [c12.T.val, cd.T_desup_o1 - 273.15, c11.T.val], label=f"Q={round(cd.Q.val)}") - ax.plot([0, cd.Q_cond, cd.Q.val], [c1.T.val, cd.T_desup_i2 - 273.15, c2.T.val], c=_[0].get_color()) - - -ax.set_ylabel("temperature") -ax.set_xlabel("heat transfer") -ax.grid() -ax.legend() - -plt.tight_layout() - -fig.savefig("mb_QT.png") \ No newline at end of file From 75f772338197f7aff08dd74775346391749db05b Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 5 Jan 2025 15:48:05 +0100 Subject: [PATCH 12/55] state and quality reporting --- src/tespy/connections/connection.py | 14 +++++++ src/tespy/networks/network.py | 2 +- src/tespy/tools/fluid_properties/__init__.py | 1 + src/tespy/tools/fluid_properties/functions.py | 8 ++++ src/tespy/tools/fluid_properties/wrappers.py | 38 ++++++++++++++++++- src/tespy/tools/global_vars.py | 8 ++++ src/tespy/tools/helpers.py | 3 +- 7 files changed, 71 insertions(+), 3 deletions(-) diff --git a/src/tespy/connections/connection.py b/src/tespy/connections/connection.py index c80b62f9e..222701cef 100644 --- a/src/tespy/connections/connection.py +++ b/src/tespy/connections/connection.py @@ -21,6 +21,7 @@ from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.fluid_properties import CoolPropWrapper from tespy.tools.fluid_properties import Q_mix_ph +from tespy.tools.fluid_properties import state_mix_ph from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import T_sat_p from tespy.tools.fluid_properties import dh_mix_dpQ @@ -704,6 +705,8 @@ def get_parameters(self): "v_ref": dc_ref( func=self.v_ref_func, deriv=self.v_ref_deriv, num_eq=1 ), + "state": dc_prop(is_var=True), + } def build_fluid_data(self): @@ -932,6 +935,11 @@ def calc_results(self): self.x.val_SI = self.calc_x() except ValueError: self.x.val_SI = np.nan + try: + if not self.state.is_set: + self.state.val_SI = self.calc_state() + except ValueError: + self.state.val_SI = np.nan try: if not self.Td_bp.is_set: self.Td_bp.val_SI = self.calc_Td_bp() @@ -1126,6 +1134,12 @@ def get_chemical_exergy(self, pamb, Tamb, Chem_Ex): self.Ex_chemical = self.m.val_SI * self.ex_chemical + def calc_state(self): + try: + return state_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data) + except NotImplementedError: + return np.nan + class Ref: r""" diff --git a/src/tespy/networks/network.py b/src/tespy/networks/network.py index b32842ca3..07ce9545e 100644 --- a/src/tespy/networks/network.py +++ b/src/tespy/networks/network.py @@ -2571,7 +2571,7 @@ def print_results(self, colored=True, colors=None, print_results=True): ) # connection properties - df = self.results['Connection'].loc[:, ['m', 'p', 'h', 'T']].copy() + df = self.results['Connection'].loc[:, ['m', 'p', 'h', 'T', 'x', 'state']].copy() df = df.astype(str) for c in df.index: if not self.get_conn(c).printout: diff --git a/src/tespy/tools/fluid_properties/__init__.py b/src/tespy/tools/fluid_properties/__init__.py index 987173bd5..f4e35e99d 100644 --- a/src/tespy/tools/fluid_properties/__init__.py +++ b/src/tespy/tools/fluid_properties/__init__.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 +from .functions import state_mix_ph from .functions import Q_mix_ph # noqa: F401 from .functions import T_mix_ph # noqa: F401 from .functions import T_mix_ps # noqa: F401 diff --git a/src/tespy/tools/fluid_properties/functions.py b/src/tespy/tools/fluid_properties/functions.py index f97ce42d1..9514812a7 100644 --- a/src/tespy/tools/fluid_properties/functions.py +++ b/src/tespy/tools/fluid_properties/functions.py @@ -155,6 +155,14 @@ def Q_mix_ph(p, h, fluid_data, mixing_rule=None): msg = "Saturation function cannot be called on mixtures." raise ValueError(msg) +def state_mix_ph(p, h, fluid_data, mixing_rule=None): + if get_number_of_fluids(fluid_data) == 1: + pure_fluid = get_pure_fluid(fluid_data) + return pure_fluid["wrapper"].state_ph(p, h) + else: + msg = "State function cannot be called on mixtures." + raise ValueError(msg) + def p_sat_T(T, fluid_data, mixing_rule=None): if get_number_of_fluids(fluid_data) == 1: diff --git a/src/tespy/tools/fluid_properties/wrappers.py b/src/tespy/tools/fluid_properties/wrappers.py index 6c298d54f..dea96604c 100644 --- a/src/tespy/tools/fluid_properties/wrappers.py +++ b/src/tespy/tools/fluid_properties/wrappers.py @@ -12,6 +12,7 @@ """ import CoolProp as CP +import numpy as np from tespy.tools.global_vars import ERR @@ -93,6 +94,9 @@ def p_sat(self, T): def Q_ph(self, p, h): self._not_implemented() + def state_ph(self, p, h): + self._not_implemented() + def d_ph(self, p, h): self._not_implemented() @@ -227,7 +231,28 @@ def p_sat(self, T): def Q_ph(self, p, h): p = self._make_p_subcritical(p) self.AS.update(CP.HmassP_INPUTS, h, p) - return self.AS.Q() + + if self.AS.phase() == CP.iphase_twophase: + return self.AS.Q() + elif self.AS.phase() == CP.iphase_liquid: + return 0 + elif self.AS.phase() == CP.iphase_gas: + return 1 + else: # all other phases - though this should be unreachable as p is sub-critical + return -1 + + def state_ph(self, p, h): + p = self._make_p_subcritical(p) + self.AS.update(CP.HmassP_INPUTS, h, p) + + if self.AS.phase() == CP.iphase_twophase: + return "tp" + elif self.AS.phase() == CP.iphase_liquid: + return "l" + elif self.AS.phase() == CP.iphase_gas: + return "g" + else: # all other phases - though this should be unreachable as p is sub-critical + return "state not recognised" def d_ph(self, p, h): self.AS.update(CP.HmassP_INPUTS, h, p) @@ -355,6 +380,17 @@ def Q_ph(self, p, h): p = self._make_p_subcritical(p) return self.AS(h=h / 1e3, P=p / 1e6).x + def state_ph(self, p, h): + p = self._make_p_subcritical(p) + x = self.AS(h=h / 1e3, P=p / 1e6).x + + if x == 0: + return "l" + elif x == 1: + return "g" + else: + return "tp" + def d_ph(self, p, h): return self.AS(h=h / 1e3, P=p / 1e6).rho diff --git a/src/tespy/tools/global_vars.py b/src/tespy/tools/global_vars.py index 8cd773f1a..098da25b2 100644 --- a/src/tespy/tools/global_vars.py +++ b/src/tespy/tools/global_vars.py @@ -97,7 +97,15 @@ 'units': {'J / kgK': 1, 'kJ / kgK': 1e3, 'MJ / kgK': 1e6}, 'latex_eq': r'0 = s_\mathrm{spec} - s\left(p, h \right)', 'documentation': {'float_fmt': '{:,.2f}'} + }, + 'state': { + 'text': 'state', + 'SI_unit': '-', + 'units': {'-': 1}, + 'latex_eq': r'', + 'documentation': {'string_fmt': '{}'} } + } combustion_gases = ['methane', 'ethane', 'propane', 'butane', 'hydrogen', 'nDodecane'] diff --git a/src/tespy/tools/helpers.py b/src/tespy/tools/helpers.py index 7b9601b43..fe116cc26 100644 --- a/src/tespy/tools/helpers.py +++ b/src/tespy/tools/helpers.py @@ -133,7 +133,8 @@ def convert_from_SI(property, SI_value, unit): if property == 'T': converters = fluid_property_data['T']['units'][unit] return SI_value / converters[1] - converters[0] - + elif property == "state": + return SI_value else: return SI_value / fluid_property_data[property]['units'][unit] From 95c3cd0e11f430f1a1adb0cf1d006d1f1f777132 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 5 Jan 2025 22:04:30 +0100 Subject: [PATCH 13/55] state and quality reporting --- src/tespy/tools/fluid_properties/wrappers.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/tespy/tools/fluid_properties/wrappers.py b/src/tespy/tools/fluid_properties/wrappers.py index dea96604c..12bbee6ea 100644 --- a/src/tespy/tools/fluid_properties/wrappers.py +++ b/src/tespy/tools/fluid_properties/wrappers.py @@ -382,14 +382,17 @@ def Q_ph(self, p, h): def state_ph(self, p, h): p = self._make_p_subcritical(p) - x = self.AS(h=h / 1e3, P=p / 1e6).x - if x == 0: + phase = self.AS(h=h / 1e3, P=p / 1e6).phase + + if phase in ["Liquid"]: return "l" - elif x == 1: + elif phase in ["Vapour"]: return "g" - else: + elif phase in ["Two phases", "Saturated vapor", "Saturated liquid"]: return "tp" + else: # to ensure consistent behaviour to CoolPropWrapper + return "phase not recognised" def d_ph(self, p, h): return self.AS(h=h / 1e3, P=p / 1e6).rho From 4a3c721a02acb26f6f5712371955a6e39c8f6054 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Tue, 7 Jan 2025 20:54:26 +0100 Subject: [PATCH 14/55] phase and quality reporting --- src/tespy/connections/connection.py | 15 ++++++++------- src/tespy/networks/network.py | 7 +++++-- src/tespy/tools/fluid_properties/__init__.py | 2 +- src/tespy/tools/fluid_properties/functions.py | 4 ++-- src/tespy/tools/fluid_properties/wrappers.py | 6 +++--- src/tespy/tools/global_vars.py | 7 ------- src/tespy/tools/helpers.py | 2 -- 7 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/tespy/connections/connection.py b/src/tespy/connections/connection.py index 222701cef..3c2319c12 100644 --- a/src/tespy/connections/connection.py +++ b/src/tespy/connections/connection.py @@ -21,7 +21,7 @@ from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.fluid_properties import CoolPropWrapper from tespy.tools.fluid_properties import Q_mix_ph -from tespy.tools.fluid_properties import state_mix_ph +from tespy.tools.fluid_properties import phase_mix_ph from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import T_sat_p from tespy.tools.fluid_properties import dh_mix_dpQ @@ -267,6 +267,7 @@ def __init__(self, source, outlet_id, target, inlet_id, if hasattr(v, "func") and v.func is not None } self.state = dc_simple() + self.phase = dc_simple() self.property_data0 = [x + '0' for x in self.property_data.keys()] self.__dict__.update(self.property_data) self.mixing_rule = None @@ -705,7 +706,6 @@ def get_parameters(self): "v_ref": dc_ref( func=self.v_ref_func, deriv=self.v_ref_deriv, num_eq=1 ), - "state": dc_prop(is_var=True), } @@ -935,11 +935,12 @@ def calc_results(self): self.x.val_SI = self.calc_x() except ValueError: self.x.val_SI = np.nan + try: - if not self.state.is_set: - self.state.val_SI = self.calc_state() + self.phase.val = self.calc_phase() except ValueError: - self.state.val_SI = np.nan + self.phase.val = np.nan + try: if not self.Td_bp.is_set: self.Td_bp.val_SI = self.calc_Td_bp() @@ -1134,9 +1135,9 @@ def get_chemical_exergy(self, pamb, Tamb, Chem_Ex): self.Ex_chemical = self.m.val_SI * self.ex_chemical - def calc_state(self): + def calc_phase(self): try: - return state_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data) + return phase_mix_ph(self.p.val_SI, self.h.val_SI, self.fluid_data) except NotImplementedError: return np.nan diff --git a/src/tespy/networks/network.py b/src/tespy/networks/network.py index 07ce9545e..123aa7093 100644 --- a/src/tespy/networks/network.py +++ b/src/tespy/networks/network.py @@ -1187,7 +1187,7 @@ def init_set_properties(self): self.all_fluids = set(self.all_fluids) cols = ( [col for prop in properties for col in [prop, f"{prop}_unit"]] - + list(self.all_fluids) + + list(self.all_fluids) + ['phase'] ) self.results['Connection'] = pd.DataFrame(columns=cols, dtype='float64') # include column for fluid balance in specs dataframe @@ -2476,7 +2476,10 @@ def process_connections(self): ] + [ c.fluid.val[fluid] if fluid in c.fluid.val else np.nan for fluid in self.all_fluids + ] + [ + c.phase.val ] + ) def process_components(self): @@ -2571,7 +2574,7 @@ def print_results(self, colored=True, colors=None, print_results=True): ) # connection properties - df = self.results['Connection'].loc[:, ['m', 'p', 'h', 'T', 'x', 'state']].copy() + df = self.results['Connection'].loc[:, ['m', 'p', 'h', 'T', 'x', 'phase']].copy() df = df.astype(str) for c in df.index: if not self.get_conn(c).printout: diff --git a/src/tespy/tools/fluid_properties/__init__.py b/src/tespy/tools/fluid_properties/__init__.py index f4e35e99d..e7add0872 100644 --- a/src/tespy/tools/fluid_properties/__init__.py +++ b/src/tespy/tools/fluid_properties/__init__.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -from .functions import state_mix_ph +from .functions import phase_mix_ph from .functions import Q_mix_ph # noqa: F401 from .functions import T_mix_ph # noqa: F401 from .functions import T_mix_ps # noqa: F401 diff --git a/src/tespy/tools/fluid_properties/functions.py b/src/tespy/tools/fluid_properties/functions.py index 9514812a7..0ef9d582e 100644 --- a/src/tespy/tools/fluid_properties/functions.py +++ b/src/tespy/tools/fluid_properties/functions.py @@ -155,10 +155,10 @@ def Q_mix_ph(p, h, fluid_data, mixing_rule=None): msg = "Saturation function cannot be called on mixtures." raise ValueError(msg) -def state_mix_ph(p, h, fluid_data, mixing_rule=None): +def phase_mix_ph(p, h, fluid_data, mixing_rule=None): if get_number_of_fluids(fluid_data) == 1: pure_fluid = get_pure_fluid(fluid_data) - return pure_fluid["wrapper"].state_ph(p, h) + return pure_fluid["wrapper"].phase_ph(p, h) else: msg = "State function cannot be called on mixtures." raise ValueError(msg) diff --git a/src/tespy/tools/fluid_properties/wrappers.py b/src/tespy/tools/fluid_properties/wrappers.py index 12bbee6ea..fd1d7274e 100644 --- a/src/tespy/tools/fluid_properties/wrappers.py +++ b/src/tespy/tools/fluid_properties/wrappers.py @@ -94,7 +94,7 @@ def p_sat(self, T): def Q_ph(self, p, h): self._not_implemented() - def state_ph(self, p, h): + def phase_ph(self, p, h): self._not_implemented() def d_ph(self, p, h): @@ -241,7 +241,7 @@ def Q_ph(self, p, h): else: # all other phases - though this should be unreachable as p is sub-critical return -1 - def state_ph(self, p, h): + def phase_ph(self, p, h): p = self._make_p_subcritical(p) self.AS.update(CP.HmassP_INPUTS, h, p) @@ -380,7 +380,7 @@ def Q_ph(self, p, h): p = self._make_p_subcritical(p) return self.AS(h=h / 1e3, P=p / 1e6).x - def state_ph(self, p, h): + def phase_ph(self, p, h): p = self._make_p_subcritical(p) phase = self.AS(h=h / 1e3, P=p / 1e6).phase diff --git a/src/tespy/tools/global_vars.py b/src/tespy/tools/global_vars.py index 098da25b2..055959143 100644 --- a/src/tespy/tools/global_vars.py +++ b/src/tespy/tools/global_vars.py @@ -98,13 +98,6 @@ 'latex_eq': r'0 = s_\mathrm{spec} - s\left(p, h \right)', 'documentation': {'float_fmt': '{:,.2f}'} }, - 'state': { - 'text': 'state', - 'SI_unit': '-', - 'units': {'-': 1}, - 'latex_eq': r'', - 'documentation': {'string_fmt': '{}'} - } } diff --git a/src/tespy/tools/helpers.py b/src/tespy/tools/helpers.py index fe116cc26..4ea3a9b03 100644 --- a/src/tespy/tools/helpers.py +++ b/src/tespy/tools/helpers.py @@ -133,8 +133,6 @@ def convert_from_SI(property, SI_value, unit): if property == 'T': converters = fluid_property_data['T']['units'][unit] return SI_value / converters[1] - converters[0] - elif property == "state": - return SI_value else: return SI_value / fluid_property_data[property]['units'][unit] From ad47269090a1f0d2e1d9bb4233f4f60f4368bdde Mon Sep 17 00:00:00 2001 From: tlmerbecks <91725999+tlmerbecks@users.noreply.github.com> Date: Wed, 8 Jan 2025 06:45:15 +0100 Subject: [PATCH 15/55] Reset changes to Q_ph --- src/tespy/tools/fluid_properties/wrappers.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/tespy/tools/fluid_properties/wrappers.py b/src/tespy/tools/fluid_properties/wrappers.py index fd1d7274e..90e16b411 100644 --- a/src/tespy/tools/fluid_properties/wrappers.py +++ b/src/tespy/tools/fluid_properties/wrappers.py @@ -231,15 +231,16 @@ def p_sat(self, T): def Q_ph(self, p, h): p = self._make_p_subcritical(p) self.AS.update(CP.HmassP_INPUTS, h, p) - - if self.AS.phase() == CP.iphase_twophase: - return self.AS.Q() - elif self.AS.phase() == CP.iphase_liquid: - return 0 - elif self.AS.phase() == CP.iphase_gas: - return 1 - else: # all other phases - though this should be unreachable as p is sub-critical - return -1 + return self.AS.Q() + + # if self.AS.phase() == CP.iphase_twophase: + # return self.AS.Q() + # elif self.AS.phase() == CP.iphase_liquid: + # return 0 + # elif self.AS.phase() == CP.iphase_gas: + # return 1 + # else: # all other phases - though this should be unreachable as p is sub-critical + # return -1 def phase_ph(self, p, h): p = self._make_p_subcritical(p) From c2024cbca9751aaa1e9b2d1ad51b34b29ea8da0d Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 12 Jan 2025 15:25:22 +0100 Subject: [PATCH 16/55] phase and quality reporting --- src/tespy/tools/document_models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/tespy/tools/document_models.py b/src/tespy/tools/document_models.py index 67fefb37d..0db50a2f9 100644 --- a/src/tespy/tools/document_models.py +++ b/src/tespy/tools/document_models.py @@ -333,6 +333,8 @@ def document_connection_params(nw, df, specs, eqs, c, rpt): unit = col + '_unit' if col == 'Td_bp': unit = 'T_unit' + elif col == 'phase': + continue col_header = ( col.replace('_', r'\_') + ' in ' + hlp.latex_unit(nw.get_attr(unit))) From b51a6cc6eba751963eb05aa45ca505f61329eda7 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Tue, 14 Jan 2025 16:07:08 +0100 Subject: [PATCH 17/55] Run isort --- src/tespy/connections/connection.py | 2 +- src/tespy/tools/fluid_properties/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tespy/connections/connection.py b/src/tespy/connections/connection.py index 3c2319c12..9e4137523 100644 --- a/src/tespy/connections/connection.py +++ b/src/tespy/connections/connection.py @@ -21,7 +21,6 @@ from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.fluid_properties import CoolPropWrapper from tespy.tools.fluid_properties import Q_mix_ph -from tespy.tools.fluid_properties import phase_mix_ph from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import T_sat_p from tespy.tools.fluid_properties import dh_mix_dpQ @@ -32,6 +31,7 @@ from tespy.tools.fluid_properties import dv_mix_pdh from tespy.tools.fluid_properties import h_mix_pQ from tespy.tools.fluid_properties import h_mix_pT +from tespy.tools.fluid_properties import phase_mix_ph from tespy.tools.fluid_properties import s_mix_ph from tespy.tools.fluid_properties import v_mix_ph from tespy.tools.fluid_properties import viscosity_mix_ph diff --git a/src/tespy/tools/fluid_properties/__init__.py b/src/tespy/tools/fluid_properties/__init__.py index e7add0872..6351f42e7 100644 --- a/src/tespy/tools/fluid_properties/__init__.py +++ b/src/tespy/tools/fluid_properties/__init__.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -from .functions import phase_mix_ph from .functions import Q_mix_ph # noqa: F401 from .functions import T_mix_ph # noqa: F401 from .functions import T_mix_ps # noqa: F401 @@ -14,6 +13,7 @@ from .functions import h_mix_pQ # noqa: F401 from .functions import h_mix_pT # noqa: F401 from .functions import isentropic # noqa: F401 +from .functions import phase_mix_ph # noqa: F401 from .functions import s_mix_ph # noqa: F401 from .functions import s_mix_pT # noqa: F401 from .functions import v_mix_ph # noqa: F401 From 010b6e2b3537f9e0ca1b14834b202f667821cd81 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Tue, 21 Jan 2025 22:06:43 +0100 Subject: [PATCH 18/55] Rename to MovingBoundaryHeatExchanger and implement generalized methods for phase handling --- src/tespy/components/__init__.py | 2 +- .../heat_exchangers/movingboundary.py | 255 ++++++++++-------- ..._partload_model_movingboundarycondenser.py | 142 ++-------- 3 files changed, 155 insertions(+), 244 deletions(-) diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index c8c2f67fb..6125c87d3 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -10,7 +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.movingboundary import MovingBoundaryHeatExchanger # 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 index ae32ddcd5..21a6d1b96 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -11,33 +11,40 @@ SPDX-License-Identifier: MIT """ import math +import numpy as np 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 h_mix_pQ +from tespy.tools.fluid_properties import single_fluid 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): +class MovingBoundaryHeatExchanger(HeatExchanger): @staticmethod def component(): - return 'moving boundary condenser' + return 'moving boundary heat exchanger' 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), + 'U_gas_gas': dc_cp(min_val=0), + 'U_gas_twophase': dc_cp(min_val=0), + 'U_gas_liquid': dc_cp(min_val=0), + 'U_liquid_gas': dc_cp(min_val=0), + 'U_liquid_twophase': dc_cp(min_val=0), + 'U_liquid_liquid': dc_cp(min_val=0), + 'U_twophase_gas': dc_cp(min_val=0), + 'U_twophase_twophase': dc_cp(min_val=0), + 'U_twophase_liquid': 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 - ), + # '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 ), @@ -48,56 +55,99 @@ def get_parameters(self): }) 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 + @staticmethod + def _get_h_steps(c1, c2): + """Get the steps for enthalpy for a change of state from one connection + to another Parameters ---------- - get_U_values : boolean - Also return the U values for the sections of the heat exchanger. + c1 : tespy.connections.connection.Connection + Inlet connection. + + c2 : tespy.connections.connection.Connection + Outlet connection. Returns ------- - tuple - U values in the heat exchange sections and boundary hot side - enthalpies + list + Steps of enthalpy of the specified connections """ - 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] + if c1.fluid_data != c2.fluid_data: + msg = "Both connections need to utilize the same fluid data." + raise ValueError(msg) + + if c1.p.val_SI != c2.p.val_SI: + msg = ( + "This method assumes equality of pressure for the inlet and " + "the outlet connection. The pressure values provided are not " + "equal, the results may be incorrect." + ) + # change the order of connections to have c1 as the lower enthalpy + # connection (enthalpy will be rising in the list) + if c1.h.val_SI > c2.h.val_SI: + c1, c2 = c2, c1 + + h_at_steps = [c1.h.val_SI, c2.h.val_SI] + fluid = single_fluid(c1.fluid_data) + # this should be generalized to "supports two-phase" + is_pure_fluid = fluid is not None + + if is_pure_fluid: + try: + h_sat_gas = h_mix_pQ(c1.p.val_SI, 1, c1.fluid_data) + h_sat_liquid = h_mix_pQ(c1.p.val_SI, 0, c1.fluid_data) + except (ValueError, NotImplementedError): + return h_at_steps + + if c1.h.val_SI < h_sat_liquid: + if c2.h.val_SI > h_sat_gas: + h_at_steps = [c1.h.val_SI, h_sat_liquid, h_sat_gas, c2.h.val_SI] + elif c2.h.val_SI > h_sat_liquid: + h_at_steps = [c1.h.val_SI, h_sat_liquid, c2.h.val_SI] + + elif c1.h.val_SI < h_sat_gas: + if c2.h.val_SI > h_sat_gas: + h_at_steps = [c1.h.val_SI, h_sat_gas, c2.h.val_SI] + + return h_at_steps - else: - h_at_steps_1 = [i1.h.val_SI, o1.h.val_SI] + @staticmethod + def _get_Q_sections(h_at_steps, mass_flow): + return [ + (h_at_steps[i + 1] - h_at_steps[i]) * mass_flow + for i in range(len(h_at_steps) - 1) + ] + + def _assign_sections(self): + h_steps_hot = self._get_h_steps(self.inl[0], self.outl[0]) + Q_sections_hot = self._get_Q_sections(h_steps_hot, self.inl[0].m.val_SI) + Q_sections_hot = np.cumsum(Q_sections_hot).round(6).tolist() - 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] + h_steps_cold = self._get_h_steps(self.inl[1], self.outl[1]) + Q_sections_cold = self._get_Q_sections(h_steps_cold, self.inl[1].m.val_SI) + Q_sections_cold = np.cumsum(Q_sections_cold).round(6).tolist() - return U_in_sections, h_at_steps_1 + all_sections = [Q for Q in Q_sections_hot + Q_sections_cold + [0.0]] + return sorted(list(set(all_sections))) - def calc_td_log_and_Q_in_sections(self, h_at_steps_1): + def _get_T_at_steps(self, Q_sections): + # now put the Q_sections back on the h_steps on both sides + h_steps_hot = [self.inl[0].h.val_SI - Q / self.inl[0].m.val_SI for Q in Q_sections] + h_steps_cold = [self.inl[1].h.val_SI + Q / self.inl[1].m.val_SI for Q in Q_sections] + + T_steps_hot = [ + T_mix_ph(self.inl[0].p.val_SI, h, self.inl[0].fluid_data, self.inl[0].mixing_rule) + for h in h_steps_hot + ] + T_steps_cold = [ + T_mix_ph(self.inl[1].p.val_SI, h, self.inl[1].fluid_data, self.inl[1].mixing_rule) + for h in h_steps_cold + ] + return T_steps_hot, T_steps_cold + + @staticmethod + def _calc_UA_in_sections(T_steps_hot, T_steps_cold, Q_sections): """Calculate logarithmic temperature difference and heat exchange in heat exchanger sections. @@ -112,61 +162,32 @@ def calc_td_log_and_Q_in_sections(self, h_at_steps_1): 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]) + T_hot - T_cold + for T_hot, T_cold in zip(T_steps_hot, T_steps_cold[::-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) + if td_at_steps[i + 1] != td_at_steps[i] else td_at_steps[i + 1] + for i in range(len(Q_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) + for Q, td_log in zip(Q_sections, td_log_in_sections) ] - return UA_in_sections + def calc_UA(self): + Q_sections = self._assign_sections() + T_steps_hot, T_steps_cold = self._get_T_at_steps(Q_sections) + Q_per_section = np.diff(Q_sections) + UA_sections = self._calc_UA_in_sections(T_steps_hot, T_steps_cold, Q_per_section) + return sum(UA_sections) + def UA_func(self, **kwargs): r""" Calculate heat transfer from heat transfer coefficients for @@ -320,27 +341,29 @@ def td_pinch_deriv(self, increment_filter, k): 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() + UA = self.calc_UA() + print(UA) + # 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 index a200701d6..f60c1b68b 100644 --- a/tests/test_components/test_partload_model_movingboundarycondenser.py +++ b/tests/test_components/test_partload_model_movingboundarycondenser.py @@ -1,4 +1,4 @@ -from tespy.components import HeatExchanger, Source, Sink, Compressor, MovingBoundaryCondenser +from tespy.components import HeatExchanger, Source, Sink, Compressor, MovingBoundaryHeatExchanger from tespy.connections import Connection from tespy.networks import Network import numpy as np @@ -12,7 +12,7 @@ # multiple-boundary heat exchanger # allow for U value change as function of volumetric flow/mass flow/... -cd = MovingBoundaryCondenser("Condenser") +cd = MovingBoundaryHeatExchanger("Condenser") cp = Compressor("compressor") si1 = Sink("cw sink") @@ -22,139 +22,27 @@ 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") +# c10 = Connection(so2, "out1", cp, "in1", label="10") +c11 = Connection(so2, "out1", cd, "in1", label="11") c12 = Connection(cd, "out1", si2, "in1", label="12") -nw.add_conns(c1, c2, c10, c11, c12) +nw.add_conns(c1, c2, 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) +# c10.set_attr(T=20, x=1) +c11.set_attr(fluid={"NH3": 1}, Td_bp=5, p=46) +c12.set_attr(Td_bp=-5, p=46) -c1.set_attr(fluid={"INCOMP::Water": 1}, m=10, T=70, p=1, h0=1e5) -c2.set_attr(h0=1e5, T=80) +c1.set_attr(fluid={"Water": 1}, m=1, p=0.3, Td_bp=-5, h0=1e5) +c2.set_attr(Td_bp=5, p=0.3) -cd.set_attr(U_desup=4000, U_cond=16000, U_subcool=5000) +# cd.set_attr(U_desup=4000, U_cond=16000, U_subcool=5000) +nw.set_attr(iterinfo=False) 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) +for Td_bp in np.linspace(5, 0.2): + c11.set_attr(Td_bp=Td_bp) + c12.set_attr(Td_bp=-Td_bp) 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 From 99caa3a3d95205010fd91c637a632dfd042ea410 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 22 Jan 2025 21:05:21 +0100 Subject: [PATCH 19/55] Add some documentation and propagate approach to minimum pinch function --- .../heat_exchangers/movingboundary.py | 94 +++++++++++++------ 1 file changed, 65 insertions(+), 29 deletions(-) diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index 21a6d1b96..86857a639 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -90,7 +90,8 @@ def _get_h_steps(c1, c2): h_at_steps = [c1.h.val_SI, c2.h.val_SI] fluid = single_fluid(c1.fluid_data) - # this should be generalized to "supports two-phase" + # this should be generalized to "supports two-phase" because it does + # not work with incompressibles is_pure_fluid = fluid is not None if is_pure_fluid: @@ -114,12 +115,35 @@ def _get_h_steps(c1, c2): @staticmethod def _get_Q_sections(h_at_steps, mass_flow): + """Calculate the heat exchange of every section given steps of + enthalpy and mass flow. + + Parameters + ---------- + h_at_steps : list + Enthalpy values at sections (inlet, phase change points, outlet) + mass_flow : float + Mass flow value + + Returns + ------- + float + Heat exchanged between defined steps of enthalpy. + """ return [ (h_at_steps[i + 1] - h_at_steps[i]) * mass_flow for i in range(len(h_at_steps) - 1) ] def _assign_sections(self): + """Assign the sections of the heat exchanger + + Returns + ------- + list + List of cumulative sum of heat exchanged defining the heat exchanger + sections. + """ h_steps_hot = self._get_h_steps(self.inl[0], self.outl[0]) Q_sections_hot = self._get_Q_sections(h_steps_hot, self.inl[0].m.val_SI) Q_sections_hot = np.cumsum(Q_sections_hot).round(6).tolist() @@ -132,6 +156,19 @@ def _assign_sections(self): return sorted(list(set(all_sections))) def _get_T_at_steps(self, Q_sections): + """Calculate the temperature values for the provided sections. + + Parameters + ---------- + Q_sections : list + Cumulative heat exchanged from the hot side to the colde side + defining the sections of the heat exchanger. + + Returns + ------- + tuple + Lists of cold side and hot side temperature + """ # now put the Q_sections back on the h_steps on both sides h_steps_hot = [self.inl[0].h.val_SI - Q / self.inl[0].m.val_SI for Q in Q_sections] h_steps_cold = [self.inl[1].h.val_SI + Q / self.inl[1].m.val_SI for Q in Q_sections] @@ -148,19 +185,23 @@ def _get_T_at_steps(self, Q_sections): @staticmethod def _calc_UA_in_sections(T_steps_hot, T_steps_cold, Q_sections): - """Calculate logarithmic temperature difference and heat exchange in - heat exchanger sections. + """Calculate the UA values per section of heat exchanged. Parameters ---------- - h_at_steps_1 : list - Enthalpy values at boundaries of sections. + T_steps_hot : list + Temperature hot side at beginning and end of sections. + + T_steps_cold : list + Temperature cold side at beginning and end of sections. + + Q_sections : list + Heat transferred in each section. Returns ------- - tuple - Lists of logarithmic temperature difference and heat exchange in - the heat exchanger sections starting from hot side inlet. + list + Lists of UA values per section of heat exchanged. """ # counter flow version td_at_steps = [ @@ -182,6 +223,13 @@ def _calc_UA_in_sections(T_steps_hot, T_steps_cold, Q_sections): return UA_in_sections def calc_UA(self): + """Calculate the sum of UA for all sections in the heat exchanger + + Returns + ------- + float + Sum of UA values of all heat exchanger sections. + """ Q_sections = self._assign_sections() T_steps_hot, T_steps_cold = self._get_T_at_steps(Q_sections) Q_per_section = np.diff(Q_sections) @@ -198,8 +246,7 @@ def UA_func(self, **kwargs): residual : float Residual value of equation. """ - UA_in_sections = self.calc_UA_in_sections() - return self.UA.val - sum(UA_in_sections) + return self.UA.val - self.calc_UA() def UA_deriv(self, increment_filter, k): r""" @@ -276,25 +323,14 @@ def calc_td_pinch(self): 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 + Q_sections = self._assign_sections() + T_steps_hot, T_steps_cold = self._get_T_at_steps(Q_sections) - else: - o2 = self.outl[1] - return o1.calc_T_sat() - o2.calc_T() + td_at_steps = [ + T_hot - T_cold + for T_hot, T_cold in zip(T_steps_hot, T_steps_cold[::-1]) + ] + return min(td_at_steps) def td_pinch_func(self): r""" @@ -330,7 +366,7 @@ def td_pinch_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ f = self.td_pinch_func - for c in [self.outl[0], self.inl[1]]: + for c in self.inl + self.outl: 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): From 77bedeae844b364fc36170f4c7851b497601cec0 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 22 Jan 2025 21:10:46 +0100 Subject: [PATCH 20/55] Update changelog --- docs/whats_new.rst | 1 + docs/whats_new/v0-7-9.rst | 14 ++++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 docs/whats_new/v0-7-9.rst diff --git a/docs/whats_new.rst b/docs/whats_new.rst index 047fddcaa..74b68deeb 100644 --- a/docs/whats_new.rst +++ b/docs/whats_new.rst @@ -3,6 +3,7 @@ What's New Discover notable new features and improvements in each release +.. include:: whats_new/v0-7-9.rst .. include:: whats_new/v0-7-8-002.rst .. include:: whats_new/v0-7-8-001.rst .. include:: whats_new/v0-7-8.rst diff --git a/docs/whats_new/v0-7-9.rst b/docs/whats_new/v0-7-9.rst new file mode 100644 index 000000000..b6f6df858 --- /dev/null +++ b/docs/whats_new/v0-7-9.rst @@ -0,0 +1,14 @@ +v0.7.9 - Under development +++++++++++++++++++++++++++ + +New Features +############ +- Implement a new property for connections to report the phase of the fluid, + i.e. :code:`"l"` for liquid, :code:`"tp"` for two-phase and :code:`"g"` for + gaseous. The phase is only reported in subcritical pressure + (`PR #592 `__). + +Contributors +############ +- `@tlmerbecks `__ +- Francesco Witte (`@fwitte `__) From 0e213ae5266d51a01a62d91b727a58d95b0db49d Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 22 Jan 2025 21:11:33 +0100 Subject: [PATCH 21/55] Bump version --- pyproject.toml | 2 +- src/tespy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ceda28881..75a578348 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ exclude = ["docs/_build"] [project] name = "tespy" -version = "0.7.8.post2" +version = "0.7.9.dev0" description = "Thermal Engineering Systems in Python (TESPy)" readme = "README.rst" authors = [ diff --git a/src/tespy/__init__.py b/src/tespy/__init__.py index 85fb30466..70ade2f64 100644 --- a/src/tespy/__init__.py +++ b/src/tespy/__init__.py @@ -3,7 +3,7 @@ import os __datapath__ = os.path.join(importlib.resources.files("tespy"), "data") -__version__ = '0.7.8.post2 - Newton\'s Nature' +__version__ = '0.7.9.dev0 - Newton\'s Nature' # tespy data and connections import from . import connections # noqa: F401 From 4f5cadd4316dedd030786329d80e124cd2207eb4 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 22 Jan 2025 21:13:08 +0100 Subject: [PATCH 22/55] Remove unused commented section --- src/tespy/tools/fluid_properties/wrappers.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/tespy/tools/fluid_properties/wrappers.py b/src/tespy/tools/fluid_properties/wrappers.py index 90e16b411..af5a3b231 100644 --- a/src/tespy/tools/fluid_properties/wrappers.py +++ b/src/tespy/tools/fluid_properties/wrappers.py @@ -233,15 +233,6 @@ def Q_ph(self, p, h): self.AS.update(CP.HmassP_INPUTS, h, p) return self.AS.Q() - # if self.AS.phase() == CP.iphase_twophase: - # return self.AS.Q() - # elif self.AS.phase() == CP.iphase_liquid: - # return 0 - # elif self.AS.phase() == CP.iphase_gas: - # return 1 - # else: # all other phases - though this should be unreachable as p is sub-critical - # return -1 - def phase_ph(self, p, h): p = self._make_p_subcritical(p) self.AS.update(CP.HmassP_INPUTS, h, p) From 421df3e61f82294a7f1db38d003ed82935b5d28d Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Fri, 24 Jan 2025 21:54:30 +0100 Subject: [PATCH 23/55] adding a steam turbine (with wet expansion) component --- .../components/turbomachinery/turbine.py | 222 ++++++++++++++++++ 1 file changed, 222 insertions(+) diff --git a/src/tespy/components/turbomachinery/turbine.py b/src/tespy/components/turbomachinery/turbine.py index ff1b8c494..052dd4e9d 100644 --- a/src/tespy/components/turbomachinery/turbine.py +++ b/src/tespy/components/turbomachinery/turbine.py @@ -11,6 +11,8 @@ """ import numpy as np +from scipy.optimize import brentq + from tespy.components.component import component_registry from tespy.components.turbomachinery.base import Turbomachine @@ -20,6 +22,7 @@ from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import isentropic +from tespy.tools.fluid_properties import h_mix_pQ @component_registry @@ -520,6 +523,8 @@ def calc_parameters(self): ) ) + self.pr.val = self.outl[0].p.val_SI / self.inl[0].p.val_SI + def exergy_balance(self, T0): r""" Calculate exergy balance of a turbine. @@ -576,3 +581,220 @@ def exergy_balance(self, T0): self.E_bus = {"chemical": 0, "physical": 0, "massless": -self.P.val} self.E_D = self.E_F - self.E_P self.epsilon = self._calc_epsilon() + +@component_registry +class SteamTurbine(Turbine): + r""" + Class for steam turbines with wet expansion. + + **Mandatory Equations** + + - :py:meth:`tespy.components.component.Component.fluid_func` + - :py:meth:`tespy.components.component.Component.mass_flow_func` + + **Optional Equations** + + - :py:meth:`tespy.components.component.Component.pr_func` + - :py:meth:`tespy.components.turbomachinery.base.Turbomachine.energy_balance_func` + - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_dry_s_func` + - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_s_func` + - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_s_char_func` + - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.cone_func` + + Inlets/Outlets + + - in1 + - out1 + + Image + + .. image:: /api/_images/Turbine.svg + :alt: flowsheet of the turbine + :align: center + :class: only-light + + .. image:: /api/_images/Turbine_darkmode.svg + :alt: flowsheet of the turbine + :align: center + :class: only-dark + + Parameters + ---------- + label : str + The label of the component. + + design : list + List containing design parameters (stated as String). + + offdesign : list + List containing offdesign parameters (stated as String). + + design_path : str + Path to the components design case. + + local_offdesign : boolean + Treat this component in offdesign mode in a design calculation. + + local_design : boolean + Treat this component in design mode in an offdesign calculation. + + char_warnings : boolean + Ignore warnings on default characteristics usage for this component. + + printout : boolean + Include this component in the network's results printout. + + P : float, dict + Power, :math:`P/\text{W}` + + eta_s : float, dict + Isentropic efficiency, :math:`\eta_s/1` + + pr : float, dict, :code:`"var"` + Outlet to inlet pressure ratio, :math:`pr/1` + + eta_s_char : tespy.tools.characteristics.CharLine, dict + Characteristic curve for isentropic efficiency, provide CharLine as + function :code:`func`. + + cone : dict + Apply Stodola's cone law (works in offdesign only). + + Example + ------- + A steam turbine expands 10 kg/s of superheated steam at 550 °C and 110 bar + to 0,5 bar at the outlet. For example, it is possible to calulate the power + output and vapour content at the outlet for a given isentropic efficiency. + + >>> from tespy.components import Sink, Source, Turbine + >>> from tespy.connections import Connection + >>> from tespy.networks import Network + >>> from tespy.tools import ComponentCharacteristics as dc_cc + >>> import shutil + >>> nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) + >>> si = Sink('sink') + >>> so = Source('source') + >>> t = Turbine('turbine') + >>> t.component() + 'turbine' + >>> inc = Connection(so, 'out1', t, 'in1') + >>> outg = Connection(t, 'out1', si, 'in1') + >>> nw.add_conns(inc, outg) + + In design conditions the isentropic efficiency is specified. For offdesign + a characteristic function will be applied, together with Stodola's cone + law coupling the turbine mass flow to inlet pressure. + + >>> t.set_attr(eta_s=0.9, design=['eta_s'], + ... offdesign=['eta_s_char', 'cone']) + >>> inc.set_attr(fluid={'water': 1}, m=10, T=550, p=110, design=['p']) + >>> outg.set_attr(p=0.5) + >>> nw.solve('design') + >>> nw.save('tmp') + >>> round(t.P.val, 0) + -10452574.0 + >>> round(outg.x.val, 3) + 0.914 + >>> inc.set_attr(m=8) + >>> nw.solve('offdesign', design_path='tmp') + >>> round(t.eta_s.val, 3) + 0.898 + >>> round(inc.p.val, 1) + 88.6 + >>> shutil.rmtree('./tmp', ignore_errors=True) + """ + + + @staticmethod + def component(): + return 'steam turbine' + + def get_parameters(self): + + params = super().get_parameters() + + params["alpha"] = dc_cp( + min_val=0.4, max_val=2.5, num_eq=0) + params["eta_dry_s"] = dc_cp( + min_val=0, max_val=1, num_eq=1, + func=self.eta_dry_s_func, + deriv=self.eta_dry_s_deriv + ) + + return params + + def eta_dry_s_func(self): + + inl = self.inl[0] + outl = self.outl[0] + + state = inl.calc_phase() + if state == "tp": # two-phase or saturated vapour + + ym = 1 - (inl.calc_x() + outl.calc_x()) / 2 # average wetness + self.eta_s.val = self.eta_dry.val * (1 - self.alpha.val * ym) + + return self.eta_s_func() + + else: # superheated vapour + dp = inl.p.val_SI - outl.p.val_SI + + # compute the pressure and enthalpy at which the expansion enters the vapour dome + def find_sat(frac): + + psat = inl.p.val_SI - frac * dp + + # calculate enthalpy under dry expansion to psat + hout_isen = isentropic( + inl.p.val_SI, + inl.h.val_SI, + psat, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) + hout = inl.h.val_SI - self.eta_dry_s.val * (inl.h.val_SI - hout_isen) + + # calculate enthalpy of saturated vapour at psat + hsat = h_mix_pQ(psat, 1, inl.fluid_data) + + return hout - hsat + frac = brentq(find_sat, 1, 0) + psat = inl.p.val_SI - frac * dp + hsat = h_mix_pQ(psat, 1, inl.fluid_data) + + # calculate the isentropic efficiency for wet expansion + ym = 1 - (1.0 + outl.calc_x()) / 2 # average wetness + eta_s = self.eta_dry_s.val * (1 - self.alpha.val * ym) + + # calculate the final outlet enthalpy + hout_isen = isentropic( + psat, + hsat, + outl.p.val_SI, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) + hout = hsat - eta_s * (hsat- hout_isen) + + # calculate the difference in enthalpy + dh_isen = hout - inl.h.val_SI + dh = outl.h.val_SI - inl.h.val_SI + + # return residual + return -dh + dh_isen + + def eta_dry_s_deriv(self, increment_filter, k): + + f = self.eta_dry_s_func + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, "p", i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, "p", o) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, "h", i) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, "h", o) From 7646dab2fbe64797b4f1950740b039556bfa61a3 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Fri, 24 Jan 2025 21:57:06 +0100 Subject: [PATCH 24/55] forcing CoolProp to return a meaningful vapour quality for subcooled or superheated states --- src/tespy/tools/fluid_properties/wrappers.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/tespy/tools/fluid_properties/wrappers.py b/src/tespy/tools/fluid_properties/wrappers.py index 90e16b411..fd1d7274e 100644 --- a/src/tespy/tools/fluid_properties/wrappers.py +++ b/src/tespy/tools/fluid_properties/wrappers.py @@ -231,16 +231,15 @@ def p_sat(self, T): def Q_ph(self, p, h): p = self._make_p_subcritical(p) self.AS.update(CP.HmassP_INPUTS, h, p) - return self.AS.Q() - - # if self.AS.phase() == CP.iphase_twophase: - # return self.AS.Q() - # elif self.AS.phase() == CP.iphase_liquid: - # return 0 - # elif self.AS.phase() == CP.iphase_gas: - # return 1 - # else: # all other phases - though this should be unreachable as p is sub-critical - # return -1 + + if self.AS.phase() == CP.iphase_twophase: + return self.AS.Q() + elif self.AS.phase() == CP.iphase_liquid: + return 0 + elif self.AS.phase() == CP.iphase_gas: + return 1 + else: # all other phases - though this should be unreachable as p is sub-critical + return -1 def phase_ph(self, p, h): p = self._make_p_subcritical(p) From cf15c321a88b295b6eba61bc21b68d75d807081f Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Fri, 24 Jan 2025 21:57:45 +0100 Subject: [PATCH 25/55] adding the steam turbine component to the component module init --- src/tespy/components/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index d459df9af..3356130a3 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -26,4 +26,5 @@ from .subsystem import Subsystem # noqa: F401 from .turbomachinery.compressor import Compressor # noqa: F401 from .turbomachinery.pump import Pump # noqa: F401 -from .turbomachinery.turbine import Turbine # noqa: F401 +from .turbomachinery.turbine import Turbine # noqa: F401 +from .turbomachinery.turbine import SteamTurbine From 39f8be2f820994ebb5188da8a858036725358b84 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 26 Jan 2025 11:02:01 +0100 Subject: [PATCH 26/55] adding the steam turbine component to the component module init --- src/tespy/components/__init__.py | 2 +- .../turbomachinery/steam_turbine.py | 237 ++++++++++++++++++ .../components/turbomachinery/turbine.py | 222 +--------------- 3 files changed, 242 insertions(+), 219 deletions(-) create mode 100644 src/tespy/components/turbomachinery/steam_turbine.py diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index 3356130a3..2912ccbd4 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -26,5 +26,5 @@ from .subsystem import Subsystem # noqa: F401 from .turbomachinery.compressor import Compressor # noqa: F401 from .turbomachinery.pump import Pump # noqa: F401 +from .turbomachinery.steam_turbine import SteamTurbine from .turbomachinery.turbine import Turbine # noqa: F401 -from .turbomachinery.turbine import SteamTurbine diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py new file mode 100644 index 000000000..b3ca1f941 --- /dev/null +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -0,0 +1,237 @@ +# -*- coding: utf-8 + +"""Module of class Turbine. + + +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/turbomachinery/turbine.py + +SPDX-License-Identifier: MIT +""" + +from scipy.optimize import brentq + + +from tespy.components.component import component_registry +from tespy.components.turbomachinery.turbine import Turbine +from tespy.tools.data_containers import ComponentProperties as dc_cp +from tespy.tools.fluid_properties import isentropic +from tespy.tools.fluid_properties import h_mix_pQ + + +@component_registry +class SteamTurbine(Turbine): + r""" + Class for steam turbines with wet expansion. + + **Mandatory Equations** + + - :py:meth:`tespy.components.component.Component.fluid_func` + - :py:meth:`tespy.components.component.Component.mass_flow_func` + + **Optional Equations** + + - :py:meth:`tespy.components.component.Component.pr_func` + - :py:meth:`tespy.components.turbomachinery.base.Turbomachine.energy_balance_func` + - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_dry_s_func` + - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_s_func` + - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_s_char_func` + - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.cone_func` + + Inlets/Outlets + + - in1 + - out1 + + Image + + .. image:: /api/_images/Turbine.svg + :alt: flowsheet of the turbine + :align: center + :class: only-light + + .. image:: /api/_images/Turbine_darkmode.svg + :alt: flowsheet of the turbine + :align: center + :class: only-dark + + Parameters + ---------- + label : str + The label of the component. + + design : list + List containing design parameters (stated as String). + + offdesign : list + List containing offdesign parameters (stated as String). + + design_path : str + Path to the components design case. + + local_offdesign : boolean + Treat this component in offdesign mode in a design calculation. + + local_design : boolean + Treat this component in design mode in an offdesign calculation. + + char_warnings : boolean + Ignore warnings on default characteristics usage for this component. + + printout : boolean + Include this component in the network's results printout. + + P : float, dict + Power, :math:`P/\text{W}` + + eta_s : float, dict + Isentropic efficiency, :math:`\eta_s/1` + + eta_dry_s : float, dict + Dry isentropic efficiency, :math:`\eta_s/1` + + pr : float, dict, :code:`"var"` + Outlet to inlet pressure ratio, :math:`pr/1` + + eta_s_char : tespy.tools.characteristics.CharLine, dict + Characteristic curve for isentropic efficiency, provide CharLine as + function :code:`func`. + + cone : dict + Apply Stodola's cone law (works in offdesign only). + + Example + ------- + A steam turbine expands 10 kg/s of superheated steam at 550 °C and 110 bar + to 0,5 bar at the outlet. For example, it is possible to calulate the power + output and vapour content at the outlet for a given isentropic efficiency. + + >>> from tespy.components import Sink, Source, Turbine + >>> from tespy.connections import Connection + >>> from tespy.networks import Network + >>> from tespy.tools import ComponentCharacteristics as dc_cc + >>> import shutil + >>> nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) + >>> si = Sink('sink') + >>> so = Source('source') + >>> st = SteamTurbine('steam turbine') + >>> st.component() + 'turbine' + >>> inc = Connection(so, 'out1', st, 'in1') + >>> outg = Connection(st, 'out1', si, 'in1') + >>> nw.add_conns(inc, outg) + + In design conditions the isentropic efficiency is specified. + >>> st.set_attr(eta_s=0.9) + >>> inc.set_attr(fluid={'water': 1}, m=10, T=250, p=20) + >>> outg.set_attr(p=0.1) + >>> nw.solve('design') + >>> round(st.P.val, 0) + -7471296.0 + >>> round(outg.x.val, 3) + 0.821 + + To capture the effect of liquid drop-out on the isentropic efficiency, the dry turbine efficiency is specified + >>> st.set_attr(eta_s=None) + >>> st.set_attr(eta_dry_s=0.9) + >>> nw.solve('design') + >>> round(st.P.val, 0) + -7009682.0 + >>> round(outg.x.val, 3) + 0.840 + """ + + + @staticmethod + def component(): + return 'steam turbine' + + def get_parameters(self): + + params = super().get_parameters() + + params["alpha"] = dc_cp( + min_val=0.4, max_val=2.5, num_eq=0) + params["eta_dry_s"] = dc_cp( + min_val=0, max_val=1, num_eq=1, + func=self.eta_dry_s_func, + deriv=self.eta_dry_s_deriv + ) + + return params + + def eta_dry_s_func(self): + + inl = self.inl[0] + outl = self.outl[0] + + state = inl.calc_phase() + if state == "tp": # two-phase or saturated vapour + + ym = 1 - (inl.calc_x() + outl.calc_x()) / 2 # average wetness + self.eta_s.val = self.eta_dry.val * (1 - self.alpha.val * ym) + + return self.eta_s_func() + + else: # superheated vapour + dp = inl.p.val_SI - outl.p.val_SI + + # compute the pressure and enthalpy at which the expansion enters the vapour dome + def find_sat(frac): + psat = inl.p.val_SI - frac * dp + + # calculate enthalpy under dry expansion to psat + hout_isen = isentropic( + inl.p.val_SI, + inl.h.val_SI, + psat, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) + hout = inl.h.val_SI - self.eta_dry_s.val * (inl.h.val_SI - hout_isen) + + # calculate enthalpy of saturated vapour at psat + hsat = h_mix_pQ(psat, 1, inl.fluid_data) + + return hout - hsat + frac = brentq(find_sat, 1, 0) + psat = inl.p.val_SI - frac * dp + hsat = h_mix_pQ(psat, 1, inl.fluid_data) + + # calculate the isentropic efficiency for wet expansion + ym = 1 - (1.0 + outl.calc_x()) / 2 # average wetness + eta_s = self.eta_dry_s.val * (1 - self.alpha.val * ym) + + # calculate the final outlet enthalpy + hout_isen = isentropic( + psat, + hsat, + outl.p.val_SI, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) + hout = hsat - eta_s * (hsat- hout_isen) + + # calculate the difference in enthalpy + dh_isen = hout - inl.h.val_SI + dh = outl.h.val_SI - inl.h.val_SI + + # return residual + return -dh + dh_isen + + def eta_dry_s_deriv(self, increment_filter, k): + + f = self.eta_dry_s_func + i = self.inl[0] + o = self.outl[0] + if self.is_variable(i.p, increment_filter): + self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, "p", i) + if self.is_variable(o.p, increment_filter): + self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, "p", o) + if self.is_variable(i.h, increment_filter): + self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, "h", i) + if self.is_variable(o.h, increment_filter): + self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, "h", o) diff --git a/src/tespy/components/turbomachinery/turbine.py b/src/tespy/components/turbomachinery/turbine.py index 052dd4e9d..4e5f950f9 100644 --- a/src/tespy/components/turbomachinery/turbine.py +++ b/src/tespy/components/turbomachinery/turbine.py @@ -11,7 +11,6 @@ """ import numpy as np -from scipy.optimize import brentq from tespy.components.component import component_registry @@ -22,7 +21,6 @@ from tespy.tools.data_containers import SimpleDataContainer as dc_simple from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import isentropic -from tespy.tools.fluid_properties import h_mix_pQ @component_registry @@ -30,6 +28,10 @@ class Turbine(Turbomachine): r""" Class for gas or steam turbines. + The component Turbine is the parent class for the components: + + - :py:class:`tespy.components.turbomachinery.steam_turbine.SteamTurbine` + **Mandatory Equations** - :py:meth:`tespy.components.component.Component.fluid_func` @@ -582,219 +584,3 @@ def exergy_balance(self, T0): self.E_D = self.E_F - self.E_P self.epsilon = self._calc_epsilon() -@component_registry -class SteamTurbine(Turbine): - r""" - Class for steam turbines with wet expansion. - - **Mandatory Equations** - - - :py:meth:`tespy.components.component.Component.fluid_func` - - :py:meth:`tespy.components.component.Component.mass_flow_func` - - **Optional Equations** - - - :py:meth:`tespy.components.component.Component.pr_func` - - :py:meth:`tespy.components.turbomachinery.base.Turbomachine.energy_balance_func` - - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_dry_s_func` - - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_s_func` - - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.eta_s_char_func` - - :py:meth:`tespy.components.turbomachinery.turbine.Turbine.cone_func` - - Inlets/Outlets - - - in1 - - out1 - - Image - - .. image:: /api/_images/Turbine.svg - :alt: flowsheet of the turbine - :align: center - :class: only-light - - .. image:: /api/_images/Turbine_darkmode.svg - :alt: flowsheet of the turbine - :align: center - :class: only-dark - - Parameters - ---------- - label : str - The label of the component. - - design : list - List containing design parameters (stated as String). - - offdesign : list - List containing offdesign parameters (stated as String). - - design_path : str - Path to the components design case. - - local_offdesign : boolean - Treat this component in offdesign mode in a design calculation. - - local_design : boolean - Treat this component in design mode in an offdesign calculation. - - char_warnings : boolean - Ignore warnings on default characteristics usage for this component. - - printout : boolean - Include this component in the network's results printout. - - P : float, dict - Power, :math:`P/\text{W}` - - eta_s : float, dict - Isentropic efficiency, :math:`\eta_s/1` - - pr : float, dict, :code:`"var"` - Outlet to inlet pressure ratio, :math:`pr/1` - - eta_s_char : tespy.tools.characteristics.CharLine, dict - Characteristic curve for isentropic efficiency, provide CharLine as - function :code:`func`. - - cone : dict - Apply Stodola's cone law (works in offdesign only). - - Example - ------- - A steam turbine expands 10 kg/s of superheated steam at 550 °C and 110 bar - to 0,5 bar at the outlet. For example, it is possible to calulate the power - output and vapour content at the outlet for a given isentropic efficiency. - - >>> from tespy.components import Sink, Source, Turbine - >>> from tespy.connections import Connection - >>> from tespy.networks import Network - >>> from tespy.tools import ComponentCharacteristics as dc_cc - >>> import shutil - >>> nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) - >>> si = Sink('sink') - >>> so = Source('source') - >>> t = Turbine('turbine') - >>> t.component() - 'turbine' - >>> inc = Connection(so, 'out1', t, 'in1') - >>> outg = Connection(t, 'out1', si, 'in1') - >>> nw.add_conns(inc, outg) - - In design conditions the isentropic efficiency is specified. For offdesign - a characteristic function will be applied, together with Stodola's cone - law coupling the turbine mass flow to inlet pressure. - - >>> t.set_attr(eta_s=0.9, design=['eta_s'], - ... offdesign=['eta_s_char', 'cone']) - >>> inc.set_attr(fluid={'water': 1}, m=10, T=550, p=110, design=['p']) - >>> outg.set_attr(p=0.5) - >>> nw.solve('design') - >>> nw.save('tmp') - >>> round(t.P.val, 0) - -10452574.0 - >>> round(outg.x.val, 3) - 0.914 - >>> inc.set_attr(m=8) - >>> nw.solve('offdesign', design_path='tmp') - >>> round(t.eta_s.val, 3) - 0.898 - >>> round(inc.p.val, 1) - 88.6 - >>> shutil.rmtree('./tmp', ignore_errors=True) - """ - - - @staticmethod - def component(): - return 'steam turbine' - - def get_parameters(self): - - params = super().get_parameters() - - params["alpha"] = dc_cp( - min_val=0.4, max_val=2.5, num_eq=0) - params["eta_dry_s"] = dc_cp( - min_val=0, max_val=1, num_eq=1, - func=self.eta_dry_s_func, - deriv=self.eta_dry_s_deriv - ) - - return params - - def eta_dry_s_func(self): - - inl = self.inl[0] - outl = self.outl[0] - - state = inl.calc_phase() - if state == "tp": # two-phase or saturated vapour - - ym = 1 - (inl.calc_x() + outl.calc_x()) / 2 # average wetness - self.eta_s.val = self.eta_dry.val * (1 - self.alpha.val * ym) - - return self.eta_s_func() - - else: # superheated vapour - dp = inl.p.val_SI - outl.p.val_SI - - # compute the pressure and enthalpy at which the expansion enters the vapour dome - def find_sat(frac): - - psat = inl.p.val_SI - frac * dp - - # calculate enthalpy under dry expansion to psat - hout_isen = isentropic( - inl.p.val_SI, - inl.h.val_SI, - psat, - inl.fluid_data, - inl.mixing_rule, - T0=inl.T.val_SI - ) - hout = inl.h.val_SI - self.eta_dry_s.val * (inl.h.val_SI - hout_isen) - - # calculate enthalpy of saturated vapour at psat - hsat = h_mix_pQ(psat, 1, inl.fluid_data) - - return hout - hsat - frac = brentq(find_sat, 1, 0) - psat = inl.p.val_SI - frac * dp - hsat = h_mix_pQ(psat, 1, inl.fluid_data) - - # calculate the isentropic efficiency for wet expansion - ym = 1 - (1.0 + outl.calc_x()) / 2 # average wetness - eta_s = self.eta_dry_s.val * (1 - self.alpha.val * ym) - - # calculate the final outlet enthalpy - hout_isen = isentropic( - psat, - hsat, - outl.p.val_SI, - inl.fluid_data, - inl.mixing_rule, - T0=inl.T.val_SI - ) - hout = hsat - eta_s * (hsat- hout_isen) - - # calculate the difference in enthalpy - dh_isen = hout - inl.h.val_SI - dh = outl.h.val_SI - inl.h.val_SI - - # return residual - return -dh + dh_isen - - def eta_dry_s_deriv(self, increment_filter, k): - - f = self.eta_dry_s_func - i = self.inl[0] - o = self.outl[0] - if self.is_variable(i.p, increment_filter): - self.jacobian[k, i.p.J_col] = self.numeric_deriv(f, "p", i) - if self.is_variable(o.p, increment_filter): - self.jacobian[k, o.p.J_col] = self.numeric_deriv(f, "p", o) - if self.is_variable(i.h, increment_filter): - self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, "h", i) - if self.is_variable(o.h, increment_filter): - self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, "h", o) From 9c479a7c4795e29bf8826ffd577cefec2ac5ac40 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 26 Jan 2025 13:48:02 +0100 Subject: [PATCH 27/55] adding the steam turbine component to the component module init --- src/tespy/components/__init__.py | 2 +- .../turbomachinery/steam_turbine.py | 37 ++++++++++++++++--- .../components/turbomachinery/turbine.py | 1 - 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index 2912ccbd4..1c7b5dc00 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -26,5 +26,5 @@ from .subsystem import Subsystem # noqa: F401 from .turbomachinery.compressor import Compressor # noqa: F401 from .turbomachinery.pump import Pump # noqa: F401 +from .turbomachinery.turbine import Turbine # noqa: F401 from .turbomachinery.steam_turbine import SteamTurbine -from .turbomachinery.turbine import Turbine # noqa: F401 diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index b3ca1f941..fafa117b6 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -16,6 +16,7 @@ from tespy.components.component import component_registry from tespy.components.turbomachinery.turbine import Turbine from tespy.tools.data_containers import ComponentProperties as dc_cp +from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import isentropic from tespy.tools.fluid_properties import h_mix_pQ @@ -132,7 +133,8 @@ class SteamTurbine(Turbine): >>> round(outg.x.val, 3) 0.821 - To capture the effect of liquid drop-out on the isentropic efficiency, the dry turbine efficiency is specified + To capture the effect of liquid drop-out on the isentropic + efficiency, the dry turbine efficiency is specified >>> st.set_attr(eta_s=None) >>> st.set_attr(eta_dry_s=0.9) >>> nw.solve('design') @@ -142,7 +144,6 @@ class SteamTurbine(Turbine): 0.840 """ - @staticmethod def component(): return 'steam turbine' @@ -156,7 +157,8 @@ def get_parameters(self): params["eta_dry_s"] = dc_cp( min_val=0, max_val=1, num_eq=1, func=self.eta_dry_s_func, - deriv=self.eta_dry_s_deriv + deriv=self.eta_dry_s_deriv, + latex=self.eta_dry_s_func_doc ) return params @@ -177,7 +179,8 @@ def eta_dry_s_func(self): else: # superheated vapour dp = inl.p.val_SI - outl.p.val_SI - # compute the pressure and enthalpy at which the expansion enters the vapour dome + # compute the pressure and enthalpy at which the expansion + # enters the vapour dome def find_sat(frac): psat = inl.p.val_SI - frac * dp @@ -213,7 +216,7 @@ def find_sat(frac): inl.mixing_rule, T0=inl.T.val_SI ) - hout = hsat - eta_s * (hsat- hout_isen) + hout = hsat - eta_s * (hsat - hout_isen) # calculate the difference in enthalpy dh_isen = hout - inl.h.val_SI @@ -235,3 +238,27 @@ def eta_dry_s_deriv(self, increment_filter, k): self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, "h", i) if self.is_variable(o.h, increment_filter): self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, "h", o) + + def eta_dry_s_func_doc(self, label): + r""" + Equation for given dry isentropic efficiency of a turbine. + + Parameters + ---------- + label : str + Label for equation. + + Returns + ------- + latex : str + LaTeX code of equations applied. + """ + latex = ( + r'\begin{split}' + '\n' + r'0 &=-\left(h_\mathrm{out}-h_\mathrm{in}\right)+\left(' + r'h_\mathrm{out,s}-h_\mathrm{in}\right)\cdot\eta_\mathrm{s}\\' + '\n' + r'\eta_\mathrm{s} &=\eta_\mathrm{s}^\mathrm{dry}\cdot(1 - \alpha*y_\mathrm{m})\\' + '\n' + r'y_\mathrm{m} &=\frac{(1-x_\mathrm{in})+(1-x_\mathrm{out})}{2}\\' + '\n' + r'\end{split}' + ) + return generate_latex_eq(self, latex, label) diff --git a/src/tespy/components/turbomachinery/turbine.py b/src/tespy/components/turbomachinery/turbine.py index 4e5f950f9..a73dc8ad8 100644 --- a/src/tespy/components/turbomachinery/turbine.py +++ b/src/tespy/components/turbomachinery/turbine.py @@ -583,4 +583,3 @@ def exergy_balance(self, T0): self.E_bus = {"chemical": 0, "physical": 0, "massless": -self.P.val} self.E_D = self.E_F - self.E_P self.epsilon = self._calc_epsilon() - From 5603db961454bf8162f99fb8980eb50c5aca2314 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 26 Jan 2025 14:11:06 +0100 Subject: [PATCH 28/55] adding doc strings --- .../turbomachinery/steam_turbine.py | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index fafa117b6..4d769f446 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -151,7 +151,6 @@ def component(): def get_parameters(self): params = super().get_parameters() - params["alpha"] = dc_cp( min_val=0.4, max_val=2.5, num_eq=0) params["eta_dry_s"] = dc_cp( @@ -164,6 +163,23 @@ def get_parameters(self): return params def eta_dry_s_func(self): + r""" + Equation for given dry isentropic efficiency of a turbine. + + Returns + ------- + residual : float + Residual value of equation. + + .. math:: + + 0 = -\left( h_{out} - h_{in} \right) + + \left( h_{out,s} - h_{in} \right) \cdot \eta_{s,e} + + \eta_{s,e} = \eta_{s,e}^{dry} \cdot \left( 1 - \alpha \cdot y_m \right) + + y_m = \frac{\left( 1-x_{in}\right)+ \left( 1-x_{out} \right)}{2} + """ inl = self.inl[0] outl = self.outl[0] @@ -227,6 +243,18 @@ def find_sat(frac): def eta_dry_s_deriv(self, increment_filter, k): + r""" + Partial derivatives for dry isentropic efficiency function. + + Parameters + ---------- + increment_filter : ndarray + Matrix for filtering non-changing variables. + + k : int + Position of derivatives in Jacobian matrix (k-th equation). + """ + f = self.eta_dry_s_func i = self.inl[0] o = self.outl[0] @@ -257,8 +285,8 @@ def eta_dry_s_func_doc(self, label): r'\begin{split}' + '\n' r'0 &=-\left(h_\mathrm{out}-h_\mathrm{in}\right)+\left(' r'h_\mathrm{out,s}-h_\mathrm{in}\right)\cdot\eta_\mathrm{s}\\' + '\n' - r'\eta_\mathrm{s} &=\eta_\mathrm{s}^\mathrm{dry}\cdot(1 - \alpha*y_\mathrm{m})\\' + '\n' - r'y_\mathrm{m} &=\frac{(1-x_\mathrm{in})+(1-x_\mathrm{out})}{2}\\' + '\n' + r'\eta_\mathrm{s} &=\eta_\mathrm{s}^\mathrm{dry}\cdot \left( 1 - \alpha \cdot y_\mathrm{m} \right)\\' + '\n' + r'y_\mathrm{m} &=\frac{\left( 1-x_\mathrm{in} \right)+\left( 1-x_\mathrm{out} \right)}{2}\\' + '\n' r'\end{split}' ) return generate_latex_eq(self, latex, label) From 3edfebae7c3ebcbe1b54a4494a0076848c4f1c05 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 26 Jan 2025 14:47:24 +0100 Subject: [PATCH 29/55] refactoring of attribute definition & renaming variables --- .../turbomachinery/steam_turbine.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index 4d769f446..ff8afd68a 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -16,6 +16,7 @@ from tespy.components.component import component_registry from tespy.components.turbomachinery.turbine import Turbine from tespy.tools.data_containers import ComponentProperties as dc_cp +from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import isentropic from tespy.tools.fluid_properties import h_mix_pQ @@ -136,7 +137,7 @@ class SteamTurbine(Turbine): To capture the effect of liquid drop-out on the isentropic efficiency, the dry turbine efficiency is specified >>> st.set_attr(eta_s=None) - >>> st.set_attr(eta_dry_s=0.9) + >>> st.set_attr(eta_dry_s=0.9, alpha=1.0) >>> nw.solve('design') >>> round(st.P.val, 0) -7009682.0 @@ -151,20 +152,19 @@ def component(): def get_parameters(self): params = super().get_parameters() - params["alpha"] = dc_cp( - min_val=0.4, max_val=2.5, num_eq=0) - params["eta_dry_s"] = dc_cp( - min_val=0, max_val=1, num_eq=1, - func=self.eta_dry_s_func, - deriv=self.eta_dry_s_deriv, - latex=self.eta_dry_s_func_doc - ) + params["alpha"] = dc_cp(min_val=0.4, max_val=2.5) + params["eta_s_dry"] = dc_cp(min_val=0.0, max_val=1.0) + params["eta_s_dry_group"] = dc_gcp( + num_eq=1, elements=["alpha", "eta_s_dry"], + func=self.eta_s_wet_func, + deriv=self.eta_s_wet_deriv, + latex=self.eta_s_wet_func_doc) return params - def eta_dry_s_func(self): + def eta_s_wet_func(self): r""" - Equation for given dry isentropic efficiency of a turbine. + Equation for given dry isentropic efficiency of a turbine under wet expansion. Returns ------- @@ -209,7 +209,7 @@ def find_sat(frac): inl.mixing_rule, T0=inl.T.val_SI ) - hout = inl.h.val_SI - self.eta_dry_s.val * (inl.h.val_SI - hout_isen) + hout = inl.h.val_SI - self.eta_s_dry.val * (inl.h.val_SI - hout_isen) # calculate enthalpy of saturated vapour at psat hsat = h_mix_pQ(psat, 1, inl.fluid_data) @@ -221,7 +221,7 @@ def find_sat(frac): # calculate the isentropic efficiency for wet expansion ym = 1 - (1.0 + outl.calc_x()) / 2 # average wetness - eta_s = self.eta_dry_s.val * (1 - self.alpha.val * ym) + eta_s = self.eta_s_dry.val * (1 - self.alpha.val * ym) # calculate the final outlet enthalpy hout_isen = isentropic( @@ -241,7 +241,7 @@ def find_sat(frac): # return residual return -dh + dh_isen - def eta_dry_s_deriv(self, increment_filter, k): + def eta_s_wet_deriv(self, increment_filter, k): r""" Partial derivatives for dry isentropic efficiency function. @@ -255,7 +255,7 @@ def eta_dry_s_deriv(self, increment_filter, k): Position of derivatives in Jacobian matrix (k-th equation). """ - f = self.eta_dry_s_func + f = self.eta_s_wet_func i = self.inl[0] o = self.outl[0] if self.is_variable(i.p, increment_filter): @@ -267,7 +267,7 @@ def eta_dry_s_deriv(self, increment_filter, k): if self.is_variable(o.h, increment_filter): self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, "h", o) - def eta_dry_s_func_doc(self, label): + def eta_s_wet_func_doc(self, label): r""" Equation for given dry isentropic efficiency of a turbine. From e4cf5319654f39cc152fdc29b289cc54eeff7eb5 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 26 Jan 2025 14:59:27 +0100 Subject: [PATCH 30/55] formatting additions --- src/tespy/components/__init__.py | 2 +- src/tespy/components/turbomachinery/steam_turbine.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index 1c7b5dc00..bbbee44b0 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -26,5 +26,5 @@ from .subsystem import Subsystem # noqa: F401 from .turbomachinery.compressor import Compressor # noqa: F401 from .turbomachinery.pump import Pump # noqa: F401 -from .turbomachinery.turbine import Turbine # noqa: F401 from .turbomachinery.steam_turbine import SteamTurbine +from .turbomachinery.turbine import Turbine # noqa: F401 diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index ff8afd68a..b6a09ff52 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -109,11 +109,9 @@ class SteamTurbine(Turbine): to 0,5 bar at the outlet. For example, it is possible to calulate the power output and vapour content at the outlet for a given isentropic efficiency. - >>> from tespy.components import Sink, Source, Turbine + >>> from tespy.components import Sink, Source, SteamTurbine >>> from tespy.connections import Connection >>> from tespy.networks import Network - >>> from tespy.tools import ComponentCharacteristics as dc_cc - >>> import shutil >>> nw = Network(p_unit='bar', T_unit='C', h_unit='kJ / kg', iterinfo=False) >>> si = Sink('sink') >>> so = Source('source') From ec913d65a990dececfb2f6e2ca7afaee90b15559 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 Jan 2025 15:17:31 +0100 Subject: [PATCH 31/55] Fix some precision related issues and ordering of sections --- .../heat_exchangers/movingboundary.py | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index 86857a639..dc746e6e1 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -102,13 +102,13 @@ def _get_h_steps(c1, c2): return h_at_steps if c1.h.val_SI < h_sat_liquid: - if c2.h.val_SI > h_sat_gas: + if c2.h.val_SI > h_sat_gas + ERR: h_at_steps = [c1.h.val_SI, h_sat_liquid, h_sat_gas, c2.h.val_SI] - elif c2.h.val_SI > h_sat_liquid: + elif c2.h.val_SI > h_sat_liquid + ERR: h_at_steps = [c1.h.val_SI, h_sat_liquid, c2.h.val_SI] - elif c1.h.val_SI < h_sat_gas: - if c2.h.val_SI > h_sat_gas: + elif c1.h.val_SI < h_sat_gas - ERR: + if c2.h.val_SI > h_sat_gas + ERR: h_at_steps = [c1.h.val_SI, h_sat_gas, c2.h.val_SI] return h_at_steps @@ -146,14 +146,13 @@ def _assign_sections(self): """ h_steps_hot = self._get_h_steps(self.inl[0], self.outl[0]) Q_sections_hot = self._get_Q_sections(h_steps_hot, self.inl[0].m.val_SI) - Q_sections_hot = np.cumsum(Q_sections_hot).round(6).tolist() + Q_sections_hot = [0.0] + np.cumsum(Q_sections_hot).tolist()[:-1] h_steps_cold = self._get_h_steps(self.inl[1], self.outl[1]) Q_sections_cold = self._get_Q_sections(h_steps_cold, self.inl[1].m.val_SI) - Q_sections_cold = np.cumsum(Q_sections_cold).round(6).tolist() + Q_sections_cold = np.cumsum(Q_sections_cold).tolist() - all_sections = [Q for Q in Q_sections_hot + Q_sections_cold + [0.0]] - return sorted(list(set(all_sections))) + return sorted(Q_sections_cold + Q_sections_hot) def _get_T_at_steps(self, Q_sections): """Calculate the temperature values for the provided sections. @@ -170,9 +169,10 @@ def _get_T_at_steps(self, Q_sections): Lists of cold side and hot side temperature """ # now put the Q_sections back on the h_steps on both sides - h_steps_hot = [self.inl[0].h.val_SI - Q / self.inl[0].m.val_SI for Q in Q_sections] + # Since Q_sections is defined increasing we have to start back from the + # outlet of the hot side + h_steps_hot = [self.outl[0].h.val_SI + Q / self.inl[0].m.val_SI for Q in Q_sections] h_steps_cold = [self.inl[1].h.val_SI + Q / self.inl[1].m.val_SI for Q in Q_sections] - T_steps_hot = [ T_mix_ph(self.inl[0].p.val_SI, h, self.inl[0].fluid_data, self.inl[0].mixing_rule) for h in h_steps_hot @@ -203,17 +203,20 @@ def _calc_UA_in_sections(T_steps_hot, T_steps_cold, Q_sections): list Lists of UA values per section of heat exchanged. """ - # counter flow version + # the temperature ranges both come with increasing values td_at_steps = [ T_hot - T_cold - for T_hot, T_cold in zip(T_steps_hot, T_steps_cold[::-1]) + for T_hot, T_cold in zip(T_steps_hot, T_steps_cold) ] 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]) - if td_at_steps[i + 1] != td_at_steps[i] else td_at_steps[i + 1] + # round is required because tiny differences may cause + # inconsistencies due to rounding errors + if round(td_at_steps[i + 1], 6) != round(td_at_steps[i], 6) + else td_at_steps[i + 1] for i in range(len(Q_sections)) ] UA_in_sections = [ @@ -328,7 +331,7 @@ def calc_td_pinch(self): td_at_steps = [ T_hot - T_cold - for T_hot, T_cold in zip(T_steps_hot, T_steps_cold[::-1]) + for T_hot, T_cold in zip(T_steps_hot, T_steps_cold) ] return min(td_at_steps) @@ -376,9 +379,6 @@ def td_pinch_deriv(self, increment_filter, k): def calc_parameters(self): super().calc_parameters() - - UA = self.calc_UA() - print(UA) # U_sections_specified = all([ # self.get_attr(f"U_{key}").is_set # for key in ["desup", "cond", "subcool"] @@ -401,5 +401,5 @@ def calc_parameters(self): # ]) - 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() + self.UA.val = self.calc_UA() + self.td_pinch.val = self.calc_td_pinch() From 07b986345845560f2782d15ac54f6299eca81f41 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 Jan 2025 15:17:54 +0100 Subject: [PATCH 32/55] Update temporary testscript --- ..._partload_model_movingboundarycondenser.py | 47 ++++++++++++++++--- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/tests/test_components/test_partload_model_movingboundarycondenser.py b/tests/test_components/test_partload_model_movingboundarycondenser.py index f60c1b68b..84840824c 100644 --- a/tests/test_components/test_partload_model_movingboundarycondenser.py +++ b/tests/test_components/test_partload_model_movingboundarycondenser.py @@ -31,18 +31,53 @@ cp.set_attr(eta_s=0.8) # c10.set_attr(T=20, x=1) -c11.set_attr(fluid={"NH3": 1}, Td_bp=5, p=46) -c12.set_attr(Td_bp=-5, p=46) +c11.set_attr(fluid={"NH3": 1}, m=10, Td_bp=40, p=40) +c12.set_attr(x=0, p=40) + +c1.set_attr(fluid={"Water": 1}, p=1, Td_bp=-5, h0=1e5) +c2.set_attr(p=1, Td_bp=20) -c1.set_attr(fluid={"Water": 1}, m=1, p=0.3, Td_bp=-5, h0=1e5) -c2.set_attr(Td_bp=5, p=0.3) # cd.set_attr(U_desup=4000, U_cond=16000, U_subcool=5000) nw.set_attr(iterinfo=False) nw.solve("design") -for Td_bp in np.linspace(5, 0.2): +c11.set_attr(p=None) +c12.set_attr(p=None) +cd.set_attr(td_pinch=5, pr1=1) +nw.solve("design") +c11.set_attr(Td_bp=30.4) +# c12.set_attr(Td_bp=-Td_bp) +nw.solve("design") + +# Q_sections = cd._assign_sections() +# T_steps_hot, T_steps_cold = cd._get_T_at_steps(Q_sections) +# print(cd.UA.val, cd.td_pinch.val, Q_sections, T_steps_hot, T_steps_cold) +# print(cd._calc_UA_in_sections(T_steps_hot, T_steps_cold, np.diff(Q_sections))) + +# c11.set_attr(Td_bp=30.5) +# # c12.set_attr(Td_bp=-Td_bp) +# nw.solve("design") + +# # Q_sections = cd._assign_sections() +# # print(cd.UA.val, cd.td_pinch.val, Q_sections, cd._get_T_at_steps(Q_sections)) + +# c11.set_attr(Td_bp=30.4) +# # c12.set_attr(Td_bp=-Td_bp) +# nw.solve("design") + +# Q_sections2 = cd._assign_sections() +# T_steps_hot2, T_steps_cold2 = cd._get_T_at_steps(Q_sections2) +# print(cd.UA.val, cd.td_pinch.val, Q_sections, T_steps_hot2, T_steps_cold2) +# print(cd._calc_UA_in_sections(T_steps_hot2, T_steps_cold2, np.diff(Q_sections2))) + +# exit() + +for Td_bp in np.linspace(40, 2): c11.set_attr(Td_bp=Td_bp) - c12.set_attr(Td_bp=-Td_bp) + # c12.set_attr(Td_bp=-Td_bp) nw.solve("design") + # print(cd.td_pinch.val, cd.UA.val) + Q_sections = cd._assign_sections() + print(Td_bp, cd.UA.val) From 9c0ef754afdd378b176558e68d819e6533a775e7 Mon Sep 17 00:00:00 2001 From: tlmerbecks Date: Sun, 26 Jan 2025 15:32:20 +0100 Subject: [PATCH 33/55] Correcting testing failures --- src/tespy/components/turbomachinery/steam_turbine.py | 6 +++--- tests/test_components/test_turbomachinery.py | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index b6a09ff52..22586ff95 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -117,7 +117,7 @@ class SteamTurbine(Turbine): >>> so = Source('source') >>> st = SteamTurbine('steam turbine') >>> st.component() - 'turbine' + 'steam turbine' >>> inc = Connection(so, 'out1', st, 'in1') >>> outg = Connection(st, 'out1', si, 'in1') >>> nw.add_conns(inc, outg) @@ -135,12 +135,12 @@ class SteamTurbine(Turbine): To capture the effect of liquid drop-out on the isentropic efficiency, the dry turbine efficiency is specified >>> st.set_attr(eta_s=None) - >>> st.set_attr(eta_dry_s=0.9, alpha=1.0) + >>> st.set_attr(eta_s_dry=0.9, alpha=1.0) >>> nw.solve('design') >>> round(st.P.val, 0) -7009682.0 >>> round(outg.x.val, 3) - 0.840 + 0.84 """ @staticmethod diff --git a/tests/test_components/test_turbomachinery.py b/tests/test_components/test_turbomachinery.py index cb727ac84..0ed70d7ac 100644 --- a/tests/test_components/test_turbomachinery.py +++ b/tests/test_components/test_turbomachinery.py @@ -16,6 +16,7 @@ from tespy.components import Pump from tespy.components import Sink from tespy.components import Source +from tespy.components import SteamTurbine from tespy.components import Turbine from tespy.components.turbomachinery.base import Turbomachine from tespy.connections import Connection @@ -329,6 +330,9 @@ def test_Turbine(self, tmp_path): ') must be (' + str(eta_s) + ').') assert eta_s == round(instance.eta_s.val, 3), msg + def test_SteamTurbine(self, temp_path): + pass + def test_Turbomachine(self): """Test component properties of turbomachines.""" instance = Turbomachine('turbomachine') From 3a930e150b9b4bb09d00a0b95e9d2df35181c82a Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 Jan 2025 15:50:43 +0100 Subject: [PATCH 34/55] Fix a typo and add the calc_eta_s method --- .../turbomachinery/steam_turbine.py | 5 ++- .../components/turbomachinery/turbine.py | 34 ++++++++++--------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index 22586ff95..580e6f8d4 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -186,9 +186,7 @@ def eta_s_wet_func(self): if state == "tp": # two-phase or saturated vapour ym = 1 - (inl.calc_x() + outl.calc_x()) / 2 # average wetness - self.eta_s.val = self.eta_dry.val * (1 - self.alpha.val * ym) - - return self.eta_s_func() + return self.calc_eta_s() - self.eta_s_dry.val * (1 - self.alpha.val * ym) else: # superheated vapour dp = inl.p.val_SI - outl.p.val_SI @@ -213,6 +211,7 @@ def find_sat(frac): hsat = h_mix_pQ(psat, 1, inl.fluid_data) return hout - hsat + frac = brentq(find_sat, 1, 0) psat = inl.p.val_SI - frac * dp hsat = h_mix_pQ(psat, 1, inl.fluid_data) diff --git a/src/tespy/components/turbomachinery/turbine.py b/src/tespy/components/turbomachinery/turbine.py index a73dc8ad8..d7e4a4ac7 100644 --- a/src/tespy/components/turbomachinery/turbine.py +++ b/src/tespy/components/turbomachinery/turbine.py @@ -251,6 +251,22 @@ def eta_s_deriv(self, increment_filter, k): if o.h.is_var and self.it == 0: self.jacobian[k, o.h.J_col] = -1 + def calc_eta_s(self): + inl = self.inl[0] + outl = self.outl[0] + return ( + (outl.h.val_SI - inl.h.val_SI) + / (isentropic( + inl.p.val_SI, + inl.h.val_SI, + outl.p.val_SI, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) - inl.h.val_SI + ) + ) + def cone_func(self): r""" Equation for stodolas cone law. @@ -510,22 +526,8 @@ def calc_parameters(self): inl = self.inl[0] outl = self.outl[0] - self.eta_s.val = ( - (outl.h.val_SI - inl.h.val_SI) - / ( - isentropic( - inl.p.val_SI, - inl.h.val_SI, - outl.p.val_SI, - inl.fluid_data, - inl.mixing_rule, - T0=inl.T.val_SI - ) - - inl.h.val_SI - ) - ) - - self.pr.val = self.outl[0].p.val_SI / self.inl[0].p.val_SI + self.eta_s.val = self.calc_eta_s() + self.pr.val = outl.p.val_SI / inl.p.val_SI def exergy_balance(self, T0): r""" From cc05a7eaa87d857a6461c66981e8a0a9911779ea Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 Jan 2025 15:51:04 +0100 Subject: [PATCH 35/55] Rename to dh_bisectioned --- .../components/turbomachinery/steam_turbine.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index 580e6f8d4..73f78a459 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -222,21 +222,21 @@ def find_sat(frac): # calculate the final outlet enthalpy hout_isen = isentropic( - psat, - hsat, - outl.p.val_SI, - inl.fluid_data, - inl.mixing_rule, - T0=inl.T.val_SI - ) + psat, + hsat, + outl.p.val_SI, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) hout = hsat - eta_s * (hsat - hout_isen) # calculate the difference in enthalpy - dh_isen = hout - inl.h.val_SI + dh_bisectioned = hout - inl.h.val_SI dh = outl.h.val_SI - inl.h.val_SI # return residual - return -dh + dh_isen + return dh - dh_bisectioned def eta_s_wet_deriv(self, increment_filter, k): From 88c249bf9b2d7b6749d5f2a954cc0e0c5f22a087 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 Jan 2025 16:16:56 +0100 Subject: [PATCH 36/55] Make some style/layout changes --- src/tespy/components/__init__.py | 2 +- .../turbomachinery/steam_turbine.py | 77 +++++++++++-------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/src/tespy/components/__init__.py b/src/tespy/components/__init__.py index bbbee44b0..6962f51bd 100644 --- a/src/tespy/components/__init__.py +++ b/src/tespy/components/__init__.py @@ -26,5 +26,5 @@ from .subsystem import Subsystem # noqa: F401 from .turbomachinery.compressor import Compressor # noqa: F401 from .turbomachinery.pump import Pump # noqa: F401 -from .turbomachinery.steam_turbine import SteamTurbine +from .turbomachinery.steam_turbine import SteamTurbine # noqa: F401 from .turbomachinery.turbine import Turbine # noqa: F401 diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index 73f78a459..4d7b7eb56 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -162,23 +162,25 @@ def get_parameters(self): def eta_s_wet_func(self): r""" - Equation for given dry isentropic efficiency of a turbine under wet expansion. + Equation for given dry isentropic efficiency of a turbine under wet + expansion. - Returns - ------- - residual : float - Residual value of equation. - - .. math:: + Returns + ------- + residual : float + Residual value of equation. - 0 = -\left( h_{out} - h_{in} \right) + - \left( h_{out,s} - h_{in} \right) \cdot \eta_{s,e} + .. math:: - \eta_{s,e} = \eta_{s,e}^{dry} \cdot \left( 1 - \alpha \cdot y_m \right) + 0 = -\left( h_{out} - h_{in} \right) + + \left( h_{out,s} - h_{in} \right) \cdot \eta_{s,e} - y_m = \frac{\left( 1-x_{in}\right)+ \left( 1-x_{out} \right)}{2} - """ + \eta_{s,e} = \eta_{s,e}^{dry} \cdot \left( 1 - \alpha + \cdot y_m \right) + y_m = \frac{\left( 1-x_{in}\right)+ \left( 1-x_{out} + \right)}{2} + """ inl = self.inl[0] outl = self.outl[0] @@ -186,7 +188,10 @@ def eta_s_wet_func(self): if state == "tp": # two-phase or saturated vapour ym = 1 - (inl.calc_x() + outl.calc_x()) / 2 # average wetness - return self.calc_eta_s() - self.eta_s_dry.val * (1 - self.alpha.val * ym) + return ( + self.calc_eta_s() + - self.eta_s_dry.val * (1 - self.alpha.val * ym) + ) else: # superheated vapour dp = inl.p.val_SI - outl.p.val_SI @@ -198,14 +203,17 @@ def find_sat(frac): # calculate enthalpy under dry expansion to psat hout_isen = isentropic( - inl.p.val_SI, - inl.h.val_SI, - psat, - inl.fluid_data, - inl.mixing_rule, - T0=inl.T.val_SI - ) - hout = inl.h.val_SI - self.eta_s_dry.val * (inl.h.val_SI - hout_isen) + inl.p.val_SI, + inl.h.val_SI, + psat, + inl.fluid_data, + inl.mixing_rule, + T0=inl.T.val_SI + ) + hout = ( + inl.h.val_SI - self.eta_s_dry.val + * (inl.h.val_SI - hout_isen) + ) # calculate enthalpy of saturated vapour at psat hsat = h_mix_pQ(psat, 1, inl.fluid_data) @@ -239,19 +247,17 @@ def find_sat(frac): return dh - dh_bisectioned def eta_s_wet_deriv(self, increment_filter, k): - r""" - Partial derivatives for dry isentropic efficiency function. - - Parameters - ---------- - increment_filter : ndarray - Matrix for filtering non-changing variables. + Partial derivatives for dry isentropic efficiency function. - k : int - Position of derivatives in Jacobian matrix (k-th equation). - """ + Parameters + ---------- + increment_filter : ndarray + Matrix for filtering non-changing variables. + k : int + Position of derivatives in Jacobian matrix (k-th equation). + """ f = self.eta_s_wet_func i = self.inl[0] o = self.outl[0] @@ -281,9 +287,12 @@ def eta_s_wet_func_doc(self, label): latex = ( r'\begin{split}' + '\n' r'0 &=-\left(h_\mathrm{out}-h_\mathrm{in}\right)+\left(' - r'h_\mathrm{out,s}-h_\mathrm{in}\right)\cdot\eta_\mathrm{s}\\' + '\n' - r'\eta_\mathrm{s} &=\eta_\mathrm{s}^\mathrm{dry}\cdot \left( 1 - \alpha \cdot y_\mathrm{m} \right)\\' + '\n' - r'y_\mathrm{m} &=\frac{\left( 1-x_\mathrm{in} \right)+\left( 1-x_\mathrm{out} \right)}{2}\\' + '\n' + r'h_\mathrm{out,s}-h_\mathrm{in}\right)\cdot\eta_\mathrm{s}\\' + + '\n' + r'\eta_\mathrm{s} &=\eta_\mathrm{s}^\mathrm{dry}\cdot \left( 1 - ' + r'\alpha \cdot y_\mathrm{m} \right)\\' + '\n' + r'y_\mathrm{m} &=\frac{\left( 1-x_\mathrm{in} \right)+\left( ' + r'1-x_\mathrm{out} \right)}{2}\\' + '\n' r'\end{split}' ) return generate_latex_eq(self, latex, label) From 164eb124b65d400858a638c62d2c3bb32a140d31 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 26 Jan 2025 16:18:57 +0100 Subject: [PATCH 37/55] Simplify residual to make it even more understandable --- src/tespy/components/turbomachinery/steam_turbine.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index 4d7b7eb56..827110bee 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -239,12 +239,8 @@ def find_sat(frac): ) hout = hsat - eta_s * (hsat - hout_isen) - # calculate the difference in enthalpy - dh_bisectioned = hout - inl.h.val_SI - dh = outl.h.val_SI - inl.h.val_SI - - # return residual - return dh - dh_bisectioned + # return residual: outlet enthalpy = calculated outlet enthalpy + return outl.h.val_SI - hout def eta_s_wet_deriv(self, increment_filter, k): r""" From 6f8a5df422fd0d3233b9e1eb0cce5c78909f729f Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sat, 8 Feb 2025 16:08:57 +0100 Subject: [PATCH 38/55] Add scipy dependency and a test for the SteamTurbine --- pyproject.toml | 1 + tests/test_components/test_turbomachinery.py | 92 +++++++++++++------- 2 files changed, 63 insertions(+), 30 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 75a578348..16adfda92 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ dependencies = [ "matplotlib>=3.2.1", "numpy>=1.13.3", "pandas>=1.3.0", + "scipy", "tabulate>=0.8.2", ] license = {text = "MIT"} diff --git a/tests/test_components/test_turbomachinery.py b/tests/test_components/test_turbomachinery.py index 0ed70d7ac..209500dbf 100644 --- a/tests/test_components/test_turbomachinery.py +++ b/tests/test_components/test_turbomachinery.py @@ -258,12 +258,22 @@ def test_Turbine(self, tmp_path): # design value of isentropic efficiency eta_s_d = ( - (self.c2.h.val_SI - self.c1.h.val_SI) / ( - isentropic(self.c1.p.val_SI, self.c1.h.val_SI, self.c2.p.val_SI, self.c1.fluid_data, self.c1.mixing_rule) - - self.c1.h.val_SI)) - msg = ('Value of isentropic efficiency must be ' + - str(round(eta_s_d, 3)) + ', is ' + str(instance.eta_s.val) + - '.') + (self.c2.h.val_SI - self.c1.h.val_SI) + / ( + isentropic( + self.c1.p.val_SI, + self.c1.h.val_SI, + self.c2.p.val_SI, + self.c1.fluid_data, + self.c1.mixing_rule + ) + - self.c1.h.val_SI + ) + ) + msg = ( + f'Value of isentropic efficiency must be {round(eta_s_d, 3)}, is ' + f'{instance.eta_s.val}.' + ) assert round(eta_s_d, 3) == round(instance.eta_s.val, 3), msg # trigger invalid value for isentropic efficiency @@ -271,11 +281,22 @@ def test_Turbine(self, tmp_path): self.nw.solve('design') self.nw._convergence_check() eta_s = ( - (self.c2.h.val_SI - self.c1.h.val_SI) / ( - isentropic(self.c1.p.val_SI, self.c1.h.val_SI, self.c2.p.val_SI, self.c1.fluid_data, self.c1.mixing_rule) - - self.c1.h.val_SI)) - msg = ('Value of isentropic efficiency must be ' + str(eta_s) + - ', is ' + str(instance.eta_s.val) + '.') + (self.c2.h.val_SI - self.c1.h.val_SI) + / ( + isentropic( + self.c1.p.val_SI, + self.c1.h.val_SI, + self.c2.p.val_SI, + self.c1.fluid_data, + self.c1.mixing_rule + ) + - self.c1.h.val_SI + ) + ) + msg = ( + f'Value of isentropic efficiency must be {round(eta_s, 3)}, is ' + f'{instance.eta_s.val}.' + ) assert round(eta_s, 3) == round(instance.eta_s.val, 3), msg # unset isentropic efficiency and inlet pressure, @@ -288,21 +309,23 @@ def test_Turbine(self, tmp_path): self.nw.solve('offdesign', design_path=tmp_path) self.nw._convergence_check() # check efficiency - msg = ('Value of isentropic efficiency (' + str(instance.eta_s.val) + - ') must be identical to design case (' + str(eta_s_d) + ').') + msg = ( + f'Value of isentropic efficiency ({instance.eta_s.val}) must be ' + f'identical to design case ({eta_s_d}).' + ) assert round(eta_s_d, 2) == round(instance.eta_s.val, 2), msg # check pressure - msg = ('Value of inlet pressure (' + str(round(self.c1.p.val_SI)) + - ') must be identical to design case (' + - str(round(self.c1.p.design)) + ').') + msg = ( + f'Value of inlet pressure ({round(self.c1.p.val_SI)}) must be ' + f'identical to design case ({round(self.c1.p.design)}).' + ) assert round(self.c1.p.design) == round(self.c1.p.val_SI), msg # lowering mass flow, inlet pressure must sink according to cone law self.c1.set_attr(m=self.c1.m.val * 0.8) self.nw.solve('offdesign', design_path=tmp_path) self.nw._convergence_check() - msg = ('Value of pressure ratio (' + str(instance.pr.val) + - ') must be at (' + str(0.128) + ').') + msg = f'Value of pressure ratio ({instance.pr.val}) must be at 0.128.' assert 0.128 == round(instance.pr.val, 3), msg # testing more parameters for eta_s_char @@ -313,31 +336,40 @@ def test_Turbine(self, tmp_path): self.nw._convergence_check() expr = self.c1.v.val_SI / self.c1.v.design eta_s = round(eta_s_d * instance.eta_s_char.char_func.evaluate(expr), 3) - msg = ('Value of isentropic efficiency (' + - str(round(instance.eta_s.val, 3)) + - ') must be (' + str(eta_s) + ').') + msg = ( + f'Value of isentropic efficiency ({round(instance.eta_s.val, 3)}) ' + f'must be {eta_s}.' + ) assert eta_s == round(instance.eta_s.val, 3), msg # test parameter specification pr instance.eta_s_char.param = 'pr' self.nw.solve('offdesign', design_path=tmp_path) self.nw._convergence_check() - expr = (self.c2.p.val_SI * self.c1.p.design / - (self.c2.p.design * self.c1.p.val_SI)) + expr = ( + self.c2.p.val_SI * self.c1.p.design + / (self.c2.p.design * self.c1.p.val_SI) + ) eta_s = round(eta_s_d * instance.eta_s_char.char_func.evaluate(expr), 3) - msg = ('Value of isentropic efficiency (' + - str(round(instance.eta_s.val, 3)) + - ') must be (' + str(eta_s) + ').') + msg = ( + f'Value of isentropic efficiency ({round(instance.eta_s.val, 3)}) ' + f'must be {eta_s}.' + ) assert eta_s == round(instance.eta_s.val, 3), msg - def test_SteamTurbine(self, temp_path): - pass + def test_SteamTurbine(self, tmp_path): + instance = SteamTurbine('turbine') + self.setup_network(instance) + fl = {'H2O': 1} + self.c1.set_attr(fluid=fl, m=15, p=100, T=500) + self.c2.set_attr(Td_bp=5) + instance.set_attr(eta_s_dry=0.99, alpha=1) + self.nw.solve('design') def test_Turbomachine(self): """Test component properties of turbomachines.""" instance = Turbomachine('turbomachine') - msg = ('Component name must be turbomachine, is ' + - instance.component() + '.') + msg = f'Component name must be turbomachine, is {instance.component()}.' assert 'turbomachine' == instance.component(), msg self.setup_network(instance) fl = {'N2': 0.7556, 'O2': 0.2315, 'Ar': 0.0129} From 47024697bc566a9bd49decf027a343458ffb2e21 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sat, 8 Feb 2025 16:11:40 +0100 Subject: [PATCH 39/55] Update the changelog --- docs/whats_new/v0-7-9.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/whats_new/v0-7-9.rst b/docs/whats_new/v0-7-9.rst index b6f6df858..95ea9815b 100644 --- a/docs/whats_new/v0-7-9.rst +++ b/docs/whats_new/v0-7-9.rst @@ -7,6 +7,11 @@ New Features i.e. :code:`"l"` for liquid, :code:`"tp"` for two-phase and :code:`"g"` for gaseous. The phase is only reported in subcritical pressure (`PR #592 `__). +- Implement the Baumann correlation for wet expansion in steamturbines. The + feature is available in a new component class, + :py:class:`tespy.components.turbomachinery.steam_turbine.SteamTurbine`. To + use the feature check the documentation of the component class + (`PR #602 `__). Contributors ############ From 0ce62d84f89ea4f99b39e8e1b9095f571f8299d7 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Mon, 10 Feb 2025 11:19:17 +0100 Subject: [PATCH 40/55] Add more tests and fix the gas only case --- .../turbomachinery/steam_turbine.py | 52 ++++++++----------- tests/test_components/test_turbomachinery.py | 40 +++++++++++++- 2 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index 827110bee..e46408b2f 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -17,9 +17,11 @@ from tespy.components.turbomachinery.turbine import Turbine from tespy.tools.data_containers import ComponentProperties as dc_cp from tespy.tools.data_containers import GroupedComponentProperties as dc_gcp -from tespy.tools.document_models import generate_latex_eq from tespy.tools.fluid_properties import isentropic from tespy.tools.fluid_properties import h_mix_pQ +from tespy.tools.logger import logger +from tespy.tools.helpers import fluidalias_in_list +from tespy.tools.fluid_properties.helpers import single_fluid @component_registry @@ -155,11 +157,25 @@ def get_parameters(self): params["eta_s_dry_group"] = dc_gcp( num_eq=1, elements=["alpha", "eta_s_dry"], func=self.eta_s_wet_func, - deriv=self.eta_s_wet_deriv, - latex=self.eta_s_wet_func_doc) + deriv=self.eta_s_wet_deriv + ) return params + def preprocess(self, num_nw_vars): + + fluid = single_fluid(self.inl[0].fluid_data) + if fluid is None: + msg = "The SteamTurbine can only be used with pure fluids." + logger.error(msg) + raise ValueError(msg) + + if not fluidalias_in_list(fluid, ["water"]): + msg = "The SteamTurbine is intended to be used with water only." + logger.warning(msg) + + return super().preprocess(num_nw_vars) + def eta_s_wet_func(self): r""" Equation for given dry isentropic efficiency of a turbine under wet @@ -194,6 +210,9 @@ def eta_s_wet_func(self): ) else: # superheated vapour + if outl.calc_phase() == "g": + return self.calc_eta_s() - self.eta_s_dry.val + dp = inl.p.val_SI - outl.p.val_SI # compute the pressure and enthalpy at which the expansion @@ -265,30 +284,3 @@ def eta_s_wet_deriv(self, increment_filter, k): self.jacobian[k, i.h.J_col] = self.numeric_deriv(f, "h", i) if self.is_variable(o.h, increment_filter): self.jacobian[k, o.h.J_col] = self.numeric_deriv(f, "h", o) - - def eta_s_wet_func_doc(self, label): - r""" - Equation for given dry isentropic efficiency of a turbine. - - Parameters - ---------- - label : str - Label for equation. - - Returns - ------- - latex : str - LaTeX code of equations applied. - """ - latex = ( - r'\begin{split}' + '\n' - r'0 &=-\left(h_\mathrm{out}-h_\mathrm{in}\right)+\left(' - r'h_\mathrm{out,s}-h_\mathrm{in}\right)\cdot\eta_\mathrm{s}\\' - + '\n' - r'\eta_\mathrm{s} &=\eta_\mathrm{s}^\mathrm{dry}\cdot \left( 1 - ' - r'\alpha \cdot y_\mathrm{m} \right)\\' + '\n' - r'y_\mathrm{m} &=\frac{\left( 1-x_\mathrm{in} \right)+\left( ' - r'1-x_\mathrm{out} \right)}{2}\\' + '\n' - r'\end{split}' - ) - return generate_latex_eq(self, latex, label) diff --git a/tests/test_components/test_turbomachinery.py b/tests/test_components/test_turbomachinery.py index 209500dbf..9035a9581 100644 --- a/tests/test_components/test_turbomachinery.py +++ b/tests/test_components/test_turbomachinery.py @@ -361,10 +361,46 @@ def test_SteamTurbine(self, tmp_path): instance = SteamTurbine('turbine') self.setup_network(instance) fl = {'H2O': 1} + # start in gas, end in gas self.c1.set_attr(fluid=fl, m=15, p=100, T=500) - self.c2.set_attr(Td_bp=5) - instance.set_attr(eta_s_dry=0.99, alpha=1) + self.c2.set_attr(Td_bp=1) + + eta_s_dry = 0.9 + instance.set_attr(eta_s_dry=0.9, alpha=1) + + self.nw.solve('design') + self.nw._convergence_check() + + eta_s = round(instance.eta_s.val, 4) + msg = ( + f"In gas expansion isentropic ({eta_s}) and dry " + f"efficiency ({eta_s_dry}) have to be identical." + ) + assert eta_s_dry == eta_s, msg + + # end in two phase + self.c2.set_attr(Td_bp=None, x=0.9) + self.nw.solve('design') + self.nw._convergence_check() + + eta_s = round(instance.eta_s.val, 4) + msg = ( + f"In gas expansion isentropic ({eta_s}) and dry " + f"efficiency ({eta_s_dry}) cannot be identical." + ) + assert eta_s_dry != eta_s, msg + + # start in two phase + self.c1.set_attr(T=None, x=0.99) self.nw.solve('design') + self.nw._convergence_check() + + eta_s = round(instance.eta_s.val, 4) + msg = ( + f"In gas expansion isentropic ({eta_s}) and dry " + f"efficiency ({eta_s_dry}) cannot be identical." + ) + assert eta_s_dry != eta_s, msg def test_Turbomachine(self): """Test component properties of turbomachines.""" From 4e9488fcb1db3635471ab3ed81145b6c1d5b9501 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Mon, 10 Feb 2025 11:20:14 +0100 Subject: [PATCH 41/55] Run isort --- src/tespy/components/turbomachinery/steam_turbine.py | 7 +++---- src/tespy/components/turbomachinery/turbine.py | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index e46408b2f..1c3d37a9b 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -12,16 +12,15 @@ from scipy.optimize import brentq - from tespy.components.component import component_registry from tespy.components.turbomachinery.turbine import Turbine 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 isentropic from tespy.tools.fluid_properties import h_mix_pQ -from tespy.tools.logger import logger -from tespy.tools.helpers import fluidalias_in_list +from tespy.tools.fluid_properties import isentropic from tespy.tools.fluid_properties.helpers import single_fluid +from tespy.tools.helpers import fluidalias_in_list +from tespy.tools.logger import logger @component_registry diff --git a/src/tespy/components/turbomachinery/turbine.py b/src/tespy/components/turbomachinery/turbine.py index d7e4a4ac7..941ef86f6 100644 --- a/src/tespy/components/turbomachinery/turbine.py +++ b/src/tespy/components/turbomachinery/turbine.py @@ -12,7 +12,6 @@ import numpy as np - from tespy.components.component import component_registry from tespy.components.turbomachinery.base import Turbomachine from tespy.tools import logger From 431660c97b36fbf0c7793eccd2360640182ea24c Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Mon, 10 Feb 2025 11:22:30 +0100 Subject: [PATCH 42/55] Add citation informatino --- docs/references.bib | 9 +++++++++ src/tespy/components/turbomachinery/steam_turbine.py | 2 ++ 2 files changed, 11 insertions(+) diff --git a/docs/references.bib b/docs/references.bib index 92af8f165..b4ebdb259 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -27,6 +27,15 @@ @misc{Kearney1988 year = {1988}, } +@book{Tanuma2017, + doi = {10.1016/C2014-0-03636-2}, + editor = {T. Tanuma}, + isbn = {9780081003145}, + publisher = {Elsevier}, + title = {Advances in Steam Turbines for Modern Power Plants}, + year = {2017}, +} + @TechReport{Lippke1995, author = {Lippke, Frank}, institution = {Sandia National Lab.}, diff --git a/src/tespy/components/turbomachinery/steam_turbine.py b/src/tespy/components/turbomachinery/steam_turbine.py index 1c3d37a9b..486e43d91 100644 --- a/src/tespy/components/turbomachinery/steam_turbine.py +++ b/src/tespy/components/turbomachinery/steam_turbine.py @@ -109,6 +109,8 @@ class SteamTurbine(Turbine): A steam turbine expands 10 kg/s of superheated steam at 550 °C and 110 bar to 0,5 bar at the outlet. For example, it is possible to calulate the power output and vapour content at the outlet for a given isentropic efficiency. + The :code:`SteamTurbine` class follows the implementation of the Baumann + rule :cite:`Tanuma2017` >>> from tespy.components import Sink, Source, SteamTurbine >>> from tespy.connections import Connection From 1bbe597d5ae90cbf4b614992fc3716fbb8b9e412 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 12 Feb 2025 16:15:55 +0100 Subject: [PATCH 43/55] Move the postprocessing behind the convergence check --- src/tespy/networks/network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/tespy/networks/network.py b/src/tespy/networks/network.py index 123aa7093..2968a669e 100644 --- a/src/tespy/networks/network.py +++ b/src/tespy/networks/network.py @@ -1992,8 +1992,6 @@ def solve(self, mode, init_path=None, design_path=None, logger.error(msg) return - self.postprocessing() - if not self.progress: msg = ( 'The solver does not seem to make any progress, aborting ' @@ -2005,6 +2003,8 @@ def solve(self, mode, init_path=None, design_path=None, logger.warning(msg) return + self.postprocessing() + msg = 'Calculation complete.' logger.info(msg) return From ac1e23eb689de677d8df45dec2537375bf15ad1c Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 12 Feb 2025 16:18:54 +0100 Subject: [PATCH 44/55] Update changelog --- docs/whats_new/v0-7-9.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/whats_new/v0-7-9.rst b/docs/whats_new/v0-7-9.rst index 95ea9815b..704fb950a 100644 --- a/docs/whats_new/v0-7-9.rst +++ b/docs/whats_new/v0-7-9.rst @@ -13,6 +13,13 @@ New Features use the feature check the documentation of the component class (`PR #602 `__). +Bug Fixes +######### +- Run the postprocessing only for a converged solution. Otherwise specified + values on buses, components or connections may change to nonsense, because + the calculation is based on not-converged results of the variables + (`PR #609 `__). + Contributors ############ - `@tlmerbecks `__ From cf1d93517f671ffdbe4b261ee839f8a3fefefa47 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 12 Feb 2025 16:20:06 +0100 Subject: [PATCH 45/55] Fix a typo --- src/tespy/networks/network.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tespy/networks/network.py b/src/tespy/networks/network.py index 2968a669e..3f4580a3d 100644 --- a/src/tespy/networks/network.py +++ b/src/tespy/networks/network.py @@ -1111,7 +1111,7 @@ def presolve_fluid_topology(self): main_conn.fluid.is_var = variable num_var = len(variable) for f in variable: - main_conn.fluid.val[f]: (1 - mass_fraction_sum) / num_var + main_conn.fluid.val[f] = (1 - mass_fraction_sum) / num_var [c.build_fluid_data() for c in all_connections] for fluid in main_conn.fluid.is_var: From 568a1073598bbfb58dfe43448c3aee969c86ab88 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Wed, 12 Feb 2025 16:54:49 +0100 Subject: [PATCH 46/55] Fix the electrolyzer example and remove an unused part in fluid vector presolving (for now) --- src/tespy/components/reactors/water_electrolyzer.py | 4 ++-- src/tespy/networks/network.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/tespy/components/reactors/water_electrolyzer.py b/src/tespy/components/reactors/water_electrolyzer.py index 76a83f0a9..d1299a00a 100644 --- a/src/tespy/components/reactors/water_electrolyzer.py +++ b/src/tespy/components/reactors/water_electrolyzer.py @@ -176,10 +176,10 @@ class WaterElectrolyzer(Component): >>> round(el.eta.val, 1) 0.8 >>> el_cmp.set_attr(v=None) - >>> el.set_attr(P=P_design * 0.66) + >>> el.set_attr(P=P_design * 1e6 * 0.2) >>> nw.solve('offdesign', design_path='tmp') >>> round(el.eta.val, 2) - 0.88 + 0.84 >>> shutil.rmtree('./tmp', ignore_errors=True) """ diff --git a/src/tespy/networks/network.py b/src/tespy/networks/network.py index 3f4580a3d..c57adc9d7 100644 --- a/src/tespy/networks/network.py +++ b/src/tespy/networks/network.py @@ -1109,9 +1109,11 @@ def presolve_fluid_topology(self): main_conn.fluid.val.update(fixed_fractions) main_conn.fluid.is_set = {f for f in fixed_fractions} main_conn.fluid.is_var = variable - num_var = len(variable) - for f in variable: - main_conn.fluid.val[f] = (1 - mass_fraction_sum) / num_var + # this seems to be a problem in some cases, e.g. the basic + # gas turbine tutorial + # num_var = len(variable) + # for f in variable: + # main_conn.fluid.val[f] = (1 - mass_fraction_sum) / num_var [c.build_fluid_data() for c in all_connections] for fluid in main_conn.fluid.is_var: From ca72f4dbae940348cb85387ac7f7076604aef3d2 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Fri, 28 Feb 2025 13:17:06 +0100 Subject: [PATCH 47/55] Fix the example and make sure it is tested against results and not only inputs --- src/tespy/components/reactors/fuel_cell.py | 25 +++++++++++++--------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/src/tespy/components/reactors/fuel_cell.py b/src/tespy/components/reactors/fuel_cell.py index a8cdf577d..403f39b8a 100644 --- a/src/tespy/components/reactors/fuel_cell.py +++ b/src/tespy/components/reactors/fuel_cell.py @@ -132,19 +132,24 @@ class FuelCell(Component): >>> water_out = Connection(fc, 'out2', water_sink, 'in1') >>> nw.add_conns(cw_in, cw_out, oxygen_in, hydrogen_in, water_out) - The fuel cell shall produce 200kW of electrical power and 200kW of heat - with an efficiency of 0.45. The thermodynamic parameters of the input - oxygen and hydrogen are given, the mass flow rates are calculated out of - the given power output. The cooling fluid is pure water. - - >>> fc.set_attr(eta=0.45, P=-200e03, Q=-200e03, pr=0.9) - >>> cw_in.set_attr(T=25, p=1, m=1, fluid={'H2O': 1}) + The fuel cell produces 200kW of electrical power with an efficiency of 0.45. + The thermodynamic parameters of the input oxygen and hydrogen are given, + the mass flow rates are calculated out of the given power output. The + temperature of the water at the outlet should be 50 °C. The cooling fluid is + pure water and is heated up from 25 °C to 40 °C. + + >>> fc.set_attr(eta=0.45, P=-200e03, pr=0.9) + >>> cw_in.set_attr(T=25, p=1, fluid={'H2O': 1}) + >>> cw_out.set_attr(T=40) >>> oxygen_in.set_attr(T=25, p=1) >>> hydrogen_in.set_attr(T=25) + >>> water_out.set_attr(T=50) >>> nw.solve('design') - >>> P_design = fc.P.val / 1e3 - >>> round(P_design, 0) - -200.0 + >>> round(cw_in.m.val, 1) + 10.2 + >>> Q = fc.Q.val / 1e3 + >>> round(Q, 0) + -642.0 >>> round(fc.eta.val, 2) 0.45 """ From 01808f9d21bdf672de05ae6f21e3aedf92564b20 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Fri, 28 Feb 2025 13:18:32 +0100 Subject: [PATCH 48/55] Update changelog --- docs/whats_new/v0-7-9.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/whats_new/v0-7-9.rst b/docs/whats_new/v0-7-9.rst index 704fb950a..df4b9ff07 100644 --- a/docs/whats_new/v0-7-9.rst +++ b/docs/whats_new/v0-7-9.rst @@ -19,6 +19,8 @@ Bug Fixes values on buses, components or connections may change to nonsense, because the calculation is based on not-converged results of the variables (`PR #609 `__). +- Fix the fuel cell example and make sure it is tested properly + (`PR #618 `__). Contributors ############ From 74128b7f6e1af39ca7263abd507a1966fa698e6a Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sat, 1 Mar 2025 12:07:10 +0100 Subject: [PATCH 49/55] Remove unused part of the component for now --- .../heat_exchangers/movingboundary.py | 73 ------------------- 1 file changed, 73 deletions(-) diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index dc746e6e1..6be5add41 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -40,11 +40,6 @@ def get_parameters(self): 'U_twophase_twophase': dc_cp(min_val=0), 'U_twophase_liquid': 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 ), @@ -272,52 +267,6 @@ def UA_deriv(self, increment_filter, k): 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 @@ -379,27 +328,5 @@ def td_pinch_deriv(self, increment_filter, k): 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 = self.calc_UA() self.td_pinch.val = self.calc_td_pinch() From eec171dee6719a497a94a9e27add240d08d7205c Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sat, 1 Mar 2025 13:04:50 +0100 Subject: [PATCH 50/55] Throw a warning for non-equal inlet and outlet pressure and add an example with documentation --- .../heat_exchangers/movingboundary.py | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index 6be5add41..09d2e36a9 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -19,9 +19,243 @@ from tespy.tools.fluid_properties import single_fluid from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.global_vars import ERR +from tespy.tools.logger import logger class MovingBoundaryHeatExchanger(HeatExchanger): + r""" + Class for counter current heat exchanger with UA sections. + + The heat exchanger is internally discretized into multiple sections, which + are defined by phase changes. The component assumes, that no pressure + losses occur. + + **Mandatory Equations** + + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.energy_balance_func` + + **Optional Equations** + + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.energy_balance_hot_func` + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.kA_func` + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.kA_char_func` + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.ttd_u_func` + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.ttd_l_func` + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.ttd_min_func` + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.eff_cold_func` + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.eff_hot_func` + - :py:meth:`tespy.components.heat_exchangers.base.HeatExchanger.eff_max_func` + - hot side :py:meth:`tespy.components.component.Component.pr_func` + - cold side :py:meth:`tespy.components.component.Component.pr_func` + - hot side :py:meth:`tespy.components.component.Component.zeta_func` + - cold side :py:meth:`tespy.components.component.Component.zeta_func` + - hot side :py:meth:`tespy.components.component.Component.dp_func` + - cold side :py:meth:`tespy.components.component.Component.dp_func` + - hot side :py:meth:`tespy.components.component.Component.UA_func` + - cold side :py:meth:`tespy.components.component.Component.td_pinch_func` + + Inlets/Outlets + + - in1, in2 (index 1: hot side, index 2: cold side) + - out1, out2 (index 1: hot side, index 2: cold side) + + Image + + .. image:: /api/_images/HeatExchanger.svg + :alt: flowsheet of the heat exchanger + :align: center + :class: only-light + + .. image:: /api/_images/HeatExchanger_darkmode.svg + :alt: flowsheet of the heat exchanger + :align: center + :class: only-dark + + Parameters + ---------- + label : str + The label of the component. + + design : list + List containing design parameters (stated as String). + + offdesign : list + List containing offdesign parameters (stated as String). + + design_path : str + Path to the components design case. + + local_offdesign : boolean + Treat this component in offdesign mode in a design calculation. + + local_design : boolean + Treat this component in design mode in an offdesign calculation. + + char_warnings : boolean + Ignore warnings on default characteristics usage for this component. + + printout : boolean + Include this component in the network's results printout. + + Q : float, dict + Heat transfer, :math:`Q/\text{W}`. + + pr1 : float, dict, :code:`"var"` + Outlet to inlet pressure ratio at hot side, :math:`pr/1`. + + pr2 : float, dict, :code:`"var"` + Outlet to inlet pressure ratio at cold side, :math:`pr/1`. + + dp1 : float, dict, :code:`"var"` + Inlet to outlet pressure delta at hot side, unit is the network's + pressure unit!. + + dp2 : float, dict, :code:`"var"` + Inlet to outlet pressure delta at cold side, unit is the network's + pressure unit!. + + zeta1 : float, dict, :code:`"var"` + Geometry independent friction coefficient at hot side, + :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. + + zeta2 : float, dict, :code:`"var"` + Geometry independent friction coefficient at cold side, + :math:`\frac{\zeta}{D^4}/\frac{1}{\text{m}^4}`. + + ttd_l : float, dict + Lower terminal temperature difference :math:`ttd_\mathrm{l}/\text{K}`. + + ttd_u : float, dict + Upper terminal temperature difference :math:`ttd_\mathrm{u}/\text{K}`. + + ttd_min : float, dict + Minumum terminal temperature difference :math:`ttd_\mathrm{min}/\text{K}`. + + eff_cold : float, dict + Cold side heat exchanger effectiveness :math:`eff_\text{cold}/\text{1}`. + + eff_hot : float, dict + Hot side heat exchanger effectiveness :math:`eff_\text{hot}/\text{1}`. + + eff_max : float, dict + Max value of hot and cold side heat exchanger effectiveness values + :math:`eff_\text{max}/\text{1}`. + + kA : float, dict + Area independent heat transfer coefficient, + :math:`kA/\frac{\text{W}}{\text{K}}`. + + kA_char : dict + Area independent heat transfer coefficient characteristic. + + kA_char1 : tespy.tools.characteristics.CharLine, dict + Characteristic line for hot side heat transfer coefficient. + + kA_char2 : tespy.tools.characteristics.CharLine, dict + Characteristic line for cold side heat transfer coefficient. + + UA : float, dict + Sum of UA in all sections of the heat exchanger. + + td_pinch : float, dict + Value of the lowest delta T between hot side and cold side at the + different sections. + + Note + ---- + The equations only apply to counter-current heat exchangers. + + Example + ------- + Water vapor should be cooled down, condensed and then further subcooled. For + this air is heated up from 15 °C to 25 °C. + + >>> from tespy.components import Source, Sink, MovingBoundaryHeatExchanger + >>> from tespy.connections import Connection + >>> from tespy.networks import Network + >>> import numpy as np + >>> nw = Network(T_unit="C", p_unit="bar") + >>> nw.set_attr(iterinfo=False) + >>> so1 = Source("vapor source") + >>> so2 = Source("air source") + >>> cd = MovingBoundaryHeatExchanger("condenser") + >>> si1 = Sink("water sink") + >>> si2 = Sink("air sink") + >>> c1 = Connection(so1, "out1", cd, "in1", label="1") + >>> c2 = Connection(cd, "out1", si1, "in1", label="2") + >>> c11 = Connection(so2, "out1", cd, "in2", label="11") + >>> c12 = Connection(cd, "out2", si2, "in1", label="12") + >>> nw.add_conns(c1, c2, c11, c12) + + To generate good guess values, first we run the simulation with fixed + pressure on the water side. The water enters at superheated vapor state + with 15 °C superheating and leaves it with 10 °C subcooling. + + >>> c1.set_attr(fluid={"Water": 1}, p=1, Td_bp=15, m=1) + >>> c2.set_attr(p=1, Td_bp=-15) + >>> c11.set_attr(fluid={"Air": 1}, p=1, T=15) + >>> c12.set_attr(p=1, T=25) + >>> nw.solve("design") + + Now we can remove the pressure specifications on the air side and impose + the minimum pinch instead, which will determine the actual water + condensation pressure. + + >>> c1.set_attr(p=None) + >>> c2.set_attr(p=None) + >>> c12.set_attr(p=None) + >>> cd.set_attr(td_pinch=5, pr1=1, pr2=1) + >>> nw.solve("design") + >>> round(c1.p.val, 3) + 0.056 + + We can also see the temperature differences in all sections of the heat + exchanger. Since the water vapor is cooled, condensed and then subcooled, + while the air does not change phase, three sections will form: + + >>> Q_sections = cd._assign_sections() + >>> T_steps_hot, T_steps_cold = cd._get_T_at_steps(Q_sections) + >>> delta_T_between_sections = [ + ... round(T_hot - T_cold, 2) + ... for T_hot, T_cold in zip(T_steps_hot, T_steps_cold) + ... ] + >>> delta_T_between_sections + [5.0, 19.75, 10.11, 25.0] + + We can see that the lowest delta T is the first one. This is the delta T + between the hot side outlet and the cold side inlet, which can also be seen + if we have a look at the network's results. + + >>> ();nw.print_results();() # doctest: +ELLIPSIS + (...) + + If we change the subcooling degree at the water outlet, the condensation + pressure and pinch will move. + + >>> c2.set_attr(Td_bp=-5) + >>> nw.solve("design") + >>> round(c1.p.val, 3) + 0.042 + >>> Q_sections = cd._assign_sections() + >>> T_steps_hot, T_steps_cold = cd._get_T_at_steps(Q_sections) + >>> delta_T_between_sections = [ + ... round(T_hot - T_cold, 2) + ... for T_hot, T_cold in zip(T_steps_hot, T_steps_cold) + ... ] + >>> delta_T_between_sections + [9.88, 14.8, 5.0, 19.88] + + Finally, in contrast to the baseclass :code:`HeatExchanger` `kA` value, the + `UA` value takes into account the heat transfer per section and calculates + the heat transfer coefficient as the sum of all sections, while the `kA` + value only takes into account the inlet and outlet temperatures and the + total heat transfer. + + >>> round(cd.kA.val) + 173307 + >>> round(cd.UA.val) + 273449 + """ @staticmethod def component(): @@ -330,3 +564,19 @@ def calc_parameters(self): super().calc_parameters() self.UA.val = self.calc_UA() self.td_pinch.val = self.calc_td_pinch() + if round(self.inl[0].p.val_SI) != round(self.outl[0].p.val_SI): + msg = ( + f"The {self.__class__.__name__} instance {self.label} is " + "defined for constant pressure. The identification of the " + "heat transfer sections might be wrong in case phase changes " + "are involved in the heat transfer process." + ) + logger.warning(msg) + if round(self.inl[1].p.val_SI) != round(self.outl[1].p.val_SI): + msg = ( + f"The {self.__class__.__name__} instance {self.label} is " + "defined for constant pressure. The identification of the " + "heat transfer sections might be wrong in case phase changes " + "are involved in the heat transfer process." + ) + logger.warning(msg) From 04339e5a3944d1070f8cc4188e3664da5532e04a Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sat, 1 Mar 2025 13:06:52 +0100 Subject: [PATCH 51/55] Include a literature reference --- docs/references.bib | 11 +++++++++++ .../components/heat_exchangers/movingboundary.py | 2 +- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/references.bib b/docs/references.bib index b4ebdb259..3e2cada42 100644 --- a/docs/references.bib +++ b/docs/references.bib @@ -420,3 +420,14 @@ @inproceedings{Fritz2024 author = {Fritz, Malte and Freißmann, Jonas and Tuschy, Ilja}, title = {Open-Source Web Dashboard zur Simulation, Analyse und Bewertung von Wärmepumpen} } + +@article{bell2015, + title = {A generalized moving-boundary algorithm to predict the heat transfer rate of counterflow heat exchangers for any phase configuration}, + journal = {Applied Thermal Engineering}, + volume = {79}, + pages = {192-201}, + year = {2015}, + issn = {1359-4311}, + doi = {https://doi.org/10.1016/j.applthermaleng.2014.12.028}, + author = {Ian H. Bell and Sylvain Quoilin and Emeline Georges and James E. Braun and Eckhard A. Groll and W. Travis Horton and Vincent Lemort}, +} diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index 09d2e36a9..9d414d2f5 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -28,7 +28,7 @@ class MovingBoundaryHeatExchanger(HeatExchanger): The heat exchanger is internally discretized into multiple sections, which are defined by phase changes. The component assumes, that no pressure - losses occur. + losses occur. In principle the implementations follows :cite:`bell2015`. **Mandatory Equations** From 260b8f7497153cd9addc83375259bd7f7d9b4604 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sat, 1 Mar 2025 13:07:01 +0100 Subject: [PATCH 52/55] Remove old testfile --- ..._partload_model_movingboundarycondenser.py | 83 ------------------- 1 file changed, 83 deletions(-) delete mode 100644 tests/test_components/test_partload_model_movingboundarycondenser.py diff --git a/tests/test_components/test_partload_model_movingboundarycondenser.py b/tests/test_components/test_partload_model_movingboundarycondenser.py deleted file mode 100644 index 84840824c..000000000 --- a/tests/test_components/test_partload_model_movingboundarycondenser.py +++ /dev/null @@ -1,83 +0,0 @@ -from tespy.components import HeatExchanger, Source, Sink, Compressor, MovingBoundaryHeatExchanger -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 = MovingBoundaryHeatExchanger("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(so2, "out1", cd, "in1", label="11") -c12 = Connection(cd, "out1", si2, "in1", label="12") - -nw.add_conns(c1, c2, c11, c12) - -cp.set_attr(eta_s=0.8) - -# c10.set_attr(T=20, x=1) -c11.set_attr(fluid={"NH3": 1}, m=10, Td_bp=40, p=40) -c12.set_attr(x=0, p=40) - -c1.set_attr(fluid={"Water": 1}, p=1, Td_bp=-5, h0=1e5) -c2.set_attr(p=1, Td_bp=20) - - -# cd.set_attr(U_desup=4000, U_cond=16000, U_subcool=5000) - -nw.set_attr(iterinfo=False) -nw.solve("design") - -c11.set_attr(p=None) -c12.set_attr(p=None) -cd.set_attr(td_pinch=5, pr1=1) -nw.solve("design") -c11.set_attr(Td_bp=30.4) -# c12.set_attr(Td_bp=-Td_bp) -nw.solve("design") - -# Q_sections = cd._assign_sections() -# T_steps_hot, T_steps_cold = cd._get_T_at_steps(Q_sections) -# print(cd.UA.val, cd.td_pinch.val, Q_sections, T_steps_hot, T_steps_cold) -# print(cd._calc_UA_in_sections(T_steps_hot, T_steps_cold, np.diff(Q_sections))) - -# c11.set_attr(Td_bp=30.5) -# # c12.set_attr(Td_bp=-Td_bp) -# nw.solve("design") - -# # Q_sections = cd._assign_sections() -# # print(cd.UA.val, cd.td_pinch.val, Q_sections, cd._get_T_at_steps(Q_sections)) - -# c11.set_attr(Td_bp=30.4) -# # c12.set_attr(Td_bp=-Td_bp) -# nw.solve("design") - -# Q_sections2 = cd._assign_sections() -# T_steps_hot2, T_steps_cold2 = cd._get_T_at_steps(Q_sections2) -# print(cd.UA.val, cd.td_pinch.val, Q_sections, T_steps_hot2, T_steps_cold2) -# print(cd._calc_UA_in_sections(T_steps_hot2, T_steps_cold2, np.diff(Q_sections2))) - -# exit() - -for Td_bp in np.linspace(40, 2): - c11.set_attr(Td_bp=Td_bp) - # c12.set_attr(Td_bp=-Td_bp) - nw.solve("design") - # print(cd.td_pinch.val, cd.UA.val) - Q_sections = cd._assign_sections() - print(Td_bp, cd.UA.val) From 003ac950f778240c8fd5f17c95b5c61a2d45c19e Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sat, 1 Mar 2025 13:08:46 +0100 Subject: [PATCH 53/55] Fix import order --- src/tespy/components/heat_exchangers/movingboundary.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tespy/components/heat_exchangers/movingboundary.py b/src/tespy/components/heat_exchangers/movingboundary.py index 9d414d2f5..a95d0db24 100644 --- a/src/tespy/components/heat_exchangers/movingboundary.py +++ b/src/tespy/components/heat_exchangers/movingboundary.py @@ -11,13 +11,14 @@ SPDX-License-Identifier: MIT """ import math + import numpy as np from tespy.components.heat_exchangers.base import HeatExchanger from tespy.tools.data_containers import ComponentProperties as dc_cp +from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.fluid_properties import h_mix_pQ from tespy.tools.fluid_properties import single_fluid -from tespy.tools.fluid_properties import T_mix_ph from tespy.tools.global_vars import ERR from tespy.tools.logger import logger From aa7cfad61a94da2ca6577cfaebd08e67326c0a5b Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sat, 1 Mar 2025 13:13:12 +0100 Subject: [PATCH 54/55] Update changelog --- docs/whats_new/v0-7-9.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/whats_new/v0-7-9.rst b/docs/whats_new/v0-7-9.rst index 704fb950a..e3f740f55 100644 --- a/docs/whats_new/v0-7-9.rst +++ b/docs/whats_new/v0-7-9.rst @@ -12,6 +12,16 @@ New Features :py:class:`tespy.components.turbomachinery.steam_turbine.SteamTurbine`. To use the feature check the documentation of the component class (`PR #602 `__). +- Implement a new component class + :py:class:`tespy.components.heat_exchangers.movingboundary.MovingBoundaryHeatExchanger`, + which allows to make specification of internal pinch temperature difference + in case of heat exchanger internal phase changes on the hot or the cold side + of the component. It splits the heat exchange automatically in sections, + where in each section the phase of the hot and the cold fluid does not change. + These sections are utilized to find the minimum pinch internally, and allow + to assing heat transfer coefficients `UA` for all individual sections as well + as the total sum + (`PR #515 `__). Bug Fixes ######### From 190bf6e0098af849c5dc86cd417a92cfe52dc7d9 Mon Sep 17 00:00:00 2001 From: Francesco Witte Date: Sun, 2 Mar 2025 09:14:30 +0100 Subject: [PATCH 55/55] Bump version --- pyproject.toml | 2 +- src/tespy/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 16adfda92..4df60e5e6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ exclude = ["docs/_build"] [project] name = "tespy" -version = "0.7.9.dev0" +version = "0.7.9" description = "Thermal Engineering Systems in Python (TESPy)" readme = "README.rst" authors = [ diff --git a/src/tespy/__init__.py b/src/tespy/__init__.py index 70ade2f64..31307c77b 100644 --- a/src/tespy/__init__.py +++ b/src/tespy/__init__.py @@ -3,7 +3,7 @@ import os __datapath__ = os.path.join(importlib.resources.files("tespy"), "data") -__version__ = '0.7.9.dev0 - Newton\'s Nature' +__version__ = '0.7.9 - Newton\'s Nature' # tespy data and connections import from . import connections # noqa: F401