You are on page 1of 4

Programming exercise 3, 2D1263, Scientific Computing

March 14, 2005


In this exercise you will use the array class from the second exercise to solve a PDE, on the grid you
generated in the first exercise. The incompressible Navier-Stokes equations, in two space dimensions, with
artificial compressibility are given by
ut + uux + vuy = px + (uxx + uyy )
vt + uvx + vvy = py + (vxx + vyy )
pt + ux + vy = 0
where u(x, y,t) and v(x, y,t) are the velocities in the x- and y-directions, and p(x, y,t) is the pressure. We
determine the steady state solution by iterating in time until the solution does not change. The pt term in
the last equation is artificial, but will disappear at steady state.
Represent the solution as a C++ class, for example
typedef struct {
bctype *u;
bctype *v;
bctype *p;
} nsbc;
class nssolve {
public:
nssolve(char *filename);
void timesteps(double dt, int nsteps);
void write_solution(char * filename);
nssolve();
protected:
double eps;
arc *u, *v, *p, *x, *y;
nsbc *bc[4];
virtual void initial_cond() = 0;
virtual void init_boundary() = 0;
};

// read x, y coordinates
// time stepping
// obvious

// epsilon
// boundary conditions
// prepare u, v, p at t = 0
// prepare bc

The real problem to be solved can then be defined by deriving from that virtual class. 1
At your disposal, you have the subroutine resid, written in Fortran 77, which you call to do the step
n
n
wn+1
i, j = wi, j + t r(wi, j )

where the equations are formally written as dw/dt = r(w) for the vector w = (u, v, p). This subroutine can
be obtained from the courses home page. The routine uses a three point difference method, so an overlap
of two should be sufficient. You have to think about how to let the resid routine access the data arrays in
1 As

usual, you can modify the interface according to your needs.

the objects u, v, p, x, y. One way is to make the function timesteps a friend of the array class. Another
is to extract the pointers to double from arc by adding a function getv to the array class.
The Fortran routine is declared in the C++ program through the statement
extern "C" int RESID(int*, int*, double*, double*, double*, double*,
double*, double*, double*, double*);
resid does not do anything on the boundary points. To handle (external, not inter-processor) boundaries,
you should define a base class bctype,2
class bctype {
public:
bctype(arc *u, int side);
virtual void impose(arc *u) = 0;
};
which serves as a base class for two derived classes bcdirichlet and bcneumann. You should create
one boundary condition object for each side of the domain. The function impose imposes the boundary
condition, and all that the derived classes need to do is to define the function. The bcdirichlet class
gives a value to the boundary point. The bcneumann class should give the normal derivative of the
solution. There, you need to add information on the direction normal to the boundary. To simplify the
programming, you will be allowed to consider the grid as orthogonal at the boundary, i.e. the normal
derivative can be computed along a single grid line. At the grid line i = 0, the boundary condition u/n =
u/x = 0 would then be u0 = u1 to first order accuracy, and u0 = 4/3u1 1/3u2 for second order accuracy.
The following boundary conditions should be used:
At the left boundary (inflow): u = 0.1y(3 y), v = 0, p/n = 0
At the right boundary (outflow): u/n = 0, v = 0, p = 0
At the upper and lower boundaries (walls): u = v = 0, p/n = 0.
Do not forget to check that a boundary is a physical boundary, and not an internal boundary between
processors.
Use the initial data
u(x, y, 0) = 0.1y(3 y)
v(x, y, 0) = 0
p(x, y, 0) = 0.
Use forward Euler, or the following two-stage Runge-Kutta method in time
w(1) = wn + t r(wn )
w(2) = w(1) + t r(w(1) )
wn+1 = (w(2) + wn )/2.
I found that this works on a 50 20 grid for = 0.1 with a time step of t = 0.002. You may well
experiment with the stability limit!
1. Implement the Navier-Stokes solver! Run it with grids of different sizes. Do not forget to adapt the
time step size. The first three elements of res provide a monitor to the convergence history. Use
MPI Reduce to obtain global values for the residual. Monitor them!
2. Measure the computing times of your codes for various meshes in dependence of the number of
processors. For that, use appropriate optimisation when compiling. The function MPI Wtime can
be used to find the wall clock time. Since one time step is very fast, I recommend that you time, say,
1000 steps.
2 Compare

Chapter 6 of the Lecture Notes.

The programming exercises should be done individually, or in groups of two. Hand in a report containing:
Comments and explanations that you think are necessary for understanding your program.
Tables containing the computing time versus the number of processors for different grid sizes.
One (one!) plot with the speedup curves for all different grid sizes. Explain your observations!
A picture of the steady state solution, using approximately 50 20 grid points. How many time steps
did you use and what are the residuals?
Program listing.
E-mail the source code to hanke@nada.kth.se

A. The routine resid


We give below the head of the routine resid.
subroutine RESID( ni, nj, u, v, p, res, x, y, eps, dt )
***********************************************************************
***
*** Resid computes a forward Euler step of the incompressible
*** Navier-Stokes equations with artificial compressibility.
***
*** A second order difference method with three point stencil is used.
***
*** Input: ni, nj - Size of grid.
***
u, v, p - Velocity and pressure at t_n
***
res
- Work space of size 3*ni*nj
***
x, y
- The grid.
***
eps
- Viscosity parameter, epsilon in the equations.
***
dt
- Time step.
***
*** Output: u, v, p - The velocity and pressure is updated to t_{n+1} by
***
forward Euler at the points (2..ni-1,2..nj-1)
***
res(1),res(2),res(3) - The first three elements of the
***
work space vector contains the maximum norm of the residual
***
for the three equations respectively.
***
*** NOTE: FORTRAN stores arrays column-wise, i.e., the first index
***
changes fastest. (ind = i + ni*(j-1) )
***
***********************************************************************
implicit none
integer ni, nj, i, j
real*8 u(ni,nj), v(ni,nj), p(ni,nj), eps, dt, x(ni,nj), y(ni,nj)
real*8 res(3,ni,nj)

B. Call conversion interface for resid


A small wrapper function can be written to avoid passing scalar arguments as pointers. Such a routine
may look like

void c_resid(int ni, int nj, double *u, double *v, double *p,
double *res, double *x, double *y, double eps, double dt)
{
int ni_tmp = ni;
int nj_tmp = nj;
double eps_tmp = eps;
double dt_tmp = dt;
RESID(&ni_tmp, &nj_tmp, u, v, p, res, x, y, &eps_tmp, &dt_tmp);
}
The function c resid only serves as an interface and resid still does all the computations. Besides
providing a more familiar interface there are two main reasons for using call conversion interfaces:
1. resid require pointer arguments and can not be called with constant input values. resid(50,
50, ...) would for example result in a compile time error.
2. The way Fortran 77 routines are called from C++ differs between platforms. Common names generated in the compiled object code include resid, resid , RESID and RESID . 3 By calling the
wrapper c resid instead at most two changes are needed as the program is ported to a different
platform.
Even more flexibility can be obtained by using call conversion interfaces defined as preprocessor directives.

3 Test

the calling convention in your environment!

You might also like