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

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 

8 

9__all__ = ["get_additional_zonal_pinch_analysis"] 

10 

11####################################################################################################### 

12# Public API --- TODO 

13####################################################################################################### 

14 

15def get_additional_zonal_pinch_analysis(pt: ProblemTable, pt_real: ProblemTable, config: Configuration): 

16 """Calculates additional graphs and targets.""" 

17 

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) 

23 

24 

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) 

28 

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) 

33 

34 # # Target co-generation of heat and power 

35 # if z.config.TURBINE_WORK_BUTTON: 

36 # z = get_power_cogeneration_above_pinch(z) 

37 

38 # # Save data for TS profiles based on HT direction 

39 

40 return { 

41 "area": area, 

42 "num_units": num_units, 

43 "capital_cost": capital_cost, 

44 "annual_capital_cost": annual_capital_cost, 

45 } 

46 

47 

48####################################################################################################### 

49# Helper functions 

50####################################################################################################### 

51 

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.") 

56 

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 

62 

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']) 

68 

69 delta_T1 = t_h1 - t_c1 

70 delta_T2 = t_h2 - t_c2 

71 

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 ) 

77 

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) 

82 

83 eff = dh / (cp_min * (t_h1 - t_c2)) 

84 cp_star = cp_min / cp_max 

85 

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 

92 

93 ntu = np.vectorize(HX_NTU)(arrangement, eff, cp_star) 

94 

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) 

98 

99 area_segments = ntu * cp_min / u_o 

100 total_area = np.sum(area_segments) 

101 

102 return float(total_area) 

103 

104 

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. 

108 

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'. 

113 

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 

120 

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 

127 

128 i_1 = i 

129 i += 1 

130 

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] 

136 

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 ) 

145 

146 num_hx += count_crossing(z.hot_streams) 

147 num_hx += count_crossing(z.cold_streams) 

148 

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 ) 

156 

157 num_hx += count_utility_crossing(z.hot_utilities) 

158 num_hx += count_utility_crossing(z.cold_utilities) 

159 num_hx -= 1 

160 

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 

166 

167 i = j 

168 i_1 = j 

169 

170 i += 1 

171 

172 return int(num_hx) 

173 

174 

175############# Review and testing needed!!!!!!!!!!!!!! 

176 

177 

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 

189 

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) 

192 

193# z.exergy_req_min = GCC_X[1][1] 

194# z.exergy_des_min = GCC_X[1][-1] 

195 

196# return GCC_X 

197 

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]) 

206 

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 

213 

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 

220 

221# n_ETE = x_sink / x_source if x_source > ZERO else 0 

222 

223# return x_source, x_sink, n_ETE 

224 

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 

230 

231 # Calculates the area table 

232 H_val = [0 for i in range(len(BCC[0]) * 2)] 

233 

234 ColT = 0 

235 ColRH = 1 

236 ColHCC = 2 

237 ColRC = 3 

238 ColCCC = 4 

239 

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...') 

243 

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] 

248 

249 H_val = H_val.sort(reverse=True) 

250 

251 CalcTable = [ [None for j in range(len(H_val) - 1)] for i in range(10)] 

252 

253 for i in range(len(H_val) - 1): 

254 CalcTable[0][i] = H_val[i] 

255 CalcTable[1][i] = H_val[i + 1] 

256 

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 

264 

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 

268 

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]) 

273 

274 dh = CalcTable[0][i] - CalcTable[1][i] 

275 

276 T_LMTD = find_LMTD(T_h1, T_h2, T_c1, T_c2) 

277 CalcTable[2][i] = T_LMTD 

278 

279 CP_hot = dh / (T_h1 - T_h2) 

280 CP_cold = dh / (T_c1 - T_c2) 

281 

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 

286 

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 

294 

295 Ntu = HX_NTU(Arrangement, eff, CP_star) 

296 

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) 

301 

302 CalcTable[3][i] = Ntu * CP_min / U_o 

303 CalcTable[4][i] = dh / (U_o * T_LMTD) 

304 

305 Area = Area + CalcTable[3][i] 

306 

307 return Area 

308 

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 

318 

319 i_1 = i 

320 i = i + 1 

321 while i < len(pt[0]): 

322 i_0 = i_1 

323 

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] 

328 

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 

335 

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 

342 

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 

348 

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 

354 

355 Num_HX -= 1 

356 

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 

362 

363 i = j 

364 i_1 = j 

365 

366 i += 1 

367 

368 return Num_HX 

369 

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 

379 

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 

403 

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 

409 

410# return GCC_X 

411 

412 

413 

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