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,440 @@
1
+ from typing import Any
2
+ from enum import Enum
3
+ from pyomo.core.base.units_container import units
4
+ from pyomo.environ import value as get_value
5
+ from ahuora_builder_types import FlowsheetSchema, UnitModelSchema, PropertiesSchema, PortsSchema
6
+ from ahuora_builder_types.flowsheet_schema import PropertyPackageType
7
+ from .methods.units_handler import idaes_specific_convert, attach_unit, get_attached_unit, get_attached_unit_str
8
+ from .methods.adapter_library import AdapterLibrary, UnitModelConstructor
9
+ from .methods import adapter_methods
10
+ from .build_state import get_state_vars
11
+ import json
12
+ from ahuora_builder.custom.energy.power_property_package import PowerParameterBlock
13
+
14
+
15
+
16
+ class Section:
17
+ """
18
+ Represents a section of a Python file
19
+ Includes a name and a list of lines
20
+ """
21
+ def __init__(self, name: str, header: bool = True, new_line: bool = True, optional: bool = False) -> None:
22
+ self._name: str = name
23
+ self._lines: list[str] = []
24
+ self._header: bool = header
25
+ self._new_line: bool = new_line
26
+ self._optional: bool = optional
27
+
28
+ def extend(self, lines: list[str]) -> None:
29
+ self._lines.extend(lines)
30
+
31
+ def header(self) -> str:
32
+ return f"### {self._name}"
33
+
34
+ def lines(self) -> list[str]:
35
+ return self._lines
36
+
37
+
38
+ class PythonFileGenerator:
39
+ """
40
+ Generate a Python file from the given model data
41
+ """
42
+
43
+ def __init__(self, schema: FlowsheetSchema) -> None:
44
+ self._schema = schema
45
+
46
+ # set some global constants
47
+ self._model = "m"
48
+ self._flowsheet = "fs"
49
+ self._solver = "ipopt"
50
+ # store the unit models, property packages, and ports
51
+ self._property_packages: dict[int, dict[str, Any]] = {} # {id: {name, vars: {vars}}}
52
+ self._ports: dict[int, dict[str, Any]] = {} # {id: {name, arc_id}}
53
+ self._arcs: dict[int, dict[str, Any]] = {} # {id: {name, source_id, destination_id}}
54
+
55
+ # create the sections
56
+ self._sections = {
57
+ "imports": Section("Imports"),
58
+ "property_package_imports": Section("Property Package Imports", header=False, new_line=False),
59
+ "unit_model_imports": Section("Unit Model Imports", header=False, new_line=False),
60
+ "utility methods": Section("Utility Methods"),
61
+ "build_model": Section("Build Model"),
62
+ "create_property_packages": Section("Create Property Packages", header=False),
63
+ "create_unit_models": Section("Create Unit Models", header=False),
64
+ "create_arcs": Section("Connect Unit Models", optional=True),
65
+ "check_model": Section("Check Model Status"),
66
+ "initialize": Section("Initialize Model"),
67
+ "solve": Section("Solve"),
68
+ "report": Section("Report"),
69
+ }
70
+
71
+
72
+ def sections(self) -> dict:
73
+ return self._sections
74
+
75
+
76
+ def setup_sections(self) -> None:
77
+ """
78
+ Set up the sections with the initial (constant) lines
79
+ """
80
+ self.extend("imports", [
81
+ "from pyomo.environ import ConcreteModel, SolverFactory, SolverStatus, TerminationCondition, Block, TransformationFactory, assert_optimal_termination",
82
+ "from pyomo.network import SequentialDecomposition, Port, Arc",
83
+ "from pyomo.core.base.units_container import _PyomoUnit, units as pyomo_units",
84
+ "from idaes.core import FlowsheetBlock",
85
+ "from idaes.core.util.model_statistics import report_statistics, degrees_of_freedom",
86
+ "from idaes.core.util.tables import _get_state_from_port",
87
+ "import idaes.logger as idaeslog",
88
+ ])
89
+ self.extend("property_package_imports", [
90
+ "from property_packages.build_package import build_package",
91
+ ])
92
+ self.extend("utility methods", [
93
+ "def units(item: str) -> _PyomoUnit:",
94
+ " ureg = pyomo_units._pint_registry",
95
+ " pint_unit = getattr(ureg, item)",
96
+ " return _PyomoUnit(pint_unit, ureg)",
97
+ ])
98
+ self.extend("build_model", [
99
+ f"{self._model} = ConcreteModel()",
100
+ f"{self._model}.{self._flowsheet} = FlowsheetBlock(dynamic=False)",
101
+ ])
102
+ self.extend("create_property_packages", [
103
+ "# Set up property packages",
104
+ ])
105
+ self.extend("create_unit_models", [
106
+ "# Create unit models",
107
+ ])
108
+ self.extend("check_model", [
109
+ f"report_statistics({self._model})",
110
+ f"print(\"Degrees of freedom:\", degrees_of_freedom({self._model}))",
111
+ ])
112
+ self.extend("solve", [
113
+ f"opt = SolverFactory(\"{self._solver}\")",
114
+ f"res = opt.solve({self._model}, tee=True)",
115
+ f"assert_optimal_termination(res)",
116
+ ])
117
+
118
+
119
+ def extend(self, section_name: str, lines: list[str] | str) -> None:
120
+ # add lines to a section
121
+ if isinstance(lines, str):
122
+ lines = [lines]
123
+ self._sections[section_name].extend(lines)
124
+
125
+
126
+ def add_section_excl(self, section_name: str, line: str) -> None:
127
+ # add a line to a section if it is not already present
128
+ if line not in self._sections[section_name].lines():
129
+ self.extend(section_name, line)
130
+
131
+
132
+ def resolve_import(self, obj: type | Enum) -> tuple[str, str]:
133
+ # get the class name and import statement for a given class
134
+ module_name = obj.__module__
135
+ if isinstance(obj, Enum):
136
+ import_name = obj.__class__.__name__ # name to be used in the import statement
137
+ class_name = f"{obj.__class__.__name__}.{obj.name}" # name to be used in the code
138
+ else:
139
+ import_name = obj.__name__
140
+ class_name = import_name
141
+ return class_name, f"from {module_name} import {import_name}"
142
+
143
+
144
+ def get_name(self, name: str) -> str:
145
+ # clean the name so that it can be used as a Python variable
146
+ name = name.strip().replace("-", "_").replace(" ", "_")
147
+ # remove any spaces and special characters
148
+ name = "".join([char for char in name if char.isalnum() or char == "_"])
149
+ # if the name is empty, use a default name
150
+ if len(name) == 0:
151
+ name = f"_unnamed_unit"
152
+ # if the name starts with a number, add an underscore
153
+ if name[0].isnumeric():
154
+ name = "_" + name
155
+ name = f"{self._model}.{self._flowsheet}.{name}"
156
+ return name
157
+
158
+
159
+ def create_property_packages(self) -> None:
160
+ """
161
+ Create property packages
162
+ """
163
+ for schema in self._schema.property_packages:
164
+ self.create_property_package(schema)
165
+
166
+
167
+ def create_property_package(self, schema: PropertyPackageType) -> None:
168
+ name = self.get_name("PP_" + str(schema.id))
169
+ compounds = schema.compounds
170
+ phases = schema.phases
171
+ type = schema.type
172
+ self.extend("create_property_packages", [
173
+ f"{name} = build_package(",
174
+ f" \"{type}\",",
175
+ f" {json.dumps(compounds)},",
176
+ ")",
177
+ ])
178
+ self._property_packages[schema.id] = { "name": name, "vars": get_state_vars(schema) }
179
+
180
+
181
+ def get_property_package(self, id: int) -> dict:
182
+ """Get the name of a property package by ID"""
183
+ if id == -1 and id not in self._property_packages:
184
+ # add a default Helmholtz property package (for testing purposes)
185
+ self.create_property_package(PropertyPackageType(id=id, type="helmholtz", compounds=["h2o"], phases=["Liq"]))
186
+ return self._property_packages[id]
187
+
188
+ def get_power_property_package(self, id: str):
189
+ power_package = PowerParameterBlock
190
+ return self._property_packages[-1]
191
+
192
+
193
+ def get_property_package_at_port(self, model_schema: UnitModelSchema, port: str) -> dict:
194
+ """Get the property package that is used at a port"""
195
+ # generally, the unitop has one property package that is used for all ports
196
+ if model_schema.args.get("property_package") is not None:
197
+ return self.get_property_package(model_schema.args["property_package"])
198
+ # this is a special case (hard-coded for now) for heat exchangers, which have two property packages
199
+ # TODO: make this dynamic once we have parent stream inheritance
200
+ package_arg = port.removesuffix("_inlet").removesuffix("_outlet") # eg. "hot_side_inlet" -> "hot_side"
201
+ return self.get_property_package(model_schema.args[package_arg]["property_package"])
202
+
203
+
204
+ def serialise_dict(self, d: dict, indent: bool = False, indent_level: int = 1, nested_indent: bool = True) -> str:
205
+ if len(d) == 0:
206
+ return "{}"
207
+ result = "{"
208
+ for k, v in d.items():
209
+ if indent:
210
+ result += "\n" + " " * indent_level
211
+ adj_k = f"\"{k}\"" if isinstance(k, str) else k
212
+ adj_v = f"\"{v}\"" if isinstance(v, str) and not v.startswith(f"{self._model}.{self._flowsheet}.") else v
213
+ if isinstance(v, dict):
214
+ # allow nested dictionaries
215
+ adj_v = self.serialise_dict(v, indent=indent and nested_indent, indent_level=indent_level + 1)
216
+ result += f"{adj_k}: {adj_v},"
217
+ result = result[:-1] + ("\n" + " " * (indent_level - 1)) * indent + "}"
218
+ return result
219
+
220
+
221
+ def serialise_list(self, l: list) -> str:
222
+ if len(l) == 0:
223
+ return "[]"
224
+ result = "["
225
+ for v in l:
226
+ adj_v = f"\"{v}\"" if isinstance(v, str) and not v.startswith(f"{self._model}.{self._flowsheet}.") else v
227
+ result += f"{adj_v},"
228
+ result = result[:-1] + "]"
229
+ return result
230
+
231
+
232
+ def setup_args(self, args: dict, arg_parsers: dict) -> dict:
233
+ """Setup the arguments for a unit model"""
234
+ result: dict[str, Any] = {}
235
+ print("args: " + str(args))
236
+ for arg_name, method in arg_parsers.items():
237
+ def match_method() -> Any:
238
+ match method.__class__:
239
+ case adapter_methods.Constant:
240
+ # constant, defined in the method
241
+ constant = method.run(None,None)
242
+ # constant can be a function, in which case we need to resolve the import
243
+ if callable(constant) or isinstance(constant, Enum):
244
+ constant_class_name, constant_import = self.resolve_import(constant)
245
+ self.add_section_excl("imports", constant_import)
246
+ return constant_class_name
247
+ return constant
248
+ case adapter_methods.Value:
249
+ # value, keep as is
250
+ return args.get(arg_name, None)
251
+ case adapter_methods.PropertyPackage:
252
+ # property package
253
+ property_package_id = args["property_package"]
254
+ print(property_package_id)
255
+ return self.get_property_package(property_package_id)["name"]
256
+ case adapter_methods.PowerPropertyPackage:
257
+ #power property package
258
+ return "m.fs.power_property_package"
259
+
260
+ case adapter_methods.Dictionary:
261
+ # another dictionary of arg parsers, recursively setup the args
262
+ return self.setup_args(args[arg_name], method._schema)
263
+ case _:
264
+ raise Exception(f"Method {method} not supported")
265
+ result[arg_name] = match_method()
266
+ return result
267
+
268
+
269
+ def write_args(self, args: dict) -> str:
270
+ args_str = ""
271
+ if len(args) == 0:
272
+ return args_str
273
+ args_str += "\n"
274
+ for key, value in args.items():
275
+ args_str += f" {key}="
276
+ if isinstance(value, dict):
277
+ args_str += self.serialise_dict(value)
278
+ else:
279
+ args_str += f"{value}"
280
+ args_str += ",\n"
281
+ args_str = args_str[:-2] + "\n" # remove the last comma
282
+ return args_str
283
+
284
+
285
+ def create_unit_models(self) -> None:
286
+ """
287
+ Create the unit models
288
+ """
289
+ for unit_model in self._schema.unit_models:
290
+ # add to imports
291
+ adapter: Adapter = AdapterLibrary[unit_model.type]
292
+ class_name, class_import = self.resolve_import(adapter.model_constructor)
293
+ self.add_section_excl("unit_model_imports", class_import)
294
+ # setup args
295
+ args = self.setup_args(unit_model.args, adapter.arg_parsers)
296
+ args_str = self.write_args(args)
297
+ print("args_str: " + args_str)
298
+
299
+ # create the unit model
300
+ name = self.get_name(unit_model.name)
301
+ self.extend("create_unit_models", f"\n# {unit_model.name}") # comment
302
+ self.extend("create_unit_models", f"{name} = {class_name}({args_str})") # constructor
303
+ self.extend("create_unit_models", self.fix_properties(name, unit_model.properties)) # fix properties
304
+ for port_name, port_data in unit_model.ports.items():
305
+ # save the port
306
+ global_name = f"{name}.{port_name}"
307
+ self._ports[port_data.id] = { "name": global_name, "arc": None }
308
+ # available_vars = self.get_property_package_at_port(unit_model, port_name)["vars"]
309
+ # fix the properties of the port
310
+ self.extend("create_unit_models", self.fix_state_block(global_name, port_data.properties))
311
+ self.extend("report", f"{name}.report()")
312
+
313
+
314
+ def fix_properties(self, prefix: str, properties_schema: PropertiesSchema) -> list[str]:
315
+ lines = []
316
+ for key, property_info in properties_schema.items():
317
+ for property_value in property_info.data:
318
+ if property_value.value is None:
319
+ continue
320
+ if property_value.discrete_indexes is not None:
321
+ indexes_tuple = tuple(property_value.discrete_indexes)
322
+ indexes_string = f"[{indexes_tuple}]" if len(property_value.discrete_indexes) > 0 else ""
323
+ else:
324
+ indexes_string = ""
325
+ val = get_value(property_value.value)
326
+ unit = property_info.unit
327
+ # TODO: Handle dynamic indexes etc.
328
+ lines.append(f"{prefix}.{key}{indexes_string}.fix({val} * units(\"{unit}\"))")
329
+
330
+ return lines
331
+
332
+
333
+ def fix_state_block(self, prefix: str, properties: PropertiesSchema) -> list[str]:
334
+ """
335
+ Fix the properties of a unit model
336
+ """
337
+ lines = []
338
+ lines.append(f"sb = _get_state_from_port({prefix}, 0)")
339
+ for key, property_info in properties.items():
340
+ for property_value in property_info.data:
341
+ if property_value.value is None:
342
+ continue
343
+ if property_value.discrete_indexes is None:
344
+ indexes_str = ""
345
+ else:
346
+ # We aren't worrying about time yet, but we will need to do in the future.
347
+ indexes_tuple = tuple(property_value.discrete_indexes)
348
+ indexes_str = f"[{indexes_tuple}]" if len(property_value.discrete_indexes) > 0 else ""
349
+ val = get_value(property_value.value)
350
+ unit = property_info.unit
351
+ lines.append(f"sb.constrain_component(sb.{key}{indexes_str}, {val} * units(\"{unit}\"))")
352
+
353
+ if len(lines) == 1:
354
+ return []
355
+ return lines
356
+
357
+
358
+ def create_arcs(self) -> None:
359
+ """
360
+ Create the arcs
361
+ """
362
+ if len(self._schema.arcs) == 0:
363
+ return
364
+ for i, arc in enumerate(self._schema.arcs):
365
+ source = self._ports[arc.source]
366
+ destination = self._ports[arc.destination]
367
+ name = f"{self._model}.{self._flowsheet}.arc_{i + 1}"
368
+ self.extend("create_arcs", f"{name} = Arc(source={source['name']}, destination={destination['name']})")
369
+ self._arcs[i] = { "name": name, "source": arc.source, "destination": arc.destination }
370
+ source["arc"] = i
371
+ destination["arc"] = i
372
+ # add to initialization: expand the arcs
373
+ self.extend("initialize", f"TransformationFactory(\"network.expand_arcs\").apply_to({self._model})")
374
+
375
+
376
+ def initialize(self) -> None:
377
+ """
378
+ Initialize the model
379
+ """
380
+ def is_connected(ports: PortsSchema) -> bool:
381
+ for _, port_data in ports.items():
382
+ port = self._ports[port_data.id]
383
+ if port["arc"] is not None:
384
+ return True
385
+ return False
386
+ # initialize everything that is not connected
387
+ for unit_model in self._schema.unit_models:
388
+ if not is_connected(unit_model.ports):
389
+ name = self.get_name(unit_model.name)
390
+ self.extend("initialize", f"{name}.initialize(outlvl=idaeslog.INFO)")
391
+ if len(self._schema.arcs) == 0:
392
+ return
393
+ # setup sequential decomposition
394
+ self.extend("utility methods", [
395
+ "\ndef init_unit(unit: Block) -> None:",
396
+ " unit.initialize(outlvl=idaeslog.INFO)"
397
+ ])
398
+ self.extend("initialize", "seq = SequentialDecomposition()")
399
+ # set tear guesses
400
+ tear_set = []
401
+
402
+ # Need to rewrite the logic for dealing with tear sets from recycle
403
+
404
+ self.extend("initialize", f"seq.set_tear_set({self.serialise_list(tear_set)})")
405
+ self.extend("initialize", f"seq.run({self._model}, init_unit)")
406
+
407
+
408
+ def generate_python_code(model_data: FlowsheetSchema) -> str:
409
+ """
410
+ Generate a Python file from the given model data
411
+ """
412
+ generator = PythonFileGenerator(model_data)
413
+ generator.setup_sections()
414
+ generator.create_property_packages()
415
+ generator.create_unit_models()
416
+ generator.create_arcs()
417
+ generator.initialize()
418
+ sections = generator.sections()
419
+
420
+ result = ""
421
+ for key, section in sections.items():
422
+ if section._optional and len(section.lines()) == 0:
423
+ # skip empty sections
424
+ continue
425
+ # add extra newline characters between sections
426
+ if section._new_line and key != list(sections.keys())[0]:
427
+ if section._header:
428
+ result += "\n\n"
429
+ else:
430
+ result += "\n"
431
+ # add section header
432
+ if section._header:
433
+ result += section.header()
434
+ if section._new_line:
435
+ result += "\n"
436
+ # write each line to the result string
437
+ # separated by a newline character
438
+ result += "\n".join(section.lines()) + "\n"
439
+ return result
440
+
@@ -0,0 +1,84 @@
1
+ from ahuora_builder.methods.adapter import add_corresponding_constraint, fix_var,fix_slice, load_initial_guess
2
+
3
+
4
+ from idaes.core import FlowsheetBlock
5
+ from pyomo.core.base.constraint import Constraint
6
+ from pyomo.environ import Block, Component, Reference
7
+ from ahuora_builder_types.id_types import PropertyValueId
8
+ from pyomo.core.base.indexed_component_slice import (
9
+ IndexedComponent_slice,
10
+ _IndexedComponent_slice_iter,
11
+ )
12
+ from pyomo.core.base.indexed_component import UnindexedComponent_set, IndexedComponent
13
+
14
+
15
+ class BlockContext:
16
+ """
17
+ Where possible, we want to fix variables at the block level (ie. unit model, state block)
18
+ rather than at the flowsheet level. This is because it is easier to solve a smaller model
19
+ during initialization, rather than dumping complexity on the solver when solving the entire
20
+ flowsheet.
21
+
22
+ Each controlling variable in the model is accompanied by a guess variable. Normally, the
23
+ guess variable is fixed during initialization, and unfixed after, while the controlling
24
+ variable (set point) is a constraint at the flowsheet level. However, if both the guess and
25
+ controlling variable are on the same block, we can avoid this and fix the controlling variable
26
+ directly at the block level.
27
+
28
+ This class provides a context to store controlling variables and guess variables while fixing
29
+ within a block. We can then apply a simple heuristic to eliminate as many pairs of guess and
30
+ controlling variables as possible.
31
+ """
32
+
33
+ def __init__(self, flowsheet: FlowsheetBlock):
34
+ """
35
+ - blk: Pyomo block the Var is on (can add constraints to this block)
36
+ - var: Pyomo Var to fix/constrain
37
+ - value: value to fix/constrain the Var to
38
+ - id: id of the property, to store the created Constraint in the properties map
39
+ """
40
+ # property id: ( var_reference, values)
41
+ self._guess_vars: dict[PropertyValueId, tuple[ IndexedComponent | IndexedComponent_slice, list[float]]] = {}
42
+ # property id: ( var_reference, values, guess_id)
43
+ self._controlled_vars: dict[PropertyValueId, tuple[IndexedComponent | IndexedComponent_slice, list[float], PropertyValueId]] = {}
44
+ self._flowsheet = flowsheet
45
+
46
+ def add_guess_var(self, var_references : IndexedComponent | IndexedComponent_slice, values : list[float], propertyvalue_id : PropertyValueId):
47
+ self._guess_vars[propertyvalue_id] = ( var_references, values)
48
+
49
+ def add_controlled_var(self, var_references: IndexedComponent | IndexedComponent_slice, values: list[float], propertyvalue_id: PropertyValueId, guess_propertyvalue_id: PropertyValueId):
50
+ self._controlled_vars[propertyvalue_id] = (var_references, values, guess_propertyvalue_id)
51
+
52
+ def apply_elimination(self):
53
+ """
54
+ Try to eliminate as many guess vars/flowsheet-level constraints as possible.
55
+ Fix the remaining guess vars or add the remaining controlled vars as constraints.
56
+ """
57
+ # TODO: Update apply_elimination with the lists of values now.
58
+ fs = self._flowsheet
59
+ for id, (var_refs, values, guess_id) in self._controlled_vars.items():
60
+ # see if we can eliminate this controlled var
61
+ if guess_id in self._guess_vars:
62
+ # fix the controlled var
63
+ c = fix_slice(var_refs, values)
64
+ add_corresponding_constraint(fs, c, id)
65
+ # load the initial guess for the guess var
66
+ var_refs, values = self._guess_vars[guess_id]
67
+ load_initial_guess(var_refs, values)
68
+ # eliminate the guess var
69
+ del self._guess_vars[guess_id]
70
+ else:
71
+ # add the control as a flowsheet-level constraint
72
+ # As the values are flattended into a list, we also need to flatten the index set into a list.
73
+ var_refs_list = list(var_refs.values()) # returns a list of VarData or ExpressionData objects
74
+ def constraint_rule(blk,idx):
75
+ return var_refs_list[idx] == values[idx]
76
+ c = Constraint(range(len(var_refs_list)), rule=constraint_rule)
77
+ name = f"control_constraint_{id}" # Maybe we could use the var name or something here? but it's a bit harder with indexed constraints. remember it has to be unique!
78
+ self._flowsheet.add_component(name, c)
79
+ add_corresponding_constraint(fs, c, id)
80
+
81
+ # fix the remaining guess vars
82
+ for id, (var_refs, values) in self._guess_vars.items():
83
+ c = fix_slice(var_refs, values)
84
+ self._flowsheet.guess_vars.append(c)
File without changes