diff --git a/pyproject.toml b/pyproject.toml index a0f680f8b..8e265d3e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/tespy/tools/pinch_analysis.py b/src/tespy/tools/pinch_analysis.py new file mode 100644 index 000000000..ac9667d92 --- /dev/null +++ b/src/tespy/tools/pinch_analysis.py @@ -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 \ No newline at end of file diff --git a/tutorial/advanced/example_pinch_analysis.py b/tutorial/advanced/example_pinch_analysis.py new file mode 100644 index 000000000..b1d32e686 --- /dev/null +++ b/tutorial/advanced/example_pinch_analysis.py @@ -0,0 +1,171 @@ + +from tespy.tools.pinch_analysis import TesypPinchAnalysis + + +# integrating a tespy model of a heat pump into a given process + +# example Pinch Analysis of a manual workflow without tespy components +Example_Analysis = TesypPinchAnalysis("Example_Process_1") + +# setting the minimum temperature difference for the analysis +Example_Analysis.set_minimum_temperature_difference(10) + +# add all the streams manually +# Example: Kemp 2007 p. 20, reduced temperature by 50 degC to fit the heat pump example +Example_Analysis.add_cold_stream_manually(-230, 20-50, 135-50) +Example_Analysis.add_hot_stream_manually(330, 170-50, 60-50) +Example_Analysis.add_cold_stream_manually(-240, 80-50, 140-50) +Example_Analysis.add_hot_stream_manually(180, 150-50, 30-50) +# additional latent streams as shown by Arpagaus 2019 p. 99 +Example_Analysis.add_hot_stream_manually(60,35,35) +Example_Analysis.add_cold_stream_manually(-40,80,80) + +# with all streams added, get the results as in pina +cold_cc_data = Example_Analysis.get_cold_cc() +hot_cc_data = Example_Analysis.get_hot_cc() +shifted_cold_cc_data = Example_Analysis.get_shifted_cold_cc() +shifted_hot_cc_data = Example_Analysis.get_shifted_hot_cc() +gcc_data = Example_Analysis.get_gcc() + +# this data can be used for other aspects like combining with tespy heat exchangers +# to plot the results use the following functions +# the composite curves +Example_Analysis.plot_cc_diagram() +# the shifted composite curves touching in the pinch point +Example_Analysis.plot_shifted_cc_diagram() +# the grand composite curve +Example_Analysis.plot_gcc_diagram() + + +# setting up the heat pump system +from tespy.networks import Network +from tespy.connections import Connection +from tespy.components import SimpleHeatExchanger, Valve, Compressor, CycleCloser + +# network +nw = Network() + +# taken from example heat pump +nw.units.set_defaults( + temperature="degC", pressure="bar", enthalpy="kJ/kg", heat="kW", power="kW" +) + +# components +condenser = SimpleHeatExchanger("Condenser") +evaporator = SimpleHeatExchanger("Evaporator") +desuperheater = SimpleHeatExchanger("Desuperheater") +expansion_valve = Valve("Expansion Valve") +compressor = Compressor("Compressor") +cycle_closer = CycleCloser("Cycle Closer") + +# connections +c1 = Connection(evaporator, "out1", compressor,"in1", label = "connection 1") +c2 = Connection(compressor, "out1", desuperheater, "in1", "connection 2") +c3 = Connection(desuperheater, "out1", condenser, "in1", label = "connection 3") +c4 = Connection(condenser, "out1", expansion_valve, "in1", label = "connection 4") +c5 = Connection(expansion_valve, "out1",cycle_closer, "in1", label = "connection 5") +c6 = Connection(cycle_closer, "out1", evaporator, "in1", label = "connection 6") +nw.add_conns(c1,c2,c3,c4,c5,c6) + +# set up general parameters of heat pump +compressor.set_attr(eta_s = 0.7) +condenser.set_attr(dp=0) +desuperheater.set_attr(dp=0) +evaporator.set_attr(dp=0) +c1.set_attr(fluid={"R290": 1}) + +# set up parameters to show specific case, the desuperheater reduces the temperature to the dewline +# in that case only the condensation is part of the condenser forming a horizontal line in the GCC as +# the most simple example case +c1.set_attr(m=0.1, p=5, x=1) +c3.set_attr(p=20, x=1) +c4.set_attr(x=0) + +# solve design +nw.solve("design") + +# reference heat pump components for plotting in the GCC +Example_Analysis.show_heat_pump_in_gcc(condenser=condenser,evaporator=evaporator) + + + +# second example using a moving boundary heat exchanger as the evaporator and a sectioned heat exchanger as the condenser +# as the heat exchangers now include the internal H,T-Data, the desuperheater is not included anymore. +# However, the connection numbers are kept to clarifiy the positions. + +# setting up the heat pump system +from tespy.components import MovingBoundaryHeatExchanger, SectionedHeatExchanger, Sink, Source + +# network +nw_2 = Network() + +# taken from example heat pump +nw_2.units.set_defaults( + temperature="degC", pressure="bar", enthalpy="kJ/kg", heat="kW", power="kW" +) + +# components +condenser_2 = SectionedHeatExchanger("Condenser_2") +evaporator_2 = MovingBoundaryHeatExchanger("Evaporator_2") +expansion_valve_2 = Valve("Expansion Valve_2") +compressor_2 = Compressor("Compressor_2") +cycle_closer_2 = CycleCloser("Cycle Closer_2") +# Sinks and Sources for the secondary media +HeatSinkIn = Source("HeatSinkIn") +HeatSinkOut = Sink("HeatSinkOut") +HeatSourceIn = Source("HeatSourceIn") +HeatSourceOut = Sink("HeatSourceOut") + +# connections +c1_2 = Connection(evaporator_2, "out2", compressor_2,"in1", label = "connection 1_2") +c2_2 = Connection(compressor_2, "out1", condenser_2, "in1", "connection 2_2") +c4_2 = Connection(condenser_2, "out1", expansion_valve_2, "in1", label = "connection 4_2") +c5_2 = Connection(expansion_valve_2, "out1",cycle_closer_2, "in1", label = "connection 5_2") +c6_2 = Connection(cycle_closer_2, "out1", evaporator_2, "in2", label = "connection 6_2") +nw_2.add_conns(c1_2,c2_2,c4_2,c5_2,c6_2) + +# add the connections for secondary media +c7_2 = Connection(HeatSinkIn, "out1", condenser_2, "in2", label = "connection 7_2") +c8_2 = Connection(condenser_2, "out2", HeatSinkOut, "in1", label = "connection 8_2") +c9_2 = Connection(HeatSourceIn, "out1", evaporator_2, "in1", label = "connection 9_2") +c10_2 = Connection(evaporator_2, "out1", HeatSourceOut, "in1", label = "connection 10_2") +nw_2.add_conns(c7_2,c8_2,c9_2,c10_2) + +# set up general parameters of heat pump +compressor_2.set_attr(eta_s = 0.7) +condenser_2.set_attr(dp1=0, dp2=0, td_pinch=2) +evaporator_2.set_attr(dp1=0, dp2=0, td_pinch=2) +c1_2.set_attr(fluid={"R290": 1}) +# media, temperatures and pressure of heat source and sink +c7_2.set_attr(fluid={"Water": 1}, T=50, p=1) +c8_2.set_attr(T=55) +c9_2.set_attr(fluid={"Water": 1}, T=10, p=1) +c10_2.set_attr(T=5) + +# set up parameters to show specific case +c1_2.set_attr(m=0.1, x=1) +c4_2.set_attr(x=0) + +# solve design +nw_2.solve("design") + +# integrating a tespy model of a heat pump into a given process + +# example Pinch Analysis of a manual workflow without tespy components +Example_Analysis2 = TesypPinchAnalysis("Example_Process_2") + +# setting the minimum temperature difference for the analysis +Example_Analysis2.set_minimum_temperature_difference(10) + +# add all the streams manually +# Example: Kemp 2007 p. 20, reduced temperature by 50 degC to fit the heat pump example +Example_Analysis2.add_cold_stream_manually(-230, 20-50, 135-50) +Example_Analysis2.add_hot_stream_manually(330, 170-50, 60-50) +Example_Analysis2.add_cold_stream_manually(-240, 80-50, 140-50) +Example_Analysis2.add_hot_stream_manually(180, 150-50, 30-50) +# additional latent streams as shown by Arpagaus 2019 p. 99 +Example_Analysis2.add_hot_stream_manually(60,35,35) +Example_Analysis2.add_cold_stream_manually(-40,80,80) + +# reference heat pump components for plotting in the GCC of the same pinch analysis +Example_Analysis2.show_heat_pump_in_gcc(condenser=condenser_2,evaporator=evaporator_2) \ No newline at end of file