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.
- ahuora_builder/__init__.py +0 -0
- ahuora_builder/arc_manager.py +33 -0
- ahuora_builder/build_state.py +57 -0
- ahuora_builder/custom/PIDController.py +494 -0
- ahuora_builder/custom/PySMOModel.py +178 -0
- ahuora_builder/custom/SimpleEffectivenessHX_DH.py +727 -0
- ahuora_builder/custom/__init__.py +0 -0
- ahuora_builder/custom/add_initial_dynamics.py +35 -0
- ahuora_builder/custom/custom_compressor.py +107 -0
- ahuora_builder/custom/custom_cooler.py +33 -0
- ahuora_builder/custom/custom_heat_exchanger.py +183 -0
- ahuora_builder/custom/custom_heat_exchanger_1d.py +258 -0
- ahuora_builder/custom/custom_heater.py +41 -0
- ahuora_builder/custom/custom_pressure_changer.py +34 -0
- ahuora_builder/custom/custom_pump.py +107 -0
- ahuora_builder/custom/custom_separator.py +371 -0
- ahuora_builder/custom/custom_tank.py +133 -0
- ahuora_builder/custom/custom_turbine.py +132 -0
- ahuora_builder/custom/custom_valve.py +300 -0
- ahuora_builder/custom/custom_variable.py +29 -0
- ahuora_builder/custom/direct_steam_injection.py +371 -0
- ahuora_builder/custom/energy/__init__.py +0 -0
- ahuora_builder/custom/energy/acBus.py +280 -0
- ahuora_builder/custom/energy/ac_property_package.py +279 -0
- ahuora_builder/custom/energy/battery.py +170 -0
- ahuora_builder/custom/energy/bus.py +182 -0
- ahuora_builder/custom/energy/energy_mixer.py +195 -0
- ahuora_builder/custom/energy/energy_splitter.py +228 -0
- ahuora_builder/custom/energy/grid.py +173 -0
- ahuora_builder/custom/energy/hydro.py +169 -0
- ahuora_builder/custom/energy/link.py +137 -0
- ahuora_builder/custom/energy/load.py +155 -0
- ahuora_builder/custom/energy/mainDistributionBoard.py +257 -0
- ahuora_builder/custom/energy/power_property_package.py +253 -0
- ahuora_builder/custom/energy/solar.py +176 -0
- ahuora_builder/custom/energy/storage.py +230 -0
- ahuora_builder/custom/energy/storage_wrapper +0 -0
- ahuora_builder/custom/energy/tests/__init__.py +0 -0
- ahuora_builder/custom/energy/tests/test_bus.py +44 -0
- ahuora_builder/custom/energy/tests/test_energy_mixer.py +46 -0
- ahuora_builder/custom/energy/tests/test_mdb.py +49 -0
- ahuora_builder/custom/energy/transformer.py +187 -0
- ahuora_builder/custom/energy/transformer_property_package.py +267 -0
- ahuora_builder/custom/energy/transmissionLine.py +228 -0
- ahuora_builder/custom/energy/wind.py +206 -0
- ahuora_builder/custom/hda_ideal_VLE.py +1341 -0
- ahuora_builder/custom/hda_reaction.py +182 -0
- ahuora_builder/custom/heat_exchanger_1d_wrapper.py +31 -0
- ahuora_builder/custom/integration_block.py +106 -0
- ahuora_builder/custom/inverted.py +81 -0
- ahuora_builder/custom/performance_curves.py +1 -0
- ahuora_builder/custom/reactions/__init__.py +0 -0
- ahuora_builder/custom/reactions/hda_stoich.py +10 -0
- ahuora_builder/custom/simple_separator.py +680 -0
- ahuora_builder/custom/tests/__init__.py +0 -0
- ahuora_builder/custom/tests/test_SimpleEffectivenessHX_DH.py +91 -0
- ahuora_builder/custom/tests/test_custom_tank.py +70 -0
- ahuora_builder/custom/tests/test_direct_steam_injection.py +41 -0
- ahuora_builder/custom/tests/test_simple_separator.py +46 -0
- ahuora_builder/custom/tests/test_waterpipe.py +46 -0
- ahuora_builder/custom/thermal_utility_systems/desuperheater.py +624 -0
- ahuora_builder/custom/thermal_utility_systems/header.py +889 -0
- ahuora_builder/custom/thermal_utility_systems/simple_heat_pump.py +567 -0
- ahuora_builder/custom/thermal_utility_systems/steam_header.py +353 -0
- ahuora_builder/custom/thermal_utility_systems/steam_user.py +944 -0
- ahuora_builder/custom/thermal_utility_systems/temp.py +349 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_desuperheater.py +142 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_header.py +998 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_ntu_hx.py +129 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_simple_heat_pump.py +120 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_steam_header.py +703 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_steam_user.py +277 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_waterpipe.py +36 -0
- ahuora_builder/custom/thermal_utility_systems/tests/test_willans_turbine.py +253 -0
- ahuora_builder/custom/thermal_utility_systems/willans_turbine.py +804 -0
- ahuora_builder/custom/translator.py +129 -0
- ahuora_builder/custom/updated_pressure_changer.py +1404 -0
- ahuora_builder/custom/valve_wrapper.py +38 -0
- ahuora_builder/custom/water_tank_with_units.py +456 -0
- ahuora_builder/diagnostics/__init__.py +0 -0
- ahuora_builder/diagnostics/infeasibilities.py +40 -0
- ahuora_builder/diagnostics/tests/__init__.py +0 -0
- ahuora_builder/diagnostics/tests/test_infeasibilities.py +28 -0
- ahuora_builder/flowsheet_manager.py +542 -0
- ahuora_builder/flowsheet_manager_type.py +20 -0
- ahuora_builder/generate_python_file.py +440 -0
- ahuora_builder/methods/BlockContext.py +84 -0
- ahuora_builder/methods/__init__.py +0 -0
- ahuora_builder/methods/adapter.py +355 -0
- ahuora_builder/methods/adapter_library.py +549 -0
- ahuora_builder/methods/adapter_methods.py +80 -0
- ahuora_builder/methods/expression_parsing.py +105 -0
- ahuora_builder/methods/load_unit_model.py +147 -0
- ahuora_builder/methods/slice_manipulation.py +7 -0
- ahuora_builder/methods/tests/__init__.py +0 -0
- ahuora_builder/methods/tests/test_expression_parsing.py +15 -0
- ahuora_builder/methods/units_handler.py +129 -0
- ahuora_builder/ml_wizard.py +101 -0
- ahuora_builder/port_manager.py +20 -0
- ahuora_builder/properties_manager.py +44 -0
- ahuora_builder/property_package_manager.py +78 -0
- ahuora_builder/solver.py +38 -0
- ahuora_builder/tear_manager.py +98 -0
- ahuora_builder/tests/__init__.py +0 -0
- ahuora_builder/tests/test_generate_python_file/__init__.py +0 -0
- ahuora_builder/tests/test_generate_python_file/configurations/compressor_generated.py +63 -0
- ahuora_builder/tests/test_generate_python_file/configurations/heat_exchanger_generated.py +70 -0
- ahuora_builder/tests/test_generate_python_file/configurations/pump_generated.py +84 -0
- ahuora_builder/tests/test_generate_python_file/configurations/recycle_generated.py +73 -0
- ahuora_builder/tests/test_generate_python_file/test_generate_python_file.py +108 -0
- ahuora_builder/tests/test_solver/__init__.py +0 -0
- ahuora_builder/tests/test_solver/configurations/BT_PR.json +59 -0
- ahuora_builder/tests/test_solver/configurations/BT_PR_solved.json +59 -0
- ahuora_builder/tests/test_solver/configurations/bus.json +99 -0
- ahuora_builder/tests/test_solver/configurations/bus_solved.json +50 -0
- ahuora_builder/tests/test_solver/configurations/compound_separator.json +377 -0
- ahuora_builder/tests/test_solver/configurations/compound_separator_solved.json +374 -0
- ahuora_builder/tests/test_solver/configurations/compressor.json +38 -0
- ahuora_builder/tests/test_solver/configurations/compressor_solved.json +68 -0
- ahuora_builder/tests/test_solver/configurations/constraints.json +44 -0
- ahuora_builder/tests/test_solver/configurations/constraints_solved.json +59 -0
- ahuora_builder/tests/test_solver/configurations/control.json +39 -0
- ahuora_builder/tests/test_solver/configurations/control_solved.json +68 -0
- ahuora_builder/tests/test_solver/configurations/dynamic_tank.json +733 -0
- ahuora_builder/tests/test_solver/configurations/dynamic_tank_solved.json +846 -0
- ahuora_builder/tests/test_solver/configurations/elimination.json +39 -0
- ahuora_builder/tests/test_solver/configurations/elimination_solved.json +68 -0
- ahuora_builder/tests/test_solver/configurations/expressions.json +68 -0
- ahuora_builder/tests/test_solver/configurations/expressions_solved.json +104 -0
- ahuora_builder/tests/test_solver/configurations/header.json +1192 -0
- ahuora_builder/tests/test_solver/configurations/header_solved.json +761 -0
- ahuora_builder/tests/test_solver/configurations/heat_exchanger.json +63 -0
- ahuora_builder/tests/test_solver/configurations/heat_exchanger_solved.json +104 -0
- ahuora_builder/tests/test_solver/configurations/heat_pump.json +137 -0
- ahuora_builder/tests/test_solver/configurations/heat_pump_solved.json +104 -0
- ahuora_builder/tests/test_solver/configurations/machine_learning.json +2156 -0
- ahuora_builder/tests/test_solver/configurations/machine_learning_solved.json +266 -0
- ahuora_builder/tests/test_solver/configurations/mass_flow_tear.json +77 -0
- ahuora_builder/tests/test_solver/configurations/mass_flow_tear_solved.json +68 -0
- ahuora_builder/tests/test_solver/configurations/milk_heater.json +521 -0
- ahuora_builder/tests/test_solver/configurations/milk_heater_solved.json +311 -0
- ahuora_builder/tests/test_solver/configurations/mixer.json +44 -0
- ahuora_builder/tests/test_solver/configurations/mixer_solved.json +86 -0
- ahuora_builder/tests/test_solver/configurations/optimization.json +62 -0
- ahuora_builder/tests/test_solver/configurations/optimization_solved.json +59 -0
- ahuora_builder/tests/test_solver/configurations/propane_heat_pump.json +167 -0
- ahuora_builder/tests/test_solver/configurations/propane_heat_pump_solved.json +158 -0
- ahuora_builder/tests/test_solver/configurations/propane_recycle.json +141 -0
- ahuora_builder/tests/test_solver/configurations/propane_recycle_solved.json +104 -0
- ahuora_builder/tests/test_solver/configurations/pump.json +64 -0
- ahuora_builder/tests/test_solver/configurations/pump_solved.json +59 -0
- ahuora_builder/tests/test_solver/configurations/pump_unit_conversions.json +63 -0
- ahuora_builder/tests/test_solver/configurations/recycle.json +49 -0
- ahuora_builder/tests/test_solver/configurations/recycle_solved.json +50 -0
- ahuora_builder/tests/test_solver/configurations/sb_vapor_frac.json +29 -0
- ahuora_builder/tests/test_solver/configurations/sb_vapor_frac_solved.json +29 -0
- ahuora_builder/tests/test_solver/configurations/solar.json +67 -0
- ahuora_builder/tests/test_solver/configurations/solar_solved.json +50 -0
- ahuora_builder/tests/test_solver/configurations/vapor_frac_target.json +67 -0
- ahuora_builder/tests/test_solver/configurations/vapor_frac_target_solved.json +68 -0
- ahuora_builder/tests/test_solver/test_solve_models.py +250 -0
- ahuora_builder/timing.py +65 -0
- ahuora_builder/types/__init__.py +1 -0
- ahuora_builder/unit_model_manager.py +48 -0
- ahuora_builder-0.1.0.dist-info/METADATA +14 -0
- ahuora_builder-0.1.0.dist-info/RECORD +167 -0
- 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)
|