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,1404 @@
1
+ # THis is unncecessary when https://github.com/IDAES/idaes-pse/pull/1556/ is merged
2
+ # and in the production of the idaes release
3
+ #################################################################################
4
+ # The Institute for the Design of Advanced Energy Systems Integrated Platform
5
+ # Framework (IDAES IP) was produced under the DOE Institute for the
6
+ # Design of Advanced Energy Systems (IDAES).
7
+ #
8
+ # Copyright (c) 2018-2024 by the software owners: The Regents of the
9
+ # University of California, through Lawrence Berkeley National Laboratory,
10
+ # National Technology & Engineering Solutions of Sandia, LLC, Carnegie Mellon
11
+ # University, West Virginia University Research Corporation, et al.
12
+ # All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
13
+ # for full copyright and license information.
14
+ #################################################################################
15
+ """
16
+ Standard IDAES pressure changer model.
17
+ """
18
+ # TODO: Missing docstrings
19
+ # pylint: disable=missing-function-docstring
20
+
21
+ # Changing existing config block attributes
22
+ # pylint: disable=protected-access
23
+
24
+ # Import Python libraries
25
+ from enum import Enum
26
+
27
+ # Import Pyomo libraries
28
+ from pyomo.environ import (
29
+ Block,
30
+ value,
31
+ Var,
32
+ Expression,
33
+ Constraint,
34
+ Reference,
35
+ check_optimal_termination,
36
+ Reals,
37
+ )
38
+ from pyomo.common.config import ConfigBlock, ConfigValue, In, Bool
39
+
40
+ # Import IDAES cores
41
+ from idaes.core import (
42
+ ControlVolume0DBlock,
43
+ declare_process_block_class,
44
+ EnergyBalanceType,
45
+ MomentumBalanceType,
46
+ MaterialBalanceType,
47
+ ProcessBlockData,
48
+ UnitModelBlockData,
49
+ useDefault,
50
+ )
51
+ from idaes.core.util.exceptions import PropertyNotSupportedError, InitializationError
52
+ from idaes.core.util.config import is_physical_parameter_block
53
+ import idaes.logger as idaeslog
54
+ from idaes.core.util import scaling as iscale
55
+ from idaes.core.solvers import get_solver
56
+ from idaes.core.initialization import SingleControlVolumeUnitInitializer
57
+ from idaes.core.util import to_json, from_json, StoreSpec
58
+
59
+
60
+ __author__ = "Emmanuel Ogbe, Andrew Lee"
61
+ _log = idaeslog.getLogger(__name__)
62
+
63
+
64
+ class ThermodynamicAssumption(Enum):
65
+ """
66
+ Enum of supported thermodynamic assumptions.
67
+ """
68
+
69
+ isothermal = 1
70
+ isentropic = 2
71
+ pump = 3
72
+ adiabatic = 4
73
+
74
+
75
+ class IsentropicPressureChangerInitializer(SingleControlVolumeUnitInitializer):
76
+ """
77
+ Initializer for isentropic pressure changer models (and derived types).
78
+
79
+ """
80
+
81
+ def initialization_routine(
82
+ self,
83
+ model: Block,
84
+ ):
85
+ """
86
+ Initialization routine for isentropic pressure changers.
87
+
88
+ This routine starts by initializing the inlet and outlet states as usual,
89
+ using the user provided operating conditions to estimate the outlet state.
90
+ The isentropic state is then initialized at the same conditions as the outlet.
91
+ Next, the pressure changer is solved with an isothermal assumption and fixed efficiency,
92
+ followed by a second solve with the isentropic constraints. Finally, if user-provided
93
+ performance constraints are present, these are activated and the model solved again.
94
+
95
+ Args:
96
+ model: model to be initialized
97
+
98
+ Returns:
99
+ Pyomo solver status object
100
+
101
+ """
102
+ init_log = idaeslog.getInitLogger(
103
+ model.name, self.get_output_level(), tag="unit"
104
+ )
105
+ solve_log = idaeslog.getSolveLogger(
106
+ model.name, self.get_output_level(), tag="unit"
107
+ )
108
+
109
+ # Create solver
110
+ solver = self._get_solver()
111
+
112
+ cv = model.control_volume
113
+
114
+ # check if performance curves exist and are active
115
+ activate_performance_curves = (
116
+ hasattr(model, "performance_curve")
117
+ and model.performance_curve.has_constraints()
118
+ and model.performance_curve.active
119
+ )
120
+ if activate_performance_curves:
121
+ model.performance_curve.deactivate()
122
+ # The performance curves will provide (maybe indirectly) efficiency
123
+ # and/or pressure ratio. To get through the standard isentropic
124
+ # pressure changer init, we'll see if the user provided a guess for
125
+ # pressure ratio or isentropic efficiency and fix them if needed. If
126
+ # not fixed and no guess provided, fill in something reasonable
127
+ # until the performance curves are turned on.
128
+ unfix_eff = {}
129
+ unfix_ratioP = {}
130
+ for t in model.flowsheet().time:
131
+ if not (
132
+ model.ratioP[t].fixed
133
+ or model.deltaP[t].fixed
134
+ or cv.properties_out[t].pressure.fixed
135
+ ):
136
+ if model.config.compressor:
137
+ if not (
138
+ value(model.ratioP[t]) >= 1.01
139
+ and value(model.ratioP[t]) <= 50
140
+ ):
141
+ model.ratioP[t] = 1.8
142
+ else:
143
+ if not (
144
+ value(model.ratioP[t]) >= 0.01
145
+ and value(model.ratioP[t]) <= 0.999
146
+ ):
147
+ model.ratioP[t] = 0.7
148
+ model.ratioP[t].fix()
149
+ unfix_ratioP[t] = True
150
+ if not model.efficiency_isentropic[t].fixed:
151
+ if not (
152
+ value(model.efficiency_isentropic[t]) >= 0.05
153
+ and value(model.efficiency_isentropic[t]) <= 1.0
154
+ ):
155
+ model.efficiency_isentropic[t] = 0.8
156
+ model.efficiency_isentropic[t].fix()
157
+ unfix_eff[t] = True
158
+
159
+ # Initialize state blocks
160
+ self.initialize_control_volume(cv, copy_inlet_state=False)
161
+
162
+ init_log.info_high("Initialization Step 1 Complete.")
163
+ # ---------------------------------------------------------------------
164
+ # Copy outlet state to isentropic state
165
+
166
+ # Map solution from inlet properties to outlet properties
167
+ state = to_json(
168
+ cv.properties_out,
169
+ wts=StoreSpec().value(),
170
+ return_dict=True,
171
+ )
172
+ from_json(
173
+ model.properties_isentropic,
174
+ sd=state,
175
+ wts=StoreSpec().value(only_not_fixed=True),
176
+ )
177
+
178
+ init_log.info_high("Initialization Step 2 Complete.")
179
+
180
+ # ---------------------------------------------------------------------
181
+ # Solve for isothermal conditions
182
+ if isinstance(
183
+ model.properties_isentropic[model.flowsheet().time.first()].temperature,
184
+ Var,
185
+ ):
186
+ model.properties_isentropic[:].temperature.fix()
187
+ elif isinstance(
188
+ model.properties_isentropic[model.flowsheet().time.first()].enth_mol,
189
+ Var,
190
+ ):
191
+ model.properties_isentropic[:].enth_mol.fix()
192
+ elif isinstance(
193
+ model.properties_isentropic[model.flowsheet().time.first()].temperature,
194
+ Expression,
195
+ ):
196
+
197
+ def tmp_rule(blk, t):
198
+ return (
199
+ blk.properties_isentropic[t].temperature
200
+ == blk.control_volume.properties_in[t].temperature
201
+ )
202
+
203
+ model.tmp_init_constraint = Constraint(
204
+ model.flowsheet().time, rule=tmp_rule
205
+ )
206
+
207
+ model.isentropic.deactivate()
208
+
209
+ with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
210
+ res = solver.solve(model, tee=slc.tee)
211
+ init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res)))
212
+
213
+ # Revert changes for isothermal assumption
214
+ if isinstance(
215
+ model.properties_isentropic[model.flowsheet().time.first()].temperature,
216
+ Var,
217
+ ):
218
+ model.properties_isentropic[:].temperature.unfix()
219
+ elif isinstance(
220
+ model.properties_isentropic[model.flowsheet().time.first()].enth_mol,
221
+ Var,
222
+ ):
223
+ model.properties_isentropic[:].enth_mol.unfix()
224
+ elif isinstance(
225
+ model.properties_isentropic[model.flowsheet().time.first()].temperature,
226
+ Expression,
227
+ ):
228
+ model.del_component(model.tmp_init_constraint)
229
+
230
+ model.isentropic.activate()
231
+
232
+ # ---------------------------------------------------------------------
233
+ # Solve unit
234
+ with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
235
+ res = solver.solve(model, tee=slc.tee)
236
+ init_log.info_high("Initialization Step 4 {}.".format(idaeslog.condition(res)))
237
+
238
+ if activate_performance_curves:
239
+ model.performance_curve.activate()
240
+ for t, v in unfix_eff.items():
241
+ if v:
242
+ model.efficiency_isentropic[t].unfix()
243
+ for t, v in unfix_ratioP.items():
244
+ if v:
245
+ model.ratioP[t].unfix()
246
+ with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
247
+ res = solver.solve(model, tee=slc.tee)
248
+ init_log.info_high(f"Initialization Step 5 {idaeslog.condition(res)}.")
249
+
250
+ return res
251
+
252
+
253
+ @declare_process_block_class("IsentropicPerformanceCurve")
254
+ class IsentropicPerformanceCurveData(ProcessBlockData):
255
+ """Block that holds performance curves. Typically, these are in the form of
256
+ constraints that relate head, efficiency, or pressure ratio to volumetric
257
+ or mass flow. Additional variables can be included if needed, such as
258
+ speed. For convenience an option is provided to add head expressions to the
259
+ block. performance curves, and any additional variables, constraints, or
260
+ expressions can be added to this block either via callback provided to the
261
+ configuration, or after the model is constructed."""
262
+
263
+ CONFIG = ProcessBlockData.CONFIG(
264
+ doc="Configuration dictionary for the performance curve block."
265
+ )
266
+ CONFIG.declare(
267
+ "build_callback",
268
+ ConfigValue(
269
+ default=None, doc="Optional callback to add performance curve constraints"
270
+ ),
271
+ )
272
+ CONFIG.declare(
273
+ "build_head_expressions",
274
+ ConfigValue(
275
+ default=True,
276
+ domain=bool,
277
+ doc="If true add expressions for 'head' and 'head_isentropic'. "
278
+ "These expressions can be used in performance curve constraints.",
279
+ ),
280
+ )
281
+
282
+ def has_constraints(self):
283
+ for _ in self.component_data_objects(Constraint):
284
+ return True
285
+ return False
286
+
287
+ def build(self):
288
+ super().build()
289
+ if self.config.build_head_expressions:
290
+ try:
291
+
292
+ @self.Expression(self.flowsheet().time)
293
+ def head_isentropic(self, t): # units are energy/mass
294
+ b = self.parent_block()
295
+ if hasattr(b.control_volume.properties_in[t], "flow_mass"):
296
+ return (
297
+ b.work_isentropic[t]
298
+ / b.control_volume.properties_in[t].flow_mass
299
+ )
300
+ else:
301
+ return (
302
+ b.work_isentropic[t]
303
+ / b.control_volume.properties_in[t].flow_mol
304
+ / b.control_volume.properties_in[t].mw
305
+ )
306
+
307
+ @self.Expression(self.flowsheet().time)
308
+ def head(self, t): # units are energy/mass
309
+ b = self.parent_block()
310
+ if hasattr(b.control_volume.properties_in[t], "flow_mass"):
311
+ return (
312
+ b.work_mechanical[t]
313
+ / b.control_volume.properties_in[t].flow_mass
314
+ )
315
+ else:
316
+ return (
317
+ b.work_mechanical[t]
318
+ / b.control_volume.properties_in[t].flow_mol
319
+ / b.control_volume.properties_in[t].mw
320
+ )
321
+
322
+ except PropertyNotSupportedError:
323
+ _log.exception(
324
+ "flow_mass or flow_mol and mw are not supported by the "
325
+ "property package but are required for isentropic pressure"
326
+ " changer head calculation"
327
+ )
328
+ raise
329
+
330
+ if self.config.build_callback is not None:
331
+ self.config.build_callback(self)
332
+
333
+
334
+ @declare_process_block_class("PressureChanger")
335
+ class PressureChangerData(UnitModelBlockData):
336
+ """
337
+ Standard Compressor/Expander Unit Model Class
338
+ """
339
+
340
+ CONFIG = UnitModelBlockData.CONFIG()
341
+
342
+ CONFIG.declare(
343
+ "material_balance_type",
344
+ ConfigValue(
345
+ default=MaterialBalanceType.useDefault,
346
+ domain=In(MaterialBalanceType),
347
+ description="Material balance construction flag",
348
+ doc="""Indicates what type of mass balance should be constructed,
349
+ **default** - MaterialBalanceType.useDefault.
350
+ **Valid values:** {
351
+ **MaterialBalanceType.useDefault - refer to property package for default
352
+ balance type
353
+ **MaterialBalanceType.none** - exclude material balances,
354
+ **MaterialBalanceType.componentPhase** - use phase component balances,
355
+ **MaterialBalanceType.componentTotal** - use total component balances,
356
+ **MaterialBalanceType.elementTotal** - use total element balances,
357
+ **MaterialBalanceType.total** - use total material balance.}""",
358
+ ),
359
+ )
360
+ CONFIG.declare(
361
+ "energy_balance_type",
362
+ ConfigValue(
363
+ default=EnergyBalanceType.useDefault,
364
+ domain=In(EnergyBalanceType),
365
+ description="Energy balance construction flag",
366
+ doc="""Indicates what type of energy balance should be constructed,
367
+ **default** - EnergyBalanceType.useDefault.
368
+ **Valid values:** {
369
+ **EnergyBalanceType.useDefault - refer to property package for default
370
+ balance type
371
+ **EnergyBalanceType.none** - exclude energy balances,
372
+ **EnergyBalanceType.enthalpyTotal** - single enthalpy balance for material,
373
+ **EnergyBalanceType.enthalpyPhase** - enthalpy balances for each phase,
374
+ **EnergyBalanceType.energyTotal** - single energy balance for material,
375
+ **EnergyBalanceType.energyPhase** - energy balances for each phase.}""",
376
+ ),
377
+ )
378
+ CONFIG.declare(
379
+ "momentum_balance_type",
380
+ ConfigValue(
381
+ default=MomentumBalanceType.pressureTotal,
382
+ domain=In(MomentumBalanceType),
383
+ description="Momentum balance construction flag",
384
+ doc="""Indicates what type of momentum balance should be
385
+ constructed, **default** - MomentumBalanceType.pressureTotal.
386
+ **Valid values:** {
387
+ **MomentumBalanceType.none** - exclude momentum balances,
388
+ **MomentumBalanceType.pressureTotal** - single pressure balance for material,
389
+ **MomentumBalanceType.pressurePhase** - pressure balances for each phase,
390
+ **MomentumBalanceType.momentumTotal** - single momentum balance for material,
391
+ **MomentumBalanceType.momentumPhase** - momentum balances for each phase.}""",
392
+ ),
393
+ )
394
+ CONFIG.declare(
395
+ "has_phase_equilibrium",
396
+ ConfigValue(
397
+ default=False,
398
+ domain=Bool,
399
+ description="Phase equilibrium construction flag",
400
+ doc="""Indicates whether terms for phase equilibrium should be
401
+ constructed, **default** = False.
402
+ **Valid values:** {
403
+ **True** - include phase equilibrium terms
404
+ **False** - exclude phase equilibrium terms.}""",
405
+ ),
406
+ )
407
+ CONFIG.declare(
408
+ "compressor",
409
+ ConfigValue(
410
+ default=True,
411
+ domain=Bool,
412
+ description="Compressor flag",
413
+ doc="""Indicates whether this unit should be considered a
414
+ compressor (True (default), pressure increase) or an expander
415
+ (False, pressure decrease).""",
416
+ ),
417
+ )
418
+ CONFIG.declare(
419
+ "thermodynamic_assumption",
420
+ ConfigValue(
421
+ default=ThermodynamicAssumption.isothermal,
422
+ domain=In(ThermodynamicAssumption),
423
+ description="Thermodynamic assumption to use",
424
+ doc="""Flag to set the thermodynamic assumption to use for the unit.
425
+ - ThermodynamicAssumption.isothermal (default)
426
+ - ThermodynamicAssumption.isentropic
427
+ - ThermodynamicAssumption.pump
428
+ - ThermodynamicAssumption.adiabatic""",
429
+ ),
430
+ )
431
+ CONFIG.declare(
432
+ "property_package",
433
+ ConfigValue(
434
+ default=useDefault,
435
+ domain=is_physical_parameter_block,
436
+ description="Property package to use for control volume",
437
+ doc="""Property parameter object used to define property
438
+ calculations, **default** - useDefault.
439
+ **Valid values:** {
440
+ **useDefault** - use default package from parent model or flowsheet,
441
+ **PropertyParameterObject** - a PropertyParameterBlock object.}""",
442
+ ),
443
+ )
444
+ CONFIG.declare(
445
+ "property_package_args",
446
+ ConfigBlock(
447
+ implicit=True,
448
+ description="Arguments to use for constructing property packages",
449
+ doc="""A ConfigBlock with arguments to be passed to a property
450
+ block(s) and used when constructing these,
451
+ **default** - None.
452
+ **Valid values:** {
453
+ see property package for documentation.}""",
454
+ ),
455
+ )
456
+ CONFIG.declare(
457
+ "support_isentropic_performance_curves",
458
+ ConfigValue(
459
+ default=False,
460
+ domain=Bool,
461
+ doc="Include a block for performance curves, configure via"
462
+ " isentropic_performance_curves.",
463
+ ),
464
+ )
465
+ CONFIG.declare(
466
+ "isentropic_performance_curves",
467
+ IsentropicPerformanceCurveData.CONFIG(),
468
+ # doc included in IsentropicPerformanceCurveData
469
+ )
470
+
471
+ def build(self):
472
+ """
473
+
474
+ Args:
475
+ None
476
+
477
+ Returns:
478
+ None
479
+ """
480
+ # Call UnitModel.build
481
+ super().build()
482
+
483
+ # Add a control volume to the unit including setting up dynamics.
484
+ self.control_volume = ControlVolume0DBlock(
485
+ dynamic=self.config.dynamic,
486
+ has_holdup=self.config.has_holdup,
487
+ property_package=self.config.property_package,
488
+ property_package_args=self.config.property_package_args,
489
+ )
490
+
491
+ # Add geometry variables to control volume
492
+ if self.config.has_holdup:
493
+ self.control_volume.add_geometry()
494
+
495
+ # Add inlet and outlet state blocks to control volume
496
+ self.control_volume.add_state_blocks(
497
+ has_phase_equilibrium=self.config.has_phase_equilibrium
498
+ )
499
+
500
+ # Add mass balance
501
+ # Set has_equilibrium is False for now
502
+ # TO DO; set has_equilibrium to True
503
+ self.control_volume.add_material_balances(
504
+ balance_type=self.config.material_balance_type,
505
+ has_phase_equilibrium=self.config.has_phase_equilibrium,
506
+ )
507
+
508
+ # Add energy balance
509
+ eb = self.control_volume.add_energy_balances(
510
+ balance_type=self.config.energy_balance_type, has_work_transfer=True
511
+ )
512
+
513
+ # add momentum balance
514
+ self.control_volume.add_momentum_balances(
515
+ balance_type=self.config.momentum_balance_type, has_pressure_change=True
516
+ )
517
+
518
+ # Add Ports
519
+ self.add_inlet_port()
520
+ self.add_outlet_port()
521
+
522
+ # Set Unit Geometry and holdup Volume
523
+ if self.config.has_holdup is True:
524
+ self.volume = Reference(self.control_volume.volume[:])
525
+
526
+ # Construct performance equations
527
+ # Set references to balance terms at unit level
528
+ # Add Work transfer variable 'work'
529
+ # If the 'work' variable wasn't already built on the control volume but is needed, create it now.
530
+ if (
531
+ not hasattr(self.control_volume, "work")
532
+ and self.config.thermodynamic_assumption == ThermodynamicAssumption.pump
533
+ and eb is None
534
+ ):
535
+ units = (
536
+ self.control_volume.config.property_package.get_metadata().get_derived_units
537
+ )
538
+ self.control_volume.work = Var(
539
+ self.flowsheet().time,
540
+ domain=Reals,
541
+ initialize=0.0,
542
+ doc="Work transferred into control volume",
543
+ units=units("power"),
544
+ )
545
+ self.work_mechanical = Reference(self.control_volume.work[:])
546
+
547
+ # Add Momentum balance variable 'deltaP'
548
+ self.deltaP = Reference(self.control_volume.deltaP[:])
549
+
550
+ # Performance Variables
551
+ self.ratioP = Var(self.flowsheet().time, initialize=1.0, doc="Pressure Ratio")
552
+
553
+ # Pressure Ratio
554
+ @self.Constraint(self.flowsheet().time, doc="Pressure ratio constraint")
555
+ def ratioP_calculation(self, t):
556
+ return (
557
+ self.ratioP[t] * self.control_volume.properties_in[t].pressure
558
+ == self.control_volume.properties_out[t].pressure
559
+ )
560
+
561
+ # Construct equations for thermodynamic assumption
562
+ if self.config.thermodynamic_assumption == ThermodynamicAssumption.isothermal:
563
+ self.add_isothermal()
564
+ elif self.config.thermodynamic_assumption == ThermodynamicAssumption.isentropic:
565
+ self.add_isentropic()
566
+ elif self.config.thermodynamic_assumption == ThermodynamicAssumption.pump:
567
+ self.add_pump()
568
+ elif self.config.thermodynamic_assumption == ThermodynamicAssumption.adiabatic:
569
+ self.add_adiabatic()
570
+
571
+ def add_pump(self):
572
+ """
573
+ Add constraints for the incompressible fluid assumption
574
+
575
+ Args:
576
+ None
577
+
578
+ Returns:
579
+ None
580
+ """
581
+ units_meta = self.control_volume.config.property_package.get_metadata()
582
+
583
+ self.work_fluid = Var(
584
+ self.flowsheet().time,
585
+ initialize=1.0,
586
+ doc="Work required to increase the pressure of the liquid",
587
+ units=units_meta.get_derived_units("power"),
588
+ )
589
+ self.efficiency_pump = Var(
590
+ self.flowsheet().time, initialize=1.0, doc="Pump efficiency"
591
+ )
592
+
593
+ @self.Constraint(self.flowsheet().time, doc="Pump fluid work constraint")
594
+ def fluid_work_calculation(self, t):
595
+ return self.work_fluid[t] == (
596
+ (
597
+ self.control_volume.properties_out[t].pressure
598
+ - self.control_volume.properties_in[t].pressure
599
+ )
600
+ * self.control_volume.properties_out[t].flow_vol
601
+ )
602
+
603
+ # Actual work
604
+ @self.Constraint(
605
+ self.flowsheet().time, doc="Actual mechanical work calculation"
606
+ )
607
+ def actual_work(self, t):
608
+ if self.config.compressor:
609
+ return self.work_fluid[t] == (
610
+ self.work_mechanical[t] * self.efficiency_pump[t]
611
+ )
612
+ else:
613
+ return self.work_mechanical[t] == (
614
+ self.work_fluid[t] * self.efficiency_pump[t]
615
+ )
616
+
617
+ def add_isothermal(self):
618
+ """
619
+ Add constraints for isothermal assumption.
620
+
621
+ Args:
622
+ None
623
+
624
+ Returns:
625
+ None
626
+ """
627
+
628
+ # Isothermal constraint
629
+ @self.Constraint(
630
+ self.flowsheet().time,
631
+ doc="For isothermal condition: Equate inlet and " "outlet temperature",
632
+ )
633
+ def isothermal(self, t):
634
+ return (
635
+ self.control_volume.properties_in[t].temperature
636
+ == self.control_volume.properties_out[t].temperature
637
+ )
638
+
639
+ def add_adiabatic(self):
640
+ """
641
+ Add constraints for adiabatic assumption.
642
+
643
+ Args:
644
+ None
645
+
646
+ Returns:
647
+ None
648
+ """
649
+
650
+ @self.Constraint(self.flowsheet().time)
651
+ def zero_work_equation(self, t):
652
+ return self.control_volume.work[t] == 0
653
+
654
+ def add_isentropic(self):
655
+ """
656
+ Add constraints for isentropic assumption.
657
+
658
+ Args:
659
+ None
660
+
661
+ Returns:
662
+ None
663
+ """
664
+ units_meta = self.control_volume.config.property_package.get_metadata()
665
+
666
+ # Get indexing sets from control volume
667
+ # Add isentropic variables
668
+ self.efficiency_isentropic = Var(
669
+ self.flowsheet().time,
670
+ initialize=0.8,
671
+ doc="Efficiency with respect to an isentropic process [-]",
672
+ )
673
+ self.work_isentropic = Var(
674
+ self.flowsheet().time,
675
+ initialize=0.0,
676
+ doc="Work input to unit if isentropic process",
677
+ units=units_meta.get_derived_units("power"),
678
+ )
679
+
680
+ # Build isentropic state block
681
+ tmp_dict = dict(**self.config.property_package_args)
682
+ tmp_dict["has_phase_equilibrium"] = self.config.has_phase_equilibrium
683
+ tmp_dict["defined_state"] = False
684
+
685
+ self.properties_isentropic = self.config.property_package.build_state_block(
686
+ self.flowsheet().time, doc="isentropic properties at outlet", **tmp_dict
687
+ )
688
+
689
+ # Connect isentropic state block properties
690
+ @self.Constraint(
691
+ self.flowsheet().time, doc="Pressure for isentropic calculations"
692
+ )
693
+ def isentropic_pressure(self, t):
694
+ return (
695
+ self.properties_isentropic[t].pressure
696
+ == self.control_volume.properties_out[t].pressure
697
+ )
698
+
699
+ # This assumes isentropic composition is the same as outlet
700
+ self.add_state_material_balances(
701
+ self.config.material_balance_type,
702
+ self.properties_isentropic,
703
+ self.control_volume.properties_out,
704
+ )
705
+
706
+ # This assumes isentropic entropy is the same as inlet
707
+ @self.Constraint(self.flowsheet().time, doc="Isentropic assumption")
708
+ def isentropic(self, t):
709
+ return (
710
+ self.properties_isentropic[t].entr_mol
711
+ == self.control_volume.properties_in[t].entr_mol
712
+ )
713
+
714
+ # Isentropic work
715
+ @self.Constraint(
716
+ self.flowsheet().time, doc="Calculate work of isentropic process"
717
+ )
718
+ def isentropic_energy_balance(self, t):
719
+ return self.work_isentropic[t] == (
720
+ sum(
721
+ self.properties_isentropic[t].get_enthalpy_flow_terms(p)
722
+ for p in self.properties_isentropic.phase_list
723
+ )
724
+ - sum(
725
+ self.control_volume.properties_in[t].get_enthalpy_flow_terms(p)
726
+ for p in self.control_volume.properties_in.phase_list
727
+ )
728
+ )
729
+
730
+ # Actual work
731
+ @self.Constraint(
732
+ self.flowsheet().time, doc="Actual mechanical work calculation"
733
+ )
734
+ def actual_work(self, t):
735
+ if self.config.compressor:
736
+ return self.work_isentropic[t] == (
737
+ self.work_mechanical[t] * self.efficiency_isentropic[t]
738
+ )
739
+ else:
740
+ return self.work_mechanical[t] == (
741
+ self.work_isentropic[t] * self.efficiency_isentropic[t]
742
+ )
743
+
744
+ if self.config.support_isentropic_performance_curves:
745
+ self.performance_curve = IsentropicPerformanceCurve(
746
+ **self.config.isentropic_performance_curves
747
+ )
748
+
749
+ def model_check(blk):
750
+ """
751
+ Check that pressure change matches with compressor argument (i.e. if
752
+ compressor = True, pressure should increase or work should be positive)
753
+
754
+ Args:
755
+ None
756
+
757
+ Returns:
758
+ None
759
+ """
760
+ if blk.config.compressor:
761
+ # Compressor
762
+ # Check that pressure does not decrease
763
+ if any(
764
+ blk.deltaP[t].fixed and (value(blk.deltaP[t]) < 0.0)
765
+ for t in blk.flowsheet().time
766
+ ):
767
+ _log.warning("{} Compressor set with negative deltaP.".format(blk.name))
768
+ if any(
769
+ blk.ratioP[t].fixed and (value(blk.ratioP[t]) < 1.0)
770
+ for t in blk.flowsheet().time
771
+ ):
772
+ _log.warning(
773
+ "{} Compressor set with ratioP less than 1.".format(blk.name)
774
+ )
775
+ if any(
776
+ blk.control_volume.properties_out[t].pressure.fixed
777
+ and (
778
+ value(blk.control_volume.properties_in[t].pressure)
779
+ > value(blk.control_volume.properties_out[t].pressure)
780
+ )
781
+ for t in blk.flowsheet().time
782
+ ):
783
+ _log.warning(
784
+ "{} Compressor set with pressure decrease.".format(blk.name)
785
+ )
786
+ # Check that work is not negative
787
+ if any(
788
+ blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) < 0.0)
789
+ for t in blk.flowsheet().time
790
+ ):
791
+ _log.warning(
792
+ "{} Compressor maybe set with negative work.".format(blk.name)
793
+ )
794
+ else:
795
+ # Expander
796
+ # Check that pressure does not increase
797
+ if any(
798
+ blk.deltaP[t].fixed and (value(blk.deltaP[t]) > 0.0)
799
+ for t in blk.flowsheet().time
800
+ ):
801
+ _log.warning(
802
+ "{} Expander/turbine set with positive deltaP.".format(blk.name)
803
+ )
804
+ if any(
805
+ blk.ratioP[t].fixed and (value(blk.ratioP[t]) > 1.0)
806
+ for t in blk.flowsheet().time
807
+ ):
808
+ _log.warning(
809
+ "{} Expander/turbine set with ratioP greater "
810
+ "than 1.".format(blk.name)
811
+ )
812
+ if any(
813
+ blk.control_volume.properties_out[t].pressure.fixed
814
+ and (
815
+ value(blk.control_volume.properties_in[t].pressure)
816
+ < value(blk.control_volume.properties_out[t].pressure)
817
+ )
818
+ for t in blk.flowsheet().time
819
+ ):
820
+ _log.warning(
821
+ "{} Expander/turbine maybe set with pressure "
822
+ "increase.".format(blk.name),
823
+ )
824
+ # Check that work is not positive
825
+ if any(
826
+ blk.work_mechanical[t].fixed and (value(blk.work_mechanical[t]) > 0.0)
827
+ for t in blk.flowsheet().time
828
+ ):
829
+ _log.warning(
830
+ "{} Expander/turbine set with positive work.".format(blk.name)
831
+ )
832
+
833
+ # Run holdup block model checks
834
+ blk.control_volume.model_check()
835
+
836
+ # Run model checks on isentropic property block
837
+ try:
838
+ for t in blk.flowsheet().time:
839
+ blk.properties_in[t].model_check()
840
+ except AttributeError:
841
+ pass
842
+
843
+ def initialize_build(
844
+ blk,
845
+ state_args=None,
846
+ routine=None,
847
+ outlvl=idaeslog.NOTSET,
848
+ solver=None,
849
+ optarg=None,
850
+ ):
851
+ """
852
+ General wrapper for pressure changer initialization routines
853
+
854
+ Keyword Arguments:
855
+ routine : str stating which initialization routine to execute
856
+ * None - use routine matching thermodynamic_assumption
857
+ * 'isentropic' - use isentropic initialization routine
858
+ * 'isothermal' - use isothermal initialization routine
859
+ state_args : a dict of arguments to be passed to the property
860
+ package(s) to provide an initial state for
861
+ initialization (see documentation of the specific
862
+ property package) (default = {}).
863
+ outlvl : sets output level of initialization routine
864
+ optarg : solver options dictionary object (default=None, use
865
+ default solver options)
866
+ solver : str indicating which solver to use during
867
+ initialization (default = None, use default solver)
868
+
869
+ Returns:
870
+ None
871
+ """
872
+ if routine is None:
873
+ # Use routine for specific type of unit
874
+ routine = blk.config.thermodynamic_assumption
875
+
876
+ # Call initialization routine
877
+ if routine is ThermodynamicAssumption.isentropic:
878
+ blk.init_isentropic(
879
+ state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg
880
+ )
881
+ elif routine is ThermodynamicAssumption.adiabatic:
882
+ blk.init_adiabatic(
883
+ state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg
884
+ )
885
+ else:
886
+ # Call the general initialization routine in UnitModelBlockData
887
+ super().initialize_build(
888
+ state_args=state_args, outlvl=outlvl, solver=solver, optarg=optarg
889
+ )
890
+
891
+ def init_adiabatic(blk, state_args, outlvl, solver, optarg):
892
+ """
893
+ Initialization routine for adiabatic pressure changers.
894
+
895
+ Keyword Arguments:
896
+ state_args : a dict of arguments to be passed to the property
897
+ package(s) to provide an initial state for
898
+ initialization (see documentation of the specific
899
+ property package) (default = {}).
900
+ outlvl : sets output level of initialization routine
901
+ optarg : solver options dictionary object (default={})
902
+ solver : str indicating which solver to use during
903
+ initialization (default = None)
904
+
905
+ Returns:
906
+ None
907
+ """
908
+ init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
909
+ solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
910
+
911
+ # Create solver
912
+ opt = get_solver(solver, optarg)
913
+
914
+ cv = blk.control_volume
915
+ t0 = blk.flowsheet().time.first()
916
+
917
+ if state_args is None:
918
+ state_args = {}
919
+ state_dict = cv.properties_in[t0].define_port_members()
920
+
921
+ for k in state_dict.keys():
922
+ if state_dict[k].is_indexed():
923
+ state_args[k] = {}
924
+ for m in state_dict[k].keys():
925
+ state_args[k][m] = state_dict[k][m].value
926
+ else:
927
+ state_args[k] = state_dict[k].value
928
+
929
+ # Initialize state blocks
930
+ flags = cv.properties_in.initialize(
931
+ outlvl=outlvl,
932
+ optarg=optarg,
933
+ solver=solver,
934
+ hold_state=True,
935
+ state_args=state_args,
936
+ )
937
+
938
+ # Get initialisation guesses for outlet state
939
+ state_args_out = {}
940
+ # refresh state_dict (may have changed during initialization)
941
+ state_dict = cv.properties_in[t0].define_port_members()
942
+ for k in state_args:
943
+ if k == "pressure" and k not in state_args_out:
944
+ # Work out how to estimate outlet pressure
945
+ if cv.properties_out[t0].pressure.fixed:
946
+ # Fixed outlet pressure, use this value
947
+ state_args_out[k] = value(cv.properties_out[t0].pressure)
948
+ elif blk.deltaP[t0].fixed:
949
+ state_args_out[k] = value(
950
+ cv.properties_in[t0].pressure + blk.deltaP[t0]
951
+ )
952
+ elif blk.ratioP[t0].fixed:
953
+ state_args_out[k] = value(
954
+ cv.properties_in[t0].pressure * blk.ratioP[t0]
955
+ )
956
+ else:
957
+ # Not obvious what to do, use inlet state
958
+ state_args_out[k] = value(cv.properties_in[t0].pressure)
959
+ elif k not in state_args_out:
960
+ # use the inlet state as a guess for the outlet state
961
+ if state_dict[k].is_indexed():
962
+ state_args_out[k] = {}
963
+ for m in state_dict[k].keys():
964
+ state_args_out[k][m] = state_dict[k][m].value
965
+ else:
966
+ state_args_out[k] = state_dict[k].value
967
+
968
+ cv.properties_out.initialize(
969
+ outlvl=outlvl,
970
+ optarg=optarg,
971
+ solver=solver,
972
+ hold_state=False,
973
+ state_args=state_args_out,
974
+ )
975
+ init_log.info_high("Initialization Step 1 Complete.")
976
+
977
+ # ---------------------------------------------------------------------
978
+ # Solve unit
979
+ with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
980
+ res = opt.solve(blk, tee=slc.tee)
981
+ init_log.info_high("Initialization Step 2 {}.".format(idaeslog.condition(res)))
982
+
983
+ # ---------------------------------------------------------------------
984
+ # Release Inlet state
985
+ blk.control_volume.release_state(flags, outlvl)
986
+
987
+ if not check_optimal_termination(res):
988
+ raise InitializationError(
989
+ f"{blk.name} failed to initialize successfully. Please check "
990
+ f"the output logs for more information."
991
+ )
992
+
993
+ init_log.info(f"Initialization Complete: {idaeslog.condition(res)}")
994
+
995
+ def init_isentropic(blk, state_args, outlvl, solver, optarg):
996
+ """
997
+ Initialization routine for isentropic pressure changers.
998
+
999
+ Keyword Arguments:
1000
+ state_args : a dict of arguments to be passed to the property
1001
+ package(s) to provide an initial state for
1002
+ initialization (see documentation of the specific
1003
+ property package) (default = {}).
1004
+ outlvl : sets output level of initialization routine
1005
+ optarg : solver options dictionary object (default={})
1006
+ solver : str indicating which solver to use during
1007
+ initialization (default = None)
1008
+
1009
+ Returns:
1010
+ None
1011
+ """
1012
+ init_log = idaeslog.getInitLogger(blk.name, outlvl, tag="unit")
1013
+ solve_log = idaeslog.getSolveLogger(blk.name, outlvl, tag="unit")
1014
+
1015
+ # Create solver
1016
+ opt = get_solver(solver, optarg)
1017
+
1018
+ cv = blk.control_volume
1019
+ t0 = blk.flowsheet().time.first()
1020
+
1021
+ # performance curves exist and are active so initialize with them
1022
+ activate_performance_curves = (
1023
+ hasattr(blk, "performance_curve")
1024
+ and blk.performance_curve.has_constraints()
1025
+ and blk.performance_curve.active
1026
+ )
1027
+ if activate_performance_curves:
1028
+ blk.performance_curve.deactivate()
1029
+ # The performance curves will provide (maybe indirectly) efficiency
1030
+ # and/or pressure ratio. To get through the standard isentropic
1031
+ # pressure changer init, we'll see if the user provided a guess for
1032
+ # pressure ratio or isentropic efficiency and fix them if needed. If
1033
+ # not fixed and no guess provided, fill in something reasonable
1034
+ # until the performance curves are turned on.
1035
+ unfix_eff = {}
1036
+ unfix_ratioP = {}
1037
+ for t in blk.flowsheet().time:
1038
+ if not (
1039
+ blk.ratioP[t].fixed
1040
+ or blk.deltaP[t].fixed
1041
+ or cv.properties_out[t].pressure.fixed
1042
+ ):
1043
+ if blk.config.compressor:
1044
+ if not (
1045
+ value(blk.ratioP[t]) >= 1.01 and value(blk.ratioP[t]) <= 50
1046
+ ):
1047
+ blk.ratioP[t] = 1.8
1048
+ else:
1049
+ if not (
1050
+ value(blk.ratioP[t]) >= 0.01
1051
+ and value(blk.ratioP[t]) <= 0.999
1052
+ ):
1053
+ blk.ratioP[t] = 0.7
1054
+ blk.ratioP[t].fix()
1055
+ unfix_ratioP[t] = True
1056
+ if not blk.efficiency_isentropic[t].fixed:
1057
+ if not (
1058
+ value(blk.efficiency_isentropic[t]) >= 0.05
1059
+ and value(blk.efficiency_isentropic[t]) <= 1.0
1060
+ ):
1061
+ blk.efficiency_isentropic[t] = 0.8
1062
+ blk.efficiency_isentropic[t].fix()
1063
+ unfix_eff[t] = True
1064
+
1065
+ if state_args is None:
1066
+ state_args = {}
1067
+ state_dict = cv.properties_in[t0].define_port_members()
1068
+
1069
+ for k in state_dict.keys():
1070
+ if state_dict[k].is_indexed():
1071
+ state_args[k] = {}
1072
+ for m in state_dict[k].keys():
1073
+ state_args[k][m] = state_dict[k][m].value
1074
+ else:
1075
+ state_args[k] = state_dict[k].value
1076
+
1077
+ # Initialize state blocks
1078
+ flags = cv.properties_in.initialize(
1079
+ outlvl=outlvl,
1080
+ optarg=optarg,
1081
+ solver=solver,
1082
+ hold_state=True,
1083
+ state_args=state_args,
1084
+ )
1085
+
1086
+ # Get initialisation guesses for outlet and isentropic states
1087
+ state_args_out = {}
1088
+ # refresh state_dict (may have changed during initialization)
1089
+ state_dict = cv.properties_in[t0].define_port_members()
1090
+ for k in state_args:
1091
+ if k == "pressure" and k not in state_args_out:
1092
+ # Work out how to estimate outlet pressure
1093
+ if cv.properties_out[t0].pressure.fixed:
1094
+ # Fixed outlet pressure, use this value
1095
+ state_args_out[k] = value(cv.properties_out[t0].pressure)
1096
+ elif blk.deltaP[t0].fixed:
1097
+ state_args_out[k] = value(
1098
+ cv.properties_in[t0].pressure + blk.deltaP[t0]
1099
+ )
1100
+ elif blk.ratioP[t0].fixed:
1101
+ state_args_out[k] = value(
1102
+ cv.properties_in[t0].pressure * blk.ratioP[t0]
1103
+ )
1104
+ else:
1105
+ # Not obvious what to do, use inlet state
1106
+ state_args_out[k] = value(cv.properties_in[t0].pressure)
1107
+ elif k not in state_args_out:
1108
+ # use the inlet state as a guess for the outlet state
1109
+ if state_dict[k].is_indexed():
1110
+ state_args_out[k] = {}
1111
+ for m in state_dict[k].keys():
1112
+ state_args_out[k][m] = state_dict[k][m].value
1113
+ else:
1114
+ state_args_out[k] = state_dict[k].value
1115
+
1116
+ cv.properties_out.initialize(
1117
+ outlvl=outlvl,
1118
+ optarg=optarg,
1119
+ solver=solver,
1120
+ hold_state=False,
1121
+ state_args=state_args_out,
1122
+ )
1123
+
1124
+ init_log.info_high("Initialization Step 1 Complete.")
1125
+ # ---------------------------------------------------------------------
1126
+ # Initialize Isentropic block
1127
+
1128
+ blk.properties_isentropic.initialize(
1129
+ outlvl=outlvl,
1130
+ optarg=optarg,
1131
+ solver=solver,
1132
+ state_args=state_args_out,
1133
+ )
1134
+
1135
+ init_log.info_high("Initialization Step 2 Complete.")
1136
+
1137
+ # ---------------------------------------------------------------------
1138
+ # Solve for isothermal conditions
1139
+ if isinstance(
1140
+ blk.properties_isentropic[blk.flowsheet().time.first()].temperature,
1141
+ Var,
1142
+ ):
1143
+ blk.properties_isentropic[:].temperature.fix()
1144
+ elif isinstance(
1145
+ blk.properties_isentropic[blk.flowsheet().time.first()].enth_mol,
1146
+ Var,
1147
+ ):
1148
+ blk.properties_isentropic[:].enth_mol.fix()
1149
+ elif isinstance(
1150
+ blk.properties_isentropic[blk.flowsheet().time.first()].temperature,
1151
+ Expression,
1152
+ ):
1153
+
1154
+ def tmp_rule(self, t):
1155
+ return (
1156
+ blk.properties_isentropic[t].temperature
1157
+ == blk.control_volume.properties_in[t].temperature
1158
+ )
1159
+
1160
+ blk.tmp_init_constraint = Constraint(blk.flowsheet().time, rule=tmp_rule)
1161
+
1162
+ blk.isentropic.deactivate()
1163
+
1164
+ with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
1165
+ res = opt.solve(blk, tee=slc.tee)
1166
+ init_log.info_high("Initialization Step 3 {}.".format(idaeslog.condition(res)))
1167
+
1168
+ if isinstance(
1169
+ blk.properties_isentropic[blk.flowsheet().time.first()].temperature,
1170
+ Var,
1171
+ ):
1172
+ blk.properties_isentropic[:].temperature.unfix()
1173
+ elif isinstance(
1174
+ blk.properties_isentropic[blk.flowsheet().time.first()].enth_mol,
1175
+ Var,
1176
+ ):
1177
+ blk.properties_isentropic[:].enth_mol.unfix()
1178
+ elif isinstance(
1179
+ blk.properties_isentropic[blk.flowsheet().time.first()].temperature,
1180
+ Expression,
1181
+ ):
1182
+ blk.del_component(blk.tmp_init_constraint)
1183
+
1184
+ blk.isentropic.activate()
1185
+
1186
+ # ---------------------------------------------------------------------
1187
+ # Solve unit
1188
+ with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
1189
+ res = opt.solve(blk, tee=slc.tee)
1190
+ init_log.info_high("Initialization Step 4 {}.".format(idaeslog.condition(res)))
1191
+
1192
+ if activate_performance_curves:
1193
+ blk.performance_curve.activate()
1194
+ for t, v in unfix_eff.items():
1195
+ if v:
1196
+ blk.efficiency_isentropic[t].unfix()
1197
+ for t, v in unfix_ratioP.items():
1198
+ if v:
1199
+ blk.ratioP[t].unfix()
1200
+ with idaeslog.solver_log(solve_log, idaeslog.DEBUG) as slc:
1201
+ res = opt.solve(blk, tee=slc.tee)
1202
+ init_log.info_high(f"Initialization Step 5 {idaeslog.condition(res)}.")
1203
+
1204
+ # ---------------------------------------------------------------------
1205
+ # Release Inlet state
1206
+ blk.control_volume.release_state(flags, outlvl)
1207
+
1208
+ if not check_optimal_termination(res):
1209
+ raise InitializationError(
1210
+ f"{blk.name} failed to initialize successfully. Please check "
1211
+ f"the output logs for more information."
1212
+ )
1213
+
1214
+ init_log.info(f"Initialization Complete: {idaeslog.condition(res)}")
1215
+
1216
+ def _get_performance_contents(self, time_point=0):
1217
+ var_dict = {}
1218
+ if hasattr(self, "deltaP"):
1219
+ var_dict["Mechanical Work"] = self.work_mechanical[time_point]
1220
+ if hasattr(self, "deltaP"):
1221
+ var_dict["Pressure Change"] = self.deltaP[time_point]
1222
+ if hasattr(self, "ratioP"):
1223
+ var_dict["Pressure Ratio"] = self.ratioP[time_point]
1224
+ if hasattr(self, "efficiency_pump"):
1225
+ var_dict["Efficiency"] = self.efficiency_pump[time_point]
1226
+ if hasattr(self, "efficiency_isentropic"):
1227
+ var_dict["Isentropic Efficiency"] = self.efficiency_isentropic[time_point]
1228
+
1229
+ return {"vars": var_dict}
1230
+
1231
+ def calculate_scaling_factors(self):
1232
+ super().calculate_scaling_factors()
1233
+
1234
+ if hasattr(self, "work_fluid"):
1235
+ for t, v in self.work_fluid.items():
1236
+ iscale.set_scaling_factor(
1237
+ v,
1238
+ iscale.get_scaling_factor(
1239
+ self.control_volume.work[t], default=1, warning=True
1240
+ ),
1241
+ )
1242
+
1243
+ if hasattr(self, "work_mechanical"):
1244
+ for t, v in self.work_mechanical.items():
1245
+ iscale.set_scaling_factor(
1246
+ v,
1247
+ iscale.get_scaling_factor(
1248
+ self.control_volume.work[t], default=1, warning=True
1249
+ ),
1250
+ )
1251
+
1252
+ if hasattr(self, "work_isentropic"):
1253
+ for t, v in self.work_isentropic.items():
1254
+ iscale.set_scaling_factor(
1255
+ v,
1256
+ iscale.get_scaling_factor(
1257
+ self.control_volume.work[t], default=1, warning=True
1258
+ ),
1259
+ )
1260
+
1261
+ if hasattr(self, "ratioP_calculation"):
1262
+ for t, c in self.ratioP_calculation.items():
1263
+ iscale.constraint_scaling_transform(
1264
+ c,
1265
+ iscale.get_scaling_factor(
1266
+ self.control_volume.properties_in[t].pressure,
1267
+ default=1,
1268
+ warning=True,
1269
+ ),
1270
+ overwrite=False,
1271
+ )
1272
+
1273
+ if hasattr(self, "fluid_work_calculation"):
1274
+ for t, c in self.fluid_work_calculation.items():
1275
+ iscale.constraint_scaling_transform(
1276
+ c,
1277
+ iscale.get_scaling_factor(
1278
+ self.control_volume.deltaP[t], default=1, warning=True
1279
+ ),
1280
+ overwrite=False,
1281
+ )
1282
+
1283
+ if hasattr(self, "actual_work"):
1284
+ for t, c in self.actual_work.items():
1285
+ iscale.constraint_scaling_transform(
1286
+ c,
1287
+ iscale.get_scaling_factor(
1288
+ self.control_volume.work[t], default=1, warning=True
1289
+ ),
1290
+ overwrite=False,
1291
+ )
1292
+
1293
+ if hasattr(self, "isentropic_pressure"):
1294
+ for t, c in self.isentropic_pressure.items():
1295
+ iscale.constraint_scaling_transform(
1296
+ c,
1297
+ iscale.get_scaling_factor(
1298
+ self.control_volume.properties_in[t].pressure,
1299
+ default=1,
1300
+ warning=True,
1301
+ ),
1302
+ overwrite=False,
1303
+ )
1304
+
1305
+ if hasattr(self, "isentropic"):
1306
+ for t, c in self.isentropic.items():
1307
+ iscale.constraint_scaling_transform(
1308
+ c,
1309
+ iscale.get_scaling_factor(
1310
+ self.control_volume.properties_in[t].entr_mol,
1311
+ default=1,
1312
+ warning=True,
1313
+ ),
1314
+ overwrite=False,
1315
+ )
1316
+
1317
+ if hasattr(self, "isentropic_energy_balance"):
1318
+ for t, c in self.isentropic_energy_balance.items():
1319
+ iscale.constraint_scaling_transform(
1320
+ c,
1321
+ iscale.get_scaling_factor(
1322
+ self.control_volume.work[t], default=1, warning=True
1323
+ ),
1324
+ overwrite=False,
1325
+ )
1326
+
1327
+ if hasattr(self, "zero_work_equation"):
1328
+ for t, c in self.zero_work_equation.items():
1329
+ iscale.constraint_scaling_transform(
1330
+ c,
1331
+ iscale.get_scaling_factor(
1332
+ self.control_volume.work[t], default=1, warning=True
1333
+ ),
1334
+ )
1335
+
1336
+ if hasattr(self, "state_material_balances"):
1337
+ cvol = self.control_volume
1338
+ phase_list = cvol.properties_in.phase_list
1339
+ phase_component_set = cvol.properties_in.phase_component_set
1340
+ mb_type = cvol._constructed_material_balance_type
1341
+ if mb_type == MaterialBalanceType.componentPhase:
1342
+ for (t, p, j), c in self.state_material_balances.items():
1343
+ sf = iscale.get_scaling_factor(
1344
+ cvol.properties_in[t].get_material_flow_terms(p, j),
1345
+ default=1,
1346
+ warning=True,
1347
+ )
1348
+ iscale.constraint_scaling_transform(c, sf)
1349
+ elif mb_type == MaterialBalanceType.componentTotal:
1350
+ for (t, j), c in self.state_material_balances.items():
1351
+ sf = iscale.min_scaling_factor(
1352
+ [
1353
+ cvol.properties_in[t].get_material_flow_terms(p, j)
1354
+ for p in phase_list
1355
+ if (p, j) in phase_component_set
1356
+ ]
1357
+ )
1358
+ iscale.constraint_scaling_transform(c, sf)
1359
+ else:
1360
+ # There are some other material balance types but they create
1361
+ # constraints with different names.
1362
+ _log.warning(f"Unknown material balance type {mb_type}")
1363
+
1364
+
1365
+ @declare_process_block_class("Turbine", doc="Isentropic turbine model")
1366
+ class TurbineData(PressureChangerData):
1367
+ """
1368
+ Pressure changer with isentropic turbine options
1369
+ """
1370
+
1371
+ default_initializer = IsentropicPressureChangerInitializer
1372
+
1373
+ CONFIG = PressureChangerData.CONFIG()
1374
+ CONFIG.compressor = False
1375
+ CONFIG.get("compressor")._default = False
1376
+ CONFIG.get("compressor")._domain = In([False])
1377
+ CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic
1378
+ CONFIG.get("thermodynamic_assumption")._default = ThermodynamicAssumption.isentropic
1379
+
1380
+
1381
+ @declare_process_block_class("Compressor", doc="Isentropic compressor model")
1382
+ class CompressorData(PressureChangerData):
1383
+ """Pressure changer with isentropic compressor options"""
1384
+
1385
+ default_initializer = IsentropicPressureChangerInitializer
1386
+
1387
+ CONFIG = PressureChangerData.CONFIG()
1388
+ CONFIG.compressor = True
1389
+ CONFIG.get("compressor")._default = True
1390
+ CONFIG.get("compressor")._domain = In([True])
1391
+ CONFIG.thermodynamic_assumption = ThermodynamicAssumption.isentropic
1392
+ CONFIG.get("thermodynamic_assumption")._default = ThermodynamicAssumption.isentropic
1393
+
1394
+
1395
+ @declare_process_block_class("Pump", doc="Pump model")
1396
+ class PumpData(PressureChangerData):
1397
+ """Pressure changer with pump options"""
1398
+
1399
+ CONFIG = PressureChangerData.CONFIG()
1400
+ CONFIG.compressor = True
1401
+ CONFIG.get("compressor")._default = True
1402
+ CONFIG.get("compressor")._domain = In([True])
1403
+ CONFIG.thermodynamic_assumption = ThermodynamicAssumption.pump
1404
+ CONFIG.get("thermodynamic_assumption")._default = ThermodynamicAssumption.pump