First some context: I am trying to integrate a coupled ODE using scipy.integrate.odeint
very often with different initial conditions x_
and parameters r_
and d_
.
I am trying to make the integration faster by compiling the right hand side of the ODE ahead of time and trying to reduce the times I call the odeint
function.
I am trying to compile a python function using numba.pycc.CC
ahead of time. This works for simple functions such as:
import numpy
from numba.pycc import CC
cc = CC('x_test')
cc.verbose = True
@cc.export('x_test', 'f8(f8[:])')
def x_test(y):
return numpy.sum(numpy.log(y) * .5) # just a random combination of numpy functions I used to test the compilation
cc.compile()
The actual function I am trying to compile, is the following:
# code_generation3.py
import numpy
from numba.pycc import CC
"""
N = 94
input for x_dot_all could look like:
x_ = numpy.ones(N * 5)
x[4::5] = 5e13
t_ := some float from a numpy linspace. it is passed by odeint.
r_ = numpy.random.random(N * 4)
d_ = numpy.random.random(N * 4) * .8
In practice the size of x_ is 470 and of r_ and d_ is 376.
"""
cc = CC('x_temp_dot1')
cc.verbose = True
@cc.export('x_temp_dot1', 'f8[:](f8[:], f8, f8[:], f8[:], f8[:])')
def x_dot_all(x_,t_,r_,d_, h):
"""
rhs of the lotka volterra equation for all "patients"
:param x: initial conditions, always in groupings of 5: the first 4 is the bacteria count, the 5th entry is the carrying capacity
:param t: placeholder required by odeint
:param r: growth rates of the types of bacteria
:param d: death rates of the types of bacteria
returns the right hand side of the competitive lotka-volterra equation with finite and shared carrying capacity in the same ordering as the initial conditions
"""
def x_dot(x, t, r, d, j):
"""
rhs of the differential equation describing the intrahost evolution of the bacteria
:param x: initial conditions i.e. bacteria sizes and environmental carrying capacity
:param t: placeholder required by odeint
:param r: growth rates of the types of bacteria
:param d: death rates of the bacteria
:param j: placeholder for the return value
returns the right hand side of the competitive lotka-volterra equation with finite and shared carrying capacity
"""
j[:-1] = x[:-1] * r * (1 - numpy.sum(x[:-1]) / x[-1]) - d * x[:-1]
j[-1] = -numpy.sum(x[:-1])
return j
N = r_.shape[0]
j = numpy.zeros(5)
g = [x_dot(x_[5 * i : 5 * (i+1)], t_, r_[4 * i : 4* (i+1)], d_[4 * i: 4 * (i+1)], j) for i in numpy.arange(int(N / 4) )]
for index, value in enumerate(g):
h[5 * index : 5 * (index + 1)] = value
return h
cc.compile()
Here I get the following error message:
[xxxxxx@xxxxxx ~]$ python code_generation3.py
cc1plus: warning: command line option ‘-Wstrict-prototypes’ is valid for C/ObjC but not for C++ [enabled by default]
generating LLVM code for 'x_temp_dot1' into /tmp/pycc-build-x_temp_dot1-wyamkfsy/x_temp_dot1.cpython-36m-x86_64-linux-gnu.o
python: /root/miniconda3/conda-bld/llvmdev_1531160641630/work/include/llvm/IR/GlobalValue.h:233: void llvm::GlobalValue::setVisibility(llvm::GlobalValue::VisibilityTypes): Assertion `(!hasLocalLinkage() || V == DefaultVisibility) && "local linkage requires default visibility"' failed.
Aborted
I am wondering what I did wrong?
Both functions work with a @jit(nopython = True)
decorator.
To my shame, I also tried hard coding the list comprehension (to try and avoid any for loops and further function calls) but this had the same problem.
I know that the way I am handling/creating the return values h
and j
, respectivly, is neither efficient nor elegant, but I was having trouble getting the return value in the correct shape for odeint
, because numba does not handle numpy.reshape well.
I searched the numba documentation for help, but this did not help me understand my problem. I have searched for the error message, but only found this link, which might be similiar. However downgrading numba to 0.38.0 did not work for me.
Thank you all!
I guess it would work if you compile x_dot
first and x_dot_all
afterwards.
Anyway I would recommend to combine this two functions.
Loops in general are not a problem within Numba
, but list comprehensions certainly are. Also try to avoid an extreme amount of small loops. (Vectorized commands eg. numpy.sum(x[:-1])
are all seperate loops). Sometimes Numba is able to combine this loops to get efficient code, but not everytime.
Example
# code_generation3.py
import numpy
import numba as nb
from numba.pycc import CC
cc = CC('x_dot_all')
cc.verbose = True
@cc.export('x_dot_all_mod', 'f8[:](f8[:], f8, f8[:], f8[:], f8[:])')
def x_dot_all(x_,t_,r_,d_, h):
N = r_.shape[0]
for i in range(int(N / 4)):
sum_x=x_[5*i+0]+x_[5*i+1]+x_[5*i+2]+x_[5*i+3]
TMP=1.-(sum_x)/x_[5*i+4]
h[i*5+0]=x_[i*5+0]*r_[4*i+0]*TMP-d_[4*i+0]*x_[i*5+0]
h[i*5+1]=x_[i*5+1]*r_[4*i+1]*TMP-d_[4*i+1]*x_[i*5+1]
h[i*5+2]=x_[i*5+2]*r_[4*i+2]*TMP-d_[4*i+2]*x_[i*5+2]
h[i*5+3]=x_[i*5+3]*r_[4*i+3]*TMP-d_[4*i+3]*x_[i*5+3]
h[i*5+4]=-sum_x
return h
if __name__ == "__main__":
cc.compile()
Performance
N=94
x_ = np.ones(N * 5)
x_[4::5] = 5e13
t_ = 15
r_ = np.random.random(N * 4)
d_ = np.random.random(N * 4) * .8
h = np.zeros(N * 5)
#your version: 38 µs
#new version: 1.8µs