Python 将sympy表达式转换为numpy数组的函数

Python 将sympy表达式转换为numpy数组的函数,python,numpy,scipy,sympy,Python,Numpy,Scipy,Sympy,我有一套用交响乐写成的颂歌体系: from sympy.parsing.sympy_parser import parse_expr xs = symbols('x1 x2') ks = symbols('k1 k2') strs = ['-k1 * x1**2 + k2 * x2', 'k1 * x1**2 - k2 * x2'] syms = [parse_expr(item) for item in strs] 我想把它转换成一个向量值函数,接受一个x值的1D numpy数组,一个k值

我有一套用交响乐写成的颂歌体系:

from sympy.parsing.sympy_parser import parse_expr

xs = symbols('x1 x2')
ks = symbols('k1 k2')
strs = ['-k1 * x1**2 + k2 * x2', 'k1 * x1**2 - k2 * x2']
syms = [parse_expr(item) for item in strs]
我想把它转换成一个向量值函数,接受一个x值的1D numpy数组,一个k值的1D numpy数组,返回一个在这些点上计算的方程的1D numpy数组。签名看起来像这样:

import numpy as np
x = np.array([3.5, 1.5])
k = np.array([4, 2])
xdot = my_odes(x, k)
from jitcode import y, jitcode
from sympy.parsing.sympy_parser import parse_expr
from sympy import symbols

xs = symbols('x1 x2')
ks = symbols('k1 k2')
strs = ['-k1 * x1**2 + k2 * x2', 'k1 * x1**2 - k2 * x2']
syms = [parse_expr(item) for item in strs]

substitutions = {x_i:y(i) for i,x_i in enumerate(xs)}
f = [sym.subs(substitutions) for sym in syms]

ODE = jitcode(f,control_pars=ks)
我想要这样的东西的原因是将此函数赋予
scipy.integrate.odeint
,因此它需要快速

尝试1:subs

当然,我可以围绕
subs
编写一个包装器:

def my_odes(x, k):
    all_dict = dict(zip(xs, x))
    all_dict.update(dict(zip(ks, k)))
    return np.array([sym.subs(all_dict) for sym in syms])
但这是超慢的,特别是对于我的真实系统,它更大,运行了很多次。我需要把这个操作编译成C代码

尝试2:theano

我可以接近:

这会编译每个表达式,但所有这些输入和输出的打包和解包都会减慢它的速度。
theano_函数返回的函数
接受numpy数组作为参数,但每个符号需要一个数组,而不是每个符号需要一个元素。这与
functify
ufunctify
的行为相同。我不需要广播行为;我需要它将数组的每个元素解释为不同的符号

尝试3:延迟向量

如果我使用
DeferredVector
,我可以创建一个接受numpy数组的函数,但如果不自己打包,我就无法将它编译成C代码或返回numpy数组

import numpy as np
import sympy as sp
from sympy import DeferredVector

x = sp.DeferredVector('x')
k =  sp.DeferredVector('k')
deferred_syms = [s.subs({'x1':x[0], 'x2':x[1], 'k1':k[0], 'k2':k[1]}) for s in syms]
f = [lambdify([x,k], s) for s in deferred_syms]

def my_odes(x, k):
    return np.array([f_i(x, k) for f_i in f])
使用
DeferredVector
我不需要解压缩输入,但仍然需要打包输出。另外,我可以使用
lambdify
,但是
ufuncify
ano_函数
版本会失效,因此不会生成快速C代码

from sympy.utilities.autowrap import ufuncify
f = [ufuncify([x,k], s) for s in deferred_syms] # error

from sympy.printing.theanocode import theano_function
f = theano_function([x,k], deferred_syms) # error

您可以使用sympy函数。比如说,

from sympy import symbols, lambdify
from sympy.parsing.sympy_parser import parse_expr
import numpy as np

xs = symbols('x1 x2')
ks = symbols('k1 k2')
strs = ['-k1 * x1**2 + k2 * x2', 'k1 * x1**2 - k2 * x2']
syms = [parse_expr(item) for item in strs]

# Convert each expression in syms to a function with signature f(x1, x2, k1, k2):
funcs = [lambdify(xs + ks, f) for f in syms]


# This is not exactly the same as the `my_odes` in the question.
# `t` is included so this can be used with `scipy.integrate.odeint`.
# The value returned by `sym.subs` is wrapped in a call to `float`
# to ensure that the function returns python floats and not sympy Floats.
def my_odes(x, t, k):
    all_dict = dict(zip(xs, x))
    all_dict.update(dict(zip(ks, k)))
    return np.array([float(sym.subs(all_dict)) for sym in syms])

def lambdified_odes(x, t, k):
    x1, x2 = x
    k1, k2 = k
    xdot = [f(x1, x2, k1, k2) for f in funcs]
    return xdot


if __name__ == "__main__":
    from scipy.integrate import odeint

    k1 = 0.5
    k2 = 1.0
    init = [1.0, 0.0]
    t = np.linspace(0, 1, 6)
    sola = odeint(lambdified_odes, init, t, args=((k1, k2),))
    solb = odeint(my_odes, init, t, args=((k1, k2),))
    print(np.allclose(sola, solb))
True
在脚本运行时打印

速度要快得多(注意计时结果单位的变化):

我写了一篇文章,专门针对像你这样的问题。 它接受符号表达式,将它们转换为C代码,围绕它包装一个Python扩展,编译它,并加载它以与
scipy.integrate.ode
scipy.integrate.solve\u ivp
一起使用

您的示例如下所示:

import numpy as np
x = np.array([3.5, 1.5])
k = np.array([4, 2])
xdot = my_odes(x, k)
from jitcode import y, jitcode
from sympy.parsing.sympy_parser import parse_expr
from sympy import symbols

xs = symbols('x1 x2')
ks = symbols('k1 k2')
strs = ['-k1 * x1**2 + k2 * x2', 'k1 * x1**2 - k2 * x2']
syms = [parse_expr(item) for item in strs]

substitutions = {x_i:y(i) for i,x_i in enumerate(xs)}
f = [sym.subs(substitutions) for sym in syms]

ODE = jitcode(f,control_pars=ks)
然后可以使用
ODE
,就像使用
scipy.integrate.ODE
的实例一样

虽然应用程序不需要此函数,但也可以提取并使用编译后的函数:

ODE.compile_C()
import numpy as np
x = np.array([3.5, 1.5])
k = np.array([4, 2])
print(ODE.f(0.0,x,*k))
请注意,与您的规范不同,
k
不是作为NumPy数组传递的。对于大多数ODE应用程序,这不应该相关,因为硬编码控制参数更有效

最后,请注意,对于这个小示例,由于
scipy.integrate.ode
scipy.integrate.solve_ivp
的开销,您可能无法获得最佳性能(另请参见或)。
对于大型微分方程(如您所见),此开销变得无关紧要。

这确实比我的任何一个版本都要快。对于
subs
,我得到了120ms;对于
theano_函数
,我得到了8.7ms;对于
lambdify
,我得到了0.6ms。如果我们能够弄清楚如何用C进行整个计算,我们应该能够节省更多的时间,不是用python打包和解包,而是列出每个迭代。如果我将
lambdafied_odes
中的调用转换为使用splatting
[f(*np.concatenate([x,k])来表示函数中的f]
,这在状态数变化时是必要的,时间增加一点到1.4毫秒仍然是最好的。如果你真的想要速度,你可以尝试
autowrap
ufuncify
。据我所知,
ufuncify
autowrap
如果你得到一个numpy数组,都需要解包输入参数,如果你想要一个numpy数组,则需要重新打包输出值。