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
File without changes
@@ -0,0 +1,33 @@
1
+ from pyomo.network import Arc
2
+ from .flowsheet_manager_type import FlowsheetManager
3
+ from ahuora_builder_types import PortId
4
+
5
+
6
+ class ArcManager:
7
+
8
+ def __init__(self, flowsheet_manager: FlowsheetManager):
9
+ """
10
+ Initializes the arc manager
11
+ """
12
+ self._flowsheet_manager = flowsheet_manager
13
+
14
+ def load(self):
15
+ """
16
+ Loads arcs from the schema and adds them to the flowsheet
17
+ """
18
+ schema = self._flowsheet_manager.schema
19
+
20
+ for arc_schema in schema.arcs:
21
+ self.add_arc(arc_schema.source, arc_schema.destination)
22
+
23
+ def add_arc(self, from_port_id: PortId, to_port_id: PortId):
24
+ """
25
+ Adds an arc between two ports
26
+ """
27
+ port_manager = self._flowsheet_manager.ports
28
+ from_port = port_manager.get_port(from_port_id)
29
+ to_port = port_manager.get_port(to_port_id)
30
+ arc = Arc(source=from_port, destination=to_port)
31
+ self._flowsheet_manager.model.fs.add_component(
32
+ f"arc_{from_port_id}_{to_port_id}", arc
33
+ )
@@ -0,0 +1,57 @@
1
+ from typing import Any
2
+ from idaes.core import FlowsheetBlock
3
+
4
+ from ahuora_builder_types.flowsheet_schema import PropertyPackageType
5
+ from ahuora_builder_types.payloads import BuildStateRequestSchema
6
+ from ahuora_builder_types.unit_model_schema import SolvedPropertyValueSchema
7
+ from .property_package_manager import create_property_package
8
+ from .methods.adapter import fix_block, serialize_properties_map, deactivate_fixed_guesses
9
+ from .methods.BlockContext import BlockContext
10
+ from pyomo.environ import ConcreteModel
11
+ from .properties_manager import PropertiesManager
12
+ from pyomo.environ import Block, assert_optimal_termination, SolverFactory
13
+ from pyomo.core.base.constraint import ScalarConstraint
14
+ from .flowsheet_manager import build_flowsheet
15
+ from idaes.core.util.model_statistics import degrees_of_freedom, number_unused_variables, number_activated_equalities, number_unfixed_variables, number_unfixed_variables_in_activated_equalities
16
+ from idaes.core.util.model_diagnostics import DiagnosticsToolbox
17
+
18
+ def solve_state_block(schema: BuildStateRequestSchema) -> list[SolvedPropertyValueSchema]:
19
+ m, sb = build_state(schema.property_package)
20
+ # if there's only one compound in the property package, remove the mole_frac_comp (as it's over specified)
21
+ if len(schema.property_package.compounds) == 1:
22
+ del schema.properties["mole_frac_comp"]
23
+
24
+ block_ctx = BlockContext(m.fs)
25
+ fix_block(sb, schema.properties, m.fs, block_ctx)
26
+
27
+ # If the degrees of freedom are not zero, don't try solve.
28
+ # However, the degree of freedom logic ignores any variables that aren't actually used. So if temperature
29
+ # and pressure are both not specified, and there are no constraints for them either, it decides that
30
+ # they are not part of the solution, and says there's 0 degrees of freedom.
31
+ # so instead, we actually check there are unfixed variables that are not in activated equalities.
32
+ # TODO: Check why adding and degrees_of_freedom(sb) == 0 makes pr fail (python manage.py test core.auxiliary.tests.test_Compounds)
33
+ if number_unfixed_variables(sb) - number_unfixed_variables_in_activated_equalities(sb) != 0:
34
+ return [] # This means no properties are returned, so the backend won't update anything.
35
+ block_ctx.apply_elimination()
36
+
37
+ # initialise the state block, which will perform a solve
38
+ sb.initialize(outlvl=1)
39
+ deactivate_fixed_guesses(m.fs.guess_vars)
40
+
41
+ return serialize_properties_map(m.fs)
42
+
43
+
44
+ def build_state(schema: PropertyPackageType) -> Any: # PropertyPackageSchema
45
+ m = build_flowsheet(dynamic=False)
46
+ # create the property package and state block
47
+ property_package = create_property_package(schema, m)
48
+ state_block = property_package.build_state_block(m.fs.time, defined_state=True)
49
+ m.fs.add_component(f"PP_{schema.id}_state", state_block)
50
+
51
+ return m, state_block
52
+
53
+
54
+ def get_state_vars(schema: PropertyPackageType) -> Any:
55
+ _, state_block = build_state(schema)
56
+
57
+ return state_block[0].define_state_vars()
@@ -0,0 +1,494 @@
1
+ from idaes.models.control.controller import PIDControllerData, ControllerType, ControllerMVBoundType, ControllerAntiwindupType, smooth_bound, smooth_heaviside
2
+ from pyomo.environ import Var
3
+ from idaes.core import UnitModelBlockData, declare_process_block_class
4
+ from pyomo.common.config import ConfigValue, In, Bool
5
+ import pyomo.environ as pyo
6
+ from idaes.core.util.exceptions import ConfigurationError
7
+ import pyomo.dae as pyodae
8
+ from idaes.core.util import scaling as iscale
9
+ import functools
10
+
11
+
12
+ @declare_process_block_class(
13
+ "PIDController2",
14
+ doc="PID controller model block. To use this the model must be dynamic.",
15
+ )
16
+ class PIDController2Data(UnitModelBlockData):
17
+ """
18
+ PID controller class.
19
+ """
20
+
21
+ CONFIG = UnitModelBlockData.CONFIG()
22
+ CONFIG.declare(
23
+ "mv_bound_type",
24
+ ConfigValue(
25
+ default=ControllerMVBoundType.NONE,
26
+ domain=In(
27
+ [
28
+ ControllerMVBoundType.NONE,
29
+ ControllerMVBoundType.SMOOTH_BOUND,
30
+ ControllerMVBoundType.LOGISTIC,
31
+ ]
32
+ ),
33
+ description="Type of bounds to apply to the manipulated variable (mv)).",
34
+ doc=(
35
+ """Type of bounds to apply to the manipulated variable output. If,
36
+ bounds are applied, the model parameters **mv_lb** and **mv_ub** set the bounds.
37
+ The **default** is ControllerMVBoundType.NONE. See the controller documentation
38
+ for details on the mathematical formulation. The options are:
39
+ **ControllerMVBoundType.NONE** no bounds, **ControllerMVBoundType.SMOOTH_BOUND**
40
+ smoothed mv = min(max(mv_unbound, ub), lb), and **ControllerMVBoundType.LOGISTIC**
41
+ logistic function to enforce bounds.
42
+ """
43
+ ),
44
+ ),
45
+ )
46
+ CONFIG.declare(
47
+ "calculate_initial_integral",
48
+ ConfigValue(
49
+ default=True,
50
+ domain=Bool,
51
+ description="Calculate the initial integral term value if True",
52
+ doc="Calculate the initial integral term value if True",
53
+ ),
54
+ )
55
+ CONFIG.declare(
56
+ "controller_type",
57
+ ConfigValue(
58
+ default=ControllerType.PI,
59
+ domain=In(
60
+ [
61
+ ControllerType.P,
62
+ ControllerType.PI,
63
+ ControllerType.PD,
64
+ ControllerType.PID,
65
+ ]
66
+ ),
67
+ description="Control type",
68
+ doc="""Controller type. The **default** = ControllerType.PI and the
69
+ options are: **ControllerType.P** Proportional, **ControllerType.PI**
70
+ proportional and integral, **ControllerType.PD** proportional and derivative, and
71
+ **ControllerType.PID** proportional, integral, and derivative
72
+ """,
73
+ ),
74
+ )
75
+ CONFIG.declare(
76
+ "antiwindup_type",
77
+ ConfigValue(
78
+ default=ControllerAntiwindupType.NONE,
79
+ domain=In(
80
+ [
81
+ ControllerAntiwindupType.NONE,
82
+ ControllerAntiwindupType.CONDITIONAL_INTEGRATION,
83
+ ControllerAntiwindupType.BACK_CALCULATION,
84
+ ]
85
+ ),
86
+ description="Type of antiwindup technique to use.",
87
+ doc=(
88
+ """Type of antiwindup technique to use. Options are **ControllerAntiwindupType.NONE**,
89
+ **ControllerAntiwindupType.CONDITIONAL_INTEGRATION**, and **ControllerAntiwindupType.BACK_CALCULATION**.
90
+ See the controller documentation for details on the mathematical formulation.
91
+ """
92
+ ),
93
+ ),
94
+ )
95
+ CONFIG.declare(
96
+ "derivative_on_error",
97
+ ConfigValue(
98
+ default=False,
99
+ domain=Bool,
100
+ description="Whether basing derivative action on process var or error",
101
+ doc="""Naive implementations of derivative action can cause large spikes in
102
+ control when the setpoint is changed. One solution is to use the (negative)
103
+ derivative of the process variable to calculate derivative action instead
104
+ of using the derivative of setpoint error. If **True**, use the derivative of
105
+ setpoint error to calculate derivative action. If **False** (default), use the
106
+ (negative) derivative of the process variable instead.
107
+ """,
108
+ ),
109
+ )
110
+
111
+ def build(self):
112
+ """
113
+ Build the PID block
114
+ """
115
+ super().build()
116
+
117
+
118
+ if self.config.dynamic is False:
119
+ raise ConfigurationError(
120
+ "PIDControllers work only with dynamic flowsheets."
121
+ )
122
+
123
+ """
124
+ We want to create the controlled and manipulated variables internally, and then the user can use constraints to set them later on.
125
+ """
126
+
127
+ self.process_var = Var(
128
+ self.flowsheet().time,
129
+ initialize=0.0,
130
+ doc="Setpoint for the PID controller",
131
+ )
132
+ self.manipulated_var = Var(
133
+ self.flowsheet().time,
134
+ initialize=0.0,
135
+ doc="Manipulated variable for the PID controller",
136
+ )
137
+
138
+ # Shorter pointers to time set information
139
+ time_set = self.flowsheet().time
140
+ time_units = self.flowsheet().time_units
141
+ if time_units is None:
142
+ time_units = pyo.units.dimensionless
143
+ t0 = time_set.first()
144
+
145
+ # Type Check
146
+ if not issubclass(self.process_var[t0].ctype, (pyo.Var, pyo.Expression)):
147
+ raise TypeError(
148
+ f"process_var must reference a Var or Expression not {self.process_var[t0].ctype}"
149
+ )
150
+ if not issubclass(self.manipulated_var[t0].ctype, pyo.Var):
151
+ raise TypeError(
152
+ f"manipulated_var must reference a Var not {self.manipulated_var[t0].ctype}"
153
+ )
154
+
155
+ if not self.config.antiwindup_type == ControllerAntiwindupType.NONE:
156
+ if not self.config.controller_type in [
157
+ ControllerType.PI,
158
+ ControllerType.PID,
159
+ ]:
160
+ raise ConfigurationError(
161
+ "User specified antiwindup method for controller without integral action."
162
+ )
163
+ if self.config.mv_bound_type == ControllerMVBoundType.NONE:
164
+ raise ConfigurationError(
165
+ "User specified antiwindup method for unbounded MV."
166
+ )
167
+
168
+ # Get the appropriate units for various controller variables
169
+ mv_units = pyo.units.get_units(self.manipulated_var[t0])
170
+ pv_units = pyo.units.get_units(self.process_var[t0])
171
+ if mv_units is None:
172
+ mv_units = pyo.units.dimensionless
173
+ if pv_units is None:
174
+ pv_units = pyo.units.dimensionless
175
+ gain_p_units = mv_units / pv_units
176
+
177
+ if not self.config.mv_bound_type == ControllerMVBoundType.NONE:
178
+ # Parameters
179
+ self.mv_lb = pyo.Param(
180
+ mutable=True,
181
+ initialize=0.05,
182
+ doc="Controller output lower bound",
183
+ units=mv_units,
184
+ )
185
+ self.mv_ub = pyo.Param(
186
+ mutable=True,
187
+ initialize=1,
188
+ doc="Controller output upper bound",
189
+ units=mv_units,
190
+ )
191
+ if self.config.mv_bound_type == ControllerMVBoundType.SMOOTH_BOUND:
192
+ self.smooth_eps = pyo.Param(
193
+ mutable=True,
194
+ initialize=1e-4,
195
+ doc="Smoothing parameter for controller output limits when the bound"
196
+ " type is SMOOTH_BOUND",
197
+ units=mv_units,
198
+ )
199
+ elif self.config.mv_bound_type == ControllerMVBoundType.LOGISTIC:
200
+ self.logistic_bound_k = pyo.Param(
201
+ mutable=True,
202
+ initialize=4,
203
+ doc="Smoothing parameter for controller output limits when the bound"
204
+ " type is LOGISTIC",
205
+ units=pyo.units.dimensionless,
206
+ )
207
+ if (
208
+ self.config.antiwindup_type
209
+ == ControllerAntiwindupType.CONDITIONAL_INTEGRATION
210
+ ):
211
+ self.conditional_integration_k = pyo.Param(
212
+ mutable=True,
213
+ initialize=200,
214
+ doc="Parameter governing steepness of transition between integrating and not integrating."
215
+ "A larger value means a steeper transition.",
216
+ units=pyo.units.dimensionless,
217
+ )
218
+
219
+ # Variable for basic controller settings may change with time.
220
+ self.setpoint = pyo.Var(
221
+ time_set, initialize=0.5, doc="Setpoint", units=pv_units
222
+ )
223
+ self.gain_p = pyo.Var(
224
+ time_set,
225
+ initialize=0.1,
226
+ doc="Gain for proportional part",
227
+ units=gain_p_units,
228
+ )
229
+ if self.config.controller_type in [ControllerType.PI, ControllerType.PID]:
230
+ self.gain_i = pyo.Var(
231
+ time_set,
232
+ initialize=0.1,
233
+ doc="Gain for integral part",
234
+ units=gain_p_units / time_units,
235
+ )
236
+ if self.config.controller_type in [ControllerType.PD, ControllerType.PID]:
237
+ self.gain_d = pyo.Var(
238
+ time_set,
239
+ initialize=0.01,
240
+ doc="Gain for derivative part",
241
+ units=gain_p_units * time_units,
242
+ )
243
+
244
+ if self.config.antiwindup_type == ControllerAntiwindupType.BACK_CALCULATION:
245
+ self.gain_b = pyo.Var(
246
+ time_set,
247
+ initialize=0.1,
248
+ doc="Gain for back calculation antiwindup",
249
+ units=1 / time_units,
250
+ )
251
+
252
+ self.mv_ref = pyo.Var(
253
+ time_set,
254
+ initialize=0.5,
255
+ doc="Controller bias",
256
+ units=mv_units,
257
+ )
258
+
259
+ # Error expression or variable (variable required for derivative term)
260
+ if (
261
+ self.config.controller_type in [ControllerType.PD, ControllerType.PID]
262
+ and self.config.derivative_on_error
263
+ ):
264
+ self.error = pyo.Var(
265
+ time_set, initialize=0, doc="Error variable", units=pv_units
266
+ )
267
+
268
+ @self.Constraint(time_set, doc="Error constraint")
269
+ def error_eqn(b, t):
270
+ return b.error[t] == b.setpoint[t] - b.process_var[t]
271
+
272
+ self.derivative_term = pyodae.DerivativeVar(
273
+ self.error,
274
+ wrt=self.flowsheet().time,
275
+ initialize=0,
276
+ units=pv_units / time_units,
277
+ )
278
+
279
+ else:
280
+
281
+ @self.Expression(time_set, doc="Error expression")
282
+ def error(b, t):
283
+ return b.setpoint[t] - b.process_var[t]
284
+
285
+ if (
286
+ self.config.controller_type in [ControllerType.PD, ControllerType.PID]
287
+ and not self.config.derivative_on_error
288
+ ):
289
+ # Need to create a Var because process_var might be an Expression
290
+ self.negative_pv = pyo.Var(
291
+ time_set,
292
+ initialize=0,
293
+ doc="Negative of process variable",
294
+ units=pv_units,
295
+ )
296
+
297
+ @self.Constraint(time_set, doc="Negative process variable equation")
298
+ def negative_pv_eqn(b, t):
299
+ return b.negative_pv[t] == -b.process_var[t]
300
+
301
+ self.derivative_term = pyodae.DerivativeVar(
302
+ self.negative_pv,
303
+ wrt=self.flowsheet().time,
304
+ initialize=0,
305
+ units=pv_units / time_units,
306
+ )
307
+
308
+ # integral term written de_i(t)/dt = e(t)
309
+ if self.config.controller_type in [ControllerType.PI, ControllerType.PID]:
310
+ self.mv_integral_component = pyo.Var(
311
+ time_set,
312
+ initialize=0,
313
+ doc="Integral contribution to control action",
314
+ units=mv_units,
315
+ )
316
+ self.mv_integral_component_dot = pyodae.DerivativeVar(
317
+ self.mv_integral_component,
318
+ wrt=time_set,
319
+ initialize=0,
320
+ units=mv_units / time_units,
321
+ doc="Rate of change of integral contribution to control action",
322
+ )
323
+
324
+ if self.config.calculate_initial_integral:
325
+
326
+ @self.Constraint(doc="Calculate initial e_i based on output")
327
+ def initial_integral_error_eqn(b):
328
+ if self.config.controller_type == ControllerType.PI:
329
+ return b.mv_integral_component[t0] == (
330
+ b.manipulated_var[t0]
331
+ - b.mv_ref[t0]
332
+ - b.gain_p[t0] * b.error[t0]
333
+ )
334
+ return b.mv_integral_component[t0] == (
335
+ b.manipulated_var[t0]
336
+ - b.mv_ref[t0]
337
+ - b.gain_p[t0] * b.error[t0]
338
+ - b.gain_d[t0] * b.derivative_term[t0]
339
+ )
340
+
341
+ @self.Expression(time_set, doc="Unbounded output for manipulated variable")
342
+ def mv_unbounded(b, t):
343
+ if self.config.controller_type == ControllerType.PID:
344
+ return (
345
+ b.mv_ref[t]
346
+ + b.gain_p[t] * b.error[t]
347
+ + b.mv_integral_component[t]
348
+ + b.gain_d[t] * b.derivative_term[t]
349
+ )
350
+ elif self.config.controller_type == ControllerType.PI:
351
+ return (
352
+ b.mv_ref[t] + b.gain_p[t] * b.error[t] + b.mv_integral_component[t]
353
+ )
354
+ elif self.config.controller_type == ControllerType.PD:
355
+ return (
356
+ b.mv_ref[t]
357
+ + b.gain_p[t] * b.error[t]
358
+ + b.gain_d[t] * b.derivative_term[t]
359
+ )
360
+ elif self.config.controller_type == ControllerType.P:
361
+ return b.mv_ref[t] + b.gain_p[t] * b.error[t]
362
+ else:
363
+ raise ConfigurationError(
364
+ f"{self.config.controller_type} is not a valid PID controller type"
365
+ )
366
+
367
+ @self.Constraint(time_set, doc="Bounded output of manipulated variable")
368
+ def mv_eqn(b, t):
369
+ if self.config.mv_bound_type == ControllerMVBoundType.SMOOTH_BOUND:
370
+ return b.manipulated_var[t] == smooth_bound(
371
+ b.mv_unbounded[t], lb=b.mv_lb, ub=b.mv_ub, eps=b.smooth_eps
372
+ )
373
+ elif self.config.mv_bound_type == ControllerMVBoundType.LOGISTIC:
374
+ return (
375
+ (b.manipulated_var[t] - b.mv_lb)
376
+ * (
377
+ 1
378
+ + pyo.exp(
379
+ -b.logistic_bound_k
380
+ * (b.mv_unbounded[t] - (b.mv_lb + b.mv_ub) / 2)
381
+ / (b.mv_ub - b.mv_lb)
382
+ )
383
+ )
384
+ ) == b.mv_ub - b.mv_lb
385
+ return b.manipulated_var[t] == b.mv_unbounded[t]
386
+
387
+ # deactivate the time 0 mv_eqn instead of skip, should be fine since
388
+ # first time step always exists.
389
+ if self.config.calculate_initial_integral:
390
+ self.mv_eqn[t0].deactivate()
391
+
392
+ if self.config.controller_type in [ControllerType.PI, ControllerType.PID]:
393
+
394
+ @self.Constraint(time_set, doc="de_i(t)/dt = e(t)")
395
+ def mv_integration_eqn(b, t):
396
+ if (
397
+ self.config.antiwindup_type
398
+ == ControllerAntiwindupType.CONDITIONAL_INTEGRATION
399
+ ):
400
+ # This expression is not sensitive to whether the "right" or "wrong" bound is active for a given
401
+ # expression of error.
402
+ return b.mv_integral_component_dot[t] == b.gain_i[t] * b.error[
403
+ t
404
+ ] * (
405
+ smooth_heaviside(
406
+ (b.mv_unbounded[t] - b.mv_lb) / (b.mv_ub - b.mv_lb),
407
+ b.conditional_integration_k,
408
+ )
409
+ # 1
410
+ - smooth_heaviside(
411
+ (b.mv_unbounded[t] - b.mv_ub) / (b.mv_ub - b.mv_lb),
412
+ b.conditional_integration_k,
413
+ )
414
+ )
415
+ elif (
416
+ self.config.antiwindup_type
417
+ == ControllerAntiwindupType.BACK_CALCULATION
418
+ ):
419
+ return b.mv_integral_component_dot[t] == b.gain_i[t] * b.error[
420
+ t
421
+ ] + b.gain_b[t] * (b.manipulated_var[t] - b.mv_unbounded[t])
422
+ else:
423
+ return b.mv_integral_component_dot[t] == b.gain_i[t] * b.error[t]
424
+
425
+ self.mv_integration_eqn[t0].deactivate()
426
+
427
+
428
+ # TODO: Initial values.
429
+ # For now, mv_integral_component is 0
430
+ self.mv_integral_component[t0].fix(0)
431
+
432
+ def calculate_scaling_factors(self):
433
+ super().calculate_scaling_factors()
434
+ gsf = iscale.get_scaling_factor
435
+ ssf = functools.partial(iscale.set_scaling_factor, overwrite=False)
436
+ cst = functools.partial(iscale.constraint_scaling_transform, overwrite=False)
437
+
438
+ # orig_pv = self.config.process_var
439
+ # orig_mv = self.config.manipulated_var
440
+ time_set = self.flowsheet().time
441
+ t0 = time_set.first()
442
+
443
+ sf_pv = iscale.get_scaling_factor(self.config.process_var[t0])
444
+ if sf_pv is None:
445
+ sf_pv = iscale.get_scaling_factor(self.process_var[t0], default=1)
446
+ sf_mv = iscale.get_scaling_factor(self.config.manipulated_var[t0])
447
+ if sf_mv is None:
448
+ sf_mv = iscale.get_scaling_factor(self.manipulated_var[t0], default=1)
449
+
450
+ # Don't like calling scaling laterally like this, but we need scaling factors for the pv and mv
451
+ # Except this causes a StackOverflow with flowsheet-level PVs or MVs---put this on ice for now
452
+ # if sf_pv is None:
453
+ # try:
454
+ # iscale.calculate_scaling_factors(self.config.process_var.parent_block())
455
+ # except RecursionError:
456
+ # raise ConfigurationError(
457
+ # f"Circular scaling dependency detected in Controller {self.name}. The only way this should be "
458
+ # "able to happen is if a loop of controllers exists manipulating each others setpoints without "
459
+ # "terminating in an actual process variable."
460
+ # )
461
+ # if sf_mv is None:
462
+ # try:
463
+ # iscale.calculate_scaling_factors(self.config.manipulated_var.parent_block())
464
+ # except RecursionError:
465
+ # raise ConfigurationError(
466
+ # f"Circular scaling dependency detected in Controller {self.name}. The only way this should be "
467
+ # "able to happen is if a loop of controllers exists manipulating each others setpoints without "
468
+ # "terminating in an actual process variable."
469
+ # )
470
+
471
+ if self.config.calculate_initial_integral:
472
+ sf_mv = gsf(self.manipulated_var[t0], default=1, warning=True)
473
+ cst(self.initial_integral_error_eqn, sf_mv)
474
+
475
+ for t in time_set:
476
+ sf_pv = gsf(self.process_var[t], default=1, warning=True)
477
+ sf_mv = gsf(self.manipulated_var[t], default=1, warning=True)
478
+
479
+ ssf(self.setpoint[t], sf_pv)
480
+ ssf(self.mv_ref[t], sf_mv)
481
+ cst(self.mv_eqn[t], sf_mv)
482
+
483
+ if self.config.controller_type in [ControllerType.PD, ControllerType.PID]:
484
+ if self.config.derivative_on_error:
485
+ ssf(self.error[t], sf_pv)
486
+ cst(self.error_eqn[t], sf_pv)
487
+ else:
488
+ ssf(self.negative_pv[t], sf_pv)
489
+ cst(self.negative_pv_eqn[t], sf_pv)
490
+
491
+ if self.config.controller_type in [ControllerType.PI, ControllerType.PID]:
492
+ ssf(self.mv_integral_component[t], sf_mv)
493
+
494
+ cst(self.mv_integration_eqn[t], sf_pv)