Flow Over a Pebble

In this tutorial, you will learn how to:

  • Generate a mesh for NekRS

  • Create NekRS input files for laminar flow

  • Create NekRS inputs in dimensional form

  • Visualize NekRS outputs

To access this tutorial,


cd cardinal/tutorials/pebble_1

Geometry and Computational Model

The domain consists of a single pebble of diameter cm within a rectangular box; the pebble origin is at . Figure 1 shows the fluid portion of the domain (the pebble is not shown). The sideset numbering in the fluid domain is:

  • 1: inlet

  • 2: outlet

  • 3: pebble surface

  • 4: side walls

Figure 1: NekRS flow domain. Sidesets in the fluid domain are colored on the right.

NekRS shall solve for laminar flow over this pebble. Details on the problem specifications are given in Table 1. The inlet velocity is specified such that the Reynolds number is . The characteristic scale for the Reynolds number is chosen as the pebble diameter,

We will apply a uniform heat flux on the pebble surface. The pebble surface heat flux is selected to give a pebble power of approximately 893 W (giving a bulk fluid temperature rise of 50 K). In the conjugate heat transfer tutorials, this heat flux will later be replaced by coupling to MOOSE.

Table 1: Geometric and operating conditions for the single-pebble flow. Fluid properties correspond to water at standard atmosphere and temperature.

ParameterValue
Pebble diameter0.03 m
Box side length0.0506 m
Domain height0.4306 m
Fluid viscosity1e-3 Pa-s
Fluid density1000 kg/m
Fluid thermal conductivity0.6 W/m/K
Fluid volumetric specific heat4186 J/kg/K

At a minimum, a NekRS model will constitute the following files:

  • pebble.re2: Mesh file

  • pebble.par: High-level settings for the solver, boundary conditions, and the equations to solve

  • pebble.udf: User-defined C++ functions for on-line postprocessing and model setup

  • pebble.oudf: User-defined OCCA kernels for boundary conditions and source terms

The default case file naming scheme used by NekRS is to use a common "casename" (in this case, we chose pebble), appended by the different file extensions which represents different parts of the input file setup. You can use a different naming scheme by setting file names for the mesh, .udf, and .oudf file in the .par file (discussed later).

commentnote

You may also optionally have a Fortran .usr file which provides some backend capability to Nek5000 routines (which is written in Fortran) which have not yet been ported to NekRS. For instructional purposes, we will show you one use case for the .usr file - modifying sideset IDs.

.re2 Mesh

NekRS uses a mesh in a custom .re2 format.

commentnote

NekRS has some restrictions on what constitutes a valid mesh: - Mesh must have hexahedral elements in either Hex8 (8-node hexahedral) or Hex20 (20-node hexahedral) forms. Hex20 is a second-order mesh format. The advantage of using a second-order format is evident for geometries with curves, like spheres or cylinders. As you refine to higher polynomial order, the mid-side quadrature points are moved to the curve surface to better capture the geometry. That said, exo2nek (discussed shortly) will automatically convert from tetrahedral and prism elements into hexahedral elements if you have a mesh starting with these other element types. - The sidesets must be numbered sequentially beginning from 1 (e.g. sideset 1, 2, 3, 4, ...).

If you have a mesh in Exodus or Gmsh format, you can convert that mesh into .re2 format using tools that ship with Nek5000. To get these features, you will need to clone Nek5000 somewhere on your system and then build the exo2nek tool (for converting Exodus meshes) or the gmsh2nek tool (for converting Gmsh meshes).


git clone https://github.com/Nek5000/Nek5000.git
cd Nek5000/tools
./maketools exo2nek

Running the above will place a binary named exo2nek at Nek5000/bin/exo2nek. We recommend adding this to your path.

Now that you have exo2nek, we are ready to convert your mesh (pebble.exo) into .re2 format. This mesh is Hex8. Run the following:


exo2nek

Follow the on-screen prompts. For this case, we have:

  • 1 fluid exo file, which is named pebble.exo (you only need to provide pebble as the name)

  • 0 solid exo files (this is nonzero when NekRS is solving for both fluid and solid domains; in this example, we apply a surface heat flux, but are not actually solving for temperature in the solid.)

  • 0 periodic surface pairs (this is nonzero when applying periodic boundary conditions)

  • output file name is pebble.re2 (you only need to provide pebble as the output name)

This will create a mesh named pebble.re2. NekRS is a spectral element code, which means that the solution in each element is represented as an -th order Lagrange polynomial (in each direction). An illustration for a 5th-order NekRS solution is shown in Figure 2 for a 2-D element. Each red dot is a node (GLL quadrature). When you create a mesh for NekRS, you do not see these GLL points in your starting Exodus/Gmsh mesh. Instead, they will be created when you launch NekRS (in other words, you do not need to create unique meshes if you want to run NekRS at different polynomial orders).

Figure 2: Nodal positions for a 5-th order polynomial solution.

.par File

The .par file is used to set up the high-level settings for the case. This file consists of blocks (in square brackets) and parameters.

[OCCA]
  backend = CPU

[GENERAL]
  polynomialOrder = 3
  numSteps = 4000
  dt = targetCFL=2.0 + initial=1e-4
  writeInterval = 500

[PRESSURE]
  residualTol = 1e-06

[VELOCITY]
  boundaryTypeMap = v, o, W, sym
  residualTol = 1e-07
  density = 1000.0
  viscosity = 1e-3

[TEMPERATURE]
  boundaryTypeMap = t, o, f, I
  residualTol = 1e-07
  rhoCp = 4186000
  conductivity = 0.6

[CASEDATA]
  Re = 50
  D = 0.03
  inlet_T = 0
(tutorials/pebble_1/pebble.par)
schooltip

You can get a comprehensive list of all the syntax supported by running


$NEKRS_HOME/bin/nekrs --help par

OCCA block

This block is used to specify whether NekRS is running on CPU or GPU (and which vendor). For our case, we will run this on CPU so we set the backend as CPU.

GENERAL block

The [GENERAL] block describes the time stepping, simulation end control, and polynomial order. A NekRS output file is written every 500 time steps. The numSteps field indicate how many time steps to take. We will use a polynomial order of . We will begin with our first time step as seconds, but allow NekRS to adaptively increase this to get a target Courant-Friedrichs-Lewy (CFL) number of 2.0.

VELOCITY, PRESSURE, TEMPERATURE blocks

Next, the [VELOCITY] and [PRESSURE] blocks describe the solution of the pressure Poisson equation and velocity Helmholtz equations. The [TEMPERATURE] block describes the solution of the energy equation. NekRS can also solve for an arbitrary number of coupled passive scalars, which would be represented using [SCALARXX] blocks, where XX would be replaced with a two digit number indicating the scalar number. Temperature is also a passive scalar, but due to its common presence in CFD simulations, the name of the temperature block is TEMPERATURE instead of SCALAR00.

In these blocks, residualTol is used to indicate the solver tolerance. Here, you also specify the type of boundary conditions to apply to each sideset (you only specify boundary conditions in the [VELOCITY] and [TEMPERATURE]/[SCALARXX] blocks, because the pressure and velocity solves are really indicating together a solution to the momentum conservation equation). The boundaryTypeMap is used to specify the mapping of boundary IDs to types of boundary conditions. NekRS uses short character strings to represent the type of boundary condition. For velocity, some of the common boundary condition strings are shown in Table 2.

Table 2: Common velocity boundary conditions in NekRS

MeaningHow to set in .par FileFunction name in .oudf File
vDirichlet velocityvelocityDirichletConditions
wNo-slip wall
oOutflow velocity + Dirichlet pressurepressureDirichletConditions (nothing needed for velocity outflow)
symxsymmetry in the -direction
symysymmetry in the -direction
symzsymmetry in the -direction
symgeneral symmetry boundary

For temperature/passive scalars, some common boundary condition strings are shown in Table 3.

Table 3: Common temperature/scalar boundary conditions in NekRS

MeaningHow to set in .par FileFunction name in .oudf File
tDirichlet valuescalarDirichletConditions
fNeumann fluxscalarNeumannConditions
Iinsulated (zero flux)

When you populate boundaryTypeMap in the input file, you simply list the character string for your desired boundary condition in the same order as the sidesets (which as you recall are numbered sequentially).

In each of the equation blocks, different parameter names are used to indicate the properties which are multiplying the time and diffusive terms.

Names of parameters for properties in each block

BlockCoefficient on time derivativeCoefficient on diffusive term
[VELOCITY]rhoviscosity|
[TEMPERATURE]rhoCpconductivity
[SCALARXX]rhodiffusivity

For convenience, NekRS allows you to pull parameters from the .par file elsewhere throughout your case files - for instance, you will need to specify boundary and initial conditions in the .oudf and .udf files. In order to streamline the model setup, you can use the [CASEDATA] block to write user-defined local variable names which we will extract later. Here, we define local variables which we arbitrarily name Re, D, and inlet_T which we'll use in our boundary and initial conditions.

.udf File

The .udf file contains additional C++ code which is typically used for setting initial conditions and postprocessing. This file at a minimum must contain 3 functions (they can be empty):

  • UDF_LoadKernels will load any user-defined physics kernels

  • UDF_Setup is called on initialization time, and is typically where initial conditions are applied

  • UDF_ExecuteStep is called once on each time step, and is typically where postprocessing is applied

#include "udf.hpp"

static dfloat inlet_v;
static dfloat inlet_T;

void UDF_LoadKernels(occa::properties & kernelInfo)
{
  // get parameters from .par file
  dfloat D, Re, mu, rho;
  platform->par->extract("casedata", "D", D);
  platform->par->extract("casedata", "Re", Re);
  platform->par->extract("casedata", "inlet_T", inlet_T);
  platform->options.getArgs("VISCOSITY", mu);
  platform->options.getArgs("DENSITY", rho);

  // send parameter to device
  inlet_v = Re * mu / D / rho;
  kernelInfo["defines/p_inlet_v"] = inlet_v;
  kernelInfo["defines/p_inlet_T"] = inlet_T;
}

void UDF_Setup(nrs_t *nrs)
{
  auto mesh = nrs->meshV;

  int n_gll_points = mesh->Np * mesh->Nelements;
  for (int n = 0; n < n_gll_points; ++n)
  {
    nrs->U[n + 0 * nrs->fieldOffset] = 0.0; // x-velocity
    nrs->U[n + 1 * nrs->fieldOffset] = 0.0; // y-velocity
    nrs->U[n + 2 * nrs->fieldOffset] = inlet_v; // z-velocity

    nrs->P[n] = 0.0; // pressure

    nrs->cds->S[n + 0 * nrs->fieldOffset] = inlet_T; // temperature
  }
}

void UDF_ExecuteStep(nrs_t *nrs, double time, int tstep)
{
}
(tutorials/pebble_1/pebble.udf)

Here, we use the UDF_Setup function to set initial conditions (to the same values we applied as our inlet boundary conditions). NekRS stores the various solution components on the host as

  • nrs->U is velocity (all three components are packed one after another, with each "slice" of length nrs->fieldOffset)

  • nrs->P is pressure

  • nrs->cds->S are the passive scalars (packed sequentially one after another, each of length nrs->cds->fieldOffset[i])

Our problem only has one passive scalar (temperature). You will have additional passive scalars if doing RANS modeling (e.g. and would be passive scalars) or if you have additional conservation equations for mass transport, etc. We apply the initial condition by looping over all the elements in the mesh, and in each element for all the GLL points.

Next, we fetch the parameters we desired from the .par file in UDF_LoadKernels. We fetch the values we provided in the [CASEDATA] block, as well as the viscosity and density we gave in the [VELOCITY] block. The kernelInfo lines in particular are sending the inlet_v and inlet_T values from host to device, so that we can later access them from the .oudf file. It is customary when sending a value to device to prepend p_ to the name, to help with clarity.

.oudf File

When you define the boundary condition types in the .par file, you also need to set the values of those boundary conditions (the non-trivial ones) n the .oudf file. The names of these functions are pre-defined by NekRS, and are paired up to the character strings you set earlier. These function names are shown in Table 2 and Table 3 in the .oudf column. Note that a --- indicates that no user-defined kernels are necessary to define that boundary condition (i.e. NekRS already has all the info it needs to apply a no-slip boundary condition - it just sets velocity to zero).

For each of these functions, the bcData struct contains all information about the current boundary that is "calling" the function:

  • bc->id is the boundary ID

  • bc->u is the -velocity

  • bc->v is the -velocity

  • bc->w is the -velocity

  • bc->s is the scalar (temperature) solution at the present quadrature point

  • bc->flux is the flux (of temperature) at the present quadrature point

  • bc->idM is the index corresponding to the current quadrature point

  • bc->scalarId is the passive scalar number (in this example temperature is the 0th scalar and we have no additional scalars)

void velocityDirichletConditions(bcData *bc)
{
  bc->u = 0.0; // x-velocity
  bc->v = 0.0; // y-velocity
  bc->w = p_inlet_v; // z-velocity
}

void scalarDirichletConditions(bcData *bc)
{
  bc->s = p_inlet_T; // temperature
}

void scalarNeumannConditions(bcData *bc)
{
  // power divided by sphere surface area
  bc->flux = 315892;
}
(tutorials/pebble_1/pebble.oudf)

.usr File

NekRS is under rapid development, and many features from Nek5000 are currently being ported over to NekRS. To access those Fortran-based features, you can optionally include a .usr file, which contains funtions for setting initial conditions, modifying the mesh, etc. Here, we will show you how to use this file for changing the sideset IDs in our mesh. Our input mesh, pebble.exo, did not order the sidesets beginning from 1; instead, the sidesets were numbered as 2, 3, 4, and 5. So, we would like to modify those sideset numbers so that they are 1, 2, 3, 4.

We perform this modification in the usrdat2() function (the other functions must be present, but can be empty - they would be used to perform actions at other "insertion" points in NekRS).

c-----------------------------------------------------------------------
      subroutine useric(i,j,k,eg)
      include 'SIZE'
      include 'TOTAL'
      include 'NEKUSE'
      integer i,j,k,e,eg

      return
      end
c-----------------------------------------------------------------------
      subroutine userchk
      include 'SIZE'
      include 'TOTAL'

      return
      end
c-----------------------------------------------------------------------
      subroutine usrdat
      include 'SIZE'
      include 'TOTAL'

      return
      end
c-----------------------------------------------------------------------
      subroutine usrdat2()
      include 'SIZE'
      include 'TOTAL'

      do iel=1,nelt
      do ifc=1,6
      if (boundaryID(ifc,iel).eq.2) then ! rename sideset 2 to 1 (inlet)
          boundaryID(ifc,iel) = 1
       else if (boundaryID(ifc,iel).eq.3) then ! rename sideset 3 to 2 (outlet)
          boundaryID(ifc,iel) = 2
       else if (boundaryID(ifc,iel).eq.4) then ! rename sideset 4 to 3 (pebble surface)
          boundaryID(ifc,iel) = 3
       else if (boundaryID(ifc,iel).eq.5) then ! rename sideset 5 to 4 (side walls)
          boundaryID(ifc,iel) = 4
       else ! NekRS wants all non-sideset faces to be boundary 0
          boundaryID(ifc,iel) = 0
      endif
      enddo
      enddo

      return
      end
c-----------------------------------------------------------------------
      subroutine usrdat3
      include 'SIZE'
      include 'TOTAL'

      return
      end
c-----------------------------------------------------------------------
(tutorials/pebble_1/pebble.usr)

The various terms in this function are:

  • iel is a loop variable name used to indicate a loop over elements

  • ifc is a loop variable name used to indicate a loop over the (six) faces on an element

  • boundaryID(ifc, iel) is the sideset ID for velocity

Execution and Postprocessing

When you compile Cardinal, you automatically also get a standalone NekRS executable. You can run the NekRS case with


$NEKRS_HOME/bin/nrsmpi pebble 4

where pebble is the case name and 4 is the number of MPI ranks you would like to use. Note that NekRS does not use any shared memory parallelism (e.g. OpenMP). Running this case will produce NekRS output files named pebble0.f<n>, where <n> is a 5-digit integer indicating the output file number (i.e. they will be sequentially-ordered based on when you told NekRS to write output files).

These files (referred to as "field" files) need to be accompanied by a small "configuration" file in order to load into Paraview. To generate that file, we need to use the visnek script like so


cardinal/scripts/visnek pebble

This will create a file named pebble.nek5000, which has very simple contents:


 filetemplate: pebble%01d.f%05d
 firsttimestep: 1
 numtimesteps: 8

This file tells Paraview how many time steps there are to load and the naming pattern for those files. To open the NekRS output files, you then need to open the pebble.nek5000 file.

commentnote

To open the files in Paraview or Visit, you must also be sure to have co-located with the pebble.nek5000 files your actual output files from NekRS (e.g. the pebble0.f<n> files).

First, let's take a look at the mesh to get a better sense for how NekRS stores data in its output files. Figure 3 shows a zoomed-in view near the pebble surface. As you can see, this mesh displays the GLL quadrature points. Outlined in red is a single element in our starting Exodus mesh (pebble.exo); the lines inside each element are connecting the nodes.

Figure 3: NekRS spectral mesh, for polynomial order 5

Note that the pebble surface is "faceted" in the same way as our starting Exodus mesh. This is the case because we used a first-order Hex8 mesh.

Figure 4 shows the velocity, pressure, and temperature predicted by NekRS.

Figure 4: NekRS predicted velocity, pressure, and temperature for laminar flow

Hex8 vs Hex20

To appreciate the difference between Hex8 and Hex20 meshes, Figure 5 shows the GLL points which would be used by NekRS if our input mesh was instead in Hex20 format. As you can see, the mid-side nodes follow the curvature of the sphere because the (single) mid-side node in our starting Hex20 mesh "knows about" the curve in the mesh at this point.

Figure 5: NekRS mesh for polynomial order 5 when using a Hex20 input mesh