Multiphysics for an SFR Pincell
In this tutorial, you will learn how to:
Couple OpenMC, NekRS, and MOOSE for modeling an Sodium Fast Reactor (SFR) pincell
Use MOOSE's reactor module to make meshes for common reactor geometries
Use subcycling to efficiently allocate computational resources
To access this tutorial,
cd cardinal/tutorials/pincell_multiphysics
Geometry and Computational Model
The geometry consists of a single pincell, cooled by sodium. The dimensions and assumed operating conditions are summarized in Table 1. Note that these conditions are not necessarily representative of full-power nuclear reactor conditions, since we elect laminar flow conditions in order to reduce meshing requirements for the sake of a fast-running tutorial. By using laminar flow, we will also use a much lower power density than seen in prototypical systems - but for the sake of a tutorial, all the essential components of multiphysics are present (coupling via power, temperature, and density). In addition, to keep the computational requirements as low as possible for NekRS, the height is only 50 cm (so the eignevalue predicted by OpenMC will also be very low, due to high axial leakage).
Parameter | Value |
---|---|
Fuel pellet diameter, | 0.825 cm |
Clad outer diameter, | 0.97 cm |
Pin pitch, | 1.28 cm |
Height | 50.0 cm |
Reynolds number | 500.0 |
Prandtl number | 0.00436 |
Inlet temperature | 573 K |
Outlet temperature | 708 K |
Power | 250 W |
We will couple OpenMC, NekRS, and MOOSE heat conduction in a "single stack" hierarchy, with OpenMC as the main application, MOOSE as the second-level application, and NekRS as the third-level application. This structure is shown in Figure 1, and will allow us to sub-cycle all three codes with respect to one another. Solid black lines indicate data transfers that occur directly from application to application , while the dashed black lines indicate data transfers that first have to pass through an "intermediate" application.
Like with all Cardinal simulations, Picard iterations are achieved "in time." We have not expounded greatly upon this notion in previous tutorials, so we dedicate some space here. The overall Cardinal simulation has a notion of "time" and a time step index, because all code applications will use a Transient executioner. However, usually only NekRS is solved with non-zero time derivatives. The notion of time-stepping is then used to customize how frequently (i.e., in units of time steps) data is exchanged. To help explain the strategy, represent the time step sizes in NekRS, MOOSE, and OpenMC as , , and , respectively. Selecting and/or is referred to as "sub-cycling." In other words, NekRS runs times for each MOOSE solve, while MOOSE runs times for each OpenMC solve, effectively reducing the total number of MOOSE solves by a factor of and the total number of OpenMC solves by a factor of compared to the naive approach to exchange data based on the smallest time step across the coupled codes. We can then also reduce the total number of data transfers by only transferring data when needed. Each Picard iteration consists of:
Run an OpenMC -eigenvalue calculation. Transfer the heat source to MOOSE.
Repeat times:
Run a steady-state MOOSE calculation. Transfer the heat flux to NekRS.
Run a transient NekRS calculation for time steps. Transfer the wall temperature to MOOSE.
Transfer the solid temperature , the fluid temperature , and the fluid density to OpenMC.
Figure 2 shows the procedure for an example selection of , meaning that the MOOSE-NekRS sub-solve occurs three times for every OpenMC solve.
In this tutorial, we will use different "time steps" for OpenMC, MOOSE, and NekRS, and we invite you at the end of the tutorial to explore the impact on changing these time step magnitudes to see how you can control the frequency of data transfers.
OpenMC Model
OpenMC is used to solve for the neutron transport and power distribution. The OpenMC model is created using OpenMC's Python API. We will build this geometry using CSG and divided the pincell into a number of axial layers. On each layer, we will apply temperature and density feedback from the coupled MOOSE-NekRS simulation.
First, we create a number of materials to represent the fuel pellet and cladding. Because these materials will only receive temperature updates (i.e. we will not change their density, because doing so would require us to move cell boundaries in order to preserve mass), we can simply create one material that we will use on all axial layers. Next, we create a number of materials to represent the sodium material in each axial layer of the model. Because the sodium cells will change both their temperature and density, we need to create a unique material for each axial layer.
Next, we divide the geometry into a number of axial layers, where on each layer we set up cells to represent the pellet, cladding, and sodium. Each layer is then described by lying between two -planes. The boundary condition on the top and bottom faces of the OpenMC model are set to vacuum. Finally, we declare a number of settings related to the initial fission source (uniform over the fissionale regions) and how temperature feedback is applied, and then export all files to XML.
from argparse import ArgumentParser
import numpy as np
import openmc
R = 0.97 / 2.0 # outer radius of the pincell (cm)
Rf = 0.825 / 2.0 # outer radius of the pellet (cm)
pitch = 1.28 # pitch between pincells (cm)
height = 50.0 # height of the pincell (cm)
T_inlet = 573.0 # inlet sodium temperature (K)
ap = ArgumentParser()
ap.add_argument('-n', dest='n_axial', type=int, default=25,
help='Number of axial cell divisions')
args = ap.parse_args()
N = args.n_axial
all_materials = []
# Then, add the materials for UO2 and zircaloy
uo2 = openmc.Material(name = 'UO2')
uo2.set_density('g/cm3', 10.29769)
uo2.add_element('U', 1.0, enrichment = 2.5)
uo2.add_element('O', 2.0)
all_materials.append(uo2)
zircaloy = openmc.Material(material_id=3, name='Zircaloy 4')
zircaloy.set_density('g/cm3', 6.55)
zircaloy.add_element('Sn', 0.014, 'wo')
zircaloy.add_element('Fe', 0.00165, 'wo')
zircaloy.add_element('Cr', 0.001, 'wo')
zircaloy.add_element('Zr', 0.98335, 'wo')
all_materials.append(zircaloy)
sodium_materials = []
for i in range(N):
sodium = openmc.Material(name = 'sodium{:n}'.format(i))
sodium.set_density('g/cm3', 1.0)
sodium.add_element('Na', 1.0)
sodium_materials.append(sodium)
all_materials.append(sodium)
materials = openmc.Materials(all_materials)
materials.export_to_xml()
pincell_surface = openmc.ZCylinder(r = R, name = 'Pincell outer radius')
pellet_surface = openmc.ZCylinder(r = Rf, name = 'Pellet outer radius')
rectangular_prism = openmc.model.RectangularPrism(width = pitch, height = pitch, axis = 'z', origin = (0.0, 0.0), boundary_type = 'reflective')
axial_coords = np.linspace(0.0, height, N + 1)
plane_surfaces = [openmc.ZPlane(z0=coord) for coord in axial_coords]
plane_surfaces[0].boundary_type = 'vacuum'
plane_surfaces[-1].boundary_type = 'vacuum'
fuel_cells = []
clad_cells = []
sodium_cells = []
for i in range(N):
# these are the two planes that bound the current layer on top and bottom
layer = +plane_surfaces[i] & -plane_surfaces[i + 1]
fuel_cells.append(openmc.Cell(fill = uo2, region = -pellet_surface & layer, name = 'Fuel{:n}'.format(i)))
clad_cells.append(openmc.Cell(fill = zircaloy, region = +pellet_surface & -pincell_surface & layer, name = 'Clad{:n}'.format(i)))
# we need to get the correct sodium material that belongs to this layer
sodium_material = sodium_materials[i]
sodium_cells.append(openmc.Cell(fill = sodium_material, region = +pincell_surface & layer & -rectangular_prism, name = 'sodium{:n}'.format(i)))
root = openmc.Universe(name = 'root')
root.add_cells(fuel_cells)
root.add_cells(clad_cells)
root.add_cells(sodium_cells)
geometry = openmc.Geometry(root)
geometry.export_to_xml()
settings = openmc.Settings()
# Create an initial uniform spatial source distribution over fissionable zones
lower_left = (-pitch, -pitch, 0.0)
upper_right = (pitch, pitch, height)
uniform_dist = openmc.stats.Box(lower_left, upper_right, only_fissionable = True)
settings.source = openmc.IndependentSource(space=uniform_dist)
settings.batches = 50
settings.inactive = 10
settings.particles = 10000
settings.temperature = {'default': T_inlet,
'method': 'interpolation',
'multipole': True,
'range': (294.0, 3000.0)}
settings.export_to_xml()
(tutorials/pincell_multiphysics/pincell.py)You can create these XML files by running
python pincell.py
Heat Conduction Model
The MOOSE heat transfer module is used to solve for energy conservation in the solid. The solid mesh is shown in Figure 3. This mesh is generated using MOOSE's reactor module, which can be used to make sophisticated meshes of typical reactor geometries such as pin lattices, ducts, and reactor vessels. In this example, we use this module to set up just a single pincell.
This mesh is described in the solid.i
file,
[Mesh]
[pin]
type = PolygonConcentricCircleMeshGenerator
num_sides = 8
num_sectors_per_side = '8 8 8 8 8 8 8 8'
ring_radii = '${fparse 0.75 * Rf} ${fparse 0.85 * Rf} ${fparse 0.95 * Rf} ${Rf} ${R}'
ring_intervals = '1 1 1 1 2'
ring_block_ids = '2 2 2 2 3'
polygon_size = '${fparse pin_pitch / 2.0}'
background_block_ids = '1'
quad_center_elements = true
[]
[delete_background]
type = BlockDeletionGenerator
input = pin
block = '1'
[]
[fluid]
type = PolygonConcentricCircleMeshGenerator
num_sides = 4
num_sectors_per_side = '10 10 10 10'
ring_radii = '${R}'
ring_intervals = '1'
ring_block_ids = '500'
polygon_size = '${fparse pin_pitch / 2.0}'
background_block_ids = '1'
background_intervals = 3
[]
[delete_pin]
type = BlockDeletionGenerator
input = fluid
block = '500'
[]
[combine]
type = CombinerGenerator
inputs = 'delete_background delete_pin'
[]
[extrude]
type = AdvancedExtruderGenerator
input = combine
heights = ${height}
num_layers = ${num_layers}
direction = '0 0 1'
[]
[rotate]
type = TransformGenerator
input = extrude
transform = ROTATE
vector_value = '45.0 0.0 0.0'
[]
# We want boundary 5 to be the clad surface, because at the time we made this tutorial
# that is what the reactor module named this boundary (so we drew that boundary in
# some figures). If you are making a mesh from scratch, you dont need to go through
# these gynmastics.
[delete_5]
type = BoundaryDeletionGenerator
input = rotate
boundary_names = '5'
[]
[name_5]
type = RenameBoundaryGenerator
input = delete_5
old_boundary = '9'
new_boundary = '5'
[]
[]
(tutorials/pincell_multiphysics/solid.i)You can generate this mesh by running
cardinal-opt -i solid.i --mesh-only
which will generate the mesh as the solid_in.e
file. You will note in Figure 3 that this mesh actually also includes mesh for the fluid region. In Figure 1, we need a mesh region to "receive" the fluid temperature and density from NekRS, because NekRS cannot directly communicate with OpenMC by skipping the second-level app (MOOSE). Therefore, no solve will occur on this fluid mesh, it simply exists to receive the fluid solution from NekRS before being passed "upwards" to OpenMC. By having this dummy "receiving-only" mesh in the solid application, we will require a few extra steps when we set up the MOOSE heat conduction model in Solid Input Files .
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:
fluid.re2
: NekRS mesh (generated byexo2nek
starting from MOOSE-generated meshes, to be discussed shortly)fluid.par
: High-level settings for the solver, boundary condition mappings to sidesets, and the equations to solvefluid.udf
: User-defined C++ functions for on-line postprocessing and model setupfluid.oudf
: User-defined OCCA kernels for boundary conditions and source terms
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.
We first create the mesh using MOOSE's reactor module. The syntax used to build a HEX27 fluid mesh is shown below. A major difference from the dummy fluid receiving portion of the solid mesh is that we now set up some boundary layers on the pincell surfaces, by providing ring_radii
(and other parameters) as vectors.
[Mesh]
[pin]
type = PolygonConcentricCircleMeshGenerator
num_sides = 4
polygon_size = '${fparse pin_pitch / 2.0}'
num_sectors_per_side = '6 6 6 6'
uniform_mesh_on_sides = true
ring_radii = '${fparse pin_diameter / 2.0} ${bl_0} ${bl_1} ${bl_2} ${bl_3} ${bl_4} ${bl_5}'
ring_intervals = '1 1 1 1 1 1 1'
ring_block_ids = '${fparse fluid_id + 1} ${fluid_id} ${fluid_id} ${fluid_id} ${fluid_id} ${fluid_id} ${fluid_id}'
background_block_ids = '${fparse fluid_id}'
background_intervals = 6
[]
[pin_surface]
type = SideSetsBetweenSubdomainsGenerator
input = pin
primary_block = ${fluid_id}
paired_block = '${fparse fluid_id + 1}'
new_boundary = '1'
[]
[delete_solid]
type = BlockDeletionGenerator
input = pin_surface
block = '${fparse fluid_id + 1}'
[]
[rotate]
type = TransformGenerator
input = delete_solid
transform = rotate
vector_value = '45.0 0.0 0.0'
[]
# We will name boundaries in the mesh for sidesets 2 - 7 using other MOOSE objects.
# The reactor module does its own sideset naming, so we make sure those IDs are
# free by deleting whatever the reactor module had.
[delete_extra_surfaces]
type = BoundaryDeletionGenerator
input = rotate
boundary_names = '2 3 4 5 6 7'
[]
[extrude]
type = AdvancedExtruderGenerator
input = delete_extra_surfaces
direction = '0 0 1'
num_layers = '${num_layers}'
heights = '${height}'
bottom_boundary = '2'
top_boundary = '3'
[]
[lateral_boundaries]
type = SideSetsFromNormalsGenerator
input = extrude
new_boundary = '4 5 6 7'
normals = '1.0 0.0 0.0
-1.0 0.0 0.0
0.0 1.0 0.0
0.0 -1.0 0.0'
normal_tol = 1e-6
[]
second_order = true
[]
(tutorials/pincell_multiphysics/fluid.i)However, NekRS requires a HEX20 mesh format. Therefore, we use a NekMeshGenerator to convert from HEX27 to HEX20 format, while also moving the higher-order side nodes on the pincell surface to match the curvilinear elements. The syntax used to convert from the HEX27 fluid mesh to a HEX20 fluid mesh, while preserving the pincell surface, is shown below.
pin_diameter = 0.97e-2 # pin outer diameter
pin_pitch = 1.28e-2 # pin pitch
flow_area = ${fparse pin_pitch * pin_pitch - pi * pin_diameter * pin_diameter / 4.0}
wetted_perimeter = ${fparse pi * pin_diameter}
hydraulic_diameter = ${fparse 4.0 * flow_area / wetted_perimeter}
[Mesh]
[file]
type = FileMeshGenerator
file = fluid_in.e
[]
[to_hex20]
type = NekMeshGenerator
input = file
boundary = '1'
radius = ${fparse pin_diameter / 2.0}
boundaries_to_rebuild = '1 2 3 4 5 6 7'
layers = '4'
geometry_type = cylinder
[]
[scale]
type = TransformGenerator
input = to_hex20
transform = scale
vector_value = '${fparse 1.0 / hydraulic_diameter} ${fparse 1.0 / hydraulic_diameter} ${fparse 1.0 / hydraulic_diameter}'
[]
[]
(tutorials/pincell_multiphysics/convert.i)To generate the meshes, we run:
cardinal-opt -i fluid.i --mesh-only
cardinal-opt -i convert.i --mesh-only
mv convert_in.e convert.exo
and then use the exo2nek
utility to convert from the exodus file format (convert.exo
) into the custom .re2
format required for NekRS. A depiction of the outputs of the two stages of the mesh generator process are shown in Figure 4. Boundary 2 is the inlet, boundary 3 is the outlet, and boundary 1 is the pincell surface. Boundaries 4 through 7 are the lateral faces of the pincell.
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 -500.0
, which is equivalent in NekRS syntax to . On the four lateral faces of the pincell, we set symmetry conditions for velocity.
# Model of single SFR pincell, in non-dimensional form
#
# Pr = 0.00439
# Re = 500.0
# Pe = Re * Pr
[OCCA]
backend = CPU
[GENERAL]
stopAt = numSteps
numSteps = 2000
dt = 2.5e-3
timeStepper = tombo2
writeControl = steps
writeInterval = 200
polynomialOrder = 5
[VELOCITY]
viscosity = -500.0
density = 1.0
boundaryTypeMap = w, v, O, symx, symx, symy, symy
residualTol = 1.0e-7
[PRESSURE]
residualTol = 1.0e-6
[TEMPERATURE]
conductivity = -2.195
rhoCp = 1.0
boundaryTypeMap = f, t, I, I, I, I, I
residualTol = 1.0e-6
(tutorials/pincell_multiphysics/fluid.par)Next, the .udf
file is used to set up initial conditions for velocity, pressure, and temperature. We set uniform axial velocity initial conditions, and temperature to a linear variation from 0 at the inlet to 1 at the outlet.
#include "udf.hpp"
#define LENGTH 42.35158086795113
void UDF_LoadKernels(occa::properties & kernelInfo)
{
}
void UDF_Setup(nrs_t *nrs)
{
auto mesh = nrs->cds->mesh[0];
// loop over all the GLL points and assign directly to the solution arrays by
// indexing according to the field offset necessary to hold the data for each
// solution component
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] = 1.0; // z-velocity
nrs->P[n] = 0.0; // pressure
nrs->cds->S[n + 0 * nrs->cds->fieldOffset[0]] = mesh->z[n] / LENGTH; // temperature
}
}
(tutorials/pincell_multiphysics/fluid.udf)In the .oudf
file, we define boundary conditions. On the flux boundary, we will be sending a heat flux from MOOSE into NekRS, so we set the flux equal to the scratch space array, bc->usrwrk[bc->idM]
.
void velocityDirichletConditions(bcData * bc)
{
bc->u = 0.0; // x-velocity
bc->v = 0.0; // y-velocity
bc->w = 1.0; // z-velocity
}
void scalarDirichletConditions(bcData * bc)
{
bc->s = 0.0;
}
void scalarNeumannConditions(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->flux = bc->usrwrk[bc->idM];
}
(tutorials/pincell_multiphysics/fluid.oudf)Multiphysics Coupling
In this section, OpenMC, NekRS, and MOOSE are coupled for multiphysics modeling of an SFR pincell. 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 defining a number of constants and setting up the mesh mirror. For simplicity, we just use the same mesh as used in the solid application, shown in Figure 3.
inlet_T = 573.0 # inlet temperature
power = 1e3 # total power (W)
Re = 500.0 # Reynolds number
outlet_P = 1e6
height = 0.5 # total height of the domain
Df = 0.825e-2 # fuel diameter
pin_diameter = 0.97e-2 # pin outer diameter
pin_pitch = 1.28e-2 # pin pitch
mu = 8.8e-5 # fluid dynamic viscosity
rho = 723.6 # fluid density
Cp = 5512.0 # fluid isobaric specific heat capacity
Rf = ${fparse Df / 2.0}
flow_area = ${fparse pin_pitch * pin_pitch - pi * pin_diameter * pin_diameter / 4.0}
wetted_perimeter = ${fparse pi * pin_diameter}
hydraulic_diameter = ${fparse 4.0 * flow_area / wetted_perimeter}
U_ref = ${fparse Re * mu / rho / hydraulic_diameter}
mdot = ${fparse rho * U_ref * flow_area}
dT = ${fparse power / mdot / Cp}
[Mesh]
[solid]
type = FileMeshGenerator
file = solid_in.e
[]
[]
(tutorials/pincell_multiphysics/openmc.i)Next, we define a number of auxiliary variables to be used for diagnostic purposes. With the exception of the FluidDensityAux, 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 FluidDensityAux auxiliary kernel on the other hand is used to compute the fluid density, given the temperature variable temp
(into which we will write the MOOSE and NekRS temperatures, into different regions of space). Note that we will not send fluid density from NekRS to OpenMC, because the NekRS model uses an incompressible Navier-Stokes model. But to a decent approximation, the fluid density can be approximated solely as a function of temperature using the SodiumSaturationFluidProperties (so named because these properties represent sodium properties at saturation temperature).
[AuxVariables]
# These auxiliary variables are all just for visualizing the solution and
# the mapping - none of these are part of the calculation sequence
[material_id]
family = MONOMIAL
order = CONSTANT
[]
[cell_temperature]
family = MONOMIAL
order = CONSTANT
[]
[cell_density]
family = MONOMIAL
order = CONSTANT
[]
[]
[AuxKernels]
[material_id]
type = CellMaterialIDAux
variable = material_id
[]
[cell_temperature]
type = CellTemperatureAux
variable = cell_temperature
[]
[cell_density]
type = CellDensityAux
variable = cell_density
[]
[density]
type = FluidDensityAux
variable = density
p = ${outlet_P}
T = nek_temp
fp = sodium
execute_on = 'INITIAL TIMESTEP_END'
[]
[]
[FluidProperties]
[sodium]
type = SodiumSaturationFluidProperties
[]
[]
(tutorials/pincell_multiphysics/openmc.i)Next, the [Problem]
and [Tallies]
blocks define all the parameters related to coupling OpenMC to MOOSE. We will send temperature to OpenMC from blocks 2 and 3 (which represent the solid regions) and we will send temperature and density to OpenMC from block 1 (which represents the fluid region). We add a CellTally to tally the heat source in the fuel.
For this problem, the temperature that gets mapped into OpenMC is sourced from two different applications, which we can customize using the temperature_variables
and temperature_blocks
parameters. Later in the Transfers
block, all data transfers from the sub-applications will write into either solid_temp
or nek_temp
.
Finally, we provide some additional specifications for how to run OpenMC. We set the number of batches and various triggers that will automatically terminate the OpenMC solution when reaching less than 2% maximum relative uncertainty in the fission power taly and 7.5E-4 standard deviation in .
[Problem]
type = OpenMCCellAverageProblem
power = ${power}
scaling = 100.0
density_blocks = '1'
cell_level = 0
# This automatically creates these variables and will read from the non-default choice of 'temp'
temperature_variables = 'solid_temp; nek_temp'
temperature_blocks = '2 3; 1'
relaxation = robbins_monro
# Set some parameters for when we terminate the OpenMC solve in each iteration;
# this will run a minimum of 30 batches, and after that, terminate once reaching
# the specified std. dev. of k and rel. err. of the fission tally
inactive_batches = 20
batches = 30
k_trigger = std_dev
k_trigger_threshold = 7.5e-4
batch_interval = 50
max_batches = 1000
[Tallies]
[heat_source]
type = CellTally
blocks = '2'
name = heat_source
check_equal_mapped_tally_volumes = true
trigger = rel_err
trigger_threshold = 2e-2
output = unrelaxed_tally_std_dev
[]
[]
[]
(tutorials/pincell_multiphysics/openmc.i)Next, we set up some initial conditions for the various fields used for coupling.
[ICs]
[nek_temp]
type = FunctionIC
variable = nek_temp
function = temp_ic
[]
[solid_temp]
type = FunctionIC
variable = solid_temp
function = temp_ic
[]
[heat_source]
type = ConstantIC
variable = heat_source
block = '2'
value = ${fparse power / (pi * Rf * Rf * height)}
[]
[]
[Functions]
[temp_ic]
type = ParsedFunction
expression = '${inlet_T} + z / ${height} * ${dT}'
[]
[]
(tutorials/pincell_multiphysics/openmc.i)Next, we create a MOOSE heat conduction sub-application, and set up transfers of data between OpenMC and MOOSE. These transfers will send solid temperature and fluid temperature from MOOSE up to OpenMC, and a power distribution to MOOSE.
[MultiApps]
[bison]
type = TransientMultiApp
input_files = 'bison.i'
execute_on = timestep_begin
sub_cycling = true
[]
[]
[Transfers]
[solid_temp_to_openmc]
type = MultiAppGeneralFieldShapeEvaluationTransfer
source_variable = T
variable = solid_temp
from_multi_app = bison
[]
[source_to_bison]
type = MultiAppCopyTransfer
source_variable = heat_source
variable = power
to_multi_app = bison
[]
[temp_from_nek]
type = MultiAppGeneralFieldShapeEvaluationTransfer
source_variable = nek_temp
from_multi_app = bison
variable = nek_temp
[]
[]
(tutorials/pincell_multiphysics/openmc.i)Finally, we will use a Transient executioner and add a number of postprocessors for diagnostic purposes.
[Outputs]
exodus = true
csv = true
[]
[Executioner]
type = Transient
dt = 0.5
num_steps = 10
[]
[Postprocessors]
[max_tally_rel_err]
type = TallyRelativeError
value_type = max
[]
[k]
type = KEigenvalue
[]
[k_std_dev]
type = KStandardDeviation
[]
[]
(tutorials/pincell_multiphysics/openmc.i)Solid Input Files
The conservation of solid energy is solved using the MOOSE heat transfer module. The input file for this portion of the physics is bison.i
. We begin by defining a number of constants and setting the mesh.
inlet_T = 573.0 # inlet temperature
power = 250 # total power (W)
Re = 500.0 # Reynolds number
height = 0.5 # total height of the domain
Df = 0.825e-2 # fuel diameter
pin_diameter = 0.97e-2 # pin outer diameter
pin_pitch = 1.28e-2 # pin pitch
mu = 8.8e-5 # fluid dynamic viscosity
rho = 723.6 # fluid density
Cp = 5512.0 # fluid isobaric specific heat capacity
Rf = ${fparse Df / 2.0}
flow_area = ${fparse pin_pitch * pin_pitch - pi * pin_diameter * pin_diameter / 4.0}
wetted_perimeter = ${fparse pi * pin_diameter}
hydraulic_diameter = ${fparse 4.0 * flow_area / wetted_perimeter}
U_ref = ${fparse Re * mu / rho / hydraulic_diameter}
mdot = ${fparse rho * U_ref * flow_area}
dT = ${fparse power / mdot / Cp}
[Mesh]
[solid]
type = FileMeshGenerator
file = solid_in.e
[]
[]
(tutorials/pincell_multiphysics/bison.i)Because some blocks in the solid mesh aren't actually used in the solid solve (i.e. the fluid block we will use to receive fluid temperature and density from NekRS), we need to explicitly tell MOOSE to not throw errors related to checking for material properties on every mesh block.
[Problem]
type = FEProblem
material_coverage_check = false
[]
(tutorials/pincell_multiphysics/bison.i)Next, we set up a nonlinear variable T
to represent solid temperature and create kernels representing heat conduction with a volumetric heating in the pellet region. In the fluid region, we need to use a NullKernel to indicate that no actual solve happens on this block. On the cladding surface, we will impose a Dirichlet boundary condition given the NekRS fluid temperature. Finally, we set up material properties for the solid blocks using HeatConductionMaterial.
[Variables]
[T]
initial_condition = ${inlet_T}
[]
[]
[Kernels]
[diffusion]
type = HeatConduction
variable = T
block = '2 3'
[]
[source]
type = CoupledForce
variable = T
v = power
block = '2'
[]
[null]
type = NullKernel
variable = T
block = '1'
[]
[]
[BCs]
[pin_outer]
type = MatchedValueBC
variable = T
v = nek_temp
boundary = '5'
[]
[]
[Materials]
[uo2]
type = HeatConductionMaterial
thermal_conductivity = 5.0
block = '2'
[]
[clad]
type = HeatConductionMaterial
thermal_conductivity = 20.0
block = '3'
[]
[]
(tutorials/pincell_multiphysics/bison.i)Next, we declare auxiliary variables to be used for:
coupling to NekRS (to receive the NekRS temperature,
nek_temp
)computing the pin surface heat flux (to send heat flux
flux
to NekRS)power
to represent the volumetric heating received from OpenMC
On each MOOSE-NekRS substep, we will run MOOSE first. For the very first time step, this means we should set an initial condition for the NekRS fluid temperature, which we simply set to a linear function of height. Finally, we create a DiffusionFluxAux to compute the heat flux on the pin surface.
[AuxVariables]
[nek_temp]
[]
[flux]
family = MONOMIAL
order = CONSTANT
[]
[power]
family = MONOMIAL
order = CONSTANT
initial_condition = ${fparse power / (pi * Rf * Rf * height)}
[]
[]
[Functions]
[axial_fluid_temp]
type = ParsedFunction
expression = '${inlet_T} + z / ${height} * ${dT}'
[]
[]
[ICs]
[nek_temp]
type = FunctionIC
variable = nek_temp
function = axial_fluid_temp
[]
[]
[AuxKernels]
[flux]
type = DiffusionFluxAux
diffusion_variable = T
component = normal
diffusivity = thermal_conductivity
variable = flux
boundary = '5'
[]
[]
(tutorials/pincell_multiphysics/bison.i)Next, we create a NekRS sub-application and set up the data transfers between MOOSE and NekRS. There are four transfers - three are related to sending the temperature and heat flux between MOOSE and NekRS, and the fourth is related to a performance optimization in the NekRS wrapping that will only interpolate data to/from NekRS at the synchronization points with the application "driving" NekRS (in this case, MOOSE heat conduction).
[MultiApps]
[nek]
type = TransientMultiApp
input_files = 'nek.i'
execute_on = timestep_end
sub_cycling = true
[]
[]
[Transfers]
[temperature]
type = MultiAppGeneralFieldNearestLocationTransfer
source_variable = temp
from_multi_app = nek
variable = nek_temp
[]
[flux]
type = MultiAppGeneralFieldNearestLocationTransfer
source_variable = flux
to_multi_app = nek
variable = avg_flux
from_boundaries = '5'
[]
[flux_integral]
type = MultiAppPostprocessorTransfer
to_postprocessor = flux_integral
from_postprocessor = flux_integral
to_multi_app = nek
[]
[send]
type = MultiAppPostprocessorTransfer
to_postprocessor = transfer_in
from_postprocessor = send
to_multi_app = nek
[]
[]
(tutorials/pincell_multiphysics/bison.i)Next, we set up a number of postprocessors, both for normalizing the total power sent from MOOSE to NekRS, as well as a few diagnostic terms.
[Postprocessors]
[send]
type = Receiver
default = 1.0
[]
[flux_integral]
# evaluate the total heat flux for normalization
type = SideDiffusiveFluxIntegral
diffusivity = thermal_conductivity
variable = T
boundary = '5'
[]
[power]
type = ElementIntegralVariablePostprocessor
variable = power
block = '2'
execute_on = 'transfer initial'
[]
[max_T_solid]
type = ElementExtremeValue
variable = T
block = '2 3'
[]
[]
(tutorials/pincell_multiphysics/bison.i)Finally, we set up the MOOSE heat conduction solver to use a Transient exeuctioner and specify the output format. Important to note here is that a user-provided choice of M
will determine how many NekRS time steps are run for each MOOSE time step.
[Executioner]
type = Transient
dt = ${fparse M * t_nek * dt0}
num_steps = 100
nl_abs_tol = 1e-8
nl_rel_tol = 1e-16
petsc_options_value = 'hypre boomeramg'
petsc_options_iname = '-pc_type -pc_hypre_type'
[]
[Outputs]
exodus = true
hide = 'power flux_integral send'
[]
(tutorials/pincell_multiphysics/bison.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 constants and by setting up the NekRSMesh mesh mirror. Because we are coupling NekRS via boundary heat transfer to MOOSE, but via volumetric temperature and densities to OpenMC, we need to use a combined boundary and volumetric mesh mirror, so both boundary
and volume = true
are provided. Because this problem is set up in non-dimensional form, we also need to rescale the mesh to match the units expected by our solid and OpenMC input files.
inlet_T = 573.0 # inlet temperature
power = 250 # total power (W)
Re = 500.0 # Reynolds number
pin_diameter = 0.97e-2 # pin outer diameter
pin_pitch = 1.28e-2 # pin pitch
mu = 8.8e-5 # fluid dynamic viscosity
rho = 723.6 # fluid density
Cp = 5512.0 # fluid isobaric specific heat capacity
flow_area = ${fparse pin_pitch * pin_pitch - pi * pin_diameter * pin_diameter / 4.0}
wetted_perimeter = ${fparse pi * pin_diameter}
hydraulic_diameter = ${fparse 4.0 * flow_area / wetted_perimeter}
U_ref = ${fparse Re * mu / rho / hydraulic_diameter}
mdot = ${fparse rho * U_ref * flow_area}
dT = ${fparse power / mdot / Cp}
[Mesh]
type = NekRSMesh
boundary = '1'
scaling = ${hydraulic_diameter}
volume = true
[]
(tutorials/pincell_multiphysics/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. In order to simplify the input file, we know a priori that OpenMC will not be sending a heat source to NekRS, so we set has_heat_source = false
so that we don't need to add a dummy heat source kernel to the fluid.oudf
file. If we had volumetric heating in the fluid, then we would instead send a heating term into NekRS, but this is neglected for this example.
[Problem]
type = NekRSProblem
casename = 'fluid'
nondimensional = true
L_ref = ${hydraulic_diameter}
T_ref = ${inlet_T}
U_ref = ${U_ref}
dT_ref = ${dT}
rho_0 = ${rho}
Cp_0 = ${Cp}
has_heat_source = false
synchronization_interval = parent_app
[]
(tutorials/pincell_multiphysics/nek.i)Then, we simply set up a Transient executioner with the NekTimeStepper. For postprocessing purposes, we also create two postprocessors to evaluate the average outlet temperature and the maximum fluid temperature. Finally, in order to not overly saturate the screen output, we will only write the Exodus output files and print to the console every 10 Nek time steps.
[Executioner]
type = Transient
[TimeStepper]
type = NekTimeStepper
[]
[]
[Outputs]
exodus = true
interval = 10
[]
(tutorials/pincell_multiphysics/nek.i)Execution
To run the input files,
mpiexec -np 4 cardinal-opt -i openmc.i
This will produce a number of output files,
openmc_out.e
, OpenMC simulation results, mapped to a mesh mirroropenmc_out_bison0.e
, MOOSE heat conduction simulation resultsopenmc_out_bison0_nek0.e
, NekRS simulation results, mapped to a mesh mirrorfluid0.f*
, NekRS output files
Figure 5 shows the temperature (a) imposed in OpenMC (the output of the CellTemperatureAux) auxiliary kernel; (b) the NekRS fluid temperature; and (c) the MOOSE solid temperature.
The OpenMC power distribution is shown in Figure 6. The small variations of sodium density with temperature cause the power distribution to be fairly symmetric in the axial direction.
Finally, Figure 7 shows the fluid and solid temperatures (a) with both phases shown and (b) with just the fluid region highlighted. The very lower power in this demonstration results in fairly small temperature gradients in the fuel, but what is important to note is the coupled solution captures typical conjugate heat transfer temperature distributions in rectangular pincells. You can always extend this tutorial to turbulent conditions and increase the power to display temperature distributions more characteristic of real nuclear systems.
In this tutorial, OpenMC, MOOSE, and NekRS each used their own time step. You may want to try re-running this tutorial using different time step sizes in the OpenMC and MOOSE heat conduction input files, to explore how to sub-cycle these codes together.