Now that we have defined the simulation domain and the boundary conditions, we need to specify the initial conditions, numerical schemes parameters and so on. This is all done within the second optional parameters block of the graph definition.
In our file we have for the moment only one object in this block:
GfsTime { end = 0 }As its name indicate, this object defines the physical and the computational time. By ``computational time'' I mean the number of time steps performed. By default both the physical time and the time step number are zero when the program starts. It is possible to set different values using for example
GfsTime { t = 1.4 i = 32 end = 0 }where i is the time step number and t is the physical time. The end identifier specifies that the simulation should stop when the physical time reaches the given value. It is also possible to stop the simulation when a specified number of time steps is reached, using the iend identifier. If both end and iend are specified, the simulation stops when either of these are reached. By default, both end and iend values are infinite.
Ok, let's then change this object to
GfsTime { end = 50 }
The next step is to specify what spatial resolution we want for the discretisation. For the moment, the only thing we have defined is the root of the quad/octree. The whole domain is thus discretised with only one grid point...
We need to specify how we want to refine this initial root cell. This is done by using an GfsRefine object. We can do this by adding the line
GfsRefine 6to the second optional parameter block. This is the simplest possible way to refine the initial root cell. We tell the program that we want to keep refining the cell tree (by dividing each cell in four children cells (in 2D, eight in 3D)) until the level of the cell is equal to five. The level of the root cell is zero, the level of all its children cells is one and so on recursively. After this refinement process completes we have created a regular Cartesian grid with 26 = 64
We now need to specify the initial conditions and the various actions (such as writing results, information messages etc...) we want to perform when the simulation is running. All these things are treated by Gerris as various types of events, all represented by objects derived from the same parent object GfsEvent.
Initial conditions are a particular type of event happening only once and before any other type of event, they are all derived from the same parent object GfsInit.
Gerris comes with a few different objects describing various initial conditions. As there is no way Gerris could provide all the different initial conditions users could think of, Gerris makes it easy for users to create their own initialisation objects by extending the GfsInit object class. In order not to have to recompile (or more exactly re-link) the whole code everytime a new class is added, Gerris uses dynamically linked modules which can be loaded at runtime. We will see later how to write your own modules.
For the moment, we will use the default GfsInit object. Just add the following lines to vorticity.gfs:
1 2 GfsSimulation GfsBox GfsGEdge {} { GfsTime { end = 50 } GfsRefine 6 GfsInit {} { U = (0.5 - rand()/(double)RAND_MAX) V = (0.5 - rand()/(double)RAND_MAX) } } GfsBox {} 1 1 right 1 1 topUsing GfsInit it is possible to set the initial value of each of the simulation variables. By default all variables are set to zero initially. In our case we tell Gerris to define the two components of the velocity field U and V as C functions. The standard rand function of the C library returns a (pseudo)-random number between 0 and RAND_MAX. The two functions we defined thus set the components of the velocities in each cell as random numbers between -0.5 and 0.5.
This is a powerful feature of the parameter file. In most cases where Gerris requires a number (such as the GfsRefine 6 line, a function of space and time can be used instead. For example, a valid parameter file could include:
... GfsRefine 6.*(1. - sqrt (x*x + y*y)) ...which would define a mesh refined in concentric circles.
Using this feature, it is possible to define most initial conditions directly in the parameter file.
The vorticity.gfs file we have now is all Gerris needs to run the simulation. However, for this run to be any use, we need to specify how and when to output the results. This is done by using another class of objects: GfsOutput, derived from GfsEvent. Gerris comes with a number of these objects allowing to output various aspects of the simulation.
The general syntax for an GfsEvent object is as follows:
GfsEvent { start = 0.5 step = 1e-2 end = 3.4 istart = 10 iend = 46 }this defines an event:
An GfsOutput object is derived from GfsEvent and follows this syntax:
GfsOutput {} filename-%d-%f-%ldThe first part of the line GfsOutput {} defines the GfsEvent part of GfsOutput and follows the syntax above. In the remainder of this tutorial, I will use the following notation to express this inheritance of syntax:
[GfsEvent] filename-%d-%f-%ldto avoid repeating the whole thing for every derived objects.
The second part filename-%d-%f-%ld specifies where to output things. The %d, %f and %ld characters are formatting strings which follow the C language syntax and will be replaced every time the event takes place according to:
We now add the following to our simulation file:
1 2 GfsSimulation GfsBox GfsGEdge {} { GfsTime { end = 50 } GfsRefine 6 GfsInit {} { U = (0.5 - rand()/(double)RAND_MAX) V = (0.5 - rand()/(double)RAND_MAX) } GfsOutputTime { istep = 10 } stdout GfsOutputProjectionStats { istep = 10 } stdout } GfsBox {} 1 1 right 1 1 topThe first line we added tells the program to output information about the time every 10 time steps on the standard output. The second line outputs statistics about the projection part of the algorithm.
You can now run the code like this:
% gerris2D vorticity.gfsand you should get an output in your console looking like this (you can stop the program using Ctrl-C):
step: 0 t: 0.00000000 dt: 0.000000e+00 MAC projection before after rate niter: 0 residual.bias: 0.000e+00 0.000e+00 residual.first: 0.000e+00 0.000e+00 0.0 residual.second: 0.000e+00 0.000e+00 0.0 residual.infty: 0.000e+00 0.000e+00 0.0 Approximate projection niter: 0 residual.bias: 0.000e+00 0.000e+00 residual.first: 1.050e-14 1.050e-14 0.0 residual.second: 1.612e-14 1.612e-14 0.0 residual.infty: 7.105e-14 7.105e-14 0.0 step: 10 t: 0.02190704 dt: 2.801016e-03 MAC projection before after rate niter: 5 residual.bias: -3.053e-16 1.403e-16 residual.first: 3.365e+01 2.949e-05 16.3 residual.second: 4.274e+01 4.676e-05 15.6 residual.infty: 1.954e+02 3.285e-04 14.3 Approximate projection niter: 5 residual.bias: 9.714e-17 2.874e-16 residual.first: 3.322e+01 2.548e-05 16.7 residual.second: 4.250e+01 4.062e-05 16.0 residual.infty: 1.880e+02 3.380e-04 14.1 step: 20 t: 0.05278371 dt: 3.531551e-03 MAC projection before after rate niter: 5 ...The lines starting with step: are written by GfsOutputTime. They give the time step number, corresponding physical time and the time step used for the previous iteration.
The other lines are written by GfsOutputProjectionStats and give you an idea of the divergence errors and convergence rate of the two projection steps (MAC and approximate) performed during the previous iteration. The various norms of the residual of the solution of the Poisson equation are given before and after the projection step. The rate column gives the average amount by which the divergence is reduced by each iteration of the multigrid solver.
Well, numbers are great but what about some images? What we want to do, for example, is output some graphical representation of a given scalar field. In 2D, a simple way to do that is to create an image where each pixel is coloured according to the local value of the scalar. Gerris provides an object to do just that: GfsOutputPPM which will create a PPM (Portable PixMap) image. This object is derived from a more general class used to deal with scalar fields: GfsOutputScalar following this syntax:
[GfsOutput] { v = U min = -1 max = 2.5 }where as before the square brackets express inheritance from the parent class. The v identifier specifies what scalar field we are dealing with, one of:
We can now use this in our simulation file:
1 2 GfsSimulation GfsBox GfsGEdge {} { GfsTime { end = 50 } GfsRefine 6 GfsInit {} { U = (0.5 - rand()/(double)RAND_MAX) V = (0.5 - rand()/(double)RAND_MAX) } GfsOutputTime { istep = 10 } stdout GfsOutputProjectionStats { istep = 10 } stdout GfsOutputPPM { step = 1 } vorticity-%4.1f.ppm { v = Vorticity } } GfsBox {} 1 1 right 1 1 topThe code will output every 1 time units a PPM image representing the vorticity field. The result will be written in files named: vorticity-00.0.ppm, vorticity-01.0.ppm...(if the %4.1f thing is not familiar, consult a C book or try % man 3 printf).
If you re-run the program using this new simulation file, you will get a number of PPM files (51 to be precise) you can then visualise with any image editing or viewing tool. I would recommend the very good ImageMagick toolbox. If you run a Linux box, these tools are very likely to be already installed on your system. Try typing this in your working directory:
% display *.ppmIf it works, you should see a small (64x64) image representing the initial vorticity field. If you click on it, a menu will appear. Select File
Before we carry on, we are going to make two modifications to the simulation file. First of all, it is not really handy to generate one file for every image generated. ImageMagick (and most other programs) can deal with multiple PPM images contained within the same file. Secondly, in the sequence of images we generate, a given value of the vorticity does not always correspond to the same colour (because the minimum and maximum values of the vorticity can vary in time). We can fix that like this:
1 2 GfsSimulation GfsBox GfsGEdge {} { GfsTime { end = 50 } GfsRefine 6 GfsInit {} { U = (0.5 - rand()/(double)RAND_MAX) V = (0.5 - rand()/(double)RAND_MAX) } GfsOutputTime { istep = 10 } stdout GfsOutputProjectionStats { istep = 10 } stdout GfsOutputScalarStats { istep = 10 } stdout { v = Vorticity } GfsOutputPPM { step = 0.1 } vorticity.ppm { v = Vorticity min = -10 max = 10 } } GfsBox {} 1 1 right 1 1 topWe have now specified fixed bounds for the vorticity (using the min and max identifiers). Each PPM image will be appended to the same file: vorticity.ppm.
How did I choose the minimum and maximum values for the vorticity? The line GfsOutputScalarStats { istep = 10 } stdout { v = Vorticity }, writes the minimum, average, standard deviation and maximum values of the vorticity. By re-running the simulation and looking at these values it is easy to find a suitable range.