Package: numbalsoda
numbalsodanumbalsodais a python wrapper to the LSODA method inODEPACK, which is for solving ordinary differential equation initial value problems. LSODA was originally written in Fortran.numbalsodais a wrapper to a C++ re-write of the original code:https://github.com/dilawar/libsodanumbalsodaalso wraps thedop853explicit Runge-Kutta method from this repository:https://github.com/jacobwilliams/dop853This package is very similar toscipy.integrate.solve_ivp(see here), when you setmethod = 'LSODA'ormethod = DOP853. But,scipy.integrate.solve_ivpinvokes the python interpreter every time step which can be slow. Also,scipy.integrate.solve_ivpcan not be used within numba jit-compiled python functions. In contrast,numbalsodanever invokes the python interpreter during integration and can be used within a numba compiled function which makesnumbalsodaa lot faster than scipy for most problems, and achieves similar performance to Julia's DifferentialEquations.jl in some cases (seebenchmarkfolder).InstallationConda:conda install -c conda-forge numbalsodaPip:python -m pip install numbalsodaBasic usagefromnumbalsodaimportlsoda_sig,lsoda,dop853fromnumbaimportnjit,cfuncimportnumpyasnp@cfunc(lsoda_sig)defrhs(t,u,du,p):du[0]=u[0]-u[0]*u[1]du[1]=u[0]*u[1]-u[1]*p[0]funcptr=rhs.address# address to ODE functionu0=np.array([5.,0.8])# Initial conditionsdata=np.array([1.0])# data you want to pass to rhs (data == p in the rhs).t_eval=np.linspace(0.0,50.0,1000)# times to evaluate solution# integrate with lsoda methodusol,success=lsoda(funcptr,u0,t_eval,data=data)# integrate with dop853 methodusol1,success1=dop853(funcptr,u0,t_eval,data=data)# usol = solution# success = True/FalseThe variablesu,duandpin therhsfunction are pointers to an array of floats. Therefore, operations likenp.sum(u)orlen(u)will not work. However, you can use the functionnb.carray()to make a numpy array out of the pointers. For example:importnumbaasnb@cfunc(lsoda_sig)defrhs(t,u,du,p):u_=nb.carray(u,(2,))p_=nb.carray(p,(1,))# ... rest of rhs goes here using u_ and p_Above,u_andp_are numpy arrays build out ofuandp, and so functions likenp.sum(u_)will work.Also, notelsodacan be called within a jit-compiled numba function (see below). This makes it much faster than scipy if a program involves many integrations in a row.@njitdeftest():usol,success=lsoda(funcptr,u0,t_eval,data=data)returnusolusol=test()# this works!@njitdeftest_sp():sol=solve_ivp(f_scipy,t_span,u0,t_eval=t_eval,method='LSODA')returnsolsol=test_sp()# this does not work :(Passing data to the right-hand-side functionIn the examples shown above, we passed a an single array of floats to the right-hand-side function:# ...data=np.array([1.0])usol,success=lsoda(funcptr,u0,t_eval,data=data)However, sometimes you might want to pass more data types than just floats. For example, you might want to pass several integers, an array of floats, and an array of integers. One way to achieve this is with generating thecfuncusing a function like this:defmake_lsoda_func(param1,param2,param3):@cfunc(lsoda_sig)defrhs(t,x,du,p):# Here param1, param2, and param3# can be accessed.du[0]=param1*t# etc...returnrhsrhs=make_lsoda_func(10.0,5,10000)funcptr=rhs.address# etc...The only drawback of this approach is if you want to do many successive integrations where the parameters change because it would required re-compiling thecfuncbetween each integration. This could be slow.But! It is possible to pass arbitrary parameters without re-compiling thecfunc, but it is a little tricky. The notebookpassing_data_to_rhs_function.ipynbgives an example that explains how.