用于简单(而不是那么简单)表达式求值的 eval() 替代品

问题描述 投票:0回答:1

我设计了一个实验设计系统。本质上,用户定义具有最小值、最大值和标称值的属性列表。如下所示,元组为 min、max、nom。这些对象还有一些其他属性,但这是保持简单的最低限度。

X = (0, 50, 42.5)
Y = (-50, 0, -42.5)
Z = (-50, 50, 0)
A = (-50, 50, 2.5)
...

从这里我可以调用各种函数,例如

Min(), Max(), Nom(), Random()
等来获取
{X:float, Y:float, Z:float, ...}
的字典,我可以在代码库的其他地方使用它。

用户还可以选择定义约束,进一步限制所定义的值。这些是简单的表达式,其中每一侧都支持标准运算

+, -, *, /, ^
,比较运算符可以位于集合
=, !=, <, <=, >, >=
中,而值可以是变量引用或数字。

X >= Y + 5
Z < X - 10
Z >= Y
A < X - .1
A > Y + .1
...

我已经使用 ply.lex 为约束表达式编写了一个简单的词法分析器,并且可以根据提供的字典将表达式评估为 true 或 false。约束表达式是上述表达式的一侧,例如 (Y+0.1) 或 (X-10)

tokens = ['NUM', 'PLUS', 'MINUS', 'MULT', 'DIV', 'EXP', 'LPAREN', 'RPAREN', 'SOURCE']
t_PLUS = r'\+'
t_MINUS = r'\-'
t_MULT = r'\*'
t_DIV = r'\/'
t_EXP = r'\^'
t_LPAREN = r'\('
t_RPAREN = r'\)'
t_SOURCE = r'[a-zA-Z]+[a-zA-Z0-9_]*'

def t_NUM(t):
    r'-?[\d.]+(?:e-?\d+)?'
    t.value = float(t.value)
    return t

t_ignore = ' \t\[\]'

def t_error(t):
    print(f'Illegal character {t.value[0]}')
    t.lexer.skip(1)

lexer = lex.lex()

class ConstraintExpression:
    input_str = None
    tokens = None
    def __init__(self, expr):
        self.input_str = str(expr)
        lexer.input(self.input_str)
        self.tokens = [x for x in lexer]

    def evaluate(self, values):
        if len(self.tokens) == 1:
            return float(self.get_value(self.tokens[0], values))

        postfix_tokens = infix_2_postfix(self.tokens)

        stack = []
        for token in postfix_tokens:
            if token.type == "NUM" or token.type == "SOURCE":
                stack.append(token)
            else:
                val1 = self.get_value(stack.pop(), values)
                val2 = self.get_value(stack.pop(), values)
                stack.append(str(eval(str(val2) + token.value + str(val1))))
        return float(stack.pop())

一切都很好。

我遇到的问题是需要更大的系统支持扫描和 DOE,而不仅仅是随机化。为此,我需要能够“解决”约束,可以说找出强制最小/最大将被赋予一组其他属性的值。

示例:除了 X 之外的所有内容都是 nom,我想以 5 步(6 点)扫描 X,目前该迭代器类似于

linspace(0, 50, 6)
,它将返回
array([ 0., 10., 20., 30., 40., 50.])
,但是,步骤 X=0 违反了
A<X-0.1 的约束
A=2.5
。从逻辑上讲,我可以看到我需要弄清楚
X>A+0.1
意味着这次扫描的实际 MIN 是 2.6,所以它实际上是
linspace(2.6, 50, 6)
->
array([ 2.6 , 12.08, 21.56, 31.04, 40.52, 50.  ])
。这也违反了对 Z 的约束,因此这将是一个迭代过程(我已经考虑过,但如果有人对此有想法,则没有循环依赖的解决方案。)

当我们开始讨论 DOE 时,这个问题会变得更糟,其中每个变量从 MIN 开始,并以 STEPS 的方式扫描到 MAX,但它是建议值的完整叉积。其中许多(通常超过 50% 是无效条件)。

X = (0, 10, 20, 30, 40, 50)
Y = (-50, -40, -30, -20, -10, 0)
Z = (-50, -25, 0, 25, 50)
A = (-50, -40, -30, -20, -10, 0, 10, 20, 30, 40, 50)

DOE1 = X=0, Y=-50, Z=-50, A=-50
DOE2 = **X=10**, Y=-50, Z=-50, A=-50
...
DOE6 = **X=50**, Y=-50, Z=-50, A=-50
DOE7 = X=0, **Y=-40**, Z=-50, A=-50
DOE8 = **X=10**, Y=-40, Z=-50, A=-50
...

我已经开始探索 SymPy,它似乎能够做到这一点,但感觉如果我走这条路,我需要把我已经用词法分析器所做的事情扔到窗外,并完全致力于使用符号公式而不是词法分析/parsing (我同意这个只是想确保这是正确的方法)。像这样的东西可以让我动态地创建、替换、简化和从这些方程中提取“新的”最小值/最大值。

>>> x, y, z, a = symbols("x y z a")
>>> c = [x>=y+1, z<x-10, z>=y, a<x-0.1, a>y+0.1]
>>> c
[x >= y + 1, z < x - 10, z >= y, a < x - 0.1, a > y + 0.1]
>>> c[0].subs(y, -42.5)
x >= -41.5
>>> c[1].subs(z, 0)
0 < x - 10
>>> simplify(c[1].subs(z, 0))
x > 10
>>> simplify(c[1].subs(z, 0)).gts
x
>>> simplify(c[1].subs(z, 0)).lts
10

重构工作量很小,因为我只需要更改

ConstaintExpression
的结构(
Constraint
只是两个
ConstraintExpression
Op
),因此约束表达式不是由令牌支持
eval() 
这将是 SymPy 支持的表达式。

>>> c1 = x
>>> c2 = y+1
>>> c3 = z
>>> c4 = x-10
>>> c1 >= c2
x >= y + 1
>>> c3 < c4
z < x - 10
>>> simplify((c1 >= c2).subs(y, -42.5))
x >= -41.5
>>> simplify((c3 < c4).subs(z, 0))
x > 10

非常感谢对此的想法以及替代方案,因为几周来我一直在努力解决这个问题,但没有取得进展。我正在使用Python 3.10.11

python sympy
1个回答
0
投票

对于简单的代数关系,可以用

AccumBounds
代替for表达式,可以检查是否都满足。如果没有,您可以决定要更改哪个变量并在替换过程中忽略该变量并求解该变量的不等式系统。

from sympy import *

X = ('X',0, 50, 42.5)
Y = ('Y',-50, 0, -42.5)
Z = ('Z',-50, 50, 0)
A = ('A',-50, 50, 2.5)

reps = {Symbol(i[0]): AccumBounds(i[1],i[2]) for i in (X,Y,Z,A)}

v = var('X Y Z A')

conditions = Tuple(X >= Y + 5, Z < X - 10, Z >= Y, A < X - 0.1, A > Y + 0.1)

nonneg = Tuple(*[i.gts - i.lts for i in conditions])

>>> any(i.max<0 for i in nonneg.subs(reps))
False

# let's change Y so it gives an invalid condition

>>> reps[Y]=AccumBounds(50,100)
>>> any(i.max<0 for i in nonneg.subs(reps))
True

现在让我们看看

Y
的临界范围是多少:

>>> Tuple(*[i for i in nonneg.subs({i:j for i,j in reps.items() if i!=Y}) if i.has(Y)])
(-Y + AccumBounds(-5, 45), -Y + AccumBounds(-50, 50), -Y + AccumBounds(-50.1, 49.9))
>>> _.replace(lambda x:x.is_Float, lambda x:Rational(str(x)))
(-Y + AccumBounds(-5, 45), -Y + AccumBounds(-50, 50), -Y + AccumBounds(-501/10, 499/10))
>>> [solve(i)[0] for i in _] # pretend the expressions are less trivial
[AccumBounds(-5, 45), AccumBounds(-50, 50), AccumBounds(-501/10, 499/10)]
>>> Intersection(*[Interval(i.min,i.max) for i in _])
Interval(-5, 45)

所以

Y
一定在那个区间内。

这是一个有趣的问题,特别是考虑到乘法变量超出范围以及迭代细化是否可以解决这个问题。

这感觉有点像线性优化问题。我想知道,对于任何在其范围内具有负数的变量,您是否可以将其替换为虚拟的

x
,这样
x = variable + min
。然后,您将拥有必须全部为非负的符号,并且可以尝试在给定约束下最小化符号的总和。这对我来说是相当未知的领域……这只是一个想法。

from sympy.solvers.simplex import lpmin
>>> lpmin(sum(v),[i.gts>=i.lts for i in conditions]+[i>=reps[i].min for 
i in v]+[i<=reps[i].max for i in v])
(-149.900000000000, {A: -49.9000000000000, X: 0, Y: -50, Z: -50})
>>> lpmax(sum(v),[i.gts>=i.lts for i in conditions]+[i>=reps[i].min for 
i in v]+[i<=reps[i].max for i in v])
(139.900000000000, {A: 49.9000000000000, X: 50, Y: 0, Z: 40})

注意如何修改

A
Z
范围以最大化或最小化变量的总和。

© www.soinside.com 2019 - 2024. All rights reserved.