FEniCS solver coupled with ODE solver in C++
In this final example we will consider a solver for a reaction-diffusion
equation described by a parabolic PDE coupled to a set of ODEs. The
equation can be written as
\[\begin{split}u_t &= \Delta u + f(u, s),
& \forall x \in\Omega,\ t>0, \\
s_t &= g(u, s),
& \forall x \in \Omega,\ t>0.\end{split}\]
Here, \(u\) is a scalar function, subscript \(t\) means differentiation with
respect to time, \(s\) is a scalar field (governed pointwise by an ODE),
and \(\Delta u\) is a Laplace term.
The problem is usually solved using a first order operator splitting scheme,
where we at a time level first solve the PDE with \(f\) evaluated at
the previous time level, and thereafter we update the ODEs using
the most recent value of \(u\). More precisely,
\[\begin{split}u^n &= u^{n-1} + \Delta t (\Delta u^n + f(u^{n-1}, s^{n-1})), \\
s^n &= s^{n-1} + \Delta t g(u^n, s^{n-1}) ,\end{split}\]
The superscript \(n\) denotes the time level, and \(\Delta t\) is the time step.
The solver for the parabolic problem is implemented in FEniCS, while
the ODE solver is implemented in a homemade C++ code. We will glue
these two different solvers together using Python. The C++ code consists
basically of the class ODEFieldSolver declared in a file
ODEFieldSolver.h:
class ODEFieldSolver {
int n; // no of points (or regions)
double* s; // discrete values of unknown s)
double* s_1; // s at the previous time level
double* u; // discrete values of external field u
double dt; // time step size
public:
ODEFieldSolver();
~ODEFieldSolver();
void redim(int n); // allocate data structures
int size(); // return the no of points/regions
virtual double g(double s, double u);
void set_dt(double dt);
void set_IC(int n, double* in_array);
void set_u (int n, double* in_array);
void set_IC(double const_value);
void set_u (double const_value);
void advance_one_timestep();
};
The set_IC functions set the initial condition of the ODE system, and
set_u provides the \(u\) field (the “environment”) to the ODE system.
Note that there are two versions of set_IC and set_u: one for
a constant value one for spatial variations. We refer to
the ODEFieldSolver.cpp
file for implementational details.
The mathematics behind the shown class is to have \(n\) regional or pointwise
values of \(u\) and \(s\), and then solve \(s_t = g(s,u)\) in each region
or at each point. In the present case we will solve for \(s\) at
the nodes in the finite element mesh used to compute \(u\).
The \(s\) and \(u\) functions are in the C++ code represented by
plain C arrays holding nodal values. The usage of the C++ code typically
goes like this:
ODEFieldSolver solver = ODEFieldSolver();
solver.redim(n); // allocate data
solver.set_dt(dt); // set time step
solver.set_IC(n, s0); // set initial conditions
t = 0
while (t <= T) {
solver.set_u(n, u); // give access to PDE solution
solver.advance_one_timestep();
// plot solver.s
}
A subclass must be written to specify the desired g function.
We need to wrap the C++ class in Python such that
the FEniCS Python solver can call the C++ code.
We would then need to transfer the computed
\(s\) back to Python. To this end, we add a member function get_s
to the class so that we can fill some array on the user’s side with
the most recently computed s values:
class ODEFieldSolver {
...
void get_s (int& n, double* out_array);
};
Wrapping with F2PY
The easiest way to interface Fortran, C, and C++ code is to use F2PY.
Although F2PY was made for interfacing Fortran and most of the documentation
is written with Fortran codes in mind, it is convenient to interface
C and C++ too. Or more precisely, F2PY can interface a pure C API, not
C++ classes. The idea is then to construct a set of C functions on top
of the C++ classes for accessing high-level operations using the those classes.
The example involving class ODEFieldSolver will illustrate the elements
in this technique.
C API to C++ code
The first step is to decide on the C API. The exposed functions in
Python must do essentially the same as the main program. A possible
set of functions is
- set_ic_and_dt(int n, double* s0, double dt) for initializing
the class object and setting the initial conditions
and the time step. Also a variant set_const_ic_and_dt for
constant initial condition s0 is handy.
- set_u(int n, double* u) for assigning the u function to the class.
- advance_one_timestep() for computing the solution at a time step.
- get_s(int n, double* s) for getting access to the computed array s
in the ODEFieldSolver class.
These functions must make use of a global variable holding a
ODEFieldSolver object and interact with this object as appropriate.
The complete code of the C API then becomes
#include "ODEFieldSolver.h"
ODEFieldSolver solver = ODEFieldSolver();
extern "C" {
void set_ic_and_dt(int n, double* s0, double dt)
{
solver.redim(n);
solver.set_dt(dt);
solver.set_IC(n, s0);
}
void set_const_ic_and_dt(int n, double s0, double dt)
{
solver.redim(n);
solver.set_dt(dt);
solver.set_const_IC(s0);
}
void set_u(int n, double* u)
{
solver.set_u(n, u);
}
void advance_one_timestep()
{
solver.advance_one_timestep();
}
void get_s(int n, double* s)
{
solver.get_s(n, s);
}
}
Writing corresponding Fortran signatures
The nice thing about F2PY is that it can automatically make a Python
interface to this C code, where NumPy arrays can be passed to the
functions taking plain C arrays as arguments. For this to work, F2PY
needs a specification of all the C functions in terms of Fortran 90
module syntax. However, F2PY can generate this module for us if we
specify the function signatures in plain Fortran 77. This is done as
follows:
subroutine set_ic_and_dt(n, s0, dt)
Cf2py intent(c) set_ic_and_dt
integer n
real*8 s0(0:n-1), dt
Cf2py intent(c) n, s0, dt
return
end
subroutine set_const_ic_and_dt(n, s0, dt)
Cf2py intent(c) set_const_ic_and_dt
integer n
real*8 s0, dt
Cf2py intent(c) n, s0, dt
return
end
subroutine set_u(n, u)
Cf2py intent(c) set_u
integer n
real*8 u(0:n-1)
Cf2py intent(c) n, u
return
end
subroutine advance_one_timestep()
Cf2py intent(c) advance_one_timestep
return
end
subroutine get_s(n, s)
Cf2py intent(c) get_s
integer n
Cf2py intent(c) n
real*8 s(0:n-1)
Cf2py intent(c, in, out) s
return
end
For each C function we
- write the corresponding Fortran subroutine or function header,
- insert an F2PY-specific comment (CF2PY) that tells that the
function is in C: intent(c),
- specify that all variables are in C: intent(c) (Fortran treats
all arguments as pointers, so the specification of C variables is
strictly needed only for non-pointers),
- specify if we want the Python interface to return one or more output
arguments.
Regarding the last point, we specify s in get_s as intent(c,in,out),
meaning that we in Python can call this function as s = get_s(s).
The s argument is needed for the function to avoid
reallocating the returned array every time the function is call. Instead
we reuse the storage provied in the s array.
If the Fortran 77 signatures are in a file signatures_capi2cpp.f we
can get F2PY to generate a Fortran 90 module in a file ODEFieldSolvercpp.pyf
by the command
Terminal> F2PY -m ODEFieldSolvercpp -h ODEFieldSolvercpp.pyf \
--overwrite-signature signatures_capi2cpp.f
The -m option specifies the name of the extension module that contains
the Python interfaces to the C API.
The module typically looks like
! -*- f90 -*-
! Note: the context of this file is case sensitive.
python module ODEFieldSolvercpp ! in
interface ! in :ODEFieldSolvercpp
subroutine set_ic_and_dt(n,s0,dt)
intent(c) set_ic_and_dt
integer, optional,intent(c),check(len(s0)>=n),depend(s0) :: n=len(s0)
real*8 dimension(n),intent(c) :: s0
real*8 intent(c) :: dt
end subroutine set_ic_and_dt
subroutine set_const_ic_and_dt(n,s0,dt)
intent(c) set_const_ic_and_dt
integer intent(c) :: n
real*8 intent(c) :: s0
real*8 intent(c) :: dt
end subroutine set_const_ic_and_dt
subroutine set_u(n,u)
intent(c) set_u
integer, optional,intent(c),check(len(u)>=n),depend(u) :: n=len(u)
real*8 dimension(n),intent(c) :: u
end subroutine set_u
subroutine advance_one_timestep ! in :ODEFieldSolvercpp:signatures_capi2cpp.f
intent(c) advance_one_timestep
end subroutine advance_one_timestep
subroutine get_s(n,s)
intent(c) get_s
integer, optional,intent(c),check(len(s)>=n),depend(s) :: n=len(s)
real*8 dimension(n),intent(c,in,out) :: s
end subroutine get_s
end interface
end python module ODEFieldSolvercpp
! This file was auto-generated with f2py (version:2).
! See http://cens.ioc.ee/projects/f2pye/
Those who are familiar with Fortran 90 modules can write such
code by hand instead of first writing Fortran 77 headers and letting
F2PY generate the module.
Building the extension module
With the aid of the Fortran 90 specification of the C functions, F2PY
can compile and link the extension module by a command like
Terminal> F2PY -c --fcompiler=gfortran -I.. --build-dir tmp1 \
-DF2PY_REPORT_ON_ARRAY_COPY=1 \
ODEFieldSolvercpp.pyf ../ODEFieldSolver.cpp capi2cpp.cpp
The -DF2PY_REPORT_ON_ARRAY_COPY=1 option is handy for letting F2PY
notify us if arrays are copied when transferred from Python to C,
as we want to avoid time-consuming copies. The C++ class is assumed
to be in the parent directory (note -I.. and the ../ prefix
in the filename). All the files generated
and built by F2PY will reside in the tmp1 directory for later
inspection if run into build problems.
The result of the above compile command is a C/C++ extension module
in the file ODEFieldSolvercpp.so. The module can be loaded
into Python and examined for content:
>>> import ODEFieldSolvercpp
>>> dir(ODEFieldSolvercpp)
['__doc__', '__file__', '__name__', '__package__', '__version__',
'advance_one_timestep', 'get_s', 'set_const_ic_and_dt',
'set_ic_and_dt', 'set_u']
>>> print ODEFieldSolvercpp.__doc__
This module 'ODEFieldSolvercpp' is auto-generated with F2PY (version:2).
Functions:
set_ic_and_dt(s0,dt,n=len(s0))
set_const_ic_and_dt(n,s0,dt)
set_u(u,n=len(u))
advance_one_timestep()
s = get_s(s,n=len(s))
A word of caution is required for newcomers to F2PY: it is extremely
important to study the doc strings of the various functions before
trying to call them from Python. The reason is that F2PY drops
unnecessary arguments, such as array lengths (since these are
contained in NumPy array objects), and all output arguments are
returned and removed from the subroutine’s argument list.
The function arguments and return values are therefore different
in Python and C!
For example, the set_ic_and_dt function only needs s0 transferred
from Python since n can be deduced from the F2PY-generated
interface. The signature of this function, as seen from Python,
is then
>>> print ODEFieldSolvercpp.set_ic_and_dt.__doc__
set_ic_and_dt - Function signature:
set_ic_and_dt(s0,dt,[n])
Required arguments:
s0 : input rank-1 array('d') with bounds (n)
dt : input float
Optional arguments:
n := len(s0) input int
Furthermore, the get_s function has specified its s
argument as input and output (intent(c,in,out)) and the
doc string shows the correct call syntax:
>>> print ODEFieldSolvercpp.get_s.__doc__
get_s - Function signature:
s = get_s(s,[n])
Required arguments:
s : input rank-1 array('d') with bounds (n)
Optional arguments:
n := len(s) input int
Return objects:
s : rank-1 array('d') with bounds (n)
Main program in Python
The Python code for calling the C++ functions in the ODEFieldSolvercpp
module can take the following form:
import ODEFieldSolvercpp as solver
import numpy
s0 = numpy.array([0, 1, 4], float) # ensure float elements!
u = numpy.array([0, 1, 1], float)
n = s0.size
s = numpy.zeros(n)
solver.set_ic_and_dt(s0, dt=0.1)
for n in range(1, 8):
solver.set_u(u)
solver.advance_one_timestep()
s = solver.get_s(s)
print n, s
A pure C version of the C++ class
It may be illustrative to also see a pure C code that implements
the same type of actions as the C++ class.
The class variables are here global variables in a library and
all the class functions are stand-alone C functions working with
these global variables. A bit more sophisticated implementation
would collect the global variables in a global struct instead, so
that the functions work with the struct.
The advantage of a pure C code is that F2PY can interface all parts
of this code directly without any need to make a C API to C++ code.
(Having said that, we should add that making a C API to C++ codes
is often a good exercise as it tends to emphasize faster computing with
arrays rather than with special (potentially small) C++ objects.
Python interfacing of C++ this way may lead to sound redesign of
the C++ code.)
The pure C implementation goes as follows:
#include <stdlib.h>
#include <stdio.h>
/* global variables */
double* s;
double* s_1;
double* u;
double dt;
int n;
void redim(int n_)
{
n = n_;
s = malloc(sizeof(double)*n);
s_1 = malloc(sizeof(double)*n);
u = malloc(sizeof(double)*n);
}
void deallocate()
{
free(s); free(s_1); free(u);
}
/* Note: do not mix upper and lower case letters as in set_IC_...
This leads to undefined symbols when f2py compiles the code.
*/
void set_ic_and_dt(int n_, double* s0, double dt_)
{
int i;
redim(n_);
dt = dt_;
for (i=0; i<n; i++) {
s_1[i] = s0[i];
}
}
void set_const_ic_and_dt(int n_, double s0, double dt_)
{
int i;
redim(n_);
dt = dt;
for (i=0; i<n; i++) {
s_1[i] = s0;
}
}
void set_u(int n_, double* u_)
{
int i;
for (i=0; i<n; i++) {
u[i] = u_[i];
}
}
double g(double s_, double u_) {
/* return s_*u_*(1 - s_); */
return s_;
}
void advance_one_timestep()
{
/* Use the Forward Euler time integration for simplicity */
int i;
for (i=0; i<n; i++) {
s[i] = s_1[i] + dt*g(s_1[i], u[i]);
/* For debugging: */
/* printf("i=%d, s_1=%g, dt=%g, g=%g, s=%g\n",
i, s_1[i], dt, g(s_1[i], u[i]), s[i]); */
}
/* Update for next time step */
for (i=0; i<n; i++) { s_1[i] = s[i]; }
}
void get_s(int n_, double* s_)
{
int i;
for (i=0; i<n; i++) {
s_[i] = s[i];
}
}
By writing the corresponding Fortran 77 signatures, F2PY can generate
a Fortran 90 module specification of the extension module, and this
code can be compiled as explained above.
We refer to the files in the f2py-c
directory for details.
Wrapping with SWIG
Next, we employ the tool SWIG to wrap the C++ class
directly and make it available as a Python class.
SWIG is also used in DOLFIN and Instant, as demonstrated
in the section How to interface a C++/DOLFIN code from Python.
Useful references on SWIG in a FEniCS context
are [Ref02] [Ref09] [Ref10] [Ref11].
To use SWIG, you must first write an interface file (ending in .i)
that tells SWIG about the parts of the C++ code you want to access
from Python. The next step is to run SWIG to generate (a lot of) wrapper
code in C. The final step is to compile the wrapper code and your C++ code
and link with required libraries.
A first attempt to write an interface for our ODEFieldSolver class
consists in listing just the class declaration:
%module ODEFieldSolver
%{
#include <arrayobject.h>
#include <sstream>
#include "ODEFieldSolver.h"
%}
%init %{
import_array();
%}
class ODEFieldSolver {
int n; // no of points (or regions)
double* s; // discrete values of unknown s)
double* s_1; // s at the previous time level
double* u; // discrete values of external field u
double dt; // time step size
public:
ODEFieldSolver();
~ODEFieldSolver();
void redim(int n); // allocate data structures
int size(); // return the no of points/regions
virtual double g(double s, double u);
void set_dt(double dt);
void set_IC(int n, double* in_array);
void set_u (int n, double* in_array);
void set_IC(double const_value);
void set_u (double const_value);
void advance_one_timestep();
void get_s (int& n, double* out_array);
};
All SWIG commands start with %. The %module command defines the
name of the module. Following this command comes a list of header files
needed by the module. The %init command includes code that should be
executed when the module is imported in Python. When using NumPy arrays
in C++ code we always need to call the import_array function to
initialize the NumPy package (removal of this statement will
result in a segmentation fault!). The rest of the code defines the
interface that should be wrapped, that is; the declaration of the
class ODEFieldSolver.
SWIG is meant to automate interfacing of C and C++ code, and there is
mainly only one thing that needs to be addressed manually: the
handling of pointers to arrays. Consider for instance the set_IC
function. Here, in_array is a pointer to the first element of a
double precision array of length n. However, the fact that
in_array is an array is not explicitly stated in C++, and therefore
SWIG simply by default handles the pointer as a plain pointer, and
this is not what we want. SWIG does, however, offer typemaps for
changing this default behavior. With typemaps we can specify that the
pointer in_array is a NumPy array object (PyObject) when it comes
from Python, and we can extract the underlying data pointer
(double*) and communicate it to C.
To enable NumPy arrays to be passed to the functions set_IC and
set_u we provide the following typemap.
%typemap(in) (int n, double* array){
if (!PyArray_Check($input)) {
PyErr_SetString(PyExc_TypeError, "Not a NumPy array");
return NULL; ;
}
PyArrayObject* pyarray;
pyarray = (PyArrayObject*)$input;
if (!(PyArray_TYPE(pyarray) == NPY_DOUBLE)) {
PyErr_SetString(PyExc_TypeError, "Not a NumPy array of doubles");
return NULL; ;
}
$1 = int(pyarray->dimensions[0]);
$2 = (double*)pyarray->data;
}
Typemap code often looks complicated, at least when viewed for the
first time. The logic is straightforward, though, once some basic
knowledge of the C API of Python and NumPy is acquired. The idea
with the typemap is to recognize a set of arguments in C/C++, here
n and in_array, and then execute some C/C++ code to transform
a Python object to the C/C++ arguments.
In the present example we want to map a NumPy array object
to an integer n (the array size) and a plain C array in_array
(the array data). All Python objects, when viewed in C, are of type
PyObject. We can think of PyObject as a superclass for all the different
object types in Python. The special NumPy array object type is
PyArrayObject.
SWIG has some special variables prefixed with $, which in the
present example are $input for the incoming NumPy array object, and
$1 and $2 for the outgoing C/C++ arguments n and in_array.
The first if statement checks that the incoming array is of right
type, and if not, a TypeError exception is raised. The return
NULL statement is essential for this exception to work. The next
step is to cast the PyObject pointer in $input to the correct
array object type, PyArrayObject, because we need this object to
call C functionality in the NumPy object to extract the data and the
array size. For safety reasons, we insert a test that the array data
are of type NPY_DOUBLE so that the array element types in Python and
C match. Then we come to the final and most essential point:
extracting data from the NumPy array object and storing them in n
($1) and in_array ($2):
$1 = int(pyarray->dimensions[0]);
$2 = (double*)pyarray->data;
Because we have overloaded the set_IC function, we also need to provide
SWIG with a typecheck to determine which of the C++ functions to use.
A suitable typecheck is:
%typecheck(SWIG_TYPECHECK_DOUBLE_ARRAY) (int n, double* array) {
$1 = PyArray_Check($input) ? 1 : 0;
}
The function
void get_s(int n, double *array);
should return NumPy arrays when called from Python as
That is, we would like to be able to call this function from Python
without providing and input array, and instead get an output
array. This means that an array must be created before being passed to
C++ and then returned to Python. To accomplish this we hide the
function by calling it _get_s. Then we extend the interface using
the %extend and %pythoncode directives with a Python function
get_s. The Python function get_s allocate an array before passing
it to the hidden _get_s function and thereafter it
returns the array. The code is
/* Wrap ODEFieldSolver::get_s in a Python function */
%rename (_get_s) ODEFieldSolver::get_s;
%extend ODEFieldSolver{
%pythoncode%{
def get_s(self):
import numpy as np
a = np.zeros(self.size())
self._get_s(a)
return a
%}
}
To summarize, the complete
SWIG interface file for wrapping the ODEFieldSolver class is
listed next.
%module ODEFieldSolver
%{
#include <arrayobject.h>
#include <sstream>
#include "ODEFieldSolver.h"
%}
%init %{
import_array();
%}
%typecheck(SWIG_TYPECHECK_DOUBLE_ARRAY) (int n, double* array) {
$1 = PyArray_Check($input) ? 1 : 0;
}
%typemap(in) (int n, double* array){
if (!PyArray_Check($input)) {
PyErr_SetString(PyExc_TypeError, "Not a NumPy array");
return NULL; ;
}
PyArrayObject* pyarray;
pyarray = (PyArrayObject*)$input;
if (!(PyArray_TYPE(pyarray) == NPY_DOUBLE)) {
PyErr_SetString(PyExc_TypeError, "Not a NumPy array of doubles");
return NULL; ;
}
$1 = int(pyarray->dimensions[0]);
$2 = (double*)pyarray->data;
}
/* Wrap ODEFieldSolver::get_s in a Python function */
%rename (_get_s) ODEFieldSolver::get_s;
%extend ODEFieldSolver{
%pythoncode%{
def get_s(self):
import numpy as np
a = np.zeros(self.size())
self._get_s(a)
return a
%}
}
%include std_string.i
%include "ODEFieldSolver.h"
To make SWIG generate the wrapper code, we run
swig -python -c++ -I. -I.. ODEFieldSolver.i
SWIG supports many languages and we therefore specify what languages
we need wrapper code for by the -python and -c++ flags. Further,
-I is used to specify where SWIG should look for interface files
(with extension .i). The C++ class files are located in the parent
directory. SWIG will from this command generate two files
ODEFieldSolver.py and ODEFieldSolver_wrap.cxx. The latter needs to
be compiled and linked with the ODEFieldSolver code to form a shared
library with name _ODEFieldSolver.so. The ODEFieldSolver.py file
is the module to use from Python and this is nothing but a Python
class wrapper to _ODEFieldSolver.so module. Building the shared
library is most conveniently done via a standard setup.py
script. The following setup.py file provides an
appropriate recipe for writing this kind of files:
import os, numpy
from distutils.core import setup, Extension
name = 'ODEFieldSolver'
swig_cmd ='swig -python -c++ -I.. -I. %s.i' % name
os.system(swig_cmd)
sources = ['../%s.cpp' % name, '%s_wrap.cxx' % name]
setup(name=name,
ext_modules= [
Extension('_' + name,
sources,
include_dirs=['..',
numpy.get_include() + "/numpy"])])
To create and install the extension module
locally in the current working directory (.),
we run
python setup.py install --install-platlib=.
Now we can do import ODEFieldSolver in Python and access the
C++ class as a Python class.
The FEniCS solver for the parabolic PDE can be implemented as
a class:
class ParabolicSolver:
def __init__(self, N, dt):
"""Set up PDE problem for NxN mesh and time step dt."""
from dolfin import UnitSquareMesh, FunctionSpace, TrialFunction, \
TestFunction, Function, dx, dot, grad
mesh = UnitSquareMesh(N,N)
self.V = V = FunctionSpace(mesh, "Lagrange", 1)
u = TrialFunction(V)
v = TestFunction(V)
a = u*v*dx + dt*dot(grad(u), grad(v))*dx
self.a = a
self.dt = dt
self.mesh = mesh
self.U = Function(V)
def advance_one_timestep(self, f, u_1):
"""
Solve the PDE for one time step.
f: the source term in the PDE.
u_1: solution at the previous time step.
"""
from dolfin import TestFunction, dx, solve
V, a, dt = self.V, self.a, self.dt # strip off self prefix
v = TestFunction(V)
L = (u_1 + dt*f)*v*dx
solve(self.a == L, self.U)
return self.U
The following pseudo code illustrates how to work with this code and
the ODE solver:
for i in range(num_time_steps): # time loop
<compute f>
<call ParabolicSolver's advance_one_time_step>
<compute g>
<call the ODE solver>
A complete code goes as follows:
import dolfin
import numpy
def F(S, U):
if isinstance(S, dolfin.Function) and isinstance(U, dolfin.Function):
from dolfin import *
f = sin(S)*exp(U)
return f
if isinstance(S, numpy.ndarray) and isinstance(U, numpy.ndarray):
from numpy import *
f = sin(S)*exp(U)
return f
class ParabolicSolver:
def __init__(self, N, dt):
"""Set up PDE problem for NxN mesh and time step dt."""
from dolfin import UnitSquareMesh, FunctionSpace, TrialFunction, \
TestFunction, Function, dx, dot, grad
mesh = UnitSquareMesh(N,N)
self.V = V = FunctionSpace(mesh, "Lagrange", 1)
u = TrialFunction(V)
v = TestFunction(V)
a = u*v*dx + dt*dot(grad(u), grad(v))*dx
self.a = a
self.dt = dt
self.mesh = mesh
self.U = Function(V)
def advance_one_timestep(self, f, u_1):
"""
Solve the PDE for one time step.
f: the source term in the PDE.
u_1: solution at the previous time step.
"""
from dolfin import TestFunction, dx, solve
V, a, dt = self.V, self.a, self.dt # strip off self prefix
v = TestFunction(V)
L = (u_1 + dt*f)*v*dx
solve(self.a == L, self.U)
return self.U
import dolfin
import numpy
N = 12 # mesh partition
dt = 0.01 # time step
parabolicsolver = ParabolicSolver(N, dt)
U1 = dolfin.Function(parabolicsolver.V)
U0 = dolfin.Function(parabolicsolver.V)
U0.vector()[:] = numpy.random.random(parabolicsolver.V.dim())
Q = dolfin.FunctionSpace(parabolicsolver.mesh, "DG", 0)
S0_ex = dolfin.Expression("x[0]")
S0 = dolfin.interpolate(S0_ex, Q)
S1 = dolfin.Function(Q)
import ODEFieldSolver # import module wrapping the ODE solver
odesolver = ODEFieldSolver.ODEFieldSolver()
odesolver.redim(S0.vector().size(0))
odesolver.set_IC(S0.vector().array())
plot = True
for i in range(0, 23): # time loop
f = F(S0, U0)
U1 = parabolicsolver.advance_one_timestep(f, U0)
U1c = dolfin.project(U1, Q)
odesolver.set_u(U1c.vector().array())
odesolver.advance_one_timestep()
S1.vector()[:] = odesolver.get_s()
U0 = U1
S0 = S1
if plot:
dolfin.plot(U1, title="U")
dolfin.plot(S1, title="S")
dolfin.interactive()
Wrapping with Cython
Cython can also be used to wrap C/C++ code.
To this end we define the C++ class in a .pyx file:
cimport numpy as cnp
import numpy as np
cdef extern from "ODEFieldSolver.h":
cppclass ODEFieldSolver_ "ODEFieldSolver":
ODEFieldSolver()
void redim(int n)
int size()
double g(double s, double u)
void set_dt(double dt)
void set_IC(int n, double* array)
void set_const_IC(double s0)
void set_u (int n, double* array)
void set_IC(double val)
void set_u (double const_value)
void advance_one_timestep()
void get_s (int n, double* array)
Here, we redefine the name to ODEFieldSolver_ such that we may shadow the
underlying class with the following Python class:
cdef class ODEFieldSolver:
cdef ODEFieldSolver_ *wrapped
def __init__(self):
self.wrapped = new ODEFieldSolver_()
def __dealloc__(self):
if self.wrapped != NULL:
del self.wrapped
We have a pointer called wrapped to the underlying SWIG-generated
C interface to the C++ code.
Simple functions like e.g. redim are straightforward to wrap,
def redim(self, n):
self.wrapped.redim(n)
For the set_IC function we need to check that the input argument is a contiguous 1-dimensional numpy array
of type double. This is spesified as cnp.ndarray[double, ndim=1, mode='c']. Further, we check that
the length of the input array is the same as self.wrapped.size before we pass the input array to the
underlying C++ object.
def set_IC(self, cnp.ndarray[double, ndim=1, mode='c'] array):
if array.shape[0] != self.wrapped.size():
raise ValueError('incorrect dimension on array')
self.wrapped.set_IC(array.shape[0], &array[0])
We allow the user to employ the get_s function both with and without an input
argument, refered to as out. If the user does not supply any input,
i.e., if out is None then we create an array of appropriate type and size. Otherwise,
we check that out has the appropriate type and size before the array is passed to
the C++ object’s get_s function. At the end, we simply return out.
def get_s(self,
cnp.ndarray[double, ndim=1, mode='c'] out=None):
if out is None:
out = np.empty(self.wrapped.size(), dtype=np.double)
elif out.shape[0] != self.wrapped.size():
raise ValueError('incorrect dimension on out')
self.wrapped.get_s(out.shape[0], &out[0])
return out
Finally, the module is built using a setup.py script for mixing Python, Cython, and C++ code:
from distutils.core import setup
from distutils.extension import Extension
from Cython.Distutils import build_ext
setup(
cmdclass = {'build_ext': build_ext},
ext_modules = [Extension("ODEFieldSolver",
["ODEFieldSolver.pyx", "../ODEFieldSolver.cpp"],
include_dirs=['..'],
language='c++')]
)