python sympy - 如何处理表达式以保护它们免受特定计算
python sympy - how to process expressions in order to protect them from specific evaluations
sympy
似乎默认评估表达式,这在自动评估对数值稳定性产生负面影响的情况下是有问题的。我需要一种方法来控制评估的内容以保持稳定性。
我知道的唯一官方机制是 UnevaluatedExpr
class,但这个解决方案对我的目的来说是有问题的。我的代码的用户不应受到任何数值稳定性考虑的负担。他们只是想输入一个表达式,代码需要完成所有剩下的工作。让他们分析自己表达式的数值稳定性不是一种选择。它需要自动完成。
首先,我试图通过 monkeypatching 来获得对 sympify()
的控制权,因为它似乎是大多数导致不需要的评估的调用背后的罪魁祸首,但我只是赶上了所有调用,而无法以我想要的方式真正控制它们。我在那里碰壁太多,我什至不知道从哪里开始。
正如您可能想象的那样,修改 sympy
本身也不是一种选择,因为我不可能要求用户为他们的本地 sympy
安装制作一些奇特的补丁。
接下来我发现可以说
with evaluate(False): doSomeStuffToExpression(expr)
这似乎无论如何都将 evaluate=False
推向 sympy
的咽喉。
然而,这意味着它从根本上停用了所有评估并且不允许任何精细控制。
具体来说,当 sympy.exp
中有 Add
时,我想停用评估
所以第三次尝试是修改表达式树。基本上开发一种方法,该方法采用表达式,遍历它并在需要时自动用 UnevaluatedExpr
包装 args
(记住:我不能手动执行此操作打扰用户)
所以我写了下面的代码来测试新的方法:
from sympy.core.expr import UnevaluatedExpr
from sympy.core.symbol import Symbol
import sympy as sp
from sympy.core.numbers import Float
x, z = sp.symbols('x z')
#expr = (x + 2.*x)/4. + sp.exp((x+sp.UnevaluatedExpr(32.))/6.)
expr = sp.sympify('(x + 2.*x)/4. + exp((x+32.)/6.)', evaluate=False)
expr_ = expr.subs(x, z)
print(expr)
print(expr_)
print('///////////\n')
def prep(expr, exp_depth = 0):
# once we are inside UnevaluatedExpr, we need to continue to traverse
# down to the Symbol and also wrap it with UnevaluatedExpr
if isinstance(expr, UnevaluatedExpr):
for arg in expr.args:
newargs = []
for arg_inside in arg.args:
if isinstance(arg_inside, Symbol) or isinstance(arg_inside, Float):
newargs.append(UnevaluatedExpr(arg_inside))
else:
newargs.append(arg_inside)
arg._args = tuple(newargs)
for arg_inside in arg.args:
prep(arg_inside, exp_depth = exp_depth + 1)
return
original_args = expr.args
# if args empty
if not original_args: return
# check if we just entered exp
is_exp = (expr.func == sp.exp)
print('\n-----')
print('expression\t\t-->', expr)
print('func || args\t\t-->', expr.func, ' || ', original_args)
print('is it exp right now?\t-->', is_exp)
print('inside exp?\t-->', exp_depth > 0)
# if we just received exp or if we are inside exp
if is_exp or exp_depth > 0:
newargs = []
for arg in original_args:
if isinstance(arg, sp.Add):
newargs.append(UnevaluatedExpr(arg))
else:
newargs.append(arg)
expr._args= tuple(newargs)
for arg in expr.args:
prep(arg, exp_depth = exp_depth + 1)
else:
for arg in original_args: prep(arg, exp_depth)
prep(expr)
print('///////////\n')
print(expr)
substituted = expr.subs(x, z)
print("substitution after prep still does not work:\n", substituted)
wewantthis = expr.subs(x, UnevaluatedExpr(z))
print("we want:\n", wewantthis)
print('///////////\n')
然而,输出令人失望,因为 subs()
再次触发可怕的评估,尽管在需要的地方将 args 包装在 UnevaluatedExpr
中。或者说我认为需要包装的地方。
出于某种原因 subs()
完全忽略了我的更改。
所以问题是:最后一种方法是否有任何希望(也许我在遍历树时仍然遗漏了一些东西) - 如果我的方法没有希望,我还应该如何实现禁用的目标在 sympy.exp
(指数函数)
中遇到 Add
时对特定 Symbol
的求值
PS:
我可能还应该提到,出于某些令人费解的原因,以下是可行的(但正如我所提到的,这是我不希望的手动解决方案)
expr = (x + 2.*x)/4. + sp.exp((x+sp.UnevaluatedExpr(32.))/6.)
expr_ = expr.subs(x, z)
print(expr)
print(expr_)
这里我们成功阻止了sp.exp
里面的Add
的求值
输出:
0.75*x + exp(0.166666666666667*(x + 32.0))
0.75*z + exp(0.166666666666667*(z + 32.0))
编辑 0:
解决方案应该允许使用浮点数。例如,一些值可能描述物理特性,测量超出整数的精度。我需要能够允许这些。
用符号替换浮点数也是有问题的,因为它会使表达式的处理或以后使用这些表达式变得非常复杂
我不确定,但我认为您遇到的问题与通过 distribute
上下文管理器控制的 Add 自动分配数字有关:
In [326]: e1 = 2*(x + 1)
In [327]: e1
Out[327]: 2⋅x + 2
In [328]: from sympy.core.parameters import distribute
In [329]: with distribute(False):
...: e2 = 2*(x + 1)
...:
In [330]: e2
Out[330]: 2⋅(x + 1)
自动分发行为是可以在 sympy 中更改的好东西。它只是不容易改变,因为它是一个如此低级的操作,而且已经这样了很长时间(它会破坏很多人的代码)。
您看到的评估的其他部分特定于您使用浮点数这一事实,不会发生在 Rational 或符号上,例如:
In [337]: exp(2*(x + 1))
Out[337]:
2⋅x + 2
ℯ
In [338]: exp(2.0*(x + 1))
Out[338]:
2.0⋅x
7.38905609893065⋅ℯ
In [339]: exp(y*(x + 1))
Out[339]:
y⋅(x + 1)
ℯ
您可以使用 nsimplify
将有理数转换为浮点数以避免这种情况,例如:
In [340]: parse_expr('exp(2.0*(x + 1))', evaluate=False)
Out[340]:
2.0⋅(x + 1)
ℯ
In [341]: parse_expr('exp(2.0*(x + 1))', evaluate=False).subs(x, z)
Out[341]:
2.0⋅z
7.38905609893065⋅ℯ
In [342]: nsimplify(parse_expr('exp(2.0*(x + 1))', evaluate=False))
Out[342]:
2⋅x + 2
ℯ
In [343]: nsimplify(parse_expr('exp(2.0*(x + 1))', evaluate=False)).subs(x, z)
Out[343]:
2⋅z + 2
ℯ
另一种可能性是使用符号并延迟替换任何值,直到进行数值评估。这是从 evalf 获得最准确结果的方法:
In [344]: exp(y*(z + 1)).evalf(subs={y:1, z:2})
Out[344]: 20.0855369231877
sympy
似乎默认评估表达式,这在自动评估对数值稳定性产生负面影响的情况下是有问题的。我需要一种方法来控制评估的内容以保持稳定性。
我知道的唯一官方机制是 UnevaluatedExpr
class,但这个解决方案对我的目的来说是有问题的。我的代码的用户不应受到任何数值稳定性考虑的负担。他们只是想输入一个表达式,代码需要完成所有剩下的工作。让他们分析自己表达式的数值稳定性不是一种选择。它需要自动完成。
首先,我试图通过 monkeypatching 来获得对 sympify()
的控制权,因为它似乎是大多数导致不需要的评估的调用背后的罪魁祸首,但我只是赶上了所有调用,而无法以我想要的方式真正控制它们。我在那里碰壁太多,我什至不知道从哪里开始。
正如您可能想象的那样,修改 sympy
本身也不是一种选择,因为我不可能要求用户为他们的本地 sympy
安装制作一些奇特的补丁。
接下来我发现可以说
with evaluate(False): doSomeStuffToExpression(expr)
这似乎无论如何都将 evaluate=False
推向 sympy
的咽喉。
然而,这意味着它从根本上停用了所有评估并且不允许任何精细控制。
具体来说,当 sympy.exp
Add
时,我想停用评估
所以第三次尝试是修改表达式树。基本上开发一种方法,该方法采用表达式,遍历它并在需要时自动用 UnevaluatedExpr
包装 args
(记住:我不能手动执行此操作打扰用户)
所以我写了下面的代码来测试新的方法:
from sympy.core.expr import UnevaluatedExpr
from sympy.core.symbol import Symbol
import sympy as sp
from sympy.core.numbers import Float
x, z = sp.symbols('x z')
#expr = (x + 2.*x)/4. + sp.exp((x+sp.UnevaluatedExpr(32.))/6.)
expr = sp.sympify('(x + 2.*x)/4. + exp((x+32.)/6.)', evaluate=False)
expr_ = expr.subs(x, z)
print(expr)
print(expr_)
print('///////////\n')
def prep(expr, exp_depth = 0):
# once we are inside UnevaluatedExpr, we need to continue to traverse
# down to the Symbol and also wrap it with UnevaluatedExpr
if isinstance(expr, UnevaluatedExpr):
for arg in expr.args:
newargs = []
for arg_inside in arg.args:
if isinstance(arg_inside, Symbol) or isinstance(arg_inside, Float):
newargs.append(UnevaluatedExpr(arg_inside))
else:
newargs.append(arg_inside)
arg._args = tuple(newargs)
for arg_inside in arg.args:
prep(arg_inside, exp_depth = exp_depth + 1)
return
original_args = expr.args
# if args empty
if not original_args: return
# check if we just entered exp
is_exp = (expr.func == sp.exp)
print('\n-----')
print('expression\t\t-->', expr)
print('func || args\t\t-->', expr.func, ' || ', original_args)
print('is it exp right now?\t-->', is_exp)
print('inside exp?\t-->', exp_depth > 0)
# if we just received exp or if we are inside exp
if is_exp or exp_depth > 0:
newargs = []
for arg in original_args:
if isinstance(arg, sp.Add):
newargs.append(UnevaluatedExpr(arg))
else:
newargs.append(arg)
expr._args= tuple(newargs)
for arg in expr.args:
prep(arg, exp_depth = exp_depth + 1)
else:
for arg in original_args: prep(arg, exp_depth)
prep(expr)
print('///////////\n')
print(expr)
substituted = expr.subs(x, z)
print("substitution after prep still does not work:\n", substituted)
wewantthis = expr.subs(x, UnevaluatedExpr(z))
print("we want:\n", wewantthis)
print('///////////\n')
然而,输出令人失望,因为 subs()
再次触发可怕的评估,尽管在需要的地方将 args 包装在 UnevaluatedExpr
中。或者说我认为需要包装的地方。
出于某种原因 subs()
完全忽略了我的更改。
所以问题是:最后一种方法是否有任何希望(也许我在遍历树时仍然遗漏了一些东西) - 如果我的方法没有希望,我还应该如何实现禁用的目标在 sympy.exp
(指数函数)
Add
时对特定 Symbol
的求值
PS:
我可能还应该提到,出于某些令人费解的原因,以下是可行的(但正如我所提到的,这是我不希望的手动解决方案)
expr = (x + 2.*x)/4. + sp.exp((x+sp.UnevaluatedExpr(32.))/6.)
expr_ = expr.subs(x, z)
print(expr)
print(expr_)
这里我们成功阻止了sp.exp
Add
的求值
输出:
0.75*x + exp(0.166666666666667*(x + 32.0))
0.75*z + exp(0.166666666666667*(z + 32.0))
编辑 0:
解决方案应该允许使用浮点数。例如,一些值可能描述物理特性,测量超出整数的精度。我需要能够允许这些。
用符号替换浮点数也是有问题的,因为它会使表达式的处理或以后使用这些表达式变得非常复杂
我不确定,但我认为您遇到的问题与通过 distribute
上下文管理器控制的 Add 自动分配数字有关:
In [326]: e1 = 2*(x + 1)
In [327]: e1
Out[327]: 2⋅x + 2
In [328]: from sympy.core.parameters import distribute
In [329]: with distribute(False):
...: e2 = 2*(x + 1)
...:
In [330]: e2
Out[330]: 2⋅(x + 1)
自动分发行为是可以在 sympy 中更改的好东西。它只是不容易改变,因为它是一个如此低级的操作,而且已经这样了很长时间(它会破坏很多人的代码)。
您看到的评估的其他部分特定于您使用浮点数这一事实,不会发生在 Rational 或符号上,例如:
In [337]: exp(2*(x + 1))
Out[337]:
2⋅x + 2
ℯ
In [338]: exp(2.0*(x + 1))
Out[338]:
2.0⋅x
7.38905609893065⋅ℯ
In [339]: exp(y*(x + 1))
Out[339]:
y⋅(x + 1)
ℯ
您可以使用 nsimplify
将有理数转换为浮点数以避免这种情况,例如:
In [340]: parse_expr('exp(2.0*(x + 1))', evaluate=False)
Out[340]:
2.0⋅(x + 1)
ℯ
In [341]: parse_expr('exp(2.0*(x + 1))', evaluate=False).subs(x, z)
Out[341]:
2.0⋅z
7.38905609893065⋅ℯ
In [342]: nsimplify(parse_expr('exp(2.0*(x + 1))', evaluate=False))
Out[342]:
2⋅x + 2
ℯ
In [343]: nsimplify(parse_expr('exp(2.0*(x + 1))', evaluate=False)).subs(x, z)
Out[343]:
2⋅z + 2
ℯ
另一种可能性是使用符号并延迟替换任何值,直到进行数值评估。这是从 evalf 获得最准确结果的方法:
In [344]: exp(y*(z + 1)).evalf(subs={y:1, z:2})
Out[344]: 20.0855369231877