Coverage for backend/ahuora-builder/src/ahuora_builder/custom/custom_separator.py: 62%

112 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-07-22 05:22 +0000

1from idaes.models.unit_models.separator import SeparatorData, SplittingType 

2 

3from functools import partial 

4from pandas import DataFrame 

5 

6from pyomo.environ import ( 

7 Block, 

8 check_optimal_termination, 

9 Constraint, 

10 Param, 

11 Reals, 

12 Reference, 

13 Set, 

14 Var, 

15 value, 

16) 

17from pyomo.network import Port 

18from pyomo.common.config import ConfigBlock, ConfigValue, In, ListOf, Bool 

19 

20from idaes.core import ( 

21 declare_process_block_class, 

22 UnitModelBlockData, 

23 useDefault, 

24 MaterialBalanceType, 

25 MomentumBalanceType, 

26 MaterialFlowBasis, 

27 VarLikeExpression, 

28) 

29from idaes.core.util.config import ( 

30 is_physical_parameter_block, 

31 is_state_block, 

32) 

33from idaes.core.util.exceptions import ( 

34 BurntToast, 

35 ConfigurationError, 

36 PropertyNotSupportedError, 

37 InitializationError, 

38) 

39from idaes.core.solvers import get_solver 

40from idaes.core.util.tables import create_stream_table_dataframe 

41from idaes.core.util.model_statistics import degrees_of_freedom 

42import idaes.logger as idaeslog 

43import idaes.core.util.scaling as iscale 

44from idaes.core.util.units_of_measurement import report_quantity 

45from idaes.core.initialization import ModularInitializerBase 

46 

47 

48# This only changes a couple of lines in the original SeparatorData class, to not fix state variables by default. 

49# The state block initialisation already does this if needed, so we can just set their value. 

50# This is because if the state block has extra constraints, such as for flow_mass, then fixing flow_mol will over-define the system. 

51# It might be worth making this a pr to idaes. 

52 

53@declare_process_block_class("CustomSeparator") 

54class CustomSeparatorData(SeparatorData): 

55 

56 def initialize_build( 

57 blk, outlvl=idaeslog.NOTSET, optarg=None, solver=None, hold_state=False 

58 ): 

59 """ 

60 Initialization routine for separator 

61 

62 Keyword Arguments: 

63 outlvl : sets output level of initialization routine 

64 optarg : solver options dictionary object (default=None, use 

65 default solver options) 

66 solver : str indicating which solver to use during 

67 initialization (default = None, use default solver) 

68 hold_state : flag indicating whether the initialization routine 

69 should unfix any state variables fixed during 

70 initialization, **default** - False. **Valid values:** 

71 **True** - states variables are not unfixed, and a dict of 

72 returned containing flags for which states were fixed 

73 during initialization, **False** - state variables are 

74 unfixed after initialization by calling the release_state 

75 method. 

76 

77 Returns: 

78 If hold_states is True, returns a dict containing flags for which 

79 states were fixed during initialization. 

80 """ 

81 init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit") 

82 solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit") 

83 

84 # Create solver 

85 opt = get_solver(solver, optarg) 

86 

87 # Initialize mixed state block 

88 if blk.config.mixed_state_block is not None: 88 ↛ 89line 88 didn't jump to line 89 because the condition on line 88 was never true

89 mblock = blk.config.mixed_state_block 

90 else: 

91 mblock = blk.mixed_state 

92 flags = mblock.initialize( 

93 outlvl=outlvl, 

94 optarg=optarg, 

95 solver=solver, 

96 hold_state=True, 

97 ) 

98 

99 

100 # Solve for split fractions only 

101 component_status = {} 

102 for c in blk.component_objects((Block, Constraint)): 

103 for i in c: 

104 if not c[i].local_name == "sum_split_frac": 104 ↛ 103line 104 didn't jump to line 103 because the condition on line 104 was always true

105 # Record current status of components to restore later 

106 component_status[c[i]] = c[i].active 

107 c[i].deactivate() 

108 

109 if degrees_of_freedom(blk) != 0: 109 ↛ 110line 109 didn't jump to line 110 because the condition on line 109 was never true

110 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: 

111 res = opt.solve(blk, tee=slc.tee) 

112 init_log.info( 

113 "Initialization Step 1 Complete: {}".format(idaeslog.condition(res)) 

114 ) 

115 

116 for c, s in component_status.items(): 

117 if s: 117 ↛ 116line 117 didn't jump to line 116 because the condition on line 117 was always true

118 c.activate() 

119 

120 if blk.config.ideal_separation: 120 ↛ 122line 120 didn't jump to line 122 because the condition on line 120 was never true

121 # If using ideal splitting, initialization should be complete 

122 return flags 

123 

124 # Initialize outlet StateBlocks 

125 outlet_list = blk.create_outlet_list() 

126 

127 # Premises for initializing outlet states: 

128 # 1. Intensive states remain unchanged - this is either a valid premise 

129 # or the actual state is impossible to calculate without solving the 

130 # full separator model. 

131 # 2. Extensive states are use split fractions if index matches, or 

132 # average of split fractions for outlet otherwise 

133 for o in outlet_list: 

134 # Get corresponding outlet StateBlock 

135 o_block = getattr(blk, o + "_state") 

136 

137 # Create dict to store fixed status of state variables 

138 o_flags = {} 

139 for t in blk.flowsheet().time: 

140 

141 # Calculate values for state variables 

142 s_vars = o_block[t].define_state_vars() 

143 for v in s_vars: 

144 for k in s_vars[v]: 

145 # Record whether variable was fixed or not 

146 o_flags[t, v, k] = s_vars[v][k].fixed 

147 

148 # If fixed, use current value 

149 # otherwise calculate guess from mixed state and fix 

150 if not s_vars[v][k].fixed: 150 ↛ 144line 150 didn't jump to line 144 because the condition on line 150 was always true

151 m_var = getattr(mblock[t], s_vars[v].local_name) 

152 if "flow" in v: 

153 # If a "flow" variable, is extensive 

154 # Apply split fraction 

155 if blk.config.split_basis == SplittingType.totalFlow: 

156 # All flows split by outlet 

157 s_vars[v][k].set_value( 

158 value(m_var[k] * blk.split_fraction[(t, o)]) 

159 ) 

160 elif "_phase_comp" in v: 160 ↛ 162line 160 didn't jump to line 162 because the condition on line 160 was never true

161 # Need to match indices, but use split frac 

162 if ( 

163 blk.config.split_basis 

164 == SplittingType.phaseComponentFlow 

165 ): 

166 s_vars[v][k].set_value( 

167 value( 

168 m_var[k] 

169 * blk.split_fraction[(t, o) + (k,)] 

170 ) 

171 ) 

172 elif ( 

173 blk.config.split_basis 

174 == SplittingType.phaseFlow 

175 ): 

176 s_vars[v][k].set_value( 

177 value( 

178 m_var[k] 

179 * blk.split_fraction[(t, o) + (k[0],)] 

180 ) 

181 ) 

182 elif ( 

183 blk.config.split_basis 

184 == SplittingType.componentFlow 

185 ): 

186 s_vars[v][k].set_value( 

187 value( 

188 m_var[k] 

189 * blk.split_fraction[(t, o) + (k[1],)] 

190 ) 

191 ) 

192 else: 

193 raise BurntToast( 

194 "{} encountered unrecognised " 

195 "SplittingType. This should not " 

196 "occur - please send this bug to " 

197 "the IDAES developers.".format(blk.name) 

198 ) 

199 elif "_phase" in v: 199 ↛ 200line 199 didn't jump to line 200 because the condition on line 199 was never true

200 if ( 

201 blk.config.split_basis 

202 == SplittingType.phaseComponentFlow 

203 ): 

204 # Need average split fraction 

205 avg_split = value( 

206 sum( 

207 blk.split_fraction[t, o, k, j] 

208 for j in mblock.component_list 

209 ) 

210 / len(mblock.component_list) 

211 ) 

212 s_vars[v][k].set_value(value(m_var[k] * avg_split)) 

213 elif ( 

214 blk.config.split_basis 

215 == SplittingType.phaseFlow 

216 ): 

217 s_vars[v][k].set_value( 

218 value( 

219 m_var[k] 

220 * blk.split_fraction[(t, o) + (k,)] 

221 ) 

222 ) 

223 elif ( 

224 blk.config.split_basis 

225 == SplittingType.componentFlow 

226 ): 

227 # Need average split fraction 

228 avg_split = value( 

229 sum( 

230 blk.split_fraction[t, o, j] 

231 for j in mblock.component_list 

232 ) 

233 / len(mblock.component_list) 

234 ) 

235 s_vars[v][k].set_value(value(m_var[k] * avg_split)) 

236 else: 

237 raise BurntToast( 

238 "{} encountered unrecognised " 

239 "SplittingType. This should not " 

240 "occur - please send this bug to " 

241 "the IDAES developers.".format(blk.name) 

242 ) 

243 elif "_comp" in v: 243 ↛ 244line 243 didn't jump to line 244 because the condition on line 243 was never true

244 if ( 

245 blk.config.split_basis 

246 == SplittingType.phaseComponentFlow 

247 ): 

248 # Need average split fraction 

249 avg_split = value( 

250 sum( 

251 blk.split_fraction[t, o, p, k] 

252 for p in mblock.phase_list 

253 ) 

254 / len(mblock.phase_list) 

255 ) 

256 s_vars[v][k].set_value(value(m_var[k] * avg_split)) 

257 elif ( 

258 blk.config.split_basis 

259 == SplittingType.phaseFlow 

260 ): 

261 # Need average split fraction 

262 avg_split = value( 

263 sum( 

264 blk.split_fraction[t, o, p] 

265 for p in mblock.phase_list 

266 ) 

267 / len(mblock.phase_list) 

268 ) 

269 s_vars[v][k].set_value(value(m_var[k] * avg_split)) 

270 elif ( 

271 blk.config.split_basis 

272 == SplittingType.componentFlow 

273 ): 

274 s_vars[v][k].set_value( 

275 value( 

276 m_var[k] 

277 * blk.split_fraction[(t, o) + (k,)] 

278 ) 

279 ) 

280 else: 

281 raise BurntToast( 

282 "{} encountered unrecognised " 

283 "SplittingType. This should not " 

284 "occur - please send this bug to " 

285 "the IDAES developers.".format(blk.name) 

286 ) 

287 else: 

288 # Assume unindexed extensive state 

289 # Need average split 

290 if ( 290 ↛ 295line 290 didn't jump to line 295 because the condition on line 290 was never true

291 blk.config.split_basis 

292 == SplittingType.phaseComponentFlow 

293 ): 

294 # Need average split fraction 

295 avg_split = value( 

296 sum( 

297 blk.split_fraction[t, o, p, j] 

298 for (p, j) in mblock.phase_component_set 

299 ) 

300 / len(mblock.phase_component_set) 

301 ) 

302 elif ( 

303 blk.config.split_basis 

304 == SplittingType.phaseFlow 

305 ): 

306 # Need average split fraction 

307 avg_split = value( 

308 sum( 

309 blk.split_fraction[t, o, p] 

310 for p in mblock.phase_list 

311 ) 

312 / len(mblock.phase_list) 

313 ) 

314 elif ( 314 ↛ 327line 314 didn't jump to line 327 because the condition on line 314 was always true

315 blk.config.split_basis 

316 == SplittingType.componentFlow 

317 ): 

318 # Need average split fraction 

319 avg_split = value( 

320 sum( 

321 blk.split_fraction[t, o, j] 

322 for j in mblock.component_list 

323 ) 

324 / len(mblock.component_list) 

325 ) 

326 else: 

327 raise BurntToast( 

328 "{} encountered unrecognised " 

329 "SplittingType. This should not " 

330 "occur - please send this bug to " 

331 "the IDAES developers.".format(blk.name) 

332 ) 

333 s_vars[v][k].set_value(value(m_var[k] * avg_split)) 

334 else: 

335 # Otherwise intensive, equate to mixed stream 

336 s_vars[v][k].set_value(m_var[k].value) 

337 

338 # Call initialization routine for outlet StateBlock 

339 o_block.initialize( 

340 outlvl=outlvl, 

341 optarg=optarg, 

342 solver=solver, 

343 hold_state=False, 

344 ) 

345 

346 # Revert fixed status of variables to what they were before 

347 for t in blk.flowsheet().time: 

348 s_vars = o_block[t].define_state_vars() 

349 for v in s_vars: 

350 for k in s_vars[v]: 

351 s_vars[v][k].fixed = o_flags[t, v, k] 

352 

353 if blk.config.mixed_state_block is None: 353 ↛ 367line 353 didn't jump to line 367 because the condition on line 353 was always true

354 with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc: 

355 res = opt.solve(blk, tee=slc.tee) 

356 

357 if not check_optimal_termination(res): 357 ↛ 358line 357 didn't jump to line 358 because the condition on line 357 was never true

358 raise InitializationError( 

359 f"{blk.name} failed to initialize successfully. Please " 

360 f"check the output logs for more information." 

361 ) 

362 

363 init_log.info( 

364 "Initialization Step 2 Complete: {}".format(idaeslog.condition(res)) 

365 ) 

366 else: 

367 init_log.info("Initialization Complete.") 

368 

369 if hold_state is True: 369 ↛ 370line 369 didn't jump to line 370 because the condition on line 369 was never true

370 return flags 

371 else: 

372 blk.release_state(flags, outlvl=outlvl) 

373 

374 

375 @staticmethod 

376 def ahuora_metadata(): 

377 from ahuora_unit_ops.json_config import ( 

378 JsonAdapterArgConfig, 

379 JsonFrontendConfig, 

380 JsonGraphicObjectConfig, 

381 JsonIdaesAdapterConfig, 

382 JsonPortAdapterConfig, 

383 JsonPortConfig, 

384 JsonPropertyConfig, 

385 JsonPropertySetGroupConfig, 

386 JsonUnitOpConfig, 

387 ) 

388 

389 return JsonUnitOpConfig( 

390 key='splitter', 

391 objectType='splitter', 

392 enumMember='Splitter', 

393 displayType='Splitter', 

394 displayName='Splitter', 

395 categoryPath=['chemical', 'mixer'], 

396 ports={ 

397 'inlet': JsonPortConfig( 

398 displayName='Inlet', 

399 type='inlet', 

400 streamType='stream', 

401 many=False, 

402 default=1, 

403 minimum=1, 

404 makeStream=True, 

405 streamOffset=0.75, 

406 streamName='S', 

407 ), 

408 'outlet': JsonPortConfig( 

409 displayName='Outlet', 

410 type='outlet', 

411 streamType='stream', 

412 many=True, 

413 default=2, 

414 minimum=2, 

415 makeStream=True, 

416 streamOffset=0.75, 

417 streamName='S', 

418 ), 

419 }, 

420 propertyPackagePorts={ 

421 '': ['inlet', 'outlet'], 

422 }, 

423 graphicObject=JsonGraphicObjectConfig( 

424 kind='unitop_graphic', 

425 ), 

426 indexSets=['splitter_fraction'], 

427 properties={ 

428 'split_fraction': JsonPropertyConfig( 

429 propertySetGroup='default', 

430 displayName='Split Ratio', 

431 indexSets=['splitter_fraction'], 

432 sumToOne=True, 

433 value=None, 

434 unit=None, 

435 unitType='ratio', 

436 description=None, 

437 type='numeric', 

438 many=False, 

439 default=1, 

440 options={}, 

441 hasTimeIndex=True, 

442 ), 

443 }, 

444 propertySetGroups={ 

445 'default': JsonPropertySetGroupConfig( 

446 type='exceptLast', 

447 displayName='Properties', 

448 stateVars=['split_fraction'], 

449 toggle=None, 

450 ), 

451 }, 

452 keyProperties=None, 

453 splitterFractionName='Outlet', 

454 idaesAdapter=JsonIdaesAdapterConfig( 

455 constructor='ahuora_builder.custom.custom_separator.CustomSeparator', 

456 args={ 

457 'property_package': JsonAdapterArgConfig( 

458 kind='property_package', 

459 label=None, 

460 ), 

461 'num_outlets': JsonAdapterArgConfig( 

462 kind='port_count', 

463 port='outlet', 

464 ), 

465 }, 

466 ports=JsonPortAdapterConfig( 

467 kind='splitter', 

468 ), 

469 properties=None, 

470 ), 

471 frontend=JsonFrontendConfig( 

472 showInPanel=True, 

473 variant=None, 

474 ), 

475 )