Overview of Heat Exchanger Models¶
This tutorial shows an overview of the different heat exchanger models available in TESPy. The table below indicates the different types and how they compare in general. Further down we have created a problem which models the heat exchanger with a variety of the mentioned types and we show the differences in the results.
Overview table¶
type |
speed |
accuracy* |
internal pinch |
|
|---|---|---|---|---|
SimpleHeatExchanger |
0D |
fastest |
lowest |
no |
HeatExchanger |
0D |
very fast |
lower |
no |
ParallelFlowHeatExchanger |
0D |
very fast |
lower |
no |
Desuperheater |
0D |
very fast |
lower |
no |
Condenser |
0D |
very fast |
low |
(no) |
MovingBoundaryHeatExchanger |
1D |
mid |
high |
yes |
SectionedHeatExchanger |
1D |
slow |
highest |
yes |
Note
The accuracy depends on the context. In many contexts, the standard 0D heat exchanger components can be just as accurate as the 1D models.
The calculation speed also depends on context for the
SectionedHeatExchanger:The number of specified sections influences the speed.
If you impose
td_pinchorUAto your model it will be significantly slower compared to other models, especially with a high number of sections.If you do not specify
td_pinchorUAthe sectioning is only applied once in the postprocessing; this takes more time than other heat exchanger types but is still relatively fast.
import pandas as pd
import matplotlib.patches as mpatches
from matplotlib import pyplot as plt
from tespy.components import (
Condenser,
HeatExchanger,
MovingBoundaryHeatExchanger,
SectionedHeatExchanger,
Sink,
Source,
)
from tespy.connections import Connection
from tespy.networks import Network
from tespy.tools.fluid_properties import h_mix_pQ
annotation_color = "black"
results = []
def make_network(heatex):
nw = Network()
nw.units.set_defaults(
temperature="°C",
pressure="bar",
pressure_difference="bar",
heat="MW",
heat_transfer_coefficient="kW/K"
)
so1 = Source("source 1")
so2 = Source("source 2")
si1 = Sink("sink 1")
si2 = Sink("sink 2")
c1 = Connection(so1, "out1", heatex, "in1", label="c1")
c2 = Connection(heatex, "out1", si1, "in1", label="c2")
d1 = Connection(so2, "out1", heatex, "in2", label="d1")
d2 = Connection(heatex, "out2", si2, "in1", label="d2")
nw.add_conns(c1, c2, d1, d2)
c1.set_attr(fluid={"R290": 1}, td_dew=50, T_dew=60, m=5)
c2.set_attr(td_bubble=5)
d1.set_attr(fluid={"water": 1}, p=1, T=45)
d2.set_attr(T=55)
heatex.set_attr(dp1=0, dp2=0)
return nw, c1, c2, d1, d2
Matplotlib is building the font cache; this may take a moment.
Model comparisons¶
For the model comparison we have selected a typical problem: the condensation of a working fluid in a heat pump to heat up water. The boundary conditions are listed in the table below.
label |
Specification |
value |
unit |
|---|---|---|---|
c1 |
fluid |
R290 |
- |
mass flow |
5 |
kg/s |
|
dew line temperature |
60 |
°C |
|
superheating |
50 |
°C |
|
c2 |
subcooling |
5 |
°C |
d1 |
fluid |
water |
- |
temperature |
45 |
°C |
|
pressure |
1 |
bar |
|
d2 |
temperature |
55 |
°C |
heatexchanger |
pressure drops |
0 |
bar |
Attention
Keep in mind: under different boundary conditions (e.g. no phase change) results of this comparison may vary a lot. There might be conditions where the 0D components yield very similar results, or where the deviation is even higher.
For the calculation of the results the following equations apply:
For the calculation of \(kA\) the terminal temperature differences ttd_u and
ttd_l are considered as \(\Delta T_0\) and \(\Delta T_1\). For the calculation
of \(UA\), the internal temperature differences \(\Delta T_{i}\) in each section
of the heat exchanger are employed.
HeatExchanger¶
hx_0d = HeatExchanger("heatexchanger")
nw, c1, c2, d1, d2 = make_network(hx_0d)
nw.solve("design")
results.append({
"model": "HeatExchanger",
"minimum pinch (K)": f"n/a ({hx_0d.ttd_min.val:.1f})",
"kA (kW/K)": f"{hx_0d.kA.val:.1f}",
"UA (kW/K)": "n/a",
})
heat = [0, abs(hx_0d.Q.val)]
T_hot = [c2.T.val_SI, c1.T.val_SI]
T_cold = [d1.T.val_SI, d2.T.val_SI]
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot(heat, T_hot, "o-", color="red")
ax.plot(heat, T_cold, "o-", color="blue")
offset = heat[-1] / 15
bar_len = heat[-1] / 15
bracket = mpatches.FancyArrowPatch(
(heat[-1] * (1 + 1 / 15), T_cold[-1]), (heat[-1] * (1 + 1 / 15), T_hot[-1]),
connectionstyle="bar,angle=0,fraction=0", arrowstyle="-", linewidth=2, color=annotation_color,
)
ax.add_patch(bracket)
ax.plot([heat[-1] + offset - bar_len/2, heat[-1] + offset + bar_len/2], [T_hot[-1], T_hot[-1]], color=annotation_color, lw=2)
ax.plot([heat[-1] + offset - bar_len/2, heat[-1] + offset + bar_len/2], [T_cold[-1], T_cold[-1]], color=annotation_color, lw=2)
ax.text(heat[-1] * 1.1, (T_hot[-1] + T_cold[-1]) / 2, r"$\Delta T = \text{ttd\_u}$", va="center", color=annotation_color)
bracket = mpatches.FancyArrowPatch(
(0 - offset, T_cold[0]), (0 - offset, T_hot[0]),
connectionstyle="bar,angle=0,fraction=0", arrowstyle="-", linewidth=2, color=annotation_color,
)
ax.add_patch(bracket)
ax.text(heat[0] - offset * 5, (T_hot[0] + T_cold[0]) / 2, r"$\Delta T = \text{ttd\_l}$", va="center", color=annotation_color)
ax.plot([heat[0] - offset - bar_len/2, heat[0] - offset + bar_len/2], [T_hot[0], T_hot[0]], color=annotation_color, lw=2)
ax.plot([heat[0] - offset - bar_len/2, heat[0] - offset + bar_len/2], [T_cold[0], T_cold[0]], color=annotation_color, lw=2)
ax.set_xbound([-5.5 * offset, heat[-1] + 4 * offset])
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
plt.show()
RuntimeWarning: invalid value encountered in scalar divide
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 1.93e+06 | 0 % | 4.61e+01 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 4.87e-06 | 100 % | 1.17e-10 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 2.33e-10 | 100 % | 5.57e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 1851.99
Condenser¶
In the condenser the upper terminal temperature difference is assigned to the temperature difference between the dew line temperature of the condensing fluid and the outlet temperature of the cold fluid.
hx_cond = Condenser("heatexchanger")
nw, c1, c2, d1, d2 = make_network(hx_cond)
hx_cond.set_attr(subcooling=True)
nw.solve("design")
results.append({
"model": "Condenser",
"minimum pinch (K)": f"n/a ({hx_cond.ttd_min.val:.1f})",
"kA (kW/K)": f"{hx_cond.kA.val:.1f}",
"UA (kW/K)": "n/a",
})
T_cond = c2.T.val_SI - c2.calc_td_dew()
heat_to_cond = c1.m.val_SI * (h_mix_pQ(c2.p.val_SI, 1, c2.fluid_data) - c2.h.val_SI) / 1e6
heat = [0, heat_to_cond, abs(hx_cond.Q.val)]
T_hot = [c2.T.val_SI, T_cond, c1.T.val_SI]
T_cold = [d1.T.val_SI, d2.T.val_SI]
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot(heat, T_hot, "o-", color="red")
ax.plot([heat[0], heat[-1]], [T_cold[0], T_cold[-1]], "o-", color="blue")
ax.plot([heat[0], heat[-1]], [T_cond, T_cond], "--", color=annotation_color)
ax.text(heat[0], T_cond + 2.5, r"$T_\text{dew}$", va="center", color=annotation_color)
offset = heat[-1] / 15
bar_len = heat[-1] / 15
bracket = mpatches.FancyArrowPatch(
(heat[-1] * (1 + 1 / 15), T_cold[-1]), (heat[-1] * (1 + 1 / 15), T_cond),
connectionstyle="bar,angle=0,fraction=0", arrowstyle="-", linewidth=2, color=annotation_color,
)
ax.add_patch(bracket)
ax.plot([heat[-1] + offset - bar_len/2, heat[-1] + offset + bar_len/2], [T_cond, T_cond], color=annotation_color, lw=2)
ax.plot([heat[-1] + offset - bar_len/2, heat[-1] + offset + bar_len/2], [T_cold[-1], T_cold[-1]], color=annotation_color, lw=2)
ax.text(heat[-1] * 1.1, (T_cond + T_cold[-1]) / 2, r"$\Delta T = \text{ttd\_u}$", va="center", color=annotation_color)
bracket = mpatches.FancyArrowPatch(
(0 - offset, T_cold[0]), (0 - offset, T_hot[0]),
connectionstyle="bar,angle=0,fraction=0", arrowstyle="-", linewidth=2, color=annotation_color,
)
ax.add_patch(bracket)
ax.text(heat[0] - offset * 5, (T_hot[0] + T_cold[0]) / 2, r"$\Delta T = \text{ttd\_l}$", va="center", color=annotation_color)
ax.plot([heat[0] - offset - bar_len/2, heat[0] - offset + bar_len/2], [T_hot[0], T_hot[0]], color=annotation_color, lw=2)
ax.plot([heat[0] - offset - bar_len/2, heat[0] - offset + bar_len/2], [T_cold[0], T_cold[0]], color=annotation_color, lw=2)
ax.set_xbound([-5.5 * offset, heat[-1] + 4 * offset])
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
plt.show()
RuntimeWarning: invalid value encountered in scalar divide
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 1.93e+06 | 0 % | 4.61e+01 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 4.87e-06 | 100 % | 1.17e-10 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 2.33e-10 | 100 % | 5.57e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2078.45
MovingBoundaryHeatExchanger¶
The moving boundary model sections the heat exchange into three different sections at the phase change points.
hx_mb = MovingBoundaryHeatExchanger("heatexchanger")
nw, c1, c2, d1, d2 = make_network(hx_mb)
nw.solve("design")
heat, T_hot, T_cold, _, _ = hx_mb.calc_sections()
heat /= 1e6
results.append({
"model": "MovingBoundaryHeatExchanger",
"minimum pinch (K)": f"{hx_mb.td_pinch.val:.2f}",
"kA (kW/K)": f"{hx_mb.kA.val:.1f}",
"UA (kW/K)": f"{hx_mb.UA.val:.1f}",
})
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot((heat, heat), (list(T_hot), list(T_cold)), color=annotation_color, linestyle="--")
ax.text(heat[2] * 1.05, (T_hot[2] + T_cold[2]) / 2, r"$\Delta T_\text{i}$", va="center", color=annotation_color)
ax.plot(heat, T_hot, "o-", color="red")
ax.plot(heat, T_cold, "o-", color="blue")
offset = heat[-1] / 15
bar_len = heat[-1] / 15
bracket = mpatches.FancyArrowPatch(
(heat[-1] * (1 + 1 / 15), T_cold[-1]), (heat[-1] * (1 + 1 / 15), T_hot[-1]),
connectionstyle="bar,angle=0,fraction=0", arrowstyle="-", linewidth=2, color=annotation_color,
)
ax.add_patch(bracket)
ax.plot([heat[-1] + offset - bar_len/2, heat[-1] + offset + bar_len/2], [T_hot[-1], T_hot[-1]], color=annotation_color, lw=2)
ax.plot([heat[-1] + offset - bar_len/2, heat[-1] + offset + bar_len/2], [T_cold[-1], T_cold[-1]], color=annotation_color, lw=2)
ax.text(heat[-1] * 1.1, (T_hot[-1] + T_cold[-1]) / 2, r"$\Delta T = \text{ttd\_u}$", va="center", color=annotation_color)
bracket = mpatches.FancyArrowPatch(
(0 - offset, T_cold[0]), (0 - offset, T_hot[0]),
connectionstyle="bar,angle=0,fraction=0", arrowstyle="-", linewidth=2, color=annotation_color,
)
ax.add_patch(bracket)
ax.text(heat[0] - offset * 5, (T_hot[0] + T_cold[0]) / 2, r"$\Delta T = \text{ttd\_l}$", va="center", color=annotation_color)
ax.plot([heat[0] - offset - bar_len/2, heat[0] - offset + bar_len/2], [T_hot[0], T_hot[0]], color=annotation_color, lw=2)
ax.plot([heat[0] - offset - bar_len/2, heat[0] - offset + bar_len/2], [T_cold[0], T_cold[0]], color=annotation_color, lw=2)
ax.set_xbound([-5.5 * offset, heat[-1] + 4 * offset])
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
plt.show()
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 1.93e+06 | 0 % | 4.61e+01 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 4.87e-06 | 100 % | 1.17e-10 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 2.33e-10 | 100 % | 5.57e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2149.27
RuntimeWarning: invalid value encountered in scalar divide
SectionedHeatExchanger¶
The sectioned model sections the heat exchange into 50 sections by default and extra sections are inserted at the moving boundaries (note the smaller sections in between).
hx_sectioned = SectionedHeatExchanger("heatexchanger")
nw_sectioned, c1, c2, d1, d2 = make_network(hx_sectioned)
nw_sectioned.solve("design")
heat_50secs, T_hot_50secs, T_cold_50secs, _, _ = hx_sectioned.calc_sections()
heat_50secs /= 1e6
results.append({
"model": "SectionedHeatExchanger (50 sections)",
"minimum pinch (K)": f"{hx_sectioned.td_pinch.val:.2f}",
"kA (kW/K)": f"{hx_sectioned.kA.val:.1f}",
"UA (kW/K)": f"{hx_sectioned.UA.val:.1f}",
})
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot((heat_50secs, heat_50secs), (list(T_hot_50secs), list(T_cold_50secs)), color=annotation_color, linestyle="--")
ax.plot(heat_50secs, T_hot_50secs, "o-", color="red")
ax.plot(heat_50secs, T_cold_50secs, "o-", color="blue")
offset = heat_50secs[-1] / 15
bar_len = heat_50secs[-1] / 15
bracket = mpatches.FancyArrowPatch(
(heat_50secs[-1] * (1 + 1 / 15), T_cold_50secs[-1]), (heat_50secs[-1] * (1 + 1 / 15), T_hot_50secs[-1]),
connectionstyle="bar,angle=0,fraction=0", arrowstyle="-", linewidth=2, color=annotation_color,
)
ax.add_patch(bracket)
ax.plot([heat_50secs[-1] + offset - bar_len/2, heat_50secs[-1] + offset + bar_len/2], [T_hot_50secs[-1], T_hot_50secs[-1]], color=annotation_color, lw=2)
ax.plot([heat_50secs[-1] + offset - bar_len/2, heat_50secs[-1] + offset + bar_len/2], [T_cold_50secs[-1], T_cold_50secs[-1]], color=annotation_color, lw=2)
ax.text(heat_50secs[-1] * 1.1, (T_hot_50secs[-1] + T_cold_50secs[-1]) / 2, r"$\Delta T = \text{ttd\_u}$", va="center", color=annotation_color)
bracket = mpatches.FancyArrowPatch(
(0 - offset, T_cold_50secs[0]), (0 - offset, T_hot_50secs[0]),
connectionstyle="bar,angle=0,fraction=0", arrowstyle="-", linewidth=2, color=annotation_color,
)
ax.add_patch(bracket)
ax.text(heat_50secs[0] - offset * 5, (T_hot_50secs[0] + T_cold_50secs[0]) / 2, r"$\Delta T = \text{ttd\_l}$", va="center", color=annotation_color)
ax.plot([heat_50secs[0] - offset - bar_len/2, heat_50secs[0] - offset + bar_len/2], [T_hot_50secs[0], T_hot_50secs[0]], color=annotation_color, lw=2)
ax.plot([heat_50secs[0] - offset - bar_len/2, heat_50secs[0] - offset + bar_len/2], [T_cold_50secs[0], T_cold_50secs[0]], color=annotation_color, lw=2)
ax.set_xbound([-5.5 * offset, heat_50secs[-1] + 4 * offset])
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
plt.show()
RuntimeWarning: invalid value encountered in scalar divide
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 1.93e+06 | 0 % | 4.61e+01 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 4.87e-06 | 100 % | 1.17e-10 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 2.33e-10 | 100 % | 5.57e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2187.67
hx_sectioned.set_attr(num_sections=6)
nw_sectioned.solve("design")
heat_6secs, T_hot_6secs, T_cold_6secs, _, _ = hx_sectioned.calc_sections()
heat_6secs /= 1e6
results.append({
"model": "SectionedHeatExchanger (6 sections)",
"minimum pinch (K)": f"{hx_sectioned.td_pinch.val:.2f}",
"kA (kW/K)": f"{hx_sectioned.kA.val:.1f}",
"UA (kW/K)": f"{hx_sectioned.UA.val:.1f}",
})
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 2.33e-10 | 100 % | 5.57e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2052.26
pd.DataFrame(results).set_index("model").style
| minimum pinch (K) | kA (kW/K) | UA (kW/K) | |
|---|---|---|---|
| model | |||
| HeatExchanger | n/a (10.0) | 75.5 | n/a |
| Condenser | n/a (5.0) | 276.1 | n/a |
| MovingBoundaryHeatExchanger | 8.08 | 75.5 | 149.4 |
| SectionedHeatExchanger (50 sections) | 8.08 | 75.5 | 150.2 |
| SectionedHeatExchanger (6 sections) | 8.08 | 75.5 | 149.9 |
MovingBoundary and Sectioned models¶
Comparing these two models, we see almost identical results in the cases shown above. However, this is not necessarily the case. There are situations where these models may yield slightly different results. This is specifically the case when there is a curvature in the isobars (e.g. supercritical conditions near the critical point) or when there is a pressure drop in a pure liquid phase nearing the two-phase region.
First, we have a look at different comparisons of the two models, where the performance is very similar.
Comparing different number of sections¶
The following graph shows the comparison of a MovingBoundaryHeatExchanger and
a SectionedHeatExchanger. There is a very small difference in the
desuperheating section.
hx_sectioned.set_attr(num_sections=1)
nw_sectioned.solve("design")
heat_1sec, T_hot_1sec, T_cold_1sec, _, _ = hx_sectioned.calc_sections()
heat_1sec /= 1e6
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot((heat_1sec, heat_1sec), (list(T_hot_1sec), list(T_cold_1sec)), color=annotation_color, linestyle="-", linewidth=0.5)
ax.plot(heat_50secs, T_hot_50secs, "o-", color=annotation_color, markersize=2, linewidth=0.5, label="SectionedHeatExchanger (50 sections)")
ax.plot(heat_1sec, T_hot_1sec, "o-", color="red", label="0D heat exchanger")
ax.plot(heat_1sec, T_cold_1sec, "o-", color="blue")
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
ax.legend()
plt.show()
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 2.33e-10 | 100 % | 5.57e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2456.40
And two sectioned models with different numbers of sections, where we can see that already with a few sections we can yield quite good results.
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot((heat_6secs, heat_6secs), (list(T_hot_6secs), list(T_cold_6secs)), color=annotation_color, linestyle="-", linewidth=0.5)
ax.plot(heat_50secs, T_hot_50secs, "o-", color=annotation_color, markersize=2, linewidth=0.5, label="SectionedHeatExchanger (50 sections)")
ax.plot(heat_6secs, T_hot_6secs, "o-", color="red", label="SectionedHeatExchanger (6 sections)")
ax.plot(heat_6secs, T_cold_6secs, "o-", color="blue")
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
ax.legend()
plt.show()
Considering pressure drop¶
Since both types of models can consider pressure drop (in the same way), they match quite well again.
hx_sectioned = SectionedHeatExchanger("heatexchanger")
nw_sectioned, c1, c2, d1, d2 = make_network(hx_sectioned)
hx_sectioned.set_attr(dp1=2, dp2=0.5)
nw_sectioned.solve("design")
heat_sect, T_hot_sect, T_cold_sect, _, _ = hx_sectioned.calc_sections()
heat_sect /= 1e6
hx_mb = MovingBoundaryHeatExchanger("heatexchanger")
nw_mb, c1, c2, d1, d2 = make_network(hx_mb)
hx_mb.set_attr(dp1=2, dp2=0.5)
nw_mb.solve("design")
heat_mb, T_hot_mb, T_cold_mb, _, _ = hx_mb.calc_sections()
heat_mb /= 1e6
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot((heat_mb, heat_mb), (list(T_hot_mb), list(T_cold_mb)), color=annotation_color, linestyle="-", linewidth=0.5)
ax.plot(heat_sect, T_hot_sect, "o-", color=annotation_color, markersize=2, linewidth=0.5, label="SectionedHeatExchanger (50 sections)")
ax.plot(heat_mb, T_hot_mb, "o-", color="red", label="MovingBoundaryHeatExchanger")
ax.plot(heat_mb, T_cold_mb, "o-", color="blue")
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
ax.legend()
plt.show()
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 2.00e+06 | 0 % | 4.79e+01 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 1.34e-05 | 100 % | 3.21e-10 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 2.33e-10 | 100 % | 5.57e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2126.39
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 2.00e+06 | 0 % | 4.79e+01 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 1.34e-05 | 100 % | 3.21e-10 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 2.33e-10 | 100 % | 5.57e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 0.00e+00 | 100 % | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2783.68
Comparing the two again in a different situation: we preheat the working fluid with a relatively high pressure drop towards saturation. The difference shows on the secondary side (blue line).
def make_network_preheating(heatex):
nw = Network()
nw.units.set_defaults(
temperature="°C",
pressure="bar",
pressure_difference="bar",
heat="MW",
heat_transfer_coefficient="kW/K"
)
so1 = Source("source 1")
so2 = Source("source 2")
si1 = Sink("sink 1")
si2 = Sink("sink 2")
c1 = Connection(so1, "out1", heatex, "in1", label="c1")
c2 = Connection(heatex, "out1", si1, "in1", label="c2")
d1 = Connection(so2, "out1", heatex, "in2", label="d1")
d2 = Connection(heatex, "out2", si2, "in1", label="d2")
nw.add_conns(c1, c2, d1, d2)
c1.set_attr(fluid={"water": 1}, p=1, T=80)
c2.set_attr(T=40)
d1.set_attr(fluid={"R290": 1}, T=10, m=5)
d2.set_attr(T_bubble=78, td_bubble=1)
heatex.set_attr(dp1=0, dp2=4)
return nw, c1, c2, d1, d2
hx_sectioned = SectionedHeatExchanger("heatexchanger")
nw_sectioned, c1, c2, d1, d2 = make_network_preheating(hx_sectioned)
nw_sectioned.solve("design")
heat_sect, T_hot_sect, T_cold_sect, _, _ = hx_sectioned.calc_sections()
heat_sect /= 1e6
hx_mb = MovingBoundaryHeatExchanger("heatexchanger")
nw_mb, c1, c2, d1, d2 = make_network_preheating(hx_mb)
nw_mb.solve("design")
heat_mb, T_hot_mb, T_cold_mb, _, _ = hx_mb.calc_sections()
heat_mb /= 1e6
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot((heat_mb, heat_mb), (list(T_hot_mb), list(T_cold_mb)), color=annotation_color, linestyle="-", linewidth=0.5)
ax.plot(heat_sect, T_cold_sect, "o-", color=annotation_color, markersize=2, linewidth=0.5, label="SectionedHeatExchanger (50 sections)")
ax.plot(heat_mb, T_hot_mb, "o-", color="red")
ax.plot(heat_mb, T_cold_mb, "o-", color="blue", label="MovingBoundaryHeatExchanger")
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
ax.legend()
plt.show()
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 7.82e+05 | 1 % | 4.67e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 8.73e-07 | 100 % | 5.21e-12 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 1.16e-10 | 100 % | 6.95e-16 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 1.16e-10 | 100 % | 6.95e-16 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2128.82
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 7.82e+05 | 1 % | 4.67e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 8.73e-07 | 100 % | 5.21e-12 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 1.16e-10 | 100 % | 6.95e-16 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 1.16e-10 | 100 % | 6.95e-16 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2703.39
Curvature of isobars¶
Very similar to the previous comparison, in the supercritical region the sectioning is quite important.
def make_network_supercritical(heatex):
nw = Network()
nw.units.set_defaults(
temperature="°C",
pressure="bar",
pressure_difference="bar",
heat="MW",
heat_transfer_coefficient="kW/K"
)
so1 = Source("source 1")
so2 = Source("source 2")
si1 = Sink("sink 1")
si2 = Sink("sink 2")
c1 = Connection(so1, "out1", heatex, "in1", label="c1")
c2 = Connection(heatex, "out1", si1, "in1", label="c2")
d1 = Connection(so2, "out1", heatex, "in2", label="d1")
d2 = Connection(heatex, "out2", si2, "in1", label="d2")
nw.add_conns(c1, c2, d1, d2)
c1.set_attr(fluid={"R290": 1}, T=150, p=45, m=5)
c2.set_attr(T=90)
d1.set_attr(fluid={"water": 1}, p=2, T=70)
d2.set_attr(T=110)
heatex.set_attr(dp1=0, dp2=0)
return nw, c1, c2, d1, d2
hx_sectioned = SectionedHeatExchanger("heatexchanger")
nw_sectioned, c1, c2, d1, d2 = make_network_supercritical(hx_sectioned)
nw_sectioned.solve("design")
heat_sect, T_hot_sect, T_cold_sect, _, _ = hx_sectioned.calc_sections()
heat_sect /= 1e6
hx_mb = MovingBoundaryHeatExchanger("heatexchanger")
nw_mb, c1, c2, d1, d2 = make_network_supercritical(hx_mb)
nw_mb.solve("design")
heat_mb, T_hot_mb, T_cold_mb, _, _ = hx_mb.calc_sections()
heat_mb /= 1e6
heatex = SectionedHeatExchanger("heatexchanger")
fig, ax = plt.subplots(1, figsize=(10, 6))
ax.plot((heat_mb, heat_mb), (list(T_hot_mb), list(T_cold_mb)), color=annotation_color, linestyle="-", linewidth=0.5)
ax.plot(heat_sect, T_hot_sect, "o-", color=annotation_color, markersize=2, linewidth=0.5, label="SectionedHeatExchanger (50 sections)")
ax.plot(heat_mb, T_hot_mb, "o-", color="red", label="MovingBoundaryHeatExchanger")
ax.plot(heat_mb, T_cold_mb, "o-", color="blue")
ax.set_ylabel("temperature in K")
ax.set_xlabel("heat transferred in MW")
ax.legend()
plt.show()
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 1.39e+06 | 0 % | 8.23e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 2.64e-07 | 100 % | 1.57e-12 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 2.33e-10 | 100 % | 1.38e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 2.33e-10 | 100 % | 1.38e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2161.18
iter | residual | progress | massflow | pressure | enthalpy | fluid | component
-------+------------+------------+------------+------------+------------+------------+------------
1 | 1.39e+06 | 0 % | 8.23e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
2 | 2.64e-07 | 100 % | 1.57e-12 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
3 | 2.33e-10 | 100 % | 1.38e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
4 | 2.33e-10 | 100 % | 1.38e-15 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00
Total iterations: 4, Calculation time: 0.00 s, Iterations per second: 2827.78