ahuora-builder 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (167) hide show
  1. ahuora_builder/__init__.py +0 -0
  2. ahuora_builder/arc_manager.py +33 -0
  3. ahuora_builder/build_state.py +57 -0
  4. ahuora_builder/custom/PIDController.py +494 -0
  5. ahuora_builder/custom/PySMOModel.py +178 -0
  6. ahuora_builder/custom/SimpleEffectivenessHX_DH.py +727 -0
  7. ahuora_builder/custom/__init__.py +0 -0
  8. ahuora_builder/custom/add_initial_dynamics.py +35 -0
  9. ahuora_builder/custom/custom_compressor.py +107 -0
  10. ahuora_builder/custom/custom_cooler.py +33 -0
  11. ahuora_builder/custom/custom_heat_exchanger.py +183 -0
  12. ahuora_builder/custom/custom_heat_exchanger_1d.py +258 -0
  13. ahuora_builder/custom/custom_heater.py +41 -0
  14. ahuora_builder/custom/custom_pressure_changer.py +34 -0
  15. ahuora_builder/custom/custom_pump.py +107 -0
  16. ahuora_builder/custom/custom_separator.py +371 -0
  17. ahuora_builder/custom/custom_tank.py +133 -0
  18. ahuora_builder/custom/custom_turbine.py +132 -0
  19. ahuora_builder/custom/custom_valve.py +300 -0
  20. ahuora_builder/custom/custom_variable.py +29 -0
  21. ahuora_builder/custom/direct_steam_injection.py +371 -0
  22. ahuora_builder/custom/energy/__init__.py +0 -0
  23. ahuora_builder/custom/energy/acBus.py +280 -0
  24. ahuora_builder/custom/energy/ac_property_package.py +279 -0
  25. ahuora_builder/custom/energy/battery.py +170 -0
  26. ahuora_builder/custom/energy/bus.py +182 -0
  27. ahuora_builder/custom/energy/energy_mixer.py +195 -0
  28. ahuora_builder/custom/energy/energy_splitter.py +228 -0
  29. ahuora_builder/custom/energy/grid.py +173 -0
  30. ahuora_builder/custom/energy/hydro.py +169 -0
  31. ahuora_builder/custom/energy/link.py +137 -0
  32. ahuora_builder/custom/energy/load.py +155 -0
  33. ahuora_builder/custom/energy/mainDistributionBoard.py +257 -0
  34. ahuora_builder/custom/energy/power_property_package.py +253 -0
  35. ahuora_builder/custom/energy/solar.py +176 -0
  36. ahuora_builder/custom/energy/storage.py +230 -0
  37. ahuora_builder/custom/energy/storage_wrapper +0 -0
  38. ahuora_builder/custom/energy/tests/__init__.py +0 -0
  39. ahuora_builder/custom/energy/tests/test_bus.py +44 -0
  40. ahuora_builder/custom/energy/tests/test_energy_mixer.py +46 -0
  41. ahuora_builder/custom/energy/tests/test_mdb.py +49 -0
  42. ahuora_builder/custom/energy/transformer.py +187 -0
  43. ahuora_builder/custom/energy/transformer_property_package.py +267 -0
  44. ahuora_builder/custom/energy/transmissionLine.py +228 -0
  45. ahuora_builder/custom/energy/wind.py +206 -0
  46. ahuora_builder/custom/hda_ideal_VLE.py +1341 -0
  47. ahuora_builder/custom/hda_reaction.py +182 -0
  48. ahuora_builder/custom/heat_exchanger_1d_wrapper.py +31 -0
  49. ahuora_builder/custom/integration_block.py +106 -0
  50. ahuora_builder/custom/inverted.py +81 -0
  51. ahuora_builder/custom/performance_curves.py +1 -0
  52. ahuora_builder/custom/reactions/__init__.py +0 -0
  53. ahuora_builder/custom/reactions/hda_stoich.py +10 -0
  54. ahuora_builder/custom/simple_separator.py +680 -0
  55. ahuora_builder/custom/tests/__init__.py +0 -0
  56. ahuora_builder/custom/tests/test_SimpleEffectivenessHX_DH.py +91 -0
  57. ahuora_builder/custom/tests/test_custom_tank.py +70 -0
  58. ahuora_builder/custom/tests/test_direct_steam_injection.py +41 -0
  59. ahuora_builder/custom/tests/test_simple_separator.py +46 -0
  60. ahuora_builder/custom/tests/test_waterpipe.py +46 -0
  61. ahuora_builder/custom/thermal_utility_systems/desuperheater.py +624 -0
  62. ahuora_builder/custom/thermal_utility_systems/header.py +889 -0
  63. ahuora_builder/custom/thermal_utility_systems/simple_heat_pump.py +567 -0
  64. ahuora_builder/custom/thermal_utility_systems/steam_header.py +353 -0
  65. ahuora_builder/custom/thermal_utility_systems/steam_user.py +944 -0
  66. ahuora_builder/custom/thermal_utility_systems/temp.py +349 -0
  67. ahuora_builder/custom/thermal_utility_systems/tests/test_desuperheater.py +142 -0
  68. ahuora_builder/custom/thermal_utility_systems/tests/test_header.py +998 -0
  69. ahuora_builder/custom/thermal_utility_systems/tests/test_ntu_hx.py +129 -0
  70. ahuora_builder/custom/thermal_utility_systems/tests/test_simple_heat_pump.py +120 -0
  71. ahuora_builder/custom/thermal_utility_systems/tests/test_steam_header.py +703 -0
  72. ahuora_builder/custom/thermal_utility_systems/tests/test_steam_user.py +277 -0
  73. ahuora_builder/custom/thermal_utility_systems/tests/test_waterpipe.py +36 -0
  74. ahuora_builder/custom/thermal_utility_systems/tests/test_willans_turbine.py +253 -0
  75. ahuora_builder/custom/thermal_utility_systems/willans_turbine.py +804 -0
  76. ahuora_builder/custom/translator.py +129 -0
  77. ahuora_builder/custom/updated_pressure_changer.py +1404 -0
  78. ahuora_builder/custom/valve_wrapper.py +38 -0
  79. ahuora_builder/custom/water_tank_with_units.py +456 -0
  80. ahuora_builder/diagnostics/__init__.py +0 -0
  81. ahuora_builder/diagnostics/infeasibilities.py +40 -0
  82. ahuora_builder/diagnostics/tests/__init__.py +0 -0
  83. ahuora_builder/diagnostics/tests/test_infeasibilities.py +28 -0
  84. ahuora_builder/flowsheet_manager.py +542 -0
  85. ahuora_builder/flowsheet_manager_type.py +20 -0
  86. ahuora_builder/generate_python_file.py +440 -0
  87. ahuora_builder/methods/BlockContext.py +84 -0
  88. ahuora_builder/methods/__init__.py +0 -0
  89. ahuora_builder/methods/adapter.py +355 -0
  90. ahuora_builder/methods/adapter_library.py +549 -0
  91. ahuora_builder/methods/adapter_methods.py +80 -0
  92. ahuora_builder/methods/expression_parsing.py +105 -0
  93. ahuora_builder/methods/load_unit_model.py +147 -0
  94. ahuora_builder/methods/slice_manipulation.py +7 -0
  95. ahuora_builder/methods/tests/__init__.py +0 -0
  96. ahuora_builder/methods/tests/test_expression_parsing.py +15 -0
  97. ahuora_builder/methods/units_handler.py +129 -0
  98. ahuora_builder/ml_wizard.py +101 -0
  99. ahuora_builder/port_manager.py +20 -0
  100. ahuora_builder/properties_manager.py +44 -0
  101. ahuora_builder/property_package_manager.py +78 -0
  102. ahuora_builder/solver.py +38 -0
  103. ahuora_builder/tear_manager.py +98 -0
  104. ahuora_builder/tests/__init__.py +0 -0
  105. ahuora_builder/tests/test_generate_python_file/__init__.py +0 -0
  106. ahuora_builder/tests/test_generate_python_file/configurations/compressor_generated.py +63 -0
  107. ahuora_builder/tests/test_generate_python_file/configurations/heat_exchanger_generated.py +70 -0
  108. ahuora_builder/tests/test_generate_python_file/configurations/pump_generated.py +84 -0
  109. ahuora_builder/tests/test_generate_python_file/configurations/recycle_generated.py +73 -0
  110. ahuora_builder/tests/test_generate_python_file/test_generate_python_file.py +108 -0
  111. ahuora_builder/tests/test_solver/__init__.py +0 -0
  112. ahuora_builder/tests/test_solver/configurations/BT_PR.json +59 -0
  113. ahuora_builder/tests/test_solver/configurations/BT_PR_solved.json +59 -0
  114. ahuora_builder/tests/test_solver/configurations/bus.json +99 -0
  115. ahuora_builder/tests/test_solver/configurations/bus_solved.json +50 -0
  116. ahuora_builder/tests/test_solver/configurations/compound_separator.json +377 -0
  117. ahuora_builder/tests/test_solver/configurations/compound_separator_solved.json +374 -0
  118. ahuora_builder/tests/test_solver/configurations/compressor.json +38 -0
  119. ahuora_builder/tests/test_solver/configurations/compressor_solved.json +68 -0
  120. ahuora_builder/tests/test_solver/configurations/constraints.json +44 -0
  121. ahuora_builder/tests/test_solver/configurations/constraints_solved.json +59 -0
  122. ahuora_builder/tests/test_solver/configurations/control.json +39 -0
  123. ahuora_builder/tests/test_solver/configurations/control_solved.json +68 -0
  124. ahuora_builder/tests/test_solver/configurations/dynamic_tank.json +733 -0
  125. ahuora_builder/tests/test_solver/configurations/dynamic_tank_solved.json +846 -0
  126. ahuora_builder/tests/test_solver/configurations/elimination.json +39 -0
  127. ahuora_builder/tests/test_solver/configurations/elimination_solved.json +68 -0
  128. ahuora_builder/tests/test_solver/configurations/expressions.json +68 -0
  129. ahuora_builder/tests/test_solver/configurations/expressions_solved.json +104 -0
  130. ahuora_builder/tests/test_solver/configurations/header.json +1192 -0
  131. ahuora_builder/tests/test_solver/configurations/header_solved.json +761 -0
  132. ahuora_builder/tests/test_solver/configurations/heat_exchanger.json +63 -0
  133. ahuora_builder/tests/test_solver/configurations/heat_exchanger_solved.json +104 -0
  134. ahuora_builder/tests/test_solver/configurations/heat_pump.json +137 -0
  135. ahuora_builder/tests/test_solver/configurations/heat_pump_solved.json +104 -0
  136. ahuora_builder/tests/test_solver/configurations/machine_learning.json +2156 -0
  137. ahuora_builder/tests/test_solver/configurations/machine_learning_solved.json +266 -0
  138. ahuora_builder/tests/test_solver/configurations/mass_flow_tear.json +77 -0
  139. ahuora_builder/tests/test_solver/configurations/mass_flow_tear_solved.json +68 -0
  140. ahuora_builder/tests/test_solver/configurations/milk_heater.json +521 -0
  141. ahuora_builder/tests/test_solver/configurations/milk_heater_solved.json +311 -0
  142. ahuora_builder/tests/test_solver/configurations/mixer.json +44 -0
  143. ahuora_builder/tests/test_solver/configurations/mixer_solved.json +86 -0
  144. ahuora_builder/tests/test_solver/configurations/optimization.json +62 -0
  145. ahuora_builder/tests/test_solver/configurations/optimization_solved.json +59 -0
  146. ahuora_builder/tests/test_solver/configurations/propane_heat_pump.json +167 -0
  147. ahuora_builder/tests/test_solver/configurations/propane_heat_pump_solved.json +158 -0
  148. ahuora_builder/tests/test_solver/configurations/propane_recycle.json +141 -0
  149. ahuora_builder/tests/test_solver/configurations/propane_recycle_solved.json +104 -0
  150. ahuora_builder/tests/test_solver/configurations/pump.json +64 -0
  151. ahuora_builder/tests/test_solver/configurations/pump_solved.json +59 -0
  152. ahuora_builder/tests/test_solver/configurations/pump_unit_conversions.json +63 -0
  153. ahuora_builder/tests/test_solver/configurations/recycle.json +49 -0
  154. ahuora_builder/tests/test_solver/configurations/recycle_solved.json +50 -0
  155. ahuora_builder/tests/test_solver/configurations/sb_vapor_frac.json +29 -0
  156. ahuora_builder/tests/test_solver/configurations/sb_vapor_frac_solved.json +29 -0
  157. ahuora_builder/tests/test_solver/configurations/solar.json +67 -0
  158. ahuora_builder/tests/test_solver/configurations/solar_solved.json +50 -0
  159. ahuora_builder/tests/test_solver/configurations/vapor_frac_target.json +67 -0
  160. ahuora_builder/tests/test_solver/configurations/vapor_frac_target_solved.json +68 -0
  161. ahuora_builder/tests/test_solver/test_solve_models.py +250 -0
  162. ahuora_builder/timing.py +65 -0
  163. ahuora_builder/types/__init__.py +1 -0
  164. ahuora_builder/unit_model_manager.py +48 -0
  165. ahuora_builder-0.1.0.dist-info/METADATA +14 -0
  166. ahuora_builder-0.1.0.dist-info/RECORD +167 -0
  167. ahuora_builder-0.1.0.dist-info/WHEEL +4 -0
@@ -0,0 +1,889 @@
1
+ # Pyomo core
2
+ import pyomo.environ as pyo
3
+ from pyomo.environ import (
4
+ Constraint,
5
+ Expression,
6
+ Param,
7
+ PositiveReals,
8
+ RangeSet,
9
+ Suffix,
10
+ Var,
11
+ value,
12
+ units as UNIT,
13
+ )
14
+ from pyomo.core.base.reference import Reference
15
+ from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool
16
+
17
+ # IDAES core
18
+ from idaes.core import (
19
+ declare_process_block_class,
20
+ UnitModelBlockData,
21
+ useDefault,
22
+ StateBlock,
23
+ )
24
+ from idaes.core.util import scaling
25
+ from idaes.core.util.config import is_physical_parameter_block
26
+ from idaes.core.util.math import smooth_min, smooth_max
27
+ from idaes.core.util.tables import create_stream_table_dataframe
28
+ from idaes.core.solvers import get_solver
29
+ from idaes.core.initialization import ModularInitializerBase
30
+ from idaes.core.util.model_statistics import degrees_of_freedom
31
+
32
+ # Logger
33
+ import idaes.logger as idaeslog
34
+
35
+ # Typing
36
+ from typing import List
37
+
38
+
39
+ __author__ = "Ahuora Centre for Smart Energy Systems, University of Waikato, New Zealand"
40
+
41
+ # Set up logger
42
+ _log = idaeslog.getLogger(__name__)
43
+
44
+ class SimpleHeaderInitializer(ModularInitializerBase):
45
+ """Initialize a Header unit block with staged seeding and solves.
46
+
47
+ This routine performs a two-stage initialization:
48
+ 1) Seed inlet and internal state variables, relax selected constraints, and
49
+ perform a first solve.
50
+ 2) Reactivate/tighten constraints and perform a second solve.
51
+
52
+ Args:
53
+ blk: The Header unit model block to initialize.
54
+ **kwargs: Optional keyword arguments:
55
+ solver: A Pyomo/IDAES solver object. If not provided, uses ``get_solver()``.
56
+ solver_options (dict): Options to set on the solver, e.g. tolerances.
57
+ outlvl: IDAES log level (e.g., ``idaeslog.WARNING``).
58
+
59
+ Returns:
60
+ pyomo.opt.results.results_.SolverResults: The result object from the final solve.
61
+
62
+ Notes:
63
+ - Inlet state blocks are initialized via their own ``initialize`` if available.
64
+ - Mixed state is seeded from inlet totals/minimums (pressure) and average
65
+ enthalpy; works with temperature- or enthalpy-based property packages.
66
+ - Temporary seeds/relaxations are undone, leaving original DOF intact.
67
+ """
68
+
69
+ def initialize(self, blk, **kwargs):
70
+ # --- Solver setup
71
+ solver = kwargs.get("solver", None) or get_solver()
72
+ solver_options = kwargs.get("solver_options", {})
73
+ for k, v in solver_options.items():
74
+ solver.options[k] = v
75
+
76
+ outlvl = kwargs.get("outlvl", idaeslog.WARNING)
77
+ log = idaeslog.getLogger(__name__)
78
+
79
+ # --- Time index
80
+ t0 = blk.flowsheet().time.first()
81
+
82
+ # --- 1) Initialize inlet state blocks
83
+ inlet_blocks = list(blk.inlet_blocks)
84
+ if len(inlet_blocks) < 1:
85
+ raise ValueError("No inlet added to header.")
86
+
87
+ for sb in inlet_blocks:
88
+ if hasattr(sb, "initialize"):
89
+ sb.initialize(outlvl=outlvl)
90
+
91
+ # --- 2) Aggregate inlet info for seeding mixed state block
92
+ F_mixed = sum(
93
+ value(sb[t0].flow_mol)
94
+ for sb in inlet_blocks
95
+ )
96
+ P_mixed = min(
97
+ value(sb[t0].pressure)
98
+ for sb in inlet_blocks
99
+ )
100
+ E_mixed = sum(
101
+ value(sb[t0].flow_mol * sb[t0].enth_mol, 0.0)
102
+ for sb in inlet_blocks
103
+ )
104
+ if F_mixed > 0:
105
+ h_mixed = E_mixed / F_mixed
106
+ else:
107
+ # Seed from the first inlet’s enthalpy (no double subscripting)
108
+ first_inlet = inlet_blocks[0]
109
+ h_mixed = value(first_inlet[t0].enth_mol)
110
+
111
+ # --- 3) Seed mixed_state: flow, pressure, enthalpy
112
+ ms = blk.mixed_state
113
+ ms[t0].flow_mol.set_value(
114
+ F_mixed
115
+ )
116
+ ms[t0].pressure.set_value(
117
+ P_mixed
118
+ )
119
+ ms[t0].enth_mol.set_value(
120
+ h_mixed
121
+ )
122
+ ms.initialize(outlvl=outlvl)
123
+
124
+ # --- 4) Seed outlet_states with pressure, enthalpy
125
+ flow_undefined = []
126
+ defined_flow = 0
127
+ for sb in blk.outlet_blocks:
128
+ sb[t0].pressure.set_value(
129
+ value(ms[t0].pressure)
130
+ )
131
+ sb[t0].enth_mol.set_value(
132
+ value(ms[t0].enth_mol)
133
+ )
134
+ if sb in [blk.outlet_condensate_state, blk.outlet_vent_state]:
135
+ sb[t0].flow_mol.set_value(
136
+ 0.0
137
+ )
138
+ else:
139
+ if value(sb[t0].flow_mol, exception=False) is None:
140
+ flow_undefined.append(sb)
141
+ else:
142
+ defined_flow += value(sb[t0].flow_mol)
143
+
144
+ tot_undefined_flow = max(sum(value(sb[t0].flow_mol) for sb in blk.inlet_blocks) - defined_flow, 0)
145
+ for sb in flow_undefined:
146
+ sb[t0].flow_mol.set_value(
147
+ tot_undefined_flow / len(flow_undefined)
148
+ )
149
+
150
+ for sb in blk.outlet_blocks:
151
+ sb.initialize(outlvl=outlvl)
152
+
153
+ res2 = solver.solve(blk, tee=False)
154
+ log.info(f"Header init status: {res2.solver.termination_condition}")
155
+
156
+ return res2
157
+
158
+ def _make_config_block(config):
159
+ """Declare configuration options for the Header unit.
160
+
161
+ Declares property package references and integer counts for inlets and outlets.
162
+
163
+ Args:
164
+ config (ConfigBlock): The mutable configuration block to populate.
165
+
166
+ Raises:
167
+ ValueError: If invalid option values are provided by the caller (via IDAES).
168
+ """
169
+
170
+ config.declare(
171
+ "property_package",
172
+ ConfigValue(
173
+ default=useDefault,
174
+ domain=is_physical_parameter_block,
175
+ description="Property package to use for control volume",
176
+ ),
177
+ )
178
+ config.declare(
179
+ "property_package_args",
180
+ ConfigBlock(
181
+ implicit=True,
182
+ description="Arguments to use for constructing property packages",
183
+ ),
184
+ )
185
+ config.declare(
186
+ "num_inlets",
187
+ ConfigValue(
188
+ default=1,
189
+ domain=In(list(range(1, 100))),
190
+ description="Number of utility providers at inlets.",
191
+ ),
192
+ )
193
+ config.declare(
194
+ "num_outlets",
195
+ ConfigValue(
196
+ default=1,
197
+ domain=In(list(range(0, 100))),
198
+ description="Number of utility users at outlets." \
199
+ "Excludes outlets associated with condensate and vent flows.",
200
+ ),
201
+ )
202
+ config.declare(
203
+ "is_liquid_header",
204
+ ConfigValue(
205
+ default=False,
206
+ domain=Bool,
207
+ description="Flag for selecting liquid or vapour (including steam and other gases).",
208
+ ),
209
+ )
210
+ @declare_process_block_class("simple_header")
211
+ class SimpleHeaderData(UnitModelBlockData):
212
+ """Thermal utility header unit operation.
213
+
214
+ The Header aggregates multiple inlet providers and distributes utility to
215
+ multiple users, with optional venting, condensate removal (or liquid overflow), heat loss, and
216
+ pressure loss. A mixed (intermediate) state is used for balances and
217
+ pressure/enthalpy coupling across outlets.
218
+
219
+ Key features:
220
+ - Material, energy, and momentum balances with smooth min/max functions.
221
+ - Vapour/liquid equilibrium calculation for mixed state.
222
+ - Shared mixed enthalpy across outlets of the same phase.
223
+ - Computed excess flow from an overall flow balance.
224
+ - Optional heat and pressure losses.
225
+
226
+ Attributes:
227
+ inlet_list (list[str]): Names for inlet ports.
228
+ outlet_list (list[str]): Names for outlet ports (incl. condensate/ and vent).
229
+ inlet_blocks (list): StateBlocks for all inlets.
230
+ outlet_blocks (list): StateBlocks for all outlets.
231
+ mixed_state: Intermediate mixture StateBlock.
232
+ heat_loss (Var): Heat loss from the header (W).
233
+ pressure_loss (Var): Pressure drop from inlet minimum to mixed state (Pa).
234
+ makeup_flow_mol (Var): Required inlet makeup molar flow (mol/s).
235
+ """
236
+
237
+ default_initializer=SimpleHeaderInitializer
238
+ CONFIG = UnitModelBlockData.CONFIG()
239
+ _make_config_block(CONFIG)
240
+
241
+ def build(self) -> None:
242
+ # 1. Inherit standard UnitModelBlockData properties and functions
243
+ super().build()
244
+
245
+ # 2. Validate input parameters are valid
246
+ self._validate_model_config()
247
+
248
+ # 3. Create lists of ports with state blocks to add
249
+ self.inlet_list = self._create_inlet_port_name_list()
250
+ self.outlet_list = self._create_outlet_port_name_list()
251
+
252
+ # 4. Declare ports, state blocks and state property bounds
253
+ self.inlet_blocks = self._add_ports_with_state_blocks(
254
+ stream_list=self.inlet_list,
255
+ is_inlet=True,
256
+ has_phase_equilibrium=False,
257
+ is_defined_state=True,
258
+ )
259
+ self.outlet_blocks = self._add_ports_with_state_blocks(
260
+ stream_list=self.outlet_list,
261
+ is_inlet=False,
262
+ has_phase_equilibrium=False,
263
+ is_defined_state=False
264
+ )
265
+ self._internal_blocks = self._add_internal_state_blocks()
266
+ self._add_bounds_to_state_properties()
267
+ self._outlet_supply_blocks = self._create_custom_state_lists()
268
+
269
+ # 4. Declare references, variables and expressions for external and internal use
270
+ self._create_references()
271
+ self._create_variables()
272
+ self._create_expressions()
273
+
274
+ # 5. Set balance equations
275
+ self._add_material_balances()
276
+ self._add_energy_balances()
277
+ self._add_momentum_balances()
278
+ self._add_additional_constraints()
279
+
280
+ # 6. Other
281
+ self.scaling_factor = Suffix(direction=Suffix.EXPORT)
282
+ self.split_flow = self._create_flow_map_references()
283
+
284
+ def _validate_model_config(self) -> bool:
285
+ """Validate configuration for inlet and outlet counts.
286
+
287
+ Raises:
288
+ ValueError: If ``num_inlets < 1`` or ``num_outlets < 1``.
289
+ """
290
+ if self.config.num_inlets < 1:
291
+ raise ValueError("Header requires at least one provider (num_inlets >= 1).")
292
+ if self.config.num_outlets < 1:
293
+ raise ValueError("Header requires at least one user (num_outlets >= 1).")
294
+ return True
295
+
296
+ def _create_inlet_port_name_list(self) -> List[str]:
297
+ """Build ordered inlet port names.
298
+
299
+ Returns:
300
+ list[str]: Names ``["inlet_1", ..., "inlet_N"]`` based on ``num_inlets``.
301
+ """
302
+ return [
303
+ f"inlet_{i+1}" for i in range(self.config.num_inlets)
304
+ ]
305
+
306
+ def _create_outlet_port_name_list(self) -> List[str]:
307
+ """Build ordered outlet port names.
308
+
309
+ Returns:
310
+ list[str]: Names ``["outlet_1", ..., "outlet_n", "outlet_condensate", "outlet_vent"]``.
311
+ """
312
+ return [
313
+ f"outlet_{i+1}" for i in range(self.config.num_outlets)
314
+ ] + ["outlet_condensate"] + ["outlet_vent"]
315
+
316
+ def _add_ports_with_state_blocks(self,
317
+ stream_list: List[str],
318
+ is_inlet: List[str],
319
+ has_phase_equilibrium: bool=False,
320
+ is_defined_state: bool=None,
321
+ ) -> List[StateBlock]:
322
+ """Construct StateBlocks and expose them as ports.
323
+
324
+ Creates a StateBlock per named stream and attaches a corresponding inlet or
325
+ outlet Port. Inlet blocks are defined states; outlet blocks are calculated states.
326
+
327
+ Args:
328
+ stream_list (list[str]): Port/StateBlock base names to create.
329
+ is_inlet (bool): If True, create inlet ports with ``defined_state=True``;
330
+ otherwise create outlet ports with ``defined_state=False``.
331
+ has_phase_equilibrium (bool)
332
+
333
+ Returns:
334
+ list: The created StateBlocks, in the same order as ``stream_list``.
335
+ """
336
+ # Create empty list to hold StateBlocks for return
337
+ state_block_ls = []
338
+
339
+ # Setup StateBlock argument dict
340
+ tmp_dict = dict(**self.config.property_package_args)
341
+ tmp_dict["has_phase_equilibrium"] = has_phase_equilibrium
342
+ if is_defined_state == None:
343
+ tmp_dict["defined_state"] = True if is_inlet else False
344
+ else:
345
+ tmp_dict["defined_state"] = is_defined_state
346
+
347
+ # Create an instance of StateBlock for all streams
348
+ for s in stream_list:
349
+ sb = self.config.property_package.build_state_block(
350
+ self.flowsheet().time, doc=f"Thermophysical properties at {s}", **tmp_dict
351
+ )
352
+ setattr(
353
+ self, s + "_state",
354
+ sb
355
+ )
356
+ state_block_ls.append(sb)
357
+ add_fn = self.add_inlet_port if is_inlet else self.add_outlet_port
358
+ add_fn(
359
+ name=s,
360
+ block=sb,
361
+ )
362
+
363
+ return state_block_ls
364
+
365
+ def _add_internal_state_blocks(self) -> List[StateBlock]:
366
+ """Create the intermediate (mixed) StateBlock.
367
+
368
+ The mixed state:
369
+ - Has phase equilibrium enabled.
370
+ - Is not a defined state (solved from balances).
371
+ """
372
+ tmp_dict = dict(**self.config.property_package_args)
373
+ tmp_dict["has_phase_equilibrium"] = True
374
+ tmp_dict["defined_state"] = False
375
+
376
+ self.mixed_state = self.config.property_package.build_state_block(
377
+ self.flowsheet().time,
378
+ doc=f"Thermophysical properties at intermediate mixed state.",
379
+ **tmp_dict
380
+ )
381
+ return [
382
+ self.mixed_state
383
+ ]
384
+
385
+ def _add_bounds_to_state_properties(self) -> None:
386
+ """Add lower and/or upper bounds to state properties.
387
+
388
+ - Set nonnegativity lower bounds on all inlet/outlet molar flows.
389
+ """
390
+ for sb in (self.inlet_blocks + self.outlet_blocks):
391
+ for t in sb:
392
+ sb[t].flow_mol.setlb(0.0)
393
+
394
+ def _create_custom_state_lists(self) -> List[StateBlock]:
395
+ """Partition outlet names into vapour outlets and capture their StateBlocks.
396
+
397
+ Populates:
398
+ - ``_outlet_supply_list``: Outlet names excluding condensate and vent.
399
+ - ``_outlet_supply_blocks``: Corresponding StateBlocks.
400
+ """
401
+ self._outlet_supply_list = [
402
+ v for v in self.outlet_list
403
+ if not v in ["outlet_condensate", "outlet_vent"]
404
+ ]
405
+ return [
406
+ getattr(self, n + "_state")
407
+ for n in self._outlet_supply_list
408
+ ]
409
+
410
+ def _create_references(self) -> None:
411
+ """Create convenient References.
412
+
413
+ Creates references to mixed_state properties:
414
+ - ``total_flow_mol``
415
+ - ``total_flow_mass``
416
+ - ``pressure``
417
+ - ``temperature``
418
+ - ``enth_mol``
419
+ - ``enth_mass``
420
+ - ``vapor_frac``
421
+ """
422
+ self.total_flow_mol = Reference(
423
+ self.mixed_state[:].flow_mol
424
+ )
425
+ self.total_flow_mass = Reference(
426
+ self.mixed_state[:].flow_mass
427
+ )
428
+ self.pressure = Reference(
429
+ self.mixed_state[:].pressure
430
+ )
431
+ self.temperature = Reference(
432
+ self.mixed_state[:].temperature
433
+ )
434
+ self.enth_mol = Reference(
435
+ self.mixed_state[:].enth_mol
436
+ )
437
+ self.enth_mass = Reference(
438
+ self.mixed_state[:].enth_mass
439
+ )
440
+ self.vapor_frac = Reference(
441
+ self.mixed_state[:].vapor_frac
442
+ )
443
+
444
+ def _create_variables(self) -> None:
445
+ """Create required variables.
446
+
447
+ Creates:
448
+ - ``heat_loss`` (W)
449
+ - ``pressure_loss`` (Pa)
450
+ """
451
+ self.heat_loss = Var(
452
+ self.flowsheet().time,
453
+ initialize=0.0,
454
+ doc="Heat loss",
455
+ units=UNIT.W
456
+ )
457
+ self.pressure_loss = Var(
458
+ self.flowsheet().time,
459
+ initialize=0.0,
460
+ doc="Pressure loss",
461
+ units=UNIT.Pa
462
+ )
463
+
464
+ def _create_expressions(self) -> None:
465
+ """Create convenient Expressions.
466
+
467
+ Creates:
468
+ - ``balance_flow_mol`` (mol/s)
469
+ - ``degree_of_superheat`` (K)
470
+ - ``makeup_flow_mol`` (mol/s)
471
+ - ``_partial_total_flow_mol`` (mol/s): used for scaling purposes in a material balance
472
+ """
473
+ self.degree_of_superheat = Expression(
474
+ self.flowsheet().time,
475
+ rule=lambda b, t: b.temperature[t] - b.outlet_condensate_state[t].temperature
476
+ )
477
+ self._partial_total_flow_mol = Expression(
478
+ self.flowsheet().time,
479
+ rule=lambda b, t: (
480
+ sum(
481
+ o[t].flow_mol
482
+ for o in (b.inlet_blocks + b._outlet_supply_blocks)
483
+ )
484
+ )
485
+ )
486
+ self.balance_flow_mol = Expression(
487
+ self.flowsheet().time,
488
+ rule=lambda b, t: (
489
+ sum(
490
+ i[t].flow_mol
491
+ for i in b.inlet_blocks
492
+ )
493
+ -
494
+ sum(
495
+ o[t].flow_mol
496
+ for o in (
497
+ b._outlet_supply_blocks +
498
+ [
499
+ b.outlet_vent_state
500
+ if self.config.is_liquid_header
501
+ else b.outlet_condensate_state
502
+ ]
503
+ )
504
+ )
505
+ )
506
+ )
507
+ self.makeup_flow_mol = Expression(
508
+ self.flowsheet().time,
509
+ rule=lambda b, t: (
510
+ (
511
+ b.outlet_condensate_state[t].flow_mol
512
+ if self.config.is_liquid_header
513
+ else b.outlet_vent_state[t].flow_mol
514
+ )
515
+ -
516
+ b.balance_flow_mol[t]
517
+ )
518
+ )
519
+
520
+ def _add_material_balances(self) -> None:
521
+ """Material balance equations summary.
522
+
523
+ Introduces:
524
+ - ``_partial_total_flow_mol``: Sum of known inlet and vapour outlet flows,
525
+ used for scaling a smooth vent calculation.
526
+
527
+ Constraints:
528
+ - ``mixed_state_material_balance``: Mixed flow equals total inlet flow.
529
+ - ``vent_flow_balance``: Depends on the header's primary phase: liquid vs gas
530
+ If gas header, smoothly enforces nonnegative vent flow.
531
+ If liquid header, determines flow from mixed-state vapour fraction
532
+ - ``condensate_flow_balance``: Depends on the header's primary phase: liquid vs gas
533
+ If gas header, determines flow from mixed-state vapour fraction
534
+ If liquid header, smoothly enforces nonnegative condensate flow.
535
+ """
536
+
537
+ @self.Constraint(
538
+ self.flowsheet().time,
539
+ doc="Mixed state material balance",
540
+ )
541
+ def mixed_state_material_balance(b, t):
542
+ return (
543
+ b.mixed_state[t].flow_mol
544
+ ==
545
+ sum(
546
+ i[t].flow_mol
547
+ for i in b.inlet_blocks
548
+ )
549
+ )
550
+
551
+ eps = 1e-5 # smoothing parameter; smaller = closer to exact max, larger = smoother
552
+ if self.config.is_liquid_header:
553
+ # Assigns excess liquid flow to outlet_condensate
554
+ @self.Constraint(
555
+ self.flowsheet().time,
556
+ doc="Condensate flow balance." \
557
+ "Determines the positive amount of excess flow that exits through outlet_condensate"
558
+ )
559
+ def condensate_flow_balance(b, t):
560
+ return (
561
+ b.outlet_condensate_state[t].flow_mol
562
+ ==
563
+ smooth_max(
564
+ b.balance_flow_mol[t] / (b._partial_total_flow_mol[t] + 1e-6),
565
+ 0.0,
566
+ eps,
567
+ ) * (b._partial_total_flow_mol[t] + 1e-6)
568
+ )
569
+
570
+ # Removes any gas/vapour from a liquid header
571
+ @self.Constraint(
572
+ self.flowsheet().time,
573
+ doc="Vent balance."
574
+ )
575
+ def vent_flow_balance(b, t):
576
+ return b.outlet_vent_state[t].flow_mol == (
577
+ b.mixed_state[t].flow_mol * b.mixed_state[t].vapor_frac
578
+ )
579
+ else:
580
+ # Assigns excess steam/vapour flow to outlet_vent
581
+ @self.Constraint(
582
+ self.flowsheet().time,
583
+ doc="Vent flow balance." \
584
+ "Determines the positive amount of excess flow that exits through the vent"
585
+ )
586
+ def vent_flow_balance(b, t):
587
+ return (
588
+ b.outlet_vent_state[t].flow_mol
589
+ ==
590
+ smooth_max(
591
+ b.balance_flow_mol[t] / (b._partial_total_flow_mol[t] + 1e-6),
592
+ 0.0,
593
+ eps,
594
+ ) * (b._partial_total_flow_mol[t] + 1e-6)
595
+ )
596
+
597
+ # Removes any condensate/liquid from a steam/gas header
598
+ @self.Constraint(
599
+ self.flowsheet().time,
600
+ doc="Condensate balance."
601
+ )
602
+ def condensate_flow_balance(b, t):
603
+ return (
604
+ b.outlet_condensate_state[t].flow_mol
605
+ ==
606
+ b.mixed_state[t].flow_mol * (1 - b.mixed_state[t].vapor_frac)
607
+ )
608
+
609
+ def _add_energy_balances(self) -> None:
610
+ """Energy balance equations summary.
611
+
612
+ Introduces:
613
+ - ``_liq_out_enth_mol``: Shared molar enthalpy for all liquid outlets,
614
+ including the condensate.
615
+ - ``_vap_out_enth_mol``: Shared molar enthalpy for all vapour outlets,
616
+ including the vent.
617
+
618
+ Constraints:
619
+ - ``inlets_to_mixed_state_energy_balance``: Inlet energy to mixed state (+ heat loss).
620
+ - ``mixed_state_to_outlets_energy_balance``: Mixed state to all outlets.
621
+ - ``molar_enthalpy_equality_eqn``: Common vapour enthalpy across vapour outlets and vent.
622
+ """
623
+ @self.Constraint(self.flowsheet().time, doc="Inlets to mixed state energy balance including heat loss")
624
+ def inlets_to_mixed_state_energy_balance(b, t):
625
+ return (
626
+ b.mixed_state[t].flow_mol * b.mixed_state[t].enth_mol
627
+ + b.heat_loss[t]
628
+ ==
629
+ sum(
630
+ i[t].flow_mol * i[t].enth_mol
631
+ for i in b.inlet_blocks
632
+ )
633
+ )
634
+ @self.Constraint(
635
+ self.flowsheet().time,
636
+ doc="Mixed state to outlets energy balance"
637
+ )
638
+ def mixed_state_to_outlets_energy_balance(b, t):
639
+ return (
640
+ b.mixed_state[t].enth_mol
641
+ *
642
+ sum(
643
+ o[t].flow_mol
644
+ for o in b.outlet_blocks
645
+ )
646
+ ==
647
+ sum(
648
+ o[t].flow_mol * o[t].enth_mol
649
+ for o in b.outlet_blocks
650
+ )
651
+ )
652
+ if self.config.is_liquid_header:
653
+ self._liq_out_enth_mol = Var(
654
+ self.flowsheet().time,
655
+ initialize=42.0 * 18,
656
+ doc="Molar enthalpy of the liquid outlets",
657
+ units=UNIT.J / UNIT.mol
658
+ )
659
+ @self.Constraint(
660
+ self.flowsheet().time,
661
+ self._outlet_supply_blocks + [self.outlet_condensate_state], # exclude vent outlet
662
+ doc="All liquid outlets (incl. condensate) share a common liquid enthalpy",
663
+ )
664
+ def molar_enthalpy_equality_eqn(b, t, o):
665
+ return (
666
+ o[t].enth_mol
667
+ ==
668
+ b._liq_out_enth_mol[t]
669
+ )
670
+ else:
671
+ self._vap_out_enth_mol = Var(
672
+ self.flowsheet().time,
673
+ initialize=2700.0 * 18,
674
+ doc="Molar enthalpy of the vapour outlets",
675
+ units=UNIT.J / UNIT.mol
676
+ )
677
+ @self.Constraint(
678
+ self.flowsheet().time,
679
+ self._outlet_supply_blocks + [self.outlet_vent_state], # exclude condensate outlet
680
+ doc="All vapour outlets (incl. vent) share a common vapour enthalpy",
681
+ )
682
+ def molar_enthalpy_equality_eqn(b, t, o):
683
+ return (
684
+ o[t].enth_mol
685
+ ==
686
+ b._vap_out_enth_mol[t]
687
+ )
688
+
689
+ def _add_momentum_balances(self) -> None:
690
+ """Momentum balance equations summary.
691
+
692
+ Computes the minimum inlet pressure via a sequential smooth minimum and
693
+ sets the mixed-state pressure to that minimum minus ``pressure_loss``,
694
+ then enforces equality to every outlet pressure.
695
+
696
+ Notes:
697
+ - Uses IDAES ``smooth_min`` for differentiable minimum pressure.
698
+ - ``_eps_pressure`` is a smoothing parameter (units of pressure).
699
+ """
700
+ inlet_idx = RangeSet(len(self.inlet_blocks))
701
+ # Get units metadata
702
+ units = self.mixed_state.params.get_metadata()
703
+ # Add variables
704
+ self._minimum_pressure = Var(
705
+ self.flowsheet().time,
706
+ inlet_idx,
707
+ doc="Variable for calculating minimum inlet pressure",
708
+ units=units.get_derived_units("pressure"),
709
+ )
710
+ self._eps_pressure = Param(
711
+ mutable=True,
712
+ initialize=1e-3,
713
+ domain=PositiveReals,
714
+ doc="Smoothing term for minimum inlet pressure",
715
+ units=units.get_derived_units("pressure"),
716
+ )
717
+ # Calculate minimum inlet pressure
718
+ @self.Constraint(
719
+ self.flowsheet().time,
720
+ inlet_idx,
721
+ doc="Calculation for minimum inlet pressure",
722
+ )
723
+ def minimum_pressure_constraint(b, t, i):
724
+ if i == inlet_idx.first():
725
+ return (
726
+ b._minimum_pressure[t, i]
727
+ ==
728
+ (b.inlet_blocks[i - 1][t].pressure)
729
+ )
730
+ else:
731
+ return (
732
+ b._minimum_pressure[t, i]
733
+ ==
734
+ smooth_min(
735
+ b._minimum_pressure[t, i - 1],
736
+ b.inlet_blocks[i - 1][t].pressure,
737
+ b._eps_pressure,
738
+ )
739
+ )
740
+ # Set mixed pressure to minimum inlet pressure minus any pressure loss
741
+ @self.Constraint(
742
+ self.flowsheet().time,
743
+ doc="Pressure equality constraint from minimum inlet to mixed state",
744
+ )
745
+ def mixture_pressure(b, t):
746
+ return (
747
+ b.mixed_state[t].pressure
748
+ ==
749
+ b._minimum_pressure[t, inlet_idx.last()] - b.pressure_loss[t]
750
+ )
751
+ # Set outlet pressures to mixed pressure
752
+ @self.Constraint(
753
+ self.flowsheet().time,
754
+ self.outlet_blocks,
755
+ doc="Pressure equality constraint from mixed state to outlets",
756
+ )
757
+ def pressure_equality_eqn(b, t, o):
758
+ return (
759
+ b.mixed_state[t].pressure
760
+ ==
761
+ o[t].pressure
762
+ )
763
+
764
+ def _add_additional_constraints(self) -> None:
765
+ """Add auxiliary constraints and bounds.
766
+
767
+ - Fix vent vapour fraction to near one (near 100% vapour).
768
+ OR
769
+ - Fix condensate vapour fraction to a small value (near 100% liquid).
770
+ """
771
+ if self.config.is_liquid_header:
772
+ @self.Constraint(self.flowsheet().time, doc="Vent vapour fraction.")
773
+ def vent_vapour_fraction(b, t):
774
+ return (
775
+ b.outlet_vent_state[t].vapor_frac
776
+ ==
777
+ 1 #1 - 1e-6
778
+ )
779
+ else:
780
+ @self.Constraint(self.flowsheet().time, doc="Condensate vapour fraction.")
781
+ def condensate_vapour_fraction(b, t):
782
+ return (
783
+ b.outlet_condensate_state[t].vapor_frac
784
+ ==
785
+ 0 #1e-6
786
+ )
787
+
788
+ def _create_flow_map_references(self):
789
+ """Create a two-key Reference for outlet flows over time and outlet name.
790
+
791
+ Builds a mapping ``(t, outlet_name) -> outlet_state[t].flow_mol`` and exposes it
792
+ as a Reference for compact access to outlet flow splits.
793
+
794
+ Returns:
795
+ pyomo.core.base.reference.Reference: A Reference indexed by ``(time, outlet)``.
796
+ """
797
+ self.outlet_idx = pyo.Set(initialize=self.outlet_list)
798
+ # Map each (t, o) to the outlet state's flow var
799
+ ref_map = {}
800
+ for o in self.outlet_list:
801
+ if o != "vent":
802
+ outlet_state_block = getattr(self, f"{o}_state")
803
+ for t in self.flowsheet().time:
804
+ ref_map[(t, o)] = outlet_state_block[t].flow_mol
805
+
806
+ return Reference(ref_map)
807
+
808
+ def calculate_scaling_factors(self):
809
+ """Assign scaling factors to improve numerical conditioning.
810
+
811
+ Sets scaling factors for performance and auxiliary variables. If present,
812
+ also scales the shared vapour enthalpy variable ``_vap_out_enth_mol``.
813
+ """
814
+ super().calculate_scaling_factors()
815
+ scaling.set_scaling_factor(self.heat_loss, 1e-6)
816
+ scaling.set_scaling_factor(self.pressure_loss, 1e-6)
817
+ scaling.set_scaling_factor(self.balance_flow_mol, 1e-3)
818
+ scaling.set_scaling_factor(self._partial_total_flow_mol, 1e-3)
819
+ if hasattr(self, "_vap_out_enth_mol"):
820
+ scaling.set_scaling_factor(self._vap_out_enth_mol, 1e-6)
821
+
822
+ def _get_stream_table_contents(self, time_point=0):
823
+ """Create a stream table for all inlets and outlets.
824
+
825
+ Args:
826
+ time_point (int | float): Time index at which to extract stream data.
827
+
828
+ Returns:
829
+ pandas.DataFrame: A tabular view suitable for reporting via
830
+ ``create_stream_table_dataframe``.
831
+ """
832
+ io_dict = {}
833
+
834
+ for inlet_name in self.inlet_list:
835
+ io_dict[inlet_name] = getattr(self, inlet_name)
836
+
837
+ for outlet_name in self.outlet_list:
838
+ io_dict[outlet_name] = getattr(self, outlet_name)
839
+
840
+ return create_stream_table_dataframe(io_dict, time_point=time_point)
841
+
842
+ def _get_performance_contents(self, time_point=0, is_full_report=True):
843
+ """Collect performance results for reporting.
844
+
845
+ Args:
846
+ time_point (int | float): Time index at which to report values.
847
+ is_full_report (bool): Flag for full or partial performance report.
848
+
849
+ Returns:
850
+ dict: A report of internal unit model results.
851
+ """
852
+ return (
853
+ {
854
+ "vars": {
855
+ "Heat Loss": self.heat_loss[time_point],
856
+ "Pressure Drop": self.pressure_loss[time_point],
857
+ "Mass Flow": self.mixed_state[time_point].flow_mass,
858
+ "Molar Flow": self.mixed_state[time_point].flow_mol,
859
+ "Balance Flow": self.balance_flow_mol[time_point],
860
+ "Pressure": self.mixed_state[time_point].pressure,
861
+ "Temperature": self.mixed_state[time_point].temperature,
862
+ "Degree of Superheat": self.degree_of_superheat[time_point],
863
+ "Vapour Fraction": self.mixed_state[time_point].vapor_frac,
864
+ "Mass Specific Enthalpy": self.mixed_state[time_point].enth_mass,
865
+ "Molar Specific Enthalpy": self.mixed_state[time_point].enth_mol,
866
+ }
867
+ } if is_full_report else {
868
+ "vars": {
869
+ "Balance Flow": self.balance_flow_mol[time_point],
870
+ "Pressure": self.mixed_state[time_point].pressure,
871
+ "Temperature": self.mixed_state[time_point].temperature,
872
+ "Degree of Superheat": self.degree_of_superheat[time_point],
873
+ }
874
+ }
875
+
876
+ )
877
+
878
+ def initialize(self, *args, **kwargs):
879
+ """Initialize the Header unit using :class:`SimpleHeaderInitializer`.
880
+
881
+ Args:
882
+ *args: Forwarded to ``SimpleHeaderInitializer.initialize``.
883
+ **kwargs: Forwarded to ``SimpleHeaderInitializer.initialize`` (e.g., solver, options).
884
+
885
+ Returns:
886
+ pyomo.opt.results.results_.SolverResults: Results from the initializer's solve.
887
+ """
888
+ init = SimpleHeaderInitializer()
889
+ return init.initialize(self, *args, **kwargs)