Postprocessing/Extracting the NekRS solution
In this tutorial, you will learn how to:
Postprocess a NekRS simulation (both "live" runs as well as from field files)
Extract the NekRS solution into MooseVariables for use with other MOOSE objects
Many of the features covered in this tutorial have already been touched upon and used in the preceding NekRS tutorials. The purpose of this dedicated tutorial is to explain more specific use cases or complex combinations of postprocessing/extraction features. We will use a variety of input cases, and will not focus too much on the physics setup - just the postprocessing and data extraction features.
Viewing the NekRS Mesh
NekRS uses a custom mesh format (with the .re2 extension). This file cannot natively be viewed in visualization software such as Paraview. Standalone NekRS users have to run at least one CFD time step, and then can only visualize the output file. This can be tedious if you need to achieve a viable CFD solve just to visualize a mesh.
Cardinal provides a way to visualize the NekRS CFD mesh. Simply set exact = true for NekRSMesh and then run in --mesh-only mode. For example, you can create a small Cardinal input file,
[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."}>>> = 'brick'
[]
[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
type = NekRSMesh
volume = true
exact = true
[](test/tests/nek_mesh/exact/exact_volume.i)And then run in --mesh-only mode.
cd test/tests/nek_mesh/exact
cardinal-opt -i exact.i --mesh-only
Loading a NekRS Time History into Exodus
To access this tutorial,
cd cardinal/tutorials/load_from_exodus
For applications with "one-way" coupling of NekRS to MOOSE, you may wish to use a time history of the NekRS solution as a boundary condition/source term in another MOOSE application. For instance, for thermal striping applications, it is often a reasonable approximation to solve a NekRS CFD simulation as a standalone case, and then apply a time history of NekRS's wall temperature as a boundary condition to a solid mechanics solve. Cardinal allows you to write the NekRS solution to an Exodus file that can then be loaded to provide a time history of the CFD solution to another application. In this example, we will use the following Cardinal input file:
[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
type = NekRSMesh
volume = true
order = SECOND
[]
[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."}>>> = 'ethier'
[FieldTransfers<<<{"href": "../syntax/Problem/FieldTransfers/index.html"}>>>]
[temperature]
type = NekFieldVariable<<<{"description": "Reads/writes volumetric field data between NekRS and MOOSE."}>>>
direction<<<{"description": "Direction in which to send data"}>>> = from_nek
[]
[]
[]
[Executioner<<<{"href": "../syntax/Executioner/index.html"}>>>]
type = Transient
[TimeStepper<<<{"href": "../syntax/Executioner/TimeStepper/index.html"}>>>]
type = NekTimeStepper
[]
[]
[Outputs<<<{"href": "../syntax/Outputs/index.html"}>>>]
exodus<<<{"description": "Output the results using the default settings for Exodus output."}>>> = true
[](tutorials/load_from_exodus/nek.i)We incidate that we want to output the NekRS temperature onto the mesh by using a NekFieldVariable. Then, we simply use a NekRSProblem and NekTimeStepper to run the NekRS CFD calculation through the Cardinal wrapper. You can run this example with
mpiexec -np 4 cardinal-opt -i nek.i
which will create an output file named nek_out.e, which contains the time history of the NekRS temperature solution on the volume mesh mirror.
In order to load the entire time history of the Nek solution, we need a separate input file that will essentailly act as the surrogate for performing the Nek solution (by instead loading from the Exodus file dumped earlier). First, we use the dumped output file directly as the mesh. Because this run won't actually solve any physics (either with NekRS or with MOOSE). we next need to turn the solve off.
[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
type = FileMesh
file = nek_out.e
[]
[Problem<<<{"href": "../syntax/Problem/index.html"}>>>]
type = FEProblem
solve = false
[](tutorials/load_from_exodus/load_nek.i)Then, to load the solution from the nek_out.e, we use a SolutionUserObject. This user object will read the temp variable from the provided mesh file (which we set to our output file from the NekRS run). We omit the timestep parameter that is sometimes provided to the SolutionUserObject so that this user object will interpolate in time based on the time stepping specified in the [Executioner] block.
This user object only loads the Exodus file into a user object - to then get that solution into an AuxVariable appropriate for transferring to another application or using in other MOOSE objects, we simply convert the user object to an auxiliary variable using a SolutionAux. This will load the temperature variable from the SolutionUserObject and place it into the new variable we have named nek_temp.
[UserObjects<<<{"href": "../syntax/UserObjects/index.html"}>>>]
[load_nek_solution]
type = SolutionUserObject<<<{"description": "Reads a variable from a mesh in one simulation to another", "href": "../source/userobjects/SolutionUserObject.html"}>>>
mesh<<<{"description": "The name of the mesh file (must be xda/xdr or exodusII file)."}>>> = nek_out.e
system_variables<<<{"description": "The name of the nodal and elemental variables from the file you want to use for values"}>>> = 'temperature'
[]
[]
[AuxVariables<<<{"href": "../syntax/AuxVariables/index.html"}>>>]
[nek_temp]
[]
[]
[AuxKernels<<<{"href": "../syntax/AuxKernels/index.html"}>>>]
[nek_temp]
type = SolutionAux<<<{"description": "Creates fields by using information from a SolutionUserObject.", "href": "../source/auxkernels/SolutionAux.html"}>>>
solution<<<{"description": "The name of the SolutionUserObject"}>>> = load_nek_solution
variable<<<{"description": "The name of the variable that this object applies to"}>>> = nek_temp
from_variable<<<{"description": "The name of the variable to extract from the file"}>>> = temperature
[]
[](tutorials/load_from_exodus/load_nek.i)Finally, we "run" this application by specifying a Transient executioner. The time stepping scheme we specify here just indicates at which time step the data in the nek_out.e file should be interpolated to. For instance, if you ran NekRS with a time step of 1e-3 seconds, but only want to couple NekRS's temperature to a solid mechanics solve on a resolution of 1e-2 seconds, then simply set the time step size in this file to dt = 1e-2. Finally, we specify a Exodus output in this file, which you can use to see that the temperature from nek_out.e was correctly loaded (nek_temp in load_nek_out.e matches temperature in nek_out.e).
[Executioner<<<{"href": "../syntax/Executioner/index.html"}>>>]
type = Transient
dt = 2e-3
num_steps = 10
[]
[Outputs<<<{"href": "../syntax/Outputs/index.html"}>>>]
exodus<<<{"description": "Output the results using the default settings for Exodus output."}>>> = true
[](tutorials/load_from_exodus/load_nek.i)Binned Spatial Postprocessors
To access this tutorial,
cd cardinal/tutorials/subchannel
Cardinal contains features for postprocessing the NekRS solution in spatial "bins" using user objects. Available user objects include:
NekBinnedPlaneIntegral: compute plane integrals over regions of space
NekBinnedPlaneAverage: compute plane averages over regions of space
NekBinnedSideIntegral: compute integrals over sidesets with subdivisions in space
NekBinnedSideAverage: compute averages over sidesets with subdivisions in space
NekBinnedVolumeIntegral: compute volume integrals over regions of space
NekBinnedVolumeAverage: compute volume averages over regions of space
These user objects can be used for operations such as:
Averaging the solution over for axisymmetric geometries
Extracting homogenized solutions, such as for feeding volume-averaged quantities to a 3-D Pronghorn model
Representing the solution in a different discretization form, such as a finite volume discretization of a subchannel geometry
An example of averaging over axisymmetric geometries was provided in Tutorial 1. Here, we will demonstrate averaging the NekRS solution in a fuel bundle geometry according to a subchannel discretization.
The model consists of a 7-pin Sodium Fast Reactor (SFR) fuel bundle; the mesh is shown in Figure 1. Our goal is to obtain subchannel-averaged and gap-averaged temperatures and velocities on a mesh that reflects a subchannel discretization. Note that the NekRS mesh does not have elements that align with the usual subchannel boundaries; therefore we require the ability to map the NekRS quadrature points to "bins" that represent our different regions of interest (subchannels and subchannel gaps).
 fuel bundle; lines are shown connecting [!ac](GLL) points](../media/sfr_nek_mesh.png)
Figure 1: NekRS mesh for 7-pin SFR fuel bundle; lines are shown connecting GLL points
The NekRS input files use typical settings; the only setting of note is that for simplicity and ease of illustration, we turn the actual physics solve off by setting solver = none for the velocity and temperature in the sfr_7pin.par file.
[GENERAL]
stopAt = numSteps
numSteps = 1
dt = 5.0e-4
timeStepper = tombo2
writeControl = steps
writeInterval = 25
dealiasing = yes
polynomialOrder = 7
[VELOCITY]
solver = none
[PRESSURE]
[TEMPERATURE]
solver = none
(tutorials/subchannel/sfr_7pin.par)Then, we specify dummy velocity, pressure, and temperature initial conditions, just for the sake of demonstrating the subchannel averaging methods.
#include "udf.hpp"
void UDF_LoadKernels(occa::properties & kernelInfo)
{
}
void UDF_Setup(nrs_t *nrs)
{
// set initial conditions for the velocity, temperature, and pressure. Because
// we turn off the solves, we're just doing postprocessing of whatever we set
// for the initial conditions
mesh_t * 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)
{
dfloat x = mesh->x[n];
dfloat y = mesh->y[n];
dfloat z = mesh->z[n];
dfloat r = std::sqrt(x*x + y*y);
dfloat theta;
if (x > 0 && y >= 0)
theta = std::atan(y / x);
if (x < 0 && y >= 0)
theta = M_PI - std::atan(std::abs(y / x));
if (x < 0 && y < 0)
theta = std::atan(y / x) + M_PI;
if (x > 0 && y < 0)
theta = 2 * M_PI - std::atan(std::abs(y / x));
dfloat Vr = 0.0;
dfloat Vt = 0.1 + 10.0 * r;
nrs->U[n + 0 * nrs->fieldOffset] = Vr * std::cos(theta) - Vt * std::sin(theta);
nrs->U[n + 1 * nrs->fieldOffset] = Vr * std::sin(theta) + Vt * std::cos(theta);
nrs->U[n + 2 * nrs->fieldOffset] = 0.0;
nrs->P[n] = x+y+z;
nrs->cds->S[n + 0 * nrs->cds->fieldOffset[0]] = x+y+3*z;
}
}
(tutorials/subchannel/sfr_7pin.udf)For instance, the temperature in the NekRS field files will remain fixed at the given function initial condition of
(1)
while the velocity is set to a swirl velocity with an angular component that increases with and zero radial component.
We will run this NekRS case with a thin wrapper input file, shown below.
[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
type = NekRSMesh
volume = true
[]
[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."}>>> = 'sfr_7pin'
[FieldTransfers<<<{"href": "../syntax/Problem/FieldTransfers/index.html"}>>>]
[temperature]
type = NekFieldVariable<<<{"description": "Reads/writes volumetric field data between NekRS and MOOSE."}>>>
direction<<<{"description": "Direction in which to send data"}>>> = from_nek
[]
[velocity_x]
type = NekFieldVariable<<<{"description": "Reads/writes volumetric field data between NekRS and MOOSE."}>>>
direction<<<{"description": "Direction in which to send data"}>>> = from_nek
[]
[velocity_y]
type = NekFieldVariable<<<{"description": "Reads/writes volumetric field data between NekRS and MOOSE."}>>>
direction<<<{"description": "Direction in which to send data"}>>> = from_nek
[]
[velocity_z]
type = NekFieldVariable<<<{"description": "Reads/writes volumetric field data between NekRS and MOOSE."}>>>
direction<<<{"description": "Direction in which to send data"}>>> = from_nek
field<<<{"description": "NekRS field variable to read/write; defaults to the name of the object"}>>> = velocity_z
[]
[]
[]
[UserObjects<<<{"href": "../syntax/UserObjects/index.html"}>>>]
[subchannel_binning]
type = HexagonalSubchannelBin<<<{"description": "Creates a unique spatial bin for each subchannel in a hexagonal lattice", "href": "../source/userobjects/HexagonalSubchannelBin.html"}>>>
bundle_pitch<<<{"description": "Bundle pitch, or flat-to-flat distance across bundle"}>>> = 0.02583914354890463
pin_pitch<<<{"description": "Pin pitch, or distance between pin centers"}>>> = 0.0089656996
pin_diameter<<<{"description": "Pin outer diameter"}>>> = 7.646e-3
n_rings<<<{"description": "Number of pin rings, including the centermost pin as a 'ring'"}>>> = 2
[]
[subchannel_gap_binning]
type = HexagonalSubchannelGapBin<<<{"description": "Creates a unique spatial bin for each subchannel in a hexagonal lattice", "href": "../source/userobjects/HexagonalSubchannelGapBin.html"}>>>
bundle_pitch<<<{"description": "Bundle pitch, or flat-to-flat distance across bundle"}>>> = 0.02583914354890463
pin_pitch<<<{"description": "Pin pitch, or distance between pin centers"}>>> = 0.0089656996
pin_diameter<<<{"description": "Pin outer diameter"}>>> = 7.646e-3
n_rings<<<{"description": "Number of pin rings, including the centermost pin as a 'ring'"}>>> = 2
[]
[axial_binning]
type = LayeredBin<<<{"description": "Creates a unique spatial bin for layers in a specified direction", "href": "../source/userobjects/LayeredBin.html"}>>>
direction<<<{"description": "The direction of the layers (x, y, or z)"}>>> = z
num_layers<<<{"description": "The number of layers between the bounding box of the domain"}>>> = 7
[]
[average_T]
type = NekBinnedVolumeAverage<<<{"description": "Compute the spatially-binned volume average of a field over the NekRS mesh", "href": "../source/userobjects/NekBinnedVolumeAverage.html"}>>>
bins<<<{"description": "Userobjects providing a spatial bin given a point"}>>> = 'subchannel_binning axial_binning'
field<<<{"description": "Field to apply this object to"}>>> = temperature
map_space_by_qp<<<{"description": "Whether to map the NekRS spatial domain to a bin according to the element centroids (true) or quadrature point locations (false)."}>>> = true
[]
[average_T_gaps]
type = NekBinnedPlaneAverage<<<{"description": "Compute the spatially-binned side average of a field over the NekRS mesh", "href": "../source/userobjects/NekBinnedPlaneAverage.html"}>>>
bins<<<{"description": "Userobjects providing a spatial bin given a point"}>>> = 'subchannel_gap_binning axial_binning'
field<<<{"description": "Field to apply this object to"}>>> = temperature
map_space_by_qp<<<{"description": "Whether to map the NekRS spatial domain to a bin according to the element centroids (true) or quadrature point locations (false)."}>>> = true
gap_thickness<<<{"description": "thickness of gap region for which to accept contributions to the side integral over the gap, expressed in the same units as the mesh."}>>> = ${fparse 0.05 * 7.646e-3}
[]
[avg_gap_velocity]
type = NekBinnedPlaneAverage<<<{"description": "Compute the spatially-binned side average of a field over the NekRS mesh", "href": "../source/userobjects/NekBinnedPlaneAverage.html"}>>>
bins<<<{"description": "Userobjects providing a spatial bin given a point"}>>> = 'subchannel_gap_binning axial_binning'
field<<<{"description": "Field to apply this object to"}>>> = velocity_component
velocity_component<<<{"description": "Direction in which to evaluate velocity when 'field = velocity_component.' Options: user (you then need to specify a direction with 'velocity_direction'); normal"}>>> = normal
map_space_by_qp<<<{"description": "Whether to map the NekRS spatial domain to a bin according to the element centroids (true) or quadrature point locations (false)."}>>> = true
gap_thickness<<<{"description": "thickness of gap region for which to accept contributions to the side integral over the gap, expressed in the same units as the mesh."}>>> = ${fparse 0.05 * 7.646e-3}
[]
[]
[AuxVariables<<<{"href": "../syntax/AuxVariables/index.html"}>>>]
# These are just for visualizing the average velocity component with Glyphs in paraview;
# the result of the 'avg_gap_velocity' user object will be represented as a vector "uo_" with 3 components
[uo_x]
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
[]
[uo_y]
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
[]
[uo_z]
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"}>>>]
[uo_x]
type = NekSpatialBinComponentAux<<<{"description": "Component-wise (x, y, z) spatial value returned from a Nek user object", "href": "../source/auxkernels/NekSpatialBinComponentAux.html"}>>>
variable<<<{"description": "The name of the variable that this object applies to"}>>> = uo_x
user_object<<<{"description": "The UserObject UserObject to get values from. Note that the UserObject _must_ implement the spatialValue() virtual function!"}>>> = avg_gap_velocity
component<<<{"description": "Component of user object"}>>> = 0
[]
[uo_y]
type = NekSpatialBinComponentAux<<<{"description": "Component-wise (x, y, z) spatial value returned from a Nek user object", "href": "../source/auxkernels/NekSpatialBinComponentAux.html"}>>>
variable<<<{"description": "The name of the variable that this object applies to"}>>> = uo_y
user_object<<<{"description": "The UserObject UserObject to get values from. Note that the UserObject _must_ implement the spatialValue() virtual function!"}>>> = avg_gap_velocity
component<<<{"description": "Component of user object"}>>> = 1
[]
[uo_z]
type = NekSpatialBinComponentAux<<<{"description": "Component-wise (x, y, z) spatial value returned from a Nek user object", "href": "../source/auxkernels/NekSpatialBinComponentAux.html"}>>>
variable<<<{"description": "The name of the variable that this object applies to"}>>> = uo_z
user_object<<<{"description": "The UserObject UserObject to get values from. Note that the UserObject _must_ implement the spatialValue() virtual function!"}>>> = avg_gap_velocity
component<<<{"description": "Component of user object"}>>> = 2
[]
[]
[MultiApps<<<{"href": "../syntax/MultiApps/index.html"}>>>]
[subchannel]
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."}>>> = 'subchannel.i'
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
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
[]
[subchannel_gap]
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."}>>> = 'subchannel_gap.i'
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
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
[]
[]
[Transfers<<<{"href": "../syntax/Transfers/index.html"}>>>]
[uo_to_sub]
type = MultiAppGeneralFieldUserObjectTransfer<<<{"description": "Transfers user object spatial evaluations from an origin app onto a variable in the target application.", "href": "../source/transfers/MultiAppGeneralFieldUserObjectTransfer.html"}>>>
source_user_object<<<{"description": "The UserObject you want to transfer values from. It must implement the SpatialValue() class routine"}>>> = average_T
to_multi_app<<<{"description": "The name of the MultiApp to transfer the data to"}>>> = subchannel
variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = average_T
[]
[uo_to_sub2]
type = MultiAppGeneralFieldUserObjectTransfer<<<{"description": "Transfers user object spatial evaluations from an origin app onto a variable in the target application.", "href": "../source/transfers/MultiAppGeneralFieldUserObjectTransfer.html"}>>>
source_user_object<<<{"description": "The UserObject you want to transfer values from. It must implement the SpatialValue() class routine"}>>> = average_T_gaps
to_multi_app<<<{"description": "The name of the MultiApp to transfer the data to"}>>> = subchannel_gap
variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = average_T
[]
[uo1_to_sub]
type = MultiAppGeneralFieldUserObjectTransfer<<<{"description": "Transfers user object spatial evaluations from an origin app onto a variable in the target application.", "href": "../source/transfers/MultiAppGeneralFieldUserObjectTransfer.html"}>>>
source_user_object<<<{"description": "The UserObject you want to transfer values from. It must implement the SpatialValue() class routine"}>>> = avg_gap_velocity
to_multi_app<<<{"description": "The name of the MultiApp to transfer the data to"}>>> = subchannel_gap
variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = avg_gap_velocity
[]
[uox_to_sub]
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"}>>> = subchannel_gap
source_variable<<<{"description": "The variable to transfer from."}>>> = uo_x
variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = uo_x
[]
[uoy_to_sub]
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"}>>> = subchannel_gap
source_variable<<<{"description": "The variable to transfer from."}>>> = uo_y
variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = uo_y
[]
[]
[VectorPostprocessors<<<{"href": "../syntax/VectorPostprocessors/index.html"}>>>]
[avg_T]
type = SpatialUserObjectVectorPostprocessor<<<{"description": "Outputs the values of a spatial user object in the order of the specified spatial points", "href": "../source/vectorpostprocessors/SpatialUserObjectVectorPostprocessor.html"}>>>
userobject<<<{"description": "The userobject whose values are to be reported"}>>> = average_T
[]
[avg_T_gaps]
type = SpatialUserObjectVectorPostprocessor<<<{"description": "Outputs the values of a spatial user object in the order of the specified spatial points", "href": "../source/vectorpostprocessors/SpatialUserObjectVectorPostprocessor.html"}>>>
userobject<<<{"description": "The userobject whose values are to be reported"}>>> = average_T_gaps
[]
[avg_v_gaps]
type = SpatialUserObjectVectorPostprocessor<<<{"description": "Outputs the values of a spatial user object in the order of the specified spatial points", "href": "../source/vectorpostprocessors/SpatialUserObjectVectorPostprocessor.html"}>>>
userobject<<<{"description": "The userobject whose values are to be reported"}>>> = avg_gap_velocity
[]
[]
[Executioner<<<{"href": "../syntax/Executioner/index.html"}>>>]
type = Transient
[TimeStepper<<<{"href": "../syntax/Executioner/TimeStepper/index.html"}>>>]
type = NekTimeStepper
[]
[]
[Outputs<<<{"href": "../syntax/Outputs/index.html"}>>>]
csv<<<{"description": "Output the scalar variable and postprocessors to a *.csv file using the default CSV output."}>>> = true
[](tutorials/subchannel/nek.i)Of note are the user objects, multiapps, and transfers. In this file, we will compute three different postprocessing operations:
Average temperature over subchannel volumes**: We will use a user object to conduct a binned spatial average of the NekRS temperature. We first form these bins as the product of unique indices for each subchannel with unique indices for each axial layer. The HexagonalSubchannelBin object assigns a unique ID for each subchannel, for which we must provide various geometric parameters describing the fuel bundle. The LayeredBin object assigns a unique ID for equal-size layers in a given direction; here, we specify 7 axial layers. Finally, we will compute a volume average of the NekRS temperature with the NekBinnedVolumeAverage object, which constructs bins as the outer product of individual bin distributions. Because the NekRS mesh in Figure 1 has 6 elements in the axial direction, but we have specified that we want to average across 7 axial layers, we set
map_space_by_qp = true. This setting will essentially apply a delta function of unity in each layer when forming the integral. Settingmap_space_by_qp = falseinstead maps each NekRS element to a bin according to its centroid (which for this particular example would result in some of the axial bins not mapping to any elements, since the NekRS mesh is coarser than the specified number of bins).Average temperature over subchannel gaps**: We form the bins as the product of unique indices for each subchannel gap with unique indices for each axial layers. The HexagonalSubchannelGapBin object assigns a unique ID for each subchannel gap ID in a hexagonal fuel bundle. The LayeredBin object then assigns a unique ID for equal-size layers in the axial direction; for simplicity, we reuse the same axial binning distribution that we selected for the volume averages. Finally, we compute the planar averages of the NekRS temperature with the NekBinnedPlaneAverage object, which takes an arbitrary number of individual bin distributions (with the only requirement being that one and only one of these distribution is a "side" distribution, which for this example is the
HexagonalSubchannelGapBin).Average the normal velocity over subchannel gaps**: We reuse the same previous bins, but set
field = velocity_componentfor a NekBinnedPlaneAverage.
The SpatialUserObjectVectorPostprocessor outputs the bin values for the user object to the CSV format specified in the [Outputs] block. These values are ordered in the same manner that the bins are defined.
Recall that the NekRS mesh does not respect the subchannel discretization, or the desired number of axial averaging layers. Therefore, if we want to properly visualize the results of the user object, we transfer the user object to two different sub-applications which serve the sole purpose of user object visualization. We use two different sub-applications because one sub-app will use a mesh that perfectly represents the volumes of the channels, while the second sub-app will use a mesh that perfectly represents the gaps between the channels.
The application that will be used to view the volume results on the channels is shown below. This sub-application constructs a subchannel mesh with the HexagonalSubchannelMesh to receive the user object into a variable named average_T and the three components of the gap normal velocity as uo_x, uo_y, and uo_z. We set solve = false so that no physics solve occurs.
[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
type = HexagonalSubchannelMesh
bundle_pitch = 0.02583914354890463
pin_pitch = 0.0089656996
pin_diameter = 7.646e-3
n_rings = 2
n_axial = 7
theta_res = 8
height = 0.008
[]
[Problem<<<{"href": "../syntax/Problem/index.html"}>>>]
solve = false
type = FEProblem
[]
[AuxVariables<<<{"href": "../syntax/AuxVariables/index.html"}>>>]
[average_T]
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
[]
[]
[Executioner<<<{"href": "../syntax/Executioner/index.html"}>>>]
type = Transient
[]
[Outputs<<<{"href": "../syntax/Outputs/index.html"}>>>]
exodus<<<{"description": "Output the results using the default settings for Exodus output."}>>> = true
[](tutorials/subchannel/subchannel.i)The application that will be used to view the gap results is shown below. This sub-application constructs a subchannel mesh with the HexagonalSubchannelGapMesh to receive the user object into a variable named average_T. We set solve = false so that no physics solve occurs.
[Mesh<<<{"href": "../syntax/Mesh/index.html"}>>>]
type = HexagonalSubchannelGapMesh
bundle_pitch = 0.02583914354890463
pin_pitch = 0.0089656996
pin_diameter = 7.646e-3
n_rings = 2
n_axial = 7
height = 0.008
[]
[Problem<<<{"href": "../syntax/Problem/index.html"}>>>]
solve = false
type = FEProblem
[]
[AuxVariables<<<{"href": "../syntax/AuxVariables/index.html"}>>>]
[average_T]
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
[]
[avg_gap_velocity]
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
[]
# These are just for visualizing the average velocity component with Glyphs in paraview
[uo_x]
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
[]
[uo_y]
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
[]
[uo_z]
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
[]
[]
[Executioner<<<{"href": "../syntax/Executioner/index.html"}>>>]
type = Transient
[]
[Outputs<<<{"href": "../syntax/Outputs/index.html"}>>>]
exodus<<<{"description": "Output the results using the default settings for Exodus output."}>>> = true
[](tutorials/subchannel/subchannel_gap.i)This input can be run as,
mpiexec -np 4 cardinal-opt -i nek.i
which will run with 4 MPI ranks. Because we don't specify any output block in the Nek-wrapping input file itself, the only output files are:
nek_out_subchannel0.e, which shows the channel-averaged temperature on a subchannel discretizationnek_out_subchannel_gap0.e, which shows the gap-averaged quantities on the gaps in a subchannel discretization
The temperature initial condition that we set from NekRS is shown in Figure 2, along with the result of the subchannel volume and gap averaging. Note that because we transferred the user object to a sub-application with a mesh that respects our axial bins, we perfectly represent the channel-wise average temperatures.

Figure 2: NekRS temperature (left), subchannel-averaged temperature (middle), and gap-averaged temperature (right) computed using Cardinal user objects
By applying the "glyph" filter in Paraview, we can visualize the gap-normal velocity with arrows. Otherwise, the result of the avg_gap_velocity user object is a magnitude, which would need to be combined with the gap unit normals (defined in the HexagonalSubchannelGapBin documentation) to visualize the direction.

Figure 3: NekRS temperature (left), subchannel-averaged temperature (middle), and gap-averaged temperature (right) computed using Cardinal user objects
For other geometries, other binning strategies can be used. Available user objects for specifying spatial bins are:
For example, you can compute an average over a number of planes perpendicular to the axis, split into two layers, by combining the two bin user objects shown below.

Figure 4: Example use case for arbitrary combinations of bin objects for spatial postprocessing of NekRS solutions