数值计算¶
像 SymPy 这样的符号计算机代数系统有助于构建和操作数学表达式。不幸的是,当需要对数值数据评估这些表达式时,符号系统通常性能不佳。
幸运的是,SymPy 提供了许多易于使用的钩子,可以与其他数值系统集成,允许你在 SymPy 中创建数学表达式,然后将它们发送到你选择的数值系统中。本页文档记录了许多可用的选项,包括 math
库、流行的数组计算包 numpy
、在 Fortran
或 C
中的代码生成,以及使用数组编译器 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
模块包含了一些有助于高效计算的方法。
binary_function 方法自动执行了将 SymPy 表达式自动包装并附加到
Function
对象所需的步骤,使用implemented_function()
。ufuncify 生成一个支持在 numpy 数组上进行广播的二进制函数,使用不同的后端,这些后端比
subs/evalf
和lambdify
更快。
所有上述内容的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 |