Introduction to block structure and field.
This tutorial walks you through the process of creating a simple waLBerla application. The source file of this tutorial can be found in apps/tutorials/01_BlocksAndFields.cpp. To compile and run this example, go to your build directory into
make and run the generated executable.
The basic data structure of waLBerla are so called blocks. Think of a block as the container of your actual simulation data, for example the lattice (or field as it is called in waLBerla). When writing a simple serial lattice Boltzmann code, one usually has one field representing the whole domain. In waLBerla, the domain is first partitioned into blocks, where each block holds its own field. This has the advantage that the domain can easily be distributed to multiple processes, where every process can have one or more local blocks.
The following snippet shows how to create a simple domain decomposition that results in a uniform block grid:
Before calling any waLBerla functions one has to create an Environment object, passing the command line arguments. This object takes care of initializing and destroying singletons like for example the MPIManager. This object can also read and parse an input/configuration file, as we will see in a later tutorial.
After initializing waLBerla, the blockforest::createUniformBlockGrid() function sets up a domain with 3×2×4 blocks in x,y,z direction, respectively. We are creating a structured block storage here, meaning the block storage has also knowledge about how the blocks are structured. Here, we specify that each block is structured in a grid consisting of 10×8×12 cells, where each cubic cell has the physical dimension of 0.5×0.5×0.5. With the next parameter, we can choose between assigning all blocks to a single process (which we do here) or putting only one block per process. In case we would only assign one block to each process, we would have to start the program with 3×2×4 processes (since we have 3×2×4 blocks). The last three parameters determine if the domain is periodic in x, y, or z direction.
There are a lot more ways for initializing the block decomposition of the simulation space in waLBerla. For this tutorial, we just use a very simple form of decomposition. In later tutorials, we will see that the domain decomposition can also be controlled via a configuration file that is passed to the application. Also, in general, you can have an arbitrary number of blocks on each process. This can be important for balancing work load. For load balancing, there exist many different algorithms: some make use of METIS, others are based on space filling curves. It is even possible to decompose the simulation space into a set of octrees, resulting in a distributed forest of octrees data structure that contains blocks of different sizes. This form of decomposition can be used for simulations that require some form of grid refinement. For now, however, we stick to a simple, uniform block decomposition.
A few words about the (basic) data types used in waLBerla:
WALBERLA_DOUBLE_ACCURACY(true by default). If, for a certain algorithm, you always need your floating point data type to be 32 or 64 bit, you of course have to use float or double instead of walberla::real_t.
You might wonder: Why are all these casts used in the code snippet above? Because every literal constant has a type.
3, for example, is of type int. blockforest::createUniformBlockGrid(), however, expects its first argument to be of type walberla::uint_t.
0.5 is of type double, but a walberla::real_t (which might be a single precision float!) is expected. If you want to know more about literal constants and their types, see http://www.cplusplus.com/doc/tutorial/constants/. In order to get rid of all type conversion warnings in every compiler, it turns out wrapping every literal constant with a walberla::*_c function is a good idea.
In order to visualize the domain, we can use the graphical user interface (GUI) module of waLBerla. Since this module depends on the Qt Library, one has to explicitly enable the module before compiling waLBerla. This is done by setting the CMake Option WALBERLA_ENABLE_GUI.
We have to add two more includes to our file, one for the GUI module and one for the time loop, which is used by the GUI. A detailed description of the time loop follows in the next tutorial.
In the main function we first create a time loop object, which we pass to the GUI:
When you run the program now, a window should open showing the domain consisting of blocks:
If no window opens, the GUI was probably not compiled. Make sure that WALBERLA_ENABLE_GUI is enabled and Qt was found.
So far, we have partitioned our domain into blocks. The next step is to allocate data on these blocks. Think of blocks as containers for your simulation data, where arbitrary data can be stored.
For a lattice Boltzmann simulation, we need a lattice data structure on each block. In waLBerla, the class for storing lattices is called field::Field. It is a four dimensional structure, where the first three dimensions correspond to 3D space, whereas the fourth dimension can be used for indexing within one cell. In lattice Boltzmann simulations, the fourth dimension is usually used for indexing the probability distribution functions (PDFs). This field has two template parameters, the first specifies the data type that is stored, the second the size of the fourth dimension. In our case, we want to store real numbers in the field. We could use float or double values here, however, we use the real_t data type of waLBerla. For this example, we want to create a scalar field, so the second template parameter is set to one.
We want to create a field on each block. Since the blocks are managed by waLBerla (how many there are, on which process they are allocated, ...), we have to tell the framework how to create this field. This is done via a callback function, which is called by waLBerla on every process for every process-local block. This function gets the current block where the data should be allocated, and a constant instance of the block storage, which can be used to get global domain information (for example how many cells per block there are supposed to be). It has to return a pointer to the created block data, which is automatically freed by the framework when the block storage is destroyed. So now lets write the call back function to create a scalar field:
The size of the field is determined by the number of cells provided by the StructuredBlockStorage. So it is the responsibility of the user to create fields that have the same size as previously set in the StructuredBlockStorage. Using this setup mechanism, waLBerla does not enforce that the fields have the same size as the block storage assumes.
Remember: For waLBerla, a block is just a container for arbitrary data - and a field is just an "arbitrary" data item stored on each block. Block data does not have to be any waLBerla data structure. It is possible to store any type of data on a block, so instead of using the field class, we could, for example, have used a std::vector<std::vector<double> > to store our lattice.
The callback function can now be registered at the block storage with the following piece of code:
In order to view the field in the GUI, click on "New Grid View" and drag a block into the window. The "Grid View Properties" dock widget can then be used to display slices of the field.
The next tutorial contains the writing of algorithms operating on block data: tutorial02