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,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
+
@@ -0,0 +1,7 @@
1
+ from pyomo.core.base.indexed_component import UnindexedComponent_set, IndexedComponent
2
+
3
+
4
+
5
+ def is_scalar_reference(component: IndexedComponent) -> bool:
6
+ # The only key in it should be None
7
+ return list(component) == [None]
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]