Multiphysics for a Molten Salt Fast Reactor

In this tutorial, you will learn how to:

  • Couple OpenMC and NekRS for modeling a Molten Salt Fast Reactor (MSFR)

  • Use DAGMC Computer Aided Design (CAD) geometry in OpeNMC

  • Use on-the-fly geometry adaptivity ("skinning") to change the OpenMC cells in response to multiphysics feedback

To access this tutorial,


cd cardinal/tutorials/msfr

This tutorial also requires you to download some mesh and restart files from Box. Please download the files from the msfr folder here.

commentnote:Computing Needs

This tutorial requires HPC for running the NekRS model. You will be able to run the OpenMC model without HPC resources. Note that you need to have built Cardinal with DAGMC support enabled, by setting export ENABLE_DAGMC=true.

Geometry and Computational Model

The geometry consists of an MSFR with geometry from Rouch et al. (2014). Volume slices through the NekRS and OpenMC domains are shown in Figure 1.

Volume slices through the NekRS and OpenMC domains

Figure 1: Volume slices through the NekRS and OpenMC domains

As this tutorial is intended primarily to demonstrate on-the-fly geometry skinning, several important features are neglected such as the transport of Delayed Neutron Precursors (DNPs). Future extensions will incorporate material movement in OpenMC driven by velocity (e.g. from a CFD solver such as NekRS), which allows this assumption to be relaxed in the future. For geometric simplicity, radial breeder blankets and axial reflectors are also neglected. The OpenMC model consists of the triangulated surface in Figure 1, with a larger bounding box surrounding it which is set to a vacuum boundary condition.

The nominal operating conditions, as well as the actual conditions used in this tutorial, are summarized in Table 1. In the present tutorial, the reactor is modeled at a 10% flow, 10% power condition (i.e. 300 MWth and 1893 kg/s) to be within an approachable range for LES. The NekRS model uses LES at polynomial oder (512 degrees of freedom per element) for 616000 hexahedral elements, or 315 million grid points. Note in Figure 1 that the meshes used by NekRS and OpenMC are very different - the OpenMC domain uses a triangulated mesh, with about 35000 elements.

Table 1: Nominal and simulated operating conditions for the MSFR

ParameterNominal ValueSimulated Value
Inlet temperature (K)898898
Bulk outlet temperature (K)998998
Power (MWth)3000300
Mass flowrate (kg/s)189231893

The heat exchanger and pump in each of the 16 legs are replaced by recycling inlet and turbulent outlet boundary conditions for velocity. The recycling inlet boundary condition applies the inlet velocity using the velocity distribution some distance upstream from the outlet boundary, essentially representing the inlet as fully-developed channel flow. The inlet temperature is then set to a uniform value of 898 K. For simplicity, the NekRS model uses an incompressible flow model with constant thermophysical properties from Rouch et al. (2014). A Boussinesq buoyancy term will be added in future work. In the OpenMC model, density feedback is approximated by evaluating a thermophysical property correlation given temperature feedback from NekRS.

OpenMC Model

OpenMC is used to solve for the neutron transport and power distribution. The OpenMC model uses DAGMC for ray tracing within triangular surface meshes in -eigenvalue mode. The OpenMC model is created using OpenMC's Python API. As shown in Figure 1, the initial OpenMC model consists of a single large volume, enclosed by one large triangulated mesh surface. This geometry will be adaptively updated according to multiphysics feedback.

The OpenMC model is created with the model.py script. The geometry is created from a triangulated volume mesh, exported from Cubit. The Python script simply creates a single material to represent the fuel salt, assigns our CAD geometry, adds some settings, and exports the xml files necessary for running OpenMC.

import openmc

salt = openmc.Material(name='salt')
salt.set_density('g/cm3', 4.1249)
salt.add_nuclide('Li7', 0.779961, 'ao')
salt.add_nuclide('Li6', 3.89999999999957e-05, 'ao')
salt.add_nuclide('U233', 0.02515, 'ao')
salt.add_element('F', 1.66, 'ao')
salt.add_element('Th', 0.19985, 'ao')
materials = openmc.Materials([salt])
materials.export_to_xml()

dagmc_univ = openmc.DAGMCUniverse(filename="msr.h5m")
geometry = openmc.Geometry(root=dagmc_univ)
geometry.export_to_xml()

settings = openmc.Settings()
lower_left = (-100.0, -100.0, -100.0)
upper_right = (100.0, 100.0, 100.0)
uniform_dist = openmc.stats.Box(lower_left, upper_right)

settings.source = openmc.IndependentSource(space=uniform_dist)

settings.batches = 1500
settings.inactive = 500
settings.particles = 50000

settings.temperature = {'default': 898.0,
                        'method': 'interpolation',
                        'multipole': True,
                        'range': (294.0, 3000.0)}

settings.export_to_xml()
(tutorials/msfr/model.py)
warningwarning

The initial DAGMC model does not contain a graveyard body - so you cannot run this file in a standalone OpenMC run. Cardinal will create a graveyard for you when you have skinning enabled.

You can create these XML files by running


python model.py

or you can simply use the XML files versioned in the directory.

NekRS Model

NekRS is used to solve the incompressible Navier-Stokes equations in non-dimensional form. The NekRS input files needed to solve the incompressible Navier-Stokes equations are:

  • msfr.re2: NekRS mesh

  • msfr.par: High-level settings for the solver, boundary condition mappings to sidesets, and the equations to solve

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

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

  • msfr.usr: User-defined Fortran functions; these are used to apply the recycling boundary condition. Long term, eventually all Nek5000 capabilities will be ported to NekRS, and this fortran file would not be necessary.

A detailed description of all of the available parameters, settings, and use cases for these input files is available on the NekRS documentation website. Because the purpose of this analysis is to demonstrate Cardinal's capabilities, only the aspects of NekRS required to understand the present case will be covered.

The mesh is created offline using gmsh, so we simply provide the final mesh on Box. Next, the .par file contains problem setup information. This input solves for pressure, velocity, and temperature. In the nondimensional formulation, the "viscosity" becomes , where is the Reynolds number, while the "thermal conductivity" becomes , where is the Peclet number. These nondimensional numbers are used to set various diffusion coefficients in the governing equations with syntax like -4.8e4, which is equivalent in NekRS syntax to .

We use the high-pass filter in NekRS for the LES, and filter the highest two modes. We set a Reynolds number of and a Prandtl number of 17.05. We restart this simulation from a previous standalone NekRS run, which used a "frozen" power distribution given in the literature Rouch et al. (2014).

[GENERAL]
  polynomialOrder = 7
  startFrom = restart.fld
  numSteps = 16000
  dt = 2.0e-4
  timeStepper = tombo2
  writeInterval = 2000

  filtering = hpfrt
  filterWeight = 0.1/${dt}
  filterModes = 2

  subCyclingSteps = 2

[PRESSURE]
  residualTol = 1.0e-4

[VELOCITY]
  boundaryTypeMap = inlet, outlet, wall
  residualTol = 1e-6
  density = 1.0
  viscosity = -4.8e4

[TEMPERATURE]
  boundaryTypeMap = inlet, outlet, flux
  rhoCp = 1.0
  conductivity = -818400.0
  residualTol = 1.0e-6
(tutorials/msfr/msfr.par)

Next, the .udf file is used to set up a heat source GPU kernel which we will use to send OpenMC's heat source from Cardinal into NekRS. As we will show later, this heat source is occupying the first (0-indexed) slot of the scratch space. In the UDF_ExecuteStep, we also do some special copies from some Nek5000 backend data (special usage here to accomplish the recycling boundary conditions) into the 3rd, 4th, and 5th slots of the scratch space (the scratch space is zero-indexed). As shown in the .par file, we will read initial conditions for velocity, pressure, and temperature from a restart file, restart.fld, so we don't need to set any other initial conditions here.

#include <math.h>
#include "udf.hpp"

static occa::kernel mooseHeatSourceKernel;

void userq(nrs_t * nrs, double time, occa::memory o_S, occa::memory o_FS)
{
  // note: when running with Cardinal, Cardinal will allocate the usrwrk
  // array. If running with NekRS standalone (e.g. nrsmpi), you need to
  // replace the usrwrk with some other value or allocate it youself from
  // the udf and populate it with values.
  auto mesh = nrs->cds->mesh[0];
  mooseHeatSourceKernel(mesh->Nelements, 0, nrs->o_usrwrk, o_FS);
}

void UDF_LoadKernels(occa::properties& kernelInfo)
{
   mooseHeatSourceKernel = oudfBuildKernel(kernelInfo, "mooseHeatSource");
}

void UDF_Setup(nrs_t *nrs)
{
  udf.sEqnSource = &userq;
}

void UDF_ExecuteStep(nrs_t *nrs, double time, int tstep)
{
  // call userchk every step for recycle inlet
  nek::ocopyToNek(time, tstep);
  nek::userchk();

 	mesh_t *mesh = nrs->meshV;
  if (tstep == 0)
  {
    double *uin= (double *) nek::scPtr(2);
    nrs->o_usrwrk.copyFrom(uin, mesh->Nelements * mesh->Np * sizeof(dfloat), 2*nrs->fieldOffset*sizeof(dfloat));

    double *vin= (double *) nek::scPtr(3);
    nrs->o_usrwrk.copyFrom(vin, mesh->Nelements * mesh->Np * sizeof(dfloat), 3*nrs->fieldOffset*sizeof(dfloat));

    double *win= (double *) nek::scPtr(4);
    nrs->o_usrwrk.copyFrom(win, mesh->Nelements * mesh->Np * sizeof(dfloat), 4*nrs->fieldOffset*sizeof(dfloat));
  }
}
(tutorials/msfr/msfr.udf)

In the .oudf file, we define boundary conditions and any GPU kernels. We see in velocityDirichletConditions how we are setting the inlet velocities equal to the 3rd, 4th, and 5th slots in the scratch space. The mooseHeatSource is a GPU kernel simply reading from scratch space into the QVOL array (which holds a user-defined volumetric heating).

void velocityDirichletConditions(bcData *bc)
{
  // note: when running with Cardinal, Cardinal will allocate the usrwrk
  // array. If running with NekRS standalone (e.g. nrsmpi), you need to
  // replace the usrwrk with some other value or allocate it youself from
  // the udf and populate it with values.
  bc->u =  bc->usrwrk[bc->idM + 2*bc->fieldOffset];
  bc->v =  bc->usrwrk[bc->idM + 3*bc->fieldOffset];
  bc->w =  bc->usrwrk[bc->idM + 4*bc->fieldOffset];
}

// Stabilized outflow (Dong et al)
void pressureDirichletConditions(bcData *bc)
{
  const dfloat iU0delta = 20.0;
  const dfloat un = bc->u*bc->nx + bc->v*bc->ny + bc->w*bc->nz;
  const dfloat s0 = 0.5 * (1.0 - tanh(un*iU0delta));
  bc->p = -0.5 * (bc->u*bc->u + bc->v*bc->v + bc->w*bc->w) * s0;
}

void scalarDirichletConditions(bcData *bc)
{
  bc->s = 0.0;
}

void scalarNeumannConditions(bcData *bc)
{
  bc->flux = 0.0;
}

@kernel void mooseHeatSource(const dlong Nelements, const dlong offset, @restrict const dfloat * source, @restrict dfloat * QVOL)
{
  for(dlong e=0;e<Nelements;++e;@outer(0)){
    for(int n=0;n<p_Np;++n;@inner(0)){
      const int id = e*p_Np + n;
      QVOL[id] = source[offset + id];
    }
  }
}
(tutorials/msfr/msfr.oudf)

Finally, we have a .usr file with Fortran code which accesses from features in Nek5000 which have not yet been fully ported over to NekRS. The code in this file is being used to apply the recycling boundary conditions.

c-----------------------------------------------------------------------
c  nek5000 user-file template
c
c  user specified routines:
c     - uservp  : variable properties
c     - userf   : local acceleration term for fluid
c     - userq   : local source term for scalars
c     - userbc  : boundary conditions
c     - useric  : initial conditions
c     - userchk : general purpose routine for checking errors etc.
c     - userqtl : thermal divergence for lowMach number flows
c     - usrdat  : modify element vertices
c     - usrdat2 : modify mesh coordinates
c     - usrdat3 : general purpose routine for initialization
c
c-----------------------------------------------------------------------
      include "recycle.f"
c-----------------------------------------------------------------------
      subroutine uservp(ix,iy,iz,eg) ! set variable properties
      implicit none
      include 'SIZE'
      include 'TOTAL'
      include 'NEKUSE'

      integer ix,iy,iz,e,eg

      udiff =0.0
      utrans=0.0

      return
      end
c-----------------------------------------------------------------------
      subroutine userf(ix,iy,iz,eg) ! set acceleration term
      implicit none
      include 'SIZE'
      include 'TOTAL'
      include 'NEKUSE'
c
c     Note: this is an acceleration term, NOT a force!
c     Thus, ffx will subsequently be multiplied by rho(x,t).
c
      integer ix,iy,iz,e,eg

      ffx = 0.0
      ffy = 0.0
      ffz = 0.0

      return
      end
c-----------------------------------------------------------------------
      subroutine userq(ix,iy,iz,eg) ! set source term
      implicit none
      include 'SIZE'
      include 'TOTAL'
      include 'NEKUSE'

      integer ix,iy,iz,e,eg

      qvol = 0.0

      return
      end
c-----------------------------------------------------------------------
      subroutine userbc(ix,iy,iz,iside,eg) ! set up boundary conditions
      implicit none
      include 'SIZE'
      include 'TOTAL'
      include 'NEKUSE'

      integer ix,iy,iz,iside,e,eg

      e = gllel(eg)

      ux   = 0.0
      uy   =-1.54261473197
      uz   = 0.0
      temp = 0.0

      return
      end
c-----------------------------------------------------------------------
      subroutine useric(ix,iy,iz,eg) ! set up initial conditions
      implicit none
      include 'SIZE'
      include 'TOTAL'
      include 'NEKUSE'

      integer ix,iy,iz,e,eg

      e = gllel(eg)

      ux   = 0.0
      uy   = 0.0
      uz   = 0.0
      temp = 0.0

      return
      end
c-----------------------------------------------------------------------
      subroutine userchk()
C      implicit none
      include 'SIZE'
      include 'TOTAL'

      real mass_flow_rate

      COMMON /NRSSCPTR/ nrs_scptr(4)
      integer*8         nrs_scptr	
	
      real qt
      common /mauricio/ qt(lx1,ly1,lz1,lelv)

      common /cvelbc/ uin(lx1,ly1,lz1,lelv)
     &              , vin(lx1,ly1,lz1,lelv)
     &              , win(lx1,ly1,lz1,lelv)
     &              , tin(lx1,ly1,lz1,lelt,ldimt)

c recycling boundary conditions
      data ubar / 1.54261473197 /
      data tbar / 0.0 /
      save ubar,tbar

      nxyz = nx1*ny1*nz1
      ntot = nxyz*nelt

      if(istep.eq.0) then
        nrs_scptr(2) = loc(uin(1,1,1,1)) ! pass to udf
        nrs_scptr(3) = loc(vin(1,1,1,1)) ! pass to udf
        nrs_scptr(4) = loc(win(1,1,1,1)) ! pass to udf
      endif

      call set_inflow_fpt(0.0,-0.8,0.0,ubar,tbar) ! for unextruded inlet channels

      call outlet_avgs()

      nrs_scptr(2) = loc(uin(1,1,1,1)) ! pass to udf
      nrs_scptr(3) = loc(vin(1,1,1,1)) ! pass to udf
      nrs_scptr(4) = loc(win(1,1,1,1)) ! pass to udf

      return
      end
c-----------------------------------------------------------------------
      subroutine userqtl ! Set thermal divergence

      call userqtl_scig 

      return
      end
c-----------------------------------------------------------------------
      subroutine usrdat()   ! This routine to modify element vertices
      implicit none
      include 'SIZE'
      include 'TOTAL'

      return
      end
c-----------------------------------------------------------------------
      subroutine usrdat2()  ! This routine to modify mesh coordinates
      implicit none
      include 'SIZE'
      include 'TOTAL'

      integer iel, ifc, id_face
      integer i, e, f, n
      real Dhi
      real xx,yy,zz,dr

      do iel=1,nelv
      do ifc=1,2*ndim
        id_face = bc(5,ifc,iel,1)
        if (id_face.eq.1) then        ! surface 1 for inlet
           cbc(ifc,iel,1) = 'v  '
        elseif (id_face.eq.2) then    ! surface 2 for outlet
           cbc(ifc,iel,1) = 'O  '
        elseif (id_face.eq.3) then    ! surface 3 for wall
           cbc(ifc,iel,1) = 'W  '
        endif
      enddo
      enddo

      do i=2,2      !ldimt1
      do e=1,nelt
      do f=1,ldim*2
        cbc(f,e,i)=cbc(f,e,1)
        if(cbc(f,e,1).eq.'W  ') cbc(f,e,i)='t  '
        if(cbc(f,e,1).eq.'v  ') cbc(f,e,i)='t  '
      enddo
      enddo
      enddo

c for boundaryTypeMap in par file 
      do iel=1,nelv
      do ifc=1,2*ndim
          boundaryID(ifc,iel) = 0
          boundaryID(ifc,iel) =  bc(5,ifc,iel,1)
          boundaryIDt(ifc,iel) = 0
          boundaryIDt(ifc,iel) =  bc(5,ifc,iel,1)
      enddo
      enddo

      return
      end
c-----------------------------------------------------------------------
      subroutine usrdat3()
      implicit none
      include 'SIZE'
      include 'TOTAL'

      return
      end
C-----------------------------------------------------------------------
      subroutine inlet_avgs(mass_flow_rate)
      include 'SIZE'
      include 'TOTAL'

      real sint2,sarea2
      real tout,touta,TAA,mass_flow_rate
	  
      ntot = lx1*ly1*lz1*nelv
      
      touta = 0.0
      TAA = 0.0

      do ie=1,nelv
      do ifc=1,2*ldim
        if(cbc(ifc,ie,1).eq.'v  ') then
        call surface_int(sint2,sarea2,vy,ie,ifc)
        TAA= TAA + sarea2
        touta= touta + sint2
        endif
      enddo
      enddo

      touta = glsum(touta,1)
      TAA = glsum(TAA,1)

      mass_flow_rate = touta

      return
      end
c-----------------------------------------------------------------------
c-----------------------------------------------------------------------
      subroutine outlet_avgs()
c      implicit none
      include 'SIZE'
      include 'TOTAL'

      real tout,touta,TAA
      real sint2,sarea2

      ntot = lx1*ly1*lz1*nelv
      
      touta = 0.0
      TAA = 0.0

      do ie=1,nelv
      do ifc=1,2*ldim
        if(cbc(ifc,ie,1).eq.'O  ') then
        call surface_int(sint2,sarea2,t(1,1,1,1,1),ie,ifc)
        TAA= TAA + sarea2
        touta= touta + sint2
        endif
      enddo
      enddo

      touta = glsum(touta,1)
      TAA = glsum(TAA,1)
      touta = touta/TAA

      return
      end  
c-----------------------------------------------------------------------
(tutorials/msfr/msfr.usr)

Multiphysics Coupling

In this section, OpenMC and NekRS are coupled for multiphysics modeling of an MSFR. This section describes all input files.

OpenMC Input Files

The neutron transport is solved using OpenMC. The input file for this portion of the physics is openmc.i. We begin by setting up the mesh mirror, which is the same volumetric mesh used to generate the DAGMC geometry.

[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
  [mesh]
    type = FileMeshGenerator<<<{"description": "Read a mesh from a file.", "href": "../source/meshgenerators/FileMeshGenerator.html"}>>>
    file<<<{"description": "The filename to read."}>>> = msr.e
  []
[]
(tutorials/msfr/openmc.i)

Next, we set some initial conditions for temperature and density, because we will run OpenMC first.

[ICs<<<{"href": "../syntax/Cardinal/ICs/index.html"}>>>]
  [temp]
    type = ConstantIC<<<{"description": "Sets a constant field value.", "href": "../source/ics/ConstantIC.html"}>>>
    variable<<<{"description": "The variable this initial condition is supposed to provide values for."}>>> = temp
    value<<<{"description": "The value to be set in IC"}>>> = 948.0
  []
  [density]
    type = ConstantIC<<<{"description": "Sets a constant field value.", "href": "../source/ics/ConstantIC.html"}>>>
    variable<<<{"description": "The variable this initial condition is supposed to provide values for."}>>> = density
    value<<<{"description": "The value to be set in IC"}>>> = '${fparse -0.882*948+4983.6}'
  []
[]
(tutorials/msfr/openmc.i)

Next, we define a number of auxiliary variables to be used for diagnostic purposes. With the exception of the ParsedAux used to compute a fluid density in terms of temperature, none of the following variables are necessary for coupling, but they will allow us to visualize how data is mapped from OpenMC to the mesh mirror. The CellTemperatureAux and CellDensityAux will display the OpenMC cell temperatures and densities (after volume-averaging from Cardinal).

[AuxVariables<<<{"href": "../syntax/AuxVariables/index.html"}>>>]
  [cell_temperature]
    family<<<{"description": "Specifies the family of FE shape functions to use for this variable"}>>> = MONOMIAL
    order<<<{"description": "Specifies the order of the FE shape function to use for this variable (additional orders not listed are allowed)"}>>> = CONSTANT
  []
  [cell_density]
    family<<<{"description": "Specifies the family of FE shape functions to use for this variable"}>>> = MONOMIAL
    order<<<{"description": "Specifies the order of the FE shape function to use for this variable (additional orders not listed are allowed)"}>>> = CONSTANT
  []
[]

[AuxKernels<<<{"href": "../syntax/AuxKernels/index.html"}>>>]
  [cell_temperature]
    type = CellTemperatureAux<<<{"description": "OpenMC cell temperature (K), mapped to each MOOSE element", "href": "../source/auxkernels/CellTemperatureAux.html"}>>>
    variable<<<{"description": "The name of the variable that this object applies to"}>>> = cell_temperature
  []
  [cell_density]
    type = CellDensityAux<<<{"description": "OpenMC fluid density (kg/m$^3$), mapped to each MOOSE element", "href": "../source/auxkernels/CellDensityAux.html"}>>>
    variable<<<{"description": "The name of the variable that this object applies to"}>>> = cell_density
  []
  [density]
    type = ParsedAux<<<{"description": "Sets a field variable value to the evaluation of a parsed expression.", "href": "../source/auxkernels/ParsedAux.html"}>>>
    variable<<<{"description": "The name of the variable that this object applies to"}>>> = density
    expression<<<{"description": "Parsed function expression to compute"}>>> = '-0.882*temp+4983.6'
    coupled_variables<<<{"description": "Vector of coupled variable names"}>>> = temp
    execute_on<<<{"description": "The list of flag(s) indicating when this object should be executed. For a description of each flag, see https://mooseframework.inl.gov/source/interfaces/SetupInterface.html."}>>> = timestep_begin
  []
[]
(tutorials/msfr/openmc.i)

Next, the [Problem] and [Tallies] blocks define all the parameters related to coupling OpenMC to MOOSE. We will send temperature and density to OpenMC, and extract power using a MeshTally. We set a number of relaxation settings to use Dufek-Gudowski relaxation, which will progressively ramp the number of particles used in the simulation (starting at 5000) so that we selectively apply computational effort only after the thermal-fluid physics are reasonably well converged. Finally, we will be "skinning" the geometry on-the-fly by providing the skinner user object (of type MoabSkinner).

[Problem<<<{"href": "../syntax/Problem/index.html"}>>>]
  type = OpenMCCellAverageProblem
  verbose = true

  # this will start each Picard iteration from the fission source from the previous one
  reuse_source = true

  scaling = 100.0

  density_blocks = '1'
  temperature_blocks = '1'
  cell_level = 0

  power = 300.0e6

  relaxation = dufek_gudowski
  first_iteration_particles = 5000

  skinner = moab

  [Tallies<<<{"href": "../syntax/Problem/Tallies/index.html"}>>>]
    [heat_source]
      type = MeshTally<<<{"description": "A class which implements unstructured mesh tallies.", "href": "../source/tallies/MeshTally.html"}>>>
      mesh_template<<<{"description": "Mesh tally template for OpenMC when using mesh tallies; at present, this mesh must exactly match the mesh used in the [Mesh] block because a one-to-one copy is used to get OpenMC's tally results on the [Mesh]."}>>> = msr.e
      output<<<{"description": "UNRELAXED field(s) to output from OpenMC for each tally score. unrelaxed_tally_std_dev will write the standard deviation of each tally into auxiliary variables named *_std_dev. unrelaxed_tally_rel_error will write the relative standard deviation (unrelaxed_tally_std_dev / unrelaxed_tally) of each tally into auxiliary variables named *_rel_error. unrelaxed_tally will write the raw unrelaxed tally into auxiliary variables named *_raw (replace * with 'name')."}>>> = unrelaxed_tally_std_dev
    []
  []
[]
(tutorials/msfr/openmc.i)

After every Picard iteration, the skinner will group the elements in the mesh according to their temperature and density. For this tutorial, we will group the elements according to temperature by 15 different bins, ranging from a minimum temperature of 800 K up to a maximum temperature of 1150 K. We will then also bin by density, so that we will have a second grouping according to density. Then, we will create a new OpenMC cell for each unique combination of temperature and density, and re-generate the DAGMC mesh surfaces that bound these regions.

Next, we create a NekRS sub-application, and set up transfers of data between OpenMC and NekRS. These transfers will send fluid temperature and density from NekRS up to OpenMC, and a power distribution to NekRS. We will use sub-cycling, and only send data to/from NekRS at those synchronization points, by using the synchronize_in postprocessor transfer.

[MultiApps<<<{"href": "../syntax/MultiApps/index.html"}>>>]
  [nek]
    type = TransientMultiApp<<<{"description": "MultiApp for performing coupled simulations with the parent and sub-application both progressing in time.", "href": "../source/multiapps/TransientMultiApp.html"}>>>
    input_files<<<{"description": "The input file for each App.  If this parameter only contains one input file it will be used for all of the Apps.  When using 'positions_from_file' it is also admissable to provide one input_file per file."}>>> = 'nek.i'
    sub_cycling<<<{"description": "Set to true to allow this MultiApp to take smaller timesteps than the rest of the simulation.  More than one timestep will be performed for each parent application timestep"}>>> = true
    execute_on<<<{"description": "The list of flag(s) indicating when this object should be executed. For a description of each flag, see https://mooseframework.inl.gov/source/interfaces/SetupInterface.html."}>>> = timestep_end
  []
[]

[Transfers<<<{"href": "../syntax/Transfers/index.html"}>>>]
  [temp_to_openmc]
    type = MultiAppGeneralFieldNearestLocationTransfer<<<{"description": "Transfers field data at the MultiApp position by finding the value at the nearest neighbor(s) in the origin application.", "href": "../source/transfers/MultiAppGeneralFieldNearestLocationTransfer.html"}>>>
    from_multi_app<<<{"description": "The name of the MultiApp to receive data from"}>>> = nek
    variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = temp
    source_variable<<<{"description": "The variable to transfer from."}>>> = temperature
  []
  [power_to_nek]
    type = MultiAppGeneralFieldNearestLocationTransfer<<<{"description": "Transfers field data at the MultiApp position by finding the value at the nearest neighbor(s) in the origin application.", "href": "../source/transfers/MultiAppGeneralFieldNearestLocationTransfer.html"}>>>
    to_multi_app<<<{"description": "The name of the MultiApp to transfer the data to"}>>> = nek
    source_variable<<<{"description": "The variable to transfer from."}>>> = kappa_fission
    variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = source
  []
  [power_integral_to_nek]
    type = MultiAppPostprocessorTransfer<<<{"description": "Transfers postprocessor data between the master application and sub-application(s).", "href": "../source/transfers/MultiAppPostprocessorTransfer.html"}>>>
    to_postprocessor<<<{"description": "The name of the Postprocessor in the MultiApp to transfer the value to.  This should most likely be a Reporter Postprocessor."}>>> = source_integral
    from_postprocessor<<<{"description": "The name of the Postprocessor in the Master to transfer the value from."}>>> = power
    to_multi_app<<<{"description": "The name of the MultiApp to transfer the data to"}>>> = nek
  []
  [synchronize_in]
    type = MultiAppPostprocessorTransfer<<<{"description": "Transfers postprocessor data between the master application and sub-application(s).", "href": "../source/transfers/MultiAppPostprocessorTransfer.html"}>>>
    to_postprocessor<<<{"description": "The name of the Postprocessor in the MultiApp to transfer the value to.  This should most likely be a Reporter Postprocessor."}>>> = transfer_in
    from_postprocessor<<<{"description": "The name of the Postprocessor in the Master to transfer the value from."}>>> = synchronize
    to_multi_app<<<{"description": "The name of the MultiApp to transfer the data to"}>>> = nek
  []
[]
(tutorials/msfr/openmc.i)

Finally, we will use a Transient executioner and add a number of postprocessors for diagnostic purposes. We set the "time step" size in OpenMC to be equal to 2000 times the (dimensional) NekRS time step size, so we are essentially running 2000 NekRS time steps for each OpenMC -eigenvalue solve.

[Postprocessors<<<{"href": "../syntax/Postprocessors/index.html"}>>>]
  [power]
    type = ElementIntegralVariablePostprocessor<<<{"description": "Computes a volume integral of the specified variable", "href": "../source/postprocessors/ElementIntegralVariablePostprocessor.html"}>>>
    variable<<<{"description": "The name of the variable that this object operates on"}>>> = kappa_fission
  []
  [k]
    type = KEigenvalue<<<{"description": "k eigenvalue computed by OpenMC", "href": "../source/postprocessors/KEigenvalue.html"}>>>
  []
  [k_std_dev]
    type = KEigenvalue<<<{"description": "k eigenvalue computed by OpenMC", "href": "../source/postprocessors/KEigenvalue.html"}>>>
    output<<<{"description": "The value to output. Options are $k_{eff}$ (mean), the standard deviation of $k_{eff}$ (std_dev), or the relative error of $k_{eff}$ (rel_err)."}>>> = 'std_dev'
  []
  [max_tally_err]
    type = TallyRelativeError<<<{"description": "Maximum/minimum tally relative error", "href": "../source/postprocessors/TallyRelativeError.html"}>>>
  []
  [max_T]
    type = ElementExtremeValue<<<{"description": "Finds either the min or max elemental value of a variable over the domain.", "href": "../source/postprocessors/ElementExtremeValue.html"}>>>
    variable<<<{"description": "The name of the variable that this postprocessor operates on"}>>> = temp
  []
  [avg_T]
    type = ElementAverageValue<<<{"description": "Computes the volumetric average of a variable", "href": "../source/postprocessors/ElementAverageValue.html"}>>>
    variable<<<{"description": "The name of the variable that this object operates on"}>>> = temp
  []
  [max_q]
    type = ElementExtremeValue<<<{"description": "Finds either the min or max elemental value of a variable over the domain.", "href": "../source/postprocessors/ElementExtremeValue.html"}>>>
    variable<<<{"description": "The name of the variable that this postprocessor operates on"}>>> = kappa_fission
  []
  [synchronize]
    type = Receiver<<<{"description": "Reports the value stored in this processor, which is usually filled in by another object. The Receiver does not compute its own value.", "href": "../source/postprocessors/Receiver.html"}>>>
    default<<<{"description": "The default value"}>>> = 1.0
  []
[]

t_nek = ${fparse 2e-4 * 7.669}

[Executioner<<<{"href": "../syntax/Executioner/index.html"}>>>]
  type = Transient
  dt = ${fparse 2000 * t_nek}
[]

[Outputs<<<{"href": "../syntax/Outputs/index.html"}>>>]
  exodus<<<{"description": "Output the results using the default settings for Exodus output."}>>> = true
  csv<<<{"description": "Output the scalar variable and postprocessors to a *.csv file using the default CSV output."}>>> = true
  hide<<<{"description": "A list of the variables and postprocessors that should NOT be output to the Exodus file (may include Variables, ScalarVariables, and Postprocessor names)."}>>> = 'synchronize'
[]
(tutorials/msfr/openmc.i)

Fluid Input Files

The fluid mass, momentum, and energy transport physics are solved using NekRS. The input file for this portion of the physics is nek.i. We begin by defining a number of file-local constants and by setting up the NekRSMesh mesh mirror. Because we are coupling NekRS via volumetric heating to OepNMC, we need to use a volumetric mesh mirror. The characteristic length chosen for the NekRS files is already 1 m, so we do not need to scale the mesh in any way.

Re = 4.8e4                              # (-)
mu = 0.011266321                        # Pa-s
rho = 4147.3                            # kg/m3

[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
  type = NekRSMesh
  volume = true
  order = SECOND
[]
(tutorials/msfr/nek.i)

The bulk of the NekRS wrapping is specified with NekRSProblem. The NekRS input files are in non-dimensional form, whereas all other coupled applications use dimensional units. The various *_ref and *_0 parameters define the characteristic scales that were used to non-dimensionalize the NekRS input. The FieldTransfers block adds the transfer of volumetric heat source (NekVolumetricSource) and temperature (NekFieldVariable).

[Problem<<<{"href": "../syntax/Problem/index.html"}>>>]
  type = NekRSProblem
  casename<<<{"description": "Case name for the NekRS input files; this is <case> in <case>.par, <case>.udf, <case>.oudf, and <case>.re2."}>>> = 'msfr'
  n_usrwrk_slots<<<{"description": "Number of slots to allocate in nrs->usrwrk to hold fields either related to coupling (which will be populated by Cardinal), or other custom usages, such as a distance-to-wall calculation"}>>> = 5

  synchronization_interval = parent_app

  [FieldTransfers<<<{"href": "../syntax/Problem/FieldTransfers/index.html"}>>>]
    [source]
      type = NekVolumetricSource<<<{"description": "Reads/writes volumetric source data between NekRS and MOOSE."}>>>
      direction<<<{"description": "Direction in which to send data"}>>> = to_nek
      usrwrk_slot<<<{"description": "When 'direction = to_nek', the slot(s) in the usrwrk array to write the incoming data; provide one entry for each quantity being passed"}>>> = 0
    []
    [temperature]
      type = NekFieldVariable<<<{"description": "Reads/writes volumetric field data between NekRS and MOOSE."}>>>
      direction<<<{"description": "Direction in which to send data"}>>> = from_nek
    []
  []

  [Dimensionalize<<<{"href": "../syntax/Problem/Dimensionalize/index.html"}>>>]
    L<<<{"description": "Reference length"}>>> = 1.0
    U<<<{"description": "Reference velocity"}>>> = '${fparse Re * mu / rho}'
    T<<<{"description": "Reference temperature"}>>> = '${fparse 625.0 + 273.15}'
    dT<<<{"description": "Reference temperature difference"}>>> = 100.0
    rho<<<{"description": "Reference density"}>>> = ${rho}
    Cp<<<{"description": "Reference isobaric specific heat"}>>> = 1524.86 # J/kg/K
  []

  normalization_abs_tol = 1e6
  normalization_rel_tol = 1e-3
[]
(tutorials/msfr/nek.i)

Then, we simply set up a Transient executioner with the NekTimeStepper.

[Executioner<<<{"href": "../syntax/Executioner/index.html"}>>>]
  type = Transient
  [TimeStepper<<<{"href": "../syntax/Executioner/TimeStepper/index.html"}>>>]
    type = NekTimeStepper
    min_dt = 1e-10
  []
[]

[Outputs<<<{"href": "../syntax/Outputs/index.html"}>>>]
  exodus<<<{"description": "Output the results using the default settings for Exodus output."}>>> = true
  hide<<<{"description": "A list of the variables and postprocessors that should NOT be output to the Exodus file (may include Variables, ScalarVariables, and Postprocessor names)."}>>> = 'source_integral'
[]
(tutorials/msfr/nek.i)

Execution

To run the input files,


mpiexec -np 150 cardinal-opt -i openmc.i

This will produce a number of output files,

  • openmc_out.e, OpenMC simulation results

  • openmc_out_nek0.e, NekRS simulation results, mapped to a SECOND order Lagrange basis

  • moab_skins_*.h5m, OpenMC cell surfaces (in units of centimeters), which can be converted to .vtk format using the mbconvert script

  • msfr0.f*, NekRS output files

Figure 2 shows the OpenMC power (on a mesh tally), NekRS fluid temperature, and the OpenMC cell temperatures for the last Picard iteration. The black lines delineate the edges of the OpenMC cells, which have been adaptively generated according to the multiphysics feedback.

OpenMC power, NekRS fluid temperature, and re-generated OpenMC cells (delinated with black lines) showing the temperature in each cell

Figure 2: OpenMC power, NekRS fluid temperature, and re-generated OpenMC cells (delinated with black lines) showing the temperature in each cell

References

  1. H. Rouch, O. Geoffroy, P. Rubiolo, A. Laureau, M. Brovchenko, D. Heuer, and E. Merle-Lucotte. Preliminary Thermal-Hydraulic Core Design of the Molten Salt Fast Reactor (MSFR). Annals of Nuclear Energy, 64:449–456, 2014. doi:10.1016/j.anucene.2013.09.012.[BibTeX]