Coverage for backend/idaes_service/solver/custom/simple_separator.py: 48%

235 statements  

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

1################################################################################# 

2# The Institute for the Design of Advanced Energy Systems Integrated Platform 

3# Framework (IDAES IP) was produced under the DOE Institute for the 

4# Design of Advanced Energy Systems (IDAES). 

5# 

6# Copyright (c) 2018-2024 by the software owners: The Regents of the 

7# University of California, through Lawrence Berkeley National Laboratory, 

8# National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon 

9# University, West Virginia University Research Corporation, et al. 

10# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md 

11# for full copyright and license information. 

12################################################################################# 

13""" 

14General purpose separator block for IDAES models 

15""" 

16 

17from enum import Enum 

18from pandas import DataFrame 

19 

20from pyomo.environ import (Block, Set) 

21from pyomo.network import Port 

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

23 

24from idaes.core import ( 

25 declare_process_block_class, 

26 UnitModelBlockData, 

27 useDefault, 

28 MaterialBalanceType, 

29 MomentumBalanceType, 

30) 

31from idaes.core.util.config import ( 

32 is_physical_parameter_block, 

33 is_state_block, 

34) 

35from idaes.core.util.exceptions import ( 

36 BurntToast, 

37 ConfigurationError, 

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__author__ = "Team Ahuora" 

48 

49 

50# Set up logger 

51_log = idaeslog.getLogger(__name__) 

52 

53 

54# Enumerate options for balances 

55class SplittingType(Enum): 

56 """ 

57 Enum of supported material split types. 

58 """ 

59 

60 totalFlow = 1 

61 phaseFlow = 2 

62 componentFlow = 3 

63 phaseComponentFlow = 4 

64 

65 

66class EnergySplittingType(Enum): 

67 """ 

68 Enum of support energy split types. 

69 """ 

70 

71 none = 0 

72 equal_molar_enthalpy = 2 

73 

74 

75class SimpleSeparatorInitializer(ModularInitializerBase): 

76 """ 

77 Initializer for Separator blocks. 

78 

79 """ 

80 

81 def initialization_routine( 

82 self, 

83 model: Block, 

84 ): 

85 """ 

86 Initialization routine for Separator Blocks. 

87 

88 This routine starts by initializing the feed and outlet streams using simple rules. 

89 

90 Args: 

91 model: model to be initialized 

92 

93 Returns: 

94 None 

95 

96 """ 

97 init_log = idaeslog.getInitLogger( 

98 model.name, self.get_output_level(), tag="unit" 

99 ) 

100 solve_log = idaeslog.getSolveLogger( 

101 model.name, self.get_output_level(), tag="unit" 

102 ) 

103 

104 # Create solver 

105 solver = self._get_solver() 

106 # Initialize mixed state block 

107 

108 mblock = model.mixed_state 

109 self.get_submodel_initializer(mblock).initialize(mblock) 

110 

111 res = None 

112 if degrees_of_freedom(model) != 0: 

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

114 res = solver.solve(model, tee=slc.tee) 

115 init_log.info( 

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

117 ) 

118 

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

120 if s: 

121 c.activate() 

122 

123 

124 # Initialize outlet StateBlocks 

125 outlet_list = model._create_outlet_list() 

126 

127 # Initializing outlet states 

128 for o in outlet_list: 

129 # Get corresponding outlet StateBlock 

130 o_block = getattr(model, o + "_state") 

131 

132 # Create dict to store fixed status of state variables 

133 for t in model.flowsheet().time: 

134 # Calculate values for state variables 

135 s_vars = o_block[t].define_state_vars() 

136 for var_name_port, var_obj in s_vars.items(): 

137 for k in var_obj: 

138 # If fixed, use current value 

139 # otherwise calculate guess from mixed state and fix 

140 if not var_obj[k].fixed: 

141 m_var = getattr(mblock[t], var_obj.local_name) 

142 if "flow" in var_name_port: 

143 # Leave initial value 

144 pass 

145 else: 

146 # Otherwise intensive, equate to mixed stream 

147 var_obj[k].set_value(m_var[k].value) 

148 

149 # Call initialization routine for outlet StateBlock 

150 self.get_submodel_initializer(o_block).initialize(o_block) 

151 

152 init_log.info("Initialization Complete.") 

153 

154 return res 

155 

156 

157@declare_process_block_class("SimpleSeparator") 

158class SimpleSeparatorData(UnitModelBlockData): 

159 """ 

160 This is a simple Splitter block with the IDAES modeling framework.  

161 Unlike the generic Separator, this block avoids use of split fractions. 

162 

163 This model creates a number of StateBlocks to represent the outgoing 

164 streams, then writes a set of phase-component material balances, an 

165 overall enthalpy balance (2 options), and a momentum balance (2 options) 

166 linked to a mixed-state StateBlock. The mixed-state StateBlock can either 

167 be specified by the user (allowing use as a sub-model), or created by the 

168 Separator. 

169 """ 

170 

171 default_initializer = SimpleSeparatorInitializer 

172 

173 CONFIG = UnitModelBlockData.CONFIG() 

174 CONFIG.declare( 

175 "property_package", 

176 ConfigValue( 

177 default=useDefault, 

178 domain=is_physical_parameter_block, 

179 description="Property package to use for mixer", 

180 doc="""Property parameter object used to define property 

181calculations, 

182**default** - useDefault. 

183**Valid values:** { 

184**useDefault** - use default package from parent model or flowsheet, 

185**PropertyParameterObject** - a PropertyParameterBlock object.}""", 

186 ), 

187 ) 

188 CONFIG.declare( 

189 "property_package_args", 

190 ConfigBlock( 

191 implicit=True, 

192 description="Arguments to use for constructing property packages", 

193 doc="""A ConfigBlock with arguments to be passed to a property 

194block(s) and used when constructing these, 

195**default** - None. 

196**Valid values:** { 

197see property package for documentation.}""", 

198 ), 

199 ) 

200 CONFIG.declare( 

201 "outlet_list", 

202 ConfigValue( 

203 domain=ListOf(str), 

204 description="List of outlet names", 

205 doc="""A list containing names of outlets, 

206**default** - None. 

207**Valid values:** { 

208**None** - use num_outlets argument, 

209**list** - a list of names to use for outlets.}""", 

210 ), 

211 ) 

212 CONFIG.declare( 

213 "num_outlets", 

214 ConfigValue( 

215 domain=int, 

216 description="Number of outlets to unit", 

217 doc="""Argument indicating number (int) of outlets to construct, 

218not used if outlet_list arg is provided, 

219**default** - None. 

220**Valid values:** { 

221**None** - use outlet_list arg instead, or default to 2 if neither argument 

222provided, 

223**int** - number of outlets to create (will be named with sequential integers 

224from 1 to num_outlets).}""", 

225 ), 

226 ) 

227 CONFIG.declare( 

228 "material_balance_type", 

229 ConfigValue( 

230 default=MaterialBalanceType.useDefault, 

231 domain=In(MaterialBalanceType), 

232 description="Material balance construction flag", 

233 doc="""Indicates what type of mass balance should be constructed, 

234**default** - MaterialBalanceType.useDefault. 

235**Valid values:** { 

236**MaterialBalanceType.useDefault - refer to property package for default 

237balance type 

238**MaterialBalanceType.none** - exclude material balances, 

239**MaterialBalanceType.componentPhase** - use phase component balances, 

240**MaterialBalanceType.componentTotal** - use total component balances, 

241**MaterialBalanceType.elementTotal** - use total element balances, 

242**MaterialBalanceType.total** - use total material balance.}""", 

243 ), 

244 ) 

245 CONFIG.declare( 

246 "momentum_balance_type", 

247 ConfigValue( 

248 default=MomentumBalanceType.pressureTotal, 

249 domain=In(MomentumBalanceType), 

250 description="Momentum balance construction flag", 

251 doc="""Indicates what type of momentum balance should be constructed, 

252 **default** - MomentumBalanceType.pressureTotal. 

253 **Valid values:** { 

254 **MomentumBalanceType.none** - exclude momentum balances, 

255 **MomentumBalanceType.pressureTotal** - pressure in all outlets is equal, 

256 **MomentumBalanceType.pressurePhase** - not yet supported, 

257 **MomentumBalanceType.momentumTotal** - not yet supported, 

258 **MomentumBalanceType.momentumPhase** - not yet supported.}""", 

259 ), 

260 ) 

261 CONFIG.declare( 

262 "has_phase_equilibrium", 

263 ConfigValue( 

264 default=False, 

265 domain=Bool, 

266 description="Calculate phase equilibrium in mixed stream", 

267 doc="""Argument indicating whether phase equilibrium should be 

268calculated for the resulting mixed stream, 

269**default** - False. 

270**Valid values:** { 

271**True** - calculate phase equilibrium in mixed stream, 

272**False** - do not calculate equilibrium in mixed stream.}""", 

273 ), 

274 ) 

275 

276 def build(self): 

277 """ 

278 General build method for SeparatorData. This method calls a number 

279 of sub-methods which automate the construction of expected attributes 

280 of unit models. 

281 

282 Inheriting models should call `super().build`. 

283 

284 Args: 

285 None 

286 

287 Returns: 

288 None 

289 """ 

290 # Call super.build() 

291 super(SimpleSeparatorData, self).build() 

292 

293 # Call setup methods from ControlVolumeBlockData 

294 self._get_property_package() 

295 self._get_indexing_sets() 

296 

297 # Create list of inlet names 

298 outlet_list = self._create_outlet_list() 

299 

300 mixed_block = self._add_mixed_state_block() 

301 

302 # Add inlet port 

303 self._add_inlet_port_objects(mixed_block) 

304 

305 # Build StateBlocks for outlet 

306 outlet_blocks = self._add_outlet_state_blocks(outlet_list) 

307 self.outlet_idx = Set(initialize=outlet_list) 

308 

309 # Construct splitting equations 

310 self._add_material_balance(mixed_block, outlet_blocks) 

311 self._add_energy_balance(mixed_block, outlet_blocks) 

312 self._add_momentum_balance(mixed_block, outlet_blocks) 

313 

314 # Construct outlet port objects 

315 self._add_outlet_port_objects(outlet_list) 

316 

317 def _create_outlet_list(self): 

318 """ 

319 Create list of outlet stream names based on config arguments. 

320 

321 Returns: 

322 list of strings 

323 """ 

324 if self.config.outlet_list is not None and self.config.num_outlets is not None: 324 ↛ 326line 324 didn't jump to line 326 because the condition on line 324 was never true

325 # If both arguments provided and not consistent, raise Exception 

326 if len(self.config.outlet_list) != self.config.num_outlets: 

327 raise ConfigurationError( 

328 "{} Separator provided with both outlet_list and " 

329 "num_outlets arguments, which were not consistent (" 

330 "length of outlet_list was not equal to num_outlets). " 

331 "Please check your arguments for consistency, and " 

332 "note that it is only necessry to provide one of " 

333 "these arguments.".format(self.name) 

334 ) 

335 elif self.config.outlet_list is None and self.config.num_outlets is None: 335 ↛ 337line 335 didn't jump to line 337 because the condition on line 335 was never true

336 # If no arguments provided for outlets, default to num_outlets = 2 

337 self.config.num_outlets = 2 

338 

339 # Create a list of names for outlet StateBlocks 

340 if self.config.outlet_list is not None: 

341 outlet_list = self.config.outlet_list 

342 else: 

343 outlet_list = [ 

344 "outlet_" + str(n) for n in range(1, self.config.num_outlets + 1) 

345 ] 

346 

347 return outlet_list 

348 

349 def _add_outlet_state_blocks(self, outlet_list): 

350 """ 

351 Construct StateBlocks for all outlet streams. 

352 

353 Args: 

354 list of strings to use as StateBlock names 

355 

356 Returns: 

357 list of StateBlocks 

358 """ 

359 # Setup StateBlock argument dict 

360 tmp_dict = dict(**self.config.property_package_args) 

361 tmp_dict["has_phase_equilibrium"] = False 

362 tmp_dict["defined_state"] = False 

363 

364 # Create empty list to hold StateBlocks for return 

365 outlet_blocks = [] 

366 

367 # Create an instance of StateBlock for all outlets 

368 for o in outlet_list: 

369 o_obj = self.config.property_package.build_state_block( 

370 self.flowsheet().time, doc="Material properties at outlet", **tmp_dict 

371 ) 

372 

373 setattr(self, o + "_state", o_obj) 

374 

375 outlet_blocks.append(getattr(self, o + "_state")) 

376 

377 return outlet_blocks 

378 

379 def _add_mixed_state_block(self): 

380 """ 

381 Constructs StateBlock to represent mixed stream. 

382 

383 Returns: 

384 New StateBlock object 

385 """ 

386 # Setup StateBlock argument dict 

387 tmp_dict = dict(**self.config.property_package_args) 

388 tmp_dict["has_phase_equilibrium"] = False 

389 tmp_dict["defined_state"] = True 

390 

391 self.mixed_state = self.config.property_package.build_state_block( 

392 self.flowsheet().time, doc="Material properties of mixed stream", **tmp_dict 

393 ) 

394 

395 return self.mixed_state 

396 

397 def _add_inlet_port_objects(self, mixed_block): 

398 """ Adds inlet Port object.""" 

399 self.add_port(name="inlet", block=mixed_block, doc="Inlet Port") 

400 

401 def _add_outlet_port_objects(self, outlet_list): 

402 """Adds outlet Port objects.""" 

403 for p in outlet_list: 

404 o_state = getattr(self, p + "_state") 

405 self.add_port(name=p, block=o_state, doc="Outlet Port") 

406 

407 def _add_material_balance(self, mixed_block, outlet_blocks): 

408 """Add overall material balance equation.""" 

409 # Get phase component list(s) 

410 pc_set = mixed_block.phase_component_set 

411 

412 # Write phase-component balances 

413 @self.Constraint(self.flowsheet().time, doc="Material balance equation") 

414 def material_balance_equation(b, t): 

415 return 0 == sum( 

416 sum( 

417 mixed_block[t].get_material_flow_terms(p, j) 

418 - 

419 sum( 

420 o[t].get_material_flow_terms(p, j) 

421 for o in outlet_blocks 

422 ) 

423 for j in mixed_block.component_list 

424 if (p, j) in pc_set 

425 ) 

426 for p in mixed_block.phase_list 

427 ) 

428 

429 def _add_energy_balance(self, mixed_block, outlet_blocks): 

430 """ 

431 Creates constraints for splitting the energy flows. 

432 """ 

433 # split basis is equal_molar_enthalpy 

434 @self.Constraint( 

435 self.flowsheet().time, 

436 self.outlet_idx, 

437 doc="Molar enthalpy equality constraint", 

438 ) 

439 def molar_enthalpy_equality_eqn(b, t, o): 

440 o_block = getattr(self, o + "_state") 

441 return mixed_block[t].enth_mol == o_block[t].enth_mol 

442 

443 def _add_momentum_balance(self, mixed_block, outlet_blocks): 

444 """ 

445 Creates constraints for splitting the momentum flows - done by equating 

446 pressures in outlets. 

447 """ 

448 if self.config.momentum_balance_type is MomentumBalanceType.pressureTotal: 448 ↛ exitline 448 didn't return from function '_add_momentum_balance' because the condition on line 448 was always true

449 @self.Constraint( 

450 self.flowsheet().time, 

451 self.outlet_idx, 

452 doc="Pressure equality constraint", 

453 ) 

454 def pressure_equality_eqn(b, t, o): 

455 o_block = getattr(self, o + "_state") 

456 return mixed_block[t].pressure == o_block[t].pressure 

457 

458 def model_check(blk): 

459 """ 

460 This method executes the model_check methods on the associated state 

461 blocks (if they exist). This method is generally called by a unit model 

462 as part of the unit's model_check method. 

463 

464 Args: 

465 None 

466 

467 Returns: 

468 None 

469 """ 

470 # Try property block model check 

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

472 try: 

473 blk.mixed_state[t].model_check() 

474 except AttributeError: 

475 _log.warning( 

476 "{} Separator inlet state block has no " 

477 "model check. To correct this, add a " 

478 "model_check method to the associated " 

479 "StateBlock class.".format(blk.name) 

480 ) 

481 

482 try: 

483 outlet_list = blk._create_outlet_list() 

484 for o in outlet_list: 

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

486 o_block[t].model_check() 

487 except AttributeError: 

488 _log.warning( 

489 "{} Separator outlet state block has no " 

490 "model checks. To correct this, add a model_check" 

491 " method to the associated StateBlock class.".format(blk.name) 

492 ) 

493 

494 def initialize_build( 

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

496 ): 

497 """ 

498 Initialization routine for separator 

499 

500 Keyword Arguments: 

501 outlvl : sets output level of initialization routine 

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

503 default solver options) 

504 solver : str indicating which solver to use during 

505 initialization (default = None, use default solver) 

506 hold_state : flag indicating whether the initialization routine 

507 should unfix any state variables fixed during 

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

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

510 returned containing flags for which states were fixed 

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

512 unfixed after initialization by calling the release_state 

513 method. 

514 

515 Returns: 

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

517 states were fixed during initialization. 

518 """ 

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

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

521 

522 # Create solver 

523 opt = get_solver(solver, optarg) 

524 

525 mblock = blk.mixed_state 

526 flags = mblock.initialize( 

527 outlvl=outlvl, 

528 optarg=optarg, 

529 solver=solver, 

530 hold_state=True, 

531 ) 

532 

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

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

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

536 init_log.info( 

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

538 ) 

539 

540 # Initialize outlet StateBlocks 

541 outlet_list = blk._create_outlet_list() 

542 

543 # Premises for initializing outlet states: 

544 for o in outlet_list: 

545 # Get corresponding outlet StateBlock 

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

547 

548 # Create dict to store fixed status of state variables 

549 o_flags = {} 

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

551 

552 # Calculate values for state variables 

553 s_vars = o_block[t].define_state_vars() 

554 for v in s_vars: 

555 for k in s_vars[v]: 

556 # Record whether variable was fixed or not 

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

558 

559 # If fixed, use current value 

560 # otherwise calculate guess from mixed state and fix 

561 if not s_vars[v][k].fixed: 

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

563 if "flow" in v: 

564 # Leave initial value, but avoid negative flows 

565 if s_vars[v][k].value < 1e-4: 

566 s_vars[v][k].set_value(1e-2) 

567 else: 

568 # Otherwise intensive, equate to mixed stream 

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

570 

571 # Call initialization routine for outlet StateBlock 

572 o_block.initialize( 

573 outlvl=outlvl, 

574 optarg=optarg, 

575 solver=solver, 

576 hold_state=False, 

577 ) 

578 

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

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

581 s_vars = o_block[t].define_state_vars() 

582 for v in s_vars: 

583 for k in s_vars[v]: 

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

585 

586 init_log.info("Initialization Complete.") 

587 return flags 

588 

589 def release_state(blk, flags, outlvl=idaeslog.NOTSET): 

590 """ 

591 Method to release state variables fixed during initialization. 

592 

593 Keyword Arguments: 

594 flags : dict containing information of which state variables 

595 were fixed during initialization, and should now be 

596 unfixed. This dict is returned by initialize if 

597 hold_state = True. 

598 outlvl : sets output level of logging 

599 

600 Returns: 

601 None 

602 """ 

603 mblock = blk.mixed_state 

604 mblock.release_state(flags, outlvl=outlvl) 

605 

606 def calculate_scaling_factors(self): 

607 mb_type = self.config.material_balance_type 

608 mixed_state = self.mixed_state 

609 if mb_type == MaterialBalanceType.useDefault: 

610 t_ref = self.flowsheet().time.first() 

611 mb_type = mixed_state[t_ref].default_material_balance_type() 

612 super().calculate_scaling_factors() 

613 

614 if hasattr(self, "temperature_equality_eqn"): 

615 for (t, i), c in self.temperature_equality_eqn.items(): 

616 s = iscale.get_scaling_factor( 

617 mixed_state[t].temperature, default=1, warning=True 

618 ) 

619 iscale.constraint_scaling_transform(c, s) 

620 

621 if hasattr(self, "pressure_equality_eqn"): 

622 for (t, i), c in self.pressure_equality_eqn.items(): 

623 s = iscale.get_scaling_factor( 

624 mixed_state[t].pressure, default=1, warning=True 

625 ) 

626 iscale.constraint_scaling_transform(c, s) 

627 

628 if hasattr(self, "material_splitting_eqn"): 

629 if mb_type == MaterialBalanceType.componentPhase: 

630 for (t, _, p, j), c in self.material_splitting_eqn.items(): 

631 flow_term = mixed_state[t].get_material_flow_terms(p, j) 

632 s = iscale.get_scaling_factor(flow_term, default=1) 

633 iscale.constraint_scaling_transform(c, s) 

634 elif mb_type == MaterialBalanceType.componentTotal: 

635 for (t, _, j), c in self.material_splitting_eqn.items(): 

636 s = None 

637 for p in mixed_state.phase_list: 

638 try: 

639 ft = mixed_state[t].get_material_flow_terms(p, j) 

640 except KeyError: 

641 # This component does not exist in this phase 

642 continue 

643 if s is None: 

644 s = iscale.get_scaling_factor(ft, default=1) 

645 else: 

646 _s = iscale.get_scaling_factor(ft, default=1) 

647 s = _s if _s < s else s 

648 iscale.constraint_scaling_transform(c, s) 

649 elif mb_type == MaterialBalanceType.total: 

650 pc_set = mixed_state.phase_component_set 

651 for (t, _), c in self.material_splitting_eqn.items(): 

652 for i, (p, j) in enumerate(pc_set): 

653 ft = mixed_state[t].get_material_flow_terms(p, j) 

654 if i == 0: 

655 s = iscale.get_scaling_factor(ft, default=1) 

656 else: 

657 _s = iscale.get_scaling_factor(ft, default=1) 

658 s = _s if _s < s else s 

659 iscale.constraint_scaling_transform(c, s) 

660 

661 def _get_performance_contents(self, time_point=0): 

662 if hasattr(self, "split_fraction"): 

663 var_dict = {} 

664 for k, v in self.split_fraction.items(): 

665 if k[0] == time_point: 

666 var_dict[f"Split Fraction [{str(k[1:])}]"] = v 

667 return {"vars": var_dict} 

668 else: 

669 return None 

670 

671 def _get_stream_table_contents(self, time_point=0): 

672 outlet_list = self._create_outlet_list() 

673 

674 io_dict = {} 

675 io_dict["Inlet"] = self.mixed_state 

676 

677 for o in outlet_list: 

678 io_dict[o] = getattr(self, o + "_state") 

679 

680 return create_stream_table_dataframe(io_dict, time_point=time_point)