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

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 

13 

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]") 

18 

19 

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") 

24 

25 return expr 

26 

27 

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) 

32 

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") 

43 

44 

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) 

71 

72 

73class 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 

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