Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
3b2fb18
Adding first elements of pinch analysis using pina 0.1.1
n-arth Nov 23, 2025
859a918
ading and correcting comments
n-arth Nov 30, 2025
8321941
adding plotting functions and example for use. Example doubles as tes…
n-arth Nov 30, 2025
7b7336f
adding ISBN for one source
n-arth Nov 30, 2025
c7ef808
Add pina dependency
fwitte Dec 1, 2025
6d283c6
starting heat pump example
n-arth Dec 3, 2025
cdea93c
Merge branch 'pinch' of https://github.com/n-arth/tespy into pinch
n-arth Dec 3, 2025
87b175f
adding simple heat pump & showing heat pump in GCC with dummy tempera…
n-arth Dec 3, 2025
c762370
adding pinch point marker in GCC and getting pinch temperature
n-arth Dec 3, 2025
c91b013
added comment for todo in plotting
n-arth Dec 3, 2025
35a9c7d
adding more pina functions for analysis data and plot markers for uti…
n-arth Dec 3, 2025
8de0369
lowering temperatures for example to match heat pump levels
n-arth Dec 14, 2025
1041da6
finishing the heat pump example
n-arth Dec 14, 2025
be3a2d0
adding values to the composit curve sections
n-arth Dec 14, 2025
b60cf15
adding missing doi of reference
n-arth Dec 21, 2025
f72ceb3
fixing one condition and adding value error for not implemented compo…
n-arth Dec 21, 2025
33f9de6
adding comments
n-arth Dec 21, 2025
1c205f2
adding comments for some tasks
n-arth Dec 21, 2025
3ca3427
adding an example with movingboundaryHX and sectionedHX and starting …
n-arth Dec 27, 2025
067f6ee
minor change for later use
n-arth Dec 27, 2025
0f1c4e3
finishing sectioned HX and moving boundary HX in pinch, added as seco…
n-arth Jan 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies = [
"matplotlib>=3.2.1",
"numpy>=1.13.3",
"pandas>=1.3.0",
"pina>=0.1.1",
"pint",
"scipy",
"tabulate>=0.8.2",
Expand Down
297 changes: 297 additions & 0 deletions src/tespy/tools/pinch_analysis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,297 @@
# using pina 0.1.1 for pinch related tasks
from pina import PinchAnalyzer, make_stream
import matplotlib.pyplot as plt

# Useful literature:
# Arpagaus 2019 Hochtemperatur-Wärmepumpen (ISBN 978-3-8007-4550-0)
# Kemp 2006 (p.20) (https://doi.org/10.1016/B978-0-7506-8260-2.X5001-9)
# Walden et al. 2023 (https://doi.org/10.1016/j.apenergy.2023.121933)


class TesypPinchAnalysis():

def __init__(self, label:str):
self.min_dT = None
self.temp_shift = None
self.streams = []
self.analyzer = None
self.label = label
self.gcc_fig = None
self.gcc_data_enthalpy = None


# including the general functions of pina to expand them later


# setting the value for shifting the composit curces (CCs)
def set_minimum_temperature_difference(self, minimum_temperature_difference:float):
# minimum temperature difference of streams
self.min_dT = minimum_temperature_difference
# the hot and cold composit curve (CC) are shifted by half the minimum temperature difference to meet at the pinch point
self.temp_shift = self.min_dT / 2


# adding cold streams to the used streams, colsd streams have to be heated and form the cold composite curvve (cold CC)
def add_cold_stream_manually(self, enthalpy_difference:float, T_inlet:float, T_outlet:float):
# check if the stream has a positive enthalpy flow difference, pina needs negative sign for hot streams, way to check user
if enthalpy_difference > 0:
print("Got positive enthaply difference, expected negative enthalpy for cold streams. stream not added")
else:
self.streams.append(make_stream(enthalpy_difference,T_inlet,T_outlet))


# adding cold streams to the used streams, colsd streams have to be heated and form the cold composite curvve (hot CC)
def add_hot_stream_manually(self, enthalpy_difference:float, T_inlet:float, T_outlet:float):
# check if the stream has a negative enthalpy flow difference, pina needs positive sign for cold streams, way to check user
if enthalpy_difference < 0:
print("Got positive enthaply difference, expected negative enthalpy for cold streams. stream not added")
else:
self.streams.append(make_stream(enthalpy_difference,T_inlet,T_outlet))


# add: functions to read lists for hot and cold later


# add: functions to remove streams later


# create the pinch analyzer from pina
def _create_analyzer(self):
# check if necessary input is set
if self.temp_shift is not None:
self.analyzer = PinchAnalyzer(self.temp_shift)
else:
print("set minimum temperature difference first")
return
self.analyzer.add_streams(*self.streams) # add a way to add streams later
self._get_analyzer_data()


# get datapoints of hot composite curve
def get_hot_cc(self):
if self.analyzer is None:
self._create_analyzer()
[self.hot_cc_data_enthalpy, self.hot_cc_data_temperature] = self.analyzer.hot_composite_curve


# get datapoints of cold composite curve
def get_cold_cc(self):
if self.analyzer is None:
self._create_analyzer()
[self.cold_cc_data_enthalpy, self.cold_cc_data_temperature] = self.analyzer.cold_composite_curve


# get datapoints of shifted hot composite curve
def get_shifted_hot_cc(self):
if self.analyzer is None:
self._create_analyzer()
[self.shifted_hot_cc_data_enthalpy, self.shifted_hot_cc_data_temperature] = self.analyzer.shifted_hot_composite_curve


# get datapoints of shifted cold composite curve
def get_shifted_cold_cc(self):
if self.analyzer is None:
self._create_analyzer()
[self.shifted_cold_cc_data_enthalpy, self.shifted_cold_cc_data_temperature] = self.analyzer.shifted_cold_composite_curve


# get datapoints of grand composite curve
def get_gcc(self):
if self.analyzer is None:
self._create_analyzer()
[self.gcc_data_enthalpy, self.gcc_data_shifted_temperature] = self.analyzer.grand_composite_curve


def _get_analyzer_data(self):
# get additional analysis data from pina
self.T_pinch = self.analyzer.pinch_temps[0]
self.cold_utility = self.analyzer.cold_utility_target
self.hot_utility = self.analyzer.hot_utility_target
self.heat_recovery = self.analyzer.heat_recovery_target

# needed to check e.g. pinch rules for heat pump integration automatically


def plot_cc_diagram(self, save_fig:bool = True, show_fig:bool = False, return_fig:bool = False):
fig, ax = plt.subplots()
# activate minor ticks
ax.minorticks_on()
# plot subplots with same axes limits
ax.plot(self.hot_cc_data_enthalpy, self.hot_cc_data_temperature, color = "red")
ax.plot(self.cold_cc_data_enthalpy, self.cold_cc_data_temperature, color = "blue")
# add visualization of sections
# get minimum temperature
T_min_CC = min(min(self.hot_cc_data_temperature), min(self.cold_cc_data_temperature))
ax.plot([0,self.cold_utility,self.heat_recovery+self.cold_utility,
self.heat_recovery+self.cold_utility+self.hot_utility],[T_min_CC-5]*4, "o-", color="black")
# adding annotations to the different sections
ax.annotate(f'{self.cold_utility:.0f}', xy=(self.cold_utility/2, T_min_CC-4), horizontalalignment='center', fontsize=10)
ax.annotate(f'{self.heat_recovery:.0f}', xy=(self.cold_utility + self.heat_recovery/2, T_min_CC-4), horizontalalignment='center', fontsize=10)
ax.annotate(f'{self.hot_utility:.0f}', xy=(self.cold_utility + self.heat_recovery + self.hot_utility/2, T_min_CC-4), horizontalalignment='center', fontsize=10)

# set x scale on lowest subplot
ax.tick_params(axis='x', which='major', labelsize=10, rotation = 0)
# set x label
ax.set_xlabel("$\dot{H}$ [kW]", loc='center', fontsize="10")
ax.xaxis.label.set_color("black")
# set y label
ax.set_ylabel("T [°C]", color = "black")
# set grid
ax.grid(visible=True, which='major', color='lightgrey', linewidth = 0.5)
ax.grid(visible=True, which='minor', color='lightgrey', linestyle='dotted', linewidth = 0.5)
# set aspect ratio of subplot
ax.set_box_aspect(1)
ax.set_title(f"Composite Curves of \"{self.label}\"",color = "black", fontsize = 10)

if save_fig:
fig.savefig(f"Composite_Curves_{self.label}.svg")
if show_fig:
fig.show()
if return_fig:
return fig


def plot_shifted_cc_diagram(self, save_fig:bool = True, show_fig:bool = False, return_fig:bool = False):
fig, ax = plt.subplots()
# activate minor ticks
ax.minorticks_on()
# plot subplots with same axes limits
ax.plot(self.shifted_hot_cc_data_enthalpy, self.shifted_hot_cc_data_temperature, color = "red")
ax.plot(self.shifted_cold_cc_data_enthalpy, self.shifted_cold_cc_data_temperature, color = "blue")
# add visualization of sections
# get minimum temperature
T_min_shifted_CC = min(min(self.shifted_hot_cc_data_temperature), min(self.shifted_cold_cc_data_temperature))
ax.plot([0,self.cold_utility,self.heat_recovery+self.cold_utility,
self.heat_recovery+self.cold_utility+self.hot_utility],[T_min_shifted_CC-5]*4, "o-", color="black")
# adding annotations to the different sections
ax.annotate(f'{self.cold_utility:.0f}', xy=(self.cold_utility/2, T_min_shifted_CC-4), horizontalalignment='center', fontsize=10)
ax.annotate(f'{self.heat_recovery:.0f}', xy=(self.cold_utility + self.heat_recovery/2, T_min_shifted_CC-4), horizontalalignment='center', fontsize=10)
ax.annotate(f'{self.hot_utility:.0f}', xy=(self.cold_utility + self.heat_recovery + self.hot_utility/2, T_min_shifted_CC-4), horizontalalignment='center', fontsize=10)
# set x scale on lowest subplot
ax.tick_params(axis='x', which='major', labelsize=10, rotation = 0)
# set x label
ax.set_xlabel("$\dot{H}$ [kW]", loc='center', fontsize="10")
ax.xaxis.label.set_color("black")
# set y label
ax.set_ylabel("shifted T* [°C]", color = "black")
# set grid
ax.grid(visible=True, which='major', color='lightgrey', linewidth = 0.5)
ax.grid(visible=True, which='minor', color='lightgrey', linestyle='dotted', linewidth = 0.5)
# set aspect ratio of subplot
ax.set_box_aspect(1)
ax.set_title(f"Shifted Composite Curves of \"{self.label}\"",color = "black", fontsize = 10)

if save_fig:
fig.savefig(f"Shifted_Composite_Curves_{self.label}.svg")
if show_fig:
fig.show()
if return_fig:
return fig


def plot_gcc_diagram(self, save_fig:bool = True, show_fig:bool = False, return_fig:bool = False):
# plot
fig, ax = plt.subplots()
# activate minor ticks
ax.minorticks_on()
# get GCC data from pina, if not done manually before
if self.gcc_data_enthalpy is None:
self.get_gcc()
# plot subplots with same axes limits
ax.plot(self.gcc_data_enthalpy, self.gcc_data_shifted_temperature, color = "black")
# add pinch point
ax.plot(0, self.T_pinch, "o", color = "black")
# set x scale on lowest subplot
ax.tick_params(axis='x', which='major', labelsize=10, rotation = 0)
# set x label
ax.set_xlabel("$\Delta\dot{H}$ [kW]", loc='center', fontsize="10")
ax.xaxis.label.set_color("black")
# set x limit to 0
ax.set_xlim(xmin=0)
# set y label
ax.set_ylabel("shifted T* [°C]", color = "black")
# set grid
ax.grid(visible=True, which='major', color='lightgrey', linewidth = 0.5)
ax.grid(visible=True, which='minor', color='lightgrey', linestyle='dotted', linewidth = 0.5)
# set aspect ratio of subplot
ax.set_box_aspect(1)
ax.set_title(f"Grand Composite Curve of \"{self.label}\"",color = "black", fontsize = 10)

self.gcc_fig, self.gcc_ax = fig, ax

if save_fig:
fig.savefig(f"Grand_Composite_Curve_{self.label}.svg")
if show_fig:
fig.show()
if return_fig:
return fig


# add: include heat cascades later (for more than one point in time / investigated interval)


# adding components from tespy models

# adding a heat pump in the GCC (by referencing evaporator and condenser)
def show_heat_pump_in_gcc(self, evaporator, condenser):
from tespy.components import SimpleHeatExchanger, MovingBoundaryHeatExchanger, SectionedHeatExchanger

# create GCC diagram, if not done before
try:
# get the GCC
fig = self.gcc_fig
ax = self.gcc_ax
except:
# create GCC without saving fig
self.plot_gcc_diagram(save_fig=False)
# get the GCC
fig = self.gcc_fig
ax = self.gcc_ax

# get the plotting data of the heat exchangers
# condensers
if isinstance(condenser,SimpleHeatExchanger):
# get plot data of heat exchangers at heat sink (taken from user meeting example of heat exchangers)
condenser_Q_vals = [0, abs(condenser.Q.val)]
condenser_T_vals = [condenser.outl[0].T.val,condenser.inl[0].T.val]
# to do: include a warning, if there is at least one change between to or from a latent stream
elif isinstance(condenser, SectionedHeatExchanger) or isinstance(condenser, MovingBoundaryHeatExchanger):
# get the data from calc_sections
condenser_Q_sections, condenser_T_steps_hot, condenser_T_steps_cold, condenser_Q_per_section, condenser_td_log_per_section = condenser.calc_sections()
condenser_Q_vals = [Q / 1000 for Q in condenser_Q_sections] # convert to kW
condenser_T_vals = [T - 273.17 for T in condenser_T_steps_hot] # convert to degC
else:
raise ValueError("The component type is not implemented as a condenser.")

# evaporators
if isinstance(evaporator,SimpleHeatExchanger):
# get plot data of heat exchangers at heat source (taken from user meeting example of heat exchangers)
evaporator_Q_vals = [0, abs(evaporator.Q.val)]
evaporator_T_vals = [evaporator.inl[0].T.val,evaporator.outl[0].T.val]
# to do: include a warning, if there is at least one change between to or from a latent stream
elif isinstance(evaporator, SectionedHeatExchanger) or isinstance(evaporator, MovingBoundaryHeatExchanger):
# get the data from calc_sections
evaporator_Q_sections, evaporator_T_steps_hot, evaporator_T_steps_cold, evaporator_Q_per_section, evaporator_td_log_per_section = evaporator.calc_sections()
evaporator_Q_vals = [Q / 1000 for Q in evaporator_Q_sections] # convert to kW
evaporator_T_vals = [T - 273.17 for T in evaporator_T_steps_cold] # convert to degC
else:
raise ValueError("The component type is not implemented as an evaporator.")

# add: expand these conditions in future for other types
# missing: HeatExchanger, ParallelFlowHeatExchanger, Desuperheater, Condenser


# show (only display) by adding the plot data of the heat exchangers to the GCC
ax.plot(condenser_Q_vals, condenser_T_vals, "-",color="red") # as in heat exchanger example
ax.plot(evaporator_Q_vals, evaporator_T_vals, "-",color="blue")

# save figure
fig.savefig(f"GCC_with_heat_pump_{self.label}.svg")


# add: check heat pump integration by using the integration rules
# see e.g. Arpagaus 2019, Walden et al. 2023
# Temperature and heat flux

# add: def add_heat_exchanger_to_streams(self):
# function to read streams from given heat exchangers, later use to iterate all heat exchangers of network with possibility to exclude streams / heat exchangers
Loading