![]() |
General Programming »
Algorithms & Recipes »
Math
Intermediate
License: The Code Project Open License (CPOL)
Solving ordinary differential equations in C++By headmyshoulderThis article explains a framework for solving ordinary differential equations, which is based on template metaprogramming. |
C++, Windows, Linux, STL, Architect, Dev
|
|
Advanced Search Add to IE Search |
|
|
|
||||||||||||||||
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.
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.
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.

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.
Now, we come to the design aspects of the library. We clearly need two main building blocks:
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:
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.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.
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.
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:
If you like to contribute to this library, or have some interesting points, criticism, or suggestions, you are cordially invited.
| You must Sign In to use this message board. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
General
News
Question
Answer
Joke
Rant
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 |