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.
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.

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
| Parameter | Nominal Value | Simulated Value |
|---|---|---|
| Inlet temperature (K) | 898 | 898 |
| Bulk outlet temperature (K) | 998 | 998 |
| Power (MWth) | 3000 | 300 |
| Mass flowrate (kg/s) | 18923 | 1893 |
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)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 meshmsfr.par: High-level settings for the solver, boundary condition mappings to sidesets, and the equations to solvemsfr.udf: User-defined C++ functions for on-line postprocessing and model setupmsfr.oudf: User-defined OCCA kernels for boundary conditions and source termsmsfr.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 resultsopenmc_out_nek0.e, NekRS simulation results, mapped to aSECONDorder Lagrange basismoab_skins_*.h5m, OpenMC cell surfaces (in units of centimeters), which can be converted to.vtkformat using thembconvertscriptmsfr0.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.

Figure 2: OpenMC power, NekRS fluid temperature, and re-generated OpenMC cells (delinated with black lines) showing the temperature in each cell
References
- 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]