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:

  1. 解决方案应该允许使用浮点数。例如,一些值可能描述物理特性,测量超出整数的精度。我需要能够允许这些。

  2. 用符号替换浮点数也是有问题的,因为它会使表达式的处理或以后使用这些表达式变得非常复杂

我不确定,但我认为您遇到的问题与通过 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