Introduces the sweep concept, sweep functions, sweep classes, sweeps on block
In the first tutorial, we set up a domain, build up out of blocks, where each block holds a field data structure. In this tutorial, we are going to write a function that operates on this data. Since the block is the basic unit of our simulation, almost all of our algorithms also work on a single block. This makes it easier to parallelize the program by distributing the blocks to different processes. A function that takes a single block as argument and operates on the block data is called sweep.
For demonstrating how to write a sweep, let us implement the following "algorithm":
In the first tutorial, we initialized the field with zeros using the constructor argument. Now we want to initialize the field with random values. There are two possibilities to achieve that: Since we have the initialization function that creates the field, this is the usual place to perform the initialization:
With this, we create an uninitialized field by leaving out the initVal constructor argument. Subsequently, we iterate the complete field and set each cell to some random value. Here is a screenshot of the randomly initialized field using the "Color Map" display style:
As we will see later, in most cases, there is no need to write a custom initialization function to add a field to all blocks. So in these cases, the field has to be initialized elsewhere.
We now go back to our original initialization function that initializes the field with zeros and have a look at another option to modify the field.
But how can we access the field that is stored inside a block?
In the main function, we have created a block storage, which holds all the blocks. So the first step is to iterate all blocks. Or more precisely: we iterate all process-local blocks. Since we do that on each process, we end up iterating all blocks in parallel. We can get the field out of every block and finally iterate the field to set the value for each cell.
Note the first line of the code snippet: when adding the block data, we get back an identifier (handle) which is required to access the added block data later on. In the first tutorial, we did not store this ID.
Having this ID available, we can iterate all blocks using the iterator provided by the block storage. Actually, we are not iterating ALL blocks, but rather only the ones stored locally. Thus when we later distribute the blocks to different processes, every process only iterates its process-local blocks.
Dereferencing the block iterator returns an IBlock, i.e. an object implementing a block interface. If we know the ID and type (!) of a block data item, we can retrieve the data from the IBlock. The IBlock::getData() function takes the BlockDataID of the data that we want to retrieve as an argument, and as a template parameter the type we expect. In principle, one could request a different type as initially stored, however, in debug mode, the error would be detected and the program would terminate.
So far, we know how to access blocks and the data stored on them. Hence, we could proceed to the implementation of an algorithm without using the sweep concept or the provided time loop classes.
However, if you use the time loop classes you obtain additional features like the sweep/data selection per block (explained in a later tutorial), automatic time measurement, or time loop control from the GUI.
So let us start with writing our first sweep. A sweep function is a simple function that takes as its only argument an IBlock pointer.
There is still a problem with this function: It can only have a single argument, otherwise we can not register it as a sweep at the time loop since we additionally need the BlockDataID of our field. One possibility would be to have a global variable where the BlockDataID is stored (very bad design, do not do that!), or we could use a construct from the standard library called std::bind(). The last can transform a function pointer of a two-argument function to a function pointer of a one-argument function by keeping the second argument constant: It "binds" the second argument to a fixed value.
So we write the function just as we need it:
and register it at the time loop with the following:
Note the strange syntax to add a sweep to the time loop. The add method returns an object (a SweepAdder) where the sweep is piped in. Moreover, in this object e.g. a BeforeFunction or a AfterFunction can be piped in. If you want to know more about the sweep registration read the documentation of timeloop::SweepTimeloop.
The variant described above using std::bind() may seem a little strange but there is also another solution. Instead of writing a sweep function, we write a functor class overloading the call operator.
With this, the registration code does not need std::bind any more:
Note that if you define a member function completely within the class definition, this function is automatically marked as inline
. Also, for larger classes, declaring all member functions within the class definition is bad style. Normally, one would have only the function declaration within the class definition. The function is then defined outside of the class:
For our small example, however, defining the member function completely within the class definition is fine. Also note that there is only one sweep class but potentially multiple blocks. So members of this class are not block local data!
There is a third sweep concept called "Sweep on Block" where the members of the class are in fact block local data.
This is described in more detail here: Sweeps on Block
The next tutorial covers writing a real algorithm, which also requires communication among blocks: Tutorial - Basics 3: Writing a Simple Cellular Automaton in waLBerla