Coverage for backend/pinch_service/OpenPinch/src/analysis/additional_analysis.py: 5%
184 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
1import pandas as pd
2import numpy as np
3from ..utils import *
4from ..lib.enums import *
5from ..classes import *
6from .support_methods import *
7from .power_cogeneration_analysis import get_power_cogeneration_above_pinch
9__all__ = ["get_additional_zonal_pinch_analysis"]
11#######################################################################################################
12# Public API --- TODO
13#######################################################################################################
15def get_additional_zonal_pinch_analysis(pt: ProblemTable, pt_real: ProblemTable, config: Configuration):
16 """Calculates additional graphs and targets."""
18 # Target heat transfer area and number of exchanger units based on Balanced CC
19 area = target_area(pt_real)
20 num_units = min_number_hx(pt)
21 capital_cost = num_units * config.FC + num_units * config.VC * (area / num_units) ** config.EXP
22 annual_capital_cost = capital_cost * capital_recovery_factor(config.DISCOUNT_RATE, config.SERV_LIFE)
25 # # Target exergy supply, rejection, and destruction
26 # gcc_x = _calc_exergy_gcc(z, pt_real, bcc, z.graphs[GT.GCC_Act.value])
27 # z.add_graph(GT.GCC_X.value, gcc_x)
29 # # Requires review and comparision to previous Excel implementation
30 # GCC_AI = None
31 # if z.config.AHT_BUTTON_SELECTED:
32 # z.add_graph('GCC_AI', GCC_AI)
34 # # Target co-generation of heat and power
35 # if z.config.TURBINE_WORK_BUTTON:
36 # z = get_power_cogeneration_above_pinch(z)
38 # # Save data for TS profiles based on HT direction
40 return {
41 "area": area,
42 "num_units": num_units,
43 "capital_cost": capital_cost,
44 "annual_capital_cost": annual_capital_cost,
45 }
48#######################################################################################################
49# Helper functions
50#######################################################################################################
52def target_area(z, pt: ProblemTable) -> float:
53 """Estimates a heat transfer area target based on counter-current heat transfer using vectorized pandas operations."""
54 if abs(pt['HCC'].iloc[0] - pt['CCC'].iloc[0]) > ZERO:
55 raise ValueError("Balanced Composite Curves are imbalanced.")
57 # Collect H_val intervals and sort
58 h_vals = pd.Series(pt['HCC'].iloc[:-1].tolist() + pt['CCC'].iloc[:-1].tolist()).sort_values().reset_index(drop=True)
59 h_start = h_vals[:-1].values
60 h_end = h_vals[1:].values
61 dh = h_start - h_end
63 # Interpolate temperatures for each H at both ends
64 t_h1 = np.interp(h_start, pt['HCC'], pt['T'])
65 t_h2 = np.interp(h_end, pt['HCC'], pt['T'])
66 t_c1 = np.interp(h_start, pt['CCC'], pt['T'])
67 t_c2 = np.interp(h_end, pt['CCC'], pt['T'])
69 delta_T1 = t_h1 - t_c1
70 delta_T2 = t_h2 - t_c2
72 t_lmtd = np.where(
73 abs(delta_T1 - delta_T2) < 1e-6,
74 (delta_T1 + delta_T2) / 2,
75 (delta_T1 - delta_T2) / np.log(delta_T1 / delta_T2)
76 )
78 cp_hot = dh / (t_h1 - t_h2)
79 cp_cold = dh / (t_c1 - t_c2)
80 cp_min = np.minimum(cp_hot, cp_cold)
81 cp_max = np.maximum(cp_hot, cp_cold)
83 eff = dh / (cp_min * (t_h1 - t_c2))
84 cp_star = cp_min / cp_max
86 if z.config.CF_SELECTED:
87 arrangement = HX.CF.value
88 elif z.config.PF_SELECTED:
89 arrangement = HX.PF.value
90 else:
91 arrangement = HX.ShellTube.value
93 ntu = np.vectorize(HX_NTU)(arrangement, eff, cp_star)
95 r_hot = np.interp(h_end, pt['HCC'], pt['RH'])
96 r_cold = np.interp(h_end, pt['CCC'], pt['RC'])
97 u_o = 1 / (r_hot + r_cold)
99 area_segments = ntu * cp_min / u_o
100 total_area = np.sum(area_segments)
102 return float(total_area)
105def min_number_hx(z, pt_df: ProblemTable, bcc_star_df: ProblemTable) -> int:
106 """
107 Estimates the minimum number of heat exchangers required for the pinch problem using vectorized interval logic.
109 Args:
110 z: Zone with hot/cold streams and utilities.
111 pt_df (ProblemTable): Problem table DataFrame with temperature column.
112 bcc_star_df (ProblemTable): Balanced Composite Curve data with 'CCC' and 'HCC'.
114 Returns:
115 int: Minimum number of exchangers.
116 """
117 T_vals = pt_df.iloc[:, 0].values
118 CCC = bcc_star_df['CCC'].values
119 HCC = bcc_star_df['HCC'].values
121 num_hx = 0
122 i = 0
123 while i < len(T_vals) - 1:
124 if abs(CCC[i + 1] - HCC[i + 1]) > ZERO:
125 break
126 i += 1
128 i_1 = i
129 i += 1
131 while i < len(T_vals):
132 i_0 = i_1
133 if abs(CCC[i] - HCC[i]) < ZERO or i == len(T_vals) - 1:
134 i_1 = i
135 T_high, T_low = T_vals[i_0], T_vals[i_1]
137 def count_crossing(streams):
138 t_max = np.array([s.t_max_star for s in streams])
139 t_min = np.array([s.t_min_star for s in streams])
140 return np.sum(
141 ((t_max > T_low + ZERO) & (t_max <= T_high + ZERO)) |
142 ((t_min >= T_low - ZERO) & (t_min < T_high - ZERO)) |
143 ((t_min < T_low - ZERO) & (t_max > T_high + ZERO))
144 )
146 num_hx += count_crossing(z.hot_streams)
147 num_hx += count_crossing(z.cold_streams)
149 def count_utility_crossing(utilities):
150 t_max = np.array([u.t_max_star for u in utilities])
151 t_min = np.array([u.t_min_star for u in utilities])
152 return np.sum(
153 (t_max > T_low + ZERO) & (t_max <= T_high + ZERO) |
154 (t_min >= T_low - ZERO) & (t_min < T_high - ZERO)
155 )
157 num_hx += count_utility_crossing(z.hot_utilities)
158 num_hx += count_utility_crossing(z.cold_utilities)
159 num_hx -= 1
161 j = i_1
162 while j < len(T_vals) - 1:
163 if abs(CCC[j + 1] - HCC[j + 1]) > ZERO:
164 break
165 j += 1
167 i = j
168 i_1 = j
170 i += 1
172 return int(num_hx)
175############# Review and testing needed!!!!!!!!!!!!!!
178############# Review and testing needed!!!!!!!!!!!!!!
179# def _calc_exergy_gcc(z, pt_real, BCC, GCC_Act):
180# """Determine Exergy Transfer Effectiveness including process and utility streams.
181# """
182# # Exergy Transfer Effectiveness proposed by Marmolejo-Correa, D., Gundersen, T., 2012.
183# # A comparison of exergy efficiency definitions with focus on low temperature processes.
184# # Energy 44, 477–489. https://doi.org/10.1016/j.energy.2012.06.001
185# x_source, x_sink, n_ETE = _calc_total_exergy(BCC)
186# z.exergy_sources = x_source
187# z.exergy_sinks = x_sink
188# z.ETE = n_ETE
190# GCC_X = z.Calc_ExGCC(GCC_Act)
191# x_source, x_sink, n_ETE = _calc_total_exergy(pt_real, Col_T=0, Col_HCC=4, Col_CCC=7)
193# z.exergy_req_min = GCC_X[1][1]
194# z.exergy_des_min = GCC_X[1][-1]
196# return GCC_X
198############# Review and testing needed!!!!!!!!!!!!!!
199# def _calc_total_exergy(z: Zone, CC, x_source=0, x_sink=0, n_ETE=0, Col_T=0, Col_HCC=2, Col_CCC=4):
200# """Determines the source and sink exergy of a balanced CC."""
201# for i in range(1, len(CC[0])):
202# T_ex1 = compute_exergetic_temperature(CC[Col_T][i - 1], T_ref=z.config.TEMP_REF)
203# T_ex2 = compute_exergetic_temperature(CC[Col_T][i], T_ref=z.config.TEMP_REF)
204# CP_hot = (CC[Col_HCC][i - 1] - CC[Col_HCC][i]) / (CC[Col_T][i - 1] - CC[Col_T][i])
205# CP_cold = (CC[Col_CCC][i - 1] - CC[Col_CCC][i]) / (CC[Col_T][i - 1] - CC[Col_T][i])
207# if T_ex1 > 0:
208# x_source = x_source + CP_hot * T_ex1
209# x_sink = x_sink + CP_cold * T_ex1
210# else:
211# x_source = x_source + CP_cold * T_ex1
212# x_sink = x_sink + CP_hot * T_ex1
214# if T_ex2 > 0:
215# x_source = x_source - CP_hot * T_ex2
216# x_sink = x_sink - CP_cold * T_ex2
217# else:
218# x_source = x_source - CP_cold * T_ex2
219# x_sink = x_sink - CP_hot * T_ex2
221# n_ETE = x_sink / x_source if x_source > ZERO else 0
223# return x_source, x_sink, n_ETE
225############# Review and testing needed!!!!!!!!!!!!!!
226def Target_Area(z, BCC):
227 """Estimates a heat transfer area target for a z based on counter-current heat transfer.
228 """
229 Area = 0
231 # Calculates the area table
232 H_val = [0 for i in range(len(BCC[0]) * 2)]
234 ColT = 0
235 ColRH = 1
236 ColHCC = 2
237 ColRC = 3
238 ColCCC = 4
240 # Check the BCC is balanced, if not stop the calculation and return an error
241 if abs(BCC[ColHCC][0] - BCC[ColCCC][0]) > ZERO:
242 raise Exception('Balanced Composite Curves are imbalanced...')
244 # Collate all H intervals
245 for i in range(1, len(BCC[0])):
246 H_val[i * 2 - 2] = BCC[ColHCC][i - 1]
247 H_val[i * 2 - 1] = BCC[ColCCC][i - 1]
249 H_val = H_val.sort(reverse=True)
251 CalcTable = [ [None for j in range(len(H_val) - 1)] for i in range(10)]
253 for i in range(len(H_val) - 1):
254 CalcTable[0][i] = H_val[i]
255 CalcTable[1][i] = H_val[i + 1]
257 r_h = 0
258 r_c = 0
259 for i in range(len(CalcTable[0])):
260 while (CalcTable[0][i] - BCC[ColHCC][r_h + 1]) <= ZERO and r_h + 2 <= len(BCC[0]):
261 r_h += 1
262 while (CalcTable[0][i] - BCC[ColCCC][r_c + 1]) <= ZERO and r_c + 2 <= len(BCC[0]):
263 r_c += 1
265 if (CalcTable[0][i] - BCC[ColHCC][r_h + 1] <= ZERO or CalcTable[0][i] - BCC[ColCCC][r_c + 1] <= ZERO) \
266 and (r_h + 1 == len(BCC[0]) or r_c + 1 == len(BCC[0])):
267 break
269 T_h1 = linear_interpolation(CalcTable[0][i], BCC[ColHCC][r_h], BCC[ColHCC][r_h + 1], BCC[ColT][r_h], BCC[ColT][r_h + 1])
270 T_h2 = linear_interpolation(CalcTable[1][i], BCC[ColHCC][r_h], BCC[ColHCC][r_h + 1], BCC[ColT][r_h], BCC[ColT][r_h + 1])
271 T_c1 = linear_interpolation(CalcTable[0][i], BCC[ColCCC][r_c], BCC[ColCCC][r_c + 1], BCC[ColT][r_c], BCC[ColT][r_c + 1])
272 T_c2 = linear_interpolation(CalcTable[1][i], BCC[ColCCC][r_c], BCC[ColCCC][r_c + 1], BCC[ColT][r_c], BCC[ColT][r_c + 1])
274 dh = CalcTable[0][i] - CalcTable[1][i]
276 T_LMTD = find_LMTD(T_h1, T_h2, T_c1, T_c2)
277 CalcTable[2][i] = T_LMTD
279 CP_hot = dh / (T_h1 - T_h2)
280 CP_cold = dh / (T_c1 - T_c2)
282 CP_min = min(CP_hot, CP_cold)
283 CP_max = max(CP_hot, CP_cold)
284 eff = dh / (CP_min * (T_h1 - T_c2))
285 CP_star = CP_min / CP_max
287 Arrangement = None
288 if z.config.CF_SELECTED:
289 Arrangement = HX.CF.value
290 elif z.config.PF_SELECTED:
291 Arrangement = HX.PF.value
292 else:
293 Arrangement = HX.ShellTube.value
295 Ntu = HX_NTU(Arrangement, eff, CP_star)
297 # Heat transfer resistance and coefficient
298 R_hot = BCC[ColRH][r_h + 1]
299 R_cold = BCC[ColRC][r_c + 1]
300 U_o = 1 / (R_hot + R_cold)
302 CalcTable[3][i] = Ntu * CP_min / U_o
303 CalcTable[4][i] = dh / (U_o * T_LMTD)
305 Area = Area + CalcTable[3][i]
307 return Area
309def MinNumberHX(z, pt, BCC_star):
310 """Estimates the minimum number of heat exchanger units for a given Pinch problem.
311 """
312 Num_HX = 0
313 i = 0
314 while i < len(pt[0]) - 1:
315 if abs(BCC_star[4][i + 1] - BCC_star[2][i + 1]) > ZERO:
316 break
317 i += 1
319 i_1 = i
320 i = i + 1
321 while i < len(pt[0]):
322 i_0 = i_1
324 if abs(BCC_star[4][i] - BCC_star[2][i]) < ZERO or i == len(pt[0]) - 1:
325 i_1 = i
326 T_high = pt[0][i_0]
327 T_low = pt[0][i_1]
329 for s in z.hot_streams:
330 T_max = s.t_max_star
331 T_min = s.t_min_star
332 if (T_max > T_low + ZERO and T_max <= T_high + ZERO) or (T_min >= T_low - ZERO \
333 and T_min < T_high - ZERO) or (T_min < T_low - ZERO and T_max > T_high + ZERO):
334 Num_HX += 1
336 for s in z.cold_streams:
337 T_max = s.t_max_star
338 T_min = s.t_min_star
339 if (T_max > T_low + ZERO and T_max <= T_high + ZERO) or (T_min >= T_low - ZERO \
340 and T_min < T_high - ZERO) or (T_min < T_low - ZERO and T_max > T_high + ZERO):
341 Num_HX += 1
343 for utility_k in z.hot_utilities:
344 T_max = utility_k.t_max_star
345 T_min = utility_k.t_min_star
346 if (T_max > T_low + ZERO and T_max <= T_high + ZERO) or (T_min >= T_low - ZERO and T_min < T_high - ZERO):
347 Num_HX += 1
349 for utility_k in z.cold_utilities:
350 T_max = utility_k.t_max_star
351 T_min = utility_k.t_min_star
352 if (T_max > T_low + ZERO and T_max <= T_high + ZERO) or (T_min >= T_low - ZERO and T_min < T_high - ZERO):
353 Num_HX += 1
355 Num_HX -= 1
357 j = i_1
358 while j < len(pt[0]) - 1:
359 if abs(BCC_star[4][j + 1] - BCC_star[2][j + 1]) > ZERO:
360 break
361 j += 1
363 i = j
364 i_1 = j
366 i += 1
368 return Num_HX
370# def Calc_ExGCC(z, GCC_Act):
371# """Transposes a normal GCC (T-h) into a exergy GCC (Tx-X).
372# """
373# GCC_X = copy.deepcopy(GCC_Act)
374# Min_X = 0
375# AbovePT = True
376# GCC_X[0][0] = compute_exergetic_temperature(GCC_Act[0][0] + z.config.DTCONT / 2, T_ref=z.config.TEMP_REF)
377# GCC_X[1][0] = 0
378# i_upper = len(GCC_X[0]) + 1
380# # Transpose to exergetic temperature and exergy flow
381# i = 1
382# gcc_act_i = 1
383# while i <= i_upper and gcc_act_i < len(GCC_Act[0]):
384# if AbovePT:
385# GCC_X[0][i] = compute_exergetic_temperature(GCC_Act[0][gcc_act_i] + z.config.DTCONT / 2, T_ref=z.config.TEMP_REF)
386# GCC_X[1][i] = (GCC_Act[1][gcc_act_i - 1] - GCC_Act[1][gcc_act_i]) / (GCC_Act[0][gcc_act_i - 1] - GCC_Act[0][gcc_act_i])
387# GCC_X[1][i] = GCC_X[1][i - 1] - GCC_X[1][i] * (GCC_X[0][i - 1] - GCC_X[0][i])
388# if GCC_Act[1][gcc_act_i] < ZERO:
389# Min_X = GCC_X[1][i]
390# for row in GCC_X:
391# row += [0, 0]
392# i += 2
393# gcc_act_i += 1
394# GCC_X[0][i] = compute_exergetic_temperature(GCC_Act[0][gcc_act_i - 1] - z.config.DTCONT / 2, T_ref=z.config.TEMP_REF)
395# GCC_X[1][i] = GCC_X[1][i - 1]
396# AbovePT = False
397# else:
398# GCC_X[0][i] = compute_exergetic_temperature(GCC_Act[0][gcc_act_i - 1] - z.config.DTCONT / 2, T_ref=z.config.TEMP_REF)
399# GCC_X[1][i] = (GCC_Act[1][gcc_act_i - 2] - GCC_Act[1][gcc_act_i - 1]) / (GCC_Act[0][gcc_act_i - 2] - GCC_Act[0][gcc_act_i - 1])
400# GCC_X[1][i] = GCC_X[1][i - 1] - GCC_X[1][i] * (GCC_X[0][i - 1] - GCC_X[0][i])
401# i += 1
402# gcc_act_i += 1
404# # Shift Exergy GCC appropriately
405# for i in range(1, len(GCC_X[0])):
406# GCC_X[1][i] = GCC_X[1][i] + abs(Min_X)
407# if abs(GCC_X[1][i]) < ZERO:
408# GCC_X[1][i] = 0
410# return GCC_X
414# def Calc_GCC_AI(z, pt_real, gcc_np):
415# """Returns a simplified array for the assisted integration GCC.
416# """
417# GCC_AI = [ [ None for j in range(len(pt_real[0]))] for i in range(2)]
418# for i in range(len(pt_real[0])):
419# GCC_AI[0][i] = pt_real[0][i]
420# GCC_AI[1][i] = pt_real[PT.H_NET.value][i] - gcc_np[1][i]
421# return GCC_AI