数值计算

像 SymPy 这样的符号计算机代数系统有助于构建和操作数学表达式。不幸的是,当需要对数值数据评估这些表达式时,符号系统通常性能不佳。

幸运的是,SymPy 提供了许多易于使用的钩子,可以与其他数值系统集成,允许你在 SymPy 中创建数学表达式,然后将它们发送到你选择的数值系统中。本页文档记录了许多可用的选项,包括 math 库、流行的数组计算包 numpy、在 FortranC 中的代码生成,以及使用数组编译器 Aesara

子目录/评估

Subs 是最慢但最简单的选项。它在 SymPy 的速度下运行。`` .subs(…).evalf() `` 方法可以用数值替换符号值,然后在 SymPy 中计算结果。

>>> from sympy import *
>>> from sympy.abc import x
>>> expr = sin(x)/x
>>> expr.evalf(subs={x: 3.14})
0.000507214304613640

这种方法很慢。只有在性能不是问题的情况下,才应该在生产环境中使用这种方法。你可以预期 .subs 需要几十微秒的时间。在原型设计时或如果你只是想一次性查看一个值时,这种方法可能会有用。

Lambdify

lambdify 函数将 SymPy 表达式转换为 Python 函数,利用了多种数值库。使用方法如下:

>>> from sympy import *
>>> from sympy.abc import x
>>> expr = sin(x)/x
>>> f = lambdify(x, expr)
>>> f(3.14)
0.000507214304614

这里 lambdify 创建了一个计算 f(x) = sin(x)/x 的函数。默认情况下,lambdify 依赖于 math 标准库中的实现。这种数值评估大约需要数百纳秒,比 .subs 方法快大约两个数量级。这是 SymPy 和纯 Python 之间的速度差异。

Lambdify 可以利用多种数值后端。默认情况下,它使用 math 库。然而,它也支持 mpmath,特别是 numpy。使用 numpy 库可以让生成的函数访问由编译的 C 代码支持的强大的矢量化 ufuncs。

>>> from sympy import *
>>> from sympy.abc import x
>>> expr = sin(x)/x
>>> f = lambdify(x, expr, "numpy")
>>> import numpy
>>> data = numpy.linspace(1, 10, 10000)
>>> f(data)
[ 0.84147098  0.84119981  0.84092844 ... -0.05426074 -0.05433146
 -0.05440211]

如果你有基于数组的数据,这可以带来显著的速度提升,大约每元素10纳秒。不幸的是,numpy会带来一些启动时间和几微秒的开销。

CuPy 是一个与 NumPy 兼容的数组库,主要运行在 CUDA 上,但对其他 GPU 制造商的支持也在不断增加。在许多情况下,它可以作为 numpy 的直接替代品使用。

>>> f = lambdify(x, expr, "cupy")
>>> import cupy as cp
>>> data = cp.linspace(1, 10, 10000)
>>> y = f(data) # perform the computation
>>> cp.asnumpy(y) # explicitly copy from GPU to CPU / numpy array
[ 0.84147098  0.84119981  0.84092844 ... -0.05426074 -0.05433146
 -0.05440211]

JAX 是 CuPy 的一个类似替代方案,通过即时编译为 XLA 提供 GPU 和 TPU 加速。在某些情况下,它也可以用作 numpy 的直接替代品。

>>> f = lambdify(x, expr, "jax")
>>> import jax.numpy as jnp
>>> data = jnp.linspace(1, 10, 10000)
>>> y = f(data) # perform the computation
>>> numpy.asarray(y) # explicitly copy to CPU / numpy array
array([ 0.84147096,  0.8411998 ,  0.84092844, ..., -0.05426079,
   -0.05433151, -0.05440211], dtype=float32)

uFuncify

autowrap 模块包含了一些有助于高效计算的方法。

  • autowrap 方法用于编译由 codegen 模块生成的代码,并将其封装以便在 Python 中使用。

  • binary_function 方法自动执行了将 SymPy 表达式自动包装并附加到 Function 对象所需的步骤,使用 implemented_function()

  • ufuncify 生成一个支持在 numpy 数组上进行广播的二进制函数,使用不同的后端,这些后端比 subs/evalflambdify 更快。

所有上述内容的API参考在此列出:sympy.utilities.autowrap()

Aesara

SymPy 与 Aesara 有着紧密的联系,Aesara 是一个数学数组编译器。SymPy 表达式可以轻松转换为 Aesara 图,然后使用 Aesara 编译器链进行编译。

>>> from sympy import *
>>> from sympy.abc import x
>>> expr = sin(x)/x
>>> from sympy.printing.aesaracode import aesara_function
>>> f = aesara_function([x], [expr])

如果需要数组广播或类型,那么 Aesara 需要这些额外信息。

>>> f = aesara_function([x], [expr], dims={x: 1}, dtypes={x: 'float64'})

Aesara 的代码生成系统比 SymPy 的 C/Fortran 代码打印机更为复杂。它处理公共子表达式并将编译到 GPU 上。Aesara 还支持 SymPy 矩阵和矩阵表达式对象。

那么我应该使用哪一个?

这里的选项按从最慢和最少依赖到最快和最多依赖的顺序列出。例如,如果你安装了 Aesara,那么这通常是最好的选择。如果你没有 Aesara 但有 f2py,那么你应该使用 ufuncify。如果你习惯使用带有 numpy 模块的 lambdify,但有一块 GPU,CuPy 和 JAX 可以在很少努力的情况下提供显著的速度提升。

工具

速度

品质

依赖项

subs/evalf

50微秒

简单

lambdify

1美元

标量函数

数学

lambdify-numpy

10纳秒

向量函数

numpy

ufuncify

10纳秒

复杂向量表达式

f2py, Cython

lambdify-cupy

10纳秒

GPU上的向量函数

cupy

lambdify-jax

10纳秒

CPU、GPU 和 TPU 上的向量函数

jax

Aesara

10纳秒

多种输出,CSE,GPUs

Aesara