4

I'm integrating a system of stiff ODE's using SciPy's integrate.odeint function. As the integration is non-trivial and time consuming I'm also using the corresponding jacobian. By rearranging the equations I can define the jacobian to be a banded matrix. Following the API documentation I would like to define the shape with the mu and ml params. Unfortunately, the documentation is a bit ambiguous so that I was not able to figure out how to implement my jacobian function.

In order to verify how odeint has to be called, I've been using the following (somewhat silly) code:

from scipy.integrate import odeint
lmax = 5

def f1(y, t):
    ydot = np.zeros(lmax)
    for i in range(lmax):
        ydot[i] = y[i]**2-y[i]**3
    return ydot

def fulljac(y, t,):
    J = np.zeros((lmax, lmax))
    J[0,0]=-3*y[0]**2 + 2*y[0]
    J[1,1]=-3*y[1]**2 + 2*y[1]
    J[2,2]=-3*y[2]**2 + 2*y[2]
    J[3,3]=-3*y[3]**2 + 2*y[3]
    J[4,4]=-3*y[4]**2 + 2*y[4]
    return J

## initial conditions and output times
delta = 0.0001;
yini  = np.array([delta]*lmax)
times = np.linspace(0, 2/delta, 100)

y, infodict = odeint(f1, yini, times, Dfun=fulljac, full_output=1)
print("f1: nst: {0}, nfe: {1}, nje: {2}".format(infodict["nst"][-1],
                                                infodict["nfe"][-1],
                                                infodict["nje"][-1]))

Using the full NxN jacobian matrix the integration is successfull. Using only the diagonal and mu=0 and ml=0 the integration succeeds as well.

To test the banded matrix use case I'm creating an artificial 3xN banded jacobian using mu=1 and ml=1, where all the derivatives off the diagonal are zero. This causes a weird behavior of the solver (similar to what I see in my original problem where the off-diagonals are non zero).

def bandjac(y, t):
    J = np.zeros((lmax, 3))
    J[0,1]=-3*y[0]**2 + 2*y[0]
    J[1,1]=-3*y[1]**2 + 2*y[1]
    J[2,1]=-3*y[2]**2 + 2*y[2]
    J[3,1]=-3*y[3]**2 + 2*y[3]
    J[4,1]=-3*y[4]**2 + 2*y[4]
    return J

y, infodict = odeint(f1, yini, times, Dfun=bandjac, full_output=1, mu=1, ml=1)
print("f1: nst: {0}, nfe: {1}, nje: {2}".format(infodict["nst"][-1],
                                                infodict["nfe"][-1],
                                                infodict["nje"][-1]))

What is the proper way to use the banded jacobian option with SciPy's integrate.odeint?

πάντα ῥεῖ
  • 1
  • 13
  • 116
  • 190
jakeret
  • 101
  • 7
  • Which version of scipy are you using? There was a bug in how `odeint` handled banded Jacobians that was fixed in scipy 0.14.0 (https://github.com/scipy/scipy/pull/3145). – Warren Weckesser Sep 22 '14 at 17:56
  • Thank you for pointing me to this. I was using SciPy 0.13.x. – jakeret Sep 23 '14 at 15:58

2 Answers2

2

For the sake of completeness I’m replying to my own question.

As pointed out by Warren Weckesser there is a bug in Scipy <0.14.0 on how odeint handles banded jacobians.

The current documentation of odeint states:

Dfun should return a matrix whose rows contain the non-zero bands (starting with the lowest diagonal).

Which I believe is incorrect. Instead it should start with the highest diagonal.

The following code snippet shows how Dfun should return the jacobian (derived from the test_integrate.py unit test):

def func(y, t, c):
    return c.dot(y)

def jac(y, t, c):
    return c

def bjac_rows(y, t, c):
    return np.array([[   0,    75,       1,  0.2], # mu - upper 
                     [ -50,  -0.1,  -1e-04,  -40], # diagonal
                     [ 0.2,   0.1,    -0.2,   0]]) # lower - ml

c = array([[-50,    75,     0,   0],
            [0.2, -0.1,     1,   0],
            [0,    0.1, -1e-4,   0.2],
            [0,      0,   -0.2, -40]])

y0 = arange(4)

t = np.linspace(0, 50, 6)

# using the full jacobian
sol0, info0 = odeint(func, y0, t, args=(c,), full_output=True,Dfun=jac)
print("f1: nst: {0}, nfe: {1}, nje: {2}".format(info0["nst"][-1],
                                                info0["nfe"][-1],
                                                info0["nje"][-1]))

# using the row based banded jacobian
sol2, info2 = odeint(func, y0, t, args=(c,), full_output=True,Dfun=bjac_rows, ml=1, mu=1)
print("f1: nst: {0}, nfe: {1}, nje: {2}".format(info2["nst"][-1],
                                                info2["nfe"][-1],
                                                info2["nje"][-1]))

Note: the transposed banded matrix does not seem to work with col_deriv=True

jakeret
  • 101
  • 7
  • Don't get too excited about upgrading to 0.14.0. While experimenting with your code, I discovered that there is still a bug in the handling of a banded Jacobian by `odeint`. It is probably the cause of the problem you see when using `col_deriv=True`. In general, the bug could potentially cause a problem when `col_deriv=False`, but in your example, it worked OK. – Warren Weckesser Sep 23 '14 at 17:43
  • @WarrenWeckesser: My initial question was on how to properly implement the banded jacobian as the online documentation isn’t clear about this, hence my reply. Regardless of that, I also think that there is a bug in SciPy with `col_deriv=True`. – jakeret Sep 24 '14 at 07:27
  • transposed banded matrix seems to work for me now in scipy 0.18.0... and it's faster – Stanley Bak Sep 30 '16 at 20:31
0

I made a function to convert a jacobian matrix to banded form as expected by odeint, as well as the mu and ml parameters. The input is expected to be an np.array. For best performance, you probably want to turn the result matrix into an np.array, and then have the jacobian function return its transpose, and use col_der=True. I think BrainGrylls is correct that you should start with the highest diagonal first, contrary to the current API documentation.

def make_banded_jacobian(matrix):
    '''returns a banded jacobian list (in odeint's format), along with mu and ml parameters'''

    # first find the values of mu and ml
    dims = matrix.shape[0]
    assert dims == matrix.shape[1]
    mu = 0
    ml = 0

    for row in xrange(dims):
        for col in xrange(dims):
            if matrix[row][col] != 0:
                if col > row:
                    dif = col - row
                    mu = max(mu, dif)
                else:
                    dif = row - col
                    ml = max(ml, dif)

    banded = []

    for yoffset in xrange(-mu, ml+1):
        row = []

        for diag in xrange(dims):
            x_index = diag
            y_index = diag + yoffset

            if y_index < 0 or y_index >= dims:
                row.append(0.0)
            else:
                row.append(matrix[y_index][x_index])

        banded.append(row)

    return (banded, mu, ml)
Stanley Bak
  • 543
  • 3
  • 16