import collections.abc as _cabc
import logging as _logging
import math as _math
import lark as _lark
from pytrnsys_process import log
from pytrnsys_process.deck import parser
from pytrnsys_process.deck import visitor_helpers as vh
[docs]
def parse_deck_for_constant_expressions(
deck_as_string: str, logger: _logging.Logger = log.default_console_logger
) -> dict[str, float | int]:
"""Evaluate constant expressions in a TRNSYS deck file and return their values.
This function parses a TRNSYS deck file string, identifies constant expressions,
and evaluates them to their numerical values. It handles mathematical operations,
functions, and variable references.
Parameters
__________
deck_as_string: A string containing the contents of a TRNSYS deck file.
logger: provide your own logger. to for example log per simulation
Returns
_______
variable_values: dict
A dictionary mapping variable names to their evaluated values (float or int).
The original case of variable names is preserved in the returned dictionary.
Expressions that could not be evaluated are not included in the returned dictionary.
"""
equations = _get_equation_trees(deck_as_string)
sub_trees_to_process = _get_expression_sub_trees_by_variable_name(
equations
)
evaluated_variables: dict[str, float | int] = {}
original_variable_names: list[str] = []
new_constants_found = True
while new_constants_found:
sub_trees_before_processing = sub_trees_to_process.copy()
# Needs to be converted into list, so items can be deleted while iteration over
# Described in this answer:
# https://stackoverflow.com/questions/5384914/how-to-delete-items-from-a-dictionary-while-iterating-over-it
for var, tree in list(sub_trees_to_process.items()):
try:
maybe_evaluated_value = (
_evaluate_or_none_if_variable_could_not_be_found(
tree, evaluated_variables
)
)
if maybe_evaluated_value is not None:
var_lower = var.casefold()
original_variable_names.append(var)
evaluated_variables[var_lower] = maybe_evaluated_value
del sub_trees_to_process[var]
except MathFuncNotFoundError as e:
failed_equation = deck_as_string[
e.meta.start_pos : e.meta.end_pos
]
func_name, _ = failed_equation.split("(")
logger.warning(
"On line %s, %s is not supported in %s=%s",
e.meta.line,
func_name,
var,
failed_equation,
)
del sub_trees_to_process[var]
except _lark.exceptions.VisitError as e:
failed_equation = deck_as_string[
e.obj.meta.start_pos : e.obj.meta.end_pos # type: ignore
]
logger.error(
"On line %s, unable to compute equation %s=%s because: %s",
e.obj.meta.line, # type: ignore
var,
failed_equation,
str(e),
exc_info=True,
)
if sub_trees_before_processing == sub_trees_to_process:
new_constants_found = False
return _rename_dict_keys_to_original_format(
evaluated_variables, original_variable_names
)
[docs]
class EquationsCollectorVisitor(_lark.Visitor):
"""This visitor is given the whole deck as a tree.
For each equation the equation() method is called and it appends it to a list of equations
"""
[docs]
def __init__(self):
self.equations_to_transform = []
[docs]
def equation(self, tree):
output_detector = self.OutputOfTrnsysTypeDetector()
output_detector.visit(tree)
if not output_detector.is_output:
self.equations_to_transform.append(tree)
[docs]
class OutputOfTrnsysTypeDetector(_lark.visitors.Visitor_Recursive):
"""Detects if equation is an output: equation_name = [15,1]"""
[docs]
def __init__(self):
self.is_output = False
[docs]
def output(self, _):
self.is_output = True
[docs]
class MathFuncNotFoundError(Exception):
"""This error is raised if the parsed 'func_call' is not supported."""
[docs]
def __init__(self, message, meta):
super().__init__(message)
self.meta = meta
[docs]
class ReferencedVariableNotEvaluatedError(Exception):
"""Raised if an equation could not be found in the dictionary of resolved equations."""
def _get_equation_trees(deck_as_string):
whole_tree = parser.parse_dck(deck_as_string)
equations_collector_visitor = EquationsCollectorVisitor()
equations_collector_visitor.visit(whole_tree)
equations = equations_collector_visitor.equations_to_transform
return equations
def _rename_dict_keys_to_original_format(
evaluated_variables, original_variable_names
) -> dict[str, float | int]:
for original_name in original_variable_names:
evaluated_variables[original_name] = evaluated_variables.pop(
original_name.casefold()
)
return evaluated_variables
def _get_expression_sub_trees_by_variable_name(
list_of_equation_trees: list[_lark.Tree],
) -> dict[str, _lark.Tree]:
equations_dict = {}
for equation_tree in list_of_equation_trees:
equations_dict[
vh.get_child_token_value(
"NAME", equation_tree.children[0].children[0], str
)
] = equation_tree.children[
1
] # right hand side of the equation as a tree
return equations_dict
def _evaluate_or_none_if_variable_could_not_be_found(
tree: _lark.Tree, evaluated_variables: _cabc.Mapping[str, float]
):
# Exceptions raised in callback need to be caught here
try:
value = EquationsTransformer(evaluated_variables).transform(tree)
return value
except _lark.exceptions.VisitError as e:
if isinstance(e.orig_exc, ReferencedVariableNotEvaluatedError):
return None
if isinstance(e.orig_exc, MathFuncNotFoundError):
raise e.orig_exc
raise