如何更好地使用Cython来更快地求解微分方程?

时间:2017-03-16 15:18:07

标签: python scipy cython differential-equations scientific-computing

我想降低Scipy的odeint解决差异所需的时间 方程。

为了练习,我使用Python in scientific computations中涵盖的示例作为模板。因为odeint将函数f作为参数,所以我将此函数编写为静态类型的Cython版本并希望 odeint的运行时间会明显减少。

函数f包含在名为ode.pyx的文件中,如下所示:

import numpy as np
cimport numpy as np
from libc.math cimport sin, cos

def f(y, t, params):
  cdef double theta = y[0], omega = y[1]
  cdef double Q = params[0], d = params[1], Omega = params[2]
  cdef double derivs[2]
  derivs[0] = omega
  derivs[1] = -omega/Q + np.sin(theta) + d*np.cos(Omega*t)
  return derivs

def fCMath(y, double t, params):
  cdef double theta = y[0], omega = y[1]
  cdef double Q = params[0], d = params[1], Omega = params[2]
  cdef double derivs[2]
  derivs[0] = omega
  derivs[1] = -omega/Q + sin(theta) + d*cos(Omega*t)
  return derivs
然后我创建了一个文件setup.py来编译函数:

from distutils.core import setup
from Cython.Build import cythonize

setup(ext_modules=cythonize('ode.pyx'))

解决微分方程的脚本(也包含Python 版本f)称为solveODE.py,其外观如下:

import ode
import numpy as np
from scipy.integrate import odeint
import time

def f(y, t, params):
    theta, omega = y
    Q, d, Omega = params
    derivs = [omega,
             -omega/Q + np.sin(theta) + d*np.cos(Omega*t)]
    return derivs

params = np.array([2.0, 1.5, 0.65])
y0 = np.array([0.0, 0.0])
t = np.arange(0., 200., 0.05)

start_time = time.time()
odeint(f, y0, t, args=(params,))
print("The Python Code took: %.6s seconds" % (time.time() - start_time))

start_time = time.time()
odeint(ode.f, y0, t, args=(params,))
print("The Cython Code took: %.6s seconds ---" % (time.time() - start_time))

start_time = time.time()
odeint(ode.fCMath, y0, t, args=(params,))
print("The Cython Code incorpoarting two of DavidW_s suggestions took: %.6s seconds ---" % (time.time() - start_time))
然后我跑了:

python setup.py build_ext --inplace
python solveODE.py 

在终端。

python版本的时间约为0.055秒, 而Cython版本大约需要0.04秒。

是否有人建议改进我的解决方案 微分方程,最好不用修改odeint例程本身,用Cython?

修改

我在两个文件ode.pyxsolveODE.py中加入了DavidW的建议。使用这些建议运行代码大约需要0.015秒。

4 个答案:

答案 0 :(得分:3)

最容易做出的改变(可能会让你获得很多)是使用C数学库sincos对单个数字而不是数字进行操作。对numpy的调用以及计算出它不是一个数组的时间相当昂贵。

from libc.math cimport sin, cos

    # later
    -omega/Q + sin(theta) + d*cos(Omega*t)

我很想为输入d分配一个类型(在不改变界面的情况下,很容易输入其他输入):

def f(y, double t, params):

我想我也会像你在Python版本中那样返回一个列表。我不认为你通过使用C阵列获得了很多。

答案 1 :(得分:3)

tldr;使用numba.jit加速3倍......

我对cython没有多少经验,但我的机器似乎得到了严格的python版本的类似计算时间,所以我们应该能够大致比较苹果和苹果。我使用numba来编译函数f(我稍微重写了一下,使它在编译器中更好用)。

def f(y, t, params):
    return np.array([y[1], -y[1]/params[0] + np.sin(y[0]) + params[1]*np.cos(params[2]*t)])

numba_f = numba.jit(f)

放入numba_f代替您的ode.f给我这个输出......

The Python Code took: 0.0468 seconds
The Numba Code took: 0.0155 seconds

然后我想知道我是否可以复制odeint并使用numba进行编译以进一步加快速度......(我不能)

这是我的Runge-Kutta数值微分方程积分器:

#function f is provided inline (not as an arg)
def runge_kutta(y0, steps, dt, args=()): #improvement on euler's method. *note: time steps given in number of steps and dt
    Y = np.empty([steps,y0.shape[0]])
    Y[0] = y0
    t = 0
    n = 0
    for n in range(steps-1):
        #calculate coeficients
        k1 = f(Y[n], t, args) #(euler's method coeficient) beginning of interval
        k2 = f(Y[n] + (dt * k1 / 2), t + (dt/2), args) #interval midpoint A
        k3 = f(Y[n] + (dt * k2 / 2), t + (dt/2), args) #interval midpoint B
        k4 = f(Y[n] + dt * k3, t + dt, args) #interval end point

        Y[n + 1] = Y[n] + (dt/6) * (k1 + 2*k2 + 2*k3 + k4) #calculate Y(n+1)
        t += dt #calculate t(n+1)
    return Y

天真循环函数通常是编译后最快的函数,尽管这可能会以更快的速度重新构造。我应该注意,这给出了与odeint不同的答案,在大约2000步之后偏离了.001,并且在3000之后完全不同。对于函数的numba版本,我只是替换了{{1}与f一起使用numba_f作为装饰器添加了编译。在这种情况下,正如预期的那样,纯python版本非常慢,但是numba版本并不比使用@numba.jit的numba更快(再次,ymmv)。

odeint

这是一个提前编译的例子。我在这台计算机上没有必要的工具链来编译,我没有管理员来安装它,所以这给了我一个错误,我没有所需的编译器,但它应该工作。

using custom integrator
The Python Code took: 0.2340 seconds
The Numba Code took: 0.0156 seconds

答案 2 :(得分:1)

如果其他人使用其他模块回答这个问题,我可能也会说:

我是JiTCODE的作者,它接受用SymPy符号编写的ODE,然后将此ODE转换为Python模块的C代码,编译此C代码,加载结果并将其用作衍生物SciPy’s ODE。您转换为JiTCODE的示例如下所示:

from jitcode import jitcode, provide_basic_symbols
import numpy as np
from sympy import sin, cos
import time

Q = 2.0
d = 1.5
Ω = 0.65

t, y = provide_basic_symbols()

f = [
    y(1),
    -y(1)/Q + sin(y(0)) + d*cos(Ω*t)
    ]

initial_state = np.array([0.0,0.0])

ODE = jitcode(f)
ODE.set_integrator("lsoda")
ODE.set_initial_value(initial_state,0.0)

start_time = time.time()
data = np.vstack(ODE.integrate(T) for T in np.arange(0.05, 200., 0.05))
end_time = time.time()
print("JiTCODE took: %.6s seconds" % (end_time - start_time))

这需要0.11秒,与基于odeint的解决方案相比非常慢,但这不是由于实际的集成,而是处理结果的方式:虽然odeint直接创建了数组内部有效,这是通过Python在这里完成的。根据您的工作情况,这可能是一个至关重要的缺点,但这很快就会变得与粗略的采样或更大的微分方程无关。

所以,让我们删除数据集合,然后通过用以下内容替换最后一行来查看集成:

ODE = jitcode(f)
ODE.set_integrator("lsoda", max_step=0.05, nsteps=1e10)
ODE.set_initial_value(initial_state,0.0)

start_time = time.time()
ODE.integrate(200.0)
end_time = time.time()
print("JiTCODE took: %.6s seconds" % (end_time - start_time))

请注意,我设置max_step=0.05以强制集成商至少执行与示例中相同的步骤,并确保唯一的区别是集成的结果不会存储到某个数组中。这运行时间为0.010秒。

答案 3 :(得分:0)

NumbaLSODA 需要 0.00088 秒(比 Cython 快 17 倍)。

from NumbaLSODA import lsoda_sig, lsoda
import numba as nb
import numpy as np
import time

@nb.cfunc(lsoda_sig)
def f(t, y_, dy, p_):
    p = nb.carray(p_, (3,))
    y = nb.carray(y_, (2,))
    theta, omega = y
    Q, d, Omega = p
    dy[0] = omega
    dy[1] = -omega/Q + np.sin(theta) + d*np.cos(Omega*t)

funcptr = f.address # address to ODE function
y0 = np.array([0.0, 0.0])
data = np.array([2.0, 1.5, 0.65])
t = np.arange(0., 200., 0.05)

start_time = time.time()
usol, success = lsoda(funcptr, y0, t, data = data)
print("NumbaLSODA took: %.8s seconds ---" % (time.time() - start_time))

结果

NumbaLSODA took: 0.000880 seconds ---