Coverage for backend/idaes_service/solver/custom/thermal_utility_systems/steam_user.py: 89%

255 statements  

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

1# Pyomo core 

2from pyomo.environ import ( 

3 Constraint, 

4 Expression, 

5 NonNegativeReals, 

6 Suffix, 

7 Var, 

8 value, 

9 units as UNIT, 

10) 

11from pyomo.core.base.reference import Reference 

12from pyomo.common.config import ConfigBlock, ConfigValue, Bool 

13 

14# IDAES core 

15from idaes.core import ( 

16 declare_process_block_class, 

17 UnitModelBlockData, 

18 useDefault, 

19 StateBlock, 

20) 

21from idaes.core.util import scaling 

22from idaes.core.util.config import is_physical_parameter_block 

23from idaes.core.util.tables import create_stream_table_dataframe 

24from idaes.core.solvers import get_solver 

25from idaes.core.initialization import ModularInitializerBase 

26 

27# Other 

28from property_packages.build_package import build_package 

29 

30# Logger 

31import idaes.logger as idaeslog 

32 

33# Typing 

34from typing import List 

35 

36 

37__author__ = "Ahuora Centre for Smart Energy Systems, University of Waikato, New Zealand" 

38 

39# Set up logger 

40_log = idaeslog.getLogger(__name__) 

41 

42class SteamUserInitializer(ModularInitializerBase): 

43 """Initializer for ``SteamUser``. 

44 

45 Parameters 

46 ---------- 

47 blk : SteamUser 

48 The unit model block to initialize. 

49 solver : optional 

50 A Pyomo/IDAES solver instance. Defaults to :func:`idaes.core.solvers.get_solver`. 

51 solver_options : dict, optional 

52 Options to set on the solver, e.g. tolerances. 

53 outlvl : int, optional 

54 IDAES log level (e.g. :data:`idaes.logger.WARNING`). 

55 

56 Returns 

57 ------- 

58 pyomo.opt.results.results_.SolverResults 

59 Result from the final solve. 

60 """ 

61 

62 def initialize(self, blk, **kwargs): 

63 # --- Solver setup 

64 solver = kwargs.get("solver", None) or get_solver() 

65 solver_options = kwargs.get("solver_options", {}) 

66 for k, v in solver_options.items(): 66 ↛ 67line 66 didn't jump to line 67 because the loop on line 66 never started

67 solver.options[k] = v 

68 

69 outlvl = kwargs.get("outlvl", idaeslog.WARNING) 

70 log = idaeslog.getLogger(__name__) 

71 

72 # --- Time index 

73 t0 = blk.flowsheet().time.first() 

74 

75 # --- 1) Initialize inlet state blocks 

76 inlet_blocks = list(blk.inlet_blocks) 

77 if blk.config.has_desuperheating: 

78 inlet_steam, inlet_water = inlet_blocks 

79 inlet_water[t0].pressure.set_value( 

80 inlet_steam[t0].pressure 

81 ) 

82 inlet_water[t0].enth_mol.set_value( 

83 blk.water.htpx( 

84 blk.bfw_temperature[t0], 

85 inlet_water[t0].pressure, 

86 ) 

87 ) 

88 else: 

89 inlet_steam = inlet_blocks[0] 

90 

91 for sb in inlet_blocks: 

92 if hasattr(sb, "initialize"): 92 ↛ 91line 92 didn't jump to line 91 because the condition on line 92 was always true

93 sb.initialize(outlvl=outlvl) 

94 

95 # --- 2) Seed satuarate inlet-related state 

96 if blk.config.has_desuperheating: 

97 sb = blk._int_inlet_sat_vap_state 

98 sb[t0].pressure.set_value( 

99 inlet_steam[t0].pressure 

100 ) 

101 sb[t0].enth_mol.set_value( 

102 inlet_steam[t0].enth_mol_sat_phase["Vap"] 

103 ) 

104 if hasattr(sb, "initialize"): 104 ↛ 108line 104 didn't jump to line 108 because the condition on line 104 was always true

105 sb.initialize(outlvl=outlvl) 

106 

107 # --- 3) Aggregate inlet info for seeding mixed state block 

108 ms = blk._int_mixed_inlet_state 

109 ms[t0].pressure.set_value( 

110 inlet_steam[t0].pressure 

111 ) 

112 if blk.config.has_desuperheating: 

113 if value(blk.deltaT_superheat[t0]) > 0: 

114 ms[t0].enth_mol.set_value( 

115 blk.water.htpx( 

116 T=blk._int_inlet_sat_vap_state[t0].temperature + blk.deltaT_superheat[t0], 

117 p=inlet_steam[t0].pressure, 

118 ) 

119 ) 

120 else: 

121 ms[t0].enth_mol.set_value( 

122 blk._int_inlet_sat_vap_state[t0].enth_mol 

123 ) 

124 

125 if abs(value(ms[t0].enth_mol - inlet_water[t0].enth_mol)) > 0: 125 ↛ 130line 125 didn't jump to line 130 because the condition on line 125 was always true

126 ms[t0].flow_mol.set_value( 

127 inlet_steam[t0].flow_mol * (inlet_steam[t0].enth_mol - ms[t0].enth_mol) / (ms[t0].enth_mol - inlet_water[t0].enth_mol) 

128 ) 

129 else: 

130 ms[t0].flow_mol.set_value( 

131 inlet_steam[t0].flow_mol 

132 ) 

133 else: 

134 ms[t0].enth_mol.set_value( 

135 inlet_steam[t0].enth_mol 

136 ) 

137 ms[t0].flow_mol.set_value( 

138 inlet_steam[t0].flow_mol 

139 ) 

140 

141 if hasattr(ms, "initialize"): 141 ↛ 146line 141 didn't jump to line 146 because the condition on line 141 was always true

142 ms.initialize(outlvl=outlvl) 

143 

144 # --- 4) Seed outlet-related states 

145 # Condensate after heating (internal) 

146 ios = blk._int_outlet_cond_state 

147 ios[t0].flow_mol.set_value( 

148 ms[t0].flow_mol 

149 ) 

150 ios[t0].pressure.set_value( 

151 ms[t0].pressure - blk.pressure_loss[t0] 

152 ) 

153 ios[t0].enth_mol.set_value( 

154 blk.water.htpx( 

155 T=blk._int_outlet_cond_state[t0].temperature_sat - blk.deltaT_subcool[t0], 

156 p=ios[t0].pressure, 

157 ) 

158 ) 

159 if hasattr(ios, "initialize"): 159 ↛ 163line 159 didn't jump to line 163 because the condition on line 159 was always true

160 ios.initialize(outlvl=outlvl) 

161 

162 # --- 5) Seed satuarate outlet-related state 

163 sb = blk._int_outlet_sat_liq_state 

164 sb[t0].pressure.set_value( 

165 ios[t0].pressure 

166 ) 

167 sb[t0].enth_mol.set_value( 

168 ios[t0].enth_mol_sat_phase["Vap"] 

169 ) 

170 if hasattr(sb, "initialize"): 170 ↛ 175line 170 didn't jump to line 175 because the condition on line 170 was always true

171 sb.initialize(outlvl=outlvl) 

172 

173 # --- 5) Seed external outlets 

174 # Return line properties at user-specified temperature 

175 ors = blk.outlet_return_state 

176 ors[t0].flow_mol.set_value(ms[t0].flow_mol * value(blk.cond_return_rate[t0])) 

177 ors[t0].pressure.set_value( 

178 ms[t0].pressure - blk.pressure_loss[t0] 

179 ) 

180 ors[t0].enth_mol.set_value( 

181 blk.water.htpx( 

182 T=blk.cond_return_temperature[t0], 

183 p=ors[t0].pressure, 

184 ) 

185 ) 

186 if hasattr(ors, "initialize"): 186 ↛ 191line 186 didn't jump to line 191 because the condition on line 186 was always true

187 ors.initialize(outlvl=outlvl) 

188 

189 # --- 6) Drain/blowdown outlet uses reference enthalpy (fixed later in build) 

190 # Flow/pressure will be solved by constraints 

191 if hasattr(blk.outlet_drain_state, "initialize"): 191 ↛ 195line 191 didn't jump to line 195 because the condition on line 191 was always true

192 blk.outlet_drain_state.initialize(outlvl=outlvl) 

193 

194 # --- 7) Solve 

195 res = solver.solve(blk, tee=False) 

196 log.info(f"SteamUser init status: {res.solver.termination_condition}") 

197 return res 

198 

199def _make_config_block(config): 

200 """Declare configuration options for the SteamUser unit. 

201 

202 Declares property package references and integer counts for inlets and outlets. 

203 

204 Args: 

205 config (ConfigBlock): The mutable configuration block to populate. 

206 """ 

207 

208 config.declare( 

209 "property_package", 

210 ConfigValue( 

211 default=useDefault, 

212 domain=is_physical_parameter_block, 

213 description="Property package to use for control volume", 

214 ), 

215 ) 

216 config.declare( 

217 "property_package_args", 

218 ConfigBlock( 

219 implicit=True, 

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

221 ), 

222 ) 

223 config.declare( 

224 "has_desuperheating", 

225 ConfigValue( 

226 default=False, 

227 domain=Bool, 

228 description="If true, include desuperheating prior to use as process heat. " \ 

229 "Adds the state variable of the degree of superheat after desuperheating. Default: 0.", 

230 ), 

231 ) 

232 

233@declare_process_block_class("SteamUser") 

234class SteamUserData(UnitModelBlockData): 

235 """Steam user unit operation. 

236 

237 The SteamUser aggregates thermal loads from multiple sub-users (heaters) within a site. 

238 Desuperheating the flow is optional. Heat loss and pressure loss may be defined.  

239 A mixed (intermediate) states are used for balances. 

240 

241 Key features: 

242 - Material, energy, and momentum balances around the user 

243 - Optional desuperheating step prior to process use 

244 - User-specified condensate return rate and temperature 

245 - Optional heat and pressure losses. 

246 

247 Attributes: 

248 inlet_list (list[str]): Names for inlet ports. 

249 outlet_list (list[str]): Names for outlet ports (incl. condensate/ and vent). 

250 inlet_blocks (list): StateBlocks for all inlets. 

251 outlet_blocks (list): StateBlocks for all outlets. 

252 _int_mixed_inlet_state: Intermediate mixture StateBlock. 

253  

254 heat_loss (Var): Heat loss from the header (W). 

255 pressure_loss (Var): Pressure drop from inlet minimum to mixed state (Pa). 

256 bfw_flow_mol (Var): Required inlet boiler feed water flow for desuperheating (mol/s). 

257 """ 

258 

259 default_initializer=SteamUserInitializer 

260 CONFIG = UnitModelBlockData.CONFIG() 

261 _make_config_block(CONFIG) 

262 

263 def build(self) -> None: 

264 """Build the unit model structure (ports, states, constraints).""" 

265 # 1. Inherit standard UnitModelBlockData properties and functions 

266 super().build() 

267 

268 # 2. Validate input parameters are valid 

269 self._validate_model_config() 

270 

271 # 3. Create lists of ports with state blocks to add 

272 self.inlet_list = self._create_inlet_port_name_list() 

273 self.outlet_list = self._create_outlet_port_name_list() 

274 

275 # 4. Declare ports, state blocks and state property bounds  

276 self.inlet_blocks = self._add_ports_with_state_blocks( 

277 stream_list=self.inlet_list, 

278 is_inlet=True, 

279 has_phase_equilibrium=False, 

280 is_defined_state=True, 

281 ) 

282 self.outlet_blocks = self._add_ports_with_state_blocks( 

283 stream_list=self.outlet_list, 

284 is_inlet=False, 

285 has_phase_equilibrium=False, 

286 is_defined_state=False 

287 ) 

288 self._internal_blocks = self._add_internal_state_blocks() 

289 self._ref_enth = self._add_environmental_reference_enth() 

290 self._add_bounds_to_state_properties() 

291 

292 # 4. Declare references, variables and expressions for external and internal use 

293 self._create_references() 

294 self._create_variables() 

295 self._create_expressions() 

296 

297 # 5. Set balance equations 

298 self._add_material_balances() 

299 self._add_energy_balances() 

300 self._add_momentum_balances() 

301 self._add_additional_constraints() 

302 

303 # 6. Other 

304 self.scaling_factor = Suffix(direction=Suffix.EXPORT) 

305 

306 # ------------------------------------------------------------------ 

307 # Helpers & construction utilities 

308 # ------------------------------------------------------------------ 

309 def _validate_model_config(self) -> bool: 

310 """Validate configuration for inlet and outlet counts. 

311 

312 Raises: 

313 ValueError: If ``property_package is None``. 

314 """ 

315 if self.config.property_package is None: 315 ↛ 316line 315 didn't jump to line 316 because the condition on line 315 was never true

316 raise ValueError("SteamUser: Property package not defined.") 

317 return True 

318 

319 def _create_inlet_port_name_list(self) -> List[str]: 

320 """Build ordered inlet port names. 

321 

322 Returns: 

323 list[str]: Names 

324 """ 

325 

326 return ( 

327 [ 

328 "inlet_steam", "inlet_water" 

329 ] 

330 if self.config.has_desuperheating else 

331 [ 

332 "inlet_steam" 

333 ] 

334 ) 

335 

336 def _create_outlet_port_name_list(self) -> List[str]: 

337 """Build ordered outlet port names. 

338 

339 Returns: 

340 list[str]: Names  

341 """ 

342 return [ 

343 "outlet_return", 

344 "outlet_drain", 

345 ] 

346 

347 def _add_ports_with_state_blocks(self, 

348 stream_list: List[str], 

349 is_inlet: List[str], 

350 has_phase_equilibrium: bool=False, 

351 is_defined_state: bool=None, 

352 ) -> List[StateBlock]: 

353 """Construct StateBlocks and expose them as ports. 

354 

355 Creates a StateBlock per named stream and attaches a corresponding inlet or 

356 outlet Port. Inlet blocks are defined states; outlet blocks are calculated states. 

357 

358 Args: 

359 stream_list (list[str]): Port/StateBlock base names to create. 

360 is_inlet (bool): If True, create inlet ports with ``defined_state=True``; 

361 otherwise create outlet ports with ``defined_state=False``. 

362 has_phase_equilibrium (bool) 

363 

364 Returns: 

365 list: The created StateBlocks, in the same order as ``stream_list``. 

366 """ 

367 # Create empty list to hold StateBlocks for return 

368 state_block_ls = [] 

369 

370 # Setup StateBlock argument dict 

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

372 tmp_dict["has_phase_equilibrium"] = has_phase_equilibrium 

373 if is_defined_state == None: 373 ↛ 374line 373 didn't jump to line 374 because the condition on line 373 was never true

374 tmp_dict["defined_state"] = True if is_inlet else False 

375 else: 

376 tmp_dict["defined_state"] = is_defined_state 

377 

378 # Create an instance of StateBlock for all streams 

379 for s in stream_list: 

380 sb = self.config.property_package.build_state_block( 

381 self.flowsheet().time, doc=f"Thermophysical properties at {s}", **tmp_dict 

382 ) 

383 setattr( 

384 self, s + "_state", 

385 sb 

386 ) 

387 state_block_ls.append(sb) 

388 add_fn = self.add_inlet_port if is_inlet else self.add_outlet_port 

389 add_fn( 

390 name=s, 

391 block=sb, 

392 ) 

393 

394 return state_block_ls 

395 

396 def _add_internal_state_blocks(self) -> List[StateBlock]: 

397 """Create the intermediate StateBlock(s).""" 

398 # The _int_outlet_cond_state: 

399 # - Is not a defined state (solved from balances). 

400 # - Represents the state of the condensate after delivering process heating. 

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

402 tmp_dict["has_phase_equilibrium"] = False 

403 tmp_dict["defined_state"] = False 

404 

405 self._int_outlet_cond_state = self.config.property_package.build_state_block( 

406 self.flowsheet().time, 

407 doc="Thermophysical properties of condensate after process heating.", 

408 **tmp_dict 

409 ) 

410 # The _int_mixed_inlet_state: 

411 # - Has phase equilibrium enabled. 

412 # - Is not a defined state (solved from balances). 

413 # - Always exists even when not desuperheating 

414 tmp_dict["has_phase_equilibrium"] = True 

415 tmp_dict["defined_state"] = False 

416 self._int_mixed_inlet_state = self.config.property_package.build_state_block( 

417 self.flowsheet().time, 

418 doc="Thermophysical properties internal mixed inlet state after desuperheating (if applicable).", 

419 **tmp_dict 

420 ) 

421 # The _int_outlet_sat_liq_state: 

422 # - Has phase equilibrium enabled. 

423 # - Is not a defined state (solved from balances). 

424 # - Always exists even when not desuperheating 

425 tmp_dict["has_phase_equilibrium"] = True 

426 tmp_dict["defined_state"] = False 

427 self._int_outlet_sat_liq_state = self.config.property_package.build_state_block( 

428 self.flowsheet().time, 

429 doc="Thermophysical properties internal mixed saturate state.", 

430 **tmp_dict 

431 ) 

432 self._int_outlet_sat_liq_state[:].flow_mol.fix(1) 

433 

434 if self.config.has_desuperheating: 

435 # The _int_inlet_sat_vap_state: 

436 # - Has phase equilibrium enabled. 

437 # - Is not a defined state (solved from balances). 

438 # - Only exists when desuperheating 

439 tmp_dict["has_phase_equilibrium"] = True 

440 tmp_dict["defined_state"] = False 

441 self._int_inlet_sat_vap_state = self.config.property_package.build_state_block( 

442 self.flowsheet().time, 

443 doc="Thermophysical properties internal mixed saturate state.", 

444 **tmp_dict 

445 ) 

446 self._int_inlet_sat_vap_state[:].flow_mol.fix(1) 

447 

448 return [ 

449 self._int_mixed_inlet_state, 

450 self._int_outlet_cond_state, 

451 self._int_outlet_sat_liq_state, 

452 self._int_inlet_sat_vap_state, 

453 ] if self.config.has_desuperheating else [ 

454 self._int_mixed_inlet_state, 

455 self._int_outlet_cond_state, 

456 self._int_outlet_sat_liq_state, 

457 ] 

458 

459 def _add_environmental_reference_enth(self) -> None: 

460 """Create a helper to compute reference enthalpy at 15°C, 1 atm (water).""" 

461 self.water = build_package("helmholtz", ["water"], ["Liq"]) 

462 return self.water.htpx( 

463 (15 + 273.15) * UNIT.K, 

464 101325 * UNIT.Pa 

465 ) 

466 

467 def _add_bounds_to_state_properties(self) -> None: 

468 """Add lower and/or upper bounds to state properties. 

469 

470 - Set nonnegativity lower bounds on all inlet/intermediate/outlet flows. 

471 """ 

472 for sb in (self.inlet_blocks + self.outlet_blocks + self._internal_blocks): 

473 for t in sb: 

474 sb[t].flow_mol.setlb(0.0) 

475 

476 def _create_references(self) -> None: 

477 """Create convenient References. 

478 

479 Creates references to _int_mixed_inlet_state properties: 

480 - ``bfw_temperature``  

481 - ``bfw_flow_mass`` 

482 - ``bfw_flow_mol`` 

483 """ 

484 # Read only variables, only applicable if desuperheating is active 

485 if self.config.has_desuperheating: 

486 self.bfw_flow_mass = Reference( 

487 self.inlet_water_state[:].flow_mass 

488 ) 

489 self.bfw_flow_mol = Reference( 

490 self.inlet_water_state[:].flow_mol 

491 ) 

492 self.inlet_water_state[:].flow_mol.unfix() 

493 

494 def _create_variables(self) -> None: 

495 """Declare decision/parameter variables for the unit. 

496 

497 Creates: 

498 - ``heat_demand``  

499 - ``cond_return_rate``  

500 - ``cond_return_temperature``  

501 - ``deltaT_subcool``  

502 - ``heat_loss``  

503 - ``pressure_loss``  

504 If desuperheating: 

505 - ``bfw_temperature``  

506 - ``deltaT_superheat``  

507 """ 

508 # Get units consistent with the property package 

509 units_meta = self.config.property_package.get_metadata() 

510 

511 # Calculated: Process heat demand (kW) — user typically fixes 

512 self.heat_demand = Var( 

513 self.flowsheet().time, 

514 domain=NonNegativeReals, 

515 doc="Process heat demand of the users. Default: 0 kW.", 

516 units=units_meta.get_derived_units("power"), 

517 ) 

518 self.heat_demand[:].set_value( 

519 0 # Default value 

520 ) 

521 # User defined: Fraction of total condensate that returns to boiler (dimensionless) 

522 self.cond_return_rate = Var( 

523 self.flowsheet().time, 

524 domain=NonNegativeReals, 

525 bounds=(0,1), 

526 doc="Fraction of condensate returned to the boiler. Default: 0.7." 

527 ) 

528 self.cond_return_rate.fix( 

529 0.7 # Default value 

530 ) 

531 # User defined: Condensate return temperature (degC or K) 

532 self.cond_return_temperature = Var( 

533 self.flowsheet().time, 

534 domain=NonNegativeReals, 

535 doc="Temperature at which the condensate returns to the boiler. Default: 80 degC.", 

536 units=units_meta.get_derived_units("temperature"), 

537 ) 

538 self.cond_return_temperature.fix( 

539 (80 + 273.15) * UNIT.K # Default fixed value 

540 ) 

541 # User defined: Subcooling target delta T for condensate after process heating (K) 

542 self.deltaT_subcool = Var( 

543 self.flowsheet().time, 

544 domain=NonNegativeReals, 

545 doc="The target amount of subcooling of the condensate after process heating. Default: 0 K.", 

546 units=units_meta.get_derived_units("temperature"), 

547 ) 

548 self.deltaT_subcool.fix( 

549 0 # Default fixed value 

550 ) 

551 # User defined: Heat and pressure losses 

552 self.heat_loss = Var( 

553 self.flowsheet().time, 

554 domain=NonNegativeReals, 

555 doc="Heat loss. Default: 0 kW.", 

556 units=units_meta.get_derived_units("power") 

557 ) 

558 self.heat_loss.fix( 

559 0 # Default fixed value 

560 ) 

561 self.pressure_loss = Var( 

562 self.flowsheet().time, 

563 domain=NonNegativeReals, 

564 doc="Pressure loss. Default: 0 Pa.", 

565 units=units_meta.get_derived_units("pressure") 

566 ) 

567 self.pressure_loss.fix( 

568 0 # Default fixed value 

569 ) 

570 

571 # User defined when desuperheating is active, otherwise do not show 

572 if self.config.has_desuperheating: 

573 # User defined: Boiler feed water temperature entering the desuperheater 

574 self.bfw_temperature = Var( 

575 self.flowsheet().time, 

576 domain=NonNegativeReals, 

577 doc="The target amount of subcooling of the condensate after process heating. Default: 0 K.", 

578 units=units_meta.get_derived_units("temperature"), 

579 ) 

580 self.bfw_temperature.fix( 

581 (110 + 273.15) * UNIT.K # Default fixed value 

582 ) 

583 # User defined: Target degree of superheat at the outlet of the desuperheater 

584 self.deltaT_superheat = Var( 

585 self.flowsheet().time, 

586 domain=NonNegativeReals, 

587 doc="The target amount of superheat present in the steam after desuperheating before use. Default: 0 K.", 

588 units=units_meta.get_derived_units("temperature"), 

589 ) 

590 self.deltaT_superheat.fix( 

591 0 # Default fixed value 

592 ) 

593 

594 def _create_expressions(self) -> None: 

595 """Create helper Expressions. 

596 

597 Creates: 

598 - ``energy_lost``  

599 """ 

600 # Calculated, always show 

601 self.energy_lost = Expression( 

602 self.flowsheet().time, 

603 rule=lambda b, t: ( 

604 b._int_outlet_cond_state[t].flow_mol 

605 * (b._int_outlet_cond_state[t].enth_mol - b._ref_enth) 

606 - 

607 b.outlet_return_state[t].flow_mol 

608 * (b.outlet_return_state[t].enth_mol - b._ref_enth) 

609 ), 

610 doc="Energy lost from condensate cooling and condensate to drain.", 

611 ) 

612 

613 # ------------------------------------------------------------------ 

614 # Balances 

615 # ------------------------------------------------------------------ 

616 def _add_material_balances(self) -> None: 

617 """Material balance equations summary. 

618 

619 Balances / Constraints: 

620 - ``overall_material_balance`` 

621 - ``condensate_return_material_eq`` 

622 - ``intermediate_material_balance_post_heating`` 

623 - ``intermediate_material_balance_pre_heating`` 

624 """ 

625 @self.Constraint( 

626 self.flowsheet().time, 

627 doc="Overall material balance", 

628 ) 

629 def overall_material_balance(b, t): 

630 return ( 

631 sum( 

632 o[t].flow_mol 

633 for o in b.outlet_blocks 

634 ) 

635 == 

636 sum( 

637 i[t].flow_mol 

638 for i in b.inlet_blocks 

639 ) 

640 ) 

641 @self.Constraint( 

642 self.flowsheet().time, 

643 doc="Intermediate material balance", 

644 ) 

645 def intermediate_material_balance_pre_heating(b, t): 

646 return ( 

647 b._int_mixed_inlet_state[t].flow_mol 

648 == 

649 sum( 

650 i[t].flow_mol 

651 for i in b.inlet_blocks 

652 ) 

653 ) 

654 @self.Constraint( 

655 self.flowsheet().time, 

656 doc="Intermediate material balance", 

657 ) 

658 def intermediate_material_balance_post_heating(b, t): 

659 return ( 

660 b._int_outlet_cond_state[t].flow_mol 

661 == 

662 sum( 

663 i[t].flow_mol 

664 for i in b.inlet_blocks 

665 ) 

666 ) 

667 @self.Constraint( 

668 self.flowsheet().time, 

669 doc="Condensate return material equation", 

670 ) 

671 def condensate_return_material_eq(b, t): 

672 return ( 

673 b.outlet_return_state[t].flow_mol 

674 == 

675 sum( 

676 i[t].flow_mol 

677 for i in b.inlet_blocks 

678 ) 

679 * 

680 b.cond_return_rate[t] 

681 ) 

682 

683 def _add_energy_balances(self) -> None: 

684 """Energy balance equations summary. 

685 

686 Balances / Constraints: 

687 - ``mixing_energy_balance`` 

688 - ``heating_energy_balance`` 

689 """ 

690 @self.Constraint( 

691 self.flowsheet().time, 

692 doc="Inlet mixing energy balance", 

693 ) 

694 def mixing_energy_balance(b, t): 

695 return ( 

696 b._int_mixed_inlet_state[t].flow_mol * b._int_mixed_inlet_state[t].enth_mol 

697 == 

698 sum( 

699 i[t].flow_mol * i[t].enth_mol 

700 for i in b.inlet_blocks 

701 ) 

702 ) 

703 @self.Constraint( 

704 self.flowsheet().time, 

705 doc="Process heating energy balance", 

706 ) 

707 def heating_energy_balance(b, t): 

708 return ( 

709 b._int_mixed_inlet_state[t].flow_mol * b._int_mixed_inlet_state[t].enth_mol 

710 == 

711 b._int_outlet_cond_state[t].flow_mol * b._int_outlet_cond_state[t].enth_mol 

712 + 

713 b.heat_loss[t] 

714 + 

715 b.heat_demand[t] 

716 ) 

717 self.outlet_drain_state[:].enth_mol.fix( 

718 value( 

719 self._ref_enth 

720 ) 

721 ) 

722 @self.Constraint( 

723 self.flowsheet().time, 

724 doc="Saturated liquid enthalpy", 

725 ) 

726 def saturated_liq_enthalpy_eq(b, t): 

727 return ( 

728 b._int_outlet_sat_liq_state[t].enth_mol_sat_phase["Liq"] 

729 == 

730 b._int_outlet_sat_liq_state[t].enth_mol 

731 ) 

732 if self.config.has_desuperheating: 

733 @self.Constraint( 

734 self.flowsheet().time, 

735 doc="Saturated vapour enthalpy", 

736 ) 

737 def saturated_vap_enthalpy_eq(b, t): 

738 return ( 

739 b._int_inlet_sat_vap_state[t].enth_mol_sat_phase["Vap"] 

740 == 

741 b._int_inlet_sat_vap_state[t].enth_mol 

742 ) 

743 

744 def _add_momentum_balances(self) -> None: 

745 """Momentum balance equations summary. 

746 

747 Balances / Constraints: 

748 - ``mixing_momentum_balance`` 

749 - ``heating_momentum_balance`` 

750 - ``outlet_momentum_balance`` 

751 If desuperheating: 

752 - ``intlet_water_momentum_balance``  

753 """ 

754 @self.Constraint( 

755 self.flowsheet().time, 

756 doc="Momentum equalities", 

757 ) 

758 def mixing_momentum_balance(b, t): 

759 return ( 

760 b.inlet_steam_state[t].pressure 

761 == 

762 b._int_mixed_inlet_state[t].pressure 

763 ) 

764 @self.Constraint( 

765 self.flowsheet().time, 

766 doc="Process heating momentum balance", 

767 ) 

768 def heating_momentum_balance(b, t): 

769 return ( 

770 b._int_mixed_inlet_state[t].pressure 

771 == 

772 b._int_outlet_cond_state[t].pressure 

773 + 

774 b.pressure_loss[t] 

775 ) 

776 @self.Constraint( 

777 self.flowsheet().time, 

778 doc="Saturated liq pressure", 

779 ) 

780 def saturated_liq_pressure_eq(b, t): 

781 return ( 

782 b._int_outlet_sat_liq_state[t].pressure 

783 == 

784 b._int_outlet_cond_state[t].pressure 

785 ) 

786 @self.Constraint( 

787 self.flowsheet().time, 

788 doc="Outlet momentum equality", 

789 ) 

790 def outlet_momentum_balance(b, t): 

791 return ( 

792 b.outlet_return_state[t].pressure 

793 == 

794 b.outlet_drain_state[t].pressure 

795 ) 

796 self.outlet_return_state[:].pressure.fix( 

797 101325 * UNIT.Pa # Fixed value, hidden from the user 

798 ) 

799 if self.config.has_desuperheating: 

800 @self.Constraint( 

801 self.flowsheet().time, 

802 doc="Inlet water momentum balance", 

803 ) 

804 def intlet_water_momentum_balance(b, t): 

805 return ( 

806 b.inlet_water_state[t].pressure 

807 == 

808 b.inlet_steam_state[t].pressure 

809 ) 

810 @self.Constraint( 

811 self.flowsheet().time, 

812 doc="Saturated vapour pressure", 

813 ) 

814 def saturated_vap_pressure_eq(b, t): 

815 return ( 

816 b.inlet_steam_state[t].pressure 

817 == 

818 b._int_inlet_sat_vap_state[t].pressure 

819 ) 

820 

821 def _add_additional_constraints(self) -> None: 

822 """Add auxiliary constraints and bounds. 

823 

824 Constraints: 

825 - ``condensate_temperature_eq`` 

826 - ``subcooling_temperature_eq`` 

827 If desuperheating: 

828 - ``desuperheating_mixed_temperature_eq``  

829 """ 

830 @self.Constraint( 

831 self.flowsheet().time, 

832 doc="Condensate return temperature", 

833 ) 

834 def condensate_temperature_eq(b, t): 

835 return ( 

836 b.outlet_return_state[t].temperature 

837 == 

838 b.cond_return_temperature[t] 

839 ) 

840 @self.Constraint( 

841 self.flowsheet().time, 

842 doc="Subcool temperature", 

843 ) 

844 def subcooling_temperature_eq(b, t): 

845 return ( 

846 b._int_outlet_cond_state[t].temperature 

847 == 

848 b._int_outlet_sat_liq_state[t].temperature - b.deltaT_subcool[t] 

849 ) 

850 if self.config.has_desuperheating: 

851 @self.Constraint( 

852 self.flowsheet().time, 

853 doc="Inlet water temperature", 

854 ) 

855 def inlet_water_temperature_eq(b, t): 

856 return ( 

857 b.inlet_water_state[t].temperature 

858 == 

859 b.bfw_temperature[t] 

860 ) 

861 @self.Constraint( 

862 self.flowsheet().time, 

863 doc="Mixed temperature after desuperheating", 

864 ) 

865 def desuperheating_mixed_temperature_eq(b, t): 

866 return ( 

867 b._int_mixed_inlet_state[t].temperature 

868 == 

869 b._int_inlet_sat_vap_state[t].temperature + b.deltaT_superheat[t] 

870 ) 

871 

872 def calculate_scaling_factors(self): 

873 """Assign scaling factors to improve numerical conditioning. 

874 

875 Sets scaling factors for performance and auxiliary variables.  

876 """ 

877 super().calculate_scaling_factors() 

878 scaling.set_scaling_factor(self.heat_loss, 1e-6) # kW scale 

879 scaling.set_scaling_factor(self.pressure_loss, 1e-6) # Pa scale 

880 

881 def _get_stream_table_contents(self, time_point=0): 

882 """Create a stream table for all inlets and outlets. 

883 

884 Args: 

885 time_point (int | float): Time index at which to extract stream data. 

886 

887 Returns: 

888 pandas.DataFrame: A tabular view suitable for reporting via 

889 ``create_stream_table_dataframe``. 

890 """ 

891 io_dict = {} 

892 

893 for inlet_name in self.inlet_list: 

894 io_dict[inlet_name] = getattr(self, inlet_name) 

895 

896 for outlet_name in self.outlet_list: 

897 io_dict[outlet_name] = getattr(self, outlet_name) 

898 

899 return create_stream_table_dataframe(io_dict, time_point=time_point) 

900 

901 def _get_performance_contents(self, time_point=0): 

902 """Collect performance variables for reporting. 

903 

904 Args: 

905 time_point (int | float): Time index at which to report values. 

906 

907 Returns: 

908 dict: Mapping used by IDAES reporters, containing human-friendly labels 

909 to Vars/References (e.g., heat/pressure loss, mixed-state properties). 

910 """ 

911 perf = { 

912 "vars": { 

913 "Heat demand [W]": self.heat_demand[time_point], 

914 "Degree of subcooling target [K]": self.deltaT_subcool[time_point], 

915 "Heat loss [W]": self.heat_loss[time_point], 

916 "Pressure loss [Pa]": self.pressure_loss[time_point], 

917 "Condensate return rate [-]:": self.cond_return_rate[time_point], 

918 "Condensate return temperature [degC]": UNIT.convert_temp_K_to_C(self.cond_return_temperature[time_point]), 

919 }, 

920 "exprs": { 

921 "Energy lost to return network [W]": self.energy_lost[time_point], 

922 }, 

923 } 

924 if self.config.has_desuperheating: 

925 perf["vars"].update( 

926 { 

927 "BFW temperature [K]": self.bfw_temperature[time_point], 

928 "Degree of superheat target [K]": self.deltaT_superheat[time_point], 

929 } 

930 ) 

931 return perf 

932 

933 def initialize(self, *args, **kwargs): 

934 """Initialize the SteamUser unit using :class:`SteamUserInitializer`. 

935 

936 Args: 

937 *args: Forwarded to ``SteamUserInitializer.initialize``. 

938 **kwargs: Forwarded to ``SteamUserInitializer.initialize`` (e.g., solver, options). 

939 

940 Returns: 

941 pyomo.opt.results.results_.SolverResults: Results from the initializer's solve. 

942 """ 

943 init = SteamUserInitializer() 

944 return init.initialize(self, *args, **kwargs)