Version 6 (modified by 14 years ago) ( diff ) | ,
---|
AMR Explained
At first glance the AMR routine in amr_control.f90 is a little intimidating, but it can be understood by examining the most basic features first.
Please note that this is an explanation of AstroBEAR's AMR implementation, not adaptive mesh refinement itself. Readers who are unfamiliar with AMR might wish to look over some the AMR resources we have on the How AstroBEAR Works page.
Round 1: A Simple, Single-Processor AMR Algorithm
First we'll start with a simplified version of the AMR algorithm. This version focuses only on the most essential features:
RECURSIVE SUBROUTINE AMR(n) INTEGER :: n, nSteps, step nSteps = 2 CALL InitInfos(n) CALL ProlongateParentsData(n) DO step=1,nSteps levels(n)%step=step IF (step == 2) CALL UpdateOverlaps(n) CALL ApplyOverlaps(n,step) CALL ApplyPhysicalBCs(n) CALL SetErrFlags(n) IF (step == 2) CALL AgeNodesChildren(n) CALL BackupNodes(n+1) CALL CreateChildrens(n) IF (step == 1) THEN CALL InheritOldNodeOverlapsChildren(n) CALL InheritNewNodeOverlapsChildren(n) ELSE CALL InheritOverlapsOldChildren(n) CALL InheritOverlapsNewChildren(n) END IF CALL InheritNeighborsChildren(n) CALL AdvanceGrids(n) CALL AMR(n+1) CALL ApplyChildrenData(n) CALL SyncFluxes(n) CALL AccumulateFluxes(n) IF (step == 2) CALL NullifyNeighbors(n) END DO CALL CoarsenDataForParents(n) END SUBROUTINE AMR
In this example, the parameter n
represents the current level of the operation.
Remember as we step through the AMR()
subroutine that it is recursive. At each step on level n
, AMR(n)
within the DO nSteps
loop). calls itself on the next level up (see the calls to AMR(n+1)
during each step on level n
. This is because for each step on level n >= 0
, there are two steps on level n+1
, and the levels above the base level are regridded at each step.
IMPORTANT: Because AMR()
is recursive, each call to AMR()
at level n
assumes that certain steps were carried out by the call at level -1
. This can make the structure of AMR()
a little confusing, especially once parallelization is included.
AMR(n)
calls two subroutines before stepping through the simulation to finish initializing level n
's data:
InitInfos(n)
— Allocates grid data (InfoDef
) structures for the grids on leveln
. Note that the tree structure and grid dimensions were created on the previous leveln-1
;InitInfos()
just creates the data structures they reference.
ProlongateParentsData(n)
— Populates the leveln
data structures with prolongated data from their parents on leveln-1
.
Once the data has been constructed, we can begin the work of advancing the simulation. Each level n
will take two steps for every single step taken by n-1
, but the process is slightly different on each step for levels above the base level.
Step 1
ApplyOverlaps(n, step)
— After initializing leveln
with prolongated data from leveln-1
,AMR()
copies over data from the previous generation of grids on leveln
. This higher-resolution data is preferable to data prolongated from the previous level, soAMR()
uses it wherever it is available.ApplyPhysicalBCs(n)
— Apply physical boundary conditions to leveln
.SetErrFlags(n)
— Determine which regions to refine. Refinement regions are determined by the physical processes involved, as well as specific conditions imposed by the problem modules.BackupNodes(n+1)
— Caches the nodes on the child leveln+1
. We are about to create the new leveln+1
nodes, and the data referenced by the backed-up nodes will be used whenApplyOverlaps(n+1)
is called (see above).CreateChildrens(n)
— This routine creates child nodes on leveln+1
using the refinement flags set on leveln
. The use of "Childrens" here is not a typo;CreateChildrens()
is so named because it applies theCreateChildren(Info)
subroutine to each grid on leveln
.InheritOldNodeOverlapsChildren(n)
— Nested grids mean that spatial relationships (overlaps and neighbors) are inherited from parent grids. This routine passes information about the previous generation ofn+1
grids to the new grids created byCreateChildrens(n)
. This routine is only executed on step 1 of theAMR()
execution loop.InheritNewNodeOverlapsChildren(n)
— The children of previous leveln
grids will also need to send their data to the children of new leveln
grids.InheritNeighborsChildren(n)
— The children of neighboring grids on leveln
will likely be neighbors on leveln+1
. This routine passes neighbor information from leveln
to leveln+1
.AdvanceGrids(n)
— Performs the hyperbolic advance step on the grids of leveln
. This is where our numerical solvers come in.AMR(n+1)
— Launches AMR routine on the child levelApplyChildrenData(n)
— The inverse ofProlongateParentData()
, this routine restricts data from the child grids onto their parent grids, providing a more accurate solution on the coarser level.SyncFluxes(n)
— To enforce mass conservation and the DivB constraint, the fluxes at grid boundaries need to be synchronized.AccumulateFluxes(n)
— Accumulates the fluxes used on leveln
to send back to the parent grids on leveln-1
.
Step 2
UpdateOverlaps(n)
— On the second step we don't need to receive overlap data from the previous generation of grids. We do, however, need to 'ghost' data with our current overlap grids or neighbors. So we treat our neighbors as our current overlaps; in effect, we use the same node list for neighbor operations and overlap operations. This is the reason why we use theNullifyNeighbors()
routine later on instead of deleting the node list.ApplyOverlaps(n, step)
- Brings over ghost data from the neighbor grids, which are the current overlaps. This neighbor grid data has been advanced by theAdvanceGrids()
subroutine, and thus is more accurate than the extrapolated data within the grid's ghost zones.ApplyPhysicalBCs(n)
— Apply physical boundary conditions to leveln
.SetErrFlags(n)
— Determine which regions to refine. Refinement regions are determined by the physical processes involved, as well as specific conditions imposed by the problem modules.AgeNodesChildren(n)
- Because of the nested grids giving us inherited relationships, we need to backup the relationships connecting us to the previous child grids on leveln+1
, as well as backing up the nodes themselves.BackupNodes(n+1)
— Caches the nodes on the child leveln+1
. We are about to create the new leveln+1
nodes, and the data referenced by the backed-up nodes will be used whenApplyOverlaps(n+1)
is called (see above).CreateChildrens(n)
— This routine creates child nodes on leveln+1
using the refinement flags set on leveln
. The use of "Childrens" here is not a typo;CreateChildrens()
is so named because it applies theCreateChildren(Info)
subroutine to each grid on leveln
.InheritOverlapsOldChildren(n)
— Nested grids mean that spatial relationships (overlaps/neighbors) are inherited from parent grids. On the second step the previous generation of leveln+1
grids are the old children of the current generation of leveln
grids.InheritOverlapsNewChildren(n)
— This inherits the relationships going the other way. The old children of level n grids will need to send their data to the new children of level n grids.InheritNeighborsChildren(n)
— The children of neighboring grids on leveln
will likely be neighbors on leveln+1
. This routine passes neighbor information from leveln
to leveln+1
.AdvanceGrids(n)
— Performs the hyperbolic advance step on the grids of leveln
. This is where our numerical solvers come in.AMR(n+1)
— Launches AMR routine on the child levelApplyChildrenData(n)
— The inverse ofProlongateParentData()
, this routine restricts data from the child grids onto their parent grids, providing a more accurate solution on the coarser level.SyncFluxes(n)
— To enforce mass conservation and the DivB constraint, the fluxes at grid boundaries need to be synchronized.AccumulateFluxes(n)
— Accumulates the fluxes used on leveln
to send back to the parent grids on leveln-1
.NullifyNeighbors(n)
— On the second step, each node's neighbor list and overlap list is pointing to the same list (the node's neighbor list). This routine nullifies the neighbor list pointers without destroying the nodes they point to. In effect, this turns the current generation's neighbor lists into the next generation's overlap lists. On the next step, theBackUpNodes()
routine will destroy the overlap nodes.CoarsenDataForParent(n)
— Now that both advance steps are complete for this level, it's time to coarsen the cell-centered data back down to then-1
level grids.
Round 2: Refined Bookkeeping and Elliptic Solvers
Now that we have constructed a simple AMR routine, we will add a few routines to improve MHD calculations and simplify AMR tree management. We will also add sink particles and the elliptic solver step to the code, expanding the capabilities of our AMR routine.
RECURSIVE SUBROUTINE AMR(n) INTEGER :: n, nSteps, step nSteps = 2 CALL InitInfos(n) CALL ProlongateParentsData(n) CALL ChildMaskOverlaps(n) DO step=1,nSteps levels(n)%step=step IF (step == 2) CALL UpdateOverlaps(n) CALL ApplyOverlaps(n,step) CALL ProlongationFixups(n) IF (lParticles) CALL ParticleUpdate(n) CALL ApplyPhysicalBCs(n) CALL SetErrFlags(n) IF (step == 2) CALL AgeNodesChildren(n) CALL BackupNodes(n+1) CALL CreateChildrens(n) IF (step == 1) THEN CALL InheritOldNodeOverlapsChildren(n) CALL InheritNewNodeOverlapsChildren(n) ELSE CALL InheritOverlapsOldChildren(n) CALL InheritOverlapsNewChildren(n) END IF CALL InheritNeighborsChildren(n) CALL AdvanceGrids(n) IF (lElliptic) CALL Elliptic(n) CALL PrintAdvance(n) CALL AMR(n+1) CALL ApplyChildrenData(n) CALL RestrictionFixups(n) CALL AfterFixups(n) CALL UpdateChildMasks(n) CALL SyncFluxes(n) CALL AccumulateFluxes(n) IF (step == 2) CALL NullifyNeighbors(n) END DO CALL CoarsenDataForParents(n) END SUBROUTINE AMR
ParticleUpdate(n)
— If there are sink particles then we update the particles here.Elliptic(n)
— If elliptic equations are being used, then the elliptic step is performed here.PrintAdvance(n)
— Just prints the 'Advancing level n …' line to the standard output (stdout
). This routine has two collective communications in it, so it can be a bottleneck on a cluster with slow network connections between its nodes.ProlongationFixups(n)
— It is better to complete the prolongation of the aux fields after receiving overlaps. This guarantees that child grids have divergence-free fields consistent with both their neighbors and their parents.ChildMaskOverlaps(n)
— This sets theChildMask
array values to 0 for ghost cells that are refined by neighbors.UpdateChildMask (n)
— This sets theChildMask
array values to 1 in grid cells that are refined by the grid's own children. This also setsChildMask
toNEIGHBOR_CHILD
in grid cells that are refined by a neighbor's children.RestrictionFixups(n)
— This updates cell-centered representations ofaux
fields after receiving restricted data from children.AfterFixups(n)
— This allows for user-defined routines to be applied after a grid has been fully updated. This is not to be confused with theAfterStep()
routine, which is executed after the hyperbolic step is completed.
Dealing with MaxLevel
Up to this point we've assumed we are on an intermediate level of the AMR tree. What is different if we are on the highest level MaxLevel?
RECURSIVE SUBROUTINE AMR(n) INTEGER :: n, nSteps, step nSteps = 2 CALL InitInfos(n) CALL ProlongateParentsData(n) CALL ChildMaskOverlaps(n) DO step=1,nSteps levels(n)%step=step IF (step == 2) CALL UpdateOverlaps(n) CALL ApplyOverlaps(n,step) CALL ProlongationFixups(n) IF (lParticles) CALL ParticleUpdate(n) CALL ApplyPhysicalBCs(n) IF (n < MaxLevel) THEN CALL SetErrFlags(n) IF (step == 2 CALL AgeNodesChildren(n) CALL BackupNodes(n+1) CALL CreateChildrens(n) IF (step == 1) THEN CALL InheritOldNodeOverlapsChildren(n) CALL InheritNewNodeOverlapsChildren(n) ELSE CALL InheritOverlapsOldChildren(n) CALL InheritOverlapsNewChildren(n) END IF CALL InheritNeighborsChildren(n) END IF CALL AdvanceGrids(n) IF (lElliptic) CALL Elliptic(n) CALL PrintAdvance(n) IF (n < MaxLevel) CALL AMR(n+1) IF (n < MaxLevel) CALL ApplyChildrenData(n) CALL RestrictionFixups(n) CALL AfterFixups(n) IF (n < MaxLevel) CALL UpdateChildMasks(n) CALL SyncFluxes(n) CALL AccumulateFluxes(n) IF (step == 2) CALL NullifyNeighbors(n) END DO CALL CoarsenDataForParents(n) END SUBROUTINE AMR
We've basically wrapped the following routines that deal with child nodes inside of conditionals that prevent their execution on the MaxLevel
- SetErrFlags - We won't be refining any regions so no need to set error flags
- AgeNodesChildren - We have no children to age
- BackupNodes - There are no level MaxLevel+1 nodes to backup
- CreateChildrens - We don't create level MaxLevel+1 grids
- InheritOverlaps - Since level MaxLevel nodes have no children there is no inheritting that needs to be done
- AMR - We are on the MaxLevel
- ApplyChildrenData - No child data to apply
- UpdateChildMask - No children to modify the childmask
Dealing with Lower Levels
RECURSIVE SUBROUTINE AMR(n) INTEGER :: n, nSteps, step IF (n <= 0) nSteps=1 IF (n > 0) nSteps = 2 IF (n > -2) THEN CALL InitInfos(n) CALL ProlongateParentsData(n) IF (n > -1) CALL ChildMaskOverlaps(n) END IF DO step=1,nSteps levels(n)%step=step IF (step == 2) CALL UpdateOverlaps(n) IF (n > -2) CALL ApplyOverlaps(n,step) IF (n > 0) CALL ProlongationFixups(n) IF (n > -1 .AND. lParticles) CALL ParticleUpdate(n) IF (n > -1) CALL ApplyPhysicalBCs(n) END IF IF (n < MaxLevel) THEN IF (n > -1) THEN CALL SetErrFlags(n) END IF IF (step == 2 .OR. n == -2) THEN CALL AgeNodesChildren(n) END IF CALL BackupNodes(n+1) CALL CreateChildrens(n) IF (n == -2) THEN CALL InheritOverlapsOldChildren(n) CALL InheritNeighborsChildren(n) CALL InheritOverlapsNewChildren(n) ELSE IF (step == 1) THEN CALL InheritOldNodeOverlapsChildren(n) CALL InheritNewNodeOverlapsChildren(n) CALL InheritNeighborsChildren(n) ELSE CALL InheritOverlapsOldChildren(n) CALL InheritNeighborsChildren(n) CALL InheritOverlapsNewChildren(n) END IF END IF END IF IF (n > -1) THEN CALL AdvanceGrids(n) IF (lElliptic) CALL Elliptic(n) CALL PrintAdvance(n) END IF IF (n < MaxLevel) CALL AMR(n+1) IF (n < MaxLevel) CALL ApplyChildrenData(n) IF (n > -1) THEN CALL RestrictionFixups(n) CALL AfterFixups(n) END IF IF (n > -1) THEN IF (n < MaxLevel) CALL UpdateChildMasks(n) CALL SyncFluxes(n) END IF IF (n > 0) CALL AccumulateFluxes(n) IF (step == 2) CALL NullifyNeighbors(n) END DO IF (n > -2) CALL CoarsenDataForParents(n) END SUBROUTINE AMR
Levels 0 and below
The root Level (level 0) represents the lowest level of hydrodynamic data (although there are lower levels of nodes). As such all levels from the root level down do not need to call:
- ProlongationFixups
- AccumulateFluxes
Were in not for costmap data, these levels would neither need to call
- ProlongateParentsData
- CoarsenDataForParents
Levels -1 and below
Levels -1 and below do not need to call any routines related solely to hydrodynamic variables. This includes in addition to the routines above:
- ParticleUpdate
- ApplyPhysicalBCs
- SetErrFlags
- AdvanceGrids
- Elliptic
- PrintAdvance
- RestrictionFixups
- AfterFixups
- SyncFluxes
Additionally since the entire domain is refined at the root level, levels < 0 do not need to maintain the childmask array. So these levels do not need to call:
- ChildMaskOverlaps
- UpdateChildMasks
Level -2
The Level 2 grid is persistent so it does not need to be initialized or overlapped. So it does not need to call
- InitInfos
- ApplyOverlaps
Additionally the level 2 grid has no parent nodes so there is no need to call parent-related routines
- ProlongateParentsData
- CoarsenDataForParents
Finally since the level 2 is persistent, it behaves like a higher level grid in between steps so it always calls
- AgeNodesChildren
- InheritOverlapsOldChildren
- InheritOverlapsNewChildren
- InheritNeighborsNewChildren
Communication
Data
There are essentially four basic data routines that involve sharing of data between grids
- ProlongateParentsData - Parent to Child (Inter-Level)
- ApplyChildrenData - Child to Parent (Inter-Level)
- ApplyOverlaps - Old Grids to Current Grids (Intra-Level)
- SyncFluxes - Current Grids to Current Grids (Intra-Level)
For parallel applications this requires some degree of communication. In order to overlap computation with communication, it is good to post the sends as soon as the data is available - and to do as much computation as possible until having to wait for the receives to complete. When the sends are checked for completion and when the receives are first posted is somewhat arbitrary. It is reasonable to post the receives before you expect the sends to post and to complete the sends sometime after you expect the receives to have finished.
For each operation there is likely to be a degree of local sharing between grids. The basic approach therefore is to post the receives followed by the sends. Then perform the local sharing before waiting on the receives to complete, and then the sends. Sometimes the posting of the receives is shifted earlier, and the completion of the sends is put off until later. For example the parallel version of ApplyOverlaps is
CALL PostRecvOverlaps ... CALL PostSendOverlaps CALL ApplyOverlaps CALL CompRecvOverlaps ... CALL CompSendOverlaps
Tree
In a similar manner there are five tree operations that require some communication between nodes
- CreateChildren
- InheritNeighborsChildren
- InheritOldNodeOverlapsChildren
- InheritNewNodeOverlapsChildren
- InheritOverlapsOldChildren
- InheritOverlapsNewChildren
As in the case with data operations, each of these requires four communication calls in order to overlap the computation with communication. In all of these cases, it is node's children that are being communicated - since this is the only tree data that is created locally.
Threading
There are several threading options for parallelizing the hydro advance across levels. There are currently three basic approaches to address this
- Threading the Advances - The advancing of each level can be done independently although higher level threads should have higher priorities
- Threading the AMR levels - Each AMR level can also be thought of as an independent thread. Unfortunately this approach requires threads to communicate with other threads on different processors. This requires MPI to be extremely thread safe
- PseudoThreading - This is essentially careful scheduling of the advances to try and mimic the switching that would occur under a threaded implementation. This has the advance of not requiring any external libraries.
For more information on threading see the Scrambler Threading page.