Coverage for backend/pinch_service/OpenPinch/src/analysis/power_cogeneration_analysis.py: 6%

175 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-11-06 23:27 +0000

1from __future__ import annotations 

2 

3from typing import TYPE_CHECKING 

4from ..utils import * 

5from ..classes import * 

6from .support_methods import * 

7 

8if TYPE_CHECKING: 

9 from ..classes import * 

10 

11__all__ = ["get_power_cogeneration_above_pinch"] 

12 

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

14# Public API --- TODO 

15####################################################################################################### 

16 

17def get_power_cogeneration_above_pinch(z: Zone): 

18 """Calculate the power cogeneration potential above pinch for a given process zone.""" 

19 

20 # === Step 1: Prepare turbine and model parameters === 

21 turbine_params = _prepare_turbine_parameters(z.config) 

22 if turbine_params is None: 

23 return z 

24 

25 # === Step 2: Preprocess utilities === 

26 utility_data = _preprocess_utilities(z, turbine_params) 

27 if utility_data is None: 

28 return z 

29 

30 # === Step 3: Solve turbine work and efficiency === 

31 w_total, Wmax = _solve_turbine_work(turbine_params, utility_data) 

32 

33 # === Step 4: Assign back to zone === 

34 z.work_target = w_total 

35 z.turbine_efficiency_target = w_total / Wmax if Wmax > ZERO else 0.0 

36 

37 return z 

38 

39 

40####################################################################################################### 

41# Helper functions 

42####################################################################################################### 

43 

44def _prepare_turbine_parameters(config: Configuration): 

45 """Load and sanitize turbine parameters from config.""" 

46 

47 P_in = float(config.P_TURBINE_BOX) # bar 

48 T_in = float(config.T_TURBINE_BOX) # °C 

49 min_eff = float(config.MIN_EFF) # minimum isentropic efficiency (decimal) 

50 model = config.COMBOBOX 

51 

52 load_frac = min(max(config.LOAD, 0), 1) # clamp between 0 and 1 

53 mech_eff = min(max(config.MECH_EFF, 0), 1) # clamp between 0 and 1 

54 

55 return { 

56 "P_in": P_in, 

57 "T_in": T_in, 

58 "min_eff": min_eff, 

59 "model": model, 

60 "load_frac": load_frac, 

61 "mech_eff": mech_eff, 

62 "CONDESATE_FLASH_CORRECTION": getattr(config, "CONDESATE_FLASH_CORRECTION", False), # optional 

63 } 

64 

65 

66def _preprocess_utilities(z: Zone, turbine_params: dict): 

67 """Identify eligible hot utilities above pinch and prepare initial turbine stream data.""" 

68 

69 P_in = turbine_params["P_in"] 

70 

71 Q_HU = 0.0 

72 HU_num = 0 

73 u: Stream 

74 for u in z.hot_utilities: 

75 if u.t_supply < T_CRIT and u.heat_flow > ZERO: 

76 HU_num += 1 

77 Q_HU += u.heat_flow 

78 

79 if Q_HU < ZERO: 

80 return None # No turbine opportunity 

81 

82 # Initialize arrays 

83 P_out = [P_in] 

84 Q_users = [0] 

85 w_k = [0] 

86 w_isen_k = [0] 

87 m_k = [0] 

88 eff_k = [0] 

89 dh_is_k = [0] 

90 h_out = [h_pT(P_in, turbine_params["T_in"])] 

91 h_tar = [hL_p(P_in)] 

92 h_sat = [hV_p(P_in)] 

93 turbine = [0] 

94 

95 m_in_est = 0.0 # estimated steam inlet mass flow 

96 Q_boiler = 0.0 # total boiler heat required 

97 

98 s = 0 

99 for u in z.hot_utilities: 

100 if u.t_supply < T_CRIT and u.heat_flow > ZERO: 

101 P_out_n = psat_T(u.t_target) if abs(u.t_supply - u.t_target) < 1 + ZERO else psat_T(u.t_target + u.dt_cont * 2) 

102 

103 if P_in >= P_out_n: 

104 s += 1 

105 for lst in [w_k, w_isen_k, eff_k, turbine]: 

106 lst.append(0) 

107 

108 P_out.append(P_out_n) 

109 Q_users.append(u.heat_flow) 

110 Q_boiler += u.heat_flow 

111 

112 h_out.append(hV_p(P_out_n)) 

113 h_tar.append(hL_p(P_out_n)) 

114 h_sat.append(hV_p(P_out_n)) 

115 

116 dh_is = h_out[0] - h_ps(P_out_n, s_ph(P_out[0], h_out[0])) 

117 dh_is_k.append(dh_is) 

118 mflow = u.heat_flow / (h_out[0] - dh_is - h_tar[-1]) 

119 m_k.append(mflow) 

120 m_in_est += mflow 

121 

122 return { 

123 "P_out": P_out, 

124 "Q_users": Q_users, 

125 "w_k": w_k, 

126 "w_isen_k": w_isen_k, 

127 "m_k": m_k, 

128 "eff_k": eff_k, 

129 "dh_is_k": dh_is_k, 

130 "h_out": h_out, 

131 "h_tar": h_tar, 

132 "h_sat": h_sat, 

133 "turbine": turbine, 

134 "m_in_est": m_in_est, 

135 "Q_boiler": Q_boiler, 

136 "s": s + 1, 

137 } 

138 

139 

140def _solve_turbine_work(turbine_params: dict, utility_data: dict): 

141 """Solve turbine mass flow, work production, and efficiency based on utilities and turbine parameters.""" 

142 

143 P_out = utility_data["P_out"] 

144 Q_users = utility_data["Q_users"] 

145 w_k = utility_data["w_k"] 

146 w_isen_k = utility_data["w_isen_k"] 

147 m_k = utility_data["m_k"] 

148 eff_k = utility_data["eff_k"] 

149 dh_is_k = utility_data["dh_is_k"] 

150 h_out = utility_data["h_out"] 

151 h_tar = utility_data["h_tar"] 

152 h_sat = utility_data["h_sat"] 

153 s = utility_data["s"] 

154 

155 model = turbine_params["model"] 

156 load_frac = turbine_params["load_frac"] 

157 n_mech = turbine_params["mech_eff"] 

158 min_eff = turbine_params["min_eff"] 

159 flash_correction = turbine_params.get("CONDESATE_FLASH_CORRECTION", False) 

160 

161 m_in_est = utility_data["m_in_est"] 

162 i = 0 

163 

164 while True: 

165 m_in = m_in_est 

166 m_in_k = m_in_est 

167 m_in_est = 0 

168 

169 for j in range(1, s): 

170 m_in_k -= m_k[j - 1] 

171 dh_is_k[j] = h_out[j - 1] - h_ps(P_out[j], s_ph(P_out[j - 1], h_out[j - 1])) 

172 w_isen_k[j] = m_in_k * dh_is_k[j] 

173 m_max = m_in_k / load_frac if load_frac > 0 else 0 

174 

175 if m_in_k > 0: 

176 if model == 'Sun & Smith (2015)': 

177 w_k[j] = Work_SunModel(P_out[j-1], h_out[j-1], P_out[j], h_sat[j], m_in_k, m_max, dh_is_k[j], n_mech) 

178 elif model == 'Medina-Flores et al. (2010)': 

179 w_k[j] = Work_MedinaModel(P_out[j-1], m_in_k, dh_is_k[j]) 

180 elif model == 'Varbanov et al. (2004)': 

181 w_k[j] = Work_THM(P_out[j-1], h_out[j-1], P_out[j], h_sat[j], m_in_k, dh_is_k[j], n_mech) 

182 elif model == 'Fixed Isentropic Turbine': 

183 w_k[j] = w_isen_k[j] * min_eff 

184 else: 

185 w_k[j] = 0 

186 

187 if w_isen_k[j] > 0: 

188 eff_k[j] = w_k[j] / w_isen_k[j] 

189 else: 

190 eff_k[j] = min_eff 

191 

192 if eff_k[j] <= min_eff: 

193 w_k[j] = min_eff * w_isen_k[j] 

194 

195 if m_in_k > 0: 

196 h_out[j] = h_out[j-1] - w_k[j] / (m_in_k * n_mech) 

197 else: 

198 h_out[j] = h_out[j-1] 

199 

200 if m_in_k > 0: 

201 if flash_correction: 

202 Q_flash = m_k[j-1] * (h_tar[j-1] - h_tar[j]) 

203 m_k[j] = (Q_users[j] - Q_flash) / (h_out[j] - h_tar[j]) 

204 else: 

205 m_k[j] = Q_users[j] / (h_out[j] - h_tar[j]) 

206 else: 

207 m_k[j] = 0 

208 

209 m_in_est += m_k[j] 

210 

211 i += 1 

212 if abs(m_in - m_in_est) < ZERO or i >= 3: 

213 break 

214 

215 w_total = sum(w_k) 

216 Wmax = sum(w_isen_k) 

217 

218 return w_total, Wmax 

219 

220 

221 

222 

223# def get_power_cogeneration_above_pinch(z: Zone): # type: ignore 

224# config: Configuration = z.config 

225# P_in = float(config.P_TURBINE_BOX) # bar 

226# T_in = float(config.T_TURBINE_BOX) # C 

227# MinEff = float(config.MIN_EFF) # % 

228# SelectedModel = config.COMBOBOX 

229 

230# load_frac = config.LOAD 

231# n_mech = config.MECH_EFF 

232 

233# if load_frac > 1: 

234# load_frac = 1 

235# elif load_frac < 0: 

236# load_frac = 0 

237 

238# if n_mech > 1: 

239# n_mech = 1 

240# elif n_mech < 0: 

241# n_mech = 0 

242 

243# Q_HU = 0 

244# for Utility_k in z.hot_utilities: 

245# Q_HU += Utility_k.heat_flow 

246# HU_num = len(z.hot_utilities) 

247# if Q_HU < ZERO: 

248# return z 

249 

250# if SelectedModel == 'Sun & Smith (2015)': 

251# SunCoef = [ [ [None for k in range(3)] for j in range(3)] for i in range(2)] 

252# Set_Coeff(SunCoef, None) 

253# elif SelectedModel == 'Varbanov et al. (2004)': 

254# VarCoef = [ [ [None for k in range(4)] for j in range(2)] for i in range(2)] 

255# Set_Coeff(None, VarCoef) 

256 

257# P_out = [P_in] # bar 

258# Q_users = [0] 

259# w_k = [0] 

260# w_isen_k = [0] 

261# m_k = [0] # kg/s 

262# eff_k = [0] 

263# dh_is_k = [0] 

264# h_out = [h_pT(P_in, T_in)] # kJ/kg for turbine 

265# h_tar = [hL_p(P_out[0])] # kJ/kg for after condensing 

266# h_sat = [hV_p(P_out[0])] # kJ/kg 

267# turbine = [0] 

268 

269# m_in_est = 0 # kg/s 

270# Q_boiler = 0 # kW 

271 

272# # Preparation of data for turbine calculation 

273# s = 0 

274# for i in range(HU_num): 

275# Utility_i = z.hot_utilities[i] 

276# if Utility_i.t_supply < T_CRIT: 

277# P_out_n = psat_T(Utility_i.t_target) if abs(Utility_i.t_supply - Utility_i.t_target) < 1 + ZERO \ 

278# else psat_T(Utility_i.t_target + Utility_i.dt_cont * 2) # bar 

279# if P_in >= P_out_n and Utility_i.heat_flow > ZERO: 

280# s += 1 

281# for l in [w_k, w_isen_k, eff_k, turbine]: 

282# l.append(0) 

283 

284# P_out.append(P_out_n) # bar 

285# Q_users.append(Utility_i.heat_flow) # kW 

286# Q_boiler += Q_users[s] # kW 

287 

288# h_out.append(hV_p(P_out[s])) # kJ/kg 

289# h_tar.append(hL_p(P_out[s])) # kJ/kg 

290# h_sat.append(hV_p(P_out[s])) # kJ/kg 

291 

292# dh_is_k.append(h_out[0] - h_ps(P_out[s], s_ph(P_out[0], h_out[0]))) # kJ/kg 

293# m_k.append(Q_users[s] / (h_out[0] - dh_is_k[s] - h_tar[s])) # kg/s 

294# m_in_est += m_k[s] # kg/s 

295# s += 1 

296 

297# # Turbine calculation 

298# i = 0 

299# while True: 

300# m_in = m_in_est 

301# m_in_k = m_in_est 

302# m_in_est = 0 

303 

304# for j in range(1, s): 

305# m_in_k -= m_k[j - 1] # kg/s 

306# dh_is_k[j] = h_out[j - 1] - h_ps(P_out[j], s_ph(P_out[j - 1], h_out[j - 1])) # kJ/kg 

307# w_isen_k[j] = m_in_k * dh_is_k[j] # kW 

308# m_max = m_in_k / load_frac # kg/s 

309 

310# # Determine the work production based on various Turbine Models 

311# if m_in_k > 0: 

312# if SelectedModel == 'Sun & Smith (2015)': 

313# w_k[j] = Work_SunModel(P_out[j - 1], h_out[j - 1], P_out[j], h_sat[j], m_in_k, m_max, dh_is_k[j], n_mech, SunCoef) # kW 

314# elif SelectedModel == 'Medina-Flores et al. (2010)': 

315# w_k[j] = Work_MedinaModel(P_out[j - 1], m_in_k, dh_is_k[j]) # kW 

316# elif SelectedModel == 'Varbanov et al. (2004)': 

317# w_k[j] = Work_THM(P_out[j - 1], h_out[j - 1], P_out[j], h_sat[j], m_in_k, dh_is_k[j], n_mech, VarCoef) # kW 

318# elif SelectedModel == 'Fixed Isentropic Turbine': 

319# w_k[j] = w_isen_k[j] * MinEff # kW 

320# else: 

321# w_k[j] = 0 

322 

323# if w_isen_k[j] > 0: 

324# eff_k[j] = w_k[j] / w_isen_k[j] 

325# else: 

326# eff_k[j] = MinEff 

327# if eff_k[j] <= MinEff: 

328# w_k[j] = MinEff * w_isen_k[j] # kW 

329# if m_in_k > 0: 

330# h_out[j] = h_out[j - 1] - w_k[j] / (m_in_k * n_mech) 

331# else: 

332# h_out[j] = h_out[j - 1] # kJ/kg 

333 

334# # Correct for high pressure condensate flash, if selected 

335# if m_in_k > 0: 

336# if config.CONDESATE_FLASH_CORRECTION: 

337# Q_flash = m_k[j - 1] * (h_tar[j - 1] - h_tar[j]) 

338# m_k[j] = (Q_users[j] - Q_flash) / (h_out[j] - h_tar[j]) 

339# else: 

340# m_k[j] = Q_users[j] / (h_out[j] - h_tar[j]) 

341# else: 

342# m_k[j] = 0 # kg/s 

343 

344# m_in_est += m_k[j] # kg/s 

345 

346# i += 1 

347# if (abs(m_in - m_in_est) < ZERO and i >= 100) or i >= 3: 

348# break 

349 

350# w = 0 

351# Wmax = 0 

352# for j in range(s): 

353# w += w_k[j] 

354# Wmax += w_isen_k[j] 

355 

356# z.work_target = w # kW 

357# if Wmax > 0: 

358# z.turbine_efficiency_target = w / Wmax 

359# else: 

360# z.turbine_efficiency_target = 0 # % 

361 

362# return z 

363 

364# ####################################################################################################### 

365# # Helper Functions 

366# ####################################################################################################### 

367 

368def Work_MedinaModel(P_in, m, dh_is): 

369 """'Determines power generation based on the thermodynamic model of Medina-Flores & Picón-Núñez (2010). 

370 """ 

371 # Reference for Turbine Model 1: Medina-Flores & Picón-Núñez (2010) 

372 # Modelling the power production of single and multiple extraction steam turbines. 

373 # Chemical Engineering Science 65, 2811-2820 

374 # Part load not used 

375 A0 = 185.4 + 43.3 * (P_in * 0.1) # a0 in kW, P in bar 

376 b0 = 1.2057 + 0.0075 * (P_in * 0.1) # b0 in dimensionless, P in bar 

377 return (m * dh_is - A0) / b0 # kW 

378 

379def Work_SunModel(P_in, h_in, P_out, h_sat, m, m_max, dh_is, n_mech, t_type=1): 

380 """Determines power generation based on the correlation model of Sun and Smith (2015). 

381 """ 

382 # 'Reference for Turbine Model 2: Sun & Smith (2015) 

383 # ' Performance Modeling of New and Existing Steam Turbines 

384 # ' I&CE 54, 1908-1915 

385 

386 coeff = { 

387 "BPST": { 

388 "a": [1.18795366, -0.00029564, 0.004647288], 

389 "b": [449.9767142, 5.670176939, -11.5045814], 

390 "c": [0.205149333, -0.000695171, 0.002844611], 

391 }, 

392 "CT": { 

393 "a": [1.314991261, -0.001634725, -0.367975103], 

394 "b": [-437.7746025, 29.00736723, 10.35902331], 

395 "c": [0.07886297, 0.000528327, -0.703153891], 

396 } 

397 } 

398 

399 # Model coefficients where P in bar 

400 A0 = coeff[t_type]["a"][0] + coeff[t_type]["a"][1] * (P_in) + coeff[t_type]["a"][2] * (P_out) 

401 b0 = coeff[t_type]["b"][0] + coeff[t_type]["b"][1] * (P_in) + coeff[t_type]["b"][2] * (P_out) 

402 c0 = coeff[t_type]["c"][0] + coeff[t_type]["c"][1] * (P_in) + coeff[t_type]["c"][2] * (P_out) 

403 

404 # Willan's line coefficients and predicted work (after isentropic and mechanical efficiency loss) 

405 W_int = c0 / A0 * (m_max * dh_is - b0) 

406 n = (1 + c0) / A0 * (dh_is - b0 / m_max) 

407 w_act = n * m - W_int 

408 h_out = h_in - w_act / (n_mech * m) 

409 

410 if h_out <= h_sat + ZERO and t_type == 1: 

411 w_act = Work_SunModel(P_in, h_in, P_out, h_sat, m, m_max, dh_is, n_mech, 2) 

412 return w_act 

413 

414def Work_THM(P_in, h_in, P_out, h_sat, m, dh_is, n_mech, t_size=1, t_type=1): 

415 """Determines power generation based on the Turbine Hardware Model of Varbanov et al. (2004). 

416 """ 

417 # 'Reference for Turbine Model 3: Varbanov et al. (2004) 

418 # ' Modelling and Optimization of Utility Systems 

419 # ' Trans IChemE, Part A, Chemical Engineering Research and Design, 2004, 82(A5): 561–578 

420 # Part load not used 

421 coeff = { 

422 "BPST": { 

423 "<2MW": [0, 0.00108, 1.097, 0.00172], 

424 ">2MW": [0, 0.00423, 1.155, 0.000538], 

425 }, 

426 "CT": { 

427 "<2MW": [0, 0.000662, 1.191, 0.000759], 

428 ">2MW": [-0.463, 0.00353, 1.22, 0.000148], 

429 } 

430 } 

431 

432 dT_sat = Tsat_p(P_in) - Tsat_p(P_out) 

433 a = (coeff[t_type][t_size][0] + coeff[t_type][t_size][1] * dT_sat) * 1000 

434 b = coeff[t_type][t_size][2] + coeff[t_type][t_size][3] * dT_sat 

435 w_max = (dh_is * m - a) / b 

436 

437 if w_max > 2000 and t_size == 1: 

438 t_size = 2 

439 w_max = Work_THM(P_in, h_in, P_out, h_sat, m, dh_is, n_mech, t_size) 

440 

441 h_out = h_in - w_max / (n_mech * m) 

442 if h_out <= h_sat + ZERO and t_type == 1: 

443 t_type = 2 

444 w_max = Work_THM(P_in, h_in, P_out, h_sat, m, dh_is, n_mech, t_size, t_type) 

445 

446 return w_max 

447 

448 

449 

450def Set_Coeff(SunCoef=None, VarCoef=None): 

451 """Sets the model coefficients.""" 

452 

453 

454 if VarCoef != None: 

455 # Model coefficients for the Varbanov et al. model 

456 # BPST 

457 # < 2MW 

458 VarCoef[0][0][0] = 0 

459 VarCoef[0][0][1] = 0.00108 

460 VarCoef[0][0][2] = 1.097 

461 VarCoef[0][0][3] = 0.00172 

462 

463 # > 2MW 

464 VarCoef[0][1][0] = 0 

465 VarCoef[0][1][1] = 0.00423 

466 VarCoef[0][1][2] = 1.155 

467 VarCoef[0][1][3] = 0.000538 

468 

469 # CT 

470 # < 2MW 

471 VarCoef[1][0][0] = 0 

472 VarCoef[1][0][1] = 0.000662 

473 VarCoef[1][0][2] = 1.191 

474 VarCoef[1][0][3] = 0.000759 

475 

476 # > 2MW 

477 VarCoef[1][1][0] = -0.463 

478 VarCoef[1][1][1] = 0.00353 

479 VarCoef[1][1][2] = 1.22 

480 VarCoef[1][1][3] = 0.000148 

481