Afivo  0.3
Multigrid tutorial

Table of Contents

Introduction

In this tutorial we present two Poisson test problems, which demonstrate the multigrid behavior on a partially refined mesh. The examples are available in 2D and in 3D, see poisson_basic_2d.f90 and poisson_basic_3d.f90

Problem description

We use the method of manufactured solutions: from an analytic solution the right-hand side and boundary conditions are computed. The equation solved here is Poisson's equation with constant coefficients:

\[ \nabla^2 u = \nabla \cdot (\nabla u) = \rho , \]

in a rectangular domain of \([0,1]^D\), where \(D\) is the problem dimension.

We pick the following solution \(u\)

\[ u(r) = \exp(|{\vec{r}-\vec{r}_1}|/\sigma) + \exp(|{\vec{r}-\vec{r}_2}|/\sigma), \]

where \(\vec{r_1} = (0.25, 0.25)\), \(\vec{r_2} = (0.75, 0.75)\) (2D) and \(\vec{r_1} = (0.25, 0.25, 0.25)\), \(\vec{r_2} = (0.75, 0.75, 0.75)\) (3D) and \(\sigma = 0.04\). An analytic expression for the right-hand side \(\rho\) is obtained by plugging the solution into the original equation.

Explanation of example

In the code, the solution is stored used the module m_gaussians. This module also contains the type m_gaussians::gauss_t and the subroutine m_gaussians::gauss_init() to initialize it as follows:

! The manufactured solution exists of Gaussians
! Amplitudes: [1.0_dp, 1.0_dp]
! Sigmas : [0.04_dp, 0.04_dp]
! Locations : x, y, z = 0.25 or x, y, z = 0.75
call gauss_init(gs, [1.0_dp, 1.0_dp], [0.04_dp, 0.04_dp], &
reshape([0.25_dp, 0.25_dp, &
0.75_dp, 0.75_dp], [2,2]))

The variable tree contains the full AMR mesh. It is of type a2_t, and a call to m_a2_core::a2_init() takes care of the initialization:

! Initialize tree
call a2_init(tree, & ! Tree to initialize
box_size, & ! A box contains box_size**DIM cells
n_var_cell, & ! Number of cell-centered variables
0, & ! Number of face-centered variables
dr, & ! Distance between cells on base level
coarsen_to=2, & ! Add coarsened levels for multigrid
cc_names=["phi", "rhs", "err", "tmp", "Dx ", "eDx"]) ! Variable names

We define box_size = 8, which means that each box has \( 8 \times 8\) cells, and dr = 0.125 (so that the domain length is one). The variable n_var_cell, the number of cell centered values, is set to 4 (reserved for i_phi, i_rhs, i_err and i_tmp, see below), whereas the variable n_var_face, the number of face centered values, is zero.

In Afivo the coarsest mesh, which covers the full computational domain, is not supposed to change. To create this mesh there is a routine a2_set_base(), which takes as input the spatial indices of the coarse boxes and their neighbors. Below, a 2D example is shown for creating a single coarse box at index \((1,1)\). Physical (non-periodic) boundaries are indicated by a negative index for the neighbor. By adjusting the neighbors one can specify different geometries, the possibilities include meshes that contain a hole, or meshes that consist of two isolated parts. The treatment of boundary conditions is discussed in Filling ghost cells

! Set up geometry. These indices are used to define the coordinates of a box,
! by default the box at [1,1] touches the origin (x,y) = (0,0)
ix_list(:, 1) = [1, 1] ! Set index of box 1
! Create the base mesh, using the box indices and their neighbor information
call a2_set_base(tree, 1, ix_list)

The routine m_a2_types::a2_print_info shows some values of the tree. At this position in the example, it lists:

*** a2_print_info(tree) ***
Current maximum level:           1
Maximum allowed level:          30
Number of boxes used:            3
Memory limit(boxes):       4793490
Memory limit(GByte):      0.16E+02
Highest id in box list:          3
Size of box list:             1000
Box size (cells):                8
Number of cc variables:          4
Number of fc variables:          0
Type of coordinates:             0
min. coords:          0.0000E+00  0.0000E+00
dx at min/max level   0.1250E+00  0.1250E+00

Note that the number of boxes is 3, although the maximum multigrid level is just 1. This arises from the fact that for multigrid cycling also a level 0 of box size \( 4 \times 4 \), and a level -1 have been defined of size \( 2 \times 2 \). The memory limit has been set on 16 GB (default value) which results in at most 4793490 boxes, based on the box size and the number of ghost cells in each direction (i.e., 2) and the numbers of cell and face centered variables. The length of the box list is 1000, but if more boxes are required, the length will be extended and boxes which are not longer used will be cleaned up. Since box_size = 8 this implies that each box has \( 8 \times 8\) cells, excluding ghost cells, and, since the problem is solved on a rectangular domain of \([0,1] \times [0,1]\), (min. coords: 0.0000E+00 0.0000E+00, the cell size dx at the coarsest level is dr = 1 / 8.

Next, we start refining the mesh by means of subroutine refine_routine which has been added to the example poisson_basic_2d.f90

! Return the refinement flags for box
subroutine refine_routine(box, cell_flags)
type(box2_t), intent(in) :: box
integer, intent(out) :: cell_flags(box%n_cell, box%n_cell)
integer :: i, j, nc
real(dp) :: rr(2), dr2, drhs
nc = box%n_cell
dr2 = box%dr**2
do j = 1, nc; do i = 1, nc
rr = a2_r_cc(box, [i, j])
! This is an estimate of the truncation error in the right-hand side,
! which is related to the fourth derivative of the solution.
drhs = dr2 * box%cc(i, j, i_rhs)
if (abs(drhs) > 1e-3_dp .and. box%lvl < 5) then
cell_flags(i, j) = af_do_ref
else
cell_flags(i, j) = af_keep_ref
end if
end do; end do
end subroutine refine_routine

Here variable id denotes the index number of the box. m_a2_types::a2_r_cc is one of those useful features from module m_a2_types to calculate the center of a cell. The refinement is based on the fourth derivative of the solution. This example only shows when a box should be refined or not, the derefinement does not play a role here. Besides, value af_do_ref, indicating you want to refine a box, the value af_keep_ref, indicating you want to keep a box's refinement, and af_rm_ref, indicating you want to derefine a box, are available. The routine m_a2_core::a2_adjust_refinement ensures the 2:1 balance is ensured, so that there is never a jump of more than one refinement level between neighboring boxes. These constraints are automatically handled, so that the user-defined refinement function does not need to impose them.

When boxes are added or removed in the refinement procedure, their connectivity is automatically updated. References to a removed box are removed from its parent and neighbors. When a new box is added, its neighbors are found through its parent. Three scenarios can occur: the neighbor can be one of the other children of the parent, the neighbor can be a child from the neighbor of the parent, or the neighbor does not exist. In the latter case, there is a refinement boundary, which is indicated by the special value m_afivo_types::af_no_box.

In the next loop, first the routine m_a2_utils::a2_loop_box is called for each box, with as argument the user-defined routine

! This routine sets the initial conditions for each box
subroutine set_initial_condition(box)
type(box2_t), intent(inout) :: box
integer :: i, j, nc
real(dp) :: rr(2), grad(2)
nc = box%n_cell
do j = 1, nc; do i = 1, nc
! Get the coordinate of the cell center at i, j
rr = a2_r_cc(box, [i, j])
! And set the rhs values
box%cc(i, j, i_rhs) = gauss_laplacian(gs, rr)
call gauss_gradient(gs, rr, grad)
box%cc(i, j, i_gradx) = grad(1)
end do; end do
end subroutine set_initial_condition

which calls gauss_laplacian from m_gaussian corresponding the problem described above. The routine a2_r_cc computes the cell center of the cells in the box. In this example, each box has 4 cell centered matrices. Here boxcc(:,:,i_rhs) is initialized with the right hand side values.

The following do loop all right hand side field of the boxed used are initialized and boxes are refined in accordance with the refinement routine to obtain an adaptive mesh.

do
! For each box, set the initial conditions
call a2_loop_box(tree, set_initial_condition)
! This updates the refinement of the tree, by at most one level per call.
! The second argument is a subroutine that is called for each box that can
! be refined or derefined, and it should set refinement flags. Information
! about the changes in refinement are returned in the third argument.
call a2_adjust_refinement(tree, refine_routine, refine_info)
! If no new boxes have been added, exit the loop
if (refine_info%n_add == 0) exit
end do

If no further refinement is required, i.e., refine_info\n_add == 0, the do loop stops.

The routine m_a2_types::a2_print_info shows after the refinement loop:

 *** a2_print_info(tree) ***
 Current maximum level:          10
 Maximum allowed level:          30
 Number of boxes used:         5487
 Memory limit(boxes):       4793490
 Memory limit(GByte):      0.16E+02
 Highest id in box list:       5487
 Size of box list:             8030
 Box size (cells):                8
 Number of cc variables:          4
 Number of fc variables:          0
 Type of coordinates:             0
 min. coords:          0.0000E+00  0.0000E+00
 dx at min/max level   0.1250E+00  0.2441E-03

So ten levels of refinement are required leading to 5487 boxes and a cell size of the highest level of \(1 / 4096\).

Viewing results

By means of a call to a2_write_vtk from module m_a2_output a vtu file can be produced to show the adapted mesh, but also the solution.

! This writes a Silo output file containing the cell-centered values of the
! leaves of the tree (the boxes not covered by refinement).
write(fname, "(A,I0)") "poisson_basic_2d_", mg_iter
call a2_write_silo(tree, trim(fname), dir="output")

The resulting file "poisson_basic_2d_0.vtu" in directory output can be viewed by Visit.

a b
poisson_basic_2d_rhs.png
a
poisson_basic_2d_0.png
b

Figure 1. a) initialization of the right hand side b) adapted mesh

Now the grid has been constructed to match the RHS function and in accordance with the refinement function refine_routine showed above, we continue to solve the Poisson problem using multigrid. Therefore we first initialize the multigrid options. The routine m_a2_multigrid::mg2_init performs some basic checks and sets default values where necessary in variable mg of type m_a2_multigrid::mg2_t.

To this end, we perform a number of multi-grid iterations. Within each loop we call routine m_a2_multigrid::mg2_fas_fmg, performing a full approximation scheme using full multigrid. At the first call of mg2_fas_fmg sets the initial guess for phi and restricts the right hand side fields rhs from the highest level down to the coarsest level. After the first call of mg2_fas_fmg it sets the right hand side field on coarser grids and restricts the field with the holding solution of phi. If required the ghost cells are filled.

Next the user-defined routine set_error is called to computed the error compared to the analytic solution.

! Set the error compared to the analytic solution
subroutine set_error(box)
type(box2_t), intent(inout) :: box
integer :: i, j, nc
real(dp) :: rr(2), dx, gradx
nc = box%n_cell
dx = box%dr
do j = 1, nc; do i = 1, nc
rr = a2_r_cc(box, [i, j])
box%cc(i, j, i_err) = box%cc(i, j, i_phi) - gauss_value(gs, rr)
gradx = 0.5_dp * (box%cc(i+1, j, i_phi) - &
box%cc(i-1, j, i_phi)) / dx
box%cc(i, j, i_egradx) = gradx - box%cc(i, j, i_gradx)
end do; end do
end subroutine set_error

The results of the first 10 iterations steps are listed below:

Multigrid iteration | max residual | max error
      1                1.00579E+01   5.13811E-05
      2                4.54153E-01   5.13903E-05
      3                2.26078E-02   5.13902E-05
      4                1.14302E-03   5.13902E-05
      5                5.79126E-05   5.13902E-05
      6                2.94218E-06   5.13902E-05
      7                1.50202E-07   5.13902E-05
      8                2.38767E-08   5.13902E-05
      9                2.36430E-08   5.13902E-05
     10                2.01674E-08   5.13902E-05

Note that the residual is still decreasing, whereas the maximum error reaches its minimum after about three iterations.

After 10 multigrid iterations we obtain the resulting file "poisson_basic_2d_10.vtu" in directory output, which can be viewed by e.g., visit.

Solution \( \phi \) Error \( \epsilon \) after 10 multigrid iteration steps
poisson_basic_2d_10_phi.png
a
poisson_basic_2d_10_err.png
b

Parallel results

The info below is coming from the corresponding 3d problem for boxes of size \(8 \times 8 \times 8\), excluding ghost cells.

*** a3_print_info(tree) ***
Current maximum level:           9
Maximum allowed level:          30
Number of boxes used:         8811
Memory limit(boxes):        525314
Memory limit(GByte):      0.16E+02
Highest id in box list:       8811
Size of box list:            12502
Box size (cells):                8
Number of cc variables:          4
Number of fc variables:          0
Type of coordinates:             0
min. coords:          0.0000E+00  0.0000E+00  0.0000E+00
dx at min/max level   0.1250E+00  0.4883E-03

On the coarsest level the variable \(dx = 1 / 8\), the size of a cell, whereas on the highest level \(dx = 1 / 2048\). We have compared, for this 3D case, the wall clock time, obtained by a calculation with just one thread and with multiple threads. This shows that the use of OpenMP is very succesful. We distinguish the wall clock time for both the creation of the adaptive mesh refinement part of the program and the multigrid process. For the adaptive mesh refinement part, we obtain a factor of 5.3, comparing the wall clock time for 16 and just one thread. For the multigrid process we achieve a speedup factor of nearly a factor 9.

WCT_basic_3d_lisa_new.png

Full program listing

program poisson_basic_2d
implicit none
integer, parameter :: box_size = 8
integer, parameter :: n_boxes_base = 1
integer, parameter :: n_iterations = 10
integer, parameter :: n_var_cell = 6
integer, parameter :: i_phi = 1
integer, parameter :: i_rhs = 2
integer, parameter :: i_err = 3
integer, parameter :: i_tmp = 4
integer, parameter :: i_gradx = 5
integer, parameter :: i_egradx = 6
type(a2_t) :: tree
type(ref_info_t) :: refine_info
integer :: mg_iter
integer :: ix_list(2, n_boxes_base)
real(dp) :: dr, residu(2), anal_err(2)
character(len=100) :: fname
type(mg2_t) :: mg
type(gauss_t) :: gs
integer :: count_rate,t_start,t_end
print *, "Running poisson_basic_2d"
print *, "Number of threads", af_get_max_threads()
! The manufactured solution exists of Gaussians
! Amplitudes: [1.0_dp, 1.0_dp]
! Sigmas : [0.04_dp, 0.04_dp]
! Locations : x, y, z = 0.25 or x, y, z = 0.75
call gauss_init(gs, [1.0_dp, 1.0_dp], [0.04_dp, 0.04_dp], &
reshape([0.25_dp, 0.25_dp, &
0.75_dp, 0.75_dp], [2,2]))
! The cell spacing at the coarsest grid level
dr = 1.0_dp / box_size
! Initialize tree
call a2_init(tree, & ! Tree to initialize
box_size, & ! A box contains box_size**DIM cells
n_var_cell, & ! Number of cell-centered variables
0, & ! Number of face-centered variables
dr, & ! Distance between cells on base level
coarsen_to=2, & ! Add coarsened levels for multigrid
cc_names=["phi", "rhs", "err", "tmp", "Dx ", "eDx"]) ! Variable names
! Set up geometry. These indices are used to define the coordinates of a box,
! by default the box at [1,1] touches the origin (x,y) = (0,0)
ix_list(:, 1) = [1, 1] ! Set index of box 1
! Create the base mesh, using the box indices and their neighbor information
call a2_set_base(tree, 1, ix_list)
call system_clock(t_start, count_rate)
do
! For each box, set the initial conditions
call a2_loop_box(tree, set_initial_condition)
! This updates the refinement of the tree, by at most one level per call.
! The second argument is a subroutine that is called for each box that can
! be refined or derefined, and it should set refinement flags. Information
! about the changes in refinement are returned in the third argument.
call a2_adjust_refinement(tree, refine_routine, refine_info)
! If no new boxes have been added, exit the loop
if (refine_info%n_add == 0) exit
end do
call system_clock(t_end, count_rate)
write(*,"(A,Es10.3,A)") " Wall-clock time generating AMR grid: ", &
(t_end-t_start) / real(count_rate,dp), " seconds"
call a2_print_info(tree)
! Initialize the multigrid options. This performs some basics checks and sets
! default values where necessary.
mg%i_phi = i_phi ! Solution variable
mg%i_rhs = i_rhs ! Right-hand side variable
mg%i_tmp = i_tmp ! Variable for temporary space
mg%sides_bc => sides_bc ! Method for boundary conditions
! This routine does not initialize the multigrid fields boxes%i_phi,
! boxes%i_rhs and boxes%i_tmp. These fileds will be initialized at the
! first call of mg2_fas_fmg
call mg2_init_mg(mg)
print *, "Multigrid iteration | max residual | max error"
call system_clock(t_start, count_rate)
do mg_iter = 1, n_iterations
! Perform a FAS-FMG cycle (full approximation scheme, full multigrid). The
! third argument controls whether the residual is stored in i_tmp. The
! fourth argument controls whether to improve the current solution.
call mg2_fas_fmg(tree, mg, set_residual=.true., have_guess=(mg_iter>1))
! Compute the error compared to the analytic solution
call a2_loop_box(tree, set_error)
! Determine the minimum and maximum residual and error
call a2_tree_min_cc(tree, i_tmp, residu(1))
call a2_tree_max_cc(tree, i_tmp, residu(2))
call a2_tree_min_cc(tree, i_err, anal_err(1))
call a2_tree_max_cc(tree, i_err, anal_err(2))
write(*,"(I8,13x,2(Es14.5))") mg_iter, maxval(abs(residu)), &
maxval(abs(anal_err))
! This writes a Silo output file containing the cell-centered values of the
! leaves of the tree (the boxes not covered by refinement).
write(fname, "(A,I0)") "poisson_basic_2d_", mg_iter
call a2_write_silo(tree, trim(fname), dir="output")
end do
call system_clock(t_end, count_rate)
write(*, "(A,I0,A,E10.3,A)") &
" Wall-clock time after ", n_iterations, &
" iterations: ", (t_end-t_start) / real(count_rate, dp), &
" seconds"
! This call is not really necessary here, but cleaning up the data in a tree
! is important if your program continues with other tasks.
call a2_destroy(tree)
contains
! Return the refinement flags for box
subroutine refine_routine(box, cell_flags)
type(box2_t), intent(in) :: box
integer, intent(out) :: cell_flags(box%n_cell, box%n_cell)
integer :: i, j, nc
real(dp) :: rr(2), dr2, drhs
nc = box%n_cell
dr2 = box%dr**2
do j = 1, nc; do i = 1, nc
rr = a2_r_cc(box, [i, j])
! This is an estimate of the truncation error in the right-hand side,
! which is related to the fourth derivative of the solution.
drhs = dr2 * box%cc(i, j, i_rhs)
if (abs(drhs) > 1e-3_dp .and. box%lvl < 5) then
cell_flags(i, j) = af_do_ref
else
cell_flags(i, j) = af_keep_ref
end if
end do; end do
end subroutine refine_routine
! This routine sets the initial conditions for each box
subroutine set_initial_condition(box)
type(box2_t), intent(inout) :: box
integer :: i, j, nc
real(dp) :: rr(2), grad(2)
nc = box%n_cell
do j = 1, nc; do i = 1, nc
! Get the coordinate of the cell center at i, j
rr = a2_r_cc(box, [i, j])
! And set the rhs values
box%cc(i, j, i_rhs) = gauss_laplacian(gs, rr)
call gauss_gradient(gs, rr, grad)
box%cc(i, j, i_gradx) = grad(1)
end do; end do
end subroutine set_initial_condition
! Set the error compared to the analytic solution
subroutine set_error(box)
type(box2_t), intent(inout) :: box
integer :: i, j, nc
real(dp) :: rr(2), dx, gradx
nc = box%n_cell
dx = box%dr
do j = 1, nc; do i = 1, nc
rr = a2_r_cc(box, [i, j])
box%cc(i, j, i_err) = box%cc(i, j, i_phi) - gauss_value(gs, rr)
gradx = 0.5_dp * (box%cc(i+1, j, i_phi) - &
box%cc(i-1, j, i_phi)) / dx
box%cc(i, j, i_egradx) = gradx - box%cc(i, j, i_gradx)
end do; end do
end subroutine set_error
! This routine sets boundary conditions for a box, by filling its ghost cells
! with approriate values.
subroutine sides_bc(box, nb, iv, bc_type)
type(box2_t), intent(inout) :: box
integer, intent(in) :: nb ! Direction for the boundary condition
integer, intent(in) :: iv ! Index of variable
integer, intent(out) :: bc_type ! Type of boundary condition
real(dp) :: rr(2)
integer :: n, nc
nc = box%n_cell
! We use dirichlet boundary conditions
bc_type = af_bc_dirichlet
! Below the solution is specified in the approriate ghost cells
select case (nb)
case (a2_neighb_lowx) ! Lower-x direction
do n = 1, nc
rr = a2_rr_cc(box, [0.5_dp, real(n, dp)])
box%cc(0, n, iv) = gauss_value(gs, rr)
end do
case (a2_neighb_highx) ! Higher-x direction
do n = 1, nc
rr = a2_rr_cc(box, [nc+0.5_dp, real(n, dp)])
box%cc(nc+1, n, iv) = gauss_value(gs, rr)
end do
case (a2_neighb_lowy) ! Lower-y direction
do n = 1, nc
rr = a2_rr_cc(box, [real(n, dp), 0.5_dp])
box%cc(n, 0, iv) = gauss_value(gs, rr)
end do
case (a2_neighb_highy) ! Higher-y direction
do n = 1, nc
rr = a2_rr_cc(box, [real(n, dp), nc+0.5_dp])
box%cc(n, nc+1, iv) = gauss_value(gs, rr)
end do
end select
end subroutine sides_bc
end program poisson_basic_2d