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,105 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
from pyomo.environ import Expression
|
|
3
|
+
from pyomo.core.base.units_container import units as pyomo_units, _PyomoUnit
|
|
4
|
+
from sympy import Symbol
|
|
5
|
+
from sympy.parsing.sympy_parser import parse_expr
|
|
6
|
+
from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, PyomoSympyBimap
|
|
7
|
+
from pyomo.core.base.indexed_component import IndexedComponent
|
|
8
|
+
from ahuora_builder.properties_manager import PropertiesManager
|
|
9
|
+
from .slice_manipulation import is_scalar_reference
|
|
10
|
+
class ExpressionParsingError(Exception):
|
|
11
|
+
"""Custom exception for errors during expression parsing."""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
# add extra units to pyomo's units library
|
|
15
|
+
# could probably make this on-demand
|
|
16
|
+
ureg = pyomo_units._pint_registry
|
|
17
|
+
ureg.define("dollar = [currency]")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def handle_special_chars(expr: str) -> str:
|
|
21
|
+
# replace special characters so they can be parsed
|
|
22
|
+
expr = expr.replace("^", "**")
|
|
23
|
+
expr = expr.replace("$", "dollar")
|
|
24
|
+
|
|
25
|
+
return expr
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_property_from_id(fs, property_id,time_index):
|
|
29
|
+
# returns a pyomo var from a property id
|
|
30
|
+
properties_map : PropertiesManager = fs.properties_map
|
|
31
|
+
pyomo_object: IndexedComponent = properties_map.get_component(property_id)
|
|
32
|
+
|
|
33
|
+
if pyomo_object is None:
|
|
34
|
+
raise ValueError(f"Symbol with id {id} not found in model")
|
|
35
|
+
# check if this is a time-indexed var, and if so get the value at the given time index
|
|
36
|
+
if is_scalar_reference(pyomo_object):
|
|
37
|
+
# reference with index None
|
|
38
|
+
return pyomo_object[None]
|
|
39
|
+
elif pyomo_object.index_set() == fs.time:
|
|
40
|
+
return pyomo_object[time_index]
|
|
41
|
+
else:
|
|
42
|
+
raise NotImplementedError("Only 0D and 1D time-indexed properties are supported in expressions")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def evaluate_symbol(fs, symbol: str,time_index) -> Any:
|
|
46
|
+
if symbol.lower() == "time" or symbol.lower() == "t":
|
|
47
|
+
return float(time_index)
|
|
48
|
+
if symbol.startswith("id_"):
|
|
49
|
+
# get the property from flowsheet properties_map
|
|
50
|
+
id = int(symbol[3:])
|
|
51
|
+
return get_property_from_id(fs, id,time_index)
|
|
52
|
+
else:
|
|
53
|
+
# assume its a unit, eg. "m" or "kg"
|
|
54
|
+
# get the unit from pint, pyomo's units library
|
|
55
|
+
ureg = pyomo_units._pint_registry
|
|
56
|
+
pint_unit = getattr(ureg, symbol)
|
|
57
|
+
pyomo_unit = _PyomoUnit(pint_unit, ureg)
|
|
58
|
+
# We want people to write expressions such as (10 * W + 5 * kW). Pyomo doesn't natively support this,
|
|
59
|
+
# so we can always convert to base units.
|
|
60
|
+
if symbol == "delta_degC" or symbol == "delta_degF":
|
|
61
|
+
# special case, because degC is not a base unit
|
|
62
|
+
return 1 * pyomo_unit
|
|
63
|
+
#return _PyomoUnit(ureg.delta_degC)
|
|
64
|
+
elif symbol == "degC" or symbol == "degF":
|
|
65
|
+
# throw an error (we do not support this, as it is unclear what to do)
|
|
66
|
+
# https://pyomo.readthedocs.io/en/6.8.1/explanation/modeling/units.html
|
|
67
|
+
raise ValueError(f"Use relative temperature units (delta_degC, delta_degF) or absolute temperature units (K, degF). Cannot use {symbol} as addition and multiplication is inconsistent on non-absolute units")
|
|
68
|
+
scale_factor, base_units = ureg.get_base_units(pint_unit, check_nonmult=True) # TODO: handle degC etc.
|
|
69
|
+
base_pyomo_unit = _PyomoUnit(base_units, ureg)
|
|
70
|
+
return pyomo_units.convert( 1 * pyomo_unit, to_units=base_pyomo_unit)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class PyomoSympyMap(PyomoSympyBimap):
|
|
74
|
+
|
|
75
|
+
def __init__(self, model,time_index):
|
|
76
|
+
self.model = model
|
|
77
|
+
self.time_index = time_index
|
|
78
|
+
|
|
79
|
+
def getPyomoSymbol(self, sympy_object: Symbol, default=None):
|
|
80
|
+
if not isinstance(sympy_object, Symbol):
|
|
81
|
+
return None # It's not in pyomo, e.g a number or something
|
|
82
|
+
return evaluate_symbol(self.model, sympy_object.name, self.time_index)
|
|
83
|
+
|
|
84
|
+
def getSympySymbol(self, pyomo_object, default=None):
|
|
85
|
+
raise NotImplementedError(
|
|
86
|
+
"getSympySymbol not implemented, because it shouldn't be needed"
|
|
87
|
+
)
|
|
88
|
+
# we don't care, it only needs to go one way
|
|
89
|
+
|
|
90
|
+
def sympyVars(self):
|
|
91
|
+
raise NotImplementedError(
|
|
92
|
+
"sympyVars not implemented, because it shouldn't be needed"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def parse_expression(expression, model,time_index) -> Expression:
|
|
97
|
+
# use the bimap to get the correct pyomo object for each symbol
|
|
98
|
+
bimap = PyomoSympyMap(model,time_index)
|
|
99
|
+
try:
|
|
100
|
+
expression = handle_special_chars(expression)
|
|
101
|
+
sympy_expr = parse_expr(expression)
|
|
102
|
+
pyomo_expr = sympy2pyomo_expression(sympy_expr, bimap)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
raise ExpressionParsingError(f"{expression}: error: {e}")
|
|
105
|
+
return pyomo_expr
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from pyomo.network import Port
|
|
4
|
+
from pyomo.environ import value as pyo_value, Var
|
|
5
|
+
from pyomo.core.base.constraint import ScalarConstraint
|
|
6
|
+
from pyomo.core.base.units_container import units
|
|
7
|
+
from idaes.core import UnitModelBlock
|
|
8
|
+
from idaes.core.util.tables import _get_state_from_port
|
|
9
|
+
from idaes.core.util.model_serializer import StoreSpec, from_json
|
|
10
|
+
|
|
11
|
+
from ahuora_builder.methods.BlockContext import BlockContext
|
|
12
|
+
from .adapter_library import UnitModelConstructor
|
|
13
|
+
from ..flowsheet_manager_type import FlowsheetManager
|
|
14
|
+
from ahuora_builder_types import UnitModelSchema
|
|
15
|
+
from .adapter import fix_block
|
|
16
|
+
from ahuora_builder.methods.adapter import get_component
|
|
17
|
+
|
|
18
|
+
def add_unit_model_to_flowsheet(
|
|
19
|
+
unit_model_def: UnitModelSchema,
|
|
20
|
+
adapter_constructor: UnitModelConstructor,
|
|
21
|
+
flowsheet_manager: FlowsheetManager,
|
|
22
|
+
) -> UnitModelBlock:
|
|
23
|
+
"""
|
|
24
|
+
Add the unit model to the flowsheet.
|
|
25
|
+
Args:
|
|
26
|
+
unit_model_def: The definition of the unit model to be added to the flowsheet.
|
|
27
|
+
adapter_schema: Methods used to create the model and parse the arguments.
|
|
28
|
+
flowsheet_manager: Flowsheet manager to interact with the flowsheet.
|
|
29
|
+
"""
|
|
30
|
+
# Create the model
|
|
31
|
+
idaes_model: UnitModelBlock =_create_model(unit_model_def, adapter_constructor, flowsheet_manager)
|
|
32
|
+
|
|
33
|
+
# Add the model to the flowsheet
|
|
34
|
+
component_name = f"{unit_model_def.name}_{unit_model_def.id}"
|
|
35
|
+
flowsheet_manager.model.fs.add_component(component_name, idaes_model)
|
|
36
|
+
|
|
37
|
+
# Import initial guesses
|
|
38
|
+
initial_values = unit_model_def.initial_values
|
|
39
|
+
|
|
40
|
+
if initial_values:
|
|
41
|
+
from_json(idaes_model, initial_values, wts=StoreSpec.value())
|
|
42
|
+
|
|
43
|
+
# Fix properties
|
|
44
|
+
block_context = BlockContext(flowsheet_manager.model.fs)
|
|
45
|
+
_fix_properties(idaes_model,unit_model_def, flowsheet_manager,block_context)
|
|
46
|
+
# Fix ports
|
|
47
|
+
_fix_ports(idaes_model, unit_model_def, flowsheet_manager,block_context)
|
|
48
|
+
# Apply degrees of freedom (add constraints for controlled vars)
|
|
49
|
+
block_context.apply_elimination()
|
|
50
|
+
return idaes_model
|
|
51
|
+
|
|
52
|
+
def _create_model(unit_model_def : UnitModelSchema,adapter_constructor: UnitModelConstructor,flowsheet_manager : FlowsheetManager) -> UnitModelBlock:
|
|
53
|
+
"""
|
|
54
|
+
Create the kwargs for the model constructor from the unit model definition.
|
|
55
|
+
"""
|
|
56
|
+
arg_parsers = adapter_constructor.arg_parsers
|
|
57
|
+
args = unit_model_def.args
|
|
58
|
+
|
|
59
|
+
kwargs : dict[str, Any] = {}
|
|
60
|
+
for name in args:
|
|
61
|
+
if not name in arg_parsers:
|
|
62
|
+
raise ValueError(
|
|
63
|
+
f"Argument {name} not found in model schema, available arguments are {[x for x in arg_parsers.keys()]}"
|
|
64
|
+
)
|
|
65
|
+
for name in arg_parsers:
|
|
66
|
+
register = arg_parsers[name]
|
|
67
|
+
# try to get the argument from the schema passed in
|
|
68
|
+
# however, in some cases the argument may not be required
|
|
69
|
+
# i.e when specified by the model adapter itself
|
|
70
|
+
# e.g methods.constant
|
|
71
|
+
|
|
72
|
+
# note that in the future we may have to support passing
|
|
73
|
+
# optional arguments to the model constructor
|
|
74
|
+
result = register.run(args.get(name, None),flowsheet_manager)
|
|
75
|
+
kwargs[name] = result
|
|
76
|
+
|
|
77
|
+
idaes_model = adapter_constructor.model_constructor(**kwargs)
|
|
78
|
+
return idaes_model
|
|
79
|
+
|
|
80
|
+
def _fix_properties( unit_model: UnitModelBlock, unit_model_def: UnitModelSchema, flowsheet_manager : FlowsheetManager,block_context: BlockContext) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Fix the properties of the unit model based on the properties in the unit model definition.
|
|
83
|
+
"""
|
|
84
|
+
# Loop through the properties in the unit model definition to fix the properties in the unit model
|
|
85
|
+
properties = unit_model_def.properties
|
|
86
|
+
fix_block(
|
|
87
|
+
unit_model, properties, flowsheet_manager.model.fs, block_context
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _fix_ports(unit_model: UnitModelBlock, unit_model_def: UnitModelSchema, flowsheet_manager:FlowsheetManager,block_context: BlockContext) -> None:
|
|
92
|
+
"""
|
|
93
|
+
Fix the ports of the unit model based on the ports in the unit model definition.
|
|
94
|
+
"""
|
|
95
|
+
# Loop through the ports in the unit model definition to fix the ports in the unit model
|
|
96
|
+
for port_name, port_schema in unit_model_def.ports.items():
|
|
97
|
+
# Get the port from the unit model
|
|
98
|
+
port = get_component(unit_model, port_name)
|
|
99
|
+
if not isinstance(port, Port):
|
|
100
|
+
raise ValueError(f"Port {port_name} not found in model")
|
|
101
|
+
# Register the port, so arcs can connect to it by id
|
|
102
|
+
flowsheet_manager.ports.register_port(port_schema.id, port)
|
|
103
|
+
# Set the port parameters
|
|
104
|
+
sb = _get_state_from_port(port, 0)
|
|
105
|
+
state_block = sb.parent_component()
|
|
106
|
+
|
|
107
|
+
# Prefer time-only properties_in/properties_out if available
|
|
108
|
+
# decide by port name containing "inlet"/"outlet" and "hot"/"cold"
|
|
109
|
+
pn = port_name.lower()
|
|
110
|
+
if "inlet" in pn:
|
|
111
|
+
if "hot" in pn and hasattr(unit_model, "hot_side") and hasattr(unit_model.hot_side, "properties_in"):
|
|
112
|
+
state_block = unit_model.hot_side.properties_in
|
|
113
|
+
elif "cold" in pn and hasattr(unit_model, "cold_side") and hasattr(unit_model.cold_side, "properties_in"):
|
|
114
|
+
state_block = unit_model.cold_side.properties_in
|
|
115
|
+
elif "outlet" in pn:
|
|
116
|
+
if "hot" in pn and hasattr(unit_model, "hot_side") and hasattr(unit_model.hot_side, "properties_out"):
|
|
117
|
+
state_block = unit_model.hot_side.properties_out
|
|
118
|
+
elif "cold" in pn and hasattr(unit_model, "cold_side") and hasattr(unit_model.cold_side, "properties_out"):
|
|
119
|
+
state_block = unit_model.cold_side.properties_out
|
|
120
|
+
|
|
121
|
+
if sb.config.defined_state:
|
|
122
|
+
"""
|
|
123
|
+
ie. Inlet state.
|
|
124
|
+
|
|
125
|
+
The inlet state needs a separate context, because its variables
|
|
126
|
+
are all fixed during initialization, so applying elimination
|
|
127
|
+
involving variables outside the inlet state would run into
|
|
128
|
+
degrees of freedom issues.
|
|
129
|
+
"""
|
|
130
|
+
inlet_ctx = BlockContext(flowsheet_manager.model.fs)
|
|
131
|
+
fix_block(
|
|
132
|
+
state_block,
|
|
133
|
+
port_schema.properties,
|
|
134
|
+
flowsheet_manager.model.fs,
|
|
135
|
+
inlet_ctx,
|
|
136
|
+
)
|
|
137
|
+
inlet_ctx.apply_elimination()
|
|
138
|
+
else:
|
|
139
|
+
# The outlet state(s) can use the unit model context.
|
|
140
|
+
fix_block(
|
|
141
|
+
state_block,
|
|
142
|
+
port_schema.properties,
|
|
143
|
+
flowsheet_manager.model.fs,
|
|
144
|
+
block_context,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from pyomo.environ import ConcreteModel, Var, value, Reference
|
|
2
|
+
from ..expression_parsing import parse_expression
|
|
3
|
+
from ...properties_manager import PropertiesManager
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_parse_expression():
|
|
7
|
+
m = ConcreteModel()
|
|
8
|
+
m.x = Var(initialize=2)
|
|
9
|
+
m.properties_map = PropertiesManager()
|
|
10
|
+
m.properties_map.add(
|
|
11
|
+
123, Reference(m.x), "m.x"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
expr = parse_expression("id_123 + 2", m,1)
|
|
15
|
+
assert value(expr) == 4
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
from typing import Any, cast, NewType
|
|
2
|
+
from pyomo.environ import value
|
|
3
|
+
from pyomo.core.base.units_container import units, _PyomoUnit
|
|
4
|
+
from pyomo.core.base.var import Var
|
|
5
|
+
from pyomo.core.base.expression import Expression, ExpressionData
|
|
6
|
+
from pyomo.core.expr import ExpressionBase, NPV_ProductExpression
|
|
7
|
+
from pint import UnitRegistry
|
|
8
|
+
|
|
9
|
+
ValueWithUnits = NewType("ValueWithUnits", NPV_ProductExpression)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_pint_unit(unit: str) -> Any:
|
|
13
|
+
"""
|
|
14
|
+
Get the pint unit object
|
|
15
|
+
"""
|
|
16
|
+
pint_unit = getattr(units.pint_registry, unit, None)
|
|
17
|
+
if pint_unit is None:
|
|
18
|
+
raise AttributeError(f"Unit `{unit}` not found.")
|
|
19
|
+
return pint_unit
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_unit(unit: str) -> _PyomoUnit:
|
|
23
|
+
"""
|
|
24
|
+
Get the pint unit object
|
|
25
|
+
@unit: str unit type
|
|
26
|
+
@return: unit object
|
|
27
|
+
"""
|
|
28
|
+
return _PyomoUnit(_get_pint_unit(unit), units.pint_registry)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def attach_unit(value: float, unit: str | None) -> ValueWithUnits:
|
|
32
|
+
"""
|
|
33
|
+
Attach the unit to the value
|
|
34
|
+
@value: float value
|
|
35
|
+
@unit: str unit
|
|
36
|
+
@return: value with unit attached
|
|
37
|
+
"""
|
|
38
|
+
value, unit = idaes_specific_convert(
|
|
39
|
+
value, unit
|
|
40
|
+
) # make sure the value is in a unit that idaes supports
|
|
41
|
+
if unit is None:
|
|
42
|
+
return value
|
|
43
|
+
pyomo_unit = get_unit(unit)
|
|
44
|
+
return value * pyomo_unit
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def check_units_equivalent(unit1: _PyomoUnit, unit2: _PyomoUnit) -> bool:
|
|
48
|
+
"""
|
|
49
|
+
Check if two units are equivalent
|
|
50
|
+
"""
|
|
51
|
+
if unit1 is None:
|
|
52
|
+
unit1 = units.dimensionless
|
|
53
|
+
if unit2 is None:
|
|
54
|
+
unit2 = units.dimensionless
|
|
55
|
+
return (
|
|
56
|
+
unit1._get_pint_unit().dimensionality
|
|
57
|
+
== unit2._get_pint_unit().dimensionality
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def idaes_specific_convert(value: float, unit: str | None) -> tuple[float, str | None]:
|
|
63
|
+
"""
|
|
64
|
+
Convert the value to a unit that is specific to idaes
|
|
65
|
+
(ie. idaes only supports K for temperature)
|
|
66
|
+
@value: float value
|
|
67
|
+
@unit: str unit
|
|
68
|
+
@return: tuple:
|
|
69
|
+
- (float) converted value
|
|
70
|
+
- (str) new unit
|
|
71
|
+
"""
|
|
72
|
+
if unit in ["degC", "degR", "degF"]: # temperature units
|
|
73
|
+
from_quantity = units.pint_registry.Quantity(value, unit)
|
|
74
|
+
to_unit = units.pint_registry.K
|
|
75
|
+
# can probably do the conversion/attachment in one step but haven't figured out how yet
|
|
76
|
+
converted_value = from_quantity.to(to_unit) # pint.Quantity object
|
|
77
|
+
return converted_value.magnitude, "K"
|
|
78
|
+
if unit in ["percent"]: # dimensionless units
|
|
79
|
+
from_quantity = units.pint_registry.Quantity(value, unit)
|
|
80
|
+
to_unit = units.pint_registry.dimensionless
|
|
81
|
+
converted_value = from_quantity.to(to_unit)
|
|
82
|
+
return converted_value.magnitude, None
|
|
83
|
+
if unit in [None, ""]:
|
|
84
|
+
return value, None
|
|
85
|
+
return value, unit
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_attached_unit(
|
|
89
|
+
var: Var | Expression | ExpressionBase | ExpressionData | float,
|
|
90
|
+
) -> _PyomoUnit | None:
|
|
91
|
+
"""
|
|
92
|
+
Get the unit of a variable.
|
|
93
|
+
"""
|
|
94
|
+
if isinstance(var, float):
|
|
95
|
+
# no attached unit
|
|
96
|
+
return units.dimensionless
|
|
97
|
+
if isinstance(var, ExpressionData):
|
|
98
|
+
var = var.parent_component()
|
|
99
|
+
if var.is_indexed():
|
|
100
|
+
# we will get the unit from the first item in the indexed variable
|
|
101
|
+
var = var[next(iter(var.index_set()))]
|
|
102
|
+
if isinstance(var, (ExpressionBase, Expression, ExpressionData)):
|
|
103
|
+
# handle expressions
|
|
104
|
+
return units.get_units(var)
|
|
105
|
+
else:
|
|
106
|
+
# handle variables
|
|
107
|
+
return var.get_units()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_attached_unit_str(var: Var | Expression | ExpressionBase) -> str:
|
|
111
|
+
"""
|
|
112
|
+
Get the unit of a variable as a string.
|
|
113
|
+
"""
|
|
114
|
+
unit = get_attached_unit(var)
|
|
115
|
+
return str(unit) if unit is not None else "dimensionless"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_value(var: Var | Expression | ExpressionBase) -> float | dict:
|
|
119
|
+
if var.is_indexed():
|
|
120
|
+
if isinstance(var, Var):
|
|
121
|
+
# can get values directly
|
|
122
|
+
return cast(dict, var.get_values())
|
|
123
|
+
else:
|
|
124
|
+
data = {}
|
|
125
|
+
for index in var.index_set():
|
|
126
|
+
data[index] = value(var[index])
|
|
127
|
+
return data
|
|
128
|
+
else:
|
|
129
|
+
return float(value(var))
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from pydantic import BaseModel
|
|
2
|
+
from ahuora_builder_types.payloads.ml_request_schema import MLTrainRequestPayload, MLTrainingCompletionPayload
|
|
3
|
+
import pandas as pd
|
|
4
|
+
import json
|
|
5
|
+
import numpy as np
|
|
6
|
+
from io import StringIO
|
|
7
|
+
import contextlib
|
|
8
|
+
from sklearn.model_selection import train_test_split
|
|
9
|
+
from sklearn.metrics import mean_squared_error, r2_score
|
|
10
|
+
from idaes.core.surrogate.pysmo_surrogate import PysmoRBFTrainer, PysmoSurrogate
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MLResult(BaseModel):
|
|
14
|
+
surrogate_model: dict
|
|
15
|
+
charts: list[dict]
|
|
16
|
+
metrics: list[dict]
|
|
17
|
+
test_inputs: dict
|
|
18
|
+
test_outputs: dict
|
|
19
|
+
task_id: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def ml_generate(schema: MLTrainRequestPayload) -> MLResult:
|
|
23
|
+
df = pd.DataFrame(schema.datapoints, columns=schema.columns)
|
|
24
|
+
train_df, test_df = train_test_split(df, test_size=0.2, random_state=42)
|
|
25
|
+
|
|
26
|
+
input_labels = schema.input_labels
|
|
27
|
+
output_labels = schema.output_labels
|
|
28
|
+
|
|
29
|
+
trainer = PysmoRBFTrainer(
|
|
30
|
+
input_labels=input_labels, output_labels=output_labels, training_dataframe=train_df)
|
|
31
|
+
trainer.config.basis_function = 'gaussian'
|
|
32
|
+
|
|
33
|
+
# Train surrogate (calls PySMO through IDAES Python wrapper)
|
|
34
|
+
stream = StringIO()
|
|
35
|
+
with contextlib.redirect_stdout(stream):
|
|
36
|
+
rbf_train = trainer.train_surrogate()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# create callable surrogate model
|
|
40
|
+
rbf_surr = PysmoSurrogate(rbf_train, input_labels,
|
|
41
|
+
output_labels, input_bounds=None)
|
|
42
|
+
f = StringIO()
|
|
43
|
+
rbf_surr.save(f)
|
|
44
|
+
content = f.getvalue()
|
|
45
|
+
json_data = json.loads(content)
|
|
46
|
+
|
|
47
|
+
df_evaluate = rbf_surr.evaluate_surrogate(test_df)
|
|
48
|
+
|
|
49
|
+
metrics = []
|
|
50
|
+
|
|
51
|
+
charts = []
|
|
52
|
+
|
|
53
|
+
for output_label in output_labels:
|
|
54
|
+
charts.append(compute_chart(
|
|
55
|
+
test_df[output_label], df_evaluate[output_label], output_label))
|
|
56
|
+
metrics.append({
|
|
57
|
+
"mean_squared_error": round(mean_squared_error(df_evaluate[output_label], test_df[output_label]), 4),
|
|
58
|
+
"r2_score": round(r2_score(df_evaluate[output_label], test_df[output_label]), 4),
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
return MLResult(
|
|
62
|
+
surrogate_model=json_data,
|
|
63
|
+
charts=charts,
|
|
64
|
+
metrics=metrics,
|
|
65
|
+
test_inputs=test_df.to_dict(orient='index'),
|
|
66
|
+
test_outputs=df_evaluate.to_dict(orient='index'),
|
|
67
|
+
task_id=schema.task_id
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def compute_chart(test_data_df, eval_data_df, output_label):
|
|
72
|
+
minn = round(np.min([np.min(test_data_df), np.min(eval_data_df)]), 4)
|
|
73
|
+
maxx = round(np.max([np.max(test_data_df), np.max(eval_data_df)]), 4)
|
|
74
|
+
qq_plot_data = compute_qq_coordinates(test_data_df, eval_data_df)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
"min": minn,
|
|
78
|
+
"max": maxx,
|
|
79
|
+
"qq_plot_data": qq_plot_data,
|
|
80
|
+
"output_label": output_label
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def compute_qq_coordinates(test_data, eval_data):
|
|
85
|
+
"""Compute QQ plot coordinates for two datasets."""
|
|
86
|
+
test_values = test_data.to_numpy().flatten()
|
|
87
|
+
eval_values = eval_data.to_numpy().flatten()
|
|
88
|
+
|
|
89
|
+
# Sort values
|
|
90
|
+
test_values.sort()
|
|
91
|
+
eval_values.sort()
|
|
92
|
+
|
|
93
|
+
# Generate QQ plot data points
|
|
94
|
+
quantiles = np.linspace(0, 1, len(test_values))
|
|
95
|
+
test_quantiles = np.quantile(test_values, quantiles)
|
|
96
|
+
eval_quantiles = np.quantile(eval_values, quantiles)
|
|
97
|
+
|
|
98
|
+
# Prepare JSON response for frontend
|
|
99
|
+
qq_data = [{"x": round(float(t), 4), "y": round(float(e), 4)}
|
|
100
|
+
for t, e in zip(test_quantiles, eval_quantiles)]
|
|
101
|
+
return json.dumps(qq_data)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from pyomo.network import Port
|
|
2
|
+
from ahuora_builder_types import PortId
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PortManager:
|
|
6
|
+
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._map: dict[PortId, Port] = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register_port(self, id: PortId, port: Port) -> None:
|
|
12
|
+
"""Registers a port with the port map, so that arcs can connect to it by id"""
|
|
13
|
+
self._map[id] = port
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_port(self, id: PortId) -> Port:
|
|
17
|
+
try:
|
|
18
|
+
return self._map[id]
|
|
19
|
+
except KeyError:
|
|
20
|
+
raise KeyError(f"Port with id `{id}` not found")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from ahuora_builder_types.id_types import PropertyValueId
|
|
2
|
+
from pyomo.core.base.indexed_component import IndexedComponent
|
|
3
|
+
class PropertyComponent:
|
|
4
|
+
def __init__(self, name: str, component : IndexedComponent, unknown_units=False):
|
|
5
|
+
"""
|
|
6
|
+
Args:
|
|
7
|
+
- name (str): The name of the property component (for debugging purposes)
|
|
8
|
+
- component (Component): The Pyomo component; i.e., Expression, Var
|
|
9
|
+
- unknown_units (bool): If units are unknown, idaes_factory will do some
|
|
10
|
+
additional processing to determine the unit category.
|
|
11
|
+
- corresponding_constraint (Constraint | Var | None): The component
|
|
12
|
+
(Var or Constraint) that was fixed or activated to set the value of
|
|
13
|
+
this property. Given an id in the properties dictionary, this field
|
|
14
|
+
can be used to unfix or deactivate the corresponding constraint.
|
|
15
|
+
Defaults to None (allowed for properties that are not fixed).
|
|
16
|
+
"""
|
|
17
|
+
self.name: str = name
|
|
18
|
+
self.component: IndexedComponent = component
|
|
19
|
+
self.unknown_units = unknown_units
|
|
20
|
+
self.corresponding_constraint = None # TODO: Type
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PropertiesManager:
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.properties : dict[PropertyValueId,PropertyComponent] = {}
|
|
26
|
+
|
|
27
|
+
def add(self, id: PropertyValueId, indexed_component: IndexedComponent, name : str, unknown_units=False)-> IndexedComponent:
|
|
28
|
+
self.properties[id] = PropertyComponent(name, indexed_component, unknown_units)
|
|
29
|
+
|
|
30
|
+
def get(self, id: PropertyValueId):
|
|
31
|
+
return self.properties[id]
|
|
32
|
+
|
|
33
|
+
def get_component(self, id: PropertyValueId):
|
|
34
|
+
return self.get(id).component
|
|
35
|
+
|
|
36
|
+
def get_constraint(self, id: PropertyValueId):
|
|
37
|
+
return self.get(id).corresponding_constraint
|
|
38
|
+
|
|
39
|
+
def add_constraint(self, id: PropertyValueId, constraint):
|
|
40
|
+
# assumes the property has already been added
|
|
41
|
+
self.get(id).corresponding_constraint = constraint
|
|
42
|
+
|
|
43
|
+
def items(self):
|
|
44
|
+
return self.properties.items()
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
from idaes.core.base.property_base import PhysicalParameterBlock
|
|
2
|
+
from idaes.core import FlowsheetBlock
|
|
3
|
+
from property_packages.build_package import build_package
|
|
4
|
+
|
|
5
|
+
from ahuora_builder_types.flowsheet_schema import PropertyPackageType
|
|
6
|
+
from .flowsheet_manager_type import FlowsheetManager
|
|
7
|
+
from ahuora_builder_types import PropertyPackageId
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def create_property_package(
|
|
11
|
+
property_package_schema: PropertyPackageType, flowsheet: FlowsheetBlock
|
|
12
|
+
) -> PhysicalParameterBlock:
|
|
13
|
+
"""
|
|
14
|
+
Create a property package from a schema
|
|
15
|
+
"""
|
|
16
|
+
compounds = property_package_schema.compounds
|
|
17
|
+
type = property_package_schema.type
|
|
18
|
+
phases = property_package_schema.phases
|
|
19
|
+
|
|
20
|
+
property_package = build_package(type, compounds)
|
|
21
|
+
property_package_id = property_package_schema.id if property_package_schema.id != -1 else "default"
|
|
22
|
+
|
|
23
|
+
flowsheet.add_component(f"PP_{property_package_id}", property_package)
|
|
24
|
+
return property_package
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PropertyPackageManager:
|
|
28
|
+
"""
|
|
29
|
+
Manages the property packages for a flowsheet
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, flowsheet_manager: FlowsheetManager) -> None:
|
|
33
|
+
"""
|
|
34
|
+
Create a new property package manager
|
|
35
|
+
"""
|
|
36
|
+
self._flowsheet_manager = flowsheet_manager
|
|
37
|
+
self._property_packages: dict[PropertyPackageId, PhysicalParameterBlock] = {}
|
|
38
|
+
|
|
39
|
+
def load(self) -> None:
|
|
40
|
+
"""
|
|
41
|
+
Load the property packages from the flowsheet definition
|
|
42
|
+
"""
|
|
43
|
+
schema = self._flowsheet_manager.schema
|
|
44
|
+
property_packages_schema = schema.property_packages
|
|
45
|
+
for property_package_schema in property_packages_schema:
|
|
46
|
+
id = property_package_schema.id
|
|
47
|
+
if id in self._property_packages:
|
|
48
|
+
raise Exception(f"Property package with id {id} already exists")
|
|
49
|
+
|
|
50
|
+
property_package = create_property_package(
|
|
51
|
+
property_package_schema, self._flowsheet_manager.model.fs
|
|
52
|
+
)
|
|
53
|
+
self._property_packages[id] = property_package
|
|
54
|
+
|
|
55
|
+
def get(self, id: PropertyPackageId) -> PhysicalParameterBlock:
|
|
56
|
+
"""
|
|
57
|
+
Get a property package by id
|
|
58
|
+
"""
|
|
59
|
+
# For backwards compatibility with other tests, use id -1 as helmholtz
|
|
60
|
+
fs = self._flowsheet_manager.model.fs
|
|
61
|
+
|
|
62
|
+
if id == -1:
|
|
63
|
+
if not hasattr(fs, "PP_default"):
|
|
64
|
+
create_property_package(
|
|
65
|
+
PropertyPackageType(
|
|
66
|
+
id=-1,
|
|
67
|
+
type="helmholtz",
|
|
68
|
+
compounds=["h2o"],
|
|
69
|
+
phases=["Liq"]
|
|
70
|
+
),
|
|
71
|
+
fs,
|
|
72
|
+
)
|
|
73
|
+
return fs.PP_default
|
|
74
|
+
else:
|
|
75
|
+
# get the property package by id
|
|
76
|
+
if id not in self._property_packages:
|
|
77
|
+
raise Exception(f"Property package with id {id} does not exist")
|
|
78
|
+
return self._property_packages[id]
|