The implementation of time integration schemes in Nektar++
General linear methods can be considered as the generalisation of a broad range of different numerical methods for ordinary differential equations. They were introduced by Butcher and they provide a unified formulation for traditional methods such as the Runge-Kutta methods and the linear multi-step methods. From an implementational point of view, this means that all these numerical methods can be abstracted in a similar way. As this allows a high level of generality, it is chosen in Nektar++ to cast all time integration schemes in the framework of general linear methods.
For background information about general linear methods, please consult the following references:
[1] Butcher, J.C. (2006) General linear methods, Acta Numerica 15, 157-256
[2] http://www.math.auckland.ac.nz/~butcher/conferences.html
The standard initial value problem can written in the form
where
is a vector containing the variable (or an array of array contianing the variables).
In the formulation of general linear methods, it is more convenient to consider the ODE in autonomous form, i.e.
Suppose the governing differential equation is given in autonomous form, the
step of the general linear method comprising
steps (as in a multi-step method)
stages (as in a Runge-Kutta method)is formulated as:
where
are referred to as the stage values and
as the stage derivatives. Both quantities are related by the differential equation:
The matrices
,
,
,
are characteristic of a specific method. Each scheme can then be uniquely defined by a partioned
matrix
Adopting the notation:
the general linear method can be written more compactly in the following form:
where
is the identity matrix of dimension
.
Although the General linear method is essentially presented for ODE's in its autonomous form, in Nektar++ it will be used to solve ODE's formulated in non-autonomous form. Given the ODE,
a single step of General Linear Method can then be evaluated in the following way:
, i.e. the
subvectors comprising ![$\boldsymbol{y}^{[n-1]}_i$](form_48.png)
, i.e. the equivalent of
for the time variable 
,
and the stage derivatives
are calculated through the relations:
is calculated as a linear combination of the stage derivatives
and the input vector
. In addition, the time vector
is also updated
, i.e. the
subvectors comprising
.
corresponds to the actual approximation at the new time level.
where
is equal to the new time level 
In Nektar++, we do not use the standard General Linear Methods formulation. As mentioned above, we will use the non-autonomous form as the explicit treatment of the time variable
allows for more flexibility.
For a detailed describtion of the formulation and a deeper insight of the numerical method see:
Peter E.J. Vos, Claes Eskilsson, Alessandro Bolis, Sehun Chun,Robert M. Kirby & Spencer J. Sherwin, (2011),
A generic framework for time-stepping partial differential equations (PDEs):
general linear methods, object-oriented implementation and application to fluid problems,
International Journal of Computational Fluid Dynamics, 25:3, 107-125
Nektar++ contains various classes and methods which implement the concept of the General Linear Methods. This toolbox is capable of numerically solving the generalised ODE using a broad range of different time-stepping methods. We distinguish the following types of General Linear Methods:
, i.e.
for
. To avoid confusion, we make a further distinction:
has now non-zero entries on the diagonal. This means that each stage value depend on the stage derivative at the same stage, requiring an implicit step. However, the calculation of the different stage values is still uncoupled. Best known are the DIRK schemes.The aim in Nektar++ is to fully support the first three types of General Linear Methods. Fully implicit methods are currently not implemented.
The goal of abstracting the concept of General Linear Methods is to provide users with a single interface for time-stepping, independent of the chosen method. The TimeIntegrationScheme class allow the user to numerically integrate ODE's using high-order complex schemes, as if it were done using the forward euler method. Switching between time-stepping schemes should be as easy as changing a parameter in an input file. The only thing the user should provide, is an implementation of the left and right hand side operation of the generalised ODE to be solved.
To introduce the implementation of time stepping schemes in Nektar++, consider the following example:
NekDouble timestepsize = 0.1; NekDouble time = 0.0; Array<OneD, Array<OneD, NekDouble> > y; LibUtilities::TimeIntegrationSchemeOperators ode; ode.DefineImplicitSolve (&function1, &object1); ode.DefineOdeRhs (&function2, &object2); LibUtilities::TimeIntegrationSchemeKey IntKey(LibUtilities::eClassicalRungeKutta4); LibUtilities::TimeIntegrationSchemeSharedPtr IntScheme = LibUtilities::TimeIntegrationSchemeManager()[IntKey]; LibUtilities::TimeIntegrationSolutionSharedPtr y_0; y_0 = IntScheme->InitializeScheme(timestepsize,y,time,ode); for(int n = 0; n < nsteps; ++n) { y = IntScheme->TimeIntegrate(timestep,y_0,ode); time += timestepsize; }
We can distinguish four different sections in the code above:
y will be used to store the solution at the end of each time-step. It corresponds to the vector
defined above. It is an Array of Arrays where the first dimension corresponds to the number of variables (eg. u,v,w) and the second dimension corresponds to the number length of the variables (e.g. the number of modes or the number of physical points).LibUtilities::TimeIntegrationSchemeOperators ode; ode.DefineImplicitSolve (&function1, &object1); ode.DefineOdeRhs (&function2, &object2);
ode is an object containig the methods. A class representing a PDE eqaution (or system of equations) should have some a series of functions representing the implicit/explicit part of the method, which represents the reduction of the PDE/s to a system of ODEs. The user should make sure this class contains a proper implementation of these necessary methods. &function1 is a functor, i.e. a pointer to a function where the method is implemented. &object1 is a pointer to the object, i.e. the class, where the function/method (&function1) is implemented. For more information about these methods, click here, were the hypotical class where the methods/functions are implemented is called Foo.LibUtilities::TimeIntegrationSchemeKey IntKey(LibUtilities::eClassicalRungeKutta4); LibUtilities::TimeIntegrationSchemeSharedPtr IntScheme = LibUtilities::TimeIntegrationSchemeManager()[IntKey];
LibUtilities::TimeIntegrationSolutionSharedPtr y_0; y_0 = IntScheme->InitializeScheme(timestepsize,y,time,ode);
y) and some additional parameters, this method constructs and returns an object y_0 which is the abstraction of the vectors
and
. This abstraction is done through the class TimeIntegrationSolution. This initialisation is essential in case of multi-step schemes, where the vector
consist of more than one entry. The object y_0 can than later be passed to the actual integration method, as it contains all necessary input.for(int n = 0; n < nsteps; ++n) { y = IntScheme->TimeIntegrate(timestep,y_0,ode); time += timestepsize; }
y_0 every time step to hold the vectors
and
at every time level n. In addition, it also returns the actual solution
(which in fact is also embedded in the object y_0)An object bar of the class Foo should be passed to the TimeIntegrationScheme class routines. This class Foo should have the following public member functions:
Foo::DoOdeRhs void Foo::DoOdeRhs(const Array<OneD, const Array<OneD, NekDouble> >& inarray, Array<OneD, Array<OneD, NekDouble> >& outarray, const NekDouble time);
inarray: the vector 
time: the time 
outarray: the result of the right hand side operatorinarray and outarray are Array of Arrays where the first dimension corresponds to the number of variables (eg. u,v,w) and the second dimension corresponds to the number length of the variables (e.g. the number of modes).Foo::DoImplicitSolve void Foo::DoImplicitSolve(const Array<OneD, const Array<OneD, NekDouble> >& inarray, Array<OneD, Array<OneD, NekDouble> >& outarray, const NekDouble time, const NekDouble lambda);
inarray: the vector 
time: the time 
lambda: the coefficient 
outarray: the result 
inarray and outarray are Array of Arrays where the first dimension corresponds to the number of variables (eg. u,v,w) and the second dimension corresponds to the number length of the variables (e.g. the number of modes).Dirichlet boundary conditions can be strongly imposed by lifting the known Dirichlet solution. This is equivalent to decompose the approximate solution
into an known lifted function,
, which satisfies the Dirichlet boundary conditions, and an unknown homogeneous function,
, which is zero on the Dirichlet boundaries, i.e.
In a Finite Element discretisation, this corresponds to splitting the solution vector of coefficients
into the known Dirichlet degrees of freedom
and the unknown homogeneous degrees of freedom
. If ordering the known coefficients first, this corresponds to:
The generalised formulation of the General Linear Method (i.e. the introduction of a left hand side operator) allows for an easier treatment of these types of boundary conditions. To better appreciate this, consider the equation for the stage values for an explicit general linear method where both the left and right hand side operator are linear operators, i.e. they can be represented by a matrix.
In case of a lifted known solution, this can be written as:
In order to calculate the stage values correctly, the Required Methods should now be implemented to do the following:
Foo::DoOdeRhs void Foo::DoOdeRhs(const Array<OneD, const Array<OneD, NekDouble> >& inarray, Array<OneD, Array<OneD, NekDouble> >& outarray, const NekDouble time);
will be used to calculate the stage values, as seen in the Foo::ODElhsSolve method. This means essentially, only the bottom part of the operation above, i.e.
Foo::DoOdeRhs method that also calculate
.Foo::DoImplicitSolve void Foo::DoImplicitSolve(const Array<OneD, const Array<OneD, NekDouble> >& inarray, Array<OneD, Array<OneD, NekDouble> >& outarray, const NekDouble time, const NekDouble lambda);
. This can be done in three steps:
,
Currently the time integration schemes below are implemented in Nektar++. We will list their coefficients here and we will also indicate what the auxiliary parameters
(
) of the multistep methods represent:
eForwardEuler, eAdamsBashforthOrder1
eBackwardEuler, eAdamsMoultonOrder1
eAdamsBashforthOrder2
eIMEXOrder1
eIMEXOrder2
is one. In the standard formulation it is 3/2)eIMEXOrder3
is one. In the standard formulation it is 11/6)eAdamsMoultonOrder2
eMidpoint
eClassicalRungeKutta4
eDIRKOrder2
eDIRKOrder3
eIMEXdirk_3_4_3
To add a new time integration scheme, follow the steps below:
1.7.1