Namespaces and Multidimensional Code Tutorial

Time to Complete: 15 mins

GOALS:
  • Organize and Simplify Syntax with Namespaces

  • Modify a 3D Code for 2D or 3D Simulation Using Macros and Compiler Directives

In this tutorial we will start from the file main.cpp from the HeatEquation_Simple example and make several modifications to the code. First we will discuss the amrex namespace, and use commands to simplify the syntax of the code. Second, we will cover AMReX macros and compiler directives commonly used to write multidimensional code. We will insert these commands into our example code to enable these features. In the end, we will have a more capable code with cleaner syntax.

Namespaces

AMReX uses namespaces to organize classes, functions, data, types and templates and avoid naming conflicts with other libraries. In the simplified Heat Equation guided tutorial, aspects of the amrex namespace were accessed by using the prefix amrex::. For example, to use the AMReX Real type we wrote,

amrex::Real dt;

Moreover, this pattern was repeated 34 times throughout the example leading to a lot of extra typing! In this case, we can simplify the code by adding the line,

using namespace amrex;

before the sections of code where we want to access the classes, functions, data, types or templates from the amrex namespace. Typically, this is done after the #include lines at the top of the file. Once this is done, we can drop the amrex:: prefix, and write only,

Real dt;

to get the same result.

At this point, we should add the using namespace amrex; line at the top of our file and remove the amrex:: prefix from all the commands that use it.

Note: C++ Standard Library Math Functions

When using math functions such as min or max from the C++ standard library, it is recommend that users prefix these commands with std::, to have std::min or std::max to avoid any conflicts with the amrex namespace.

Note: Best Practices

It is good practice to avoid placing a using namespace command in a header (.H) file.

The rest of this tutorial will assume we implemented using namespace amrex; and removed the amrex:: prefixes.

Writing Multidimensional Code

In this section of the tutorial we focus on using compiler or preprocessor directives and AMReX macros to write code that will run in 1-, 2- or 3-Dimensions, depending on what was chosen at compile time. We continue modifications of the main.cpp file from the Tutorial: Heat Equation - Simple.

What is a compiler directive?

A compiler directive allows a programmer to tell the compiler to take specific actions at compile time. In C++ they are often called preprocessor directives, because they are interpreted before the compilation process. In AMReX they are commonly used in conjunction with macros for writing 2D/3D code.

What is a Macro?

Macros are a form of text substitution. For example,

#define VALUE_OF_PI 3.14

will substitute 3.14 when the string VALUE_OF_PI is found in the code. They can also include more advanced functionality. As we’ll see in the tutorial, AMReX has several helpful macros. They are named in all caps to avoid confusion with normal variables.

In this section, we’ll start from the main.cpp code used in the HeatEquation_Simple example, and modify it for 2D/3D compilation. We’ll begin by adding several macros.

AMREX_D_DECL

The first line we’ll modify is

IntVect dom_lo(0,0,0);

to

IntVect dom_lo(AMREX_D_DECL(0,0,0));

The AMREX_D_DECL macro expands to a comma-separated list of 1, 2, or 3 of the arguments depending on the dimension selected at compile time. To be explicit,

if compiled with DIM=2 or AMReX_SPACEDIM=2 the line above will evaluate to,

IntVect dom_lo (0,0)

if compiled with DIM=3 or AMReX_SPACEDIM=3 it will evaluate to,

IntVect dom_lo (0,0,0)

Next, modify the definitions of dom_hi and real_box to use the AMREX_D_DECL macro in a similar manner.

AMREX_SPACEDIM

When we arrive at the line

Array<int,3> is_periodic{1,1,1};

we encounter a slightly different situation. This time we need to change the dimension of the Array as well as the number of inputs. For this we change the 3 in Array<int,3>, to AMREX_SPACEDIM. The inputs to is_periodic are treated as above, giving:

Array<int,AMREX_SPACEDIM> is_periodic{AMREX_D_DECL(1,1,1)};

The AMREX_SPACEDIM macro in this statement will evaluate to 1, 2 or 3 depending on the dimension selected at compile time. Now, modify the line that defines the GpuArray dx in a similar way.

Preprocessor Directives

While macros address many of the dimensional needs of our code sometimes its necessary to use them in conjunction with preprocessor directives, such as #if, #elif, and #endif, to allow for algorithmic differences for different dimensions. In our code example, this need arises within calls to ParallelFor.

As a first step to writing a multidimensional version of this code, consider what the algorithm looks like in 3D dimensions (This is what we see in the code we’re starting with.):

ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k)
{
    Real x = (i+0.5) * dx[0];
    Real y = (j+0.5) * dx[1];
    Real z = (k+0.5) * dx[2];
    Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/0.01;
    phiOld(i,j,k) = 1. + std::exp(-rsquared);
});

If we wanted a similar initialization in 2D, it would be:

ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k)
{
    Real x = (i+0.5) * dx[0];
    Real y = (j+0.5) * dx[1];
    Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5))/0.01;
    phiOld(i,j,k) = 1. + std::exp(-rsquared);
});

Notice that much of this code is redundant. The declarations of x, y and value assigned to phiOld are all the same. The difference comes with the addition of z and definition of the distance rsquared in 2D and 3D. We can address this by adding preprocessor directives with AMReX macros to create different sections of code for different numbers of dimensions.

Splitting the Code by Dimensional Dependence

Now we will separate the parts of the code by the number of dimensions they exclusively pertain to. Because the x, y and phiOld lines are included in all cases, they will remain outside the preprocessor directives. Then we can separate the code like this,

// included in all cases
Real x = (i+0.5) * dx[0];
Real y = (j+0.5) * dx[1];

// dimensional dependent code

// included in all cases
phiOld(i,j,k) = 1. + std::exp(-rsquared);

Adding the 2-Dimensional Section

To address the 2-dimensional case, we add preprocessor directives #if and #endif with the logic, #if (AMREX_SPACEDIM == 2). This will check if the value of the macro AMREX_SPACEDIM is equal to 2. If true, it will compile the code inside this section. Therefore we write:

// included in all cases
Real x = (i+0.5) * dx[0];
Real y = (j+0.5) * dx[1];

// dimensional dependent code
#if (AMREX_SPACEDIM == 2)
   Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5))/0.01;
#endif

// included in all cases
phiOld(i,j,k) = 1. + std::exp(-rsquared);

Adding the 3-dimensional Section

With the above additions, the code will work if we compile with the option DIM=2 or AMReX_SPACEDIM=2. For three dimensions, we include #elif (AMREX_SPACEDIM == 3) and add the lines for z and the 3D version of rsquared:

Real x = (i+0.5) * dx[0];
Real y = (j+0.5) * dx[1];

#if (AMREX_SPACEDIM == 2)
   Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5))/0.01;

#elif (AMREX_SPACEDIM == 3)
   Real z = (k+0.5) * dx[2];
   Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/0.01;
#endif

phiOld(i,j,k) = 1. + std::exp(-rsquared);

This adds another preprocessor directive which evaluates the statement AMREX_SPACEDIM==3. If true (and AMREX_SPACEDIM==2 false), it will compile this section of code and not the 2-dimensional section.

2D/3D Multidimensional Version

Altogether the 2D/3D multidimensional version of the call to ParallelFor is:

ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k)
{
   Real x = (i+0.5) * dx[0];
   Real y = (j+0.5) * dx[1];

#if (AMREX_SPACEDIM == 2)
   Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5))/0.01;

#elif (AMREX_SPACEDIM == 3)
   Real z = (k+0.5) * dx[2];
   Real rsquared = ((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)+(z-0.5)*(z-0.5))/0.01;
#endif

   phiOld(i,j,k) = 1. + std::exp(-rsquared);
});

The section of code that will be compiled and executed is now determined by the value of DIM or AMReX_SPACEDIM configured at compile time. The final addition to this tutorial is to see we need to add another preprocessor directive to modify the ParallelFor responsible for advancing the data by dt. In this case all we need is a conditional statement to isolate the code that updates the third dimension.

A Note About ParallelFor

The ParallelFor function automatically optimizes code execution for the hardware according to commands given at compile time. However, when writing multidimensional code the syntax of the ParallelFor loop is always written as if compiling for three dimensions. Consider,

ParallelFor(bx, [=] AMREX_GPU_DEVICE(int i, int j, int k){ ...  });

and note that we included the iterative variables i,j,k even though the 2-dimensional code will not use the k variable. The ParallelFor function is aware of the dimension specified at compile time, and automatically makes this adjustment for the convenience of AMReX users.

Conclusion

Congratulations, you should now be able to compile the code for 2- or 3-dimensional simulations. At this point, our modified main.cpp code should look similar to the code used in the Example: HeatEquation_EX1_C. The commands to compile for each number of dimensions with GNU Make and CMake are listed in the table below.

Compile Commands

GNU Make

CMake

2 Dimensions

make DIM=2

cmake .. -DAMReX_SPACEDIM=2

3 Dimensions

make DIM=3

cmake .. -DAMReX_SPACEDIM=3

Please be aware that the plotfiles generated in each version of the code will have different requirements due to the difference in dimensions. For example, a plotfile from the 3D code will need, amrvis3d while the 2D code will need amrvis2d.