Coverage for backend/idaes_service/solver/methods/expression_parsing.py: 85%
64 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-11-06 23:27 +0000
1from typing import Any
2from pyomo.environ import Expression
3from pyomo.core.base.units_container import units as pyomo_units, _PyomoUnit
4from sympy import Symbol
5from sympy.parsing.sympy_parser import parse_expr
6from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, PyomoSympyBimap
7from pyomo.core.base.indexed_component import IndexedComponent
8from idaes_service.solver.properties_manager import PropertiesManager
9from .slice_manipulation import is_scalar_reference
10class ExpressionParsingError(Exception):
11 """Custom exception for errors during expression parsing."""
12 pass
14# add extra units to pyomo's units library
15# could probably make this on-demand
16ureg = pyomo_units._pint_registry
17ureg.define("dollar = [currency]")
20def handle_special_chars(expr: str) -> str:
21 # replace special characters so they can be parsed
22 expr = expr.replace("^", "**")
23 expr = expr.replace("$", "dollar")
25 return expr
28def 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)
33 if pyomo_object is None: 33 ↛ 34line 33 didn't jump to line 34 because the condition on line 33 was never true
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: 39 ↛ 42line 39 didn't jump to line 42 because the condition on line 39 was always true
40 return pyomo_object[time_index]
41 else:
42 raise NotImplementedError("Only 0D and 1D time-indexed properties are supported in expressions")
45def evaluate_symbol(fs, symbol: str,time_index) -> Any:
46 if symbol.lower() == "time" or symbol.lower() == "t": 46 ↛ 47line 46 didn't jump to line 47 because the condition on line 46 was never true
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": 64 ↛ 67line 64 didn't jump to line 67 because the condition on line 64 was never true
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)
73class PyomoSympyMap(PyomoSympyBimap):
75 def __init__(self, model,time_index):
76 self.model = model
77 self.time_index = time_index
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)
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
90 def sympyVars(self):
91 raise NotImplementedError(
92 "sympyVars not implemented, because it shouldn't be needed"
93 )
96def 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