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

时间:2022-06-23 20:20:16

I have a system of ODEs written in sympy:

我有一个同意的ODE系统:

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]

I would like to convert this into a vector valued function, accepting a 1D numpy array of the x value, a 1D numpy array of the k values, returning a 1D numpy array of the equations evaluated at those points. The signature would look something like this:

我想将其转换为向量值函数,接受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)

The reason I want something like this is to give this function to scipy.integrate.odeint, so it needs to be fast.

我想要这样的东西的原因是将此函数提供给scipy.integrate.odeint,因此它需要很快。

Attempt 1: subs

尝试1:潜艇

Of course, I can write a wrapper around 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])

But this is super slow, especially for my real system which is much bigger and is run many times. I need to compile this operation to C code.

但这是超级慢的,特别是对于我的真实系统,它更大,并且运行了很多次。我需要将此操作编译为C代码。

Attempt 2: theano

尝试2:theano

I can get close with sympy's integration with theano:

我可以通过sympy与theano的整合来接近:

from sympy.printing.theanocode import theano_function

f = theano_function(xs + ks, syms)

def my_odes(x, k):
    return np.array(f(*np.concatenate([x,k]))))

This compiles each expression, but all this packing and unpacking of the inputs and outputs slows it back down. The function returned by theano_function accepts numpy arrays as arguments, but it needs one array for each symbol rather than one element for each symbol. This is the same behavior for functify and ufunctify as well. I don't need the broadcast behavior; I need it to interpret each element of the array as a different symbol.

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

Attempt 3: DeferredVector

尝试3:DeferredVector

If I use DeferredVector I can make a function that accepts numpy arrays, but I can't compile it to C code or return a numpy array without packaging it myself.

如果我使用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])

Using DeferredVector I do not need to unpack the inputs, but I still need to pack the outputs. Also, I can use lambdify, but the ufuncify and theano_function versions perish, so no fast C code is generated.

使用DeferredVector我不需要解压缩输入,但我仍然需要打包输出。另外,我可以使用lambdify,但是ufuncify和theano_function版本会消失,因此不会生成快速的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

2 个解决方案

#1


8  

You can use the sympy function lambdify. For example,

你可以使用sympy函数lambdify。例如,

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 is printed when the script is run.

运行脚本时会打印True。

It is much faster (note the change in units of the timing results):

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

In [79]: t = np.linspace(0, 10, 1001)

In [80]: %timeit sol = odeint(my_odes, init, t, args=((k1, k2),))
1 loops, best of 3: 239 ms per loop

In [81]: %timeit sol = odeint(lambdified_odes, init, t, args=((k1, k2),))
1000 loops, best of 3: 610 µs per loop

#2


1  

I wrote a module named JiTCODE, which is tailored to problems such as yours. It takes symbolic expressions, converts them to C code, wraps a Python extension around it, compiles this, and loads it for use with scipy.integrate.ode or scipy.integrate.solve_ivp.

我写了一个名为JiTCODE的模块,它适用于像你这样的问题。它需要符号表达式,将它们转换为C代码,在其周围包装Python扩展,编译它,并加载它以与scipy.integrate.ode或scipy.integrate.solve_ivp一起使用。

Your example would look like this:

你的例子看起来像这样:

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)

You can then use ODE pretty much like an instance of scipy.integrate.ode.

然后,您可以像使用scipy.integrate.ode的实例一样使用ODE。

While you would not need this for your application, you can also extract and use the compiled function:

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

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))

Note that in contrast to your specifications, k is not passed as a NumPy array. For most ODE applications, this should not be relevant, because it is more efficient to hardcode the control parameters.

请注意,与您的规范相反,k不会作为NumPy数组传递。对于大多数ODE应用程序,这应该不相关,因为硬编码控制参数更有效。

Finally, note that for this small example, you may not get the best performance due to the overheads of scipy.integrate.ode or scipy.integrate.solve_ivp (also see SciPy Issue #8257 or this answer of mine). For large differential equations (as you have), this overhead becomes irrelevant.

最后,请注意,对于这个小例子,由于scipy.integrate.ode或scipy.integrate.solve_ivp的开销,您可能无法获得最佳性能(另请参阅SciPy问题#8257或我的这个答案)。对于大型微分方程(如你所知),这种开销变得无关紧要。

#1


8  

You can use the sympy function lambdify. For example,

你可以使用sympy函数lambdify。例如,

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 is printed when the script is run.

运行脚本时会打印True。

It is much faster (note the change in units of the timing results):

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

In [79]: t = np.linspace(0, 10, 1001)

In [80]: %timeit sol = odeint(my_odes, init, t, args=((k1, k2),))
1 loops, best of 3: 239 ms per loop

In [81]: %timeit sol = odeint(lambdified_odes, init, t, args=((k1, k2),))
1000 loops, best of 3: 610 µs per loop

#2


1  

I wrote a module named JiTCODE, which is tailored to problems such as yours. It takes symbolic expressions, converts them to C code, wraps a Python extension around it, compiles this, and loads it for use with scipy.integrate.ode or scipy.integrate.solve_ivp.

我写了一个名为JiTCODE的模块,它适用于像你这样的问题。它需要符号表达式,将它们转换为C代码,在其周围包装Python扩展,编译它,并加载它以与scipy.integrate.ode或scipy.integrate.solve_ivp一起使用。

Your example would look like this:

你的例子看起来像这样:

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)

You can then use ODE pretty much like an instance of scipy.integrate.ode.

然后,您可以像使用scipy.integrate.ode的实例一样使用ODE。

While you would not need this for your application, you can also extract and use the compiled function:

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

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))

Note that in contrast to your specifications, k is not passed as a NumPy array. For most ODE applications, this should not be relevant, because it is more efficient to hardcode the control parameters.

请注意,与您的规范相反,k不会作为NumPy数组传递。对于大多数ODE应用程序,这应该不相关,因为硬编码控制参数更有效。

Finally, note that for this small example, you may not get the best performance due to the overheads of scipy.integrate.ode or scipy.integrate.solve_ivp (also see SciPy Issue #8257 or this answer of mine). For large differential equations (as you have), this overhead becomes irrelevant.

最后,请注意,对于这个小例子,由于scipy.integrate.ode或scipy.integrate.solve_ivp的开销,您可能无法获得最佳性能(另请参阅SciPy问题#8257或我的这个答案)。对于大型微分方程(如你所知),这种开销变得无关紧要。