Integrands in C or Fortran

Older implementations of the vegas algorithm have been used extensively in C and Fortran codes. The Python implementation described here uses a more powerful algorithm. It is relatively straightforward to combine this version with integrands coded in C or Fortran. Such integrands are usually substantially faster than integrands coded directly in Python; they are similar in speed to optimized Cython code. There are many ways to access C and Fortran integrands from Python. Here we review a few of the options.

cffi for C

The simplest way to access an integrand coded in C is to use the cffi module in Python. To illustrate, consider the following integrand, written in C and stored in file cfcn.c:

// file cfcn.c
#include <math.h>

double fcn(double x[], int dim)
{
      int i;
      double xsq = 0.0;
      for(i=0; i<dim; i++)
            xsq += x[i] * x[i] ;
      return exp(-100. * sqrt(xsq)) * pow(100.,dim);
}

Running the following Python code creates (in file cfcn_cffi.c) and compiles code for a Python module called cfcn_cffi that provides access to fcn(x,dim):

# file cfcn_cffi-builder.py
from cffi import FFI
ffibuilder = FFI()

# specify functions, etc to be made available to Python
ffibuilder.cdef('double fcn(double x[], int dim);')

# specify code needed to build the Python module
ffibuilder.set_source(
    module_name='cfcn_cffi',
    source='double fcn(double x[], int dim);',
    sources=['cfcn.c'],     # other sources -- file containing fcn(x, dim)
    libraries=['m'],        # may need to specify the math library (-lm)
    )

if __name__ == '__main__':
    # create C code for module and compile it
    ffibuilder.compile(verbose=True)

We integrate the function in this module by wrapping it in a Python function (here f(x)), which is integrated using vegas:

import vegas

from cfcn_cffi import ffi, lib

def f(x):
    _x = ffi.cast('double*', x.ctypes.data) # pointer to x's data
    return lib.fcn(_x, 4)

def main():
    integ = vegas.Integrator(4 * [[0., 1.]])
    print(integ(f, neval=1e6, nitn=10).summary())
    print(integ(f, neval=1e6, nitn=10).summary())

if __name__ == '__main__':
    main()

The output shows 10 iterations that are used to adapt vegas to the integrand, and then an additional 10 iterations to generate the final result:

itn   integral        wgt average     chi2/dof        Q
-------------------------------------------------------
  1   4.1(2.1)        4.1(2.1)            0.00     1.00
  2   7.403(42)       7.401(42)           2.52     0.11
  3   7.366(27)       7.376(23)           1.51     0.22
  4   7.4041(73)      7.4014(70)          1.48     0.22
  5   7.4046(36)      7.4039(32)          1.15     0.33
  6   7.4003(23)      7.4015(19)          1.09     0.37
  7   7.4036(20)      7.4025(14)          1.01     0.42
  8   7.4017(16)      7.4022(10)          0.89     0.52
  9   7.4010(14)      7.40174(83)         0.84     0.57
 10   7.4017(13)      7.40174(70)         0.74     0.67

itn   integral        wgt average     chi2/dof        Q
-------------------------------------------------------
  1   7.4016(12)      7.4016(12)          0.00     1.00
  2   7.4030(11)      7.40239(81)         0.79     0.37
  3   7.4020(10)      7.40224(63)         0.44     0.64
  4   7.40249(92)     7.40232(52)         0.31     0.82
  5   7.40258(86)     7.40239(44)         0.25     0.91
  6   7.40093(81)     7.40205(39)         0.70     0.62
  7   7.40228(76)     7.40210(35)         0.60     0.73
  8   7.40276(72)     7.40222(31)         0.61     0.75
  9   7.40181(71)     7.40216(29)         0.57     0.80
 10   7.40178(70)     7.40210(27)         0.53     0.85

The final estimate for the integral is 7.40210(27) (about 10,000 times more accurate than the first iteration).

This code can be made substantially faster by converting the integrand into a batch integrand. We do this by adding a batch version of the integrand function to the cfcn_cffi module:

# file cfcn_cffi-builder.py
from cffi import FFI
ffibuilder = FFI()

# specify functions, etc made available to Python
ffibuilder.cdef("""
    void batch_fcn(double ans[], double x[], int n, int dim);
    """)

# specify code needed to build the module
ffibuilder.set_source(
    module_name='cfcn_cffi',
    source="""
    // code for module
    double fcn(double x[], int dim);

    void batch_fcn(double ans[], double x[], int n, int dim)
    {
        int i;
        for(i=0; i<n; i++)
            ans[i] = fcn(&x[i * dim], dim);
    }
    """,
    sources=['cfcn.c'],     # other sources -- file containing fcn(x, dim)
    libraries=['m'],        # may need to specify the math library (-lm)
    )

if __name__ == '__main__':
    # create C code for module and compile it
    ffibuilder.compile(verbose=True)

The resulting module is then used by the following integration code:

import vegas
import numpy as np

from cfcn_cffi import ffi, lib

@vegas.batchintegrand
def batch_f(x):
    n, dim = x.shape
    ans = np.empty(n, float)
    _x = ffi.cast('double*', x.ctypes.data)
    _ans = ffi.cast('double*', ans.ctypes.data)
    lib.batch_fcn(_ans, _x, n, dim)
    return ans

def main():
    integ = vegas.Integrator(4 * [[0., 1.]])
    print(integ(batch_f, neval=1e6, nitn=10).summary())
    print(integ(batch_f, neval=1e6, nitn=10).summary())

if __name__ == '__main__':
    main()

Running this code gives identical results to those above, but in about 1/10 the time.

Obviously cffi can also be used to access C++ functions by creating C wrappers for those functions. Another option for accessing C code is the ctypes module, but it is more complicated to use and typically gives slower code.

cffi for Fortran

Module cffi can be used for Fortran integrands by calling the Fortran function from C code. Consider a Fortran implementation of the integrand discussed above, stored in file ffcn.f:

c file ffcn.f
c
      function fcn(x, dim)
      integer i, dim
      real*8 x(dim), xsq, fcn
      xsq = 0.0
      do i=1,dim
        xsq = xsq + x(i) ** 2
      end do
      fcn = exp(-100. * sqrt(xsq)) * 100. ** dim
      return
      end

This file is compiled into ffcn.o using something like

gfortran -c ffcn.c -o ffcn.o

The cffi build script (batch version) from the previous section can be used here with only three modifications: 1) the Fortran function must be compiled separately and so is included in a list of object files (extra_objects); 2) the name of the Fortran function may have an extra underscore (or other modification, depending on the compilers used; on UNIX systems nm ffcn.o lists the names in the file); and 3) the Fortran function’s arguments are passed by address and so are pointers in C. The modified script is:

# file ffcn_cffi-builder.py
from cffi import FFI
ffibuilder = FFI()

# specify functions, etc needed by Python
ffibuilder.cdef("""
    void batch_fcn(double ans[], double x[], int n, int dim);
    """)

# specify code needed to build the module
ffibuilder.set_source(
    module_name='ffcn_cffi',
    source="""
    // code for module
    double fcn_(double* x, int* dim);   // Fortran function in ffcn.o

    void batch_fcn(double ans[], double x[], int n, int dim)
    {
        int i;
        for(i=0; i<n; i++)
            ans[i] = fcn_(&x[i * dim], &dim);
    }
    """,
    extra_objects=['ffcn.o'],   # compiled Fortran
    libraries=['m'],            # may need to specify the math library (-lm)
    )

if __name__ == "__main__":
    # create C code for module and compile it
    ffibuilder.compile(verbose=True)

The new Python module is used exactly the same way as for C code, with module ffcn_cffi replacing module cfcn_cffi (see the previous section):

import vegas
import numpy as np

from ffcn_cffi import ffi, lib

@vegas.batchintegrand
def batch_f(x):
    n, dim = x.shape
    ans = np.empty(n, float)
    _x = ffi.cast('double*', x.ctypes.data)
    _ans = ffi.cast('double*', ans.ctypes.data)
    lib.batch_fcn(_ans, _x, n, dim)
    return ans

def main():
    integ = vegas.Integrator(4 * [[0., 1.]])
    print(integ(batch_f, neval=1e6, nitn=10).summary())
    print(integ(batch_f, neval=1e6, nitn=10).summary())

if __name__ == '__main__':
    import numpy as np
    np.random.seed(12)

This code runs about as fast as the corresponding C code in the previous section.

Cython for C (or C++ or Fortran)

A more flexible interface to a C integrand can be created using Cython. To increase efficiency, we use Cython code in file cfcn.pyx to convert the original function (in cfcn.c) into a batch integral (again):

# file cfcn.pyx
import numpy as np
import vegas

cdef extern double fcn (double[] x, int n)

@vegas.batchintegrand
def batch_f(double[:, ::1] x):
    cdef double[:] ans
    cdef int i, dim=x.shape[1]
    ans = np.empty(x.shape[0], type(x[0,0]))
    for i in range(x.shape[0]):
        ans[i] = fcn(&x[i, 0], dim)
    return ans

We also have to tell Cython how to construct the cfcn Python module since that module needs to include compiled code from cfcn.c. This is done with a .pyxbld file:

# file cfcn.pyxbld
import numpy as np

def make_ext(modname, pyxfilename):
    from distutils.extension import Extension
    return Extension(name = modname,
                     sources=[pyxfilename, 'cfcn.c'],
                     libraries=[],
                     include_dirs=[np.get_include()],
                     )

def make_setup_args():
    return dict()

Finally the integral is evaluated using the Python code

import vegas

# compile cfcn, if needed, at import
import pyximport
pyximport.install(inplace=True)

import cfcn

def main():
    integ = vegas.Integrator(4 *[[0,1]])
    print(integ(cfcn.batch_f, neval=1e4, nitn=10).summary())
    print(integ(cfcn.batch_f, neval=1e4, nitn=10).summary())

if __name__ == '__main__':
    main()

where, again, pyximport guarantees that the cfcn module is compiled the first time the code is run.

This implementation is as fast as the other batch implementations, above. Cython also works with C++. It can also work with Fortran code, analogously to cffi.

f2py for Fortran

The f2py package, which is distributed with numpy, makes it relatively easy to compile Fortran code directly into Python modules. Using the same Fortran code as above (in ffcn.f), the code is compiled into a Python module using

f2py -m ffcn -c ffcn.f

and the resulting module provides access to the integrand from Python:

import vegas
import ffcn

def main():
    integ = vegas.Integrator(4 *[[0,1]])
    print(integ(ffcn.fcn, neval=1e4, nitn=10).summary())
    print(integ(ffcn.fcn, neval=1e4, nitn=10).summary())

if __name__ == '__main__':
    main()

Again you can make the code somewhat faster by converting the integrand into a batch integrand inside the Fortran module. Adding the following function to the end of file ffcn.f above :

c part 2 of file ffcn.f --- batch form of integrand

      subroutine batch_fcn(ans, x, dim, nbatch)
      integer dim, nbatch, i, j
      real*8 x(nbatch, dim), xi(dim), ans(nbatch), fcn
cf2py intent(out) ans
      do i=1,nbatch
            do j=1,dim
                  xi(j) = x(i, j)
            end do
            ans(i) = fcn(xi, dim)
      end do
      end

results in a second Python function ffcn.batch_fcn(x) that takes the integration points x[i,d] as input and returns an array of integrand values ans[i]. (The second Fortran comment tells f2py that array ans should be returned by the correponding Python function; f2py also has the function automatically deduce dim and nbatch from the shape of x.) The correponding Python script for doing the integral is then:

import vegas
import ffcn_f2py
import numpy as np

def main():
    integ = vegas.Integrator(4 *[[0,1]])
    batch_fcn = vegas.batchintegrand(ffcn_f2py.batch_fcn)
    print(integ(batch_fcn, neval=1e6, nitn=10).summary())
    print(integ(batch_fcn, neval=1e6, nitn=10).summary())

if __name__ == '__main__':
    main()

This runs roughly twice as fast as the original, and about the same speed as the batch versions of the C code, above.