Refinement schemes & implementation#

Please make sure to read the lecture on the AMR structure before following this lecture on the refinement criterions


1. What is refinement about?#

1.1 Introduction#

Refinement is the process of adapting the grid to the physical state of the simulation, by increasing or decreasing the resolution according to user-refinement criteria. In RAMSES, which uses a cell-based adaptive mesh, this consists in creating or destroying grids (i.e. a set of \(2^{\mathrm{ndim}}\) cells) when needed. Contrary to other codes, RAMSES does not have a derefinement criteria: a refined grid is marked for derefinement as soon as it no longer satisfies the criterion for refinement. In addition to the user-defined criteria, RAMSES ensures that the refinement map follows some geometrical rules that simplify the implementation of the Godunov solver and help with the stability of the numerical scheme.

1.2 General description#

Let first review the description of the refinement algorithm in Teyssier 2002.

In short, the refinement proceeds in two phases:

  1. First go through all levels, starting from the finest, and marking (or flagging) cells that need to be refined according to the user-defined criteria. Doing so, the algorithm makes sures that the following rules are respected:

    • The 2:1 rule: If your neighbor is at level \(l\), your level should be \(l\) or \(l\pm1\). This ensures that at any boundary of a cell, you have either one neighbor (of level \(l\) or \(l - 1\)) or two (of level \(l + 1\)).

    • The smoothing rule: Every “physically” refined cell should be surrounded by at least nexpand refined cells in every directions.

    • “Not too fast” derefinement rule: At each step, in any position, the refinement can only be reduced by one level

  2. Split or merge the cells, according to the map built in first phase.

2. Flagging cells for refinement#

Building the refinement map is done by the routine flag in amr/flag_utils.f90. It consists in filling the array flag1, with information on which cells should be refined. The actual refinement (if needed), is done in the next phase by the routine refine (see Section TBD).

The array flag1 is defined in amr/amr_commons.f90 by

integer ,allocatable,dimension(:)  ::flag1   ! flag for refine

and allocated in amr/init_amr.f90 by

! Allocate AMR cell-based arrays
allocate(flag1(0:ncell)) ! Note: starting from 0

where ncell is the maximal number of cells for one MPI process. It is a cell-based array, meaning that flag1(icell) contains the flag information for the cell icell of the current process. If flag1(icell) == 1 it means that the cells should be refined at during this time step, while flag1(icell) == 0 means it should not be in a refined state (so derefined if it is currently refined).

In Ramses jargon, to flag or to flag1 a cell icell means setting flag1(icell) to 1.

Warning

The array flag1 is also used in other part of the code for various purposes (including load balance). The refinement information is thus only stored and valid in the routinerefine, which should be called just after flag.

The routine flag calls the routine flag_fine over each level, starting from the finest one, and then the routine flag_coarse on the coarse grid (i.e. \(l = 0\)). For each level, flag_fine proceeds in 5 steps:

Step 1: Initialisation#

The subroutine init_flag(ilevel) builds the initial state at each level. It first initializes the flag1 array at zero, as this array has been altered in other part of the code for other purposes. Please pay attention on how we go through all the active cells of a given level since this is used quite a bit here.

! Initialize flag1 to 0
nflag=0
do ind=1,twotondim
   iskip=ncoarse+(ind-1)*ngridmax
   do i=1,active(ilevel)%ngrid
      flag1(active(ilevel)%igrid(i)+iskip)=0
   end do
end do

Then, cells are flagged depending on the state of their childeren (if they have any). This is done in the test_flag subroutine. If son(icell)>0 the cell is currenlty refined. We then loop over the children cells to see if any of them * a) has been already flagged for refinement, i.e. has flag1(i_child_cell)=1, which is known because the flagging operation is done level by level, starting from the finer ones, * b) is currently refined.

If any of these conditions is true, the cell is flagged. Three example situations are show in the figure below.

By flagging the cells that contain refined cells, we keep them in their refined state, ensuring the “Not too fast” derefinement rule is respected (see the middle example).

Of course, in levels below levelmin, all cells are marked for refinement.

Step 2: Cubic buffer#

The next step consist in marking the \(3^\mathrm{ndim} − 1\) neighboring cells of each one of the cells marked at step 1. This is performed by the smooth_fine subroutine. This ensures that 2:1 rule is respected (as long as all the levels are flagged in this timestep, ie no adaptive timestepping is used - see Step 5).

The smooth_fine routine proceeds in ndim iterations: - iteration 1: flag1 cells with at least 1 flag1 neighbors (if ndim > 0) - iteration 2: flag1 cells with at least 2 flag1 neighbors (if ndim > 1) - iteration 3: flag1 cells with at least 2 flag1 neighbors (if ndim > 2)

In each step, it uses the flag2 array as a temporary array, because we want to distinguish the cells that were flagged before entering the step from the ones flagged during the step. flag2 is built the same way as flag1.

By doing so, it ensures that each previously flagged cells will be surrounded by flagged cells (see illustration below)

image1

Step 3: Apply user-defined criterion#

This is where you may want to change things

Cells are flagged according to a user specified criterion in the routine userflag_fine. The different criteria are detailled in the next section.

In cosmo runs, the whole level is prevented to refine in certain conditions.

There are several subroutines for refinement criteria: - subroutine flag_utils.f90/poisson_refine(): local Lagrangian and variable refinement criteria, based on the mass (or another variable) in each cell, using the parameters m_refine, ivar_refine, var_cut_refine: - subroutine godunov_utils.f90/hydro_refine(): error on gradients (parameters err_grad_d, err_grad_p, err_grad_u, rt_err_grad_xHII, rt_err_grad_xHI, as well as MHD variables ) - subroutine godunov_utils.f90/jeans_length_refine(): refinement over the jeans length (parameter jeans_refine) - subroutine rt_hydro_refine(): refinement over rt_err_grad_cn.

After each of these routine, a bunch of new cell are flagged as to be refinable using the temporary array ok array. There is then an additional filter implementing the geometrical refinement (subroutine geometry_refine), that will unmark all the cells that do not fulffill it by updating the ok array. By using a temporary array instead of flag1, there is no risk of breaking the refinement rules)

[1]. Finally all the cells marked with ok are marked with flag1.

Step 4: Mesh smoothing#

Enforce the smoothing rule. This simply applies the smooth_fine() subroutine, that was used in step 2, nexpand times.

image2

Step 5: Ensure the 2:1 refinement rule (if we are in an adaptive step)#

Step 1-4 ensure that the refinement rules are always respected, as long as they are applied to all levels.

However, if adaptive timestepping is used, it may well be that the flag_fine routine applied at level ilevel but not at level ilevel - 1 [2], and thus that the planned refinement may break the 2:1 rule. If we are within such a timestep, that is if icount (the current subcycling step at this level) is not the last one (nsubcycle(ilevel-1)), we call the ensure_ref_rules subroutine. This subroutine goes through each grid \(i\) at level ilevel and makes sure that all the \(2^\mathrm{ndim}\) neighboring grids exists. If this is not the case, the cells within the grid i cannot be refined without potentially breaking the 2:1 rule and are unflagged.

Warning

Note that because of this, the mesh smoothing rule with nexpand > 2 is actually not ensured.

3. Existing user defined criteria#

In this section, we review how the existing user-defined criteria are implemented. They are controlled by the REFINE_PARAMS namelist block (see the doc here).

3.1. Mass (“Quasi Lagrangian strategy”)#

This corresponds to the parameters m_refine, mass_sph and mass_cut_refine and is applied in the poisson_refine routine.

The idea is to compute the total mass in each cell (including the contrinution for particles), and comparing it to a reference mass mass_sph. If the computed mass exceed m_refine(ilevel) * mass_sph, the cell is refined. The mimics the typical refinement of SPH codes.

Danger

In this part of the code, the arrays cpu_map and phi are used as temporary array to flag the cells that should be refined and the total “Lagrangian” mass in each cell. This allows to save memory and allocation time and works because the information that was stored in these arrays is no longer needed in the flag routine.

The routine works differently depending on whether gravity is turned on or not.

If poisson == .true., prior entering the flag_fine routine, the array cpu_map is filled with a pre-refinement map, containing 1 for the cells that should be refined according to quasi lagragian strategy, and 0 for the others. This is done in the rho_fine routine in pm/rho_fine.f90 where the total density field for the gravity is calculated (the mass-refine information comes almost for free there). The routine rho_fine firsts initialize the phi array (strange name again, because this is also a reused array) with the mass of gas in each cell divided by mass_sph. The contribution of the particles is then added using the cloud-in-cell scheme. The map is then computed (stored in cpu_map) by comparing the array phi with m_refine(ilevel). In poisson_refine, we just use the content of the array cpu_map to set the array ok, which is then used to update the array flag1.

If poisson == .false., the mass of the gas in the cell is simply compared to m_refine(ilevel) * mass_sph.

3.2 Variables and passive scalar strategy#

The user can also refine the initial grid by inputing a threshold on the value of a variable. This is controlled by the ivar_refine and var_cut_refine parameters. Cells where the hydro variable with the index ivar_refine has a value larger than var_cut_refine should be refined.

Warning

This refinement strategy is only working when gravity is on (poisson == .true.)

This is also carried on within the poisson_refine and the rho_fine routines. It acts as a filter on the quasi-lagrangian strateg by preventing the refinement on the mass whenever uold(ivar_refine)/rho < var_cut_refine.

3.3. Gradients (hydro and rt) strategy#

This refines on the gradients of user defined variables. The logic is simpler (compute the gradients and then apply the criterion), but the routines are spread among several files.

Exercise

Follow the different calls of hydro_flag and then rt_hydro_flag to see when the array ok is updated. Did you notice something strange with rt_hydro_flag?

3.4. Jeans length strategy#

This strategy is used a lot in simulations that care about the Truelove criterion, which says that to avoid artifical fragmentation of the gas, its Jeans length should be resolved by at least 4 cells.

It is also applied in the hydro_flag routine, which calls jeans_length_refine which is computing the Jeans length and applying the criterion.

3.5. Geometrical strategy#

This is actually a filter on the other kinds of refinement, which is ran after all the previous criteria filled the ok array. Only the cells that have ok(icell) == 1 and satisfy the geometrical criterion are actually flagged to be refined (in flag1). Namely, the user has to define a zone at each level in which the refinement can happen.

Exercise 1: Refine on a variable or a combination of variables#

Level 1#

Modify the “Variables and passive scalar” strategy to make it more useful: 1. Make the “Variables and passive scalar” strategy independent from the “Quasi Lagrangian strategy” (you can code it as a new strategy, with its own parameters). 2. Make it possible to have a different threshold at each level (ie make var_cut_refine an array)

Level 2#

If you have time, you can also decide to refine on a combination of variables. You will read and parse a formula from the namelist and refine on the result.

Exercise 2: Improve the geometrical criterion#

Modify geometry_refine to add more than one region of refinement, allowing for more complex shapes.

4. Modify the tree structure#

In the previous step, we built a refinement map, stored in flag1, whether each cell should be refined or not. Now it’s time to actually apply the change, which consist adding a son grid in the leaf cells that were marked for refinement (ie, splitting them) and were not refined before, or destroy the son grid in refined cells that are no longer flagged.

The is done is by the refine routine in amr/refine.f90. This routine applies the refinement map on all levels, but this time going from the coarsest level to the finest. The creation and destruction of grid was discussed in the lecture on the mesh structure.

An important question that arises when creating a cell is how to interpolate the value of the variables. For the hydro variable, this is done automatically by the code and controlled by the user with the interpol_var and interpol_type parameters.

Warning

If you add a new variable, you may have to tell the code how to interpolate it in the refine_utils/make_new_grid routine.