Coverage for backend/ahuora-builder/src/ahuora_builder/custom/thermal_utility_systems/heat_user.py: 75%

239 statements  

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

1"""Heat user unit model.""" 

2 

3from typing import Dict, Iterable, List, Optional 

4 

5from pyomo.common.config import Bool, ConfigBlock, ConfigValue, In 

6import pyomo.environ as pyo 

7from pyomo.environ import ( 

8 Constraint, 

9 Expression, 

10 NonNegativeReals, 

11 Suffix, 

12 Var, 

13 Param, 

14 value, 

15 units as pyunits, 

16 check_optimal_termination, 

17) 

18from pyomo.core.base.reference import Reference 

19 

20from idaes.core import StateBlock, UnitModelBlockData, declare_process_block_class, useDefault 

21from idaes.core.initialization import ModularInitializerBase 

22from idaes.core.solvers import get_solver 

23from idaes.core.util.config import is_physical_parameter_block 

24from idaes.core.util.exceptions import InitializationError 

25from idaes.core.scaling import CustomScalerBase, ConstraintScalingScheme 

26from idaes.core.util.tables import create_stream_table_dataframe 

27from idaes.core.util.model_statistics import degrees_of_freedom, report_statistics 

28from idaes.core.util.model_diagnostics import DiagnosticsToolbox 

29 

30import idaes.logger as idaeslog 

31 

32_log = idaeslog.getLogger(__name__) 

33 

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

35 

36 

37def _build_config(config: ConfigBlock) -> None: 

38 """Declare config entries for HeatUser.""" 

39 config.declare( 

40 "dynamic", 

41 ConfigValue( 

42 domain=In([False]), 

43 default=False, 

44 description="Dynamic model flag - must be False", 

45 ), 

46 ) 

47 config.declare( 

48 "has_holdup", 

49 ConfigValue( 

50 default=False, 

51 domain=In([False]), 

52 description="Holdup construction flag - must be False", 

53 ), 

54 ) 

55 config.declare( 

56 "property_package", 

57 ConfigValue( 

58 default=useDefault, 

59 domain=is_physical_parameter_block, 

60 description="Property package used to build StateBlocks.", 

61 ), 

62 ) 

63 config.declare( 

64 "property_package_args", 

65 ConfigBlock( 

66 implicit=True, 

67 description="Arguments passed when constructing StateBlocks.", 

68 ), 

69 ) 

70 config.declare( 

71 "has_phase_equilibrium", 

72 ConfigValue( 

73 default=False, 

74 domain=Bool, 

75 description="Default phase-equilibrium flag for inlet/outlet StateBlocks.", 

76 ), 

77 ) 

78 

79class HeatUserScaler(CustomScalerBase): 

80 """ 

81 Default modular scaler for the generic unit model. 

82 This Scaler relies on the associated property and reaction packages, 

83 either through user provided options (submodel_scalers argument) or by default 

84 Scalers assigned to the packages. 

85 """ 

86 

87 DEFAULT_SCALING_FACTORS = { 

88 "var1": 1e-3, 

89 "var2": 1e-3, 

90 } 

91 

92 

93 

94@declare_process_block_class("HeatUser") 

95class HeatUserData(UnitModelBlockData): 

96 """Heat user unit operation in the new template format.""" 

97 

98 default_scaler = HeatUserScaler 

99 

100 CONFIG = ConfigBlock() 

101 _build_config(CONFIG) 

102 

103 def build(self): 

104 """Build the generic unit structure then delegate unit-specific details. 

105 """ 

106 super().build() 

107 units_meta = self.config.property_package.get_metadata().get_derived_units 

108 

109 """ 

110 1. Build inlet, outlet and internal state blocks and associate  

111 with ports (where applicable) 

112 """ 

113 self.inlet_blocks = self._build_state_blocks( 

114 stream_name_list=["inlet"], 

115 has_phase_equilibrium=self.config.has_phase_equilibrium, 

116 is_defined_state=True, 

117 is_build_port=True, 

118 ) 

119 self.outlet_blocks = self._build_state_blocks( 

120 stream_name_list=["outlet_return", "outlet_drain"], 

121 has_phase_equilibrium=self.config.has_phase_equilibrium, 

122 is_defined_state=False, 

123 is_build_port=True, 

124 ) 

125 self.internal_blocks = self._build_state_blocks( 

126 stream_name_list=["int_outlet"], 

127 has_phase_equilibrium=self.config.has_phase_equilibrium, 

128 is_defined_state=False, 

129 is_build_port=False, 

130 ) 

131 

132 """ 

133 2. Create parameters, variables, references and expressions 

134 """ 

135 # State variables 

136 self.return_rate = Var( 

137 self.flowsheet().time, 

138 initialize=0.7, 

139 bounds=(0, 1), 

140 units=pyunits.dimensionless, 

141 doc="Fraction of condensate returned to the boiler.", 

142 ) 

143 self.deltaT_subcool = Var( 

144 self.flowsheet().time, 

145 initialize=0.01 * 300 * pyunits.K, 

146 bounds=(0, None), 

147 units=units_meta("temperature"), 

148 doc="Target subcooling after process heating.", 

149 ) 

150 self.outlet_temperature = Var( 

151 self.flowsheet().time, 

152 initialize=300 * pyunits.K, 

153 bounds=(0, None), 

154 units=units_meta("temperature"), 

155 doc="Target outlet temperature after process heating.", 

156 ) 

157 self.user_heat_loss = Var( 

158 self.flowsheet().time, 

159 initialize=0, 

160 bounds=(0, None), 

161 units=units_meta("power"), 

162 doc="Heat loss.", 

163 ) 

164 self.user_pressure_loss = Var( 

165 self.flowsheet().time, 

166 initialize=0, 

167 bounds=(0, None), 

168 units=units_meta("pressure"), 

169 doc="Pressure loss.", 

170 ) 

171 self.return_heat_loss = Var( 

172 self.flowsheet().time, 

173 initialize=0, 

174 bounds=(0, None), 

175 units=units_meta("power"), 

176 doc="Heat loss from the return and drain streams.", 

177 ) 

178 self.return_temperature = Var( 

179 self.flowsheet().time, 

180 initialize=(80 + 273.15) * pyunits.K, 

181 bounds=(0, None), 

182 units=units_meta("temperature"), 

183 doc="Condensate return temperature.", 

184 ) 

185 self.return_pressure = Var( 

186 self.flowsheet().time, 

187 initialize=101325 * pyunits.Pa, 

188 bounds=(0, None), 

189 units=units_meta("pressure"), 

190 doc="User-specified return/drain pressure target.", 

191 ) 

192 

193 # Non-normal state variables, other parameters and expression 

194 self.heat_demand = Var( 

195 self.flowsheet().time, 

196 initialize=0, 

197 bounds=(0, None), 

198 units=units_meta("power"), 

199 doc="Process heat demand.", 

200 ) 

201 self.env_temperature = Param( 

202 self.flowsheet().time, 

203 initialize=(15 + 273.15) * pyunits.K, 

204 mutable=True, 

205 units=units_meta("temperature"), 

206 doc="User-specified target for drain stream.", 

207 ) 

208 self.energy_lost = Expression( 

209 self.flowsheet().time, 

210 rule=lambda b, t: ( 

211 b.int_outlet_state[t].flow_mol * b.int_outlet_state[t].enth_mol 

212 - b.outlet_return_state[t].flow_mol * b.outlet_return_state[t].enth_mol 

213 - b.outlet_drain_state[t].flow_mol * b.outlet_drain_state[t].enth_mol 

214 ), 

215 doc="Energy lost from condensate cooling and discharge.", 

216 ) 

217 

218 """ 

219 3. Declare constraints to define mass, energy, and momentum balances,  

220 unit operation performance and other constraint  

221 """ 

222 # a) Material balance equations 

223 @self.Constraint(self.flowsheet().time, doc="Overall material balance") 

224 def eq_overall_material_balance(b, t): 

225 return b.outlet_return_state[t].flow_mol + b.outlet_drain_state[t].flow_mol == b.inlet_state[t].flow_mol 

226 @self.Constraint(self.flowsheet().time, doc="Internal material balance") 

227 def eq_internal_material_balance(b, t): 

228 return b.int_outlet_state[t].flow_mol == b.inlet_state[t].flow_mol 

229 @self.Constraint(self.flowsheet().time, doc="Condensate return balance") 

230 def eq_condensate_return_balance(b, t): 

231 return b.outlet_return_state[t].flow_mol == b.inlet_state[t].flow_mol * b.return_rate[t] 

232 

233 # b) Energy balance equations 

234 @self.Constraint(self.flowsheet().time, doc="User energy balance") 

235 def eq_user_energy_balance(b, t): 

236 return ( 

237 b.int_outlet_state[t].flow_mol * b.int_outlet_state[t].enth_mol + b.heat_demand[t] + b.user_heat_loss[t] 

238 == 

239 b.inlet_state[t].flow_mol * b.inlet_state[t].enth_mol 

240 ) 

241 @self.Constraint(self.flowsheet().time, doc="Return energy balance") 

242 def eq_return_energy_balance(b, t): 

243 return ( 

244 b.outlet_return_state[t].flow_mol * b.outlet_return_state[t].enth_mol + b.outlet_drain_state[t].flow_mol * b.outlet_drain_state[t].enth_mol + self.return_heat_loss[t] 

245 == 

246 b.int_outlet_state[t].flow_mol * b.int_outlet_state[t].enth_mol 

247 ) 

248 

249 # c) Momentum balance equations  

250 @self.Constraint(self.flowsheet().time, doc="User momentum balance") 

251 def eq_user_momentum_balance(b, t): 

252 return b.int_outlet_state[t].pressure + b.user_pressure_loss[t] == b.inlet_state[t].pressure 

253 @self.Constraint(self.flowsheet().time, doc="Return momentum balance") 

254 def eq_return_momentum_balance(b, t): 

255 return b.outlet_return_state[t].pressure == b.return_pressure[t] 

256 @self.Constraint(self.flowsheet().time, doc="Drain momentum balance") 

257 def eq_drain_momentum_balance(b, t): 

258 return b.outlet_drain_state[t].pressure == b.return_pressure[t] 

259 

260 # d) Performance equations and other constraints  

261 @self.Constraint(self.flowsheet().time, doc="Subcooling temperature relation") 

262 def eq_subcooling_temperature(b, t): 

263 return b.int_outlet_state[t].temperature == b.int_outlet_state[t].temperature_sat - b.deltaT_subcool[t] 

264 @self.Constraint(self.flowsheet().time, doc="Outlet temperature relation") 

265 def eq_outlet_temperature(b, t): 

266 return b.int_outlet_state[t].temperature == b.outlet_temperature[t] 

267 @self.Constraint(self.flowsheet().time, doc="Utility return temperature") 

268 def eq_return_temperature(b, t): 

269 return b.outlet_return_state[t].temperature == b.return_temperature[t] 

270 @self.Constraint(self.flowsheet().time, doc="Utility drain temperature") 

271 def eq_drain_temperature(b, t): 

272 return b.outlet_drain_state[t].temperature == b.env_temperature[t] 

273 

274 

275 def initialize_build(self, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None): 

276 """ 

277 General wrapper for template initialization routines 

278 

279 Keyword Arguments: 

280 state_args : a dict of arguments to be passed to the property 

281 package(s) to provide an initial state for 

282 initialization (see documentation of the specific 

283 property package) (default = {}). 

284 outlvl : sets output level of initialization routine 

285 optarg : solver options dictionary object (default=None) 

286 solver : str indicating which solver to use during 

287 initialization (default = None) 

288 

289 Returns: None 

290 """ 

291 init_log = idaeslog.getInitLogger(self.name, outlvl, tag="unit") 

292 solve_log = idaeslog.getSolveLogger(self.name, outlvl, tag="unit") 

293 t0 = self.flowsheet().config.time.first() # use first time point for initialisation 

294 

295 # Set solver options 

296 opt = get_solver(solver, optarg) 

297 

298 pp = self.inlet_state[t0].params # proporty package calculation block 

299 state_args = {} if state_args is None else dict(state_args) 

300 

301 # Helper functions 

302 def _value_or_none(obj): # returns the value of a Var or Param if it is fixed, otherwise returns None 

303 return value(obj, exception=False) 

304 

305 def _pick_seed(*candidates): # loops through different options of seeding and picks the first (in order of preference) 

306 for candidate in candidates: 306 ↛ 309line 306 didn't jump to line 309 because the loop on line 306 didn't complete

307 if candidate is not None: 

308 return candidate 

309 return None 

310 

311 def _enthalpy_from_tp(temperature, pressure): #  

312 if temperature is None or pressure is None: 312 ↛ 313line 312 didn't jump to line 313 because the condition on line 312 was never true

313 return None 

314 return value(pp.htpx(temperature * pyunits.K, pressure * pyunits.Pa)) 

315 

316 inlet_has_source = len(list(self.inlet.sources())) > 0 # A source Arc on the inlet port is a good signal that the current inlet values were populated by an upstream unit, which we should use as seeds 

317 

318 return_rate = _value_or_none(self.return_rate[t0]) 

319 user_pressure_loss = _value_or_none(self.user_pressure_loss[t0]) 

320 return_pressure = _value_or_none(self.return_pressure[t0]) 

321 return_temperature = _value_or_none(self.return_temperature[t0]) 

322 env_temperature = _value_or_none(self.env_temperature[t0]) 

323 

324 #----------------------------------------------------- 

325 # 1. Build state seeds. Prefer explicit state_args, then connected/fixed 

326 # values already present on the unit, then infer from local specs, then 

327 # fall back to generic guesses. 

328 

329 # Inlet seeds 

330 inlet_flow_from_fixed_inlet = ( 

331 _value_or_none(self.inlet_state[t0].flow_mol) 

332 if self.inlet_state[t0].flow_mol.fixed 

333 else None 

334 ) 

335 inlet_flow_from_upstream = ( 

336 _value_or_none(self.inlet_state[t0].flow_mol) 

337 if inlet_has_source 

338 else None 

339 ) 

340 return_flow_fixed = ( 

341 _value_or_none(self.outlet_return_state[t0].flow_mol) 

342 if self.outlet_return_state[t0].flow_mol.fixed 

343 else None 

344 ) 

345 drain_flow_fixed = ( 

346 _value_or_none(self.outlet_drain_state[t0].flow_mol) 

347 if self.outlet_drain_state[t0].flow_mol.fixed 

348 else None 

349 ) 

350 inlet_flow_from_return = None 

351 if return_flow_fixed is not None and return_rate is not None and abs(return_rate) >= 1e-8: 

352 inlet_flow_from_return = return_flow_fixed / return_rate 

353 inlet_flow_from_drain = None 

354 if drain_flow_fixed is not None and return_rate is not None and abs(1 - return_rate) >= 1e-8: 

355 inlet_flow_from_drain = drain_flow_fixed / (1 - return_rate) 

356 inlet_flow_from_total_outlets = None 

357 if return_flow_fixed is not None and drain_flow_fixed is not None: 

358 inlet_flow_from_total_outlets = return_flow_fixed + drain_flow_fixed 

359 

360 # Flow ranking: 

361 # 1. Explicit state_args 

362 # 2. A fixed inlet flow on this unit 

363 # 3. A propagated upstream inlet flow 

364 # 4. Back-calculate from fixed outlet flow(s) plus return split 

365 # 5. Nominal fallback 

366 f_inlet = _pick_seed( 

367 state_args.get("flow_mol"), 

368 inlet_flow_from_fixed_inlet, 

369 inlet_flow_from_upstream, 

370 inlet_flow_from_return, 

371 inlet_flow_from_drain, 

372 inlet_flow_from_total_outlets, 

373 500.0, 

374 ) 

375 

376 inlet_pressure_from_fixed_inlet = ( 

377 _value_or_none(self.inlet_state[t0].pressure) 

378 if self.inlet_state[t0].pressure.fixed 

379 else None 

380 ) 

381 inlet_pressure_from_upstream = ( 

382 _value_or_none(self.inlet_state[t0].pressure) 

383 if inlet_has_source 

384 else None 

385 ) 

386 # Pressure ranking: 

387 # 1. Explicit state_args 

388 # 2. A fixed inlet pressure on this unit 

389 # 3. A propagated upstream inlet pressure 

390 # 4. Nominal fallback 

391 p_inlet = _pick_seed( 

392 state_args.get("pressure"), 

393 inlet_pressure_from_fixed_inlet, 

394 inlet_pressure_from_upstream, 

395 10e5, 

396 ) 

397 

398 # Set the internal pressure first so the property block can report the corresponding saturation temperature for the condensate guess. 

399 p_internal = p_inlet - _pick_seed(user_pressure_loss, 0.0) 

400 self.int_outlet_state[t0].pressure.set_value(p_internal) 

401 T_sat_inlet = pyo.value(self.int_outlet_state[t0].temperature_sat) 

402 

403 inlet_temperature_from_known_enthalpy = ( 

404 _value_or_none(self.inlet_state[t0].temperature) 

405 if (self.inlet_state[t0].enth_mol.fixed or inlet_has_source) 

406 else None 

407 ) 

408 T_inlet = _pick_seed( 

409 inlet_temperature_from_known_enthalpy, 

410 T_sat_inlet + 10.0, 

411 ) 

412 inlet_enthalpy_from_fixed_inlet = ( 

413 _value_or_none(self.inlet_state[t0].enth_mol) 

414 if self.inlet_state[t0].enth_mol.fixed 

415 else None 

416 ) 

417 inlet_enthalpy_from_upstream = ( 

418 _value_or_none(self.inlet_state[t0].enth_mol) 

419 if inlet_has_source 

420 else None 

421 ) 

422 # Enthalpy ranking: 

423 # 1. Explicit state_args 

424 # 2. A fixed inlet enthalpy on this unit 

425 # 3. A propagated upstream inlet enthalpy 

426 # 4. Reconstruct from temperature and pressure 

427 h_inlet = _pick_seed( 

428 state_args.get("enth_mol"), 

429 inlet_enthalpy_from_fixed_inlet, 

430 inlet_enthalpy_from_upstream, 

431 _enthalpy_from_tp(T_inlet, p_inlet), 

432 ) 

433 

434 if self.deltaT_subcool[t0].fixed: 

435 T_int_outlet = T_sat_inlet - _pick_seed(_value_or_none(self.deltaT_subcool[t0]), 0.0) 

436 else: 

437 T_int_outlet = _pick_seed(_value_or_none(self.outlet_temperature[t0]), T_sat_inlet) 

438 h_int_outlet = _enthalpy_from_tp(T_int_outlet, p_internal) 

439 

440 # Outlet flow ranking: 

441 # 1. Fixed outlet flow on this unit 

442 # 2. Return split applied to inlet flow 

443 # 3. Nominal fallback 

444 f_return = _pick_seed( 

445 return_flow_fixed, 

446 None if return_rate is None else f_inlet * return_rate, 

447 0.7 * f_inlet, 

448 ) 

449 f_drain = _pick_seed( 

450 drain_flow_fixed, 

451 f_inlet - f_return, 

452 0.0, 

453 ) 

454 

455 return_pressure_from_fixed_outlet = ( 

456 _value_or_none(self.outlet_return_state[t0].pressure) 

457 if self.outlet_return_state[t0].pressure.fixed 

458 else None 

459 ) 

460 p_return = _pick_seed( 

461 return_pressure, 

462 return_pressure_from_fixed_outlet, 

463 2e5, 

464 ) 

465 return_enthalpy_from_fixed_outlet = ( 

466 _value_or_none(self.outlet_return_state[t0].enth_mol) 

467 if self.outlet_return_state[t0].enth_mol.fixed 

468 else None 

469 ) 

470 h_return = _pick_seed( 

471 return_enthalpy_from_fixed_outlet, 

472 _enthalpy_from_tp(return_temperature, p_return), 

473 ) 

474 drain_enthalpy_from_fixed_outlet = ( 

475 _value_or_none(self.outlet_drain_state[t0].enth_mol) 

476 if self.outlet_drain_state[t0].enth_mol.fixed 

477 else None 

478 ) 

479 h_drain = _pick_seed( 

480 drain_enthalpy_from_fixed_outlet, 

481 _enthalpy_from_tp(env_temperature, p_return), 

482 ) 

483 #----------------------------------------------------- 

484 # 2. Initialize state blocks using explicitly seeded values for all state variables 

485 # TODO: make this generic for all property packages 

486 self.inlet_state.initialize( 

487 solver=solver, 

488 optarg=optarg, 

489 outlvl=outlvl, 

490 state_args={"flow_mol": f_inlet, 

491 "pressure": p_inlet, 

492 "enth_mol": h_inlet}, 

493 

494 ) 

495 

496 init_log.info_high("Inlet state initialization complete") 

497 

498 flags_int = self.int_outlet_state.initialize( 

499 solver=solver, 

500 optarg=optarg, 

501 outlvl=outlvl, 

502 state_args={"flow_mol": f_inlet, 

503 "pressure": p_internal, 

504 "enth_mol": h_int_outlet #- pyo.value(self.user_heat_loss[t0]) / (f_inlet + 1e-6)}, 

505 }, 

506 hold_state=True, 

507 ) 

508 init_log.info_high("Internal outlet state initialization complete") 

509 

510 flags_return = self.outlet_return_state.initialize( 

511 solver=solver, 

512 optarg=optarg, 

513 outlvl=outlvl, 

514 state_args={"flow_mol": f_return, 

515 "pressure": p_return, 

516 "enth_mol": h_return}, 

517 hold_state=True, 

518 ) 

519 init_log.info_high("Return return state initialization complete") 

520 

521 flags_drain = self.outlet_drain_state.initialize( 

522 solver=solver, 

523 optarg=optarg, 

524 outlvl=outlvl, 

525 state_args={"flow_mol": f_drain, 

526 "pressure": p_return, 

527 "enth_mol": h_drain}, 

528 hold_state=True, 

529 ) 

530 init_log.info_high("Drain outlet state initialization complete") 

531 

532 # #----------------------------------------------------- 

533 # # 3. Deactivate constraints not specified in Step 2 then solve first pass 

534 relaxed_eqns = [ 

535 self.eq_overall_material_balance, 

536 self.eq_internal_material_balance, 

537 self.eq_condensate_return_balance, 

538 self.eq_return_energy_balance, 

539 self.eq_user_momentum_balance, 

540 self.eq_return_momentum_balance, 

541 self.eq_drain_momentum_balance, 

542 self.eq_return_temperature, 

543 self.eq_drain_temperature, 

544 self.eq_subcooling_temperature, 

545 self.eq_outlet_temperature 

546 ] 

547 

548 for con in relaxed_eqns: 

549 con.deactivate() 

550 

551 report_statistics(self) 

552 

553 dt = DiagnosticsToolbox(self) 

554 dt.report_structural_issues() 

555 dt.display_underconstrained_set() 

556 

557 

558 # Solve 

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

560 res = opt.solve(self, tee=slc.tee) 

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

562 dt.report_numerical_issues() 

563 raise InitializationError(f"{self.name} failed relaxed initialization") 

564 

565 

566 # Restore full model 

567 self.int_outlet_state.release_state(flags_int) 

568 self.outlet_return_state.release_state(flags_return) 

569 self.outlet_drain_state.release_state(flags_drain) 

570 

571 for con in relaxed_eqns: 

572 con.activate() 

573 

574 dof = degrees_of_freedom(self) 

575 if dof != 0: 575 ↛ 576line 575 didn't jump to line 576 because the condition on line 575 was never true

576 raise InitializationError( 

577 f"{self.name} degrees of freedom were not 0 before final solve. DoF = {dof}" 

578 ) 

579 

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

581 res = opt.solve(self, tee=slc.tee) 

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

583 raise InitializationError(f"{self.name} failed final initialization") 

584 

585 init_log.info(f"Initialization complete: {idaeslog.condition(res)}") 

586 ''' ''' 

587 '''  

588 init_log.info_high(f"Degrees of Freedom before solve: {degrees_of_freedom(self)}") 

589  

590 #self.model().pprint() 

591  

592 report_statistics(self) 

593 

594 dt = DiagnosticsToolbox(self) 

595 dt.report_structural_issues() 

596 dt.display_underconstrained_set() 

597  

598 # First solve 

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

600 res = opt.solve(self, tee=slc.tee) 

601 init_log.info_high("Relaxed initialization pass: {}.".format(idaeslog.condition(res))) 

602 

603 if not check_optimal_termination(res): 

604 raise InitializationError(f"Unit model {self.name} failed relaxed initialization") 

605 ''' 

606 

607 

608 

609 def _initialize_prop(self, tar_sb_name, prop_name, t = None): 

610 if hasattr(self.inlet_state, "is_indexed") and t is not None: 

611 inlet_state = self.inlet_state[t] 

612 return_pressure = self.return_pressure[t] 

613 env_temperature = self.env_temperature[t] 

614 else: 

615 inlet_state = self.inlet_state 

616 return_pressure = self.return_pressure 

617 env_temperature = self.env_temperature 

618 

619 if prop_name == 'enth_mol': 

620 if tar_sb_name == "int_outlet": 

621 prop_val = inlet_state.enth_mol 

622 elif tar_sb_name == "outlet_return": 

623 prop_val = inlet_state.enth_mol * self.return_rate[t] 

624 elif tar_sb_name == "outlet_drain": 

625 prop_val = inlet_state.enth_mol * (1 - self.return_rate[t]) 

626 else: 

627 raise Exception( 

628 f"{self.name}: Initialization method of {prop_name} for {tar_sb_name} not found." 

629 ) 

630 

631 elif prop_name == 'pressure': 

632 if tar_sb_name == "int_outlet": 

633 prop_val = inlet_state.pressure 

634 elif tar_sb_name in ["outlet_return", "outlet_drain"]: 

635 prop_val = return_pressure 

636 else: 

637 raise Exception( 

638 f"{self.name}: Initialization method of {prop_name} for {tar_sb_name} not found." 

639 ) 

640 

641 elif prop_name == 'temperature': 

642 if tar_sb_name in ["int_outlet", "outlet_return", "outlet_drain"]: 

643 prop_val = env_temperature 

644 else: 

645 raise Exception( 

646 f"{self.name}: Initialization method of {prop_name} for {tar_sb_name} not found." 

647 ) 

648 

649 elif prop_name == "vapor_frac": 

650 if tar_sb_name in ["int_outlet", "outlet_return", "outlet_drain"]: 

651 prop_val = 0 * pyunits.dimensionless 

652 else: 

653 raise Exception( 

654 f"{self.name}: Initialization method of {prop_name} for {tar_sb_name} not found." 

655 ) 

656 else: 

657 raise Exception( 

658 f"{self.name}: Initialization method of {prop_name} for {tar_sb_name} not found." 

659 ) 

660 

661 return prop_val 

662 

663 

664 def _get_performance_contents(self, time_point=0): 

665 """Collect performance variables for reporting.""" 

666 var_dict = { 

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

668 "User heat loss [W]": self.user_heat_loss[time_point], 

669 "Return heat loss [W]": self.return_heat_loss[time_point], 

670 "Pressure loss [Pa]": self.user_pressure_loss[time_point], 

671 "Condensate return rate [-]": self.return_rate[time_point], 

672 "Condensate return temperature [degC]": pyunits.convert_temp_K_to_C( 

673 self.return_temperature[time_point] 

674 ), 

675 "Return pressure [Pa]": self.return_pressure[time_point], 

676 } 

677 

678 

679 var_dict["Degree of subcooling target [K]"] = self.deltaT_subcool[time_point] 

680 var_dict["Outlet temperature target [degC]"] = pyunits.convert_temp_K_to_C( 

681 self.outlet_temperature[time_point] 

682 ) 

683 

684 expr_dict = { 

685 "Energy lost to return and drain [W]": self.energy_lost[time_point], 

686 } 

687 return {"vars": var_dict, "exprs": expr_dict} 

688 

689 

690 # ----------------------------------------------------------------- 

691 # Common utilities 

692 # ----------------------------------------------------------------- 

693 

694 def calculate_scaling_factors(self): 

695 super().calculate_scaling_factors() 

696 

697 

698 def _build_state_blocks( 

699 self, 

700 stream_name_list: Iterable[str], 

701 has_phase_equilibrium: bool, 

702 is_defined_state: Optional[bool] = False, 

703 is_build_port: Optional[bool] = False, 

704 ) -> List[StateBlock]: 

705 blocks: List[StateBlock] = [] 

706 

707 base_args = dict(self.config.property_package_args) 

708 base_args["has_phase_equilibrium"] = has_phase_equilibrium 

709 base_args["defined_state"] = is_defined_state 

710 

711 for stream_name in stream_name_list: 

712 args = dict(base_args) 

713 args["doc"] = f"Thermophysical properties at {stream_name}" 

714 sb = self.config.property_package.build_state_block(self.flowsheet().time, **args) 

715 setattr(self, f"{stream_name}_state", sb) 

716 blocks.append(sb) 

717 

718 if is_build_port: # No port is needed for intermediate/internal state blocks 

719 self.add_port(name=stream_name, block=sb) 

720 

721 return blocks 

722 

723 

724 def _get_stream_table_contents(self, time_point=0): 

725 io_dict = {name: getattr(self, name) for name in [*self.inlet_blocks, *self.outlet_blocks, *self.internal_blocks]} 

726 return create_stream_table_dataframe(io_dict, time_point=time_point)