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