Bernoulli numbers (Python)

From LiteratePrograms
Jump to: navigation, search
Other implementations: Erlang | Python

The Bernoulli numbers Bm are a sequence of rational numbers defined by the identity

\sum_{j=0}^m {{m+1}\choose{j}} B_j = 0

with the initial condition B0 = 1. Here, {{n}\choose{k}} denotes a binomial coefficient. The sequence, which continues -1/2, 1/6, 0, -1/30, 0, 1/42, ..., obeys no simple pattern, and computing Bm for m as small as a few hundred is a relatively time-consuming task.

The following programs for calculating Bernoulli numbers will assume the presence of a rational type mpq (with support for basic arithmetic) and a function bincoef for calculating binomial coefficients, neither of which is available in Python's standard library. The GMPY library provides these, however.

We will have to create a trivial wrapper around GMPY's bincoef since for some reason it does not handle the case when k < 0, which will be of use later on.

from gmpy import mpq, bincoef as _bincoef

def bincoef(n, k):
    if k < 0:
        return 0
        return _bincoef(n, k)


[edit] Basic implementation

The m:th Bernoulli number can be obtained by extracting the last term from the sum we used to define the sequence, but this requires that B1 ... Bm-1 are already known. This is a perfect opportunity to employ recursion: bernoulli(m) will call bernoulli(0) ... bernoulli(m-1), which in turn will call bernoulli(0) ... bernoulli(m-2) and so on, until all fall back on bernoulli(0). The number of computations needed will grow exponentially with m, so a practical implementation must use memoization. We will create a decorator for this purpose:

class memo:
    def __init__(self, f):
        self.func = f
        self.cache = {}
        self.highest = 0
    def __call__(self, m):
        if m < self.highest:
            return self.cache[m]
            x = self.func(m)
            self.cache[m] = x
            self.highest = m
            return x

Now the main function. As a final optimization, we will exploit the fact that Bm = 0 if m > 1 is odd.


def bernoulli(m):
    if m == 0: return 1
    if m == 1: return -mpq(1,2)
    if m % 2: return 0
    s = 0
    for j in range(0, m):
        s += bincoef(m+1, j) * bernoulli(j)
    b = -mpq(s)/bincoef(m+1, m)
    return b

The function can be used as follows:

>>> [bernoulli(n) for n in range(10)]
[1, mpq(-1,2), mpq(1,6), 0, mpq(-1,30), 0, mpq(1,42), 0, mpq(-1,30), 0]

[edit] A faster implementation

The computation can be performed more efficiently according to the following identities due to Srinivasa Ramanujan:

m\equiv 0\,\bmod\,6\qquad {{m+3}\choose{m}}B_m={{m+3}\over3}-A(m, m/6)
m\equiv 2\,\bmod\,6\qquad {{m+3}\choose{m}}B_m={{m+3}\over3}-A(m, (m-2)/6)
m\equiv 4\,\bmod\, 6\qquad{{m+3}\choose{m}}B_m=-{{m+3}\over6}-A(m, (m-4)/6)


A(m, M) = \sum_{j=1}^M {m+3\choose{m-6j}}B_{m-6j}.

So instead of summing over m Bernoulli numbers and binomial coefficients, a sum over m/6 terms suffices.

We implement A:

def A(m, M):
    s = 0
    for j in range(1, M+1):
        s += bincoef(m+3, m-6*j) * bernoulli(m-6*j)
    return s

We may now implement the three different cases modulo 6 as three separately memoized functions. (Separating them is necessary since the memo decorator assumes that if bernoulli(m) has been computed, then so have all of bernoulli(0) ... bernoulli(m-1). But here we want to rely only on m that are congruent modulo 6.)

<<modular cases>>=
def b0mod6(m):
    return (mpq(m+3, 3) - A(m, m//6)) / bincoef(m+3, m)

def b2mod6(m):
    return (mpq(m+3, 3) - A(m, (m-2)//6)) / bincoef(m+3, m)

def b4mod6(m):
    return (-mpq(m+3, 6) - A(m, (m-4)//6)) / bincoef(m+3, m)

Wrapping things up:

<<main faster>>=
def bernoulli(m):
    assert m >= 0
    if m == 0: return 1
    if m == 1: return -mpq(1,2)
    if m % 6 == 0: return b0mod6(m)
    if m % 6 == 2: return b2mod6(m)
    if m % 6 == 4: return b4mod6(m)
    return 0
modular cases
main faster

[edit] A further enhancement

There is some inefficiency in the implementation of A, namely that it calculates a new binomial coefficient "from scratch" for each term of the sum. For large inputs, it is faster to compute each binomial coefficient from the previous one.

<<A faster>>=
def product(low, high):
    p = 1
    for k in range(low, high+1):
        p *= k
    return p

def A(m, mtop):
    s = 0
    a = bincoef(m+3, m-6)
    for j in range(1, mtop+1):
        s += a*bernoulli(m-6*j)
        a *= product(m-6 - 6*j + 1, m-6*j)
        a //= product(6*j+4, 6*j+9)
    return s
A faster
modular cases
main faster

[edit] Testing

A good way to check that all implementations are correct is to test them against each other. We will also time the computation of B1000 with each method.

from bernoulli_simple import bernoulli as bernoulli1
from bernoulli_faster import bernoulli as bernoulli2
from bernoulli_faster_yet import bernoulli as bernoulli3
from time import clock

for i in range(50):
    assert bernoulli1(i) == bernoulli2(i) == bernoulli3(i)

a = clock()
temp = bernoulli1(1000)
print "simple:", clock() - a, "seconds"

a = clock()
temp = bernoulli2(1000)
print "faster:", clock() - a, "seconds"

a = clock()
temp = bernoulli3(1000)
print "faster yet:", clock() - a, "seconds"

The output will probably resemble the following:

simple: 15.7621247952 seconds
faster: 1.00002039365 seconds
faster yet: 0.568878726207 seconds

The difference will become even more significant with larger input. In fact, the third version was used to compute the 10,000 first Bernoulli numbers in less than an hour on a 1400 MHz Athlon (version two needed four hours). By contrast, Mathematica took 1.5 hours to perform the same calculation and the specialized program calcbn needed 2.5 hours. However, it should be noted that the program implemented here is not well suited for calculating individual extremely large Bernoulli numbers, say, B100,000.

Download code