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
« prev ^ index » next coverage.py v7.10.7, created at 2026-05-13 02:47 +0000
1"""Heat user unit model."""
3from typing import Dict, Iterable, List, Optional
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
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
30import idaes.logger as idaeslog
32_log = idaeslog.getLogger(__name__)
34__author__ = "Ahuora Centre for Smart Energy Systems, University of Waikato, New Zealand"
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 )
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 """
87 DEFAULT_SCALING_FACTORS = {
88 "var1": 1e-3,
89 "var2": 1e-3,
90 }
94@declare_process_block_class("HeatUser")
95class HeatUserData(UnitModelBlockData):
96 """Heat user unit operation in the new template format."""
98 default_scaler = HeatUserScaler
100 CONFIG = ConfigBlock()
101 _build_config(CONFIG)
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
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 )
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 )
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 )
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]
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 )
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]
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]
275 def initialize_build(self, state_args=None, outlvl=idaeslog.NOTSET, solver=None, optarg=None):
276 """
277 General wrapper for template initialization routines
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)
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
295 # Set solver options
296 opt = get_solver(solver, optarg)
298 pp = self.inlet_state[t0].params # proporty package calculation block
299 state_args = {} if state_args is None else dict(state_args)
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)
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
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))
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
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])
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.
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
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 )
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 )
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)
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 )
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)
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 )
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},
494 )
496 init_log.info_high("Inlet state initialization complete")
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")
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")
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")
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 ]
548 for con in relaxed_eqns:
549 con.deactivate()
551 report_statistics(self)
553 dt = DiagnosticsToolbox(self)
554 dt.report_structural_issues()
555 dt.display_underconstrained_set()
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")
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)
571 for con in relaxed_eqns:
572 con.activate()
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 )
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")
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)}")
590 #self.model().pprint()
592 report_statistics(self)
594 dt = DiagnosticsToolbox(self)
595 dt.report_structural_issues()
596 dt.display_underconstrained_set()
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)))
603 if not check_optimal_termination(res):
604 raise InitializationError(f"Unit model {self.name} failed relaxed initialization")
605 '''
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
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 )
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 )
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 )
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 )
661 return prop_val
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 }
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 )
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}
690 # -----------------------------------------------------------------
691 # Common utilities
692 # -----------------------------------------------------------------
694 def calculate_scaling_factors(self):
695 super().calculate_scaling_factors()
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] = []
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
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)
718 if is_build_port: # No port is needed for intermediate/internal state blocks
719 self.add_port(name=stream_name, block=sb)
721 return blocks
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)