Setting up a new PeleLM Case

In order to set up and run a new case in PeleLM, the user must provide problem-specific code for two main tasks

  • Initial conditions

  • Boundary conditions

These functions are typically collected into a single subfolder in ${PELELM_HOME}/Exec, such as FlameSheet. The user can organize these tasks in any way that is convenient - the examples distributed with PeleLM represent a certain style for managing this with some level of flexibility, but the basic requirement is simply that source be linked into the build for the functions pelelm_initdata for initial conditions and bcnormal for boundary conditions.

Initial Conditions

At the beginning of a PeleLM run, for each level, after grids are generated, the cell-centered values of the state must be initialized. In the code, this is done in an MFIter loop over grids, and a call to the user’s initialization function, pelelm_initdata, that must be provided:

for (MFIter mfi(S_new,TilingIfNotGPU()); mfi.isValid(); ++mfi)
{
    const Box& box = mfi.validbox();
    auto sfab = S_new.array(mfi);

    amrex::ParallelFor(box,
    [=] AMREX_GPU_DEVICE (int i, int j, int k) noexcept
    {
      pelelm_initdata(i, j, k, sfab, geomdata, *lprobparm, lpmfdata);
    });
}

where (i,j,k) are the cell indices, sfab is a light data pointer to the initial state MultiFab and geomdata, lprobparm and lpmfdata are container for the geometry, user-define input and PMF data. The associated user function (in pelelm_prob.H) will provide a value for each entry of the state (velocity, density, mass fraction, …) :

AMREX_GPU_DEVICE
AMREX_FORCE_INLINE
void
pelelm_initdata (int i, int j, int k,
                 amrex::Array4<amrex::Real> const& state,
                 amrex::GeometryData const& geomdata,
                 ProbParm const& prob_parm,
                 PmfData const *pmf_data)
{

   const amrex::Real* prob_lo = geomdata.ProbLo();
   const amrex::Real* prob_hi = geomdata.ProbHi();
   const amrex::Real* dx      = geomdata.CellSize();

   const amrex::Real z = prob_lo[2] + (k+0.5)*dx[2];
   const amrex::Real y = prob_lo[1] + (j+0.5)*dx[1];
   const amrex::Real x = prob_lo[0] + (i+0.5)*dx[0];

   constexpr amrex::Real Pi = 3.14159265358979323846264338327950288;
   const amrex::Real L_x = prob_hi[0] - prob_lo[0];
   const amrex::Real L_y = prob_hi[1] - prob_lo[1];


   ...


   state(i,j,k,DEF_Temp) = prob_parm.Temp;

   for (int n = 0; n < NUM_SPECIES; n++){
     massfrac[n] = 1.0/NUM_SPECIES;
   }


   state(i,j,k,Xvel) = 0.0;
   state(i,j,k,Yvel) =  prob_parm.Vel;
#elif ( AMREX_SPACEDIM == 3 )
   state(i,j,k,Zvel) = 0.0;
#endif

   amrex::Real rho_cgs, P_cgs;
   P_cgs = prob_parm.P_mean * 10.0;

   auto eos = pele::physics::PhysicsType::eos();
   eos.PYT2R(P_cgs, massfrac, state(i,j,k,DEF_Temp), rho_cgs);
   state(i,j,k,Density) = rho_cgs * 1.0e3;            // CGS -> MKS conversion

   eos.TY2H(state(i,j,k,DEF_Temp), massfrac, state(i,j,k,DEF_RhoH));
   state(i,j,k,DEF_RhoH) = state(i,j,k,DEF_RhoH) * 1.0e-4 * state(i,j,k,Density);   // CGS -> MKS conversion

   for (int n = 0; n < NUM_SPECIES; n++) {
     state(i,j,k,DEF_first_spec+n) = massfrac[n] * state(i,j,k,Density);
   }
}

Note home the geometry data are retrived from the geomdata object to obtain the coordinate of each cell center. The state data indices (Xvel, Yvel, Density, … ) are prescribed in the $(PELELM_HOME)/Source/IndexDefines.H file. Note that the conserved states are stored for species and enthalpy (i.e., \(\rho Y_i\) and \(\rho h\)); these are the variables that the user must fill in the initial and boundary condition routines. Typically, however, the primitive state (i.e., \(Y_i\) and \(T\)) is known directly. If that is the case, the user can make use of the compiled-in model-specific equation-of-state routines (eos.) to translate primitive to conserved state values. Consult the example setups provided to see how to call these routines, and how to load the final values required for initial data.

The runtime option (such as initial temperature, inlet velocity, …) are gathered in the ProbParm C++ structure defined in pelelm_prob_parm.H and filled from the input file in pelelm_prob.cpp using AMReX parser. This structure can be modified by the user to hold any data necessary for initial or boundary conditions.

Boundary Conditions

In PeleLM, a single function is used to fill all the state component at physical boundaries. The function bcnormal is in the pelelm_prob.H file. The main objective of this function is to fill the s_ext array fill boundary state data. The function bcnormal is called on each side (lo or hi) for each spatial dimension and will be used to fill the ghost cells of the state variables for which the PeleLM internal boundary condition is EXT_DIR (external Dirichlet) on that face. For example, specifying a PeleLM Inflow boundary condition on the lower face in the y direction in the input file leads to an EXT_DIR for species mass fraction, which then need to be provided in bcnormal. An example of the bcnormal of the FlameSheet is presented here:

AMREX_GPU_DEVICE
AMREX_FORCE_INLINE
void
bcnormal(
  const amrex::Real x[AMREX_SPACEDIM],
  amrex::Real s_ext[DEF_NUM_STATE],
  const int idir,
  const int sgn,
  const amrex::Real time,
  amrex::GeometryData const& geomdata,
  ProbParm const& prob_parm,
  ACParm const& ac_parm,
  PmfData const *pmf_data)
{
  const amrex::Real* prob_lo = geomdata.ProbLo();
  const amrex::Real* prob_hi = geomdata.ProbHi();
  amrex::GpuArray<amrex::Real, NUM_SPECIES + 4> pmf_vals = {0.0};
  amrex::Real molefrac[NUM_SPECIES] = {0.0};
  amrex::Real massfrac[NUM_SPECIES] = {0.0};

  if (sgn == 1) {
    PMF::pmf(pmf_data,prob_lo[idir], prob_lo[idir], pmf_vals);

    s_ext[Xvel] = 0.0;
#if ( AMREX_SPACEDIM == 2 )
    s_ext[Yvel] = pmf_vals[1]*1e-2;
#elif (AMREX_SPACEDIM == 3)
    s_ext[Yvel] = 0.0;
    s_ext[Zvel] = pmf_vals[1]*1e-2;
#endif

    s_ext[DEF_Temp] = pmf_vals[0];

    for (int n = 0; n < NUM_SPECIES; n++){
      molefrac[n] = pmf_vals[3 + n];
    }
    auto eos = pele::physics::PhysicsType::eos();
    eos.X2Y(molefrac, massfrac);

    amrex::Real rho_cgs, P_cgs, RhoH_temp;
    P_cgs = prob_parm.P_mean * 10.0;

    eos.PYT2R(P_cgs, massfrac, s_ext[DEF_Temp], rho_cgs);
    s_ext[Density] = rho_cgs * 1.0e3;

    eos.TY2H(s_ext[DEF_Temp], massfrac, RhoH_temp);
    s_ext[DEF_RhoH] = RhoH_temp * 1.0e-4 * s_ext[Density];   // CGS -> MKS conversion

    for (int n = 0; n < NUM_SPECIES; n++) {
      s_ext[DEF_first_spec+n] = massfrac[n] * s_ext[Density];
    }
  }
}

The sgn input takes a value of 1 on the low face and -1 on the high face, while ìdir provide the spatial direction (0, 1 or 2 corresponding to X, Y or Z, respectively). This allow to differentiate between the various boundary conditions when more than 1 ÈXT_DIR is needed. In this example, the boundary conditions are extracted from a pre-computed premixed flame which data are stored in the pmf_data structure.

Here, we’ve made use of a local convenience function, bcnormal endowed with the knowledge of all boundary values, and extract the appropriate quantity from the results of that call. This was done to localize all boundary condition calculations to a single routine in the code, and helps to preserve consistency. This is only one style though, and as long as appropriate Dirichlet values are set for this state, it makes no difference how the work is organized. For example, data may be provided by interpolating “live data” being actively generated by a co-running separate code, by interpolating data files, evaluating functional forms, etc.

Note that although the array structure to be filled contains valid cell-centered state data where it overlaps the valid domain, the values set in the grow cells of the container will be applied on the boundary face of the corresponding cells. Internally, all PeleLM code understands to apply Dirichlet conditions on the boundary faces.

Refinement Criteria

The dynamic creation and destruction of grid levels is a fundamental part of PeleLM’s capabilities. The process for this is described in some detail in the AMReX documentation, but we summarize the key points here.

At regular intervals (set by the user), each Amr level that is not the finest allowed for the run will invoke a “regrid” operation. When invoked, a list of error tagging functions is traversed. For each, a field specific to that function is derived from the state over the level, and passed through a kernel that “set“‘s or “clear“‘s a flag on each cell. The field and function for each error tagging quantity is identified in the setup phase of the code where the state descriptors are defined (i.e., in PeleLM_setup.cpp). Each function in the list adds or removes to the list of cells tagged for refinement. This final list of tagged cells is sent to a grid generation routine, which uses the Berger-Rigoutsos algorithm to create rectangular grids which will define a new finer level (or set of levels). State data is filled over these new grids, copying where possible, and interpolating from coarser level when no fine data is available. Once this process is complete, the existing Amr level(s) is removed, the new one is inserted into the hierarchy, and the time integration continues.

The traditional AMReX approach to setting up and controlling the regrid process involves explicitly creating (“hard coding”) a number of functions directly into PeleLM’s setup code. (Consult the source code and AMReX documentation for precisely how this is done). PeleLM provides a limited capability to augment the standard set of error functions that is based entirely on runtime data specified in the inputs (ParmParse) data. The following example portion of a ParmParse’d input file demonstrates the usage of this feature:

amr.refinement_indicators = flame_tracer lo_temp gradT

amr.flame_tracer.max_level = 3
amr.flame_tracer.value_greater = 1.e-6
amr.flame_tracer.field_name = Y(H)

amr.lo_temp.max_level = 1
amr.lo_temp.value_less = 450
amr.lo_temp.field_name = temp

amr.gradT.max_level = 2
amr.gradT.adjacent_difference_greater = 20
amr.gradT.field_name = temp
amr.gradT.start_time = 0.001
amr.gradT.end_name = 0.002

Here, we have added three new custom-named criteria – flame_tracer: cells with the mass fraction of H greater than 1 ppm; lo_temp: cells with T less than 450K, and gradT: cells having a temperature difference of 20K from that of their immediate neighbor. The first will trigger up to Amr level 3, the second only to level 1, and the third to level 2. The third will be active only when the problem time is between 0.001 and 0.002 seconds.

Note that these additional user-created criteria operate in addition to those defined as defaults. Also note that these can be modified between restarts of the code. By default, the new criteria will take effect at the next scheduled regrid operation. Alternatively, the user may restart with amr.regrid_on_restart = 1 in order to do a full (all-levels) regrid after reading the checkpoint data and before advancing any cells.