Coverage for backend/ahuora-builder/src/ahuora_builder/custom/salt/crystallizer.py: 65%

203 statements  

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

1"""Generic indirect-heated evaporator / crystallizer unit model for IDAES. 

2 

3This unit is intended for a *process-side* property package with liquid, 

4vapor, and solid phases named ``Liq``, ``Vap``, and ``Sol``. It assumes the 

5process state block exposes: 

6 

7* ``flow_mass_phase_comp``, 

8* ``temperature``, 

9* ``pressure``, and 

10* ``get_enthalpy_flow_terms(phase)``. 

11 

12The unit also contains a *utility side* with an inlet and outlet state pair. 

13This is aimed at steam / condensate service and works naturally with 

14IDAES Helmholtz/IAPWS pressure-enthalpy states on ``flow_mol``, ``pressure``, 

15and ``enth_mol`` state variables. 

16 

17Model structure 

18--------------- 

191. One brine/feed inlet. 

202. One concentrate outlet that contains the liquid + solid phases. 

213. One vapor outlet that contains the boiled-off vapor phase. 

224. Phase equilibrium is enforced directly on both process-side outlet states by 

23 the process property package, while unit-level balances couple the outlets. 

245. One utility inlet and one utility outlet linked by enthalpy drop. 

25 

26The model does *not* include an explicit UA/LMTD heat-transfer equation. 

27Instead, it represents an ideal indirect heater where the utility-side 

28enthalpy decrease exactly matches the process-side heat duty. To close the 

29model, the user must provide one thermal specification, for example by fixing 

30``heat_duty[t]``, fixing a utility-outlet state variable (such as outlet 

31enthalpy or vapor fraction), or adding an external UA/LMTD-style relation in 

32an enclosing flowsheet. 

33""" 

34 

35from copy import deepcopy 

36 

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

38from pyomo.environ import Constraint, Expression, Param, Set, Var, check_optimal_termination 

39from pyomo.environ import units as pyunits 

40import pyomo.environ as pyo 

41 

42from idaes.core import UnitModelBlockData, declare_process_block_class, useDefault 

43from idaes.core.util.config import is_physical_parameter_block 

44from idaes.core.util.initialization import fix_state_vars, revert_state_vars 

45from idaes.core.util.model_statistics import degrees_of_freedom 

46from idaes.core.util.tables import create_stream_table_dataframe 

47from idaes.core.util.exceptions import ConfigurationError, InitializationError 

48import idaes.core.util.scaling as iscale 

49import idaes.logger as idaeslog 

50from ahuora_builder.state_args import extract_state_args 

51 

52try: # WaterTAP installs a compatible convenience wrapper. 

53 from watertap.core.solvers import get_solver 

54except ImportError: # pragma: no cover - fallback when WaterTAP is absent. 

55 from idaes.core.solvers import get_solver 

56 

57 

58_log = idaeslog.getLogger(__name__) 

59 

60 

61@declare_process_block_class("Crystallizer") 

62class CrystallizerData(UnitModelBlockData): 

63 """Indirect-heated evaporator / crystallizer with explicit utility side.""" 

64 

65 LIQUID_PHASE = "Liq" 

66 VAPOR_PHASE = "Vap" 

67 SOLID_PHASE = "Sol" 

68 

69 CONFIG = UnitModelBlockData.CONFIG() 

70 

71 CONFIG.declare( 

72 "process_property_package", 

73 ConfigValue( 

74 default=useDefault, 

75 domain=is_physical_parameter_block, 

76 description="Process-side property package", 

77 ), 

78 ) 

79 CONFIG.declare( 

80 "process_property_package_args", 

81 ConfigBlock( 

82 implicit=True, 

83 description="Arguments used when constructing process-side state blocks", 

84 ), 

85 ) 

86 

87 CONFIG.declare( 

88 "utility_property_package", 

89 ConfigValue( 

90 default=useDefault, 

91 domain=is_physical_parameter_block, 

92 description="Utility-side property package", 

93 ), 

94 ) 

95 CONFIG.declare( 

96 "utility_property_package_args", 

97 ConfigBlock( 

98 implicit=True, 

99 description="Arguments used when constructing utility-side state blocks", 

100 ), 

101 ) 

102 

103 CONFIG.declare( 

104 "absent_phase_flow", 

105 ConfigValue( 

106 default=1e-6, 

107 domain=float, 

108 description="Small numeric flow assigned to excluded outlet phases", 

109 ), 

110 ) 

111 CONFIG.declare( 

112 "has_pressure_change", 

113 ConfigValue( 

114 default=False, 

115 domain=Bool, 

116 description="Whether to include explicit process- and utility-side dP vars", 

117 ), 

118 ) 

119 

120 def build(self): 

121 super().build() 

122 

123 process_pp = self.config.process_property_package 

124 utility_pp = self.config.utility_property_package 

125 

126 if process_pp is useDefault: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true

127 raise ConfigurationError( 

128 f"{self.name}: process_property_package must be provided explicitly." 

129 ) 

130 if utility_pp is useDefault: 130 ↛ 131line 130 didn't jump to line 131 because the condition on line 130 was never true

131 raise ConfigurationError( 

132 f"{self.name}: utility_property_package must be provided explicitly." 

133 ) 

134 

135 missing_phases = [ 

136 p 

137 for p in ( 

138 self.LIQUID_PHASE, 

139 self.VAPOR_PHASE, 

140 self.SOLID_PHASE, 

141 ) 

142 if p not in process_pp.phase_list 

143 ] 

144 if missing_phases: 144 ↛ 145line 144 didn't jump to line 145 because the condition on line 144 was never true

145 raise ConfigurationError( 

146 f"{self.name}: process property package is missing expected phases: " 

147 f"{missing_phases}. Available phases: {list(process_pp.phase_list)}" 

148 ) 

149 

150 time = self.flowsheet().time 

151 

152 self.absent_phase_flow = Param( 

153 initialize=self.config.absent_phase_flow, 

154 mutable=True, 

155 units=pyunits.dimensionless, 

156 doc="Small numeric flow used for outlet phases that are excluded by the separator", 

157 ) 

158 

159 # ------------------------------------------------------------------ 

160 # Process-side state blocks 

161 proc_in_args = dict(**self.config.process_property_package_args) 

162 proc_in_args.setdefault("defined_state", True) 

163 proc_in_args.setdefault("has_phase_equilibrium", False) 

164 self.process_in = process_pp.build_state_block(time, **proc_in_args) 

165 

166 proc_sep_args = dict(**self.config.process_property_package_args) 

167 proc_sep_args.setdefault("defined_state", False) 

168 proc_sep_args.setdefault("has_phase_equilibrium", True) 

169 self.concentrate_state = process_pp.build_state_block(time, **proc_sep_args) 

170 self.vapor_state = process_pp.build_state_block(time, **proc_sep_args) 

171 

172 # ------------------------------------------------------------------ 

173 # Utility-side state blocks 

174 util_in_args = dict(**self.config.utility_property_package_args) 

175 util_in_args.setdefault("defined_state", True) 

176 self.utility_in = utility_pp.build_state_block(time, **util_in_args) 

177 

178 util_out_args = dict(**self.config.utility_property_package_args) 

179 util_out_args.setdefault("defined_state", False) 

180 self.utility_out = utility_pp.build_state_block(time, **util_out_args) 

181 

182 # ------------------------------------------------------------------ 

183 # Convenience references to variable units / phase-component indexing. 

184 t0 = time.first() 

185 self._phase_component_set = Set( 

186 initialize=list(self.concentrate_state[t0].flow_mass_phase_comp.keys()), 

187 dimen=2, 

188 ordered=True, 

189 doc="Phase-component pairs used in the process-side property package", 

190 ) 

191 

192 first_pc = next(iter(self._phase_component_set)) 

193 flow_ref = self.concentrate_state[t0].flow_mass_phase_comp[first_pc] 

194 flow_units = pyunits.get_units(flow_ref) 

195 self._process_flow_units = pyunits.dimensionless if flow_units is None else flow_units 

196 

197 # ------------------------------------------------------------------ 

198 # Ports 

199 self.add_port(name="feed_inlet", block=self.process_in) 

200 self.add_port(name="utility_inlet", block=self.utility_in) 

201 self.add_port(name="concentrate_outlet", block=self.concentrate_state) 

202 self.add_port(name="vapor_outlet", block=self.vapor_state) 

203 self.add_port(name="utility_outlet", block=self.utility_out) 

204 

205 # ------------------------------------------------------------------ 

206 # Unit variables and convenience expressions 

207 self.heat_duty = Var( 

208 time, 

209 initialize=1e5, 

210 units=pyunits.J / pyunits.s, 

211 doc="Heat transferred from the utility side to the process side", 

212 ) 

213 

214 self.deltaP_process = Var( 

215 time, 

216 initialize=0.0, 

217 units=pyunits.Pa, 

218 doc="Process-side pressure change, outlet - inlet", 

219 ) 

220 self.deltaP_utility = Var( 

221 time, 

222 initialize=0.0, 

223 units=pyunits.Pa, 

224 doc="Utility-side pressure change, outlet - inlet", 

225 ) 

226 

227 if not self.config.has_pressure_change: 227 ↛ 232line 227 didn't jump to line 232 because the condition on line 227 was always true

228 for t in time: 

229 self.deltaP_process[t].fix(0.0) 

230 self.deltaP_utility[t].fix(0.0) 

231 

232 self.utility_enthalpy_drop = Expression( 

233 time, 

234 rule=lambda b, t: b.utility_in[t].flow_mol * b.utility_in[t].enth_mol 

235 - b.utility_out[t].flow_mol * b.utility_out[t].enth_mol, 

236 doc="Utility-side enthalpy flow decrease", 

237 ) 

238 

239 self.concentrate_liquid_phase_flow = Expression( 

240 time, 

241 rule=lambda b, t: sum( 

242 b.concentrate_state[t].flow_mass_phase_comp[p, j] 

243 for p, j in b._phase_component_set 

244 if p == b.LIQUID_PHASE 

245 ), 

246 doc="Total liquid-phase flow in the concentrate outlet", 

247 ) 

248 self.concentrate_solid_phase_flow = Expression( 

249 time, 

250 rule=lambda b, t: sum( 

251 b.concentrate_state[t].flow_mass_phase_comp[p, j] 

252 for p, j in b._phase_component_set 

253 if p == b.SOLID_PHASE 

254 ), 

255 doc="Total solid-phase flow in the concentrate outlet", 

256 ) 

257 self.vapor_phase_flow = Expression( 

258 time, 

259 rule=lambda b, t: sum( 

260 b.vapor_state[t].flow_mass_phase_comp[p, j] 

261 for p, j in b._phase_component_set 

262 if p == b.VAPOR_PHASE 

263 ), 

264 doc="Total vapor-phase flow in the vapor outlet", 

265 ) 

266 

267 # ------------------------------------------------------------------ 

268 # Process-side balances 

269 @self.Constraint(time, process_pp.component_list, doc="Total component balances") 

270 def process_component_balances(b, t, j): 

271 return sum( 

272 b.process_in[t].flow_mass_phase_comp[p, j] 

273 for p in process_pp.phase_list 

274 if (p, j) in b.process_in[t].phase_component_set 

275 ) == sum( 

276 b.concentrate_state[t].flow_mass_phase_comp[p, j] 

277 for p in (b.LIQUID_PHASE, b.SOLID_PHASE) 

278 if (p, j) in b.concentrate_state[t].phase_component_set 

279 ) + sum( 

280 b.vapor_state[t].flow_mass_phase_comp[p, j] 

281 for p in (b.VAPOR_PHASE,) 

282 if (p, j) in b.vapor_state[t].phase_component_set 

283 ) 

284 

285 @self.Constraint(time, doc="Process-side total enthalpy balance") 

286 def process_energy_balance(b, t): 

287 return sum( 

288 b.concentrate_state[t].get_enthalpy_flow_terms(p) 

289 for p in (b.LIQUID_PHASE, b.SOLID_PHASE) 

290 if p in process_pp.phase_list 

291 ) + sum( 

292 b.vapor_state[t].get_enthalpy_flow_terms(p) 

293 for p in (b.VAPOR_PHASE,) 

294 if p in process_pp.phase_list 

295 ) == sum( 

296 b.process_in[t].get_enthalpy_flow_terms(p) 

297 for p in process_pp.phase_list 

298 ) + b.heat_duty[t] 

299 

300 @self.Constraint(time, doc="Concentrate pressure balance") 

301 def process_pressure_balance(b, t): 

302 return ( 

303 b.concentrate_state[t].pressure 

304 == b.process_in[t].pressure + b.deltaP_process[t] 

305 ) 

306 

307 @self.Constraint(time, doc="Outlet pressure equality") 

308 def outlet_pressure_equality(b, t): 

309 return b.vapor_state[t].pressure == b.concentrate_state[t].pressure 

310 

311 @self.Constraint(time, doc="Outlet temperature equality") 

312 def outlet_temperature_equality(b, t): 

313 return b.vapor_state[t].temperature == b.concentrate_state[t].temperature 

314 

315 # ------------------------------------------------------------------ 

316 # Utility-side balances 

317 @self.Constraint(time, doc="Utility total-flow continuity") 

318 def utility_flow_balance(b, t): 

319 return b.utility_out[t].flow_mol == b.utility_in[t].flow_mol 

320 

321 @self.Constraint(time, doc="Utility pressure balance") 

322 def utility_pressure_balance(b, t): 

323 return b.utility_out[t].pressure == b.utility_in[t].pressure + b.deltaP_utility[t] 

324 

325 @self.Constraint(time, doc="Utility-to-process heat balance") 

326 def utility_energy_balance(b, t): 

327 return b.heat_duty[t] == b.utility_enthalpy_drop[t] 

328 

329 # ------------------------------------------------------------------ 

330 # Direct outlet phase exclusions. The active phases in each outlet are 

331 # determined by the property package, but the stream identity is 

332 # enforced here by excluding phases that do not belong in that outlet. 

333 @self.Constraint( 

334 time, 

335 doc="Exclude vapor and other non-concentrate phases from the concentrate outlet", 

336 ) 

337 def eq_concentrate_phase_exclusion(b, t,): 

338 return ( 

339 (sum(b.concentrate_state[t].flow_mass_phase_comp[p, j] 

340 for p, j in b._phase_component_set 

341 if p != b.LIQUID_PHASE and p != b.SOLID_PHASE 

342 ) 

343 - b.absent_phase_flow * b._process_flow_units 

344 )/1e5 # Scale this down to avoid numerical issues. 

345 == 0 

346 ) 

347 

348 @self.Constraint( 

349 time, 

350 doc="Exclude liquid and solid phases from the vapor outlet", 

351 ) 

352 def eq_vapor_phase_exclusion(b, t,): 

353 return ( 

354 (sum(b.vapor_state[t].flow_mass_phase_comp[p, j] 

355 for p, j in b._phase_component_set 

356 if p != b.VAPOR_PHASE 

357 ) 

358 - b.absent_phase_flow * b._process_flow_units 

359 )/1e5 # Scale this down to avoid numerical issues. 

360 == 0 

361 ) 

362 

363 

364 

365 # ------------------------------------------------------------------ 

366 # Initialization and reporting 

367 def initialize_build( 

368 self, 

369 process_state_args=None, 

370 utility_state_args=None, 

371 outlvl=idaeslog.NOTSET, 

372 solver=None, 

373 optarg=None, 

374 ): 

375 """Initialize the unit model. 

376 

377 The routine initializes the process and utility inlet states, copies 

378 those states to outlet-side state blocks as seeds, and then solves the 

379 full unit model. 

380 """ 

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

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

383 opt = get_solver(solver, optarg) 

384 t0 = self.flowsheet().time.first() 

385 outlvl=idaeslog.DEBUG 

386 process_flags = self.process_in.initialize( 

387 state_args=process_state_args, 

388 hold_state=True, 

389 outlvl=outlvl, 

390 solver=solver, 

391 optarg=optarg, 

392 ) 

393 utility_flags = self.utility_in.initialize( 

394 state_args=utility_state_args, 

395 hold_state=True, 

396 outlvl=outlvl, 

397 solver=solver, 

398 optarg=optarg, 

399 ) 

400 init_log.info_high("Initialization Step 1: inlet states initialized.") 

401 

402 if process_state_args is None: 402 ↛ 404line 402 didn't jump to line 404 because the condition on line 402 was always true

403 process_state_args = extract_state_args(self.process_in[t0]) 

404 if utility_state_args is None: 404 ↛ 407line 404 didn't jump to line 407 because the condition on line 404 was always true

405 utility_state_args = extract_state_args(self.utility_in[t0]) 

406 

407 self.concentrate_state.initialize( 

408 state_args=deepcopy(process_state_args), 

409 hold_state=False, 

410 outlvl=outlvl, 

411 solver=solver, 

412 optarg=optarg, 

413 ) 

414 self.vapor_state.initialize( 

415 state_args=deepcopy(process_state_args), 

416 hold_state=False, 

417 outlvl=outlvl, 

418 solver=solver, 

419 optarg=optarg, 

420 ) 

421 

422 self.utility_out.initialize( 

423 state_args=deepcopy(utility_state_args), 

424 hold_state=False, 

425 outlvl=outlvl, 

426 solver=solver, 

427 optarg=optarg, 

428 ) 

429 init_log.info_high("Initialization Step 2: outlet states seeded.") 

430 

431 if degrees_of_freedom(self) != 0: 431 ↛ 432line 431 didn't jump to line 432 because the condition on line 431 was never true

432 raise InitializationError( 

433 f"{self.name}: degrees of freedom are {degrees_of_freedom(self)} during initialization; expected 0. " 

434 "Fix heat_duty, fix a utility-outlet state, or add an external heat-transfer relation." 

435 ) 

436 

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

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

439 init_log.info_high(f"Initialization Step 3: {idaeslog.condition(res)}.") 

440 

441 self.process_in.release_state(process_flags, outlvl=outlvl) 

442 self.utility_in.release_state(utility_flags, outlvl=outlvl) 

443 

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

445 raise InitializationError( 

446 f"{self.name} failed to initialize successfully; solver did not terminate optimally." 

447 ) 

448 

449 init_log.info("Initialization complete.") 

450 

451 def _get_stream_table_contents(self, time_point=0): 

452 return create_stream_table_dataframe( 

453 { 

454 "Feed Inlet": self.feed_inlet, 

455 "Utility Inlet": self.utility_inlet, 

456 "Concentrate Outlet": self.concentrate_outlet, 

457 "Vapor Outlet": self.vapor_outlet, 

458 "Utility Outlet": self.utility_outlet, 

459 }, 

460 time_point=time_point, 

461 ) 

462 

463 def _get_performance_contents(self, time_point=0): 

464 data = { 

465 "Heat Duty": self.heat_duty[time_point], 

466 # "Utility Enthalpy Drop": self.utility_enthalpy_drop[time_point], 

467 # "Concentrate Liquid-Phase Flow": self.concentrate_liquid_phase_flow[time_point], 

468 # "Concentrate Solid-Phase Flow": self.concentrate_solid_phase_flow[time_point], 

469 # "Vapor Outlet Flow": self.vapor_phase_flow[time_point], 

470 "Process dP": self.deltaP_process[time_point], 

471 "Utility dP": self.deltaP_utility[time_point], 

472 } 

473 

474 return {"vars": data} 

475 

476 def calculate_scaling_factors(self): 

477 super().calculate_scaling_factors() 

478 

479 process_pp = self.config.process_property_package 

480 

481 for t in self.flowsheet().time: 

482 if iscale.get_scaling_factor(self.heat_duty[t]) is None: 

483 sf_flow = iscale.get_scaling_factor( 

484 self.utility_in[t].flow_mol, 

485 default=1.0, 

486 ) 

487 sf_enth = iscale.get_scaling_factor( 

488 self.utility_in[t].enth_mol, 

489 default=1e-4, 

490 ) 

491 iscale.set_scaling_factor(self.heat_duty[t], sf_flow * sf_enth) 

492 

493 if iscale.get_scaling_factor(self.deltaP_process[t]) is None: 

494 iscale.set_scaling_factor(self.deltaP_process[t], 1e-5) 

495 if iscale.get_scaling_factor(self.deltaP_utility[t]) is None: 

496 iscale.set_scaling_factor(self.deltaP_utility[t], 1e-5) 

497 

498 iscale.constraint_scaling_transform( 

499 self.process_energy_balance[t], 

500 iscale.get_scaling_factor(self.heat_duty[t], default=1e-5), 

501 overwrite=False, 

502 ) 

503 iscale.constraint_scaling_transform( 

504 self.utility_energy_balance[t], 

505 iscale.get_scaling_factor(self.heat_duty[t], default=1e-5), 

506 overwrite=False, 

507 ) 

508 iscale.constraint_scaling_transform( 

509 self.process_pressure_balance[t], 

510 iscale.get_scaling_factor(self.deltaP_process[t], default=1e-5), 

511 overwrite=False, 

512 ) 

513 iscale.constraint_scaling_transform( 

514 self.outlet_pressure_equality[t], 

515 iscale.get_scaling_factor(self.deltaP_process[t], default=1e-5), 

516 overwrite=False, 

517 ) 

518 iscale.constraint_scaling_transform( 

519 self.outlet_temperature_equality[t], 

520 iscale.get_scaling_factor(self.concentrate_state[t].temperature, default=1e-2), 

521 overwrite=False, 

522 ) 

523 iscale.constraint_scaling_transform( 

524 self.utility_pressure_balance[t], 

525 iscale.get_scaling_factor(self.deltaP_utility[t], default=1e-5), 

526 overwrite=False, 

527 ) 

528 

529 for j in process_pp.component_list: 

530 sf_j = 1.0 

531 for p in process_pp.phase_list: 

532 if (p, j) in self.process_in[t].phase_component_set: 

533 sf_j = iscale.get_scaling_factor( 

534 self.process_in[t].flow_mass_phase_comp[p, j], default=1.0 

535 ) 

536 break 

537 iscale.constraint_scaling_transform( 

538 self.process_component_balances[t, j], sf_j, overwrite=False 

539 ) 

540 

541 for t, c in self.eq_concentrate_phase_exclusion.items(): 

542 sf = 1.0 

543 for p, j in self._phase_component_set: 

544 if p not in (self.LIQUID_PHASE, self.SOLID_PHASE): 

545 sf = iscale.get_scaling_factor( 

546 self.concentrate_state[t].flow_mass_phase_comp[p, j], 

547 default=1.0, 

548 ) 

549 break 

550 iscale.constraint_scaling_transform(c, sf, overwrite=False) 

551 

552 for t, c in self.eq_vapor_phase_exclusion.items(): 

553 sf = 1.0 

554 for p, j in self._phase_component_set: 

555 if p != self.VAPOR_PHASE: 

556 sf = iscale.get_scaling_factor( 

557 self.vapor_state[t].flow_mass_phase_comp[p, j], 

558 default=1.0, 

559 ) 

560 break 

561 iscale.constraint_scaling_transform(c, sf, overwrite=False) 

562 

563 

564 def diagnose(self) -> list[tuple[Component, str]]: 

565 """ 

566 Test a few common issues with the heat exchanger model and provide hints to the user. 

567 returns a list with the variable the it is most relevant to and a message describing the issue 

568 """ 

569 problems = [] 

570 utility_vap_frac = pyo.value(self.utility_in[0].vapor_frac) or -1 

571 if utility_vap_frac < 0.1: 571 ↛ 580line 571 didn't jump to line 580 because the condition on line 571 was always true

572 problems.append( 

573 ( 

574 self.utility_in[0].vapor_frac, 

575 f"""Utility inlet vapor fraction is {utility_vap_frac:.2f}.  

576 This model is intended for steam/condensate service; if there is no steam it is unlikely you will have 

577 sufficient driving force for heat transfer.""" 

578 ) 

579 ) 

580 process_vap_frac = pyo.value(self.process_in[0].vapor_frac) or -1 

581 if process_vap_frac > 0.9: 581 ↛ 582line 581 didn't jump to line 582 because the condition on line 581 was never true

582 problems.append( 

583 ( 

584 self.process_in[0].vapor_frac, 

585 f"""Process inlet vapor fraction is {process_vap_frac:.2f}.  

586 There is not much to evaporate; this model requires liquid to be present in the feedstock.""" 

587 ) 

588 ) 

589 vap_flow = pyo.value(self.vapor_state[0].flow_mass) or -1 

590 if vap_flow < 0.1: 590 ↛ 591line 590 didn't jump to line 591 because the condition on line 590 was never true

591 problems.append( 

592 ( 

593 self.vapor_state[0].flow_mass, 

594 f"""Vapor flow is {vap_flow:.2f}.  

595 This model requires a sufficient vapor flow for proper operation.  

596 Perhaps there is not enough heat to create vapor?""" 

597 ) 

598 ) 

599 flow_in = pyo.value(self.process_in[0].flow_mass) or -1 

600 utility_flow_in = pyo.value(self.utility_in[0].flow_mass) or -1 

601 flow_ratio = flow_in / utility_flow_in 

602 if flow_ratio > 1000 or flow_ratio < 0.001: 602 ↛ 603line 602 didn't jump to line 603 because the condition on line 602 was never true

603 problems.append( 

604 ( 

605 self.utility_in[0].flow_mol, 

606 f"""Process inlet mass flow is {flow_in:.2f} kg/s, while utility inlet molar flow is {utility_flow_in:.2f} mol/s,  

607 giving a flow ratio of {flow_ratio:.2f}.  

608 This model assumes the utility is steam; if the utility flow is very low compared to the process flow,  

609 you may not have enough heat transfer driving force for the model to work well.""" 

610 ) 

611 ) 

612 

613 utility_temp_out = pyo.value(self.utility_out[0].temperature) or 0 

614 if utility_temp_out < 280: 614 ↛ 615line 614 didn't jump to line 615 because the condition on line 614 was never true

615 problems.append( 

616 ( 

617 self.utility_out[0].temperature, 

618 f"""Utility outlet temperature is {utility_temp_out:.2f} K.  

619 This probably means there is insufficient utility flow or temperature to provide the necessary heat duty.""" 

620 ) 

621 ) 

622 

623 utility_temp_in = pyo.value(self.utility_in[0].temperature) or 0 

624 if utility_temp_in < utility_temp_out: 624 ↛ 625line 624 didn't jump to line 625 because the condition on line 624 was never true

625 problems.append( 

626 ( 

627 self.utility_in[0].temperature, 

628 f"""Utility inlet temperature is {utility_temp_in:.2f} K, which is less than the outlet temperature of {utility_temp_out:.2f} K.  

629 This is not physically consistent; check your utility inlet conditions. Are you trying to cool the process instead of heat it? This model is intended for heating applications with steam as the utility.""" 

630 ) 

631 ) 

632 

633 return problems