Tutorial: Heat Equation - Simple

Time to Complete: 25 mins

  • Compile an AMReX Code

  • Introduce Basic AMReX Elements

  • Generate and Visualize Output

In this tutorial we take the steps needed to go from source download to visualized output of an AMReX code. We will demonstrate basic building, compiling and output generation. We will also examine several key AMReX features in a C++ code and plot the output with Python in a Jupyter notebook.

Setting Up Your Environment

This tutorial recommends using Gitpod (Requires a GitHub account). Gitpod provides an online terminal that is already preconfigured for our development environment.

Click here to be taken to the Gitpod workspace.

Building the Project

This example will use CMake to build the project. Navigate to the directory /workspace/amrex-tutorials/GuidedTutorials/HeatEquation_Simple/ and type

mkdir Build
cd Build
cmake ..

This will create a build folder and run CMake to setup the build configuration. During this process CMake will read the CmakeLists.txt file in the subdirectory to generate and download, if necessary, the the build files it needs to compile the tutorial in the next step.

More information about building options, such as disabling MPI, can be found at https://amrex-codes.github.io/amrex/docs_html/BuildingAMReX_Chapter.html.

Compiling the Code with CMake

After building the project, in the Build directory you will find several new files and directories created by CMake during the configuration process. At the prompt type

cmake --build . -j2

CMake will then compile the code and dependencies. The -j2 flag tells CMake to use 2 processes to speed up compilation. The first time you call cmake --build . you should see a list of all the source AMReX files being compiled:


When CMake finishes you will be left with an executable named HeatEquation_Simple. To run the code type:

./HeatEquation_Simple inputs

This command will run the HeatEquation_Simple code with the inputs file as the input parameters. Parsing of the information in the inputs file is done by ParmParse. More details can be found at https://amrex-codes.github.io/amrex/docs_html/Basics.html#parmparse

Code Highlights

At this point we have built, compiled and ran the HeatEquation_Simple code. Now we will walk through the code and explain some essential features of AMReX syntax.

Basic Structure

 ├──── Initialize AMReX
 ├──── Declare Simulation Parameters
 ├──── Read Parameter Values From Input File
 ├──── Define Simulation Setup & Geometry
 ├──── Initialize Data Loop
 │     └──── Set Values For Each Cell
 ├──── Write Initial Plotfile
 ├──── Main Time Evolution Loop
 │     ├──── Evolve Values For Each Cell
 │     ├──── Increment
 │     └──── Write Plotfile At Given Interval
 └──── Finalize AMReX

AMReX Namespace and Required Commands

The AMReX namespace contains many useful features. They are accessed by including the necessary header files and using the prefix amrex::. Each int main(...) using AMReX should begin with amrex::Initialize() immediately followed by { and end with } immediately followed by amrex::Finalize(). Together these commands are responsible for initializing the AMReX execution environment and proper release of resources. AMReX classes and features not located between the commands will not function properly.

Other useful features include amrex::Print() which was written to handle print output during parallel execution.

The MultiFab Data Structure

A MultiFab is a data structure that AMReX can distribute among parallel processes. In this Heat Equation example we use two MultiFabs to hold the current and previous values of \(\phi\) as defined here.

The declaration of the first MultiFab for the previous values of \(\phi\) is:

amrex::MultiFab phi_old(ba, dm, Ncomp, Nghost);

Here ba is a BoxArray that stores a collection of boxes on a single level of mesh refinement. dm is a DistributionMapping that describes how to distribute processing across multiple CPUs and threads. Ncomp is the number of values stored for each cell of the mesh; in this case, 1 for the scalar \(\phi\). The value for Nghost tells AMReX how many ghost cells to create outside the box’s valid region.

MFIter and ParallelFor

Now we will examine the main time evolution loop. In this section AMReX’s MFIter and ParallelFor constructs work in conjunction to provide efficient parallel execution. The code where this happens is:

for (int step = 1; step <= nsteps; ++step){


     for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi ){

         const amrex::Box& bx = mfi.validbox();

         const amrex::Array4<amrex::Real>& phiOld = phi_old.array(mfi);
         const amrex::Array4<amrex::Real>& phiNew = phi_new.array(mfi);

         amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k){

             phiNew(i,j,k) = phiOld(i,j,k) + dt *
                 ( (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0])
                  +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1])
                  +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]) );

         }); // end ParallelFor

     time = time + dt;
     amrex::MultiFab::Copy(phi_old, phi_new, 0, 0, 1, 0);
     amrex::Print() << "Advanced step " << step << "\n";

     if (plot_int > 0 && step%plot_int == 0){
         const std::string& pltfile = amrex::Concatenate("plt",step,5);
         WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step);

First note the outer for loop that counts the time step in our simulation. At each step we begin by calling phi_old.FillBoundary(geom.periodicity()). This fills ghost cells based on the previous state of \(\phi\) with periodic boundary conditions.


The next for loop,

for ( amrex::MFIter mfi(phi_old); mfi.isValid(); ++mfi )

uses the data object MFIter to separate the mesh across processes for individual operations. Within this loop the active piece of the mesh is defined by mfi.validbox() and is accessed via bx on the line,

const amrex::Box& bx = mfi.validbox();

In the next lines, the part of MultiFab data that pertains to the current active piece of the mesh is converted to an Array4 data type for i,j,k access:

const amrex::Array4<amrex::Real>& phiOld = phi_old.array(mfi);
const amrex::Array4<amrex::Real>& phiNew = phi_new.array(mfi);


ParallelFor provides parallel execution of i,j,k operations that would otherwise require three nested loops. This AMReX construct automatically adapts for efficient computation based on the available hardware, including CPU and CPU+GPU variations. In this example, it is here we compute the forward Euler step (see Heat Eqn) with the code:

amrex::ParallelFor(bx, [=] AMREX_GPU_DEVICE (int i, int j, int k){

phiNew(i,j,k) = phiOld(i,j,k) + dt *
   ( (phiOld(i+1,j,k) - 2.*phiOld(i,j,k) + phiOld(i-1,j,k)) / (dx[0]*dx[0])
   +(phiOld(i,j+1,k) - 2.*phiOld(i,j,k) + phiOld(i,j-1,k)) / (dx[1]*dx[1])
   +(phiOld(i,j,k+1) - 2.*phiOld(i,j,k) + phiOld(i,j,k-1)) / (dx[2]*dx[2]) );

}); // end ParallelFor

The rest of the code in the main time evolution loop updates the time and MultiFab data, prints a status update to terminal, and writes output to a plot file that will be used for visualization.

Visualizing Output

Data Files

In main.cpp we called a plot function in two places. The first time was to plot initial data.

129     if (plot_int > 0)
130     {
131         int step = 0;
132         const std::string& pltfile = amrex::Concatenate("plt",step,5);
133         WriteSingleLevelPlotfile(pltfile, phi_old, {"phi"}, geom, time, 0);
134     }

The second time plots were generated at given intervals during the main time progression loop.

171         if (plot_int > 0 && step%plot_int == 0)
172         {
173             const std::string& pltfile = amrex::Concatenate("plt",step,5);
174             WriteSingleLevelPlotfile(pltfile, phi_new, {"phi"}, geom, time, step);
175         }

Each time we run the code it will create a series of directories which contain data for visualization. Now run HeatEquation_Simple with the inputs file. After it finishes your directory should look like this.


Visualization in Jupyter

We will use Python and the yt package in a Jupyter notebook to generate plots for the data in the directories created in the previous step. First launch the Jupyter notebook with the command:

jupyter notebook

When Jupyter starts, it will generate a token at the command line and ask for a password in the window it opened. Copy the token to enter to the notebook.


Once the notebook starts, find Visualization.ipynb and open it. In this file there are additional notes about the heat equation example, followed by several cells that use yt commands to read AMReX output files.


The following commands import the yt package and plot a 2D slice of the output at from the 1000th time step.

import yt
from yt.frontends.boxlib.data_structures import AMReXDataset
ds = AMReXDataset("plt01000")
sl = yt.SlicePlot(ds, 2, ('boxlib', 'phi'))

In our example, the commands are already written in the notebook. To run them, select from the menu: Kernel -> Restart & Run All. Once the run is complete, you will get the following plot.


What’s Next?

The code in this example was simplified down to a single file. Other convenient features that require more complex syntax were removed for the sake of a straight-forward presentation. In the next example we’ll put these pieces back and write code like an AMReX developer.