1D Slab Benchmark: Neutron Transport Coupled With Thermal Conduction and Material Expansion

This verification problem demonstrates the ability to couple neutron transport with thermal conduction and material expansion. The analytic benchmark slab problem (Griesheimer and Kooreman, 2022) describes a slab where S neutron transport is coupled with thermal conduction, convective boundary conditions, Doppler-broadened cross sections, and nonlinear thermal expansion effects along the heated slab. The verification provides the analytic solutions for steady-state temperature, neutron flux, heating, multiplication factor, and the heated slab length.

While the restriction to S neutron transport would be considered overly simplistic for most radiation transport simulations, the objective here is to verify thermomechanical-OpenMC coupling using analytic solutions for three coupled solution fields (displacements, temperature, and flux). Simplifications of each individual physics included in this model are not imposed by inherent limitations of the moving-mesh capabilities and only aim to allow the study on the interactions between multiphysics with an analytic solution.

This work was published in Eltawila et al. (2026), where more details can be found.

Problem description

A 1D slab (Griesheimer and Kooreman, 2022) with initial length and density has neutrons traveling with directions and with zero incoming neutron flux boundary conditions on both sides. The slab is mechanically constrained and perfectly insulated in the transverse dimensions (i.e., and axes) but free to expand via frictionless sliding along the axis as the temperature of the slab changes. Both ends of the slab are exposed to a convective heat sink at a fixed temperature .

The neutron heating in the slab drives the temperature distribution and causes thermal expansion. This slab elongation feeds back into neutronics and heat conduction by influencing the domain length and material density. A schematic of the problem domain and boundary conditions is shown in Figure 1.

Schematic of the three coupled physics.

Figure 1: Schematic of the three coupled physics.

The benchmark uses a one-group assumption for neutron cross sections. The neutron flux distribution in the slab is governed by quasi-static transport in 1D. The resulting transport equation is

(1)

where we consider the vacuum flux boundary conditions

(2)

where is the neutron flux, is the macroscopic total cross section, and is the combined in-scattering and (quasi-static) fission source term. is defined as

(3)

where is the average number of neutrons released per fission, is the multiplication factor, and and are the constant fractional probabilities of fission and scattering, respectively, per collision. The magnitude of the neutron flux is normalized to the total power of the slab, , by

(4)

where is the energy released per neutron interaction. The Doppler-broadened total microscopic cross section is defined with an inverse square root behavior on temperature as

(5)

and the Doppler-broadened total macroscopic cross section is defined as

(6)

where is the unheated macroscopic total cross section of the slab material. is the conversion between mass density and number density, given by , where is Avogadro’s constant and is the atomic mass of the constituent atoms/molecules in the slab material. is the mass density.

The steady-state temperature distribution in the slab due to neutron heating is governed by the heat equation

(7)

where there are convective boundary conditions at both ends given by

(8)

where is the thermal conductivity. The thermal conductivity is described by a linear function of temperature as

(9)

The differential length at every point along the slab expands upon heating, where temperature is also varying, in proportion to the local thermal strain , such as

(10)

where and are differential length elements in the heated and unheated slab, respectively. The thermal strain at any location is given by

(11)

where the linear expansion coefficient is given by an inverse square root dependence on temperature,

(12)

We use total mass preservation to get material density change from the initial to the final state, starting from:

(13)

to get the relation

(14)

Analytic solutions

The reader is referred to the original benchmark publication (Griesheimer and Kooreman, 2022) and the updates made in Eltawila et al. (2026) for full details on the derivation of these analytical solutions for verification. Here we present the final formulas that we use later for comparison with our models.

The heating distribution is given by

(15)

the neutron flux distribution is given by

(16)

and the temperature distribution is given by

(17)

Note that Eq. (16) is identical to the formula for an ellipse rather than the cosine shape normally seen with constant cross sections in finite slab geometries, as a result of the benchmark physics coupling.

The solution for the eigenvalue, , is given by

(18)

Using manufactured parameters for the benchmark specifications (e.g., , ), the values for the microscopic total cross section and convective heat transfer coefficient are determined such that the temperature distribution also satisfies the manufactured scenario where the flux and temperature distributions have the same shape as given by Eq. (17).

The final length of the heated slab was evaluated using the flux and temperature solutions Eq. (16) and Eq. (17), which were substituted into the mass conservation equation, Eq. (13). Integrating both sides gives the total length of the heated slab implicitly by preserving the total mass from the initial to the final state, resulting in

The final length of the heated slab is then given by

(19)

where is the Gauss hypergeometric function.

To ensure a physically meaningful solution (i.e., the heat flux leaving the slab is consistent with the ), there is an additional constraint on the manufactured temperature distribution:

(20)

Substituting Eq. (16) and Eq. (17) into Eq. (20) gives a minimum condition for power in terms of the other slab parameters, as

(21)

The example set of model parameter values shown in Table 1 are the suggested benchmark parameters (Griesheimer and Kooreman, 2022), resulting in and a heated slab length of cm with eV/s.

Table 1: Example set of model parameters (Griesheimer and Kooreman, 2022).

ParameterValueUnit
g.cm
cm
g/mol
K
eV
eV/s/cm/K
#/cm/s
-
-
eV/s

Problem model

buildconstruction:To run this model you need to build your application with the S OpenMC patch

This model is using S neutron transport to compare with the 1D Analytic benchmark. To replicate the results, you have to build your application with this patch of OpenMC. You can do this simply with:

cd cardinal/contrib/openmc
git remote add s2patch https://github.com/meltawila/openmc_S2transport.git
git fetch s2patch
git cherry-pick d34189845b394895ed210008218c40bc754bce25

Cardinal is used to couple OpenMC with MOOSE via Picard iteration. OpenMC tallies neutron flux, heating, and the multiplication factor. A conceptual depiction of the meshes used as well as the data transfers that occur on each Picard iteration is shown in Figure 2. Three different meshes/geometries are involved. The thermomechanics mesh is a simple 1D mesh of EDGE2 elements; the thermomechanical physics do not require higher dimensional (e.g. 2D or 3D) meshes for this problem. The OpenMC geometry is represented using DAGMC, where cells are bounded by a triangulated surface (TRI3 elements). To facilitate data transfers between OpenMC and MOOSE thermomechanics, there is also an intermediate volumetric data transfer mesh composed of TET4 elements; no physics solves happen on this intermediate mesh. Figure 2 highlights one element (for MOOSE and the Cardinal intermediate data transfer mesh) and one OpenMC cell each in blue, with adjacent portions of the geometry shown in gray for context.

Data transfers between OpenMC and MOOSE and conceptual depiction of the different meshes used.

Figure 2: Data transfers between OpenMC and MOOSE and conceptual depiction of the different meshes used.

We show the input files for a coarse mesh here with more details on results convergence shown in the next section.

The OpenMC input files is as follows:

# This script creates the OpenMC model for the slab benchmark with temperature-dependent
# cross sections and uses DAGMC for geometry representation.

import openmc
import openmc.mgxs as mgxs
import numpy as np
import h5py

T0 = 293 # K
Tmax = 390 # K
Tmin = 290 # K
L0 = 100 # cm
L = 106.47 # cm
rho = 1.2 # g/cc
N_A = 6.022e23 # Avagadro's number
A = 180 # mass number for slab material
num_dens = rho*N_A/A
P = 1.0e22 # eV/s
q = 1e8 # eV
k0 = 1.25e19 # eV/(s-cm-K^2)
phi0 = 2.5e14 # 1/s-cm^2
s = 0.45 # Sigma_s/Sigma_t
f = 1.5 # nu Sigma_f/Sigma_t
nu = f/(1-s)
lam = 0.5*(1+np.sqrt(1+(16*q*q*phi0*phi0)/(P*P)))
Sig_t0 = np.sqrt(P/((lam-1)*k0*L))/(T0) # 1/cm
sig_t0 = Sig_t0/num_dens # cm^2
N = 20
infdim = 0.5

model = openmc.Model()

dagmc_univ = openmc.DAGMCUniverse(filename="slab20v.h5m")
model.geometry = openmc.Geometry(root=dagmc_univ)
cells = dagmc_univ.get_all_cells().values()

slab = openmc.Material(1, "slab")
slab.set_density('macro',1.)
slab.add_macroscopic('slab_xs')

model.materials = openmc.Materials([slab])
model.materials.cross_sections = 'one_group.h5'

groups = mgxs.EnergyGroups(group_edges=[0.0, 20.0e6])
NT = 100
temps = np.linspace(Tmin,Tmax,NT)
xsdata = openmc.XSdata('slab_xs', energy_groups=groups, temperatures=temps, num_delayed_groups=0)
xsdata.order = 0

for T in temps:
    Sig_t = (Sig_t0 * T0) / T
    Sig_s = s*Sig_t
    nu_Sig_f = f*Sig_t
    Sig_f = nu_Sig_f / nu
    Sig_a = Sig_f
    xsdata.set_total(np.array([Sig_t]),temperature=T)
    xsdata.set_scatter_matrix(np.array([[[Sig_s]]]),temperature=T)
    xsdata.set_absorption(np.array([Sig_a]),temperature=T)
    xsdata.set_fission(np.array([Sig_f]),temperature=T)
    xsdata.set_nu_fission(np.array([nu_Sig_f]),temperature=T)
    xsdata.set_kappa_fission(np.array([q*Sig_t]),temperature=T)

one_g_XS_file = openmc.MGXSLibrary(groups)
one_g_XS_file.add_xsdata(xsdata)
one_g_XS_file.export_to_hdf5('one_group.h5')

model.settings = openmc.Settings()
batches = 150
inactive = 50
particles = 50000
model.settings.energy_mode = 'multi-group'
model.settings.batches = batches
model.settings.inactive = inactive
model.settings.particles = particles
model.settings.output = {'tallies': True,'summary':False}

bounds = [-60, -infdim, -infdim, 60, infdim, infdim]
uniform_dist = openmc.stats.Box(bounds[:3], bounds[3:])
mu = openmc.stats.Discrete([0.0],[1.0])
phi = openmc.stats.Discrete([0,np.pi],[0.5,0.5])
bidirectional_x = openmc.stats.PolarAzimuthal(mu=mu,phi=phi)
model.settings.source = openmc.IndependentSource(space=uniform_dist,angle=bidirectional_x)
model.settings.temperature = {'default': (Tmin+Tmax)/2,
                        'method': 'interpolation',
                        'tolerance': 1,
                        'range': (Tmin, Tmax)}

model.export_to_model_xml()
(test/tests/neutronics/expanding_slab/model.py)

The neutronics input file is as follows:

# Cardinal input file for slab benchmark OpenMC model to couple with thermomechanics

P = ${units 1.0e22 eV/s}
eV_to_J = ${units 1.602176634e-19 J/eV}
initial_temperature = ${units 293 K}

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

[GlobalParams<<<{"href": "../../syntax/GlobalParams/index.html"}>>>]
  displacements = 'disp_x disp_y disp_z'
  use_displaced_mesh = true
[]

[Variables<<<{"href": "../../syntax/Variables/index.html"}>>>]
  [disp_x]
  []
  [disp_y]
    initial_condition<<<{"description": "Specifies a constant initial condition for this variable"}>>> = 0.0
  []
  [disp_z]
    initial_condition<<<{"description": "Specifies a constant initial condition for this variable"}>>> = 0.0
  []
[]

[AuxVariables<<<{"href": "../../syntax/AuxVariables/index.html"}>>>]
  [bins]
    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"}>>>]
  [bins]
    type = SkinnedBins<<<{"description": "Bins created by a skinner", "href": "../../source/auxkernels/SkinnedBins.html"}>>>
    variable<<<{"description": "The name of the variable that this object applies to"}>>> = bins
    skinner<<<{"description": "MOAB mesh skinner"}>>> = moab
  []
[]

[ICs<<<{"href": "../../syntax/Cardinal/ICs/index.html"}>>>]
  [temperature]
    type = FunctionIC<<<{"description": "An initial condition that uses a normal function of x, y, z to produce values (and optionally gradients) for a field variable.", "href": "../../source/ics/FunctionIC.html"}>>>
    variable<<<{"description": "The variable this initial condition is supposed to provide values for."}>>> = temperature
    function<<<{"description": "The initial condition function."}>>> = ${initial_temperature}
  []
[]

[Problem<<<{"href": "../../syntax/Problem/index.html"}>>>]
  type = OpenMCCellAverageProblem
  verbose = true
  source_rate_normalization = 'kappa_fission'
  inactive_batches = 150
  batches = 300
  particles = 100000
  power = '${fparse P*eV_to_J}'
  cell_level = 0
  temperature_variables = 'temperature'
  temperature_blocks = '1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20'
  volume_calculation = vol
  skinner = moab
  [Tallies<<<{"href": "../../syntax/Problem/Tallies/index.html"}>>>]
    [kappa_fission]
      type = CellTally<<<{"description": "A class which implements distributed cell tallies.", "href": "../../source/tallies/CellTally.html"}>>>
      estimator<<<{"description": "Type of tally estimator to use in OpenMC"}>>> = collision
      block<<<{"description": "Subdomains for which to add tallies in OpenMC. If not provided, tallies will be applied over the entire domain corresponding to the [Mesh] block."}>>> = '1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20'
      name<<<{"description": "Auxiliary variable name(s) to use for OpenMC tallies. If not specified, defaults to the names of the scores"}>>> = kappa_fission
      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
    []
    [flux]
      type = CellTally<<<{"description": "A class which implements distributed cell tallies.", "href": "../../source/tallies/CellTally.html"}>>>
      estimator<<<{"description": "Type of tally estimator to use in OpenMC"}>>> = collision
      block<<<{"description": "Subdomains for which to add tallies in OpenMC. If not provided, tallies will be applied over the entire domain corresponding to the [Mesh] block."}>>> = '1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20'
      score<<<{"description": "Score(s) to use in the OpenMC tallies. If not specified, defaults to 'kappa_fission'"}>>> = flux
      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
    []
  []
[]

[Executioner<<<{"href": "../../syntax/Executioner/index.html"}>>>]
  type = Transient
  start_time = 0.0
  end_time = 1e5
  dt = 500
[]

[MultiApps<<<{"href": "../../syntax/MultiApps/index.html"}>>>]
  [solid]
    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."}>>> = 'solid.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
  []
[]

[Transfers<<<{"href": "../../syntax/Transfers/index.html"}>>>]
  [kappa_fission_to_solid]
    type = MultiAppGeneralFieldShapeEvaluationTransfer<<<{"description": "Transfers field data at the MultiApp position using the finite element shape functions from the origin application.", "href": "../../source/transfers/MultiAppGeneralFieldShapeEvaluationTransfer.html"}>>>
    to_multi_app<<<{"description": "The name of the MultiApp to transfer the data to"}>>> = solid
    variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = kappa_fission
    source_variable<<<{"description": "The variable to transfer from."}>>> = kappa_fission
    from_postprocessors_to_be_preserved<<<{"description": "The name of the Postprocessor in the from-app to evaluate an adjusting factor."}>>> = heat_source
    to_postprocessors_to_be_preserved<<<{"description": "The name of the Postprocessor in the to-app to evaluate an adjusting factor."}>>> = source_integral
    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
    displaced_source_mesh<<<{"description": "Whether or not to use the displaced mesh for the source mesh."}>>> = true
    displaced_target_mesh<<<{"description": "Whether or not to use the displaced mesh for the target mesh."}>>> = true
  []
  [temperature_from_solid]
    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"}>>> = solid
    variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = temperature
    source_variable<<<{"description": "The variable to transfer from."}>>> = temperature_praux
    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
    displaced_source_mesh<<<{"description": "Whether or not to use the displaced mesh for the source mesh."}>>> = true
    displaced_target_mesh<<<{"description": "Whether or not to use the displaced mesh for the target mesh."}>>> = true
  []
  [dispx]
    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"}>>>
    source_variable<<<{"description": "The variable to transfer from."}>>> = disp_praux
    variable<<<{"description": "The auxiliary variable to store the transferred values in."}>>> = disp_x
    from_multi_app<<<{"description": "The name of the MultiApp to receive data from"}>>> = solid
    displaced_source_mesh<<<{"description": "Whether or not to use the displaced mesh for the source mesh."}>>> = true
    displaced_target_mesh<<<{"description": "Whether or not to use the displaced mesh for the target mesh."}>>> = true
  []
[]

[Outputs<<<{"href": "../../syntax/Outputs/index.html"}>>>]
  exodus<<<{"description": "Output the results using the default settings for Exodus output."}>>> = false
  csv<<<{"description": "Output the scalar variable and postprocessors to a *.csv file using the default CSV output."}>>> = true
[]

[Postprocessors<<<{"href": "../../syntax/Postprocessors/index.html"}>>>]
  [heat_source]
    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
    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."}>>> = 'transfer initial timestep_end'
  []
  [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'
  []
[]

[VectorPostprocessors<<<{"href": "../../syntax/VectorPostprocessors/index.html"}>>>]
  [temperature]
    type = ElementValueSampler<<<{"description": "Samples values of variables on elements.", "href": "../../source/vectorpostprocessors/ElementValueSampler.html"}>>>
    variable<<<{"description": "The names of the variables that this VectorPostprocessor operates on"}>>> = 'temperature'
    sort_by<<<{"description": "What to sort the samples by. Options include 'x', 'y', 'z', 'id', and the name of any of the sampled quantities (which each create a vector of the same name)."}>>> = x
    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."}>>> = 'FINAL'
  []
[]

[UserObjects<<<{"href": "../../syntax/UserObjects/index.html"}>>>]
  [vol]
    type = OpenMCVolumeCalculation<<<{"description": "Stochastic volume calculation of the OpenMC cells which map to MOOSE", "href": "../../source/userobjects/OpenMCVolumeCalculation.html"}>>>
    n_samples<<<{"description": "Number of samples to use for the stochastic volume calculation"}>>> = 200000
  []
  [moab]
    type = MoabSkinner<<<{"description": "Re-generate the OpenMC geometry on-the-fly according to changes in the mesh geometry and/or contours in temperature and density", "href": "../../source/userobjects/MoabSkinner.html"}>>>
    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'
    verbose<<<{"description": "Whether to print diagnostic information"}>>> = true
    temperature<<<{"description": "Temperature variable by which to bin elements"}>>> = temperature
    n_temperature_bins<<<{"description": "Number of temperature bins"}>>> = 1.0
    temperature_min<<<{"description": "Lower bound of temperature bins"}>>> = 0.0
    temperature_max<<<{"description": "Upper bound of temperature bins"}>>> = 1000
    build_graveyard<<<{"description": "Whether to build a graveyard around the geometry"}>>> = true
    graveyard_scale_inner<<<{"description": "Multiplier on mesh bounding box to form inner graveyard surface"}>>> = 1.5
    graveyard_scale_outer<<<{"description": "Multiplier on mesh bounding box to form outer graveyard surface"}>>> = 1.6
  []
[]
(test/tests/neutronics/expanding_slab/openmc.i)

The thermomechanics input file is as follows:

# MOOSE Thermomechanics model input file for the slab benchmark problem

T0 = ${units 293 K}
L0 = ${units 100 cm}
L = ${units 106.47 cm}
P = ${units 1.0e22 eV/s}
q = ${units 1e8 eV}
k0 = ${units 1.25e19 eV/s/cm/K^2} # k(T) = k0 T(x)
phi0 = ${units 2.5e14 1/s/cm^2} # (flux at the origin)
eV_to_J = ${units 1.602176634e-19 J/eV}
lam = '${fparse 0.5*(1+sqrt(1+(16*q*q*phi0*phi0)/(P*P)))}' # eigenvalue solution
h = '${fparse 1/(sqrt(L*(lam-1)/(k0*P)) - (2*T0)/(P))*eV_to_J }' # W/cm^2-K

E = ${units 1e6 N/m^2}
nu = 0.0
rho = ${units 20 g/cm^3}
c_p = ${units 0.3 J/g/K}

[Mesh<<<{"href": "../../syntax/Mesh/index.html"}>>>]
  [generated]
    type = GeneratedMeshGenerator<<<{"description": "Create a line, square, or cube mesh with uniformly spaced or biased elements.", "href": "../../source/meshgenerators/GeneratedMeshGenerator.html"}>>>
    dim<<<{"description": "The dimension of the mesh to be generated"}>>> = 1
    nx<<<{"description": "Number of elements in the X direction"}>>> = 20
    xmin<<<{"description": "Lower X Coordinate of the generated mesh"}>>> = -50
    xmax<<<{"description": "Upper X Coordinate of the generated mesh"}>>> = 50
  []
  [pin]
    type = ExtraNodesetGenerator<<<{"description": "Creates a new node set and a new boundary made with the nodes the user provides.", "href": "../../source/meshgenerators/ExtraNodesetGenerator.html"}>>>
    input<<<{"description": "The mesh we want to modify"}>>> = generated
    new_boundary<<<{"description": "The names of the boundaries to create"}>>> = pin
    coord<<<{"description": "The nodes with coordinates you want to be in the nodeset. Separate multple coords with ';' (Either this parameter or \"nodes\" must be supplied)."}>>> = '0 0 0'
  []
[]

[Problem<<<{"href": "../../syntax/Problem/index.html"}>>>]
  type = ReferenceResidualProblem
  extra_tag_vectors = 'ref'
  reference_vector = 'ref'
[]

[GlobalParams<<<{"href": "../../syntax/GlobalParams/index.html"}>>>]
  displacements = 'disp_x'
  use_displaced_mesh = true
[]

[Variables<<<{"href": "../../syntax/Variables/index.html"}>>>]
  [temperature]
  []
  [disp_x]
  []
[]

[Physics<<<{"href": "../../syntax/Physics/index.html"}>>>]
  [SolidMechanics<<<{"href": "../../syntax/Physics/SolidMechanics/index.html"}>>>]
    [QuasiStatic<<<{"href": "../../syntax/Physics/SolidMechanics/QuasiStatic/index.html"}>>>]
      [block1]
        strain<<<{"description": "Strain formulation"}>>> = FINITE
        add_variables<<<{"description": "Add the displacement variables"}>>> = true
        eigenstrain_names<<<{"description": "List of eigenstrains to be applied in this strain calculation"}>>> = eigenstrain
        generate_output<<<{"description": "Add scalar quantity output for stress and/or strain"}>>> = 'strain_xx'
        temperature<<<{"description": "The temperature"}>>> = temperature
      []
    []
  []
[]

[ICs<<<{"href": "../../syntax/Cardinal/ICs/index.html"}>>>]
  [temperature]
    type = FunctionIC<<<{"description": "An initial condition that uses a normal function of x, y, z to produce values (and optionally gradients) for a field variable.", "href": "../../source/ics/FunctionIC.html"}>>>
    variable<<<{"description": "The variable this initial condition is supposed to provide values for."}>>> = temperature
    function<<<{"description": "The initial condition function."}>>> = ${T0}
  []
[]

[Kernels<<<{"href": "../../syntax/Kernels/index.html"}>>>]
  [heat_conduction]
    type = HeatConduction<<<{"description": "Diffusive heat conduction term $-\\nabla\\cdot(k\\nabla T)$ of the thermal energy conservation equation", "href": "../../source/kernels/HeatConduction.html"}>>>
    variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = temperature
  []
  [kappa_fission]
    type = CoupledForce<<<{"description": "Implements a source term proportional to the value of a coupled variable. Weak form: $(\\psi_i, -\\sigma v)$.", "href": "../../source/kernels/CoupledForce.html"}>>>
    variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = temperature
    v<<<{"description": "The coupled variable which provides the force"}>>> = kappa_fission
  []
  [dt]
    type = HeatConductionTimeDerivative<<<{"description": "Time derivative term $\\rho c_p \\frac{\\partial T}{\\partial t}$ of the thermal energy conservation equation.", "href": "../../source/kernels/HeatConductionTimeDerivative.html"}>>>
    variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = temperature
  []
[]

[AuxVariables<<<{"href": "../../syntax/AuxVariables/index.html"}>>>]
  [kappa_fission]
    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
    initial_condition<<<{"description": "Specifies a constant initial condition for this variable"}>>> = '${fparse P*eV_to_J/L0 }'
  []
  [temperature_praux]
    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
  []
  [disp_praux]
  []
[]

[AuxKernels<<<{"href": "../../syntax/AuxKernels/index.html"}>>>]
  [temperature_projection]
    type = ProjectionAux<<<{"description": "Returns the specified variable as an auxiliary variable with a projection of the source variable. If they are the same type, this amounts to a simple copy.", "href": "../../source/auxkernels/ProjectionAux.html"}>>>
    variable<<<{"description": "The name of the variable that this object applies to"}>>> = temperature_praux
    v<<<{"description": "Variable to take the value of."}>>> = temperature
  []
  [disp_projection]
    type = ProjectionAux<<<{"description": "Returns the specified variable as an auxiliary variable with a projection of the source variable. If they are the same type, this amounts to a simple copy.", "href": "../../source/auxkernels/ProjectionAux.html"}>>>
    variable<<<{"description": "The name of the variable that this object applies to"}>>> = disp_praux
    v<<<{"description": "Variable to take the value of."}>>> = disp_x
  []
[]

[Materials<<<{"href": "../../syntax/Materials/index.html"}>>>]
  [thermal_parameters]
    type = HeatConductionMaterial<<<{"description": "General-purpose material model for heat conduction", "href": "../../source/materials/HeatConductionMaterial.html"}>>>
    temperature<<<{"description": "Coupled Temperature"}>>> = temperature
    thermal_conductivity_temperature_function<<<{"description": "Thermal conductivity as a function of temperature."}>>> = conductivity
    specific_heat<<<{"description": "The specific heat value"}>>> = ${c_p}
  []
  [density]
    type = ParsedMaterial<<<{"description": "Parsed expression Material.", "href": "../../source/materials/ParsedMaterial.html"}>>>
    property_name<<<{"description": "Name of the parsed material property"}>>> = density
    expression<<<{"description": "Parsed function (see FParser) expression for the parsed material"}>>> = ${rho}
  []
  [elasticity_tensor]
    type = ComputeIsotropicElasticityTensor<<<{"description": "Compute a constant isotropic elasticity tensor.", "href": "../../source/materials/ComputeIsotropicElasticityTensor.html"}>>>
    youngs_modulus<<<{"description": "Young's modulus of the material."}>>> = ${E}
    poissons_ratio<<<{"description": "Poisson's ratio for the material."}>>> = ${nu}
  []
  [stress]
    type = ComputeFiniteStrainElasticStress<<<{"description": "Compute stress using elasticity for finite strains", "href": "../../source/materials/ComputeFiniteStrainElasticStress.html"}>>>
  []
  [thermal_expansion_strain]
    type = ComputeMeanThermalExpansionFunctionEigenstrain<<<{"description": "Computes eigenstrain due to thermal expansion using a function that describes the mean thermal expansion as a function of temperature", "href": "../../source/materials/ComputeMeanThermalExpansionFunctionEigenstrain.html"}>>>
    stress_free_temperature<<<{"description": "Reference temperature at which there is no thermal expansion for thermal eigenstrain calculation"}>>> = ${T0}
    thermal_expansion_function_reference_temperature<<<{"description": "Reference temperature for thermal_exansion_function (IMPORTANT: this is different in general from the stress_free_temperature)"}>>> = ${T0}
    thermal_expansion_function<<<{"description": "Function describing the mean thermal expansion as a function of temperature"}>>> = cte_func_mean
    temperature<<<{"description": "Coupled temperature"}>>> = temperature
    eigenstrain_name<<<{"description": "Material property name for the eigenstrain tensor computed by this model. IMPORTANT: The name of this property must also be provided to the strain calculator."}>>> = eigenstrain
  []
[]

[BCs<<<{"href": "../../syntax/BCs/index.html"}>>>]
  [sides_convective_BC]
    type = ConvectiveFluxFunction<<<{"description": "Determines boundary value by fluid heat transfer coefficient and far-field temperature", "href": "../../source/bcs/ConvectiveFluxFunction.html"}>>>
    T_infinity<<<{"description": "Function describing far-field temperature"}>>> = ${T0}
    variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = temperature
    boundary<<<{"description": "The list of boundary IDs from the mesh where this object applies"}>>> = 'left right'
    coefficient<<<{"description": "Function describing heat transfer coefficient"}>>> = ${h}
  []
  [pin_x]
    type = DirichletBC<<<{"description": "Imposes the essential boundary condition $u=g$, where $g$ is a constant, controllable value.", "href": "../../source/bcs/DirichletBC.html"}>>>
    variable<<<{"description": "The name of the variable that this residual object operates on"}>>> = disp_x
    boundary<<<{"description": "The list of boundary IDs from the mesh where this object applies"}>>> = 'pin'
    value<<<{"description": "Value of the BC"}>>> = 0.0
  []
[]

[Functions<<<{"href": "../../syntax/Functions/index.html"}>>>]
  [conductivity]
    type = ParsedFunction<<<{"description": "Function created by parsing a string", "href": "../../source/functions/MooseParsedFunction.html"}>>>
    expression<<<{"description": "The user defined function."}>>> = '${fparse k0*eV_to_J} * t'
  []
  [cte_func_mean]
    type = ParsedFunction<<<{"description": "Function created by parsing a string", "href": "../../source/functions/MooseParsedFunction.html"}>>>
    expression<<<{"description": "The user defined function."}>>> = '1 / (2*sqrt(${T0}*t))'
  []
[]

[Executioner<<<{"href": "../../syntax/Executioner/index.html"}>>>]
  type = Transient
  solve_type = 'PJFNK'
  verbose = true
  nl_abs_tol = 1e-9
  nl_rel_tol = 1e-9
  petsc_options = '-ksp_snes_ew'
  petsc_options_iname = '-pc_type -pc_factor_mat_solver_package'
  petsc_options_value = 'lu       superlu_dist'
  snesmf_reuse_base = false
  line_search = 'none'
  nl_max_its = 100
  [TimeStepper<<<{"href": "../../syntax/Executioner/TimeStepper/index.html"}>>>]
    type = IterationAdaptiveDT
    dt = 1e-1
    optimal_iterations = 10
    growth_factor = 1.25
    cutback_factor = 0.8
  []
[]

[Preconditioning<<<{"href": "../../syntax/Preconditioning/index.html"}>>>]
  [smp]
    type = SMP<<<{"description": "Single matrix preconditioner (SMP) builds a preconditioner using user defined off-diagonal parts of the Jacobian.", "href": "../../source/preconditioners/SingleMatrixPreconditioner.html"}>>>
    full<<<{"description": "Set to true if you want the full set of couplings between variables simply for convenience so you don't have to set every off_diag_row and off_diag_column combination."}>>> = true
  []
[]

[Outputs<<<{"href": "../../syntax/Outputs/index.html"}>>>]
  exodus<<<{"description": "Output the results using the default settings for Exodus output."}>>> = false
  csv<<<{"description": "Output the scalar variable and postprocessors to a *.csv file using the default CSV output."}>>> = true
[]

[Postprocessors<<<{"href": "../../syntax/Postprocessors/index.html"}>>>]
  [source_integral]
    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
    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."}>>> = 'transfer initial timestep_end'
  []
  [strain_xx]
    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"}>>> = strain_xx
  []
  [total_length]
    type = FunctionElementIntegral<<<{"description": "Integrates a function over elements", "href": "../../source/postprocessors/FunctionElementIntegral.html"}>>>
    function<<<{"description": "Name of function to integrate"}>>> = 1
    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 LINEAR TIMESTEP_END'
  []
[]
(test/tests/neutronics/expanding_slab/solid.i)

It is important to note that incremental (hypoelastic) formulations of finite strain behavior inherently introduce inaccuracies arising from the time integration error. This limitation is inherent to this formulation and therefore exists in the MOOSE implementation and the implementations of other major commercial codes. The magnitude of these errors grows with increasing strain, so this typically becomes a practical issue only for very large strains, for which hyperelastic models are more appropriate (Belytschko et al., 2003).

The main sources of error in this coupled model are: (i) the statistical error from use of finite particles in OpenMC; (ii) the user-input cross-section data, which is defined with 1 K T spacings used in all cases while taking the nearest temperature point cross section; and (iii) discretization error from a finite spatial mesh in both OpenMC (temperature feedback resolution) and MOOSE thermomechanics. Additional sources of error are present, but anticipated to be negligible due to the use of fine settings for these simulation parameters: (i) finite Picard iterations, and (ii) nonzero nonlinear tolerances in the MOOSE thermomechanics model.

Results

We see the heating result in Figure 3 compared against the analytic solution, as well as flux and temperature solutions in Figure 4 and Figure 5. Figures are only included for eV/s and on a fine mesh where the number of OpenMC and mesh elements is , since all results have a similar shape. Both the heating and flux tally everywhere agree with the analytic solution within 3.

Heating result from OpenMC for N = 100 mesh elements compared against the analytic solution.

Figure 3: Heating result from OpenMC for N = 100 mesh elements compared against the analytic solution.

Flux result from OpenMC for N = 100 mesh elements compared against the analytic solution.

Figure 4: Flux result from OpenMC for N = 100 mesh elements compared against the analytic solution.

Temperature result from MOOSE for N = 100 mesh elements compared against the analytic solution.

Figure 5: Temperature result from MOOSE for N = 100 mesh elements compared against the analytic solution.

Table 2 shows results convergence with mesh refinement.

Table 2: Results and corresponding errors with different mesh sizes (Eltawila et al., 2026).

ResolutionHeated length (cm)Error (μm)Error (pcm)
Analytic solution106.470.29557
5106.3298-14020.29608 ± 0.0000151 ± 1
10106.4382-3180.29619 ± 0.0000162 ± 1
20106.4671-290.29557 ± 0.000010 ± 1
50106.4730300.29536 ± 0.00001-21 ± 1
100106.4745450.29549 ± 0.00001-8 ± 1
200106.4748480.29556 ± 0.00001-1 ± 1

In Figure 6 and Figure 7, convergence of flux and temperature relative L norms, defined as

and

where is the analytic flux solution, is the Cardinal flux solution, is the analytic temperature solution, and is the Cardinal temperature solution.

Convergence of flux relative L$^2$ norm.

Figure 6: Convergence of flux relative L norm.

Convergence of temperature relative L$^2$ norm.

Figure 7: Convergence of temperature relative L norm.

A first-order convergence of the temperature is achieved with mesh refinement, while the flux starts to converge following a first-order trend then stagnates where other error components dominate as discussed earlier. The stochastic noise error influence is highlighted by keeping the number of particles constant for each line, where we see further convergence when there is less stochastic noise.

We hypothesize that the observed first-order convergence arises from the use of cell tallies in OpenMC, where heating results are tallied as constant monomials. This may be limiting the convergence rate of the coupled solve.

More details and discussion can be found in Eltawila et al. (2026).

References

  1. Ted Belytschko, Wing Kam Liu, and Brian Moran. Nonlinear Finite Elements for Continua and Structures. John Wiley & Sons, West Sussex, England, 2003.[Export]
  2. Mahmoud Eltawila, Pierre-Clément A. Simon, Guillaume L. Giudicelli, Helen Brooks, Nicholas Wozniak, and April J. Novak. Thermomechanics coupling to Monte Carlo particle transport on unstructured mesh geometries using Cardinal. Annals of Nuclear Energy, 229:112068, 2026. URL: https://www.sciencedirect.com/science/article/pii/S0306454925008850, doi:https://doi.org/10.1016/j.anucene.2025.112068.[Export]
  3. D. P. Griesheimer and G. Kooreman. Analytical benchmark solution for 1-D neutron transport coupled with thermal conduction and material expansion. In Proceedings of PHYSOR, 697–706. ANS, 07 2022.[Export]