Click here to Skip to main content
6,822,613 members and growing! (20,033 online)
Email Password   helpLost your password?
General Programming » Algorithms & Recipes » Math     Intermediate License: The Code Project Open License (CPOL)

Solving ordinary differential equations in C++

By headmyshoulder

This article explains a framework for solving ordinary differential equations, which is based on template metaprogramming.
C++, Windows, Linux, STL, Architect, Dev
Revision:6 (See All)
Posted:10 Nov 2009
Views:6,310
Bookmarked:34 times
printPrint   add Share
      Discuss Discuss   Broken Article?Report  
14 votes for this article.
Popularity: 5.18 Rating: 4.52 out of 5

1

2
3 votes, 21.4%
3
1 vote, 7.1%
4
10 votes, 71.4%
5

Introduction

This article introduces the C++ framework odeint for solving ordinary differential equations (ODEs), which is based on template meta-programming. I think this framework has some nice advantages over existing code on ODEs, and it uses templates in a very elegant way. Furthermore, odeint has a STL-like syntax, and is independent of a specific container. The whole project lives in the boost sandbox; the code presented here is more or less a snapshot of the current development.

Background

I will only very briefly describe ordinary differential equations. If there is some interest in a more detailed explanation of ODEs, I can extend this part in future versions of the article.

Ordinary differential equations

An ordinary differential equation describes the evolution of some quantity x, and has the form:

d x(t) / d t = f( x(t) )

The function f defines the ODE, and x and f can be vectors. Associated with every ODE is an initial value problem (IVP) that is the ODE, and an initial value x(t0)=x0. The solution of the initial value problem is the temporal evolution of x(t), with the additional condition that x(t0)=x0, and it can be shown that every IVP has a unique solution. Of course, ordinary differential equations are not restricted to temporal problems, hence the variable t can be replaced by another quantity, like a spatial coordinate. ODEs and their relative PDEs (partial differential equation) are very important in nearly all scientific disciplines. All major equations in physics fall in this class, like Newton's law for classical physics, the Maxwell's equations for electromagnetism, the Schrödinger equation and its relativistic generalizations for the quantum world, or Einstein's equation for general relativity. Very often, the spatial axes of a PDE are discretized, and an ODE on a lattice results.

In the following figure, an example of an ODE from chaos theory is shown: the famous Lorenz attractor. It was the first example of a deterministic chaotic system, and triggered a huge amount of scientific work. The Lorenz attractor is described by a set of coupled ordinary differential equations:

d x1 / dt = sigma * ( x2 - x1 )
d x2 / dt = R * x1 - x2 - x1 * x3
d x3 / dt = x1 * x2 - b * x3

with:

sigma = 10
R = 28
b = 8 / 3

This equation has no analytical solution, such that it can only be solved numerically.

lorenz_const_step.jpg

Numerical solutions of ODEs

To solve ODEs numerically, various methods exist; all of them discretize the time. The easiest method is surely the explicit Euler scheme, which writes the derivative as the difference quotient:

d x(t) / d t = x(t+dt) - x(t) / dt

Then, some basic algebraic manipulations yield:

x(t+dt) = x(t) + dt*f(x(t))

such that x(t+dt) can be obtained from the knowledge of x at the time t. Note again, that x does not have to be a scalar, but can be a vector. The rest is very simple. We choose an initial condition x(t=0)=x0, and iteratively apply the Euler step to x(t) to obtain the evolution of x(t). Of course, more advanced solvers exist, and the most commonly used solver is probably the Runge-Kutta method of fourth order.

In the following source code, the function f(x(t)) defining the ODE is synonymously called system or dynamical system.

The main ideas

Now, we come to the design aspects of the library. We clearly need two main building blocks:

  1. A stepper function, which calculates the state x(t+d t) from the knowledge of x(t).
  2. An integrate function, which performs the iteration, hence which applies the stepper function many times.

Stepper methods

We try to avoid abstract classes, since this costs some extra performance. Instead, we use concepts which describe how a type or class has to behave, and whose methods have to be available. For the stepper function, we need the following ingredients:

template< class Container , class Time = double , 
          class Resizer = resizer< Container > >
class ode_step
{
 // provide basic typedefs
 public:

    typedef Container container_type;
    typedef Resizer resizer_type;
    typedef Time time_type;
    typedef const unsigned short order_type;
    typedef typename container_type::value_type value_type;
    typedef typename container_type::iterator iterator;

 // public interface
 public:

    order_type order() const;

    template< class DynamicalSystem >
    void next_step( DynamicalSystem &system ,
                    container_type &x ,
                    const container_type &dxdt ,
                    time_type t ,
                    time_type dt );

    template< class DynamicalSystem >
    void next_step( DynamicalSystem &system ,
                    container_type &x ,
                    time_type t ,
                    time_type dt );
};

The container type defines how the stated is stored. It has to fulfill some basic requirements, like the begin() and end() iterator and the value type. The resizer is an abstraction of the resize() functionality of the sequence concept. Its usage will be shown below. The next_step functions calculate x(t+dt) from the knowledge of x(t), and they transform x(t) in-place, meaning that x(t) is replaces by x(t+dt) within the next_step function. The argument system defines the ODE. This can be a simple function with three arguments: system( state , dxdt , t ), or a class with the appropriate brackets operator()(state,dxdt,t). Two versions of next_step exist: one which calculates x(t+dt) with the explicit knowledge of f(x(t)), and one which calculates this value internally. In principle, the second version only calls the first version. Both functions exist, because in many situations, the user needs the knowledge of f(x(t)) and will call the system method with x(t). To avoid a double call of system, the next_step version with the explicit declaration of f(x(t)) is introduced.

Note that the above class definition is not found in the code; it is only used here, to show the stepper functionality. All specific classes possessing these typedefs and methods are said to be stepper classes; we also say that all classes with these definitions fulfill the stepper concept.

Now, we look at a specific method, the Euler solver:

template< class Container , class Time = double , 
          class Resizer = resizer< Container > >
class ode_step_euler
{
 // provide basic typedefs
 public:

    typedef Container container_type;
    typedef Resizer resizer_type;
    typedef Time time_type;
    typedef const unsigned short order_type;
    typedef typename container_type::value_type value_type;
    typedef typename container_type::iterator iterator;

 // private members
 private:

    container_type m_dxdt;
    resizer_type m_resizer;

 // public interface
 public:

    order_type order() const { return 1; }

    template< class DynamicalSystem >
    void next_step( DynamicalSystem &system ,
                    container_type &x ,
                    const container_type &dxdt ,
                    time_type t ,
                    time_type dt )
    {
        // computes x = x + dt * dxdt
        detail::it_algebra::increment( x.begin() , x.end() , dxdt.begin() , dt );
    }

    template< class DynamicalSystem >
    void next_step( DynamicalSystem &system ,
                    container_type &x ,
                    time_type t ,
                    time_type dt )
    {
        m_resizer.adjust_size( x , m_dxdt );
        system( x , m_dxdt , t );
        next_step( system , x , m_dxdt , t , dt );
    }
};

The definition is straightforward, only three notes:

  • All iterator operations have been put in their own functions. This has the advantage that the code is small and clear, since no iterators need to be defined in the stepper functions. Furthermore, it could be possible to specialize and optimize iterator functions for specific iterators.
  • The resizer checks that the internally stored derivative m_dxdt has the same size as the state. Only, using the resize() function of the sequence concept is not sufficient here. For example, the std::tr1::array has no resize() and could not be used with this code. But a specialized version of the resizer for arrays exists, which will avoid calling resize(). Furthermore, the resizer concepts could allow the usage of matrix types or types from Blitz++ or MTL.
  • The resizer functionality is necessary if the system size changes during the evolution; think, for example, of an ODE defined on a network with a non-constant number of nodes.

This is not the end of the story of the stepper functions. odeint also provides an advanced stepper concept which calculates the numerical errors made during each step. With this error calculation, it is possible to adapt the step-size dt during each step in the iteration. But this concept will not be described here.

The following code demonstrates the usage of the stepper function:

#include <iostream>

#include <tr1/array>

#include <odeint.hpp>


#define tab "\t"

using namespace std;
using namespace odeint;

typedef std::tr1::array< double , 3 > state_type;

const double sigma = 10.0;
const double R = 28.0;    
const double b = 8.0 / 3.0;

void lorenz( state_type &x , state_type &dxdt , double t )
{                                                         
    dxdt[0] = sigma * ( x[1] - x[0] );                    
    dxdt[1] = R * x[0] - x[1] - x[0] * x[2];              
    dxdt[2] = x[0]*x[1] - b * x[2];                       
}                                                         

int main( int argc , char **argv )
{
    const double dt = 0.01;

    state_type x = {{ 1.0 , 0.0 , 0.0 }};
    ode_step_euler< state_type > stepper;

    double t = 0.0;
    for( size_t oi=0 ; oi<10000 ; ++oi,t+=dt )
    {
        stepper.next_step( lorenz , x , t , dt );
        cout << x[0] << tab << x[1] << tab << x[2] << endl;
    }

    return 0;
}

Here, lorenz could also be defined as a class:

class lorenz_class
{
 public:
    void operator()( state_type &x , state_type &dxdt , double t )
    {
        dxdt[0] = sigma * ( x[1] - x[0] );
        dxdt[1] = R * x[0] - x[1] - x[0] * x[2];
        dxdt[2] = x[0]*x[1] - b * x[2];
    }
}

which is then called via:

lorenz_class lorenzo;
stepper.next_step( lorenzo , x , t , dt );

With classes more complicated, dynamical systems (ODEs) can be created.

Integration function

The next main idea is to provide one or more functions to automatically iterate the state of the ODE, with the possibility to observe the state in each step. This is done with the integrate function:

template< class Stepper , class DynamicalSystem , class Observer >
void integrate( Stepper &stepper ,
                DynamicalSystem &system ,
                typename Stepper::time_type start_time ,
                typename Stepper::time_type dt ,
                typename Stepper::container_type &state ,
                typename Stepper::time_type end_time ,
                Observer &observer )
{
    if( start_time > end_time )
        throw std::invalid_argument( "integrate() : start_time > end_time" );

    observer( start_time , state , system );
    while( start_time < end_time )
    {
        stepper.next_step( system , state , start_time , dt );
        start_time += dt;
        observer( start_time , state , system );
    }
}

which solves the ODE with constant steps dt in the time interval start_time, end_time. The initial state has also to be provided. Furthermore, the concept of an observer is introduced, although this is not exactly an Observer pattern. Again, the observer can have any type, it only has to have the bracket operator, operator()( time , state , system ). For example, lambda expressions from boost can be used like:

integrate( stepper , system , 0.0 , dt , x , 10.0 , 
           cout << _1 << "\t" << _2[0] << "\n" );

or:

vector<double> x;
back_insert_iterator< vector<double> > iter( x );
integrate( stepper , system , 0.0 , dt , x , 10.0 , var(*iter++) = _2[0] );

Furthermore, an integrate version with an adaptive step-size exists.

Summary and outlook

I have shown you some design aspects, and the usage of odeint - a C++ framework for solving ordinary differential equations. It is very easy to use and very flexible. The performance is also very good. I have made one test run, where I compared odeint with the gsl. As a test system, the Lorenz system was used. It turned out that odeint is about two times faster than the gsl routines.

The source code provided with this article is a snapshot of the current development. It is not complete, and will be extended in the near future. For example, now odeint contains only the Euler method and Runge-Kutta 4, but higher order methods will be introduced very soon. We have a small Todo list, which has to be completed before the final release:

  • More methods: RK8, RK78, Midpoint, RK45
  • An observer container
  • Adaptors for Blitz++, MTL, and boost::ublas
  • Symplectic integrators for separable Hamiltonian system
  • Documentation

If you like to contribute to this library, or have some interesting points, criticism, or suggestions, you are cordially invited.

History

  • 9.11.2009 - Initial document.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

headmyshoulder


Member

Location: Germany Germany

Other popular Algorithms & Recipes articles:

Article Top
You must Sign In to use this message board.
FAQ FAQ 
 
Noise Tolerance  Layout  Per page   
 Msgs 1 to 10 of 10 (Total in Forum: 10) (Refresh)FirstPrevNext
GeneralHow to compile it? Pinmembercaviiar13:38 30 Nov '09  
GeneralRe: How to compile it? Pinmemberheadmyshoulder1:25 1 Dec '09  
GeneralRe: How to compile it? Pinmembercaviiar11:04 1 Dec '09  
GeneralRe: How to compile it? Pinmemberheadmyshoulder11:57 1 Dec '09  
GeneralRe: How to compile it? Pinmembercaviiar11:13 3 Dec '09  
GeneralRe: How to compile it? Pinmemberheadmyshoulder11:21 3 Dec '09  
GeneralRe: How to compile it? Pinmembercaviiar14:35 3 Dec '09  
GeneralI suggest you remind yourself about numerical analysis. PinmemberMicroImaging16:45 12 Nov '09  
GeneralRe: I suggest you remind yourself about numerical analysis. Pinmemberheadmyshoulder21:17 12 Nov '09  
GeneralRe: I suggest you remind yourself about numerical analysis. PinmemberMario Mulansky2:18 13 Nov '09  

General General    News News    Question Question    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads.

PermaLink | Privacy | Terms of Use
Last Updated: 10 Nov 2009
Editor: Smitha Vijayan
Copyright 2009 by headmyshoulder
Everything else Copyright © CodeProject, 1999-2010
Web22 | Advertise on the Code Project