Source code for pyfemtet.opt.problem.variable_manager._string_as_expression

import re
from numbers import Number

from sympy import sympify
from sympy.core.sympify import SympifyError
from sympy import Min, Max, Add, Symbol, Expr, Basic  # TODO: Add sqrt, pow

from pyfemtet._util.symbol_support_for_param_name import convert_symbols

__all__ = [
    '_ExpressionFromString', 'InvalidExpression', 'SympifyError'
]


def _convert(expr_str):
    """ \" で囲まれた部分があれば記号を置き換える """

    def repl(m: re.Match) -> str:
        inner = m.group(1)
        return f'"{convert_symbols(inner)}"'

    # 非貪欲で " 内だけを拾う
    expr_str = re.sub(r'"(.+?)"', repl, expr_str)
    # " を消す
    expr_str = expr_str.replace('"', '')

    return expr_str


[docs] class InvalidExpression(Exception): pass
def get_valid_functions(values_may_be_used_in_mean: dict[str, Number] = None): v = values_may_be_used_in_mean or {} return { 'mean': lambda *args: Add(*args).subs(v) / len(args), 'max': Max, 'min': Min, 'S': Symbol('S') } class _ExpressionFromString: _expr_str: str _sympy_expr: Expr def __init__( self, expression_string: str | Number = None, sympy_expr: Expr = None, ): """ Raises: SympifyError: Sympy が認識できない場合 InvalidExpression: Sympy は認識できるが PyFemtet で想定する型ではない場合(tuple など) Examples: e = Expression('1') e.expr # '1' e.value # 1.0 e = Expression(1) e.expr # '1' e.value # 1.0 e = Expression('a') e.expr # 'a' e.value # ValueError e = Expression('1/2') e.expr # '1/2' e.value # 0.5 e = Expression('1.0000') e.expr # '1.0' e.value # 1.0 # To use "-", "@" and "." for name e = Expression('"Sample-1.0@Part1" * 2') e.expr # 'Sample_hyphen_1_dot_0_at_Part1 * 2' """ # check assert not (expression_string is None and sympy_expr is None) if sympy_expr is not None: assert expression_string is None self._sympy_expr = sympy_expr self._expr_str = str(sympy_expr) else: assert expression_string is not None self._expr_str: str = _convert(str(expression_string)) # max(name1, name2) など関数を入れる際に問題になるので # 下記の仕様は廃止、使い方として数値桁区切り , を入れてはいけない # # sympify 時に tuple 扱いになるので , を置き換える # # 日本人が数値に , を使うとき Python では _ を意味する # # expression に _ が入っていても構わない # tmp_expr = str(self._expr_str).replace(',', '_') self._sympy_expr = sympify(self._expr_str, locals=get_valid_functions()) # noqa if not isinstance(self._sympy_expr, Basic): raise InvalidExpression(f'{self._expr_str} は数式ではありません。') @property def dependency(self) -> set[str]: s: Symbol return {s.name for s in self._sympy_expr.free_symbols} def is_number(self) -> bool: return self._sympy_expr.is_number def is_expression(self) -> bool: return not self.is_number() @property def expression_string(self) -> str: return self._expr_str def eval(self, dependency_values: dict[str, Number]): # 型チェック assert all([isinstance(value, Number) for value in dependency_values.values()]), \ 'ExpressionFromString では数値変数のみをサポートしています。' re_sympy_expr = sympify( # noqa self.expression_string, locals=get_valid_functions(dependency_values), ) evaluated_sympy_obj = re_sympy_expr.subs(dependency_values) # noqa try: evaluated_value = float(evaluated_sympy_obj) except (ValueError, TypeError) as e: raise type(e)(*e.args, f'{evaluated_sympy_obj=} cannot convert to float.') from None return evaluated_value