Coverage for backend/ahuora-builder/src/ahuora_builder/custom/thermal_utility_systems/simple_boiler.py: 91%

208 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-06-23 21:51 +0000

1"""Simple boiler unit model.""" 

2 

3from typing import Iterable, List, Optional 

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

5from pyomo.environ import ( 

6 Constraint, 

7 Param, 

8 Suffix, 

9 Var, 

10 check_optimal_termination, 

11 value, 

12 units as pyunits, 

13) 

14 

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

16from idaes.core.scaling import CustomScalerBase 

17from idaes.core.solvers import get_solver 

18from idaes.core.util.config import is_physical_parameter_block 

19from idaes.core.util.exceptions import InitializationError 

20from idaes.core.util.model_diagnostics import DiagnosticsToolbox 

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

22from idaes.core.util.tables import create_stream_table_dataframe 

23 

24import idaes.logger as idaeslog 

25 

26_log = idaeslog.getLogger(__name__) 

27 

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

29__Note__ = "When there is insufficient energy to raise feedwater to saturation, the model will fail in initialisation, if not enough energy is available to fully vaporise steam, saturated water will exit in the steam port." 

30 

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

32 """Declare config entries for SimpleBoiler.""" 

33 config.declare( 

34 "dynamic", 

35 ConfigValue( 

36 domain=In([False]), 

37 default=False, 

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

39 ), 

40 ) 

41 config.declare( 

42 "has_holdup", 

43 ConfigValue( 

44 default=False, 

45 domain=In([False]), 

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

47 ), 

48 ) 

49 config.declare( 

50 "property_package", 

51 ConfigValue( 

52 default=useDefault, 

53 domain=is_physical_parameter_block, 

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

55 ), 

56 ) 

57 config.declare( 

58 "property_package_args", 

59 ConfigBlock( 

60 implicit=True, 

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

62 ), 

63 ) 

64 config.declare( 

65 "has_phase_equilibrium", 

66 ConfigValue( 

67 default=False, 

68 domain=Bool, 

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

70 ), 

71 ) 

72 

73 

74class SimpleBoilerScaler(CustomScalerBase): 

75 """Default scaler placeholder for the simple boiler model.""" 

76 

77 DEFAULT_SCALING_FACTORS = { 

78 "fuel_mass_flow": 1, 

79 "fuel_calorific_value": 1e-6, 

80 "heat_added": 1e-6, 

81 } 

82 

83 

84@declare_process_block_class("SimpleBoiler") 

85class SimpleBoilerData(UnitModelBlockData): 

86 """Simple boiler energy-side unit operation.""" 

87 

88 default_scaler = SimpleBoilerScaler 

89 

90 CONFIG = ConfigBlock() 

91 _build_config(CONFIG) 

92 

93 def build(self): 

94 """Build the unit model structure and equations.""" 

95 super().build() 

96 

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

98 

99 """ 

100 1. Build inlet and outlet state blocks and associate  

101 with ports (where applicable) 

102 """ 

103 self.inlet_blocks = self._build_state_blocks( 

104 stream_name_list=["inlet"], 

105 has_phase_equilibrium=self.config.has_phase_equilibrium, 

106 is_defined_state=True, 

107 is_build_port=True, 

108 ) 

109 self.outlet_blocks = self._build_state_blocks( 

110 stream_name_list=["steam", "blowdown"], 

111 has_phase_equilibrium=self.config.has_phase_equilibrium, 

112 is_defined_state=False, 

113 is_build_port=True, 

114 ) 

115 for sb in self.inlet_blocks + self.outlet_blocks: 

116 for t in sb: 

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

118 

119 """ 

120 2. Create parameters, variables, references and expressions 

121 """ 

122 self.blowdown_fraction = Var( 

123 self.flowsheet().time, 

124 initialize=0.05, 

125 bounds=(0, None), 

126 units=pyunits.dimensionless, 

127 doc="Blowdown fraction relative to steam flow.", 

128 ) 

129 

130 self.fuel_mass_flow = Var( 

131 self.flowsheet().time, 

132 initialize=0.01, 

133 bounds=(0, None), 

134 units=pyunits.kg / pyunits.s, 

135 doc="Fuel mass flow to the boiler.", 

136 ) 

137 self.fuel_calorific_value = Var( 

138 self.flowsheet().time, 

139 initialize=40e6, 

140 bounds=(0, None), 

141 units=pyunits.J / pyunits.kg, 

142 doc="Fuel calorific value.", 

143 ) 

144 self.boiler_efficiency = Var( 

145 self.flowsheet().time, 

146 initialize=0.85, 

147 bounds=(0, 1), 

148 units=pyunits.dimensionless, 

149 doc="Fraction of fuel energy transferred to the water/steam side.", 

150 ) 

151 

152 self.heat_added = Var( 

153 self.flowsheet().time, 

154 initialize=3.4e5, 

155 bounds=(0, None), 

156 units=pyunits.W, 

157 doc="Useful heat added to the boiler water/steam side.", 

158 ) 

159 

160 self.feedwater_tds = Param( 

161 self.flowsheet().time, 

162 initialize=250, 

163 mutable=True, 

164 units=pyunits.ppm, 

165 doc="Total dissolved solids in the boiler feedwater.", 

166 ) 

167 self.max_boiler_tds = Param( 

168 self.flowsheet().time, 

169 initialize=3500, 

170 mutable=True, 

171 units=pyunits.ppm, 

172 doc="Maximum allowable total dissolved solids in the boiler water.", 

173 ) 

174 

175 """ 

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

177 unit operation performance and other constraint  

178 """ 

179 # Material balance: inlet splits into steam plus blowdown. 

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

181 def eq_overall_material_balance(b, t): 

182 return ( 

183 b.inlet_state[t].flow_mol 

184 == b.steam_state[t].flow_mol + b.blowdown_state[t].flow_mol 

185 ) 

186 

187 @self.Constraint(self.flowsheet().time, doc="Blowdown flow relation") 

188 def eq_blowdown_flow(b, t): 

189 return ( 

190 b.blowdown_state[t].flow_mol 

191 == b.blowdown_fraction[t] * b.steam_state[t].flow_mol 

192 ) 

193 

194 @self.Constraint(self.flowsheet().time, doc="Blowdown fraction from TDS") 

195 def eq_blowdown_fraction_tds(b, t): 

196 feedwater_tds = pyunits.convert( 

197 b.feedwater_tds[t], 

198 to_units=pyunits.dimensionless, 

199 ) 

200 max_boiler_tds = pyunits.convert( 

201 b.max_boiler_tds[t], 

202 to_units=pyunits.dimensionless, 

203 ) 

204 return ( 

205 b.blowdown_fraction[t] 

206 == feedwater_tds / (max_boiler_tds - feedwater_tds) 

207 ) 

208 

209 # Energy balance: fuel heat raises the total boiler-side enthalpy. 

210 @self.Constraint(self.flowsheet().time, doc="Overall energy balance") 

211 def eq_overall_energy_balance(b, t): 

212 return ( 

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

214 + b.heat_added[t] 

215 == b.steam_state[t].flow_mol * b.steam_state[t].enth_mol 

216 + b.blowdown_state[t].flow_mol * b.blowdown_state[t].enth_mol 

217 ) 

218 

219 # Momentum: both outlet streams leave at inlet pressure in this simple model. 

220 @self.Constraint(self.flowsheet().time, doc="Steam pressure equality") 

221 def eq_steam_pressure(b, t): 

222 return b.steam_state[t].pressure == b.inlet_state[t].pressure 

223 

224 @self.Constraint(self.flowsheet().time, doc="Blowdown pressure equality") 

225 def eq_blowdown_pressure(b, t): 

226 return b.blowdown_state[t].pressure == b.inlet_state[t].pressure 

227 

228 @self.Constraint(self.flowsheet().time, doc="Blowdown saturated liquid relation") 

229 def eq_blowdown_saturated_liquid(b, t): 

230 return b.blowdown_state[t].vapor_frac == 0 

231 

232 @self.Constraint(self.flowsheet().time, doc="Steam outlet must be at least saturated vapour") 

233 def eq_steam_outlet_minimum_vapour_enthalpy(b, t): 

234 return b.steam_state[t].enth_mol >= b.steam_state[t].enth_mol_sat_phase["Vap"] 

235 

236 # Performance relation tying the useful heat duty back to fuel input. 

237 @self.Constraint(self.flowsheet().time, doc="Heat-added definition from fuel input") 

238 def eq_heat_added_definition(b, t): 

239 return b.heat_added[t] == ( 

240 b.fuel_mass_flow[t] 

241 * b.fuel_calorific_value[t] 

242 * b.boiler_efficiency[t] 

243 ) 

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

245 

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

247 """ 

248 General wrapper for template initialization routines. 

249 

250 Keyword Arguments: 

251 state_args : a dict of arguments to be passed to the property package(s) 

252 outlvl : sets output level of initialization routine 

253 optarg : solver options dictionary object 

254 solver : str indicating which solver to use 

255 

256 Returns: 

257 None 

258 """ 

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

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

261 t0 = self.flowsheet().config.time.first() 

262 

263 opt = get_solver(solver, optarg) 

264 

265 pp = self.inlet_state[t0].params 

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

267 

268 shared_state_args = { 

269 key: state_args[key] 

270 for key in ("flow_mol", "pressure", "enth_mol", "temperature") 

271 if key in state_args 

272 } 

273 state_args_inlet = dict(shared_state_args) 

274 state_args_inlet.update(state_args.get("inlet", {})) 

275 state_args_steam = dict(state_args.get("steam", {})) 

276 state_args_blowdown = dict(state_args.get("blowdown", {})) 

277 

278 # Step 1: prepare a few local helpers and normalise any caller-supplied state args. 

279 # Keeping these utilities close to the initializer makes the seeding rules below 

280 # easier to follow without having to jump around the file. 

281 def _value_or_none(obj): 

282 return value(obj, exception=False) 

283 

284 def _pick_seed(*candidates): 

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

286 if candidate is not None: 

287 return candidate 

288 return None 

289 

290 def _enthalpy_from_tp(temperature, pressure): 

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

292 return None 

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

294 

295 def _safe_positive(value_, fallback): 

296 if value_ is None or value_ <= 1e-9: 

297 return fallback 

298 return value_ 

299 

300 def _saturated_phase_enthalpy(state, phase): 

301 try: 

302 return _value_or_none(state.enth_mol_sat_phase[phase]) 

303 except (AttributeError, KeyError): 

304 return None 

305 

306 # Step 2: build starting guesses for the boiler state. 

307 # The goal here is not to solve the model yet, only to give the property blocks 

308 # physically reasonable values before the first relaxed solve. 

309 inlet_has_source = len(list(self.inlet.sources())) > 0 

310 

311 # Start with the operating knobs that live on the unit itself. 

312 feedwater_tds = value( 

313 pyunits.convert( 

314 self.feedwater_tds[t0], 

315 to_units=pyunits.dimensionless, 

316 ) 

317 ) 

318 max_boiler_tds = value( 

319 pyunits.convert( 

320 self.max_boiler_tds[t0], 

321 to_units=pyunits.dimensionless, 

322 ) 

323 ) 

324 blowdown_fraction_from_tds = ( 

325 feedwater_tds / (max_boiler_tds - feedwater_tds) 

326 if max_boiler_tds > feedwater_tds 

327 else None 

328 ) 

329 blowdown_fraction = _pick_seed( 

330 _value_or_none(self.blowdown_fraction[t0]) 

331 if self.blowdown_fraction[t0].fixed 

332 else None, 

333 blowdown_fraction_from_tds, 

334 0.05, 

335 ) 

336 self.blowdown_fraction[t0].set_value(blowdown_fraction) 

337 fuel_mass_flow = _pick_seed(_value_or_none(self.fuel_mass_flow[t0]), 0.01) 

338 fuel_calorific_value = _pick_seed( 

339 _value_or_none(self.fuel_calorific_value[t0]), 

340 40e6, 

341 ) 

342 boiler_efficiency = _pick_seed(_value_or_none(self.boiler_efficiency[t0]), 0.85) 

343 heat_added = fuel_mass_flow * fuel_calorific_value * boiler_efficiency 

344 

345 # Flow seeding: 

346 # 1. Explicit state_args 

347 # 2. Fixed values already present on the unit 

348 # 3. Values propagated from connected upstream units 

349 # 4. A simple internal estimate based on the blowdown-to-steam ratio 

350 # 5. A conservative fallback if nothing else is available 

351 inlet_flow_from_fixed = ( 

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

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

354 else None 

355 ) 

356 inlet_flow_from_upstream = ( 

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

358 if inlet_has_source 

359 else None 

360 ) 

361 steam_flow_from_fixed = ( 

362 _value_or_none(self.steam_state[t0].flow_mol) 

363 if self.steam_state[t0].flow_mol.fixed 

364 else None 

365 ) 

366 blowdown_flow_from_fixed = ( 

367 _value_or_none(self.blowdown_state[t0].flow_mol) 

368 if self.blowdown_state[t0].flow_mol.fixed 

369 else None 

370 ) 

371 inlet_flow_from_steam = None 

372 if steam_flow_from_fixed is not None: 

373 inlet_flow_from_steam = steam_flow_from_fixed * (1 + blowdown_fraction) 

374 inlet_flow_from_blowdown = None 

375 if blowdown_flow_from_fixed is not None and abs(blowdown_fraction) >= 1e-8: 375 ↛ 376line 375 didn't jump to line 376 because the condition on line 375 was never true

376 inlet_flow_from_blowdown = ( 

377 blowdown_flow_from_fixed * (1 + blowdown_fraction) / blowdown_fraction 

378 ) 

379 

380 f_inlet = _pick_seed( 

381 state_args_inlet.get("flow_mol"), 

382 inlet_flow_from_fixed, 

383 inlet_flow_from_upstream, 

384 inlet_flow_from_steam, 

385 inlet_flow_from_blowdown, 

386 500.0, 

387 ) 

388 

389 # The outlet flows are split from the inlet flow using B = ratio * steam. 

390 f_blowdown = _pick_seed( 

391 state_args_blowdown.get("flow_mol"), 

392 blowdown_flow_from_fixed, 

393 f_inlet * blowdown_fraction / (1 + blowdown_fraction), 

394 ) 

395 # Steam is the remaining flow after removing blowdown. 

396 f_steam = _pick_seed( 

397 state_args_steam.get("flow_mol"), 

398 steam_flow_from_fixed, 

399 f_inlet - f_blowdown, 

400 0.95 * f_inlet, 

401 ) 

402 

403 # Pressure seeding: 

404 # This simple boiler keeps all three streams at the same pressure. 

405 inlet_pressure_from_fixed = ( 

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

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

408 else None 

409 ) 

410 inlet_pressure_from_upstream = ( 

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

412 if inlet_has_source 

413 else None 

414 ) 

415 steam_pressure_from_fixed = ( 

416 _value_or_none(self.steam_state[t0].pressure) 

417 if self.steam_state[t0].pressure.fixed 

418 else None 

419 ) 

420 blowdown_pressure_from_fixed = ( 

421 _value_or_none(self.blowdown_state[t0].pressure) 

422 if self.blowdown_state[t0].pressure.fixed 

423 else None 

424 ) 

425 p_inlet = _pick_seed( 

426 state_args_inlet.get("pressure"), 

427 inlet_pressure_from_fixed, 

428 inlet_pressure_from_upstream, 

429 steam_pressure_from_fixed, 

430 blowdown_pressure_from_fixed, 

431 10e5, 

432 ) 

433 p_steam = _pick_seed(state_args_steam.get("pressure"), steam_pressure_from_fixed, p_inlet) 

434 p_blowdown = _pick_seed( 

435 state_args_blowdown.get("pressure"), 

436 blowdown_pressure_from_fixed, 

437 p_inlet, 

438 ) 

439 

440 # Enthalpy seeding: 

441 # Inlet enthalpy comes from explicit or propagated state data first, then from 

442 # a T/P estimate. Blowdown is biased toward a saturated-liquid guess, and steam 

443 # is back-calculated from the boiler-side energy balance. 

444 inlet_enthalpy_from_fixed = ( 

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

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

447 else None 

448 ) 

449 inlet_enthalpy_from_upstream = ( 

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

451 if inlet_has_source 

452 else None 

453 ) 

454 inlet_temperature_from_state = ( 

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

456 if inlet_has_source or self.inlet_state[t0].enth_mol.fixed 

457 else None 

458 ) 

459 h_inlet = _pick_seed( 

460 state_args_inlet.get("enth_mol"), 

461 inlet_enthalpy_from_fixed, 

462 inlet_enthalpy_from_upstream, 

463 _enthalpy_from_tp(inlet_temperature_from_state, p_inlet), 

464 _enthalpy_from_tp(298.15, p_inlet), 

465 ) 

466 

467 # Blowdown is treated as saturated liquid at the outlet pressure. 

468 self.blowdown_state[t0].pressure.set_value(p_blowdown) 

469 blowdown_sat_liq_enth = _saturated_phase_enthalpy( 

470 self.blowdown_state[t0], 

471 "Liq", 

472 ) 

473 blowdown_sat_temp = _value_or_none( 

474 getattr(self.blowdown_state[t0], "temperature_sat", None) 

475 ) 

476 h_blowdown = _pick_seed( 

477 state_args_blowdown.get("enth_mol"), 

478 ( 

479 _value_or_none(self.blowdown_state[t0].enth_mol) 

480 if self.blowdown_state[t0].enth_mol.fixed 

481 else None 

482 ), 

483 blowdown_sat_liq_enth, 

484 _enthalpy_from_tp(blowdown_sat_temp - 1e-4, p_blowdown) 

485 if blowdown_sat_temp is not None 

486 else None, 

487 h_inlet, 

488 ) 

489 

490 # Steam is the energy-balance remainder after the blowdown enthalpy is fixed. 

491 steam_enthalpy_from_fixed = ( 

492 _value_or_none(self.steam_state[t0].enth_mol) 

493 if self.steam_state[t0].enth_mol.fixed 

494 else None 

495 ) 

496 f_steam_safe = _safe_positive(f_steam, 1.0) 

497 h_steam_from_balance = ( 

498 (f_inlet * h_inlet + heat_added - f_blowdown * h_blowdown) 

499 / f_steam_safe 

500 ) 

501 self.steam_state[t0].pressure.set_value(p_steam) 

502 steam_sat_temp = _value_or_none(getattr(self.steam_state[t0], "temperature_sat", None)) 

503 h_steam = _pick_seed( 

504 state_args_steam.get("enth_mol"), 

505 steam_enthalpy_from_fixed, 

506 h_steam_from_balance, 

507 _enthalpy_from_tp(steam_sat_temp + 10.0, p_steam) 

508 if steam_sat_temp is not None 

509 else None, 

510 ) 

511 

512 # Step 3: initialize the property blocks with the guesses above. 

513 # The inlet is released immediately, while the outlet states are held so the 

514 # relaxed unit solve can reconcile the model equations against the seeds. 

515 self.inlet_state.initialize( 

516 solver=solver, 

517 optarg=optarg, 

518 outlvl=outlvl, 

519 state_args={ 

520 "flow_mol": f_inlet, 

521 "pressure": p_inlet, 

522 "enth_mol": h_inlet, 

523 }, 

524 ) 

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

526 

527 flags_steam = self.steam_state.initialize( 

528 solver=solver, 

529 optarg=optarg, 

530 outlvl=outlvl, 

531 state_args={ 

532 "flow_mol": f_steam, 

533 "pressure": p_steam, 

534 "enth_mol": h_steam, 

535 }, 

536 hold_state=True, 

537 ) 

538 init_log.info_high("Steam state initialization complete") 

539 

540 flags_blowdown = self.blowdown_state.initialize( 

541 solver=solver, 

542 optarg=optarg, 

543 outlvl=outlvl, 

544 state_args={ 

545 "flow_mol": f_blowdown, 

546 "pressure": p_blowdown, 

547 "enth_mol": h_blowdown, 

548 }, 

549 hold_state=True, 

550 ) 

551 init_log.info_high("Blowdown state initialization complete") 

552 

553 # Step 4: temporarily relax the unit equations so the first solve is easy. 

554 # This mirrors the other thermal utility units: seed the states first, then let 

555 # the solver clean up the structure before we restore the full model. 

556 relaxed_eqns = [ 

557 self.eq_overall_material_balance, 

558 self.eq_blowdown_flow, 

559 self.eq_blowdown_fraction_tds, 

560 self.eq_overall_energy_balance, 

561 self.eq_steam_pressure, 

562 self.eq_blowdown_pressure, 

563 self.eq_blowdown_saturated_liquid, 

564 self.eq_steam_outlet_minimum_vapour_enthalpy, 

565 ] 

566 

567 for con in relaxed_eqns: 

568 con.deactivate() 

569 

570 report_statistics(self) 

571 

572 dt = DiagnosticsToolbox(self) 

573 dt.report_structural_issues() 

574 dt.display_underconstrained_set() 

575 

576 active_constraints = sum( 

577 1 for _ in self.component_data_objects(Constraint, active=True, descend_into=True) 

578 ) 

579 

580 if active_constraints > 0: 580 ↛ 587line 580 didn't jump to line 587 because the condition on line 580 was always true

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

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

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

584 dt.report_numerical_issues() 

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

586 else: 

587 init_log.info_high( 

588 "Relaxed initialization pass skipped: no active constraints remained after seeding." 

589 ) 

590 

591 # Step 5: restore the full model and solve the complete unit. 

592 # At this point the state blocks have been seeded and the unit should be square. 

593 self.steam_state.release_state(flags_steam) 

594 self.blowdown_state.release_state(flags_blowdown) 

595 

596 for con in relaxed_eqns: 

597 con.activate() 

598 

599 dof = degrees_of_freedom(self) 

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

601 raise InitializationError( 

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

603 ) 

604 

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

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

607 if not check_optimal_termination(res): 

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

609 

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

611 

612 def _get_performance_contents(self, time_point=0): 

613 """Collect performance variables for reporting.""" 

614 var_dict = { 

615 "Blowdown fraction": self.blowdown_fraction[time_point], 

616 "Fuel mass flow": self.fuel_mass_flow[time_point], 

617 "Fuel calorific value": self.fuel_calorific_value[time_point], 

618 "Boiler efficiency": self.boiler_efficiency[time_point], 

619 } 

620 expr_dict = { 

621 "Feedwater TDS": self.feedwater_tds[time_point], 

622 "Maximum boiler TDS": self.max_boiler_tds[time_point], 

623 "Heat added": self.heat_added[time_point], 

624 } 

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

626 

627 def calculate_scaling_factors(self): 

628 super().calculate_scaling_factors() 

629 

630 def _build_state_blocks( 

631 self, 

632 stream_name_list: Iterable[str], 

633 has_phase_equilibrium: bool, 

634 is_defined_state: Optional[bool] = False, 

635 is_build_port: Optional[bool] = False, 

636 ) -> List[StateBlock]: 

637 blocks: List[StateBlock] = [] 

638 

639 base_args = dict(self.config.property_package_args) 

640 base_args["has_phase_equilibrium"] = has_phase_equilibrium 

641 base_args["defined_state"] = is_defined_state 

642 

643 for stream_name in stream_name_list: 

644 args = dict(base_args) 

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

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

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

648 blocks.append(sb) 

649 

650 if is_build_port: 650 ↛ 643line 650 didn't jump to line 643 because the condition on line 650 was always true

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

652 

653 return blocks 

654 

655 def _get_stream_table_contents(self, time_point=0): 

656 io_dict = { 

657 name: getattr(self, name) 

658 for name in ["inlet", "steam", "blowdown"] 

659 } 

660 return create_stream_table_dataframe(io_dict, time_point=time_point)